diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index d1600346c..bd2130571 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -54,7 +54,7 @@ fib.s Different output kinds accumulate in `.scriptc/`; rebuilding a kind updates its file. -The IR and LLVM source outputs require only Node. On supported macOS, Linux, and Windows hosts, assembly and object outputs use the matching native helper installed with scriptc and do not invoke an external compiler, archiver, linker, or SDK. The object is a relocatable program object with undefined scr_* runtime symbols and a required scr_runtime_abi_v7 marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by --print=native-link-info. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, main entry, installed precompiled runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See Native Program Objects for complete C-driver and direct-linker examples. --emit=exe is the default. LLVM builds emit the program object through the helper and link precompiled runtime objects; runtime development with AddressSanitizer compiles an instrumented C runtime. +The IR and LLVM source outputs require only Node. On supported macOS, Linux, and Windows hosts, assembly and object outputs use the matching native helper installed with scriptc and do not invoke an external compiler, archiver, linker, or SDK. The object is a relocatable program object with undefined scr_* runtime symbols and a required scr_runtime_abi_v8 marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by --print=native-link-info. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, main entry, installed precompiled runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See Native Program Objects for complete C-driver and direct-linker examples. --emit=exe is the default. LLVM builds emit the program object through the helper and link precompiled runtime objects; runtime development with AddressSanitizer compiles an instrumented C runtime. ## scriptc run diff --git a/docs/content/docs/how-it-works.mdx b/docs/content/docs/how-it-works.mdx index c0a4587dd..0d59767de 100644 --- a/docs/content/docs/how-it-works.mdx +++ b/docs/content/docs/how-it-works.mdx @@ -68,7 +68,7 @@ Without `-o`, artifacts are written to `.scriptc/` next to the input. Different ### External linking -Program objects define `main` and leave selected `scr_*` runtime functions undefined. A strong reference to `scr_runtime_abi_v7` checks runtime compatibility at link time. +Program objects define `main` and leave selected `scr_*` runtime functions undefined. A strong reference to `scr_runtime_abi_v8` checks runtime compatibility at link time. `--print=native-link-info` emits the program object and a JSON recipe containing the runtime pack and link order for external builds. This object ABI is experimental and requires an exact runtime version. See [Native Program Objects](/docs/native-objects) for examples. diff --git a/docs/content/docs/native-objects.mdx b/docs/content/docs/native-objects.mdx index 54f1efacb..7315493c5 100644 --- a/docs/content/docs/native-objects.mdx +++ b/docs/content/docs/native-objects.mdx @@ -8,7 +8,7 @@ title: "Native Program Objects" The external object ABI is **experimental**. Its `scr_*` function and data surface may change before 1.0, so consumers must use the exact runtime-pack version reported by the same compiler installation. This is stricter than semver compatibility. -The object intentionally leaves its selected runtime symbols undefined. It also holds a strong reference to `scr_runtime_abi_v7`, which the matching runtime defines. Linking an object against a runtime with another ABI marker fails at link time with the missing versioned symbol; it cannot become a latent runtime incompatibility. +The object intentionally leaves its selected runtime symbols undefined. It also holds a strong reference to `scr_runtime_abi_v8`, which the matching runtime defines. Linking an object against a runtime with another ABI marker fails at link time with the missing versioned symbol; it cannot become a latent runtime incompatibility. ## Machine-readable link information diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 4b0f2a929..cbbe881f9 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.15.0", "nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119", - "artifactVersion": "32b04265083c89d00ea3", + "artifactVersion": "ae934a592466ef1e67c1", "rowCount": 3662 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 643394fd5..1391cf23b 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -410,9 +410,9 @@ "apiEntries": 461, "static": { "supported": 0, - "partial": 3, + "partial": 8, "refused": 0, - "not-implemented": 458, + "not-implemented": 453, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -1130,9 +1130,9 @@ "apiEntries": 18, "static": { "supported": 0, - "partial": 3, + "partial": 7, "refused": 0, - "not-implemented": 15, + "not-implemented": 11, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -22256,9 +22256,9 @@ "anchor": "class-systemerror", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -22279,9 +22279,9 @@ "anchor": "class-systemerror", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -22302,9 +22302,9 @@ "anchor": "class-systemerror", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -22371,9 +22371,9 @@ "anchor": "class-systemerror", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -22417,9 +22417,9 @@ "anchor": "class-systemerror", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -80894,9 +80894,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -81062,9 +81062,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -81174,9 +81174,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" }, "dynamic": { "status": "not-implemented", @@ -81258,9 +81258,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" }, "dynamic": { "status": "partial", diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 8bd3a5d37..e73d033de 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -8,8 +8,8 @@ "tiers": { "static": { "replace-refusal": 42, - "verify-gap": 2427, - "audit-partial": 737, + "verify-gap": 2418, + "audit-partial": 746, "classify": 2, "implement": 80 }, @@ -18651,12 +18651,16 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:errors", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:errors.code", + "tests": [ + "tests/corpus/caught-system-error-fields.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/project-config.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -18679,12 +18683,16 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:errors", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:errors.dest", + "tests": [ + "tests/corpus/caught-system-error-fields.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/project-config.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -18707,12 +18715,16 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:errors", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:errors.errno", + "tests": [ + "tests/corpus/caught-system-error-fields.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/project-config.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -18791,12 +18803,16 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:errors", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:errors.path", + "tests": [ + "tests/corpus/caught-system-error-fields.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/project-config.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -18847,12 +18863,16 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:errors", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:errors.syscall", + "tests": [ + "tests/corpus/caught-system-error-fields.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/project-config.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -90173,12 +90193,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:tty", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-dedicated:readStream.isTTY", + "tests": [ + "tests/corpus/1448-stream-columns.ts", + "tests/corpus/2992-stream-rows.ts", + "tests/corpus/terminal-stream-properties.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/terminal-geometry.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -90373,12 +90399,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:tty", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-dedicated:writeStream.getWindowSize", + "tests": [ + "tests/corpus/1448-stream-columns.ts", + "tests/corpus/2992-stream-rows.ts", + "tests/corpus/terminal-stream-properties.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/terminal-geometry.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -90480,6 +90512,8 @@ "tests": [ "tests/corpus/1448-stream-columns.ts", "tests/corpus/2992-stream-rows.ts", + "tests/corpus/terminal-stream-properties.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/terminal-geometry.test.ts" ] }, @@ -90509,12 +90543,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:tty", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-dedicated:writeStream.isTTY", + "tests": [ + "tests/corpus/1448-stream-columns.ts", + "tests/corpus/2992-stream-rows.ts", + "tests/corpus/terminal-stream-properties.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/terminal-geometry.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -90550,6 +90590,8 @@ "tests": [ "tests/corpus/1448-stream-columns.ts", "tests/corpus/2992-stream-rows.ts", + "tests/corpus/terminal-stream-properties.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/terminal-geometry.test.ts" ] }, @@ -90612,12 +90654,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:tty", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-dedicated:tty.isatty", + "tests": [ + "tests/corpus/1448-stream-columns.ts", + "tests/corpus/2992-stream-rows.ts", + "tests/corpus/terminal-stream-properties.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/terminal-geometry.test.ts" + ] }, "dynamic": { "status": "partial", diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 62073fb98..faa5855d9 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -459,9 +459,9 @@ "apiEntries": 461, "static": { "supported": 0, - "partial": 3, + "partial": 8, "refused": 0, - "not-implemented": 458, + "not-implemented": 453, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -1251,9 +1251,9 @@ "apiEntries": 18, "static": { "supported": 0, - "partial": 3, + "partial": 7, "refused": 0, - "not-implemented": 15, + "not-implemented": 11, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -37699,8 +37699,11 @@ "status": "partial", "evidence": "derived:descendants", "tests": [ + "tests/corpus/caught-system-error-fields.ts", "tests/corpus/error-stack-frames.ts", - "tests/corpus/error-stack-limit.ts" + "tests/corpus/error-stack-limit.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/project-config.test.ts" ] }, "dynamic": { @@ -38037,8 +38040,13 @@ "anchor": "class-systemerror", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-feature:errors.code", + "tests": [ + "tests/corpus/caught-system-error-fields.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/project-config.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -38059,8 +38067,13 @@ "anchor": "class-systemerror", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-feature:errors.dest", + "tests": [ + "tests/corpus/caught-system-error-fields.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/project-config.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -38081,8 +38094,13 @@ "anchor": "class-systemerror", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-feature:errors.errno", + "tests": [ + "tests/corpus/caught-system-error-fields.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/project-config.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -38147,8 +38165,13 @@ "anchor": "class-systemerror", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-feature:errors.path", + "tests": [ + "tests/corpus/caught-system-error-fields.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/project-config.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -38191,8 +38214,13 @@ "anchor": "class-systemerror", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-feature:errors.syscall", + "tests": [ + "tests/corpus/caught-system-error-fields.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/project-config.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -107797,6 +107825,8 @@ "tests": [ "tests/corpus/1448-stream-columns.ts", "tests/corpus/2992-stream-rows.ts", + "tests/corpus/terminal-stream-properties.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/terminal-geometry.test.ts" ] }, @@ -107911,8 +107941,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tty" + "status": "partial", + "evidence": "compiler-dedicated:readStream.isTTY", + "tests": [ + "tests/corpus/1448-stream-columns.ts", + "tests/corpus/2992-stream-rows.ts", + "tests/corpus/terminal-stream-properties.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/terminal-geometry.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -108076,8 +108113,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tty" + "status": "partial", + "evidence": "compiler-dedicated:writeStream.getWindowSize", + "tests": [ + "tests/corpus/1448-stream-columns.ts", + "tests/corpus/2992-stream-rows.ts", + "tests/corpus/terminal-stream-properties.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/terminal-geometry.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -108162,6 +108206,8 @@ "tests": [ "tests/corpus/1448-stream-columns.ts", "tests/corpus/2992-stream-rows.ts", + "tests/corpus/terminal-stream-properties.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/terminal-geometry.test.ts" ] }, @@ -108189,8 +108235,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tty" + "status": "partial", + "evidence": "compiler-dedicated:writeStream.isTTY", + "tests": [ + "tests/corpus/1448-stream-columns.ts", + "tests/corpus/2992-stream-rows.ts", + "tests/corpus/terminal-stream-properties.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/terminal-geometry.test.ts" + ] }, "dynamic": { "status": "not-implemented", @@ -108221,6 +108274,8 @@ "tests": [ "tests/corpus/1448-stream-columns.ts", "tests/corpus/2992-stream-rows.ts", + "tests/corpus/terminal-stream-properties.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/terminal-geometry.test.ts" ] }, @@ -108275,8 +108330,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tty" + "status": "partial", + "evidence": "compiler-dedicated:tty.isatty", + "tests": [ + "tests/corpus/1448-stream-columns.ts", + "tests/corpus/2992-stream-rows.ts", + "tests/corpus/terminal-stream-properties.ts", + "tests/fixtures/node-types/terminal-error-fields.ts", + "tests/harness/terminal-geometry.test.ts" + ] }, "dynamic": { "status": "partial", diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index da1c5128e..80cdeb2f9 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -96,7 +96,7 @@ "node-builtin.test": ["tests/harness/node-test.test.ts"], "node-builtin.timers": ["tests/corpus/1804-timers-module.ts", "tests/corpus/2093-timers-promises.ts"], "node-builtin.tls": ["tests/corpus/2331-tls-server-options-typed.ts", "tests/corpus/2557-tls-ca-store.ts"], - "node-builtin.tty": ["tests/corpus/1448-stream-columns.ts"], + "node-builtin.tty": ["tests/corpus/1448-stream-columns.ts", "tests/corpus/terminal-stream-properties.ts", "tests/harness/terminal-geometry.test.ts"], "node-builtin.url": ["tests/corpus/1794-searchparams-url-live.ts"], "node-builtin.url.pathToFileURL": ["tests/corpus/path-to-file-url-options.ts", "tests/corpus/node24-file-url-options.ts", "tests/corpus/node24-file-url-errors.js"], "node-builtin.url.fileURLToPath": ["tests/corpus/file-url-percent-decoding.ts", "tests/corpus/node24-file-url-options.ts", "tests/corpus/node24-file-url-errors.js"], @@ -118,6 +118,7 @@ "stdlib.abort": ["tests/harness/fetch-conformance.test.ts"] }, "features": [ + { "chapter": "errors", "parent": "Class: `SystemError`", "symbols": ["code", "errno", "syscall", "path", "dest"], "status": "partial", "evidence": ["tests/corpus/caught-system-error-fields.ts", "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/project-config.test.ts"] }, {"chapter": "worker_threads", "symbols": ["Worker"], "status": "partial", "evidence": ["tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-environment.ts", "tests/corpus/native-worker-roots/main.ts"]}, {"chapter": "worker_threads", "symbols": ["MessagePort", "parentPort", "worker_threads.workerData", "isMainThread", "threadId"], "status": "partial", "evidence": ["tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-port-lifetime.ts", "tests/corpus/native-worker-references.ts"]}, {"chapter": "worker_threads", "symbols": ["worker.postMessage", "port.postMessage"], "status": "partial", "evidence": ["tests/corpus/native-worker-message-graphs.ts", "tests/corpus/native-worker-shared-views.ts", "tests/corpus/native-worker-clone-errors.ts", "tests/corpus/native-worker-delayed-messages.ts"]}, @@ -233,7 +234,7 @@ ], "dedicated": [ { "symbols": ["assert.doesNotThrow", "assert.doesNotReject"], "status": "partial", "evidence": ["tests/corpus/node24-no-error-assertions.ts", "tests/corpus/node24-no-error-assertions.cjs", "tests/corpus/node24-no-error-assertions-coercion.js", "tests/fixtures/node-types/no-error-assertions.ts", "tests/harness/project-config.test.ts"] }, - { "symbols": ["writeStream.rows", "writeStream.columns"], "status": "partial", "evidence": ["tests/corpus/1448-stream-columns.ts", "tests/corpus/2992-stream-rows.ts", "tests/harness/terminal-geometry.test.ts"] }, + { "symbols": ["writeStream.rows", "writeStream.columns", "writeStream.isTTY", "writeStream.getWindowSize", "readStream.isTTY", "tty.isatty"], "status": "partial", "evidence": ["tests/corpus/1448-stream-columns.ts", "tests/corpus/2992-stream-rows.ts", "tests/corpus/terminal-stream-properties.ts", "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/terminal-geometry.test.ts"] }, { "symbols": ["console.log", "console.info", "console.debug", "console.error", "console.warn"], "status": "partial", "evidence": ["tests/corpus/1460-console-error-warn.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2631-create-require/main.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts"] }, { "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts"] }, { "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.timingSafeEqual", "crypto.randomFillSync", "crypto.randomInt", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/harness/npm-static.test.ts"] }, diff --git a/packages/cli/README.md b/packages/cli/README.md index 44fd5917d..d15bdcb24 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -34,7 +34,7 @@ Builds use a bounded persistent cache. Unchanged source can reuse the validated - `scriptc run ` — compile and run - `scriptc coverage ` — what compiles statically, and why the rest doesn't -`scriptc build app.ts --emit=ir|llvm|asm|obj` selects serialized typed IR, textual LLVM IR, target assembly, or a relocatable program object as the one primary artifact. `--emit=exe` is the default. Assembly/object emission uses the matching helper on supported macOS, Linux, and Windows hosts (and produces WASI artifacts when that target is selected). Objects retain undefined `scr_*` runtime references plus the `scr_runtime_abi_v7` compatibility marker; they are not library archives. External consumption is experimental and requires the exact matching runtime. `scriptc build app.ts --print=native-link-info -o app.o` prints the versioned JSON target/runtime/link recipe without performing a link. `--emit=asm|obj --sanitize` is rejected until ASan pipeline parity is available. +`scriptc build app.ts --emit=ir|llvm|asm|obj` selects serialized typed IR, textual LLVM IR, target assembly, or a relocatable program object as the one primary artifact. `--emit=exe` is the default. Assembly/object emission uses the matching helper on supported macOS, Linux, and Windows hosts (and produces WASI artifacts when that target is selected). Objects retain undefined `scr_*` runtime references plus the `scr_runtime_abi_v8` compatibility marker; they are not library archives. External consumption is experimental and requires the exact matching runtime. `scriptc build app.ts --print=native-link-info -o app.o` prints the versioned JSON target/runtime/link recipe without performing a link. `--emit=asm|obj --sanitize` is rejected until ASan pipeline parity is available. For embedder-hosted modules that are not installed npm packages, coverage can map an exact bare specifier to a local declaration with repeatable `--external-types ` options. This is analysis-only: the types unblock application measurement, while runtime module uses remain reported as blockers. diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 73e429be8..19b9c4a84 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 7, marker: "scr_runtime_abi_v7" }, + runtime_abi: { version: 8, marker: "scr_runtime_abi_v8" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v7"); + expect(error?.stderr).toContain("scr_runtime_abi_v8"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 41378ff9b..8177c5b90 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -250,24 +250,8 @@ declare var process: { * preserved. The boolean is Node's backpressure signal — these synchronous * writes always return true. Static BufferEncoding arguments and the * completion callback overloads match Node's WritableStream surface. */ - stdout: { - write(data: string | Uint8Array, callback?: (error?: Error | null) => void): boolean; - write( - data: string | Uint8Array, - encoding: BufferEncoding, - callback?: (error?: Error | null) => void, - ): boolean; - readonly isTTY: boolean; - }; - stderr: { - write(data: string | Uint8Array, callback?: (error?: Error | null) => void): boolean; - write( - data: string | Uint8Array, - encoding: BufferEncoding, - callback?: (error?: Error | null) => void, - ): boolean; - readonly isTTY: boolean; - }; + stdout: import("tty").WriteStream; + stderr: import("tty").WriteStream; /* The stdin stream, the piped-input slice: the TTY probe, the * data/end/error events (on and once — a 'data' listener keeps the * event loop alive until EOF, like Node's flowing stdin), destroy() @@ -4203,6 +4187,9 @@ declare module "tty" { callback?: (error?: Error | null) => void, ): boolean; readonly isTTY: boolean; + readonly columns: number; + readonly rows: number; + getWindowSize(): [number, number]; } export function isatty(fd: number): boolean; } diff --git a/packages/compiler/src/backend/cycle-analysis.test.ts b/packages/compiler/src/backend/cycle-analysis.test.ts index 0fe62630c..113b0eb47 100644 --- a/packages/compiler/src/backend/cycle-analysis.test.ts +++ b/packages/compiler/src/backend/cycle-analysis.test.ts @@ -19,7 +19,7 @@ import { computeTraced as llvmTraced } from "./llvm/shapes.js"; const loc = { file: "cycles.ts", start: 0, end: 1 }; function module(): IrModule { return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", functions: [{ name: "main", locals: [], params: [], returnType: VOID, body: [], loc }], diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index d9029a644..8710759e2 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -9,7 +9,7 @@ const loc = { file: "class-membership.ts", start: 0, end: 1 }; test("leaf layouts keep direct teardown while recursive layouts share the depth budget", () => { const linked = { kind: "object" as const, className: "Linked" }; const mod: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }], @@ -61,7 +61,7 @@ function graph() { { name: "FlippedString", base: "PairString", genericOf: "Flipped" }, ]; const mod: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", functions: [], @@ -95,7 +95,7 @@ test.each(["i32", "i64"])( expect(helper).toContain("label %ordinary"); expect(helper).toContain("or i1"); const ordinary: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", functions: [], diff --git a/packages/compiler/src/backend/llvm/constant-callbacks.test.ts b/packages/compiler/src/backend/llvm/constant-callbacks.test.ts index a49a4e089..f81b592fd 100644 --- a/packages/compiler/src/backend/llvm/constant-callbacks.test.ts +++ b/packages/compiler/src/backend/llvm/constant-callbacks.test.ts @@ -66,7 +66,7 @@ function fixture(): IrModule { loc, }; return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", functions: [predicate, consumer, entry], diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index 1ef04762e..f60eaeabf 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -1,7 +1,9 @@ import { expect, test } from "vitest"; import { BOOL, + DATE_T, F64, + PROCSTREAM_T, STRING, VOID, type IrExpr, @@ -59,7 +61,7 @@ function moduleFor( loc, }; return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", functions: [ @@ -147,6 +149,27 @@ test("shared exceptional exits preserve the function's scalar return ABI", () => expect(blocks[0]).toContain("ret double 0x0000000000000000"); }); +test("exceptional exits use each scalar kind's LLVM return type", () => { + for (const type of [F64, DATE_T, BOOL, PROCSTREAM_T]) { + for (const ownedLocals of [false, true]) { + const module = moduleFor([call(), call(), { kind: "throw", value: literal("failed"), loc }]); + const work = module.functions[1]!; + work.returnType = type; + if (!ownedLocals) { + work.params = []; + work.locals = []; + } + const llvm = emitLlvmModule(module); + const body = /^define internal [^\n]*@sc_f_work\([^]*?^}/m.exec(llvm)?.[0]; + expect(body).toBeDefined(); + expect(body).toContain( + type.kind === "bool" ? "ret i1 false" : "ret double 0x0000000000000000", + ); + expect(body).not.toContain("ret ptr null"); + } + } +}); + const receiverLoc = { file: "read-receiver.ts", start: 0, end: 0 }; const leaf: IrType = { kind: "record", shapeId: "leaf" }; const root: IrType = { kind: "record", shapeId: "root" }; @@ -184,7 +207,7 @@ const text = (obj: IrExpr): IrExpr => ({ function work(expr: IrExpr, parameter: IrType, boxed = false, tdz = false): string { const module: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: receiverLoc.file, entry: "main", records: [ @@ -355,7 +378,7 @@ function sharedFieldModule(prefixes: IrType[][], fieldType: IrType): IrModule { ], })); return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", records, diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 476808e93..329467856 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1600,7 +1600,7 @@ export class LlEmitter { `@scr_error_vts = external ${tl}global [${RUNTIME_ERROR_CLASSES.size} x %ScrVt]`, `declare void @scr_init()`, `declare void @scr_lib_init(i32, ptr)`, - ...(this.mod.workers ? [`declare void @scr_runtime_workers_v7()`] : []), + ...(this.mod.workers ? [`declare void @scr_runtime_workers_v8()`] : []), ...(this.runtimeAbiMarker && this.mod.lib === undefined ? [`declare void @${RUNTIME_ABI_MARKER}()`] : []), @@ -1841,7 +1841,7 @@ export class LlEmitter { `define i32 @${this.mod.workers ? "sc_context_entry" : this.wasi ? "__main_argc_argv" : "main"}(i32 %argc, ptr %argv) ${FN_ATTRS} {`, `entry:`, ...(this.runtimeAbiMarker ? [` call void @${RUNTIME_ABI_MARKER}()`] : []), - ...(this.mod.workers ? [` call void @scr_runtime_workers_v7()`] : []), + ...(this.mod.workers ? [` call void @scr_runtime_workers_v8()`] : []), ` call void @scr_init()`, ...stamps, // Event-surface programs (signal/exit listeners) fill the loop's @@ -3014,13 +3014,13 @@ export class LlEmitter { } else if (this.currentWasiCoro !== null) { terminator = `br label %${this.currentWasiCoro.finalLabel}`; } else { - const t = this.currentReturnType; + const t = this.llType(this.currentReturnType); terminator = - t.kind === "void" + t === "void" ? "ret void" - : t.kind === "f64" || t.kind === "date" + : t === "double" ? `ret double ${f64Lit(0)}` - : t.kind === "bool" + : t === "i1" ? "ret i1 false" : "ret ptr null"; } diff --git a/packages/compiler/src/backend/llvm/expr-primitives.test.ts b/packages/compiler/src/backend/llvm/expr-primitives.test.ts index 878fae3d8..bd570a052 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.test.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.test.ts @@ -17,7 +17,7 @@ function fixture(): IrModule { loc, }); return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "__main", functions: [{ name: "__main", params: [], returnType: VOID, locals: [], body, loc }], diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts index 9f46ac3f3..9741cf216 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts @@ -20,7 +20,7 @@ function sharedGraph(roots: number): IrModule { unionId: `input${index}`, })); return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", records: [ diff --git a/packages/compiler/src/backend/llvm/local-array-reads.test.ts b/packages/compiler/src/backend/llvm/local-array-reads.test.ts index ef6778281..c85ef11e1 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.test.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.test.ts @@ -124,7 +124,7 @@ function fixture(): IrModule { ], }; return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", records: [{ id: "cell", fields: [{ name: "x", type: F64 }] }], diff --git a/packages/compiler/src/backend/llvm/local-union-storage.test.ts b/packages/compiler/src/backend/llvm/local-union-storage.test.ts index 63f88eefa..6118f4653 100644 --- a/packages/compiler/src/backend/llvm/local-union-storage.test.ts +++ b/packages/compiler/src/backend/llvm/local-union-storage.test.ts @@ -81,7 +81,7 @@ function fixture(): IrModule { ], }; return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", unions: [{ id: "optional", arms: [STRING, UNDEFINED_T] }], diff --git a/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts b/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts index dc8d91ff5..927c1247c 100644 --- a/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts +++ b/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts @@ -66,7 +66,7 @@ function fixture(value: IrType = record, key: IrType = F64): IrModule { ], }; return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", records: [{ id: "cell", fields: [{ name: "x", type: F64 }] }], diff --git a/packages/compiler/src/backend/llvm/stack-unions.test.ts b/packages/compiler/src/backend/llvm/stack-unions.test.ts index 1f61b655b..03f8ba157 100644 --- a/packages/compiler/src/backend/llvm/stack-unions.test.ts +++ b/packages/compiler/src/backend/llvm/stack-unions.test.ts @@ -69,7 +69,7 @@ function fixture(arm: IrType = record): IrModule { body: [ret(narrow(ref("value"), arm))], }; return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", records: [{ id: "cell", fields: [{ name: "x", type: F64 }] }], diff --git a/packages/compiler/src/backend/llvm/string-construction.test.ts b/packages/compiler/src/backend/llvm/string-construction.test.ts index 160dd768f..b22b06b21 100644 --- a/packages/compiler/src/backend/llvm/string-construction.test.ts +++ b/packages/compiler/src/backend/llvm/string-construction.test.ts @@ -17,7 +17,7 @@ const concat = (left: IrExpr, right: IrExpr): IrExpr => ({ function module(value: IrExpr): IrModule { return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", functions: [ diff --git a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts index b1d667659..acbf20747 100644 --- a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts +++ b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts @@ -68,7 +68,7 @@ function fn(name: string, names: string[], value: IrExpr): IrFunction { } function mod(...functions: IrFunction[]): IrModule { return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", functions: [ diff --git a/packages/compiler/src/backend/llvm/switch-dispatch.test.ts b/packages/compiler/src/backend/llvm/switch-dispatch.test.ts index f40a02d5a..9e4517b7d 100644 --- a/packages/compiler/src/backend/llvm/switch-dispatch.test.ts +++ b/packages/compiler/src/backend/llvm/switch-dispatch.test.ts @@ -8,7 +8,7 @@ const string = (value: string): IrExpr => ({ kind: "strLit", value, type: STRING function emit(type: IrType, tests: (IrExpr | null)[], pointerBits: 32 | 64 = 64): string { const mod: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", functions: [ diff --git a/packages/compiler/src/backend/may-throw.test.ts b/packages/compiler/src/backend/may-throw.test.ts index 899ed300e..1aecf25c9 100644 --- a/packages/compiler/src/backend/may-throw.test.ts +++ b/packages/compiler/src/backend/may-throw.test.ts @@ -26,7 +26,7 @@ function fn(name: string, body: IrStmt[], locals: IrLocal[] = [local]): IrFuncti return { name, body, locals, params: [], returnType: VOID, loc }; } function moduleWith(...functions: IrFunction[]): IrModule { - return { irVersion: 14, sourceFile: loc.file, entry: "caller", functions }; + return { irVersion: 15, sourceFile: loc.file, entry: "caller", functions }; } const assignment: IrStmt = { kind: "assign", localId: local.id, value, loc }; const expression: IrExpr = { kind: "assignExpr", localId: local.id, value, type: F64, loc }; diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 739a86cb1..a4efac02e 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 7 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v7" as const; +export const RUNTIME_ABI_VERSION = 8 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v8" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 8e8598cad..d071822c2 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 7, marker: "scr_runtime_abi_v7" }, + runtime_abi: { version: 8, marker: "scr_runtime_abi_v8" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 986bb0655..e46b52786 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -112,7 +112,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 7, marker: "scr_runtime_abi_v7" }, + runtime_abi: { version: 8, marker: "scr_runtime_abi_v8" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/backend/target-diagnostics.test.ts b/packages/compiler/src/backend/target-diagnostics.test.ts index c4c25e270..8e6e53ca6 100644 --- a/packages/compiler/src/backend/target-diagnostics.test.ts +++ b/packages/compiler/src/backend/target-diagnostics.test.ts @@ -35,7 +35,7 @@ const fetch: IrExpr = { }; function module(direct: IrExpr[] = []): IrModule { return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "%main", records: [], diff --git a/packages/compiler/src/frontend/lowering/array-values.ts b/packages/compiler/src/frontend/lowering/array-values.ts index a35e11b4b..066ea7140 100644 --- a/packages/compiler/src/frontend/lowering/array-values.ts +++ b/packages/compiler/src/frontend/lowering/array-values.ts @@ -1,3 +1,4 @@ +import { InternalCompilerError } from "../../errors.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { BOOL, @@ -175,3 +176,86 @@ export function arrayValueStore( } return { kind: "arraySet", arr, index, value: lowerer.coerceToExpected(value, elem), loc }; } + +/** An ordinary indexed read: values retain their element type, while + * holes, missing properties, and present undefined yield undefined. */ +export function lowerSafeIndexRead( + lowerer: Lowerer, + arr: IrExpr, + index: IrExpr, + loc: SrcLoc, +): IrExpr | null { + if (arr.type.kind !== "array") throw new InternalCompilerError("indexed read requires an array"); + const elem = arr.type.elem; + if (elem.kind === "void") return null; + const resultT = arrayValueType(lowerer, elem); + const key = `idxOr:${typeKey(elem)}`; + let name = lowerer.arrHofHelpers.get(key); + if (!name) { + name = `%arr.idxOr.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, name); + const arrT = arr.type; + lowerer.liftedFns.push({ + name, + params: [ + { localId: "a.0", name: "a", type: arrT }, + { localId: "i.0", name: "i", type: F64 }, + ], + returnType: resultT, + locals: [ + { id: "a.0", name: "a", type: arrT, mutable: false }, + { id: "i.0", name: "i", type: F64, mutable: false }, + ], + body: [ + { + kind: "return", + value: arrayValueRead( + lowerer, + varRef("a.0", arrT, loc), + varRef("i.0", F64, loc), + elem, + loc, + ), + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: name, args: [arr, index], type: resultT, loc }; +} + +/** Fuse only our own f64-array read helper with immediate ToNumber. + * Reuse its arguments verbatim so the receiver and index still evaluate + * exactly once, in order, before the read. Do not specialize user calls, + * union-element arrays, or optional values stored in locals. */ +export function tryLowerNumericIndexRead( + lowerer: Lowerer, + operand: IrExpr, + loc: SrcLoc, +): IrExpr | null { + if ( + operand.kind !== "call" || + operand.callee !== lowerer.arrHofHelpers.get(`idxOr:${typeKey(F64)}`) + ) + return null; + const [arr, index] = operand.args; + if ( + operand.args.length !== 2 || + arr?.type.kind !== "array" || + arr.type.elem.kind !== "f64" || + index?.type.kind !== "f64" + ) + return null; + // The intrinsic owns the evaluated receiver through index evaluation, + // then borrows it for one slot lookup. No extra helper parameter or + // separate state/getter expressions need to retain the array again. + return { + kind: "arrIntrinsic", + method: "getNumber", + receiver: arr, + args: [index], + type: F64, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts index 6281980bc..e612aa4f4 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -51,19 +51,42 @@ export function lowerErrorCodeProperty( }; } } - if (expr.name.text !== "code" && expr.name.text !== "cause" && expr.name.text !== "stack") + const systemField = ["errno", "syscall", "path", "dest"].includes(expr.name.text); + if ( + !systemField && + expr.name.text !== "code" && + expr.name.text !== "cause" && + expr.name.text !== "stack" + ) return null; // Error-rooted classes only — builtin or user subclass (both embed the // code and cause slots in their layout prefix). let info = lowerer.classes.get(recvT.className) ?? null; while (info && info.base) info = info.base; if (!info || info.def.name !== "%Error") return null; - if (!lowerer.isStdlibMember(expr)) return null; + if ( + !lowerer.isStdlibMember(expr) && + !(recvT.className === "%Error" && lowerer.typeOf(expr.expression).isIntersectionType()) + ) + return null; const rawReceiver = lowerer.lowerExpr(expr.expression); const receiver = rawReceiver.type.kind === "dyn" ? lowerer.coerceInto(expr.expression, rawReceiver, recvT) : rawReceiver; + if (systemField) { + const loc = locOf(expr); + return lowerer.coerceToExpected( + { + kind: "dynKeyGet", + value: lowerer.coerceToExpected(receiver, DYN), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + type: DYN, + loc, + }, + lowerer.withUndefinedArm(expr.name.text === "errno" ? F64 : STRING), + ); + } return { kind: "libCall", fn: diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 5b97ba0c0..6bbaa54da 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -22,7 +22,6 @@ import { arrayOf, canBoxFuncIntoDyn, funcOf, - typeKey, } from "../../../ir/ir.js"; import { lowerBuiltinLoaderValue } from "../lower-builtin-values.js"; import { lowerProcessIpcSend, ipcMessageListener, ipcDisconnectListener } from "./ipc.js"; @@ -30,9 +29,9 @@ import { lowerDiagnosticsSubscriber, lowerTracingArguments } from "./async-conte /** Method calls on first-class process-stream receivers (procStream — * a WritableStream-typed value like prefixStream's `output` param): - * write(data) with one string, dispatched at runtime onto the exact - * stdout/stderr write paths (the fd IS the value). Everything else - * @types/node declares on WritableStream fences member-qualified. + * write(data) with one string and terminal geometry, dispatched onto + * the original stdout/stderr descriptor. Other declared stream members + * retain member-qualified diagnostics. * Null for non-procStream receivers. */ export function lowerProcStreamMethodCall( lowerer: Lowerer, @@ -58,6 +57,22 @@ export function lowerProcStreamMethodCall( }; } } + if (name === "getWindowSize" && call.arguments.length === 0) { + const receiver = lowerer.coerceToExpected(lowerer.lowerExpr(access.expression), DYN); + return lowerer.coerceInto( + call, + { + kind: "dynInvoke", + recv: receiver, + method: name, + calleeName: call.expression.getText(), + args: [], + type: DYN, + loc, + }, + lowerer.irTypeOf(call), + ); + } if (name === "write" && call.arguments.length === 1) { const receiver = lowerer.lowerExpr(access.expression); const data = lowerer.lowerExpr(call.arguments[0]!); @@ -73,25 +88,33 @@ export function lowerProcStreamMethodCall( lowerer.noLowering( `WritableStream.${name}`, call, - "write(data) with one string is the supported stream-value member", + "write(data) with one string and getWindowSize() are the supported stream-value methods", lowerer.checker.getSymbolAtLocation(access.name), ); } -/** `process.stdin/stdout/stderr.isTTY` → isatty(3) on the stream's fd - * (a REAL boolean: Node's non-TTY streams expose `undefined` here — the - * documented divergence; truthiness tests, the actual usage, agree), and - * `process.stdout/stderr.columns/rows` → ioctl(TIOCGWINSZ) on the fd, with - * Node's non-TTY answer intact: the read is `number | undefined` and a - * non-TTY (or ioctl-refusing) stream yields the undefined arm. The - * receiver match sees through parens and as-casts to the SYMBOL — - * `(process.stderr as typeof process.stderr & { columns?: number }) - * .columns` is the wild widening pattern (@types/node declares a plain - * `number`, so honest code casts the undefined possibility back in), and - * the cast changes the expression's TYPE, never the value. columns sites - * whose checker type does NOT admit undefined are fenced with that exact - * fix instead of lowering to a lie. Null for anything else, so the - * property chain keeps trying. */ +/** Standard streams expose optional terminal properties: pipes have no + * isTTY, columns, or rows. The declarations do not prove their presence. */ +export function isOptionalProcessStreamProperty( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): boolean { + if (!["isTTY", "columns", "rows"].includes(expr.name.text)) return false; + let recv = expr.expression; + while (ts.isParenthesizedExpression(recv) || ts.isAsExpression(recv) || ts.isTypeAssertion(recv)) + recv = recv.expression; + if (ts.isPropertyAccessExpression(recv)) { + const stream = lowerer.stdlibGlobalMember(recv, "process"); + if ( + stream === "stdout" || + stream === "stderr" || + (stream === "stdin" && expr.name.text === "isTTY") + ) + return true; + } + return lowerer.mapTypeOf(lowerer.typeOf(expr.expression))?.kind === "procStream"; +} + export function lowerProcessStreamProperty( lowerer: Lowerer, expr: ts.PropertyAccessExpression, @@ -137,6 +160,25 @@ export function lowerProcessStreamProperty( left = left.expression; if (ts.isPropertyAccessExpression(left)) recv = left; } + if ( + lowerer.mapTypeOf(lowerer.typeOf(recv))?.kind === "procStream" && + !( + ts.isPropertyAccessExpression(recv) && + ["stdout", "stderr"].includes(lowerer.stdlibGlobalMember(recv, "process") ?? "") + ) + ) { + const receiver = lowerer.coerceToExpected(lowerer.lowerExpr(expr.expression), DYN); + return lowerer.coerceToExpected( + { + kind: "dynKeyGet", + value: receiver, + key: strLit(member, locOf(expr)), + type: DYN, + loc: locOf(expr), + }, + lowerer.withUndefinedArm(member === "isTTY" ? BOOL : F64), + ); + } if (!ts.isPropertyAccessExpression(recv)) return null; const stream = lowerer.stdlibGlobalMember(recv, "process"); if (stream !== "stdin" && stream !== "stdout" && stream !== "stderr") return null; @@ -148,22 +190,21 @@ export function lowerProcessStreamProperty( loc, }; if (member === "isTTY") { - return { kind: "libCall", fn: "process.isTTY", args: [fd], type: BOOL, loc }; + const type = lowerer.withUndefinedArm(BOOL); + return { + kind: "ternary", + cond: { kind: "libCall", fn: "process.isTTY", args: [fd], type: BOOL, loc }, + then: lowerer.coerceToExpected(boolLit(true, loc), type), + else_: lowerer.coerceToExpected( + { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type, + ), + type, + loc, + }; } - if (stream === "stdin") return null; // no geometry on a ReadStream — generic fences apply - const declared = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (stream === "stdin") return null; const want = lowerer.withUndefinedArm(F64); - // JS files skip the annotation fence: there is no annotation to fix — - // the read IS Node's `number | undefined` and lowers to exactly that - // (commander's `isTTY ? columns : undefined` help-width probes). - if ((!declared || typeKey(declared) !== typeKey(want)) && !isJsSourceFile(expr.getSourceFile())) { - lowerer.noLowering( - `process.${stream}.${member} as a plain number`, - expr, - `on a non-TTY stream Node's .${member} is undefined — type the read to admit it: ` + - `(process.${stream} as typeof process.${stream} & { ${member}?: number }).${member}`, - ); - } return { kind: "libCall", fn: member === "rows" ? "process.rows" : "process.columns", diff --git a/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts b/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts new file mode 100644 index 000000000..b88ba93af --- /dev/null +++ b/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts @@ -0,0 +1,84 @@ +import { varRef } from "../../ir/build.js"; +import { BOOL, CAUGHT, DYN, type IrFunction, type IrType } from "../../ir/ir.js"; +import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; +import type { Lowerer } from "./lowerer.js"; + +/** Exception snapshots retain native objects. Recover their known layouts + * before crossing into unknown, using the same live capsules as ordinary + * class values. The reachable throw census grows with the lowering worklist. */ +export class CaughtValueDispatch { + private readonly thrownClasses = new Set(); + private readonly convertedClasses = new Set(); + private helper: IrFunction | null = null; + + process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean { + const consumers: IrFunction[] = []; + for (const fn of functions) { + if (fn === this.helper) continue; + let consumes = false; + everyStmtList(fn.body, { + stmt: (stmt) => { + if (stmt.kind === "throw" && stmt.value.type.kind === "object") { + const info = lowerer.classes.get(stmt.value.type.className); + if (info && lowerer.inHierarchy(info)) this.thrownClasses.add(info.def.name); + } + return true; + }, + expr: (expr) => { + if (expr.kind === "caughtToDyn") consumes = true; + return true; + }, + }); + if (consumes) consumers.push(fn); + } + if (!this.helper && consumers.length === 0) return false; + let changed = false; + if (!this.helper) { + const loc = consumers[0]!.loc; + const value = varRef("caught", CAUGHT, loc); + this.helper = { + name: "%caught.dynamicValue", + params: [{ localId: "caught", name: "caught", type: CAUGHT }], + locals: [{ id: "caught", name: "caught", type: CAUGHT, mutable: false }], + returnType: DYN, + body: [{ kind: "return", value: { kind: "caughtToDyn", value, type: DYN, loc }, loc }], + loc, + }; + lowerer.liftedFns.push(this.helper); + changed = true; + } + const helper = this.helper; + for (const className of this.thrownClasses) { + if (this.convertedClasses.has(className)) continue; + this.convertedClasses.add(className); + const loc = helper.loc; + const value = varRef("caught", CAUGHT, loc); + const type: IrType = { kind: "object", className }; + helper.body.unshift({ + kind: "if", + cond: { kind: "caughtTest", value, test: "instanceof", className, type: BOOL, loc }, + then: [ + { + kind: "return", + value: lowerer.coerceToExpected({ kind: "caughtNarrow", value, type, loc }, DYN), + loc, + }, + ], + else_: null, + loc, + }); + changed = true; + } + for (const fn of consumers) { + fn.body = transformStmtList(fn.body, { + stmt: (stmt) => stmt, + expr: (expr) => + expr.kind === "caughtToDyn" + ? { kind: "call", callee: helper.name, args: [expr.value], type: DYN, loc: expr.loc } + : expr, + }); + changed = true; + } + return changed; + } +} diff --git a/packages/compiler/src/frontend/lowering/class-callbacks.ts b/packages/compiler/src/frontend/lowering/class-callbacks.ts index 3308371a9..400988f0d 100644 --- a/packages/compiler/src/frontend/lowering/class-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/class-callbacks.ts @@ -10,15 +10,15 @@ import { type IrStmt, type SrcLoc, } from "../../ir/ir.js"; -import { locOf } from "../program.js"; +import { isJsSourceFile, locOf } from "../program.js"; import type { Lowerer } from "./lowerer.js"; import { findMethodOn, type ClassInfo } from "./lower-classes.js"; import { classPropertiesHelper } from "./class-dynamic-dispatch.js"; import { classPrototypeData } from "./class-prototypes.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; -/** Observed method slots and zero-argument notifications can be replaced - * through instance properties or their shared prototype chain. */ +/** Unmodified typed methods retain direct or virtual native dispatch. + * JavaScript notification methods keep their checked property fallback. */ export function isClassCallback(lowerer: Lowerer, info: ClassInfo, name: string): boolean { if (!isClassOwnEnumerableFieldName(name) || name.startsWith("get:") || name.startsWith("set:")) return false; @@ -35,7 +35,10 @@ export function isClassCallback(lowerer: Lowerer, info: ClassInfo, name: string) !!found && !found.sig.abstract && (lowerer.prototypeMethodAccesses.has(name) || - (found.sig.params.length === 0 && found.sig.ret.kind === "void")) + (info.decl !== null && + isJsSourceFile(info.decl.getSourceFile()) && + found.sig.params.length === 0 && + found.sig.ret.kind === "void")) ); } diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index ddd1f9193..5b927e8a7 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -59,6 +59,7 @@ import { isJsSourceFile } from "../program.js"; import * as ts from "../ts7/adapter.js"; import { SYMBOL_T } from "../../ir/ir.js"; import { isClassCallback } from "./class-callbacks.js"; +import { CaughtValueDispatch } from "./caught-value-dispatch.js"; import { refreshDescriptorGuards } from "./class-descriptors.js"; import { ClassConstructionDispatch } from "./class-construction.js"; import { emitterRooted } from "./lower-event-emitter.js"; @@ -499,6 +500,7 @@ export class ClassDynamicDispatch { constructor(private readonly asyncFree = false) {} private readonly generatorDispatch = new GeneratorDynamicDispatch(); + private readonly caughtDispatch = new CaughtValueDispatch(); private readonly constructionDispatch = new ClassConstructionDispatch(); private readonly boxed = new Set(); private readonly boxedConstructors = new Set(); @@ -557,7 +559,8 @@ export class ClassDynamicDispatch { ) fn.body.shift(); } - let changed = refreshErrorMethodDispatch(lowerer, functions); + let changed = this.caughtDispatch.process(lowerer, functions); + changed = refreshErrorMethodDispatch(lowerer, functions) || changed; changed = refreshClassMethodValueSelections(lowerer) || changed; changed = refreshErrorPropertyDispatch(lowerer, functions) || changed; changed = this.generatorDispatch.process(lowerer, functions, this.generated) || changed; diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts new file mode 100644 index 000000000..0ddf65bc3 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -0,0 +1,431 @@ +import * as ts from "../ts7/adapter.js"; +import { literalValues } from "../literal-values.js"; +import { isJsSourceFile } from "../program.js"; +import type { Lowerer } from "./lowerer.js"; + +/** Census observable slots before choosing native calls. Names are shared + * conservatively across classes: aliases cannot hide a named replacement. */ +export function collectClassMethodMutations( + lowerer: Lowerer, + files: readonly ts.SourceFile[], +): void { + const methods = new Set(); + const arrowFields = new Set(); + const receiverFields = new Set(); + const computedWrites: { + key: ts.Expression | undefined; + receiver: ts.Expression | undefined; + site: ts.Node; + }[] = []; + const reflection: ts.PropertyAccessExpression[] = []; + const globals: ts.Identifier[] = []; + const jsAssignments: (ts.PropertyAccessExpression | ts.ElementAccessExpression)[] = []; + const computedMethods: ts.Expression[] = []; + let unknownKey: ts.Node | undefined; + let numericKey: ts.Node | undefined; + let unknownOwnField = false; + // JavaScript method inference and dynamic class factories keep their + // existing dispatch contract. The native-call proof applies to typed + // declarations, while writes in either source language can invalidate it. + for (const file of files) { + if (isJsSourceFile(file)) continue; + ts.walkPreorder(file, (node) => { + if ( + !ts.isMethodDeclaration(node) || + (!ts.isClassDeclaration(node.parent) && !ts.isClassExpression(node.parent)) + ) + return; + if ( + ts.isIdentifier(node.name) || + ts.isStringLiteralLike(node.name) || + ts.isNumericLiteral(node.name) + ) + methods.add(node.name.text); + else if (ts.isComputedPropertyName(node.name)) computedMethods.push(node.name.expression); + }); + } + lowerer.checker.prefetchClassCollection(computedMethods, []); + for (const key of computedMethods) + for (const name of literalValues(lowerer.typeOf(key)) ?? []) methods.add(String(name)); + const markName = (name: string, site: ts.Node, arrow = false): void => { + if (name === "prototype" || name === "__proto__") unknownKey = site; + if (methods.has(name)) lowerer.prototypeMethodAccesses.set(name, site); + if (!arrow) receiverFields.add(name); + }; + const unwrap = (value: ts.Expression): ts.Expression => { + while ( + ts.isParenthesizedExpression(value) || + ts.isAsExpression(value) || + ts.isTypeAssertion(value) || + ts.isNonNullExpression(value) + ) + value = value.expression; + return value; + }; + // Structural types can be live views of class instances. A fresh + // allocation in a constant binding proves that a dictionary write cannot + // replace a class method without relying on its declared shape. + const freshReceiver = (value: ts.Expression): boolean => { + value = unwrap(value); + if (ts.isIdentifier(value)) { + const symbol = lowerer.checker.getSymbolAtLocation(value); + const decl = symbol && lowerer.checker.declarationsOf(symbol)[0]; + if ( + !decl || + !ts.isVariableDeclaration(decl) || + !ts.isIdentifier(decl.name) || + !ts.isVariableDeclarationList(decl.parent) || + !(decl.parent.flags & ts.NodeFlags.Const) || + !decl.initializer + ) + return false; + value = unwrap(decl.initializer); + } + if (ts.isObjectLiteralExpression(value) || ts.isArrayLiteralExpression(value)) return true; + return ( + ts.isCallExpression(value) && + ts.isPropertyAccessExpression(value.expression) && + lowerer.stdlibGlobalMember(value.expression, "Object") === "create" + ); + }; + const primitiveDictionary = (value: ts.Expression): boolean => { + const primitive = (type: ts.Type): boolean => + type.isUnionType() + ? ts.constituentTypes(type).every(primitive) + : (type.flags & + (ts.TypeFlags.StringLike | + ts.TypeFlags.NumberLike | + ts.TypeFlags.BooleanLike | + ts.TypeFlags.BigIntLike | + ts.TypeFlags.Null | + ts.TypeFlags.Undefined | + ts.TypeFlags.ESSymbolLike)) !== + 0; + return lowerer.checker + .getIndexInfosOfType(lowerer.typeOf(value)) + .some( + (index) => + (index.keyType.flags & ts.TypeFlags.StringLike) !== 0 && primitive(index.valueType), + ); + }; + const literalLoopKeys = (key: ts.Expression): (string | number)[] | null => { + if (!ts.isIdentifier(key)) return null; + const symbol = lowerer.checker.getSymbolAtLocation(key); + const decl = symbol && lowerer.checker.declarationsOf(symbol)[0]; + if (!decl || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return null; + const list = decl.parent; + if (!ts.isVariableDeclarationList(list) || !(list.flags & ts.NodeFlags.Const)) return null; + const loop = list.parent; + if (!ts.isForOfStatement(loop) || loop.initializer !== list) return null; + const values = unwrap(loop.expression); + if (!ts.isArrayLiteralExpression(values)) return null; + const names: (string | number)[] = []; + for (const value of values.elements) { + if (ts.isStringLiteralLike(value)) names.push(value.text); + else if (ts.isNumericLiteral(value)) names.push(Number(value.text)); + else return null; + } + return names; + }; + const sameBinding = (left: ts.Expression, right: ts.Expression): boolean => { + left = unwrap(left); + right = unwrap(right); + if (!ts.isIdentifier(left) || !ts.isIdentifier(right)) return false; + const symbol = lowerer.checker.getSymbolAtLocation(left); + return symbol !== undefined && symbol === lowerer.checker.getSymbolAtLocation(right); + }; + const preservesExistingMethod = ( + key: ts.Expression, + receiver: ts.Expression, + site: ts.Node, + ): boolean => { + // An immediately guarded insertion cannot replace a method already + // present on the receiver or its prototype. Restrict the body to this + // one write so no intervening statement can change either binding. + const statement = site.parent; + if (!ts.isExpressionStatement(statement)) return false; + const branch = statement.parent; + if (!ts.isIfStatement(branch) || branch.thenStatement !== statement) return false; + const test = unwrap(branch.expression); + if (!ts.isPrefixUnaryExpression(test) || test.operator !== ts.SyntaxKind.ExclamationToken) + return false; + const present = unwrap(test.operand); + return ( + ts.isBinaryExpression(present) && + present.operatorToken.kind === ts.SyntaxKind.InKeyword && + sameBinding(key, present.left) && + sameBinding(receiver, present.right) + ); + }; + const deletesOwnEnumerableKey = ( + key: ts.Expression, + receiver: ts.Expression, + site: ts.Node, + ): boolean => { + if (!ts.isDeleteExpression(site) || !ts.isIdentifier(key)) return false; + const statement = site.parent; + if (!ts.isExpressionStatement(statement)) return false; + const loop = statement.parent; + if (!ts.isForOfStatement(loop) || loop.statement !== statement) return false; + if (!ts.isVariableDeclarationList(loop.initializer)) return false; + const decl = loop.initializer.declarations[0]; + if (!decl || !ts.isIdentifier(decl.name) || !sameBinding(key, decl.name)) return false; + const keys = unwrap(loop.expression); + return ( + ts.isCallExpression(keys) && + ts.isPropertyAccessExpression(keys.expression) && + lowerer.stdlibGlobalMember(keys.expression, "Object") === "keys" && + keys.arguments.length === 1 && + sameBinding(receiver, keys.arguments[0]!) + ); + }; + const markKey = (key: ts.Expression, site: ts.Node, receiver?: ts.Expression): void => { + key = unwrap(key); + if (ts.isStringLiteralLike(key)) markName(key.text, site); + else if (ts.isNumericLiteral(key)) markName(String(Number(key.text)), site); + else computedWrites.push({ key, receiver, site }); + }; + const markFields = (value: ts.Expression, site: ts.Node, receiver?: ts.Expression): void => { + value = unwrap(value); + if (!ts.isObjectLiteralExpression(value)) { + computedWrites.push({ key: undefined, receiver, site }); + return; + } + for (const field of value.properties) { + if (ts.isSpreadAssignment(field)) markFields(field.expression, site, receiver); + else if (field.name && (ts.isIdentifier(field.name) || ts.isStringLiteralLike(field.name))) + markName(field.name.text, site); + else if (field.name && ts.isComputedPropertyName(field.name)) + markKey(field.name.expression, site, receiver); + } + }; + const markTarget = (target: ts.Expression, site: ts.Node, arrow = false): void => { + target = unwrap(target); + if (ts.isPropertyAccessExpression(target)) markName(target.name.text, site, arrow); + else if (ts.isElementAccessExpression(target)) + markKey(target.argumentExpression, site, target.expression); + else if (ts.isArrayLiteralExpression(target)) { + for (const element of target.elements) { + if (ts.isSpreadElement(element)) markTarget(element.expression, site); + else if (!ts.isOmittedExpression(element)) markTarget(element, site); + } + } else if (ts.isObjectLiteralExpression(target)) { + for (const field of target.properties) { + if (ts.isPropertyAssignment(field)) markTarget(field.initializer, site); + else if (ts.isSpreadAssignment(field)) markTarget(field.expression, site); + } + } else if ( + ts.isBinaryExpression(target) && + target.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) + markTarget(target.left, site); + }; + for (const file of files) + ts.walkPreorder(file, (node) => { + if (lowerer.dynamic) unknownKey = file; + if ( + (ts.isPropertyDeclaration(node) || + ts.isPropertyAssignment(node) || + ts.isMethodDeclaration(node) || + ts.isGetAccessorDeclaration(node) || + ts.isSetAccessorDeclaration(node)) && + ts.isComputedPropertyName(node.name) + ) { + markKey(node.name.expression, node); + } + + if ( + (ts.isPropertyDeclaration(node) || + ts.isGetAccessorDeclaration(node) || + ts.isSetAccessorDeclaration(node)) && + (ts.isIdentifier(node.name) || + ts.isStringLiteralLike(node.name) || + ts.isNumericLiteral(node.name)) + ) { + if ( + ts.isPropertyDeclaration(node) && + node.initializer && + ts.isArrowFunction(unwrap(node.initializer)) + ) + arrowFields.add(node.name.text); + else receiverFields.add(node.name.text); + } + if ( + (ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node)) && + (ts.isIdentifier(node.name) || + ts.isStringLiteralLike(node.name) || + ts.isNumericLiteral(node.name)) + ) { + if (!ts.isPropertyAssignment(node) || !ts.isArrowFunction(unwrap(node.initializer))) + receiverFields.add(node.name.text); + } + if (ts.isParameter(node) && ts.isIdentifier(node.name) && node.modifiers?.length) + receiverFields.add(node.name.text); + if ( + ts.isMethodDeclaration(node) && + (ts.isIdentifier(node.name) || + ts.isStringLiteralLike(node.name) || + ts.isNumericLiteral(node.name)) + ) { + receiverFields.add(node.name.text); + } + if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + node.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ) { + markTarget( + node.left, + node, + node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isArrowFunction(unwrap(node.right)), + ); + if ( + isJsSourceFile(file) && + node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + (ts.isPropertyAccessExpression(node.left) || + (ts.isElementAccessExpression(node.left) && + ts.isStringLiteralLike(node.left.argumentExpression))) + ) + jsAssignments.push(node.left); + } + if (ts.isDeleteExpression(node)) markTarget(node.expression, node); + if ( + (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && + (node.operator === ts.SyntaxKind.PlusPlusToken || + node.operator === ts.SyntaxKind.MinusMinusToken) + ) + markTarget(node.operand, node); + if ( + (ts.isForOfStatement(node) || ts.isForInStatement(node)) && + !ts.isVariableDeclarationList(node.initializer) && + node.initializer.kind !== ts.SyntaxKind.MissingDeclaration + ) + markTarget(node.initializer, node); + if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) { + const base = unwrap(node.expression); + const prototype = + (ts.isPropertyAccessExpression(base) && base.name.text === "prototype") || + (ts.isElementAccessExpression(base) && + ts.isStringLiteralLike(base.argumentExpression) && + base.argumentExpression.text === "prototype"); + if (prototype) { + if (ts.isPropertyAccessExpression(node)) + lowerer.prototypeMethodAccesses.set(node.name.text, node); + else if (ts.isStringLiteralLike(node.argumentExpression)) + lowerer.prototypeMethodAccesses.set(node.argumentExpression.text, node); + else markKey(node.argumentExpression, node); + } + } + // Extracted reflection functions and aliases of Object/Reflect can + // mutate arbitrary names. Direct calls below retain precise keys. + if (ts.isIdentifier(node) && (node.text === "Object" || node.text === "Reflect")) + globals.push(node); + if ( + ts.isPropertyAccessExpression(node) && + [ + "setPrototypeOf", + "defineProperty", + "set", + "deleteProperty", + "defineProperties", + "assign", + ].includes(node.name.text) + ) + reflection.push(node); + }); + // The census crosses deferred bodies. Batch only the binding and key + // queries it actually needs instead of eagerly querying every identifier. + lowerer.checker.prefetchClassCollection( + jsAssignments.flatMap((access) => + ts.isElementAccessExpression(access) ? [access.expression] : [], + ), + jsAssignments.flatMap((access) => (ts.isPropertyAccessExpression(access) ? [access.name] : [])), + ); + for (const access of jsAssignments) { + const name = ts.isPropertyAccessExpression(access) + ? access.name.text + : (access.argumentExpression as ts.StringLiteral).text; + const symbol = ts.isPropertyAccessExpression(access) + ? lowerer.checker.getSymbolAtLocation(access.name) + : lowerer.checker.getPropertyOfType(lowerer.typeOf(access.expression), name); + if (symbol && lowerer.checker.declarationsOf(symbol).some(ts.isMethodDeclaration)) + lowerer.prototypeMethodAccesses.set(name, access); + } + lowerer.checker.prefetchSymbolRoots([...globals, ...reflection]); + for (const node of globals) + if ( + (lowerer.isStdlibGlobal(node, "Object") || lowerer.isStdlibGlobal(node, "Reflect")) && + (!ts.isPropertyAccessExpression(node.parent) || node.parent.expression !== node) + ) + unknownKey = node; + for (const node of reflection) { + const objectMethod = lowerer.stdlibGlobalMember(node, "Object"); + const reflectMethod = lowerer.stdlibGlobalMember(node, "Reflect"); + if (objectMethod === "setPrototypeOf" || reflectMethod === "setPrototypeOf") unknownKey = node; + const single = + objectMethod === "defineProperty" || + reflectMethod === "defineProperty" || + reflectMethod === "set" || + reflectMethod === "deleteProperty"; + const multiple = objectMethod === "defineProperties" || objectMethod === "assign"; + if (!single && !multiple) continue; + const call = node.parent; + if (!ts.isCallExpression(call) || call.expression !== node) { + unknownKey = node; + continue; + } + const receiver = call.arguments[0]; + if (single && call.arguments[1]) markKey(call.arguments[1], call, receiver); + else if (objectMethod === "defineProperties" && call.arguments[1]) + markFields(call.arguments[1], call, receiver); + else if (objectMethod === "assign") + for (const source of call.arguments.slice(1)) markFields(source, call, receiver); + } + lowerer.checker.prefetchClassCollection( + computedWrites.flatMap((write) => [ + ...(write.key ? [write.key] : []), + ...(write.receiver ? [write.receiver] : []), + ]), + computedWrites.flatMap((write) => [ + ...(write.key ? [write.key] : []), + ...(write.receiver ? [write.receiver] : []), + ]), + ); + for (const { key, receiver, site } of computedWrites) { + const type = key ? lowerer.typeOf(key) : undefined; + const names = type && (literalValues(type) ?? (key ? literalLoopKeys(key) : null)); + if (names) { + for (const name of names) markName(String(name), site); + continue; + } + if (key && receiver && preservesExistingMethod(key, receiver, site)) continue; + if (key && receiver && deletesOwnEnumerableKey(key, receiver, site)) { + // Class methods are non-enumerable prototype properties. Own callback + // fields remain observable, so this proof does not certify those. + unknownOwnField = true; + continue; + } + // A primitive-only string index excludes callable method slots. Unlike + // structural interfaces, it cannot be a live class-method view. + if (receiver && (freshReceiver(receiver) || primitiveDictionary(receiver))) continue; + const flags = type?.flags ?? ts.TypeFlags.Unknown; + if (flags & ts.TypeFlags.NumberLike) { + // Numeric writes can replace numeric names but not ordinary names. + numericKey = site; + } else if (!(flags & ts.TypeFlags.ESSymbolLike)) { + unknownKey = site; + } + } + if (numericKey) { + for (const name of [...methods, ...arrowFields]) { + if (String(Number(name)) === name) markName(name, numericKey); + } + } + if (unknownKey) + for (const method of methods) lowerer.prototypeMethodAccesses.set(method, unknownKey); + if (!unknownKey && !unknownOwnField) + for (const field of arrowFields) { + if (!receiverFields.has(field)) lowerer.receiverFreeCallbackFields.add(field); + } +} diff --git a/packages/compiler/src/frontend/lowering/collection-narrowing.ts b/packages/compiler/src/frontend/lowering/collection-narrowing.ts new file mode 100644 index 000000000..57b75daf5 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/collection-narrowing.ts @@ -0,0 +1,36 @@ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; + +/** instanceof uses Map/Set in the standard declarations. + * Recover an unambiguous native collection's stored arguments when that + * test erased a readonly view's arguments. Never choose between layouts. */ +export function concreteCollectionNarrow( + lowerer: Lowerer, + node: ts.Node, + narrowed: ts.Type, +): ts.Type | null { + if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return null; + const symbol = narrowed.getSymbol(); + const name = symbol?.name; + if (name !== "Map" && name !== "Set") return null; + if ( + !symbol || + !lowerer.checker + .declarationsOf(symbol) + .some((decl) => lowerer.isStdlibFile(decl.getSourceFile())) + ) + return null; + const args = lowerer.checker.getTypeArguments(narrowed as ts.TypeReference); + if (args.length === 0 || !args.every((arg) => (arg.flags & ts.TypeFlags.Any) !== 0)) return null; + const binding = lowerer.checker.getSymbolAtLocation( + ts.isPropertyAccessExpression(node) ? node.name : node, + ); + if (!binding) return null; + const stored = lowerer.checker.getTypeOfSymbol(binding); + const arms = stored.isUnionType() ? ts.constituentTypes(stored) : [stored]; + const candidates = arms.filter((arm) => { + const mapped = lowerer.mapTypeOf(arm); + return mapped?.kind === (name === "Map" ? "map" : "set"); + }); + return candidates.length === 1 ? candidates[0]! : null; +} diff --git a/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts b/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts new file mode 100644 index 000000000..b88de3795 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts @@ -0,0 +1,63 @@ +import * as ts from "../../ts7/adapter.js"; +import { locOf } from "../../program.js"; +import { varRef } from "../../../ir/build.js"; +import { DYN, type IrExpr, type IrStmt, isUnitType } from "../../../ir/ir.js"; +import type { Lowerer } from "../lowerer.js"; +import { completeFuncValueArgs } from "../call-arguments.js"; +import { lowerOptionalNumber } from "../lower-exprs.js"; +import { lowerSafeIndexRead } from "../array-values.js"; + +/** Retain native function slots during indexed calls. The array remains + * the receiver, and the selected callback is captured before arguments + * can replace it. Missing slots throw only after argument evaluation. */ +export function lowerArrayCallbackCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.ElementAccessExpression, + array: IrExpr, +): IrExpr | null { + if (array.type.kind !== "array" || call.arguments.some(ts.isSpreadElement)) return null; + const elem = array.type.elem; + const arms = elem.kind === "union" ? (lowerer.unions.get(elem.unionId)?.arms ?? []) : [elem]; + const functions = arms.filter((arm) => arm.kind === "func"); + const fn = functions[0]; + if ( + functions.length !== 1 || + fn?.kind !== "func" || + !arms.every((arm) => arm.kind === "func" || isUnitType(arm)) || + fn.rest + ) + return null; + if (!(lowerer.typeOf(access.argumentExpression).flags & ts.TypeFlags.NumberLike)) return null; + const loc = locOf(call); + const statements: IrStmt[] = []; + const store = (value: IrExpr, prefix: string): IrExpr => { + const local = lowerer.declareHiddenLocal(prefix, value.type); + statements.push({ kind: "varDecl", localId: local.id, init: value, loc }); + return varRef(local.id, local.type, loc); + }; + const receiver = store(array, "%callbackArray"); + const index = lowerOptionalNumber( + lowerer, + lowerer.lowerExpr(access.argumentExpression), + locOf(access.argumentExpression), + access.argumentExpression, + ); + const read = lowerSafeIndexRead(lowerer, receiver, index, loc); + if (!read || read.type.kind !== "union") return null; + const callee = store(read, "%arrayCallback"); + const args = completeFuncValueArgs(lowerer, call, fn, loc).map((arg) => + store(arg, "%callbackArg"), + ); + const helper = lowerer.narrowedArmHelper(read.type.unionId, fn, loc); + if (!helper) return null; + const result: IrExpr = { + kind: "callValue", + callee: { kind: "call", callee: helper, args: [callee], type: fn, loc }, + receiver: lowerer.coerceToExpected(receiver, DYN), + args, + type: fn.ret, + loc, + }; + return { kind: "seqExpr", stmts: statements, result, type: result.type, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 96c53721e..ef06b67b7 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -1904,92 +1904,6 @@ export function lowerTupleReadMethodCall( return { kind: "call", callee: helper, args: [receiver, fnArg], type: outType, loc }; } -/** An ordinary indexed read: values retain their element type, while - * holes, missing properties, and present undefined yield undefined. */ -export function lowerSafeIndexRead( - lowerer: Lowerer, - arr: IrExpr, - index: IrExpr, - loc: SrcLoc, -): IrExpr | null { - if (arr.type.kind !== "array") throw new InternalCompilerError("indexed read requires an array"); - const elem = arr.type.elem; - if (elem.kind === "void") return null; - const resultT = arrayValueType(lowerer, elem); - const key = `idxOr:${typeKey(elem)}`; - let name = lowerer.arrHofHelpers.get(key); - if (!name) { - name = `%arr.idxOr.${lowerer.arrHofHelpers.size}`; - lowerer.arrHofHelpers.set(key, name); - const arrT = arr.type; - lowerer.liftedFns.push({ - name, - params: [ - { localId: "a.0", name: "a", type: arrT }, - { localId: "i.0", name: "i", type: F64 }, - ], - returnType: resultT, - locals: [ - { id: "a.0", name: "a", type: arrT, mutable: false }, - { id: "i.0", name: "i", type: F64, mutable: false }, - ], - body: [ - { - kind: "return", - value: arrayValueRead( - lowerer, - varRef("a.0", arrT, loc), - varRef("i.0", F64, loc), - elem, - loc, - ), - loc, - }, - ], - loc, - }); - } - return { kind: "call", callee: name, args: [arr, index], type: resultT, loc }; -} - -/** Fuse only our own f64-array read helper with immediate ToNumber. - * Reuse its arguments verbatim so the receiver and index still evaluate - * exactly once, in order, before the read. Do not specialize user calls, - * union-element arrays, or optional values stored in locals. */ -export function tryLowerNumericIndexRead( - lowerer: Lowerer, - operand: IrExpr, - loc: SrcLoc, -): IrExpr | null { - if ( - operand.kind !== "call" || - operand.callee !== lowerer.arrHofHelpers.get(`idxOr:${typeKey(F64)}`) - ) - return null; - const [arr, index] = operand.args; - if ( - operand.args.length !== 2 || - arr?.type.kind !== "array" || - arr.type.elem.kind !== "f64" || - index?.type.kind !== "f64" - ) - return null; - // The intrinsic owns the evaluated receiver through index evaluation, - // then borrows it for one slot lookup. No extra helper parameter or - // separate state/getter expressions need to retain the array again. - return { - kind: "arrIntrinsic", - method: "getNumber", - receiver: arr, - args: [index], - type: F64, - loc, - }; -} - -/** Backward-compatible name for the npm-static probe path. */ -export const lowerNpmStaticSafeIndexRead = lowerSafeIndexRead; - /** Interned synthetic function for one (elem, argument-shape) concat — * `%arr.concat.(a, x0, x1, ...)`: a fresh array takes a's elements * (pushSpread), then each argument pushes (element) or spreads (array) diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index 3dbfc2477..06890193a 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -92,7 +92,17 @@ export function lowerMapMethodCall( // result union. `undefined` sorts LAST among all possible arm // typeKeys, so when V is itself a union its arms keep their tags in // the result union — the backend leans on that (docs/ir.md). - const type = receiverIr.value.kind === "dyn" ? DYN : lowerer.irTypeOf(call); + // instanceof on a readonly view can erase the checker's value type + // to any. Recover that case from native storage; otherwise preserve + // the contextual return mapping for recursive and generic values. + const erased = (lowerer.typeOf(call).flags & ts.TypeFlags.Any) !== 0; + const type = + receiverIr.value.kind === "dyn" + ? DYN + : erased + ? (lowerer.withUndefinedArmOf(receiverIr.value) ?? + lowerer.withUndefinedArm(receiverIr.value)) + : lowerer.irTypeOf(call); if (type.kind !== "union" && type.kind !== "dyn") lowerer.badType(call, lowerer.typeOf(call)); return { kind: "mapIntrinsic", method: "get", receiver, args: [k], type, loc }; } diff --git a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts index 3829c846c..a9df9e662 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts @@ -102,6 +102,7 @@ export function isNativeBuiltinValueInitializer( lowerer.stdlibGlobalMember(expr, "globalThis") === "console" || lowerer.stdlibGlobalMember(expr, "process") === "getBuiltinModule" || lowerer.stdlibGlobalMember(expr, "process") === "hrtime" || + ["stdin", "stdout", "stderr"].includes(lowerer.stdlibGlobalMember(expr, "process") ?? "") || (expr.name.text === "bigint" && ts.isPropertyAccessExpression(expr.expression) && lowerer.stdlibGlobalMember(expr.expression, "process") === "hrtime") || diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 225e13aec..1298bddaa 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,3 +1,4 @@ +import { lowerArrayCallbackCall } from "./containers/array-callback-call.js"; /** Dispatch source calls, builtin members and receiver-specific operations. * Signature collection, argument completion and function bodies have their * own owners; this module selects the applicable call path. */ @@ -1422,6 +1423,25 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (sig && !(jsSpreadArgs && spreadNeedsRuntimeArity(lowerer, sig.params, expr.arguments))) { lowerer.noteEdge(sig.name); const args = lowerer.completeArgs(expr.arguments, sig.params, loc, expr); + const symbol = lowerer.resolveValueSymbol(expr.expression); + const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + // A bare call supplies undefined as this, even inside a callback + // currently carrying a receiver. The value-call ABI saves and + // restores that ambient receiver around the function invocation. + if ( + declaration && + ts.isFunctionDeclaration(declaration) && + declaration.parameters.some(isThisParameter) + ) { + const callee = lowerer.lowerExpr(expr.expression); + return reconcileOverloadReturn(lowerer, expr, { + kind: "callValue", + callee, + args, + type: sig.returnType, + loc, + }); + } return reconcileOverloadReturn(lowerer, expr, { kind: "call", callee: sig.name, @@ -1871,6 +1891,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { ) { const access = expr.expression; const value = tryLowerExpression(lowerer, access.expression); + if (value?.type.kind === "array" && !isJsSourceFile(expr.getSourceFile())) { + const callback = lowerArrayCallbackCall(lowerer, expr, access, value); + if (callback) return callback; + } + if ( value?.type.kind === "dyn" || value?.type.kind === "generator" || @@ -8991,7 +9016,9 @@ export function lowerObjectMethodCall( const receiverLocal = lowerer.declareHiddenLocal("%callReceiver", target.obj.type); const init: IrStmt = { kind: "varDecl", localId: receiverLocal.id, init: target.obj, loc }; target.obj = { kind: "varRef", localId: receiverLocal.id, type: receiverLocal.type, loc }; - const receiver = lowerer.coerceToExpected(target.obj, DYN); + const receiver: { receiver?: IrExpr } = {}; + if (!lowerer.receiverFreeCallbackFields.has(access.name.text)) + receiver.receiver = lowerer.coerceToExpected(target.obj, DYN); const finish = (result: IrExpr): IrExpr => ({ kind: "seqExpr", stmts: [init], @@ -9021,12 +9048,12 @@ export function lowerObjectMethodCall( ) { const boxed: IrExpr = { kind: "dynFrom", value: callee, type: DYN, loc }; const spread = lowerSpreadArgsCall(lowerer, call, boxed, loc); - if (spread?.kind === "dynCall") return finish({ ...spread, receiver }); + if (spread?.kind === "dynCall") return finish({ ...spread, ...receiver }); const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); return finish({ kind: "dynCall", callee: boxed, - receiver, + ...receiver, calleeName: access.getText(), args, type: DYN, @@ -9034,7 +9061,7 @@ export function lowerObjectMethodCall( }); } const args = completeFuncValueArgs(lowerer, call, callee.type, locOf(call)); - return finish({ kind: "callValue", callee, receiver, args, type: callee.type.ret, loc }); + return finish({ kind: "callValue", callee, ...receiver, args, type: callee.type.ret, loc }); } if (callee?.type.kind === "dyn") { if (call.arguments.some((a) => ts.isSpreadElement(a))) { @@ -9044,7 +9071,7 @@ export function lowerObjectMethodCall( return finish({ kind: "dynCall", callee, - receiver, + ...receiver, calleeName: access.getText(), args, type: DYN, diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 452099ff1..0c5fd0f26 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,4 +1,5 @@ import { dynUndefinedExpr, nodeThrowExpr } from "../../ir/build.js"; +import { collectClassMethodMutations } from "./class-method-mutations.js"; import { InternalCompilerError } from "../../errors.js"; /* Class lowering: shape collection over the single-inheritance graph * (fields, methods, accessors, overrides), constructor/member lowering with @@ -758,6 +759,10 @@ export function registerBuiltinErrorClasses(lowerer: Lowerer): void { { name: "%nameEnumerable", type: BOOL }, { name: "%stackFrames", type: STRING }, { name: "%stack", type: STRING }, + { name: "%systemErrno", type: F64 }, + { name: "%systemCall", type: STRING }, + { name: "%systemPath", type: STRING }, + { name: "%systemDest", type: STRING }, ], loc, }, @@ -5984,36 +5989,7 @@ export function collectInheritedJsFieldWrites( * hierarchy identifies the original declarations through aliases as well. */ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.SourceFile[]): void { const families: [ts.MethodDeclaration, ts.MethodDeclaration][] = []; - // Record named prototype accesses before collecting method signatures. - // Those slots need a callable ABI even when JavaScript would otherwise - // specialize each call independently. - for (const file of files) - ts.walkPreorder(file, (node) => { - if (!ts.isPropertyAccessExpression(node) && !ts.isElementAccessExpression(node)) return; - const name = ts.isPropertyAccessExpression(node) - ? node.name.text - : ts.isStringLiteralLike(node.argumentExpression) - ? node.argumentExpression.text - : null; - if (name === null) return; - const prototype = - ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "prototype"; - const assigned = - ts.isBinaryExpression(node.parent) && - node.parent.left === node && - node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken; - const symbol = !assigned - ? undefined - : ts.isPropertyAccessExpression(node) - ? lowerer.checker.getSymbolAtLocation(node.name) - : lowerer.checker.getPropertyOfType(lowerer.typeOf(node.expression), name); - if ( - prototype || - (symbol && lowerer.checker.declarationsOf(symbol).some(ts.isMethodDeclaration)) - ) { - lowerer.prototypeMethodAccesses.set(name, node); - } - }); + collectClassMethodMutations(lowerer, files); const visit = (node: ts.Node): void => { if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { const accessors = node.members.filter( diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 6afd99152..1392a24e4 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,3 +1,4 @@ +import { isOptionalProcessStreamProperty } from "./builtins/process.js"; import { lowerWorkerMetadata } from "./builtins/workers.js"; import { dynUndefinedExpr, @@ -105,12 +106,13 @@ import { import { PoisonError, dynFallbackType, jsFuncNameOf, neverTaintedJsType, own } from "./lowerer.js"; import { lowerCollectionSpread } from "./containers/collection-methods.js"; import { - lowerNpmStaticSafeIndexRead, + arrayValueRead, + arrayValueStore, + arrayValueType, lowerSafeIndexRead, tryLowerNumericIndexRead, -} from "./containers/array-methods.js"; +} from "./array-values.js"; import { strCharsCall } from "./containers/array-construction.js"; -import { arrayValueRead, arrayValueStore, arrayValueType } from "./array-values.js"; import { lowerOptionalStringIndex } from "./string-index.js"; import { tryLowerIndexedComparison } from "./indexed-comparison.js"; import { npmStaticPackageOfPath } from "../npm-static.js"; @@ -3583,6 +3585,9 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx return expr; } if (expr.type.kind !== "union") return expr; + if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node)) + return expr; + // Represented fields and index values can include undefined even when // checker inference or lib.d.ts declares strings (RegExp captures). // Only a declaration that represents the optional arm can provide a @@ -4316,7 +4321,26 @@ function lowerNullishPair( // arm that has no first-class union representation. const fresh = rest.length === 1 ? lowerer.emptyCollectionFor(expr.right, rest[0]!) : null; if (fresh) return { kind: "nullish", left, right: fresh, type: fresh.type, loc }; - const type = lowerer.irTypeOf(expr); + let type = lowerer.irTypeOf(expr); + // A runtime-optional read can reach the default even when declarations + // call the left side present. Preserve a differently typed primitive + // fallback rather than checking it against that incomplete result type. + const rightType = lowerer.mapTypeOf(lowerer.typeOf(expr.right)); + const primitive = (t: IrType): boolean => + t.kind === "f64" || t.kind === "string" || t.kind === "bool"; + if ( + rightType && + primitive(type) && + primitive(rightType) && + !typeEquals(type, rightType) && + rest.every(primitive) + ) { + const arms = [...rest]; + if (!arms.some((arm) => typeEquals(arm, rightType))) arms.push(rightType); + arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + type = { kind: "union", unionId: lowerer.unions.intern(arms) }; + lowerer.runtimeOptionalArithmeticTypes.set(expr, type); + } if (type.kind === "dyn" && lowerer.dynConvertible(left.type)) { return { kind: "nullish", @@ -6397,7 +6421,12 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre // exit primitives eagerly): the engine element read, exiting at the // declared number type. if (recv.type.kind === "jsval") return islandElementRead(lowerer, expr, recv); - const index = lowerer.lowerExpr(expr.argumentExpression); + const index = lowerOptionalNumber( + lowerer, + lowerer.lowerExpr(expr.argumentExpression), + locOf(expr.argumentExpression), + expr.argumentExpression, + ); if (isJsSourceFile(expr.getSourceFile()) && recv.type.kind === "bytes") { return { kind: "dynKeyGet", @@ -6698,38 +6727,23 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } if (receiverIr?.kind === "string") { - // `s[i]` with a number index reads a UTF-16 code unit — charAt's - // exact job (the UTF-16-exact runtime). Without - // noUncheckedIndexedAccess the checker types the read `string`, - // and charAt's "" is the only string-typed answer for an - // out-of-range or fractional index where JS reads `undefined` — - // SEMANTICS.md documents the divergence; in-range integer reads - // (the loop pattern) are JS-exact. Optional results instead retain - // undefined for missing string properties. + // An indexed string property is absent for non-integer and missing + // indexes. charAt has different coercion and out-of-range semantics. const recv = lowerer.lowerExpr(expr.expression); - const index = lowerer.lowerExpr(expr.argumentExpression); - const resultType = lowerer.mapTypeOf(lowerer.typeOf(expr)); + const index = lowerOptionalNumber( + lowerer, + lowerer.lowerExpr(expr.argumentExpression), + locOf(expr.argumentExpression), + expr.argumentExpression, + ); if (index.type.kind === "f64" && recv.type.kind === "string") { - if (resultType?.kind === "string") { - return { - kind: "strIntrinsic", - method: "charAt", - receiver: recv, - args: [index], - type: STRING, - loc: locOf(expr), - }; - } - if (resultType?.kind === "union") { - const arms = lowerer.unions.get(resultType.unionId)?.arms; - if ( - arms?.length === 2 && - lowerer.armTag(resultType.unionId, STRING) >= 0 && - lowerer.armTag(resultType.unionId, UNDEFINED_T) >= 0 - ) { - return lowerOptionalStringIndex(lowerer, recv, index, resultType, locOf(expr)); - } - } + return lowerOptionalStringIndex( + lowerer, + recv, + index, + lowerer.withUndefinedArm(STRING), + locOf(expr), + ); } lowerer.unsupported( "SC1090", @@ -6902,7 +6916,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre arr.method === "slice" && npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null ) { - const safe = lowerNpmStaticSafeIndexRead(lowerer, arr, index, locOf(expr)); + const safe = lowerSafeIndexRead(lowerer, arr, index, locOf(expr)); if (safe) return safe; } // A union-element read narrows like an identifier when the checker has @@ -8525,13 +8539,9 @@ export function lowerAsExpression( }; } } - // Narrowed reads (`err instanceof Error` proven) still pass below; - // other targets keep the narrowness fence with the checked-cast fix. - // In a JS FILE the AsExpression is tsgo's spelling of a JSDoc cast - // (`/** @type {T} */ (e)` — JS has no `as`); 5.9.3 parsed that as a - // plain parenthesized read, which lowered caught→dyn and met the - // target through the ordinary checked-cast coercion — so the dyn - // bridge stays open here exactly for that shape. + // Structural assertions use the ordinary checked dynamic bridge. It + // retains primitive values and native class identity before validating + // the requested shape; an assertion does not bypass runtime checks. const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); const bridged = narrowed && @@ -8539,7 +8549,7 @@ export function lowerAsExpression( narrowed.kind === "bool" || narrowed.kind === "string" || narrowed.kind === "object" || - (narrowed.kind === "dyn" && isJsSourceFile(expr.getSourceFile()))); + narrowed.kind === "dyn"); if (!bridged) lowerer.unsupported("SC1063", expr); } const inner = lowerer.lowerExpr(expr.expression); @@ -10074,7 +10084,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr } const optionalStringPlus = lowerOptionalStringPlus(); if (optionalStringPlus) return optionalStringPlus; - if (isJsSourceFile(expr.getSourceFile())) { + { const primitive = (type: IrType): boolean => type.kind === "f64" || type.kind === "string" || @@ -11542,6 +11552,37 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc ) { const collection = lowerer.isStdlibGlobal(expr.right, "Map") ? "map" : "set"; const value = lowerer.lowerExpr(expr.left); + if (value.type.kind === "union") { + const union = lowerer.unions.get(value.type.unionId); + if (union) { + const local = lowerer.declareHiddenLocal("%collectionTest", value.type); + const reference = varRef(local.id, value.type, loc); + let result: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; + union.arms.forEach((arm, tag) => { + if (arm.kind !== collection) return; + const test: IrExpr = { + kind: "unionIsTag", + unionId: union.id, + value: reference, + tag, + negated: false, + type: BOOL, + loc, + }; + result = + result.kind === "boolLit" + ? test + : { kind: "logical", op: "||", left: result, right: test, type: BOOL, loc }; + }); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }], + result, + type: BOOL, + loc, + }; + } + } if (value.type.kind === "dyn") return { kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index e106483a0..2c004133b 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -25,7 +25,30 @@ import { appendImplicitUndefinedReturn, blockBodyOf, } from "./function-returns.js"; -import { generatorMeta, type ParamShape } from "./call-signatures.js"; +import { generatorMeta, isThisParameter, type ParamShape } from "./call-signatures.js"; + +/** Save the call-site receiver before defaults or body effects. Escaping + * arrows capture this binding instead of reading a later call's receiver. */ +function declareCallReceiver( + lowerer: Lowerer, + node: ts.FunctionLikeDeclaration, + generator: boolean, +): IrStmt { + const receiver = lowerer.declareThis(DYN); + const loc = locOf(node); + return { + kind: "varDecl", + localId: receiver.id, + init: { + kind: "libCall", + fn: generator ? "dyn.generatorThis" : "dyn.this", + args: [], + type: DYN, + loc, + }, + loc, + }; +} /** True when the identifier resolves (through import aliases) to a * top-level function declaration of ANY program file (not merely a @@ -253,25 +276,15 @@ export function lowerLambda( const diagsBefore = lowerer.diags.length; lowerer.fnStack.push(fnCtx); try { + const receiver = + (checkedReceiver || + isJsSourceFile(node.getSourceFile()) || + node.parameters.some(isThisParameter)) && + !ts.isArrowFunction(node) + ? declareCallReceiver(lowerer, node, isGenerator) + : null; const { params, prologue } = lowerer.declareParams(node.parameters, shapes); - // Checked JavaScript functions take their receiver from the call site. - // Store it as this function's binding so escaping arrows capture it, - // and a method nested inside another method never captures its owner. - if ((checkedReceiver || isJsSourceFile(node.getSourceFile())) && !ts.isArrowFunction(node)) { - const receiver = lowerer.declareThis(DYN); - prologue.unshift({ - kind: "varDecl", - localId: receiver.id, - init: { - kind: "libCall", - fn: isGenerator ? "dyn.generatorThis" : "dyn.this", - args: [], - type: DYN, - loc, - }, - loc, - }); - } + if (receiver) prologue.unshift(receiver); declareFunctionArguments( lowerer, node, @@ -447,7 +460,12 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I const diagsBefore = lowerer.diags.length; lowerer.fnStack.push(ctx); try { + const receiver = + decl.parameters.some(isThisParameter) || isJsSourceFile(decl.getSourceFile()) + ? declareCallReceiver(lowerer, decl, sig.generator !== undefined) + : null; const { params, prologue } = lowerer.declareParams(decl.parameters, sig.params); + if (receiver) prologue.unshift(receiver); declareFunctionArguments( lowerer, decl, diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index ac3f2671a..55a31c85d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -16,6 +16,7 @@ import { arrayValueStore, arrayValueType, unionArrayValueRead, + lowerSafeIndexRead, } from "./array-values.js"; import { lowerForAwaitGenerator, @@ -96,7 +97,6 @@ import { lowerForOfSearchParams, lowerForOfSet, } from "./containers/for-of.js"; -import { lowerSafeIndexRead } from "./containers/array-methods.js"; import { objectIterOverIndexShape } from "./containers/indexed-objects.js"; import { strCharsCall } from "./containers/array-construction.js"; import { @@ -4691,10 +4691,13 @@ export function lowerVarDecl( const arithmetic = raw.type.kind === "union" ? lowerer.unions.get(raw.type.unionId)?.arms : undefined; if ( - g.type.kind === "string" && arithmetic?.length === 2 && - arithmetic.some((a) => a.kind === "f64") && - arithmetic.some((a) => a.kind === "string") + arithmetic.every((a) => a.kind === "f64" || a.kind === "string" || a.kind === "bool") && + (lowerer.runtimeOptionalArithmeticTypes.has(decl.initializer) || + lowerer.runtimeOptionalBindingType(decl.name)?.kind === "union" || + (g.type.kind === "string" && + arithmetic.some((a) => a.kind === "f64") && + arithmetic.some((a) => a.kind === "string"))) ) { g.type = raw.type; lowerer.runtimeOptionalArithmeticGlobals.add(g); @@ -5107,9 +5110,13 @@ export function lowerVarDecl( ); }; const runtimeStringArithmetic = - settledType.kind === "string" && - ((arithmeticType && typeEquals(arithmeticType, init.type)) || - isStringArithmeticUnion(init.type)); + (arithmeticType !== undefined && typeEquals(arithmeticType, init.type)) || + (settledType.kind === "string" && isStringArithmeticUnion(init.type)) || + (init.type.kind === "union" && + typeEquals(lowerer.runtimeOptionalBindingType(decl.name, settledType), init.type) && + lowerer.unions + .get(init.type.unionId) + ?.arms.every((a) => a.kind === "f64" || a.kind === "string" || a.kind === "bool") === true); if (runtimeStringArithmetic) { settledType = init.type; } @@ -5255,7 +5262,7 @@ export function lowerVarDecl( // subtyping (`const p: {a: number} = wider;`) is rejected, not coerced. init = lowerer.coerceInto(decl.initializer, init, settledType); const local = lowerer.declareLocal(decl.name, decl.name.text, settledType, isLet); - if (runtimeStringArithmetic && isStringArithmeticUnion(init.type)) { + if (runtimeStringArithmetic && init.type.kind === "union") { lowerer.runtimeOptionalArithmeticLocals.add(lowerer.runtimeOptionalRootOf(local)); } if (runtimeOptional) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index c71007209..d73d83370 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,3 +1,5 @@ +import { isOptionalProcessStreamProperty } from "./builtins/process.js"; +import { concreteCollectionNarrow } from "./collection-narrowing.js"; import { lowerWorkerMetadata } from "./builtins/workers.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js"; import type { FieldLift } from "./coercions/structural-plans.js"; @@ -2249,6 +2251,8 @@ export class Lowerer { /** Named method slots observed before class collection need a callable * ABI and shared prototype descriptors instead of call-site specialization. */ readonly prototypeMethodAccesses = new Map(); + /** Field names whose complete source census stores only lexical arrows. */ + readonly receiverFreeCallbackFields = new Set(); /** Inferred JS methods participating in an override chain keep a vtable * ABI instead of call-site specialization. Filled before class collection. */ readonly virtualJsMethods = new Set(); @@ -3628,6 +3632,7 @@ export class Lowerer { const optionalSymbols = new Set(); const optionalReturns = new Set(); const arithmeticReturns = new Map(); + const primitiveBindings = new Map(); const optionalParams = new Map>(); const optionalFields = new Map>(); const dynamicObjectEntryRows = new Set(); @@ -3758,10 +3763,11 @@ export class Lowerer { } return false; }; - const isArrayRead = (node: ts.Expression): boolean => { + const isIndexedRead = (node: ts.Expression): boolean => { const e = peel(node); if (!ts.isElementAccessExpression(e)) return false; - return this.mapTypeOf(this.typeOf(e.expression))?.kind === "array"; + const kind = this.mapTypeOf(this.typeOf(e.expression))?.kind; + return kind === "array" || kind === "string"; }; const isDynamicObjectEntryRead = (node: ts.Expression): boolean => { const read = peel(node); @@ -3796,7 +3802,8 @@ export class Lowerer { // function/global ABI to `T | undefined`. if (explicitlyNonNull(node)) return false; const e = peel(node); - if (isArrayRead(e)) return true; + if (isIndexedRead(e)) return true; + if (ts.isPropertyAccessExpression(e) && isOptionalProcessStreamProperty(this, e)) return true; if (ts.isCallExpression(e) && this.runtimeOptionalReduceTypes.has(e)) return true; if ( ts.isCallExpression(e) && @@ -3848,28 +3855,43 @@ export class Lowerer { } return false; }; - const optionalStringArithmeticType = (node: ts.Expression): IrType | null => { + const optionalPrimitiveResultType = (node: ts.Expression): IrType | null => { const e = peel(node); - if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.PlusToken) + if (ts.isIdentifier(e)) { + const symbol = symbolOf(e); + return symbol ? (primitiveBindings.get(symbol) ?? null) : null; + } + if (ts.isCallExpression(e)) { + const symbol = callableSymbolOf(e.expression); + return symbol ? (arithmeticReturns.get(symbol) ?? null) : null; + } + if (!ts.isBinaryExpression(e)) return null; + if (e.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken && mayBeOptional(e.left)) { + const left = this.mapTypeOf(this.typeOf(e.left)); + const right = this.mapTypeOf(this.typeOf(e.right)); + const primitive = (t: IrType | null): t is IrType => + t?.kind === "f64" || t?.kind === "string" || t?.kind === "bool"; + if (primitive(left) && primitive(right) && !typeEquals(left, right)) { + const arms = [left, right].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + return { kind: "union", unionId: this.unions.intern(arms) }; + } return null; - const stringArrayRead = (part: ts.Expression): boolean => { + } + if (e.operatorToken.kind !== ts.SyntaxKind.PlusToken) return null; + const stringRead = (part: ts.Expression): boolean => { const p = peel(part); if (!ts.isElementAccessExpression(p)) return false; const t = this.mapTypeOf(this.typeOf(part)); if (t?.kind !== "string") return false; const recv = this.mapTypeOf(this.typeOf(p.expression)); - return recv?.kind === "array" && recv.elem.kind === "string"; + return recv?.kind === "string" || (recv?.kind === "array" && recv.elem.kind === "string"); }; const primitive = (part: ts.Expression): boolean => { const t = this.mapTypeOf(this.typeOf(part)); return t?.kind === "f64" || t?.kind === "string"; }; - if (!stringArrayRead(e.left) && !stringArrayRead(e.right)) return null; - if ( - !(primitive(e.left) || stringArrayRead(e.left)) || - !(primitive(e.right) || stringArrayRead(e.right)) - ) - return null; + if (!stringRead(e.left) && !stringRead(e.right)) return null; + if (!primitive(e.left) || !primitive(e.right)) return null; return { kind: "union", unionId: this.unions.intern([F64, STRING]) }; }; const noteField = (decl: ts.VariableDeclaration, name: string): boolean => { @@ -4181,6 +4203,16 @@ export class Lowerer { const scan = (node: ts.Node): void => { if (ts.isVariableDeclaration(node)) { if (node.initializer) { + const result = optionalPrimitiveResultType(node.initializer); + const symbol = ts.isIdentifier(node.name) ? symbolOf(node.name) : null; + if (result?.kind === "union" && symbol) { + const before = primitiveBindings.get(symbol); + if (!before || !typeEquals(before, result)) { + primitiveBindings.set(symbol, result); + this.runtimeOptionalBindingTypes.set(symbol, result); + changed = true; + } + } if (ts.isIdentifier(node.name) && isDynamicObjectEntryRead(node.initializer)) { const symbol = symbolOf(node.name); if (symbol && !dynamicObjectEntryRows.has(symbol)) { @@ -4472,7 +4504,7 @@ export class Lowerer { optionalReturns.add(symbol); changed = true; } - const arithmetic = optionalStringArithmeticType(expression); + const arithmetic = optionalPrimitiveResultType(expression); if (arithmetic && !arithmeticReturns.has(symbol)) { arithmeticReturns.set(symbol, arithmetic); changed = true; @@ -4887,7 +4919,7 @@ export class Lowerer { this.diags.length > 0 ? null : { - irVersion: 14, + irVersion: 15, sourceFile: this.entry.fileName, functions, classes: artifacts.classes, @@ -5974,6 +6006,8 @@ export class Lowerer { const arm = sym !== undefined ? this.aliasNarrowTypes.get(sym) : undefined; if (arm !== undefined) return arm; } + const collection = concreteCollectionNarrow(this, node, t); + if (collection) return collection; // IMPLICIT-ANY instance bodies: an identifier reference to a BOUND // param answers the call site's concrete type wherever the checker // still says `any` (there is no `T` for mapType to substitute — the diff --git a/packages/compiler/src/frontend/lowering/sanitize-class-types.test.ts b/packages/compiler/src/frontend/lowering/sanitize-class-types.test.ts index 31d51d7d7..d80499ef3 100644 --- a/packages/compiler/src/frontend/lowering/sanitize-class-types.test.ts +++ b/packages/compiler/src/frontend/lowering/sanitize-class-types.test.ts @@ -21,7 +21,7 @@ test("fenced instance slots are erased throughout signatures, storage and nested }; const ref = (type: IrType): IrExpr => ({ kind: "varRef", localId: "x", type, loc }); const module: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", globals: [{ id: "g", name: "g", type: composite, mutable: false }], @@ -110,7 +110,7 @@ test("modules without optional tables and registered class types retain their st const loc = { file: "registered.ts", start: 0, end: 1 }; const type: IrType = { kind: "object", className: "Registered" }; const module: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", functions: [ diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 55453eddb..8080b0fed 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1514,6 +1514,9 @@ export const BUILTIN_MODULE_FNS: Record< isUtf8: { fn: "buffer.isUtf8", params: [DYN], result: BOOL }, transcode: { fn: "buffer.transcode", params: [DYN, DYN, DYN], result: BYTES_U8 }, }, + tty: { + isatty: { fn: "process.isTTY", params: [F64], result: BOOL }, + }, process: { loadEnvFile: { fn: "process.loadEnvFile", params: [DYN], result: VOID }, }, diff --git a/packages/compiler/src/frontend/type-mapper.test.ts b/packages/compiler/src/frontend/type-mapper.test.ts index dbfa8a5ce..cf78554b4 100644 --- a/packages/compiler/src/frontend/type-mapper.test.ts +++ b/packages/compiler/src/frontend/type-mapper.test.ts @@ -73,12 +73,30 @@ describe("nullable collection union builders", () => { }, ); - test("collection generators retain the data-sibling refusal", () => { - expect( - genResultRecord(mapOf(STRING, F64), STRING, new ShapeRegistry(), new UnionRegistry()), - ).toBeNull(); + test.each([ + [mapOf(STRING, F64), STRING], + [setOf(STRING), F64], + ])("collection generator results retain each data arm", (yieldT, retT) => { + const shapes = new ShapeRegistry(); + const unions = new UnionRegistry(); + const result = genResultRecord(yieldT, retT, shapes, unions); + const value = + result && shapes.get(result.shapeId)?.fields.find((field) => field.name === "value")?.type; + expect(value?.kind).toBe("union"); + if (value?.kind !== "union") throw new Error("missing value union"); + expect(unions.get(value.unionId)?.arms).toEqual( + expect.arrayContaining([yieldT, retT, UNDEFINED_T]), + ); + }); + + test("promise generators retain the data-sibling refusal", () => { expect( - genResultRecord(setOf(STRING), F64, new ShapeRegistry(), new UnionRegistry()), + genResultRecord( + { kind: "promise", inner: STRING }, + F64, + new ShapeRegistry(), + new UnionRegistry(), + ), ).toBeNull(); }); }); diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 7168390b0..72933b90e 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1550,6 +1550,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // runtime-bearing class, callable, indexed, and non-literal data shapes // remain intersections with no representation. if (widened.isIntersectionType()) { + const errorMetadata = mapErrorMetadataIntersection(widened, ctx); + if (errorMetadata) return errorMetadata; const brandedPrimitive = mapBrandedPrimitiveIntersection(widened, ctx); if (brandedPrimitive) return brandedPrimitive; } @@ -2000,7 +2002,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // (fs/exec throw sites stamp `code`), so the type maps to the %Error // root — `err as NodeJS.ErrnoException` from an Error-typed value is a // no-op cast, error-typed listener params accept it, and the `.code` - // read has its own lowering (errno/syscall/path stay per-member fences). + // reads have dedicated lowerings that preserve absent metadata. if (isStdlibInterface("ErrnoException")) return { kind: "object", className: "%Error" }; // spawn consumes a presence-sensitive option bag, including optional // stdio and env values. Keep it checked-dynamic for native normalization. @@ -2445,7 +2447,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "tty"), + (isDeclaredInAmbientModule(d, "tty") || + (isDeclaredInAmbientModule(d, "process") && + isDeclaredInAmbientNamespace(d, "NodeJS"))), )) || (psym?.name === "WritableStream" && checker @@ -2568,8 +2572,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // NodeJS.ErrnoException: @types/node's `interface ErrnoException extends // Error` (dns/fs callback error types). It IS an Error at runtime — map // it to the runtime %Error hierarchy like the lib Error interfaces - // above; the optional errno/code/path/syscall extras fence per member - // (the .stack stance). Provenance-checked like the rest. + // above; optional metadata uses the same checked property view. + // Provenance-checked like the rest. if ( psym?.name === "ErrnoException" && checker @@ -4162,7 +4166,7 @@ export function unitOnlyUnion(unions: UnionRegistry): IrType { * answers undefined). ONE shape per channel pair — `g.next()`'s lowering, * `.return()`, `.throw()`, the for-of desugar, and mapType's * IteratorResult alias mapping all intern through here, so reads agree. - * Null when the combined union would be illegal (a container arm beside + * Null when the combined union would be illegal (a promise arm beside * data arms, a regex/Date arm — kinds with no narrowing test here): such * generators stay unmapped. */ export function genResultRecord( @@ -4191,7 +4195,7 @@ export function genResultRecord( if (!add(yieldT) || !add(retT)) return null; byKey.set(typeKey(UNDEFINED_T), UNDEFINED_T); const arms = [...byKey.values()]; - // Containers keep the shared nullable-only rule. Generator function + // Promises keep the shared nullable-only rule. Generator function // payloads retain their existing unit-only boundary too. if ( !unionContainerArmsOk(arms) || @@ -4329,6 +4333,62 @@ function mapHybridCallableIntersection(widened: ts.Type, ctx: TypeMapperCtx): Ir return { kind: "record", shapeId: shapes.intern(fields, false, undefined, declaredOrder) }; } +/** Optional standard error metadata refines an existing Error identity; + * it does not allocate a structural copy or extend its native layout. */ +function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType | null { + const parts = ts.constituentTypes(type); + // Do not map unrelated intersections speculatively: mapping recursive + // structural types can intern their representation before it is complete. + const hasError = parts.some((part) => { + const symbol = part.getSymbol(); + return ( + symbol !== undefined && + [ + "Error", + "EvalError", + "RangeError", + "ReferenceError", + "SyntaxError", + "TypeError", + "URIError", + "AggregateError", + ].includes(symbol.name) && + ctx.checker.declarationsOf(symbol).some((decl) => ctx.isStdlibFile(decl.getSourceFile())) + ); + }); + if (!hasError) return null; + let error: IrType | null = null; + for (const part of parts) { + const mapped = mapType(part, ctx); + if (mapped?.kind === "object" && RUNTIME_ERROR_CLASSES.has(mapped.className)) { + if (error && !typeEquals(error, mapped)) return null; + error = mapped; + continue; + } + if ( + ctx.checker.getCallSignatures(part).length || + ctx.checker.getConstructSignatures(part).length || + ctx.checker.getIndexInfosOfType(part).length + ) + return null; + for (const property of ctx.checker.getPropertiesOfType(part)) { + const expected = + property.name === "errno" + ? "f64" + : ["code", "syscall", "path", "dest"].includes(property.name) + ? "string" + : null; + if (!expected || !(property.flags & ts.SymbolFlags.Optional)) return null; + const field = mapType(ctx.checker.getTypeOfSymbol(property), ctx); + const arms = + field?.kind === "union" ? ctx.unions.get(field.unionId)?.arms : field ? [field] : []; + if (!arms?.length || !arms.every((arm) => arm.kind === expected || arm.kind === "undefinedT")) + return null; + } + } + return error; +} + /** A conventional primitive brand has one runtime-carrying primitive part * and one or more phantom object parts whose fields are literal marker * values. The marker can use a string or unique-symbol key, can be imported @@ -4971,10 +5031,7 @@ function armHasUnionHome(arm: IrType, siblingCount: number): boolean { case "generator": case "dyn": return false; - // Containers map only beside unit siblings; data siblings have no - // supported runtime narrowing test against them. - case "map": - case "set": + // Promise data siblings still lack a supported narrowing contract. case "promise": return siblingCount === 0; default: diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 03d4a3a91..d8ee6df86 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3395,9 +3395,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "fs.accessSync": { argTypes: [STRING, F64], result: VOID }, "fs.readFdSync": { argTypes: [F64, STRING], result: STRING }, "fs.readFdSyncBytes": { argTypes: [F64], result: BYTES_U8 }, - /** isatty(3) over an fd literal (0/1/2 — process.stdin/stdout/stderr - * .isTTY reads). A real boolean: false where Node's non-TTY streams - * expose undefined (documented divergence). Never throws. */ + /** Validated isatty(3) probe. Stream property lowering maps false to + * undefined; tty.isatty exposes the boolean result directly. */ "process.isTTY": { argTypes: [F64], result: BOOL }, // Like process.envGet: the result is the module's interned // `number | undefined` union — checked by arms in the libCall case. diff --git a/packages/compiler/src/ir/constant-tables.test.ts b/packages/compiler/src/ir/constant-tables.test.ts index c1bc9a094..454880274 100644 --- a/packages/compiler/src/ir/constant-tables.test.ts +++ b/packages/compiler/src/ir/constant-tables.test.ts @@ -20,7 +20,7 @@ const expr = (value: IrExpr): IrStmt => ({ kind: "exprStmt", expr: value, loc }) function fixture(): IrModule { return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", globals: [{ id, name: "table", type, mutable: false }], diff --git a/packages/compiler/src/ir/initializer-bindings.test.ts b/packages/compiler/src/ir/initializer-bindings.test.ts index 6891935c5..d810236b4 100644 --- a/packages/compiler/src/ir/initializer-bindings.test.ts +++ b/packages/compiler/src/ir/initializer-bindings.test.ts @@ -36,7 +36,7 @@ function fixture(): IrModule { ], }; return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: fn.name, functions: [fn], diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index cb5515f73..5a845f2b5 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -771,15 +771,12 @@ export function funcOf(params: IrType[], ret: IrType): IrType { return { kind: "func", params, ret }; } -/** Maps, sets and promises may share a union with null/undefined only. - * Unit tag tests can narrow nullable containers without losing identity; - * arbitrary data siblings require a separate runtime narrowing operation. - * Share this rule across checker mapping, synthesized unions and validation. */ +/** Maps and sets use tagged union storage and retain their native identity. + * Promises still require nullable-only unions until their data-arm narrowing + * and await boundary have a matching contract. */ export function unionContainerArmsOk(arms: IrType[]): boolean { return arms.every( - (a, i) => - (a.kind !== "map" && a.kind !== "set" && a.kind !== "promise") || - arms.every((b, j) => j === i || isUnitType(b)), + (a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b)), ); } @@ -903,7 +900,7 @@ export function isRefCounted(t: IrType): boolean { export interface IrModule { /** Bumped on any breaking IR change; serialize.ts refuses mismatches. */ - irVersion: 14; + irVersion: 15; sourceFile: string; functions: IrFunction[]; /** Class shapes. Constructors and methods are ordinary module functions diff --git a/packages/compiler/src/ir/scalar-records.test.ts b/packages/compiler/src/ir/scalar-records.test.ts index 5b632639b..0694c12eb 100644 --- a/packages/compiler/src/ir/scalar-records.test.ts +++ b/packages/compiler/src/ir/scalar-records.test.ts @@ -40,7 +40,7 @@ const read = (localId: string, field = "a"): IrExpr => ({ function fixture(): IrModule { return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", records: [ diff --git a/packages/compiler/src/ir/serialize.ts b/packages/compiler/src/ir/serialize.ts index cb5738135..b156300ea 100644 --- a/packages/compiler/src/ir/serialize.ts +++ b/packages/compiler/src/ir/serialize.ts @@ -2,9 +2,9 @@ * JSON's null/zero spellings must not erase NaN, infinities or negative zero. */ import type { IrModule } from "./ir.js"; -// Version 14 records worker execution, which requires thread-local program -// state and matching runtime objects. Older readers must not erase that mode. -export const IR_VERSION = 14 as const; +// Version 15 expands the native Error prefix with filesystem metadata. +// Older modules embed incompatible subclass offsets and must be rejected. +export const IR_VERSION = 15 as const; /** IR artifacts are plain data. Scan each function once before deciding * whether its JSON encoding needs the complete-number-domain replacer. */ diff --git a/packages/compiler/src/ir/validate.test.ts b/packages/compiler/src/ir/validate.test.ts index e6f240f87..956e08cd8 100644 --- a/packages/compiler/src/ir/validate.test.ts +++ b/packages/compiler/src/ir/validate.test.ts @@ -283,7 +283,7 @@ test.each([mapOf(STRING, F64), setOf(STRING), { kind: "promise", inner: F64 } as }, ); -test.each([mapOf(STRING, F64), setOf(STRING), { kind: "promise", inner: F64 } as IrType])( +test.each([{ kind: "promise", inner: F64 } as IrType])( "%j payloads still refuse unrelated data siblings", (type) => { const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [ @@ -295,13 +295,12 @@ test.each([mapOf(STRING, F64), setOf(STRING), { kind: "promise", inner: F64 } as }, ); -test("two differently typed Map payloads cannot silently share one tag test", () => { +test("differently typed collection payloads preserve separate union tags", () => { const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [ { id: "maps", arms: [mapOf(STRING, F64), mapOf(STRING, STRING), UNDEFINED_T] }, ]); - expect( - validateModule(mod).filter((error) => error.message.includes("beside non-unit arms")), - ).toHaveLength(2); + expect(validateModule(mod)).toEqual([]); + expect(deserializeModule(serializeModule(mod))).toEqual(mod); }); function numericReadModule(overrides: Partial = {}): IrModule { @@ -315,7 +314,7 @@ function numericReadModule(overrides: Partial ...overrides, }; return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", functions: [ @@ -399,7 +398,7 @@ test.each(["sortPrimitive", "toSortedPrimitive"] as const)( function expressionModule(expr: IrExpr, unions: IrUnionDef[]): IrModule { return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", unions, @@ -1286,7 +1285,7 @@ test.each([ function tdzModule(mutable = true): IrModule { const value: IrExpr = { kind: "numLit", value: 0, type: F64, loc }; return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", functions: [ @@ -1454,7 +1453,7 @@ test("TDZ locals require a shared box", () => { function discriminatedModule(): IrModule { return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [], loc }], diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index b1814019a..856f452e4 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -804,9 +804,8 @@ export function validateModule(mod: IrModule): IrValidationError[] { } u.arms.forEach((arm, i) => { // The unit kinds (undefinedT/nullT) are valid arms — union membership - // is the ONLY place they may appear. Containers are valid beside - // unit arms: tag tests distinguish absence while the reference - // payload preserves identity. The shared rule refuses data siblings. + // is the ONLY place they may appear. Container payloads preserve + // identity; promise data siblings retain their separate fence. if ( arm.kind === "void" || arm.kind === "union" || @@ -817,10 +816,7 @@ export function validateModule(mod: IrModule): IrValidationError[] { ) { errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc }); } - if ( - (arm.kind === "map" || arm.kind === "set" || arm.kind === "promise") && - !unionContainerArmsOk(u.arms) - ) { + if (arm.kind === "promise" && !unionContainerArmsOk(u.arms)) { errors.push({ message: `union ${u.id}: ${arm.kind} arm ${i} beside non-unit arms`, loc: noLoc, @@ -1117,14 +1113,15 @@ function validateFunction( if (isUnitType(fn.returnType)) { err(`return type is bare unit type ${fn.returnType.kind}`, fn.loc); } - // Exception snapshots may be captured, but are never public parameters - // or return values. Their payload crosses those boundaries explicitly. + // Exception snapshots may be captured or passed to internal conversion + // helpers. They are not source-level types and never return as values. if (fn.returnType.kind === "caught") err("return type is caught", fn.loc); for (const p of fn.params) { + if (p.type.kind === "caught" && fn.name !== "%caught.dynamicValue") + err(`param "${p.name}" is caught-typed`, fn.loc); if (!locals.has(p.localId)) { err(`param "${p.name}" has no local entry "${p.localId}"`, fn.loc); } - if (p.type.kind === "caught") err(`param "${p.name}" is caught-typed`, fn.loc); } for (const c of [...(fn.captures ?? []), ...(fn.classCaptures ?? [])]) { const local = locals.get(c.localId); diff --git a/packages/compiler/src/library/fence-eval.test.ts b/packages/compiler/src/library/fence-eval.test.ts index cbffca9c7..80511e970 100644 --- a/packages/compiler/src/library/fence-eval.test.ts +++ b/packages/compiler/src/library/fence-eval.test.ts @@ -15,7 +15,7 @@ function program(member: string): IrModule { loc, }; return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "probe", functions: [ diff --git a/packages/compiler/src/library/int-infer.test.ts b/packages/compiler/src/library/int-infer.test.ts index d06dd32cd..2822ca00b 100644 --- a/packages/compiler/src/library/int-infer.test.ts +++ b/packages/compiler/src/library/int-infer.test.ts @@ -97,7 +97,7 @@ const sink = (name: string): IrFunction => ({ /** A module holding the case function plus the two declared sinks. */ function caseModule(params: string[], locals: string[], body: IrStmt[]): IrModule { return { - irVersion: 14, + irVersion: 15, sourceFile: "corpus.ts", functions: [ sink("send"), @@ -196,7 +196,7 @@ const RECORD_CFG: IntSlotConfig = { function recordCase(body: IrStmt[], names = ["m"], extraFns: IrFunction[] = []): IrModule { return { - irVersion: 14, + irVersion: 15, sourceFile: "fields.ts", functions: [ ...extraFns, @@ -237,7 +237,7 @@ const classCountRead = (): IrExpr => ({ function onlyOrdinaryClass(body: IrStmt[]): IntVerdict { const mod: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: "class-fields.ts", functions: [ sink("send"), @@ -581,7 +581,7 @@ describe("the domain's edges beyond the corpus", () => { loc, }; const mod: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: "optional.ts", functions: [ { diff --git a/packages/compiler/src/library/library-cache.test.ts b/packages/compiler/src/library/library-cache.test.ts index 4e98e6d94..d93db0c5b 100644 --- a/packages/compiler/src/library/library-cache.test.ts +++ b/packages/compiler/src/library/library-cache.test.ts @@ -261,7 +261,7 @@ test("semantic library cache restores and rebases IR after a comment-only edit", const sourceBefore = await readFile(f.source, "utf8"); const returnStart = sourceBefore.indexOf("return"); const semanticMod = { - irVersion: 14, + irVersion: 15, sourceFile: f.source, functions: [ { @@ -324,7 +324,7 @@ test("semantic library cache refuses token and directive edits", async () => { const f = await fixture(); const sourceBefore = await readFile(f.source, "utf8"); const semanticMod = { - irVersion: 14, + irVersion: 15, sourceFile: f.source, functions: [ { @@ -372,7 +372,7 @@ test("semantic LLVM cache accepts comment edits that shift source lines", async const f = await fixture(); const sourceBefore = await readFile(f.source, "utf8"); const semanticMod = { - irVersion: 14, + irVersion: 15, sourceFile: f.source, functions: [ { @@ -447,7 +447,7 @@ test("semantic LLVM cache accepts line separator normalization", async () => { frontend: tracker.snapshot(), semantic: { mod: { - irVersion: 14, + irVersion: 15, sourceFile: f.source, functions: [], entry: "__main", @@ -468,7 +468,7 @@ test("semantic LLVM cache rebases comment edits in multi-source graphs", async ( const importedSource = "export function helper(): number { return 1; }\n"; await writeFile(imported, importedSource); const semanticMod = { - irVersion: 14, + irVersion: 15, sourceFile: f.source, functions: [ { diff --git a/packages/compiler/src/native/compiler.test.ts b/packages/compiler/src/native/compiler.test.ts index 7adde68a2..2c2afcc10 100644 --- a/packages/compiler/src/native/compiler.test.ts +++ b/packages/compiler/src/native/compiler.test.ts @@ -60,7 +60,7 @@ test.each([ writeFileSync(output, "previous executable"); writeFileSync(llvmPath, "previous LLVM"); const module: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: entry, entry: "%main", functions: [ @@ -197,7 +197,7 @@ test.each([false, true])( writeFileSync(output, "previous object"); writeFileSync(llvmPath, "previous LLVM"); const module: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: entry, entry: "%main", functions: [ diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 565e627ce..75645d769 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1975,6 +1975,12 @@ "status": "static", "note": "recognized module (bare and node:-prefixed specifiers)" }, + { + "id": "node-builtin.tty.isatty", + "kind": "node-builtin", + "name": "tty.isatty", + "status": "static" + }, { "id": "node-builtin.url", "kind": "node-builtin", diff --git a/packages/compiler/test/bytes-element-emission.test.ts b/packages/compiler/test/bytes-element-emission.test.ts index 9c40b0aeb..953687993 100644 --- a/packages/compiler/test/bytes-element-emission.test.ts +++ b/packages/compiler/test/bytes-element-emission.test.ts @@ -75,7 +75,7 @@ function fixture(): IrModule { ); return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "__main", functions: [{ name: "__main", params: [], returnType: VOID, locals, body, loc }], @@ -145,7 +145,7 @@ function receiverReassignmentFixture(): IrModule { ]; return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "__main", functions: [{ name: "__main", params: [], returnType: VOID, locals, body, loc }], @@ -212,7 +212,7 @@ function integerLoopFixture(mutatesIndex = false): IrModule { { kind: "bytesSet", arr: bytesRef(), index: indexRef(), value: ref("sum"), loc }, ); return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "__main", functions: [ @@ -333,7 +333,7 @@ test("large record clones stay outlined while small clones remain inlineable", ( const fields = Array.from({ length: count }, (_, i) => ({ name: `f${i}`, type: F64 })); const type = { kind: "record", shapeId: id } as const; return { - irVersion: 14, + irVersion: 15, sourceFile: "record-clone.ts", entry: "__main", records: [{ id, fields }], diff --git a/packages/compiler/test/emit-llvm.test.ts b/packages/compiler/test/emit-llvm.test.ts index 67f6ef798..0f8d85c34 100644 --- a/packages/compiler/test/emit-llvm.test.ts +++ b/packages/compiler/test/emit-llvm.test.ts @@ -35,7 +35,7 @@ test("strings: literals, concat in a loop, toString, RC-clean under audit", asyn // while (i < 3) { acc = acc + ("-" + i); i = i + 1; } // console.log(acc, acc === "x-0-1-2", "α∂" < "β"); const mod: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: "s.ts", entry: "__main", functions: [ @@ -137,7 +137,7 @@ test("short-circuit: right operand of && only evaluates when left is true", asyn // if (false && sideEffect()) {} ; if (true || sideEffect()) {} // console.log("done") const mod: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: "l.ts", entry: "__main", functions: [ @@ -227,7 +227,7 @@ test("string params: callee owns and releases; returns transfer ownership", asyn // function greet(who: string): string { return "hi " + who; } // console.log(greet("world")); const mod: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: "p.ts", entry: "__main", functions: [ diff --git a/packages/compiler/test/fixtures/fib-ir.ts b/packages/compiler/test/fixtures/fib-ir.ts index f1a4a4218..5d68f8d78 100644 --- a/packages/compiler/test/fixtures/fib-ir.ts +++ b/packages/compiler/test/fixtures/fib-ir.ts @@ -17,7 +17,7 @@ const n = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, lo const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); export const fibModule: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: "fib.ts", entry: "__main", functions: [ diff --git a/packages/compiler/test/fromcharcode-emission.test.ts b/packages/compiler/test/fromcharcode-emission.test.ts index 4ddccbe67..2b4fb75d5 100644 --- a/packages/compiler/test/fromcharcode-emission.test.ts +++ b/packages/compiler/test/fromcharcode-emission.test.ts @@ -7,7 +7,7 @@ test("scalar fromCharCode avoids a heap argument pack in LLVM", () => { const loc = { file: "scalar-char.ts", start: 0, end: 0 }; const value: IrExpr = { kind: "numLit", value: 65, type: F64, loc }; const mod: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "__main", globals: [], diff --git a/packages/compiler/test/ir.test.ts b/packages/compiler/test/ir.test.ts index e8e49128a..2fd3c43f5 100644 --- a/packages/compiler/test/ir.test.ts +++ b/packages/compiler/test/ir.test.ts @@ -22,7 +22,7 @@ test("fib module JSON round-trips", () => { test("validator rejects type mismatches and bad references", () => { const loc = { file: "t.ts", start: 0, end: 0 }; const bad: IrModule = { - irVersion: 14, + irVersion: 15, sourceFile: "t.ts", entry: "__main", functions: [ @@ -108,7 +108,7 @@ test("serializer round-trips ±Infinity and NaN", () => { }); test("deserializer rejects the previous IR version", () => { - const json = serializeModule(fibModule).replace('"irVersion": 14', '"irVersion": 13'); + const json = serializeModule(fibModule).replace('"irVersion": 15', '"irVersion": 13'); expect(() => deserializeModule(json)).toThrow(/version mismatch/); }); diff --git a/packages/compiler/test/llvm-lib-dispatch.test.ts b/packages/compiler/test/llvm-lib-dispatch.test.ts index a92e93948..31204299c 100644 --- a/packages/compiler/test/llvm-lib-dispatch.test.ts +++ b/packages/compiler/test/llvm-lib-dispatch.test.ts @@ -19,7 +19,7 @@ const dyn = (): IrExpr => ({ kind: "dynFrom", value: str("value"), type: DYN, lo function libCallModule(fn: IrLibFn, args: IrExpr[], type: IrType): IrModule { return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "__main", functions: [ diff --git a/packages/compiler/test/native-callback-emission.test.ts b/packages/compiler/test/native-callback-emission.test.ts new file mode 100644 index 000000000..33d02d313 --- /dev/null +++ b/packages/compiler/test/native-callback-emission.test.ts @@ -0,0 +1,83 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule, validateModule } from "../src/index.js"; + +async function lower(source: string) { + const dir = mkdtempSync(join(tmpdir(), "scriptc-callback-emission-")); + try { + const entry = join(dir, "main.ts"); + const output = join(dir, "main.ir.json"); + writeFileSync(entry, source); + const result = await compile(entry, { outputKind: "ir", outDir: dir, outPath: output }); + if (!result.ok) throw new Error(JSON.stringify(result.diagnostics)); + const module = deserializeModule(readFileSync(output, "utf8")); + expect(validateModule(module)).toEqual([]); + return module; + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +test("native callback fields and unchanged methods do not demand class reflection", async () => { + const source = Array.from( + { length: 12 }, + (_, i) => ` +class Counter${i} { + value = 0; + notify(): void { this.value++; } + callback = (n: number): number => this.value + n; +} +const counter${i} = new Counter${i}(); +counter${i}.notify(); +console.log(counter${i}.callback(${i})); +`, + ).join("\n"); + const module = await lower( + source + + ` +const values: Record = {}; +function key(): string { return "notify"; } +values[key()] = 1; +function update(values: Record, name: string): void { values[name] = 2; } +update(values, key()); +console.log(values[key()]); +`, + ); + expect(module.functions.some((fn) => fn.name.startsWith("%dyn.class."))).toBe(false); + const json = JSON.stringify(module.functions); + expect(json).not.toContain('"kind":"dynCall"'); + expect(json).not.toContain('"kind":"dynFrom"'); + expect(json).toContain('"kind":"callValue"'); +}); + +test("own-key rebuilding and absent-key insertion preserve inherited methods", async () => { + const module = await lower(` +class Counter { value = 0; notify(): void { this.value++; } } +const counter = new Counter(); +function rebuild(value: Record): void { + const entries = Object.entries(value); + for (const key of Object.keys(value)) delete value[key]; + for (const [key, entry] of entries) if (!(key in value)) value[key] = entry; +} +const metadata: Record = { label: "ready" }; +rebuild(metadata); +counter.notify(); +console.log(metadata.label, counter.value); +`); + const initializers = module.functions.filter((fn) => fn.name.startsWith("%init.")); + expect(JSON.stringify(initializers)).toContain('"callee":"%Counter.notify"'); + expect(JSON.stringify(initializers)).not.toContain('"kind":"dynCall"'); +}); + +test("observable method replacement keeps class property dispatch", async () => { + const module = await lower(` +class Counter { value = 0; notify(): void { this.value++; } } +const counter = new Counter(); +Object.defineProperty(counter, "notify", { value: (): void => { counter.value += 10; } }); +counter.notify(); +console.log(counter.value); +`); + expect(module.functions.some((fn) => fn.name.startsWith("%dyn.class."))).toBe(true); +}); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 860d9d9a3..eec354147 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -312,7 +312,7 @@ ${[0, 1, 2, 3, 4, 5].map((index) => ` call void @add${index}(i64 ${index + 2})` expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v7"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v8"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -646,7 +646,7 @@ ${[0, 1, 2, 3, 4, 5].map((index) => ` call void @add${index}(i64 ${index + 2})` .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v7"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v8"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/string-accumulation-emission.test.ts b/packages/compiler/test/string-accumulation-emission.test.ts index 5e1f40099..b323221ba 100644 --- a/packages/compiler/test/string-accumulation-emission.test.ts +++ b/packages/compiler/test/string-accumulation-emission.test.ts @@ -86,7 +86,7 @@ function fixture(): IrModule { .find((fn) => fn.name === "negative")! .body.splice(1, 0, { kind: "varDecl", localId: "other", init: str("other"), loc }); return { - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "__main", globals: [{ id: "%g.e.acc", name: "globalAccumulator", type: STRING, mutable: true }], diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 1c25a7273..3581d2c9f 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -14637,6 +14637,48 @@ "/tests/corpus/native-worker-terminate-wait.ts" ], "diags": [] + }, + "/tests/corpus/caught-class-identity.ts": { + "order": [ + "/tests/corpus/caught-class-identity.ts" + ], + "diags": [] + }, + "/tests/corpus/caught-system-error-fields.ts": { + "order": [ + "/tests/corpus/caught-system-error-fields.ts" + ], + "diags": [] + }, + "/tests/corpus/collection-data-unions.ts": { + "order": [ + "/tests/corpus/collection-data-unions.ts" + ], + "diags": [] + }, + "/tests/corpus/native-callback-dispatch.ts": { + "order": [ + "/tests/corpus/native-callback-dispatch.ts" + ], + "diags": [] + }, + "/tests/corpus/terminal-stream-properties.ts": { + "order": [ + "/tests/corpus/terminal-stream-properties.ts" + ], + "diags": [] + }, + "/tests/corpus/typed-index-arguments.ts": { + "order": [ + "/tests/corpus/typed-index-arguments.ts" + ], + "diags": [] + }, + "/tests/fixtures/node-types/terminal-error-fields.ts": { + "order": [ + "/tests/fixtures/node-types/terminal-error-fields.ts" + ], + "diags": [] } } } diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 0812c1345..bf079ec6b 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -239,7 +239,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 7, marker: "scr_runtime_abi_v7" }, + runtime_abi: { version: 8, marker: "scr_runtime_abi_v8" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index c783c1a20..204ef2ff0 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -293,7 +293,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 7, marker: "scr_runtime_abi_v7" }, + runtime_abi: { version: 8, marker: "scr_runtime_abi_v8" }, compiler: { command: compiler, identity: compilerVersion, target: matrix.target.llvm_triple }, macros: { executable: ["SCR_DYNAMIC", "SCR_TEXT_DECODER_LEGACY"], diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index abe9ef0b1..a52bf5367 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -107,7 +107,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v7(void) {} +void scr_runtime_abi_v8(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_context.c b/packages/runtime/src/scr_context.c index bb735bc1b..759c96ff0 100644 --- a/packages/runtime/src/scr_context.c +++ b/packages/runtime/src/scr_context.c @@ -4,7 +4,7 @@ #include #include -void scr_runtime_workers_v7(void) {} +void scr_runtime_workers_v8(void) {} typedef struct ScrContextExit { void (*fn)(void); diff --git a/packages/runtime/src/scr_error.c b/packages/runtime/src/scr_error.c index ccc4040ec..882669555 100644 --- a/packages/runtime/src/scr_error.c +++ b/packages/runtime/src/scr_error.c @@ -49,6 +49,9 @@ static void scr_error_gcfree(void *obj) { scr_str_release(e->code); /* NULL-safe: absent on most errors */ scr_str_release(e->stack_frames); scr_str_release(e->stack); + scr_str_release(e->system_call); + scr_str_release(e->system_path); + scr_str_release(e->system_dest); if (e->error_cause && scr_error_cause_drop) scr_error_cause_drop(obj); scr_obj_free_note(); scr_cyc_free(e); @@ -80,6 +83,9 @@ static void scr_error_destroy(void *obj) { scr_str_release(e->code); scr_str_release(e->stack_frames); scr_str_release(e->stack); + scr_str_release(e->system_call); + scr_str_release(e->system_path); + scr_str_release(e->system_dest); if (e->error_cause && scr_error_cause_drop) scr_error_cause_drop(obj); scr_obj_free_note(); if (scr_error_traced) scr_cyc_free(e); diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index f43adf552..7af30c5e8 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -686,9 +686,9 @@ typedef struct { static SCR_TL ScrStdio scr_stdio_streams[3] = {{.fd = 0}, {.fd = 1}, {.fd = 2}}; enum { STDIO_WRITE, STDIO_ON, STDIO_ONCE, STDIO_OFF, STDIO_PAUSE, STDIO_RESUME, - STDIO_IS_PAUSED, STDIO_READ, STDIO_RAW, STDIO_DESTROY, STDIO_METHOD_COUNT }; + STDIO_IS_PAUSED, STDIO_READ, STDIO_RAW, STDIO_DESTROY, STDIO_WINDOW_SIZE, STDIO_METHOD_COUNT }; static const char *const scr_stdio_names[] = { - "write", "on", "once", "removeListener", "pause", "resume", "isPaused", "read", "setRawMode", "destroy" + "write", "on", "once", "removeListener", "pause", "resume", "isPaused", "read", "setRawMode", "destroy", "getWindowSize" }; static SCR_TL ScrDyn *scr_stdio_methods[STDIO_METHOD_COUNT]; static SCR_TL bool scr_stdio_initialized; @@ -852,7 +852,13 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc ScrStdio *stream = self->v.handle.ptr; ScrDyn *arg = argc ? args[0] : scr_dyn_undefined(); ScrDyn *result = NULL; - if (method == STDIO_WRITE && stream->fd != 0) { + if (method == STDIO_WINDOW_SIZE && stream->fd != 0) { + result = scr_dyn_new_arr(); + ScrDyn *columns = scr_dyn_new_num(scr_process_columns(stream->fd)); + ScrDyn *rows = scr_dyn_new_num(scr_process_rows(stream->fd)); + scr_dyn_arr_push(result, columns); + scr_dyn_arr_push(result, rows); + } else if (method == STDIO_WRITE && stream->fd != 0) { ScrDyn *encoding = argc > 1 ? args[1] : scr_dyn_undefined(); ScrDyn *callback = argc > 2 ? args[2] : scr_dyn_undefined(); if (encoding->kind == SCR_DYN_FUNC) { callback = encoding; encoding = scr_dyn_undefined(); } @@ -974,6 +980,7 @@ static ScrDyn *scr_stdio_get(void *ptr, const char *key, size_t length) { if (length == 11 && memcmp(key, "addListener", 11) == 0) return scr_dyn_retain(scr_stdio_methods[STDIO_ON]); for (int i = 0; i < STDIO_METHOD_COUNT; i++) { if (strlen(scr_stdio_names[i]) != length || memcmp(key, scr_stdio_names[i], length) != 0) continue; + if (i == STDIO_WINDOW_SIZE) return stream->fd != 0 && scr_process_is_tty(stream->fd) ? scr_dyn_retain(scr_stdio_methods[i]) : NULL; if (i == STDIO_WRITE) return stream->fd == 0 ? NULL : scr_dyn_retain(stream->write ? stream->write : scr_stdio_methods[i]); if (i == STDIO_RAW && !scr_process_is_tty(stream->fd)) return NULL; if (i >= STDIO_PAUSE && stream->fd != 0) return scr_stdio_refusal(scr_stdio_names[i]); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 563b5d822..00893e8f5 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4580,7 +4580,11 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) { d->v.obj.entries[d->v.obj.len - 1].enumerable = e->message_enumerable; } if (e->stack) scr_dyn_obj_set(d, "stack", 5, scr_dyn_new_str(e->stack)); + if (e->system_call) scr_dyn_obj_set(d, "errno", 5, scr_dyn_new_num(e->system_errno)); if (e->code) scr_dyn_obj_set(d, "code", 4, scr_dyn_new_str(e->code)); + if (e->system_call) scr_dyn_obj_set(d, "syscall", 7, scr_dyn_new_str(e->system_call)); + if (e->system_path) scr_dyn_obj_set(d, "path", 4, scr_dyn_new_str(e->system_path)); + if (e->system_dest) scr_dyn_obj_set(d, "dest", 4, scr_dyn_new_str(e->system_dest)); if (e->error_cause) { scr_dyn_obj_set(d, "cause", 5, scr_dyn_retain(e->error_cause)); d->v.obj.entries[d->v.obj.len - 1].enumerable = e->cause_enumerable; diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 66176d6b8..b6d2273cc 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -15,6 +15,7 @@ * scr_fs_exists never throws, like Node's existsSync. */ #include "scr_runtime.h" +#include "scr_system_errors.h" #include #ifdef SCR_WORKERS @@ -2316,6 +2317,25 @@ static const char *scr_fs_err_path(const ScrStr *path, char buf[PATH_MAX]) { #endif /* Exported (scr_runtime.h): scr_bytes.c's fs Buffer forms share it. */ +/* Keep metadata on the error itself until it crosses a checked-value + * boundary. Failed operations do not populate the process identity cache. */ +static void scr_fs_throw_details(int e, const char *name, const char *message, size_t length, + const char *op, const ScrStr *path, const ScrStr *dest) { + double number = e > 0 ? -e : e; +#define SCR_FS_ERRNO_VALUE(code, text) if (strcmp(name, #code) == 0) number = SCR_NODE_UV__##code; + SCR_NODE_UV_ERRNO_MAP(SCR_FS_ERRNO_VALUE) +#undef SCR_FS_ERRNO_VALUE + ScrStr *text = scr_str_new(message, length); + ScrError *error = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + scr_error_set_code(error, name); + error->system_errno = number; + error->system_call = scr_str_new(op, strlen(op)); + error->system_path = path ? scr_str_retain((ScrStr *)path) : NULL; + error->system_dest = dest ? scr_str_retain((ScrStr *)dest) : NULL; + scr_throw_obj(error, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + void scr_fs_throw(int e, const char *op, const ScrStr *path) { #ifdef _WIN32 /* The CRT lands ERROR_ACCESS_DENIED in errno as EACCES; libuv's @@ -2338,12 +2358,7 @@ void scr_fs_throw(int e, const char *op, const ScrStr *path) { int len = path ? snprintf(msg, cap, "%s: %s, %s '%s'", name, text, op, shown) : snprintf(msg, cap, "%s: %s, %s", name, text, op); - /* A real Error instance (name "Error", message = Node's text) — what a - * typed catch's `e instanceof Error` + `e.message` observes in Node — - * with `code` stamped to the errno name (the exotic-errno fallback - * stamps its "E" spelling; Node would carry the uv name there). - * errno/syscall/path stay unrepresented (SEMANTICS.md divergence 13). */ - scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, name); + scr_fs_throw_details(e, name, msg, (size_t)len, op, path, NULL); free(msg); } @@ -3198,7 +3213,7 @@ static double scr_fs_write_bytes(double fd, const void *data, size_t length, const char *text = scr_errno_text(e); char msg[160]; int len = snprintf(msg, sizeof msg, "%s: %s, write", name, text); - scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, name); + scr_fs_throw_details(e, name, msg, (size_t)len, "write", NULL, NULL); return 0; } return (double)n; @@ -3359,7 +3374,7 @@ double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length, const char *text = scr_errno_text(e); char msg[160]; int len = snprintf(msg, sizeof msg, "%s: %s, read", name, text); - scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, name); + scr_fs_throw_details(e, name, msg, (size_t)len, "read", NULL, NULL); return 0; } return (double)n; @@ -3440,7 +3455,7 @@ void scr_fs_close(double fd) { const char *text = scr_errno_text(e); char msg[160]; int len = snprintf(msg, sizeof msg, "%s: %s, close", name, text); - scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, name); + scr_fs_throw_details(e, name, msg, (size_t)len, "close", NULL, NULL); } } @@ -3459,7 +3474,7 @@ static void scr_fs_throw2(int e, const char *op, const ScrStr *src, const ScrStr scr_trap("scriptc: out of memory\n"); } int len = snprintf(msg, cap, "%s: %s, %s '%s' -> '%s'", name, text, op, shown_src, shown_dest); - scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, name); + scr_fs_throw_details(e, name, msg, (size_t)len, op, src, dest); free(msg); } @@ -3972,7 +3987,7 @@ static void scr_fs_throw_nopath(int e, const char *op) { const char *text = scr_errno_text(e); char msg[256]; int len = snprintf(msg, sizeof msg, "%s: %s, %s", name, text, op); - scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, name); + scr_fs_throw_details(e, name, msg, (size_t)len, op, NULL, NULL); } ScrStr *scr_fs_read_fd(double fd) { @@ -3991,7 +4006,10 @@ ScrBytes *scr_fs_read_fd_bytes(double fd) { /* ── the tty probes ──────────────────────────────────────────────────── */ -bool scr_process_is_tty(double fd) { return isatty((int)fd) != 0; } +bool scr_process_is_tty(double fd) { + if (!isfinite(fd) || fd < 0 || fd > INT_MAX || trunc(fd) != fd) return false; + return isatty((int)fd) != 0; +} /* Terminal geometry for process.stdout/stderr.columns/rows: ioctl(TIOCGWINSZ) on * the stream's fd, exactly Node's tty.WriteStream source of truth. A diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index c78e34560..4d3e53616 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -62,8 +62,8 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v7(void); -void scr_runtime_workers_v7(void); +void scr_runtime_abi_v8(void); +void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── * Every unrecoverable runtime trap — OOM, semantic range traps, internal- @@ -617,6 +617,10 @@ typedef struct ScrError { bool name_enumerable; ScrStr *stack_frames; /* captured native source frames */ ScrStr *stack; /* lazily formatted, cached stack */ + double system_errno; /* present exactly when system_call is non-NULL */ + ScrStr *system_call; + ScrStr *system_path; + ScrStr *system_dest; } ScrError; enum { @@ -654,6 +658,10 @@ typedef struct ScrDomException { bool name_enumerable; ScrStr *stack_frames; ScrStr *stack; + double system_errno; + ScrStr *system_call; + ScrStr *system_path; + ScrStr *system_dest; double dom_code; /* the WebIDL legacy code (0 when the name is off-table) */ bool has_cause; /* the options form carried a `cause` member */ struct ScrDyn *cause; /* owned; NULL when has_cause is false */ diff --git a/tests/corpus/1408-string-indexing.ts b/tests/corpus/1408-string-indexing.ts index 3648e0c5f..2c9525927 100644 --- a/tests/corpus/1408-string-indexing.ts +++ b/tests/corpus/1408-string-indexing.ts @@ -1,8 +1,4 @@ -// String indexing `s[i]` reads a UTF-16 code unit — charAt's exact job, -// which is how it lowers. In-bounds integer indices (the loop pattern) are -// JS-exact, astral pairs split into lone surrogates included; the -// out-of-range "" - vs - undefined difference is a documented divergence -// and stays out of the corpus. Node is the oracle. +// Indexed string reads preserve missing values while in-range reads match charAt. const s = "héllo \u{1f600}!"; console.log("len", s.length); diff --git a/tests/corpus/caught-class-identity.ts b/tests/corpus/caught-class-identity.ts new file mode 100644 index 000000000..de5dfd717 --- /dev/null +++ b/tests/corpus/caught-class-identity.ts @@ -0,0 +1,43 @@ +class EventBase { + label: string; + constructor(label: string) { this.label = label; } +} +class RetryEvent extends EventBase { + attempts = 0; +} +class StopEvent extends EventBase {} + +function inspect(value: unknown): string { + if (value instanceof RetryEvent) { + value.attempts++; + return value.label; + } + return value instanceof StopEvent ? 'stop' : 'other'; +} +function raise(value: EventBase): void { throw value; } +const event = new RetryEvent('retry'); +try { raise(event); } catch (error) { + const saved: unknown = error; + console.log(error instanceof RetryEvent, inspect(saved), inspect(error)); + console.log(saved === event, (saved as RetryEvent).attempts); + try { throw error; } catch (again) { console.log(inspect(again)); } +} +console.log(event.attempts); +try { throw new StopEvent('done'); } catch (error) { console.log(inspect(error)); } +try { throw 42; } catch (error) { console.log(inspect(error), typeof error); } +try { throw 'reason'; } catch (error) { console.log(inspect(error), typeof error); } + +type FailureCode = { code?: string }; +function readCode(value: unknown): string { return (value as FailureCode).code ?? 'none'; } +try { throw { code: 'RETRY', detail: 7 }; } catch (error) { + console.log((error as FailureCode).code, readCode(error)); +} +try { throw new Error('plain'); } catch (error) { + console.log((error as FailureCode).code ?? 'none'); +} + +function delayed(): () => boolean { + try { throw event; } catch (error) { return () => error === event && inspect(error) === 'retry'; } +} +const checkLater = delayed(); +console.log(checkLater(), event.attempts); diff --git a/tests/corpus/caught-system-error-fields.ts b/tests/corpus/caught-system-error-fields.ts new file mode 100644 index 000000000..c52a664d6 --- /dev/null +++ b/tests/corpus/caught-system-error-fields.ts @@ -0,0 +1,42 @@ +import { readFileSync, mkdirSync, copyFileSync, closeSync, mkdtempSync, rmSync } from 'node:fs'; +import { getSystemErrorName } from 'node:util'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +interface Failure { code?: string; errno?: number; syscall?: string; path?: string; dest?: string; } +function code(error: unknown): string { return (error as Failure).code ?? 'none'; } +const root = mkdtempSync(join(tmpdir(), 'scriptc-error-fields-')); +const missing = join(root, 'missing.txt'); +const destination = join(root, 'output.txt'); +try { + try { readFileSync(missing, 'utf8'); } catch (error: unknown) { + const details = error as Failure; + console.log(Object.keys(error as object).join(',')); + const typed = error as NodeJS.ErrnoException; + console.log(code(error), details.syscall, details.path === missing, details.dest === undefined); + console.log(typed.code, typed.syscall, typed.path === missing, getSystemErrorName(typed.errno!) === typed.code); + console.log((error as Error & { code?: string }).code, error instanceof Error); + if (error instanceof Error) console.log('code' in error); + try { throw error; } catch (again) { console.log(again === error, code(again)); } + } + try { mkdirSync(root); } catch (error) { + const details = error as Failure; + console.log(details.code, details.syscall, details.path === root, getSystemErrorName(details.errno!) === details.code); + } + try { copyFileSync(missing, destination); } catch (error) { + const details = error as Failure; + console.log(details.code, details.syscall, details.path === missing, details.dest === destination); + } + try { closeSync(1000000); } catch (error) { + const details = error as Failure; + console.log(details.code, details.syscall, details.path === undefined, getSystemErrorName(details.errno!) === details.code); + } + try { throw new Error('plain'); } catch (error) { + const details = error as Failure; + console.log(code(error), details.errno, details.syscall, details.path, details.dest); + } + class LocalError extends Error { code = 'LOCAL'; errno = 3; syscall = 'custom'; } + try { throw new LocalError('local'); } catch (error) { + const details = error as Failure; + console.log(code(error), details.errno, details.syscall); + } +} finally { rmSync(root, { recursive: true }); } diff --git a/tests/corpus/collection-data-unions.ts b/tests/corpus/collection-data-unions.ts new file mode 100644 index 000000000..2b87e9878 --- /dev/null +++ b/tests/corpus/collection-data-unions.ts @@ -0,0 +1,45 @@ +type Value = ReadonlyMap | ReadonlySet | string | number | null | undefined; +let evaluations = 0; +function once(value: Value): Value { evaluations++; return value; } +function inspect(value: Value): string { + if (once(value) instanceof Map) console.log('map-test'); + if (value instanceof Map) { + let sum = 0; + for (const [key, entry] of value) { sum += entry; console.log(key); } + console.log(value.has('first'), value.get('first'), value.get('missing')); + return 'map:' + value.size + ':' + sum; + } + if (value instanceof Set) { + let text = ''; + for (const entry of value) text += entry; + return 'set:' + text; + } + if (typeof value === 'string') return 'text:' + value; + if (typeof value === 'number') return String(value); + return value === null ? 'null' : 'undefined'; +} +const map = new Map([['first', 1], ['second', 2]]); +const set = new Set(['a', 'b']); +console.log(inspect(map), inspect(set), inspect('hello'), inspect(7), inspect(null), inspect(undefined)); +console.log(evaluations); +function update(value: Map | string): void { + if (value instanceof Map) value.set('third', 3); +} +function same(value: Value): boolean { return value === map; } +update(map); +console.log(map.get('third'), same(map), same(set)); + +function label(value: Map | { text: string }): string { + return value instanceof Map ? String(value.get('first')) : value.text; +} +console.log(label(map), label({ text: 'record' })); + +function* collectionSteps(): Generator, string, unknown> { + yield map; + return 'done'; +} +const steps = collectionSteps(); +const first = steps.next(); +console.log(first.done, first.value instanceof Map, first.value === map); +const last = steps.next(); +console.log(last.done, last.value); diff --git a/tests/corpus/native-callback-dispatch.ts b/tests/corpus/native-callback-dispatch.ts new file mode 100644 index 000000000..2acba0284 --- /dev/null +++ b/tests/corpus/native-callback-dispatch.ts @@ -0,0 +1,77 @@ +class Counter { + count = 0; + notify(): void { this.count++; } + callback = (n: number): number => { this.count += n; return this.count; }; +} +const counter = new Counter(); +counter.notify(); +console.log(counter.callback(2), counter.count); +counter.callback = (n: number): number => n * 3; +console.log(counter.callback(4), counter.count); + +const calls: ((n: number) => number)[] = [(n: number): number => n + 1]; +let receivers = 0; +let indexes = 0; +function receiver(): ((n: number) => number)[] { receivers++; return calls; } +function index(): number { indexes++; return 0; } +function replace(): number { calls[0] = (n: number): number => n + 100; return 5; } +console.log(receiver()[index()](replace()), calls[0](5), receivers, indexes); +let effects = 0; +function effect(): number { effects++; return 1; } +try { calls[3](effect()); } catch (error) { console.log(error instanceof TypeError, effects); } +function withReceiver(this: unknown, n: number): number { + console.log(this === calls); + return n + 9; +} +calls[0] = withReceiver; +console.log(calls[0](2)); +function capture(this: unknown): () => boolean { return () => this === calls; } +const captures: (() => () => boolean)[] = [capture]; +const saved = captures[0](); +console.log(saved()); +const captureCalls: ((same?: unknown) => () => boolean)[] = []; +function captured(this: unknown, same: unknown = this): () => boolean { return () => this === same; } +captureCalls.push(captured); +const check = captureCalls[0](); +console.log(check()); +function plainReceiver(this: unknown): boolean { return this === undefined; } +function nestedReceiver(this: unknown): boolean { return plainReceiver(); } +const nestedCalls: (() => boolean)[] = [nestedReceiver]; +console.log(nestedCalls[0]()); + +interface Callable { notify(): void; } +function replaceNamed(value: Callable): void { + value.notify = (): void => { counter.count += 10; }; +} +replaceNamed(counter); +counter.notify(); +console.log(counter.count); +function replaceLiteral(value: Callable): void { + value["notify"] = (): void => { counter.count += 20; }; +} +replaceLiteral(counter); +counter.notify(); +console.log(counter.count); +function replaceComputed(value: Callable, key: keyof Callable): void { + value[key] = (): void => { counter.count += 30; }; +} +replaceComputed(counter, "notify"); +counter.notify(); +console.log(counter.count); +function replaceReflected(value: Callable): void { + Object.defineProperty(value, "notify", { value: (): void => { counter.count += 40; } }); +} +replaceReflected(counter); +counter.notify(); +console.log(counter.count); + +class CallbackOwner { + value = 7; + callback = (): number => this.value; +} +const owner = new CallbackOwner(); +function replaceField(value: CallbackOwner): void { + value.callback = function (this: CallbackOwner): number { return this.value + 5; }; +} +replaceField(owner); +console.log(owner.callback()); diff --git a/tests/corpus/terminal-stream-properties.ts b/tests/corpus/terminal-stream-properties.ts new file mode 100644 index 000000000..7e8465e52 --- /dev/null +++ b/tests/corpus/terminal-stream-properties.ts @@ -0,0 +1,25 @@ +import type { WriteStream } from 'node:tty'; +import { isatty } from 'node:tty'; +function dimensions(stream: WriteStream): string { + const columns = stream.columns; + const rows = stream.rows; + return `${columns ?? 'none'}:${rows ?? 'none'}:${stream.isTTY ?? 'none'}`; +} +const out = process.stdout; +const err = process.stderr; +console.log(dimensions(out), dimensions(err)); +console.log(process.stdin.isTTY, out.isTTY, err.isTTY); +console.log(out.columns, err.rows, isatty(1), isatty(2)); +let reads = 0; +function output(): WriteStream { reads++; return out; } +console.log(output().columns ?? 80, reads); +try { out.getWindowSize(); } catch (error) { console.log(error instanceof TypeError); } + +console.log(isatty(-1), isatty(0.5), isatty(NaN), isatty(Infinity), isatty(2147483648)); + +function selectOutput(fail: boolean): WriteStream { + if (fail) throw new Error('unavailable'); + return process.stdout; +} +try { selectOutput(true).write('unreachable'); } catch (error) { console.log(error instanceof Error); } +console.log(selectOutput(false) === process.stdout); diff --git a/tests/corpus/typed-index-arguments.ts b/tests/corpus/typed-index-arguments.ts new file mode 100644 index 000000000..e821e325b --- /dev/null +++ b/tests/corpus/typed-index-arguments.ts @@ -0,0 +1,20 @@ +class Samples { + values = new Int32Array([11, 22, 33, 44]); + read(position: number): number { return this.values[position]; } +} +const samples = new Samples(); +const positions: number[] = [0, 2, 3]; +console.log(samples.read(1), samples.read(positions[0]), samples.read(positions[1])); +let calls = 0; +function position(): number { calls++; return positions[2]; } +console.log(samples.read(position()), calls); +const text = 'abcd'; +function letter(position: number): string { return text[position]; } +console.log(letter(1), letter(positions[0]), letter(position()), calls); +function optionalLetter(position: number): string | undefined { return text[position]; } +console.log(optionalLetter(positions[8]), optionalLetter(-1), optionalLetter(0.5)); + +function first(text: string): string { return text[0]; } +function combine(a: string, b: string): string { return a[0] + b[0]; } +console.log(first('abc'), first('')); +console.log(combine('a', 'b'), combine('', 'b'), combine('', '')); diff --git a/tests/fixtures/node-types/terminal-error-fields.ts b/tests/fixtures/node-types/terminal-error-fields.ts new file mode 100644 index 000000000..25bb4b5e7 --- /dev/null +++ b/tests/fixtures/node-types/terminal-error-fields.ts @@ -0,0 +1,20 @@ +import type { WriteStream } from 'node:tty'; +import { readFileSync } from 'node:fs'; +import { getSystemErrorName } from 'node:util'; +function dimensions(stream: WriteStream): string { + return `${stream.columns ?? 'none'}:${stream.rows ?? 'none'}:${stream.isTTY ?? 'none'}`; +} +console.log(dimensions(process.stdout), dimensions(process.stderr)); +console.log(process.stdout.columns ?? 'none', process.stderr.rows ?? 'none'); +try { process.stdout.getWindowSize(); } catch (error) { console.log(error instanceof TypeError); } +try { readFileSync('missing-terminal-error-fixture', 'utf8'); } catch (error) { + const details = error as NodeJS.ErrnoException; + console.log(details.code, details.syscall, details.path, getSystemErrorName(details.errno!) === details.code); + console.log((error as Error & { code?: string }).code, error instanceof Error); +} +const output = process.stdout; +console.log(output.columns ?? 80, output.isTTY ?? false); +const width = output.columns ?? 'auto'; +console.log(width); +function selectedWidth() { return output.columns ?? 'auto'; } +console.log(selectedWidth()); diff --git a/tests/fixtures/self-hosting/class-types.ts b/tests/fixtures/self-hosting/class-types.ts index 0d3528eb9..4d51d7c25 100644 --- a/tests/fixtures/self-hosting/class-types.ts +++ b/tests/fixtures/self-hosting/class-types.ts @@ -8,7 +8,7 @@ const kept: IrType = { kind: "object", className: "Registered" }; const expr: IrExpr = { kind: "varRef", localId: "x", type: missing, loc }; const nested: IrType = { kind: "func", params: [{ kind: "array", elem: missing }], ret: { kind: "generator", yieldT: missing, retT: kept, nextT: missing } }; -const module: IrModule = { irVersion: 14, sourceFile: loc.file, entry: "main", +const module: IrModule = { irVersion: 15, sourceFile: loc.file, entry: "main", functions: [{ name: "main", params: [{ localId: "x", name: "x", type: missing }], returnType: nested, locals: [], loc, body: [{ kind: "exprStmt", expr, loc }] }], records: [{ id: "r", fields: [{ name: "callback", type: nested }], indexValue: missing }], diff --git a/tests/fixtures/terminal-geometry/main.ts b/tests/fixtures/terminal-geometry/main.ts index 29c349e4f..1624c8d0f 100644 --- a/tests/fixtures/terminal-geometry/main.ts +++ b/tests/fixtures/terminal-geometry/main.ts @@ -1,10 +1,13 @@ +import type { WriteStream } from 'node:tty'; +const out = process.stdout; +function size(stream: WriteStream): [number, number] { return stream.getWindowSize(); } function geometry(): number[] { - return [ - (process.stdout as typeof process.stdout & { columns?: number }).columns ?? -1, - (process.stdout as typeof process.stdout & { rows?: number }).rows ?? -1, - (process.stderr as typeof process.stderr & { columns?: number }).columns ?? -1, - (process.stderr as typeof process.stderr & { rows?: number }).rows ?? -1, - ]; + const output = out.getWindowSize(); + const errors = size(process.stderr); + if (!out.isTTY || !process.stderr.isTTY || process.stdin.isTTY !== undefined) throw new Error(`tty flags: ${out.isTTY},${process.stderr.isTTY},${process.stdin.isTTY}`); + if (output[0] !== out.columns || output[1] !== process.stdout.rows) throw new Error('stdout geometry'); + if (errors[0] !== process.stderr.columns || errors[1] !== process.stderr.rows) throw new Error('stderr geometry'); + return [output[0], output[1], errors[0], errors[1]]; } console.log(JSON.stringify(geometry())); let ticks = 0; diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index 801ec9f14..0be93c850 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -611,3 +611,21 @@ test("dot-parent: bare '.' and '..' imports build and run (the TS project dialec const { stdout } = await execFileAsync(result.binaryPath); expect(stdout).toBe("parent-banner:lib-index!\n"); }); + +test("node-types: terminal properties and filesystem error metadata match Node", async () => { + const outDir = outDirFor("terminal-error-fields"); + const entry = join(nodeTypesDir, "terminal-error-fields.ts"); + const result = await compile(entry, { + outPath: join(outDir, "terminal-error-fields"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 5298c592f..6e75fe7f5 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -100,7 +100,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 7, marker: "scr_runtime_abi_v7" }, + runtime_abi: { version: 8, marker: "scr_runtime_abi_v8" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/tests/harness/self-hosting-validator-cases.ts b/tests/harness/self-hosting-validator-cases.ts index fed9f4781..1aeeb4273 100644 --- a/tests/harness/self-hosting-validator-cases.ts +++ b/tests/harness/self-hosting-validator-cases.ts @@ -16,7 +16,7 @@ import { boolLit, numLit, strLit, varRef } from "../../packages/compiler/src/ir/ const loc = { file: "validator-input.ts", start: 12, end: 34 }; const expression = (expr: IrExpr): IrStmt => ({ kind: "exprStmt", expr, loc }); const base = (): IrModule => ({ - irVersion: 14, + irVersion: 15, sourceFile: loc.file, entry: "main", functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [], loc }],