diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a726aee8..fbc921b74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -498,6 +498,30 @@ jobs: - name: TypeScript 7 full parity sweep run: pnpm test:ts7 + # tsc-ts, the default checker, and tsgo (SCRIPTC_CHECKER=tsgo) serve the same TypeScript 7 API. Every corpus + # program must compile to the same LLVM IR, with the same diagnostics, with either checker. + checker_identity: + needs: [validation_scope, lanes] + if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }} + name: test (tsgo and tsc-ts give the same IR) + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 11.1.3 + - uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build + - name: Corpus LLVM IR with tsgo and with tsc-ts (release) + run: pnpm test:checker-identity --optimization release + - name: Corpus LLVM IR with tsgo and with tsc-ts (dev) + run: pnpm test:checker-identity --optimization dev + llvm_artifacts_linux_x64: needs: [validation_scope, lanes] if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }} @@ -863,7 +887,28 @@ jobs: # Require this check on the stack base. Upper layers must not pass before # full validation, so untested groups cannot merge. Merge bottom-up. test: - needs: [validation_scope, lanes, code_quality, native_macos, tests, tests_macos, native_linux, effects, bootstrap_seed, bootstrap, linux_host_clang, ts7_parity, llvm_artifacts_linux_x64, llvm_artifacts_macos_arm64, llvm_artifacts_macos_x64, windows_cli, llvm_artifacts_windows_x64, mobile_runtime_packs] + needs: + [ + validation_scope, + lanes, + code_quality, + native_macos, + tests, + tests_macos, + native_linux, + effects, + bootstrap_seed, + bootstrap, + linux_host_clang, + ts7_parity, + checker_identity, + llvm_artifacts_linux_x64, + llvm_artifacts_macos_arm64, + llvm_artifacts_macos_x64, + windows_cli, + llvm_artifacts_windows_x64, + mobile_runtime_packs, + ] # Run even after failed or cancelled lanes: skipped required jobs allow merging. if: ${{ always() }} runs-on: ubuntu-latest @@ -887,6 +932,7 @@ jobs: lane bootstrap "${{ needs.bootstrap.result }}" "${{ needs.lanes.outputs.bootstrap }}" lane linux_host_clang "${{ needs.linux_host_clang.result }}" "${{ needs.lanes.outputs.linux }}" lane ts7_parity "${{ needs.ts7_parity.result }}" "${{ needs.lanes.outputs.linux }}" + lane checker_identity "${{ needs.checker_identity.result }}" "${{ needs.lanes.outputs.linux }}" lane llvm_artifacts_linux_x64 "${{ needs.llvm_artifacts_linux_x64.result }}" "${{ needs.lanes.outputs.linux }}" lane llvm_artifacts_macos_arm64 "${{ needs.llvm_artifacts_macos_arm64.result }}" "${{ needs.lanes.outputs.macos }}" lane llvm_artifacts_macos_x64 "${{ needs.llvm_artifacts_macos_x64.result }}" "${{ needs.lanes.outputs.macos }}" diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index c4fdf7876..8be1a2642 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -178,6 +178,9 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
SCRIPTC_RUNTIME_PACK
A runtime pack directory to use instead of an installed @scriptc/runtime-<target> package.
+ +
SCRIPTC_CHECKER
+
The TypeScript 7 API server that checks the program: tsc-ts (the default), which scriptc includes, or tsgo, from the typescript package scriptc also includes. Both give the same diagnostics and generated code. On Windows the default is tsgo, because tsc-ts's API server does not yet accept the named pipe scriptc connects over there.
```console diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index b27327b91..1bb645bd9 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -79,7 +79,6 @@ Some code compiles but can behave differently than under Node.js. Coverage repor - `SC6002`: values of one record type are built in different key orders, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output. - `SC6003`: `localeCompare` compares a string literal outside the built-in Latin-script collation. - `SC6004`: a call passes a class instance to a function that casts it to an unrelated class. -- `SC6005`: a `Date` string literal uses a form the native parser does not accept. ## Gating builds and CI diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index 3c6958841..e6020fe54 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -52,10 +52,12 @@ Type-only imports follow Node.js's type stripping. `import type` and `export typ scriptc compiles a package automatically when it: -- ships its own type declarations, and -- ships readable, unminified JavaScript. +- ships its own type declarations, or is typed by its DefinitelyTyped `@types/` package, or is a dependency of another package that has no declarations at all, and +- ships readable JavaScript: unminified, or minified by a bundler whose helpers keep their names. -A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself. A package whose declarations come from a separate `@types/` package doesn't ship its own; its diagnostic names the package and the `--npm-static` option that compiles it anyway. +The declarations check your program; the package's JavaScript compiles with the types scriptc infers from it. A program that doesn't typecheck against the inferred types, for example because an `@types/` package describes another release, fails the build at that use. + +A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself. @@ -63,17 +65,17 @@ A package that doesn't meet these requirements, or that uses something scriptc c ## Explicit package selection -Some packages rely on separately installed declarations, such as an `@types/` package, or on types inferred from their JavaScript. To try compiling one of them, name it with `--npm-static`: +Some packages your program imports directly ship no declarations, so their types can only be inferred from their JavaScript. To try compiling one of them, name it with `--npm-static`: ```console $ scriptc build app.ts --npm-static=three -o app >/dev/null ``` -Naming a package skips the declaration requirement above, but the package still has to compile. The option selects only the packages it names: a dependency of a named package that doesn't ship its own declarations is still refused. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=fs-extra...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated. +Naming a package skips the declaration requirement above, but the package still has to compile. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=fs-extra...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated. ## CommonJS packages -CommonJS packages compile with Node.js's module semantics. Each `require()` evaluates its module at the call, once, and later calls return the cached exports. A package can assign `module.exports` and run code before it requires its dependencies. If that code reads a `const` or `let` require binding before its `require` runs, the read throws the same `ReferenceError` as Node.js. A `var` binding that earlier code can reach is reported at build time. A `require()` of a JSON file, such as a package's own `package.json`, returns one object parsed at build time, shared by every module that requires it. A require cycle compiles when each of its modules assigns its exports before requiring anything, as semver's classes do, and uses the other modules only inside functions that its top-level code can't call. Other require cycles are reported at build time. The [limitations](/docs/limitations) page lists the supported `require` forms. +CommonJS packages compile with Node.js's module semantics. Each `require()` evaluates its module at the call, once, and later calls return the cached exports. A package can assign `module.exports` and run code before it requires its dependencies. If that code reads a `const` or `let` require binding before its `require` runs, the read throws the same `ReferenceError` as Node.js. A `var` binding that earlier code can reach is reported at build time. A `require()` of a JSON file, such as a package's own `package.json`, returns one object parsed at build time, shared by every module that requires it. A require cycle compiles when each of its modules assigns its exports before requiring anything, as semver's classes and esbuild's per-file output do, and uses the other modules only inside functions that its top-level code can't call. Bundles built by esbuild compile with their bundled CommonJS modules and lazily initialized ES modules, each evaluated once, at its first use, as under Node.js. Other require cycles are reported at build time. The [limitations](/docs/limitations) page lists the supported `require` forms. ## Unsupported code inside a package diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index d261b6385..c64c5cebd 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -228,6 +228,10 @@ title: "Limitations" ../limitations/process-arguments-and-errors.mdx +### Module path globals + +../limitations/module-path-globals.mdx + ## Runtime code evaluation diff --git a/docs/content/limitations/builtin-apis/date.mdx b/docs/content/limitations/builtin-apis/date.mdx index 4c1c7e034..ac8bf9115 100644 --- a/docs/content/limitations/builtin-apis/date.mdx +++ b/docs/content/limitations/builtin-apis/date.mdx @@ -1,5 +1,5 @@ -- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale formatters are unsupported. - Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. +- **Date:** Date unions, spread constructor arguments, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale formatters are unsupported. + Zero-argument, single number/string, and local-time year/month field constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`. - See [Dates and timezones](#dates-and-timezones) for parsing restrictions. + See [Dates and timezones](#dates-and-timezones) for timezone data. diff --git a/docs/content/limitations/builtin-apis/exit-status.mdx b/docs/content/limitations/builtin-apis/exit-status.mdx index d9bb779db..3cdf10496 100644 --- a/docs/content/limitations/builtin-apis/exit-status.mdx +++ b/docs/content/limitations/builtin-apis/exit-status.mdx @@ -1,3 +1,4 @@ - **Exit status:** Numeric `process.exitCode = value` writes in statement position set the implicit exit status, which `process.exit()` uses when called without an argument. `process.exit(code)` and `process.exitCode` throw a `RangeError` (`ERR_OUT_OF_RANGE`) for a code that is not a safe integer, and convert other codes to int32, as Node does. - Reads, resets, and assignments used as values are unsupported. + Reading `process.exitCode` answers the assigned code, or `undefined` before any assignment; `process.exit(code)` assigns it before the `'exit'` listeners run. + Resets (`process.exitCode = undefined`) and assignments used as values are unsupported. diff --git a/docs/content/limitations/builtin-apis/object-freezing.mdx b/docs/content/limitations/builtin-apis/object-freezing.mdx index 9784cd715..38f0155c0 100644 --- a/docs/content/limitations/builtin-apis/object-freezing.mdx +++ b/docs/content/limitations/builtin-apis/object-freezing.mdx @@ -1,3 +1,8 @@ - **Object freezing:** `Object.freeze` and `Object.isFrozen` support plain checked dictionaries and primitives. Freezing is shallow and preserves accessors. Aliased typed records, arrays, proxies, and other native objects cannot be frozen. + Class instances support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible` when the static type of the argument names the class. + A write to a read-only field throws Node's `TypeError` in strict mode code and leaves the field unchanged in sloppy mode code. + `Object.defineProperty` with a string-literal key changes the value, `writable`, `enumerable`, and `configurable` attributes of a declared field; an object-literal accessor descriptor (`get` or `set`) for a declared field is refused at compile time with `SC2020`, and another accessor descriptor for a declared field throws `SC2020` when the call runs. + Freezing, sealing, or preventing extensions of a class instance held in an untyped value throws `SC2020`. + A JavaScript class field that is first created after its instance became non-extensible is added instead of throwing. diff --git a/docs/content/limitations/builtin-apis/prototypes.mdx b/docs/content/limitations/builtin-apis/prototypes.mdx index 4ac2ee438..3e0b5789d 100644 --- a/docs/content/limitations/builtin-apis/prototypes.mdx +++ b/docs/content/limitations/builtin-apis/prototypes.mdx @@ -1,2 +1,6 @@ - **Prototypes:** Named properties and methods on program class prototypes support replacement, inheritance, and deletion. Bare prototype reflection, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported. + Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly. + Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not. + `Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported. + The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called, and `instanceof` and `new` against them remain unsupported. diff --git a/docs/content/limitations/builtin-apis/test-mocks.mdx b/docs/content/limitations/builtin-apis/test-mocks.mdx index abfdb3657..903a873b9 100644 --- a/docs/content/limitations/builtin-apis/test-mocks.mdx +++ b/docs/content/limitations/builtin-apis/test-mocks.mdx @@ -3,7 +3,8 @@ Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`. Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`. `object.method === mock` is `false`, because the field holds a wrapper of the mock. - `mock.fn()` without an original, `mock.getter`, `mock.setter`, `mock.property`, `mock.module` and mock options throw `SC2020`. + `mock.fn()` without an original returns `undefined` and records the arguments of each call, as in Node; with a function type whose return type does not include `undefined`, such as `mock.fn<() => number>()`, it throws `SC2020`. + `mock.getter`, `mock.setter`, `mock.property`, `mock.module` and mock options throw `SC2020`. - **Mock timers:** `mock.timers.enable`, `tick`, `runAll`, `reset`, `setTime` and `[Symbol.dispose]()` compile, on the module's `mock` and on `t.mock`, for the `setTimeout`, `setInterval`, `setImmediate`, `Date`, `scheduler.wait` and `AbortSignal.timeout` APIs. `enable()` takes no options or an object literal whose `apis`, when given, is an array literal. Options and `apis` lists that are not literals, and the `MockTimers` object used as a value, throw `SC2020`. diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index c08540c1a..80d0814b1 100644 --- a/docs/content/limitations/builtin-function-values.mdx +++ b/docs/content/limitations/builtin-function-values.mdx @@ -5,6 +5,10 @@ Some builtins can be stored and passed as functions: - `util.isDeepStrictEqual`, `styleText`, `stripVTControlCharacters`, `toUSVString`, `inherits`, `deprecate`, `callbackify`, `debuglog`, and `debug`. - `fs.existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, and `closeSync`, and `fs/promises.unlink`, `chmod`, and `rename`. +In JavaScript, `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, `queueMicrotask`, and `process.nextTick` are function values wherever they are stored, including conditional and logical expressions and assignments: `typeof` answers `"function"`, identity holds across spellings, and calls schedule as the direct calls do, returning the same numeric timer handles. +Calls through these values bypass `node:test` mock timers. +Other builtin globals stored in JavaScript remain opaque identity values. + Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere. `util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call. diff --git a/docs/content/limitations/dates-and-timezones.mdx b/docs/content/limitations/dates-and-timezones.mdx index e5655eb85..cba1d9004 100644 --- a/docs/content/limitations/dates-and-timezones.mdx +++ b/docs/content/limitations/dates-and-timezones.mdx @@ -1,7 +1,7 @@ -String constructors and `Date.parse` accept ECMAScript date-only forms, date-times with an explicit `Z` or numeric offset, and GMT certificate-validity strings from the supported X509 APIs. -Other V8-specific forms and offset-less local date-times produce an Invalid Date or `NaN`; `SC6005` reports string literals in those forms. +String constructors and `Date.parse` use V8's date-string grammar: ECMAScript date-time strings and the legacy forms Node.js accepts, such as RFC 1123 HTTP dates (`Mon, 01 Jan 2024 00:00:00 GMT`), `Date#toString` output, and `Jan 2, 2024`. +Strings without a timezone, and `new Date(year, monthIndex, ...)` fields, are local time. -Local calendar getters use the operating system's timezone database. +Local calendar getters and local-time conversions use the operating system's timezone database. Historical results can differ from Node.js when timezone data differs. For valid extreme years outside a platform calendar API's range, scriptc uses a calendar-equivalent year in the 400-year Gregorian cycle to obtain the timezone rule. diff --git a/docs/content/limitations/language-features/function-values.mdx b/docs/content/limitations/language-features/function-values.mdx index 88422c406..75229ff95 100644 --- a/docs/content/limitations/language-features/function-values.mdx +++ b/docs/content/limitations/language-features/function-values.mdx @@ -1,4 +1,4 @@ - **Function values:** Immutable aliases of overloaded program functions retain per-call overload resolution and identity. A function stored in a slot with a different signature remains the same function for equality, array searches, `length`, and own properties. - Typed rest-parameter functions can be stored and called indirectly. + Typed rest-parameter functions can be stored and called indirectly, including through a slot with fixed parameters, where the slot's arguments arrive in the rest array. Builtin function values have API-specific restrictions; see [Builtin function values](#builtin-function-values). diff --git a/docs/content/limitations/module-path-globals.mdx b/docs/content/limitations/module-path-globals.mdx new file mode 100644 index 000000000..358d96f95 --- /dev/null +++ b/docs/content/limitations/module-path-globals.mdx @@ -0,0 +1,4 @@ +`__dirname` and `__filename` are compile-time constants: the absolute path of the module's source directory and file at build time, the same strings as `import.meta.dirname` and `import.meta.filename`. +A `.ts` or `.tsx` file keeps them even when it uses `import` syntax, which Node.js would load as an ES module without these globals, because TypeScript projects are commonly shipped as CommonJS bundles where they exist. +In such a file `typeof __dirname` is `"string"`, where Node.js running the source directly reports `"undefined"`. +JavaScript ES modules (`.mjs`, or `.js` in a `"type": "module"` package) do not have them, as in Node.js. diff --git a/docs/content/limitations/types-and-class-definitions/null-and-optional-values.mdx b/docs/content/limitations/types-and-class-definitions/null-and-optional-values.mdx index e8b351679..be8b07571 100644 --- a/docs/content/limitations/types-and-class-definitions/null-and-optional-values.mdx +++ b/docs/content/limitations/types-and-class-definitions/null-and-optional-values.mdx @@ -1,3 +1,4 @@ - **Null and optional values:** `undefined` and `null` compile as standalone values and union members. Optional record and class fields and optional, default, and rest parameters also compile, including rest parameters typed `unknown[]` or `any[]`, whose functions take any number of arguments. - A function value with fixed parameters cannot be stored where such a variadic function type is expected. + A function value whose fixed parameters are all `unknown` or `any`, or that has none, can be stored where such a variadic function type without fixed parameters is expected: each call passes the arguments in order, `undefined` for the ones the caller left out. + Other function values with fixed parameters cannot be stored there. diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 817b723e3..2d10f73b4 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -55940,7 +55940,9 @@ "confidence": "high", "source": "compiler-feature:process.'disconnect'", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] } } @@ -55982,7 +55984,9 @@ "confidence": "high", "source": "compiler-feature:process.'message'", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] } } @@ -56365,7 +56369,9 @@ "confidence": "high", "source": "compiler-feature:process.process.disconnect", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] } } @@ -57013,7 +57019,9 @@ "confidence": "high", "source": "compiler-feature:process.process.send", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] } } @@ -57467,7 +57475,9 @@ "confidence": "high", "source": "compiler-feature:process.connected", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] } } @@ -57582,7 +57592,10 @@ "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", - "tests/corpus/2944-process-exit-code-wrap.ts" + "tests/corpus/2944-process-exit-code-wrap.ts", + "tests/corpus/6422-process-exitcode-read.ts", + "tests/corpus/6423-process-exitcode-default.ts", + "tests/corpus/6424-process-exit-keeps-exitcode.ts" ] } } @@ -66531,6 +66544,7 @@ "source": "compiler-feature:test.mock.fn", "tests": [ "tests/fixtures/node-test/cases/mock-fn.ts", + "tests/fixtures/node-test/cases/mock-fn-default.ts", "tests/harness/node-test.test.ts" ] } @@ -76203,6 +76217,8 @@ "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", + "tests/corpus/6428-promisify-callback-functions.ts", + "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/harness/project-config.test.ts" ] } diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 3fc7b1a54..637a83345 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -79409,6 +79409,11 @@ "tests/corpus/5210-process-memory-usage.ts", "tests/corpus/5211-process-identity.ts", "tests/corpus/5809-process-exit-and-rejections.cjs", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts", + "tests/corpus/6422-process-exitcode-read.ts", + "tests/corpus/6423-process-exitcode-default.ts", + "tests/corpus/6424-process-exit-keeps-exitcode.ts", "tests/corpus/990-process-basics.ts", "tests/corpus/node24-env-files-errors.ts", "tests/corpus/node24-env-files.ts", @@ -79486,7 +79491,9 @@ "status": "partial", "evidence": "compiler-feature:process.'disconnect'", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] }, "anchorSource": "exact" @@ -79525,7 +79532,9 @@ "status": "partial", "evidence": "compiler-feature:process.'message'", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] }, "anchorSource": "exact" @@ -79938,7 +79947,9 @@ "status": "partial", "evidence": "compiler-feature:process.process.disconnect", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] }, "anchorSource": "exact" @@ -80583,7 +80594,9 @@ "status": "partial", "evidence": "compiler-feature:process.process.send", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] }, "anchorSource": "exact" @@ -81002,7 +81015,9 @@ "status": "partial", "evidence": "compiler-feature:process.connected", "tests": [ - "tests/corpus/2946-child-fork-ipc/main.ts" + "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/6420-process-send-presence.ts", + "tests/corpus/6421-process-send-presence-fork/main.ts" ] }, "anchorSource": "chapter" @@ -81111,7 +81126,10 @@ "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", - "tests/corpus/2944-process-exit-code-wrap.ts" + "tests/corpus/2944-process-exit-code-wrap.ts", + "tests/corpus/6422-process-exitcode-read.ts", + "tests/corpus/6423-process-exitcode-default.ts", + "tests/corpus/6424-process-exit-keeps-exitcode.ts" ] }, "anchorSource": "chapter" @@ -91145,6 +91163,7 @@ "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", "tests/fixtures/node-test/cases/get-test-context.ts", + "tests/fixtures/node-test/cases/mock-fn-default.ts", "tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-method.ts", "tests/fixtures/node-test/cases/mock-timers-date.ts", @@ -92556,6 +92575,7 @@ "evidence": "compiler-feature:test.mock.fn", "tests": [ "tests/fixtures/node-test/cases/mock-fn.ts", + "tests/fixtures/node-test/cases/mock-fn-default.ts", "tests/harness/node-test.test.ts" ] }, @@ -101237,6 +101257,7 @@ "tests/corpus/5832-util-types-brands-dynamic.js", "tests/corpus/5835-util-format-integer-specifiers.ts", "tests/corpus/5884-inspect-layout-options.ts", + "tests/corpus/6428-promisify-callback-functions.ts", "tests/corpus/node24-deep-equality-properties.js", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality.ts", @@ -101263,6 +101284,7 @@ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/deep-equality.ts", "tests/fixtures/node-types/env-files.ts", + "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/fixtures/node-types/promisify-fs.ts", @@ -102921,6 +102943,8 @@ "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", + "tests/corpus/6428-promisify-callback-functions.ts", + "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/harness/project-config.test.ts" ] }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 668c3740c..701b5aac1 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -148,7 +148,7 @@ "node-builtin.process.versions": ["tests/corpus/process-versions-storage.ts"], "node-builtin.process.rows": ["tests/corpus/2992-stream-rows.ts", "tests/harness/terminal-geometry.test.ts"], "node-builtin.process.columns": ["tests/corpus/1448-stream-columns.ts", "tests/harness/terminal-geometry.test.ts"], - "node-builtin.process.exitCode": ["tests/corpus/5809-process-exit-and-rejections.cjs", "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts"], + "node-builtin.process.exitCode": ["tests/corpus/5809-process-exit-and-rejections.cjs", "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts", "tests/corpus/6422-process-exitcode-read.ts", "tests/corpus/6423-process-exitcode-default.ts", "tests/corpus/6424-process-exit-keeps-exitcode.ts"], "node-builtin.punycode": ["tests/corpus/6122-punycode-conversions.ts", "tests/corpus/6124-punycode-require-warnings.cjs", "tests/corpus/6126-punycode-dependency/main.cjs"], "node-builtin.querystring": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2917-builtin-callable-widths.ts"], "node-builtin.readline": ["tests/corpus/1475-readline-closed-stdin.ts"], @@ -341,7 +341,7 @@ { "chapter": "fs", "signatures": ["`fsPromises.lstat(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2941-fsp-lstat-identity.ts"] }, { "chapter": "fs", "signatures": ["`fs.globSync(pattern[, options])`", "`fs.glob(pattern[, options], callback)`", "`fsPromises.glob(pattern[, options])`"], "status": "partial", "evidence": ["tests/corpus/5448-fs-glob-patterns.ts", "tests/corpus/5449-fs-glob-options.ts", "tests/corpus/5450-fs-glob-validation.js", "tests/corpus/5451-fs-promises-glob.ts", "tests/corpus/5452-fs-glob-callback.ts", "tests/fixtures/node-types/fs-glob.ts"] }, { "chapter": "fs", "signatures": ["`fs.cpSync(src, dest[, options])`", "`fs.cp(src, dest[, options], callback)`", "`fsPromises.cp(src, dest[, options])`"], "status": "partial", "evidence": ["tests/corpus/5567-fs-cpsync.ts", "tests/corpus/5568-fs-cp-errors.js", "tests/corpus/5569-fs-cp-symlinks.ts", "tests/corpus/5570-fs-cp-async.ts"] }, - { "chapter": "process", "signatures": ["`process.send(message[, sendHandle[, options]][, callback])`", "`process.disconnect()`", "`connected` Type: {boolean}", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts"] }, + { "chapter": "process", "signatures": ["`process.send(message[, sendHandle[, options]][, callback])`", "`process.disconnect()`", "`connected` Type: {boolean}", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts", "tests/corpus/6420-process-send-presence.ts", "tests/corpus/6421-process-send-presence-fork/main.ts"] }, { "chapter": "fs", "symbols": ["fs.Stats", "stats.isBlockDevice", "stats.isCharacterDevice", "stats.isDirectory", "stats.isFIFO", "stats.isFile", "stats.isSocket", "stats.isSymbolicLink"], "status": "partial", "evidence": ["tests/corpus/5541-fs-stats-fields.cjs"] }, { "chapter": "fs", "parent": "Class: `fs.Stats`", "signatures": ["`mode` Type: {number|bigint}", "`nlink` Type: {number|bigint}", "`uid` Type: {number|bigint}", "`gid` Type: {number|bigint}", "`rdev` Type: {number|bigint}", "`size` Type: {number|bigint}", "`blksize` Type: {number|bigint}", "`blocks` Type: {number|bigint}", "`atimeMs` Type: {number|bigint}", "`mtimeMs` Type: {number|bigint}", "`birthtimeMs` Type: {number|bigint}", "`atimeNs` Type: {bigint}", "`mtimeNs` Type: {bigint}", "`ctimeNs` Type: {bigint}", "`birthtimeNs` Type: {bigint}", "`atime` Type: {Date}", "`mtime` Type: {Date}", "`ctime` Type: {Date}", "`birthtime` Type: {Date}"], "status": "partial", "evidence": ["tests/corpus/5541-fs-stats-fields.cjs"] }, { "chapter": "fs", "signatures": ["`dev` Type: {number|bigint}", "`ino` Type: {number|bigint}", "`ctimeMs` Type: {number|bigint}"], "status": "partial", "evidence": ["tests/corpus/2941-fsp-lstat-identity.ts"] }, @@ -370,7 +370,7 @@ { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.filePath", "passed", "attempt", "tags", "workerId"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-members.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `SuiteContext`", "symbols": ["context.name", "context.fullName", "context.filePath", "context.diagnostic", "passed", "attempt"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-context.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.diagnostic", "context.name"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] }, - { "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.fn"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/harness/node-test.test.ts"] }, + { "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.fn"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-fn-default.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.method", "mock.reset", "mock.restoreAll"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-method.ts", "tests/corpus/5810-mock-method-records.ts", "tests/corpus/5811-mock-tracker-restore.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `MockTimers`", "symbols": ["timers.enable", "timers.reset", "timers.tick", "timers.runAll", "timers.setTime"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers.ts", "tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/corpus/5813-mock-timers-module.ts", "tests/corpus/5814-mock-timers-date.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `MockTimers`", "signatures": ["`timers[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/harness/node-test.test.ts"] }, @@ -378,7 +378,7 @@ { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "symbols": ["getTestContext"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/get-test-context.ts", "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] }, - { "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/harness/project-config.test.ts"] }, + { "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/harness/project-config.test.ts"] }, { "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] }, { "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] }, { "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] } diff --git a/package.json b/package.json index 7318e04f5..03a763372 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "bench:backend-analysis": "node scripts/bench-backend-analysis.mjs", "test": "vitest run", "test:ts7": "node scripts/test-ts7.mjs", + "test:checker-identity": "node scripts/checker-identity.mjs", "generate:ts7-ast": "node scripts/generate-ts7-ast-schema.mjs", "test:fetch-conformance": "vitest run tests/harness/fetch-conformance.test.ts", "test:test262": "tsx tests/test262/run.ts", diff --git a/packages/cli/scripts/install-native.mjs b/packages/cli/scripts/install-native.mjs index 0d125970d..84eb846bc 100644 --- a/packages/cli/scripts/install-native.mjs +++ b/packages/cli/scripts/install-native.mjs @@ -42,6 +42,7 @@ export function relocateToolchain(manifest, sourceDirectory, destinationDirector const pathFrom = (value) => relative(destinationDirectory, resolve(sourceDirectory, value)); for (const name of [ "ts7", + "tsc_ts", "llvm_package", "runtime_pack", "runtime_sources", @@ -96,6 +97,7 @@ export function installNativeCli( if (process.platform !== "win32") { for (const path of [ manifest.ts7, + ...(manifest.tsc_ts === undefined ? [] : [manifest.tsc_ts]), manifest.comptime, join(manifest.llvm_package, "bin/scriptc-llvm-codegen"), ]) { diff --git a/packages/cli/src/bootstrap.ts b/packages/cli/src/bootstrap.ts index 2d12883fa..1466707d6 100644 --- a/packages/cli/src/bootstrap.ts +++ b/packages/cli/src/bootstrap.ts @@ -116,6 +116,13 @@ async function tryFastPath(): Promise { // the whole compiler graph to rediscover it. const helperRuntimePackTarget = !values.sanitize ? startup.precompiledRuntimePackTarget() : null; const helperObjectRoute = helperRuntimePackTarget !== null; + // The full compiler reports a checker it cannot find. + let checker: string | undefined; + try { + checker = startup.checkerCacheIdentity(); + } catch { + return null; + } const hit = await startup.readRoutedExecutableCache(root, { entryPath: input, outDir, @@ -126,6 +133,7 @@ async function tryFastPath(): Promise { ...(optimization === "dev" || optimization === "speed" ? { optimization } : {}), ...(values.strip ? { strip: true as const } : {}), ...(values["detached-exit"] ? { detachedExit: true as const } : {}), + ...(checker === undefined ? {} : { checker }), npmStatic, ffiProfile: ffiPath === null ? null : { path: ffiPath, bytes: ffiBytes! }, target: `${process.env["SCRIPTC_TARGET"] ?? "native"}:${buildPlatform}:${process.arch}:${ diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 3bf97fe6a..436bcace3 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3372,6 +3372,13 @@ declare module "util" { info: import("node:crypto").BinaryLike, keylen: number, ) => Promise; + /* A callback-only function (`obj.close.bind(obj)`): before the targets + * whose leading parameters would otherwise capture it. Builtins that take + * arguments before their callback never match these. */ + export function promisify( + fn: (callback: (err: any, result: TResult) => void) => void, + ): () => Promise; + export function promisify(fn: (callback: (err?: any) => void) => void): () => Promise; export function promisify( fn: typeof import("node:crypto").randomFill, ): ( diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 6e5442dc6..b6a18db06 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -39,6 +39,7 @@ }, "dependencies": { "@scriptc/runtime": "workspace:*", + "tsc-ts": "0.0.3", "typescript": "7.0.2", "typescript5": "npm:typescript@5.9.3" }, diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 86b4f5be8..7e414daee 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -34,6 +34,7 @@ import { InternalCompilerError } from "../../errors.js"; * ScrDynPath { parent, key, index } — the %ScrDynPath type. */ import type { IrRecordShape, IrType, IrUnionDef } from "../../ir/ir.js"; import { + accessorSlotProp, DYN_HANDLE_KINDS, isDirentShape, isDynTypedRefType, @@ -613,6 +614,18 @@ export class LlDyn { * tracking does not count the read's reference. Returns the value and * the matching release. */ private memberRead(B: BlockBuilder, key: string, t: IrType): { value: string; release: string } { + // An accessor slot reads the getter or setter of the accessor property + // the checked-dynamic copy holds for it (see accessorSlotProp). + const slot = accessorSlotProp(key); + if (slot !== null) { + this.host.declare(`declare ptr @scr_dyn_obj_accessor_fn(ptr, ptr, ${this.S}, i1 zeroext)`); + this.host.declare(`declare void @scr_dyn_release(ptr)`); + const fn = B.tmp(); + B.line( + `${fn} = call ptr @scr_dyn_obj_accessor_fn(ptr %d, ptr ${this.host.cstr(slot.prop)}, ${this.S} ${Buffer.byteLength(slot.prop, "utf8")}, i1 ${slot.kind === "set" ? "true" : "false"})`, + ); + return { value: fn, release: "scr_dyn_release" }; + } const held = this.isRecursiveShape(t) || this.holdsRecursiveShape(t); const release = held ? "scr_dyn_release_held" : "scr_dyn_release"; this.host.declare(`declare void @${release}(ptr)`); @@ -2687,13 +2700,53 @@ export class LlDyn { // Keys insert in DECLARED order (JS insertion order); internal // '%'-fields follow so a record→dyn→record round trip keeps them. const byName = new Map(shape.fields.map((f) => [f.name, f])); - const order = shape.declaredOrder ?? shape.fields.map((f) => f.name); + // An accessor property is one key whose getter and setter are the + // slot closures (see accessorSlotProp), in accessorOrder's place. + const order = shape.accessorOrder ?? shape.declaredOrder ?? shape.fields.map((f) => f.name); const inOrder = new Set(order); - const dynFields = [ - ...order.map((n) => byName.get(n)).filter((f) => f !== undefined), - ...shape.fields.filter((f) => !inOrder.has(f.name)), + const accessors = new Set( + shape.accessorOrder === undefined + ? [] + : shape.fields.flatMap((f) => { + const slot = accessorSlotProp(f.name); + return slot === null ? [] : [slot.prop]; + }), + ); + type DynField = { name: string; type: IrType } | { accessor: string }; + const dynFields: DynField[] = [ + ...order.flatMap((n) => { + if (accessors.has(n)) return [{ accessor: n } as DynField]; + const f = byName.get(n); + return f === undefined ? [] : [f as DynField]; + }), + ...shape.fields.filter( + (f) => + !inOrder.has(f.name) && !(accessors.size > 0 && accessorSlotProp(f.name) !== null), + ), ]; - for (const f of dynFields) { + for (const entry of dynFields) { + if ("accessor" in entry) { + host.declare( + `declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`, + ); + const boxed = (kind: "get" | "set"): string => { + const slot = byName.get(`%${kind}:${entry.accessor}`); + if (slot === undefined) return "null"; + const fv = loadFieldOf(slot.name, slot.type); + const conv = B.tmp(); + B.line( + `${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`, + ); + return conv; + }; + const getter = boxed("get"); + const setter = boxed("set"); + B.line( + `call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`, + ); + continue; + } + const f = entry; const klen = Buffer.byteLength(f.name, "utf8"); const fv = loadFieldOf(f.name, f.type); // An ABSENT optional field contributes no key. diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts index d71df333a..b7954c174 100644 --- a/packages/compiler/src/backend/llvm/expr-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-callbacks.ts @@ -1,6 +1,13 @@ /* Focused LLVM expression emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; -import { type IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/ir.js"; +import { + type IrType, + UNDEFINED_T, + isRefCounted, + isUnitType, + typeEquals, + typeKey, +} from "../../ir/ir.js"; import { mangleGenResThunk, mangleRecordNew, mangleRecordStruct } from "../mangle.js"; import { FN_ATTRS, llFieldType, releaseSym, retainSym, traceArg, vAdapters } from "./shapes.js"; import { LlvmUnsupportedError } from "./unsupported.js"; @@ -48,6 +55,23 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp if (sym) return sym; sym = `sc_race_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + const toArms = to.kind === "union" ? (host.unionsById.get(to.unionId)?.arms ?? []) : []; + if (from.kind === "void" && !toArms.some((a) => a.kind === "undefinedT")) { + // A Promise entry (the frontend admits void entries without an + // undefined result arm only for those): it can only reject, so its + // fulfillment adapter never runs. + host.declare(`declare void @scr_trap(ptr)`); + const msg = host.cstr("scriptc: a Promise race entry fulfilled\n"); + host.resolveThunkDefs.push( + `define internal void @${sym}(ptr %dst, ptr %src) ${FN_ATTRS} { ; race ${key}`, + `entry:`, + ` call void @scr_trap(ptr ${msg})`, + ` unreachable`, + `}`, + ``, + ); + return sym; + } if (to.kind !== "union") throw new InternalCompilerError("llvm emitter bug: race adapter to a non-union"); const toDef = host.unionsById.get(to.unionId); @@ -68,6 +92,14 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp `define internal void @${sym}(ptr %dst, ptr %src) ${FN_ATTRS} { ; race ${key}`, `entry:`, ]; + if (from.kind === "void") { + // A Promise entry fulfills with undefined: the result's + // undefined arm, whatever the entry's payload slot holds. + const tag = tagOf(UNDEFINED_T); + d.push(` ${fulfill(host.unitInstanceRef(to.unionId, tag))}`, ` ret void`, `}`, ``); + host.resolveThunkDefs.push(...d); + return sym; + } if (from.kind !== "union") { // One arm wrap, straight off the payload accessors. const tag = tagOf(from); diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 8a24fcf23..bc4f1128a 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -535,6 +535,27 @@ export function emitDynamicExpr( e.key.type.kind === "dyn" ? host.dyn.dynComputedKeyGetHelper() : host.dyn.dynKeyGetHelper(); const t = B.tmp(); const generic = `call ptr @${helper}(ptr ${d.name}, ptr ${k.name}, i1 ${e.optional ? "true" : "false"})`; + const inherited = e.key.kind === "strLit" && INHERITED_OBJECT_MEMBERS.has(e.key.value); + // Object.prototype itself answers its own members before the ordinary + // read (which fences reflection on the prototype). + let prototypeJoin: { member: string; from: string; join: string } | null = null; + if (inherited) { + host.declare(`declare ptr @scr_dyn_object_prototype_member(ptr, ptr)`); + const member = B.tmp(); + B.line( + `${member} = call ptr @scr_dyn_object_prototype_member(ptr ${d.name}, ptr ${k.name})`, + ); + const isMember = B.tmp(); + B.line(`${isMember} = icmp ne ptr ${member}, null`); + const from = B.newLabel("dkg.op"); + const read = B.newLabel("dkg.rd"); + const join = B.newLabel("dkg.opj"); + B.condBr(isMember, from, read); + B.startBlock(from); + B.br(join); + B.startBlock(read); + prototypeJoin = { member, from, join }; + } if (e.key.kind === "strLit") { // A constant key on an ordinary object reads through the site's // cache; every other receiver keeps the shared helper. @@ -560,7 +581,27 @@ export function emitDynamicExpr( } else { B.line(`${t} = ${generic}`); } - const out = host.own({ name: t, type: e.type }); + let result = t; + if (prototypeJoin) { + // An undefined read of an Object.prototype member name answers the + // member the receiver inherits (scr_dyn_invoke.c; the frontend + // links that unit for these reads). + host.declare(`declare ptr @scr_dyn_inherited_read(ptr, ptr, ptr)`); + const read = B.tmp(); + B.line( + `${read} = call ptr @scr_dyn_inherited_read(ptr ${d.name}, ptr ${k.name}, ptr ${t})`, + ); + const readEnd = B.newLabel("dkg.rde"); + B.br(readEnd); + B.startBlock(readEnd); + B.br(prototypeJoin.join); + B.startBlock(prototypeJoin.join); + result = B.tmp(); + B.line( + `${result} = phi ptr [ ${prototypeJoin.member}, %${prototypeJoin.from} ], [ ${read}, %${readEnd} ]`, + ); + } + const out = host.own({ name: result, type: e.type }); host.emitPendingCheck(); return out; } @@ -1204,3 +1245,13 @@ function emitUnionEqWrappedScalar( function wrappedStringLiteral(e: IrExpr): string | null { return e.kind === "unionWrap" && e.value.kind === "strLit" ? e.value.value : null; } + +/** Object.prototype member names whose constant-key dynamic reads fall back + * to the inherited member (runtime-features.ts links the unit). */ +export const INHERITED_OBJECT_MEMBERS: ReadonlySet = new Set([ + "constructor", + "valueOf", + "toString", + "hasOwnProperty", + "propertyIsEnumerable", +]); diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 8a53b5fa8..847ed1e20 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -5,14 +5,17 @@ import { streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js"; import { type IrRecordShape, type IrType, + CLASS_ATTRIBUTES, DYN_CLASS_PROPERTIES, RUNTIME_STREAM_CLASSES, SYMBOL_T, classDynViewSupported, + classFieldAttributesName, isClassOwnEnumerableFieldName, isDirentShape, isDynTypedRefType, isRefCounted, + shapeHasAccessorSlots, typeKey, } from "../../ir/ir.js"; import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js"; @@ -766,6 +769,7 @@ export function streamTypedRefMaterializeAdapter( host.declare(`declare ptr @scr_stream_dyn_view(ptr)`); B.line(`${out} = call ptr @scr_stream_dyn_view(ptr %p)`); } else B.line(`${out} = call ptr @scr_dyn_new_obj()`); + const withAttributes = meta.def.fields.some((field) => field.name === CLASS_ATTRIBUTES); if (meta.def.tracksOwnFields) { host.declare(`declare ptr @scr_dyn_copy_property_descriptors(ptr, ptr)`); host.declare(`declare void @scr_dyn_release_v(ptr)`); @@ -835,9 +839,27 @@ export function streamTypedRefMaterializeAdapter( const nullable = host.nullableFields.get(t.className, field.name); if (nullable) fieldValue = nullableFieldUnion(host, B, fieldValue, nullable); const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue); + // Attributes copied from the bag do not stop the view taking the value. + const setter = withAttributes ? "scr_dyn_obj_set_field" : "scr_dyn_obj_set"; + if (withAttributes) + host.declare(`declare void @scr_dyn_obj_set_field(ptr, ptr, ${host.sizeType}, ptr)`); B.line( - `call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`, + `call void @${setter}(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`, ); + // A field redefined through Object.defineProperty keeps its attributes. + const attributesField = classFieldAttributesName(field.name); + if (meta.def.fields.some((f) => f.name === attributesField)) { + host.declare(`declare void @scr_dyn_class_field_attributes(ptr, ptr, double)`); + const slot = B.tmp(), + bits = B.tmp(); + B.line( + `${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${classFieldIndex(meta, attributesField).index}`, + ); + B.line(`${bits} = load double, ptr ${slot}`); + B.line( + `call void @scr_dyn_class_field_attributes(ptr ${out}, ptr ${host.internLiteral(field.name)}, double ${bits})`, + ); + } if (nullable) { host.declare(`declare void @scr_union_release(ptr)`); B.line(`call void @scr_union_release(ptr ${fieldValue})`); @@ -920,6 +942,29 @@ export function streamTypedRefMaterializeAdapter( B.line(`${copied} = call ptr @scr_dyn_copy_property_descriptors(ptr ${out}, ptr ${bag})`); B.line(`call void @scr_dyn_release_v(ptr ${copied})`); } + // An Error subclass instance's own `name` sits where its class's + // construction creates it: after a field, or after every other + // property when no constructor sets it. + const nameAfter = meta.def.errorNameAfter; + if (meta.root.def.name === "%Error" && !meta.def.runtime && nameAfter !== null) { + host.declare(`declare void @scr_error_dyn_place_name(ptr, ptr, ${host.sizeType})`); + B.line( + nameAfter === undefined + ? `call void @scr_error_dyn_place_name(ptr ${out}, ptr null, ${host.sizeType} 0)` + : `call void @scr_error_dyn_place_name(ptr ${out}, ptr ${host.cstr(nameAfter)}, ${host.sizeType} ${Buffer.byteLength(nameAfter, "utf8")})`, + ); + } + // A frozen, sealed or non-extensible instance's view says so. + if (withAttributes) { + host.declare(`declare void @scr_dyn_class_view_attributes(ptr, double)`); + const slot = B.tmp(), + bits = B.tmp(); + B.line( + `${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${classFieldIndex(meta, CLASS_ATTRIBUTES).index}`, + ); + B.line(`${bits} = load double, ptr ${slot}`); + B.line(`call void @scr_dyn_class_view_attributes(ptr ${out}, double ${bits})`); + } B.terminate(`ret ptr ${out}`); } else if (t.kind === "record") { const shape = host.recordsById.get(t.shapeId); @@ -933,6 +978,7 @@ export function streamTypedRefMaterializeAdapter( * Declared-field records and tuples cover the live stream values. */ const ordinary = shape.indexValue || + shapeHasAccessorSlots(shape) || shape.fields.some((field) => field.name === "handleEvent" && field.type.kind === "func"); if (ordinary) { const out = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 44741edc8..38c6bdbe0 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -294,9 +294,12 @@ export const LIB_FN_SYMS: Record = { "process.rusage": "scr_process_rusage", "process.activeResources": "scr_active_resources", "process.exiting": "scr_process_exiting", + "process.ipcOpen": "scr_process_ipc_open", "process.exit": "scr_process_exit", "process.setExitCode": "scr_process_set_exit_code", "process.currentExitCode": "scr_process_exit_code_or_zero", + "process.setExitCodeTracked": "scr_process_set_exit_code_tracked", + "process.hasExitCode": "scr_process_has_exit_code", "process.envSet": "scr_env_set", "process.envUnset": "scr_env_unset", "process.envPairs": "scr_env_pairs", @@ -307,12 +310,13 @@ export const LIB_FN_SYMS: Record = { "date.now": "scr_date_now", "date.newNow": "scr_date_now", "date.newMs": "scr_date_new_ms", - "date.newString": "scr_date_parse_get_time", - "date.parse": "scr_date_parse_get_time", + "date.newString": "scr_date_parse", + "date.parse": "scr_date_parse", "date.getTime": "scr_date_get_time", "date.valueOf": "scr_date_get_time", - "date.parseGetTime": "scr_date_parse_get_time", + "date.parseGetTime": "scr_date_parse", "date.utc": "scr_date_utc", + "date.newLocal": "scr_date_new_local", "date.getFullYear": "scr_date_get_full_year_local", "date.getUTCFullYear": "scr_date_get_full_year_utc", "date.getMonth": "scr_date_get_month_local", @@ -827,6 +831,7 @@ export const LIB_FN_SYMS: Record = { "dyn.sameValue": "scr_dyn_same_value", "error.toString": "scr_error_to_string", "error.stack": "scr_error_stack", + "error.setStack": "scr_error_set_stack", "error.stackLimitGet": "scr_stack_limit_get", "error.stackLimitSet": "scr_stack_limit_set", "class.name": "scr_classobj_name", @@ -922,16 +927,27 @@ export const LIB_FN_SYMS: Record = { "dyn.promiseAll": "scr_dyn_promise_all", "dyn.promiseAllSettled": "scr_dyn_promise_all_settled", "dyn.promiseAny": "scr_dyn_promise_any", + "dyn.promiseResolve": "scr_dyn_promise_resolve", "dyn.typeof": "scr_dyn_typeof", - "dyn.objectTag": "scr_dyn_object_tag", + "dyn.objectTag": "scr_dyn_object_tag_of", "dyn.freeze": "scr_dyn_freeze", "dyn.isFrozen": "scr_dyn_is_frozen", + "class.defineField": "scr_class_define_field", + "class.definedBits": "scr_class_defined_bits", + "class.definedValue": "scr_class_defined_value", + "class.descriptorHasValue": "scr_class_descriptor_has_value", + "class.throwReadOnly": "scr_class_throw_read_only", + "class.throwNotExtensible": "scr_class_throw_not_extensible", "dyn.nativeSetNew": "scr_dyn_native_set_new", "dyn.nativeSetIs": "scr_dyn_native_set_is", "dyn.nativeMapIs": "scr_dyn_native_map_is", "dyn.nativeUrlIs": "scr_dyn_native_url_is", "dyn.nativeDateIs": "scr_dyn_native_date_is", + "dyn.globalConstructor": "scr_dyn_global_constructor", + "dyn.builtinFunction": "scr_dyn_builtin_function", "date.nativeNew": "scr_dyn_native_date_new", + "date.nativeNewFields": "scr_dyn_native_date_new_fields", + "date.nativeNewValue": "scr_dyn_native_date_new_value", "date.checkedValue": "scr_dyn_native_date_value", "dyn.nativeRegexIs": "scr_dyn_native_regex_is", "dyn.toString": "scr_dyn_to_string_argument", @@ -1655,4 +1671,5 @@ export const USES_TIMERS_LIB_FNS = new Set([ "timers.queueMicrotaskDyn", "timers.setImmediateFnValue", "timers.immediatePromise", + "dyn.builtinFunction", ]); diff --git a/packages/compiler/src/backend/stack-observations.test.ts b/packages/compiler/src/backend/stack-observations.test.ts index a95f0140f..0e1178ce9 100644 --- a/packages/compiler/src/backend/stack-observations.test.ts +++ b/packages/compiler/src/backend/stack-observations.test.ts @@ -1,5 +1,6 @@ import { expect, test } from "vitest"; import { + DYN, F64, STRING, VOID, @@ -49,6 +50,40 @@ test("a reachable stack read is observed", () => { expect(observesStacks(mod([fn("%main", [call("parse")]), fn("parse", [stackRead])]))).toBe(true); }); +test("a stack read on a checked-dynamic value is observed", () => { + const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN }; + const read = (key: string): IrStmt => ({ + loc, + kind: "exprStmt", + expr: { + loc, + kind: "dynKeyGet", + value, + key: { loc, kind: "strLit", value: key, type: STRING }, + type: DYN, + }, + }); + expect(observesStacks(mod([fn("%main", [read("stack")])]))).toBe(true); + expect(observesStacks(mod([fn("%main", [read("message")])]))).toBe(false); +}); + +test("a computed key reads the stack when the program spells it", () => { + const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN }; + const key: IrExpr = { loc, kind: "varRef", localId: "k", type: STRING }; + const computed: IrStmt = { + loc, + kind: "exprStmt", + expr: { loc, kind: "dynKeyGet", value, key, type: DYN }, + }; + const spelled: IrStmt = { + loc, + kind: "exprStmt", + expr: { loc, kind: "strLit", value: "stack", type: STRING }, + }; + expect(observesStacks(mod([fn("%main", [computed, spelled])]))).toBe(true); + expect(observesStacks(mod([fn("%main", [computed])]))).toBe(false); +}); + test("programs without stack reads are not", () => { expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false); }); diff --git a/packages/compiler/src/backend/stack-observations.ts b/packages/compiler/src/backend/stack-observations.ts index 7debf0751..cc6d109e4 100644 --- a/packages/compiler/src/backend/stack-observations.ts +++ b/packages/compiler/src/backend/stack-observations.ts @@ -13,12 +13,34 @@ import { writesPublished } from "./publication.js"; /** Lib calls that read a captured stack trace. */ const STACK_READS: ReadonlySet = new Set(["error.stack"]); -function readsStack(expr: IrExpr): boolean { - return expr.kind === "libCall" && STACK_READS.has(expr.fn); +/** A typed `error.stack` read, or a read on a checked-dynamic value that + * can reach an error's own `stack` accessor: the constant key `stack`, or + * a computed key when `computedKeys` (the program spells "stack", so a + * computed key can name it). A key the program assembles from other + * strings or from data it reads keeps the static answer. */ +function readsStack(expr: IrExpr, computedKeys: boolean): boolean { + if (expr.kind === "libCall") return STACK_READS.has(expr.fn); + if (expr.kind !== "dynKeyGet") return false; + if (expr.key.kind === "strLit") return expr.key.value === "stack"; + return computedKeys; } -function containsStackRead(fn: IrFunction): boolean { - return !everyStmtList(fn.body, { stmt: () => true, expr: (expr) => !readsStack(expr) }); +/** Whether the module contains the string literal "stack". */ +function spellsStack(mod: IrModule): boolean { + return mod.functions.some( + (fn) => + !everyStmtList(fn.body, { + stmt: () => true, + expr: (expr) => !(expr.kind === "strLit" && expr.value === "stack"), + }), + ); +} + +function containsStackRead(fn: IrFunction, computedKeys: boolean): boolean { + return !everyStmtList(fn.body, { + stmt: () => true, + expr: (expr) => !readsStack(expr, computedKeys), + }); } /** Visit every expression of `body` that execution can reach: statements @@ -43,7 +65,8 @@ function visitLiveExprs(body: IrStmt[], visit: (expr: IrExpr) => void): void { * and those reads must not cost every call of the program a frame. * Programs without any stack read answer false without the walk. */ export function observesStacks(mod: IrModule): boolean { - if (!mod.functions.some(containsStackRead)) return false; + const computedKeys = spellsStack(mod); + if (!mod.functions.some((fn) => containsStackRead(fn, computedKeys))) return false; const byName = new Map(mod.functions.map((fn) => [fn.name, fn])); const classNames = new Set((mod.classes ?? []).map((cls) => cls.name)); const pending: string[] = [...moduleFunctionReferences(mod)]; @@ -67,7 +90,7 @@ export function observesStacks(mod: IrModule): boolean { const fn = byName.get(name); if (fn === undefined) continue; visitLiveExprs(fn.body, (expr) => { - if (readsStack(expr)) observed = true; + if (readsStack(expr, computedKeys)) observed = true; if (expr.kind === "call" && !reached.has(expr.callee)) pending.push(expr.callee); const referenced = exprFunctionReference(expr); if (referenced !== undefined && !reached.has(referenced)) pending.push(referenced); diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 4acd6bc50..764592f1c 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -59,8 +59,9 @@ * order that differs from insertion order where the program * observes it (SC6002), localeCompare over text the built-in * collation does not cover (SC6003), a call feeding an instance - * to a cast to an unrelated class (SC6004), and Date strings the native parser does not - * accept (SC6005) + * to a cast to an unrelated class (SC6004); SC6005 (Date strings + * outside the native parser's grammar) is retired — the runtime + * parses date strings with V8's grammar * SC9xxx internal compiler errors (still source-anchored) */ import type { SrcLoc } from "../ir/ir.js"; @@ -200,18 +201,6 @@ export function unrelatedClassCastDivergenceDiag( }; } -/** SC6005 — a Date string literal outside the grammar the native parser - * accepts (ISO 8601 with an explicit offset, date-only forms, and the - * certificate-time shape): Node parses many more forms. */ -export function dateLiteralDivergenceDiag(text: string, loc: SrcLoc): ScrDiagnostic { - return { - code: "SC6005", - message: `the date string '${text}' parses to an invalid date (NaN) natively, while Node accepts it (the native parser takes ISO 8601 date-times with an explicit offset, such as '2024-01-02T03:04:05Z', and date-only forms)`, - loc, - hint: "write the date in ISO 8601 with an explicit offset ('Z' or '+HH:MM'), or build it from numbers with Date.UTC", - }; -} - /* ── SC5xxx: native FFI ───────────────────────────────────────────────── */ /** SC5001 — the outbound native-FFI manifest is malformed or unreadable. */ diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index 1b42c3eca..54cce5ef5 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -199,7 +199,7 @@ test("early executable cache misses on source and resolution changes", async () expect(await readEarlyExecutableCache(f.root, f.options)).toBeNull(); }); -test("early executable keys isolate compile modes, paths, implementation, and FFI manifests", async () => { +test("early executable keys isolate compile modes, paths, implementation, checker, and FFI manifests", async () => { const f = await fixture(); const tracker = new FrontendInputTracker(); tracker.run(() => trackedReadFile(f.source)); @@ -216,6 +216,7 @@ test("early executable keys isolate compile modes, paths, implementation, and FF { ...f.options, optimization: "dev" }, { ...f.options, optimization: "speed" }, { ...f.options, detachedExit: true }, + { ...f.options, checker: "tsc-ts-executable-identity" }, { ...f.options, npmStatic: "auto" }, { ...f.options, npmStatic: ["commander"] }, { ...f.options, outPath: join(f.options.outDir, "other") }, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 8c67c1006..b502dae06 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -116,6 +116,9 @@ export interface EarlyExecutableCacheOptions { windowsSubsystem?: "gui"; /** Omitted unless --detached-exit is set. */ detachedExit?: true; + /** Omitted for the default tsgo checker; with SCRIPTC_CHECKER=tsc-ts, the + * tsc-ts executable's file identity. */ + checker?: string; /** Omitted unless SCRIPTC_YOUNG_NURSERY=1 (backend/young-nursery.ts). */ youngNursery?: true; npmStatic: readonly string[] | "auto" | null; @@ -230,6 +233,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string { ...(options.strip ? ["strip"] : []), ...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []), ...(options.detachedExit ? ["detached-exit"] : []), + ...(options.checker !== undefined ? [`checker:${options.checker}`] : []), ...(options.youngNursery ? ["young-nursery"] : []), options.npmStatic === null ? "" @@ -270,6 +274,7 @@ function routeKey(options: Omit { expect(helperTokens(a)).not.toBe(helperTokens(b)); }); @@ -44,8 +46,12 @@ test.each([ "'\\8'", "`template`", "/regexp/", - "0", "0x10", + "07", + "1.5", + "1e3", + "1n", + "1_000", "a / b", "\\u0061", "café", diff --git a/packages/compiler/src/frontend/helper-tokens.ts b/packages/compiler/src/frontend/helper-tokens.ts index d7a968755..908cc0d22 100644 --- a/packages/compiler/src/frontend/helper-tokens.ts +++ b/packages/compiler/src/frontend/helper-tokens.ts @@ -1,8 +1,8 @@ /** Token fingerprints for the fixed bundler helpers we recognize. This is * deliberately not a JavaScript parser: regexes, templates, escaped names, - * numbers, and unknown punctuation refuse the helper. None occur in the - * canonical helpers. Every accepted token is represented in the result; - * comments and whitespace alone may disappear. */ + * numbers other than plain decimal integers, and unknown punctuation refuse + * the helper. None occur in the canonical helpers. Every accepted token is + * represented in the result; comments and whitespace alone may disappear. */ const operators = [ ">>>=", "===", @@ -157,6 +157,15 @@ export function helperTokens(text: string): string | null { result += fingerprint("s", token.value); i = token.end; previous = "string"; + } else if (/[0-9]/.test(ch)) { + // Plain decimal integers only (esbuild's `cb[keys(cb)[0]]`): a + // legacy octal, fraction, exponent, radix prefix, BigInt suffix or + // numeric separator refuses rather than aliasing another value. + const start = i++; + while (i < text.length && /[0-9]/.test(text.charAt(i))) i++; + if (/[A-Za-z0-9_$.\\]/.test(text.charAt(i)) || (ch === "0" && i - start > 1)) return null; + previous = text.slice(start, i); + result += fingerprint("n", previous); } else if (/[A-Za-z_$]/.test(ch)) { const start = i++; while (i < text.length && /[A-Za-z0-9_$]/.test(text.charAt(i))) i++; diff --git a/packages/compiler/src/frontend/input-tracker.ts b/packages/compiler/src/frontend/input-tracker.ts index 304a21cfa..fa2e92568 100644 --- a/packages/compiler/src/frontend/input-tracker.ts +++ b/packages/compiler/src/frontend/input-tracker.ts @@ -164,8 +164,28 @@ export function markFrontendInputsUnstable(): void { (synchronousTracker ?? activeTracker.getStore())?.markUnstable(); } +/** Compiler-synthesized module sources (a bundle's CommonJS chunks split + * into sibling modules, npm-static-bundled-chunks.ts). They answer like + * regular files but are never recorded: each derives from the content of + * a recorded bundle read, which already keys every cache that sees it. */ +const virtualSources = new Map(); + +export function setVirtualSource(path: string, text: string): void { + virtualSources.set(resolve(path), text); +} + +export function isVirtualSource(path: string): boolean { + return virtualSources.has(resolve(path)); +} + +export function clearVirtualSources(): void { + virtualSources.clear(); +} + export function trackedReadFile(path: string): string | null { path = resolve(path); + const virtual = virtualSources.get(path); + if (virtual !== undefined) return virtual; try { const text = readFileSync(path, "utf8"); record({ op: "file", path, digest: frontendSourceDigest(text) }); @@ -178,6 +198,7 @@ export function trackedReadFile(path: string): string | null { export function trackedFileExists(path: string): boolean { path = resolve(path); + if (virtualSources.has(path)) return true; const kind = pathKind(path); record({ op: "kind", path, kind }); return kind === "file"; @@ -185,6 +206,7 @@ export function trackedFileExists(path: string): boolean { export function trackedDirectoryExists(path: string): boolean { path = resolve(path); + if (virtualSources.has(path)) return false; const kind = pathKind(path); record({ op: "kind", path, kind }); return kind === "directory"; @@ -192,6 +214,7 @@ export function trackedDirectoryExists(path: string): boolean { export function trackedExists(path: string): boolean { path = resolve(path); + if (virtualSources.has(path)) return true; const kind = pathKind(path); record({ op: "kind", path, kind }); return kind !== "missing"; @@ -199,6 +222,7 @@ export function trackedExists(path: string): boolean { export function trackedRealpath(path: string): string | null { path = resolve(path); + if (virtualSources.has(path)) return path; try { const target = systemRealpath(path); record({ op: "realpath", path, target }); @@ -223,9 +247,13 @@ export function trackedAccessibleEntries( } files.sort(); directories.sort(); - const answer = { files, directories }; - record({ op: "entries", path, files, directories }); - return answer; + record({ op: "entries", path, files: [...files], directories }); + for (const virtual of virtualSources.keys()) { + if (dirname(virtual) === path && !files.includes(basename(virtual))) + files.push(basename(virtual)); + } + files.sort(); + return { files, directories }; } catch { // Enumeration can fail even while the path remains a directory. Preserve // the failed operation itself so a permission/ACL repair invalidates the diff --git a/packages/compiler/src/frontend/lowering/builtin-calls.ts b/packages/compiler/src/frontend/lowering/builtin-calls.ts index 7e1852600..6be798de8 100644 --- a/packages/compiler/src/frontend/lowering/builtin-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts @@ -12,6 +12,7 @@ import { lowerEventsEmitterHelper, lowerEventsOnce } from "./lower-event-emitter import type { Lowerer } from "./lowerer.js"; import { lowerTimersMemberCall } from "./lower-timers.js"; import { lowerStreamModuleCall } from "./lower-stream.js"; +import { lowerCallbackPromisify } from "./builtins/promisify-callback.js"; import { lowerNodeModuleCall } from "./builtins/module-resolution.js"; import { builtinModuleFnOf, builtinFenceHintOf } from "./surfaces.js"; @@ -87,6 +88,10 @@ export function lowerBuiltinCall( if (testServed) return testServed; const utilServed = lowerer.lowerUtilModuleCall(expr, bi, loc); if (utilServed) return utilServed; + if (bi.module === "util" && bi.member === "promisify") { + const promisified = lowerCallbackPromisify(lowerer, expr, loc); + if (promisified) return promisified; + } const streamServed = lowerStreamModuleCall(lowerer, expr, bi, loc); if (streamServed) return streamServed; const fsTs = lowerer.lowerFsToUnixTimestampCall(expr, bi, loc); diff --git a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts index 1669978af..afb221d04 100644 --- a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts +++ b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts @@ -1,11 +1,12 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; -import { isJsSourceFile } from "../../program.js"; +import { isJsSourceFile, locOf } from "../../program.js"; import { builtinModuleFnOf } from "../surfaces.js"; import { registerHttpClientFnBinding } from "../lower-server.js"; import { isPromisifiedZlibMember } from "./compression.js"; import { isPromisifiedCryptoMember } from "./crypto.js"; import { isPromisifiedDnsMember } from "../lower-dgram.js"; +import { callbackPromisifyShape } from "./promisify-callback.js"; /** Compile-time util.promisify projections. The immutable result binding * owns no runtime slot: calls rewrite to the target's promise lowering, @@ -164,11 +165,14 @@ export function registerPromisifiedBuiltinDecl( : target?.module === "dns" && isPromisifiedDnsMember(target.member) ? { kind: "promisified-dns" as const, member: target.member } : null; + // A typed callback-last function value: the call lowers to a native + // promisified function, and the binding holds it like any closure. + if (!projection && callbackPromisifyShape(lowerer, e, locOf(e)) !== null) return false; if (!projection) { lowerer.noLowering( "util.promisify of this target", argNode ?? e, - "child_process.exec, child_process.execFile, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions (randomBytes, pbkdf2, scrypt, hkdf, randomFill, randomInt, generateKey), dns.lookup, dns.lookupService and the fs callback functions with an fs/promises counterpart are the supported targets", + "child_process.exec, child_process.execFile, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions (randomBytes, pbkdf2, scrypt, hkdf, randomFill, randomInt, generateKey), dns.lookup, dns.lookupService, the fs callback functions with an fs/promises counterpart, and typed callback-last functions whose callback takes (error?: Error | null, value?) are the supported targets", ); } const symbol = lowerer.checker.getSymbolAtLocation(nameNode); diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index 32fe09ab3..31be995e7 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -58,9 +58,8 @@ export function lowerDateCall( } return { kind: "libCall", fn: "date.now", args: [], type: F64, loc }; } - // Date.parse uses the same bounded date-string parser as - // new Date(dateString).getTime(); keep their NaN and TimeClip behavior - // identical for the ISO timestamps used by CLI applications. + // Date.parse uses the same date-string parser as new Date(dateString) + // (V8's grammar in the runtime), so their NaN and TimeClip behavior agree. if (lowerer.stdlibGlobalMember(access, "Date") === "parse") { if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) { lowerer.noLowering(`Date.parse with ${call.arguments.nodes.length} arguments`, call); diff --git a/packages/compiler/src/frontend/lowering/builtins/ipc.ts b/packages/compiler/src/frontend/lowering/builtins/ipc.ts index cf9d96117..90448854d 100644 --- a/packages/compiler/src/frontend/lowering/builtins/ipc.ts +++ b/packages/compiler/src/frontend/lowering/builtins/ipc.ts @@ -1,7 +1,17 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; -import { BOOL, type IrExpr, type IrType, STRING, type SrcLoc } from "../../../ir/ir.js"; +import { + BOOL, + type IrExpr, + type IrStmt, + type IrType, + STRING, + type SrcLoc, + UNDEFINED_T, +} from "../../../ir/ir.js"; +import { nodeThrowExpr } from "../../../ir/build.js"; +import { processHasIpc } from "./process.js"; /** Method calls on ChildProcess receivers: `child.on("exit"|"error", cb)` * registers a listener with the event loop's child registry. The event @@ -86,8 +96,54 @@ export function ipcSendCallback(lowerer: Lowerer, node: ts.Expression): IrExpr { return cb; } +/** process.send in a program without fork targets. This binary never opens + * a channel then, so only a Node.js parent's NODE_CHANNEL_FD can define + * process.send, and that channel has no lowering: a call fences when it runs. + * Without any channel the calls behave as Node's do on an undefined member: + * `process.send?.(...)` skips its arguments and answers undefined, and + * `process.send(...)` evaluates them and throws a TypeError. The message is + * never sent, so its type is not checked. */ +function lowerChannelLessSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr { + const loc = locOf(call); + const fence = (type: IrType): IrExpr => + nodeThrowExpr( + 0, + "SC2020", + "process.send over an IPC channel opened by a Node.js parent (NODE_CHANNEL_FD) is not supported yet", + type, + loc, + ); + const hasIpc = processHasIpc(lowerer, loc); + if (call.questionDotToken) { + const type = lowerer.mapTypeOf(lowerer.typeOf(call)) ?? BOOL; + const absent: IrExpr = + type.kind === "bool" + ? { kind: "boolLit", value: false, type: BOOL, loc } + : lowerer.coerceToExpected( + { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type, + ); + return { kind: "ternary", cond: hasIpc, then: fence(type), else_: absent, type, loc }; + } + const stmts: IrStmt[] = call.arguments.nodes.map((arg) => ({ + kind: "exprStmt", + expr: lowerer.lowerExpr(ts.isSpreadElement(arg) ? arg.expression : arg), + loc: locOf(arg), + })); + const result: IrExpr = { + kind: "ternary", + cond: hasIpc, + then: fence(BOOL), + else_: nodeThrowExpr(1, "", "process.send is not a function", BOOL, loc), + type: BOOL, + loc, + }; + return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: BOOL, loc }; +} + export function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr { const loc = locOf(call); + if (lowerer.forkTargets.length === 0) return lowerChannelLessSend(lowerer, call); if ( call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2 || diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 555eaf84f..eb3703a53 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -17,6 +17,7 @@ import { type IrExpr, type IrLibFn, type IrStmt, + type IrType, STRING, type SrcLoc, UNDEFINED_T, @@ -26,10 +27,149 @@ import { funcOf, } from "../../../ir/ir.js"; import { lowerBuiltinLoaderValue } from "../lower-builtin-values.js"; +import { builtinFunctionValue } from "../lower-exprs.js"; import { canonicalBuiltinModule } from "../../builtin-modules.js"; import { lowerProcessIpcSend, ipcMessageListener, ipcDisconnectListener } from "./ipc.js"; import { lowerDiagnosticsSubscriber, lowerTracingArguments } from "./async-context.js"; +/** Whether the process has an IPC channel, Node's test for defining + * process.send and process.disconnect: a channel this fork child opened + * (programs with fork targets only), or a non-empty NODE_CHANNEL_FD from a + * Node.js parent, the variable Node itself tests at startup. Built from the + * existing environment read, so the presence test links no runtime code. */ +export function processHasIpc(lowerer: Lowerer, loc: SrcLoc): IrExpr { + const envT = lowerer.envValueType(); + const unionId = (envT as IrType & { kind: "union" }).unionId; + const stringTag = lowerer.armTag(unionId, STRING); + const value = lowerer.declareHiddenLocal("%channelFd", envT); + const fromNode: IrExpr = { + kind: "seqExpr", + stmts: [ + { + kind: "varDecl", + localId: value.id, + init: { + kind: "libCall", + fn: "process.envGet", + args: [strLit("NODE_CHANNEL_FD", loc)], + type: envT, + loc, + }, + loc, + }, + ], + result: { + kind: "logical", + op: "&&", + left: { + kind: "unionIsTag", + unionId, + tag: stringTag, + negated: false, + value: varRef(value.id, envT, loc), + type: BOOL, + loc, + }, + right: { + kind: "toBool", + operand: { + kind: "unionNarrow", + unionId, + tag: stringTag, + value: varRef(value.id, envT, loc), + type: STRING, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + if (lowerer.forkTargets.length === 0) return fromNode; + return { + kind: "logical", + op: "||", + left: { kind: "libCall", fn: "process.ipcOpen", args: [], type: BOOL, loc }, + right: fromNode, + type: BOOL, + loc, + }; +} + +/** True when the value of `node` is consumed only by ToBoolean: a + * condition, the operand of `!`, `Boolean(node)`, or an operand of `&&` / + * `||` whose own value is consumed that way (or discarded, for the left + * operand). */ +export function isTruthinessOnlyRead(lowerer: Lowerer, node: ts.Expression): boolean { + let child: ts.Node = node; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) { + child = parent; + parent = parent.parent; + } + if (parent === undefined) return false; + if (ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent)) + return parent.expression === child; + if (ts.isForStatement(parent)) return parent.condition === child; + if (ts.isConditionalExpression(parent)) return parent.condition === child; + if (ts.isPrefixUnaryExpression(parent)) return parent.operator === ts.SyntaxKind.ExclamationToken; + if (ts.isCallExpression(parent)) { + return ( + !parent.questionDotToken && + parent.arguments.nodes.length === 1 && + parent.arguments.nodes[0] === child && + lowerer.isStdlibGlobal(parent.expression, "Boolean") + ); + } + if (ts.isBinaryExpression(parent)) { + const op = parent.operatorToken.kind; + if (op !== ts.SyntaxKind.AmpersandAmpersandToken && op !== ts.SyntaxKind.BarBarToken) + return false; + if (isTruthinessOnlyRead(lowerer, parent)) return true; + let outer: ts.Node | undefined = parent.parent; + while (outer !== undefined && ts.isParenthesizedExpression(outer)) outer = outer.parent; + return parent.left === child && outer !== undefined && ts.isExpressionStatement(outer); + } + return false; +} + +/** Whether a program source reads process.exitCode. Such a program + * records its exit-code assignments (process.setExitCodeTracked), so a + * read can tell an unset code from 0; other programs keep the untracked + * write. Computed once per lowering pass. */ +export function programReadsExitCode(lowerer: Lowerer): boolean { + if (lowerer.readsExitCode !== null) return lowerer.readsExitCode; + let found = false; + const walk = (node: ts.Node): void => { + if (found) return; + if ( + ts.isPropertyAccessExpression(node) && + node.name.text === "exitCode" && + !( + ts.isBinaryExpression(node.parent) && + node.parent.left === node && + node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) && + lowerer.stdlibGlobalMember(node, "process") === "exitCode" + ) { + found = true; + return; + } + ts.forEachChild(node, walk); + }; + for (const sf of lowerer.program.getImplementationSourceFiles()) { + if (sf.isDeclarationFile) continue; + walk(sf); + if (found) break; + } + lowerer.readsExitCode = found; + return found; +} + /** The pinned Node release the runtime reports as process.versions.node * (packages/runtime/src/scr_lib.c SCR_NODE_COMPAT_VERSION, kept equal to * internal/compatibility/node-v24.json by test/node-pin.test.ts). */ @@ -374,9 +514,45 @@ export function lowerProcessProperty( const member = lowerer.stdlibGlobalMember(expr, "process"); if (member === null) return null; const loc = locOf(expr); + // `if (process.send)`: Node defines process.send and process.disconnect + // only when the process has an IPC channel. A read whose value only + // feeds a truthiness test answers that presence; other value uses keep + // the member fence. + if ( + (member === "send" || member === "disconnect") && + !expr.questionDotToken && + isTruthinessOnlyRead(lowerer, expr) + ) { + return processHasIpc(lowerer, loc); + } if (member === "versions") { return { kind: "libCall", fn: "process.versions", args: [], type: DYN, loc }; } + // process.exitCode: undefined until something assigns it, then the + // assigned code. A read the checker narrowed to number follows an + // assignment, so the code is set. + if (member === "exitCode" && !expr.questionDotToken) { + const current: IrExpr = { + kind: "libCall", + fn: "process.currentExitCode", + args: [], + type: F64, + loc, + }; + if (lowerer.mapTypeOf(lowerer.typeOf(expr))?.kind === "f64") return current; + const type: IrType = { kind: "union", unionId: lowerer.unions.intern([F64, UNDEFINED_T]) }; + return { + kind: "ternary", + cond: { kind: "libCall", fn: "process.hasExitCode", args: [], type: BOOL, loc }, + then: lowerer.coerceToExpected(current, type), + else_: lowerer.coerceToExpected( + { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type, + ), + type, + loc, + }; + } // process.features as a value (`"sea" in process.features`): an object // holding none of the flags, consistent with the member reads above. if (member === "features") { @@ -403,6 +579,11 @@ export function lowerProcessProperty( if (member === "hrtime") { return { kind: "libCall", fn: "process.hrtimeValue", args: [], type: DYN, loc }; } + // A stored process.nextTick in JavaScript (`cond ? setImmediate : + // process.nextTick`): the interned builtin function value. + if (member === "nextTick" && isJsSourceFile(expr.getSourceFile())) { + return builtinFunctionValue("process.nextTick", loc); + } if (member === "getBuiltinModule") { // Keep the native loader's argument validation when the function is // stored. The declaration's string signature must not insert a checked @@ -1575,10 +1756,36 @@ export function lowerProcessMethodCall( } if (member === "exit") { const arg = call.arguments.nodes[0]; - const code: IrExpr = + let code: IrExpr = arg !== undefined ? lowerer.lowerExprExpecting(arg, F64) : { kind: "libCall", fn: "process.currentExitCode", args: [], type: F64, loc }; + // process.exit(code) assigns process.exitCode before the 'exit' + // listeners run. Only a program that reads the property can observe + // that, so only such a program records the assignment. + if (arg !== undefined && programReadsExitCode(lowerer)) { + const binding = lowerer.declareHiddenLocal("%exitCode", F64); + code = { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: binding.id, init: code, loc }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "process.setExitCodeTracked", + args: [varRef(binding.id, F64, loc)], + type: VOID, + loc, + }, + loc, + }, + ], + result: varRef(binding.id, F64, loc), + type: F64, + loc, + }; + } return { kind: "libCall", fn: "process.exit", args: [code], type: VOID, loc }; } return null; // process.argv(...) etc. are tsc errors before lowering @@ -1594,6 +1801,16 @@ export function lowerProcessOptionalMethodCall( lowerer: Lowerer, expr: ts.CallExpression, ): IrExpr | null { + // `process.send!(message)`: the asserted direct call of the optional + // member (the @types/node spelling under strict null checks). + if ( + !expr.questionDotToken && + ts.isNonNullExpression(expr.expression) && + ts.isPropertyAccessExpression(expr.expression.expression) && + !expr.expression.expression.questionDotToken && + lowerer.stdlibGlobalMember(expr.expression.expression, "process") === "send" + ) + return lowerProcessIpcSend(lowerer, expr); if (!expr.questionDotToken) return null; if (!ts.isPropertyAccessExpression(expr.expression)) return null; const member = lowerer.stdlibGlobalMember(expr.expression, "process"); diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 2656da18b..55290f720 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -1,7 +1,8 @@ -import { checkedPromiseAll } from "../checked-promise-all.js"; +import { checkedPromiseAll, thenReader } from "../checked-promise-all.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; -import { type Lowerer } from "../lowerer.js"; +import { newFnCtx, type Lowerer } from "../lowerer.js"; +import { varRef } from "../../../ir/build.js"; import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js"; import { isJsSourceFile, locOf } from "../../program.js"; import { @@ -10,6 +11,7 @@ import { type IrFunction, type IrStmt, type IrType, + UNDEFINED_T, VOID, arrayOf, isUnitType, @@ -17,6 +19,168 @@ import { typeKey, } from "../../../ir/ir.js"; +/** `Promise.all([p, q, r])` over DIFFERENT promise types — the checker's + * tuple overload, `Promise<[A, B, C]>`. One interned async helper per + * entry/result signature takes the entries as arguments, awaits the + * runtime combinator over the same promises (each widened to + * Promise, a type-only view), so settlement is Node's exactly: the + * first rejection in settlement order wins and every later one counts as + * handled; then it reads each (now fulfilled) entry and returns the + * tuple. Entries keep their identity — no value converts through the + * checked-dynamic tree. Non-promise entries (numbers, strings, booleans, + * unit values — never thenable) pass straight into their slot; a + * checked-dynamic entry (a JavaScript function's result) joins through + * PromiseResolve: the promise it holds, a promise that adopts a thenable, + * or a promise fulfilled with its plain value. The tuple + * fulfills one microtask turn after the combinator does. Null when the + * result type or an entry has no typed home here. */ +function lowerTuplePromiseAll( + lowerer: Lowerer, + call: ts.CallExpression, + elems: IrExpr[], +): IrExpr | null { + const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); + if (resultT?.kind !== "promise" || resultT.inner.kind !== "record") return null; + const shape = lowerer.shapes.get(resultT.inner.shapeId); + if (!shape?.tuple || shape.fields.length !== elems.length) return null; + const plainValue = (t: IrType): boolean => + t.kind === "f64" || t.kind === "string" || t.kind === "bool" || isUnitType(t); + // A checked-dynamic entry (a JavaScript function's result) may hold a + // promise or a plain value at run time; it joins through its promise view. + const settles = (t: IrType): boolean => t.kind === "promise" || t.kind === "dyn"; + if (!elems.some((e) => settles(e.type))) return null; + if (!elems.every((e) => settles(e.type) || plainValue(e.type))) return null; + const loc = locOf(call); + const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(resultT.inner)}`; + let name = lowerer.arrHofHelpers.get(`promise.allTuple:${key}`); + if (name === undefined) { + name = `%promise.allTuple.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(`promise.allTuple:${key}`, name); + const ctx = newFnCtx(false, null, null, resultT.inner); + ctx.isAsync = true; + lowerer.fnStack.push(ctx); + try { + const params = elems.map((e, i) => ({ + localId: `entry.${i}`, + name: `entry${i}`, + type: e.type, + })); + const ref = (i: number): IrExpr => varRef(`entry.${i}`, elems[i]!.type, loc); + const dynPromise: IrType = { kind: "promise", inner: DYN }; + const body: IrStmt[] = []; + // Each dyn entry's promise view, in entry order: the promise it + // holds, a promise that adopts a thenable's settlement, or a promise + // fulfilled with the plain value (PromiseResolve's rule). + const views = new Map(); + elems.forEach((e, i) => { + if (e.type.kind !== "dyn") return; + const view = lowerer.declareHiddenLocal("%allEntry", dynPromise); + body.push({ + kind: "varDecl", + localId: view.id, + init: { + kind: "libCall", + fn: "dyn.promiseResolve", + args: [ref(i), thenReader(lowerer, loc)], + type: dynPromise, + loc, + }, + loc, + }); + views.set(i, varRef(view.id, dynPromise, loc)); + }); + const widened: IrExpr[] = []; + elems.forEach((e, i) => { + const view = views.get(i); + if (view !== undefined) { + widened.push({ + kind: "promiseVoidWiden", + value: view, + type: { kind: "promise", inner: VOID }, + loc, + }); + return; + } + if (e.type.kind !== "promise") return; + widened.push( + e.type.inner.kind === "void" + ? ref(i) + : { + kind: "promiseVoidWiden", + value: ref(i), + type: { kind: "promise", inner: VOID }, + loc, + }, + ); + }); + const settled: IrExpr = { + kind: "awaitExpr", + value: { + kind: "intrinsic", + name: "promise.all", + args: [ + { + kind: "arrayLit", + elems: widened, + type: arrayOf({ kind: "promise", inner: VOID }), + loc, + }, + ], + type: { kind: "promise", inner: VOID }, + loc, + }, + type: VOID, + loc, + }; + // Shape fields are sorted by name ("10" before "2"); a tuple field's + // name is its position. + const fields = shape.fields.map((field) => { + const i = Number(field.name); + const entry = elems[i]!; + let value: IrExpr; + const view = views.get(i); + if (view !== undefined) { + value = { kind: "awaitExpr", value: view, type: DYN, loc }; + } else if (entry.type.kind === "promise") { + value = + entry.type.inner.kind === "void" + ? { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } + : { kind: "awaitExpr", value: ref(i), type: entry.type.inner, loc }; + } else { + value = ref(i); + } + return { name: field.name, value: lowerer.coerceToExpected(value, field.type) }; + }); + body.push( + { kind: "exprStmt", expr: settled, loc }, + { + kind: "return", + value: { kind: "recordLit", fields, type: resultT.inner, loc }, + loc, + }, + ); + lowerer.liftedFns.push({ + name, + params, + returnType: resultT.inner, + locals: [ + ...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), + ...lowerer.ctx.locals, + ], + body, + async: true, + loc, + }); + } catch (e) { + lowerer.arrHofHelpers.delete(`promise.allTuple:${key}`); + throw e; + } finally { + lowerer.fnStack.pop(); + } + } + return { kind: "call", callee: name, args: elems, type: resultT, loc }; +} + /** `Promise.race([...])` on THE Promise global: the entries lower * individually (the array never materializes — promise-element arrays * have no representation) into a promise.race intrinsic; the result @@ -65,12 +229,26 @@ export function lowerPromiseStaticCall( const compatible = (inner: IrType): boolean => { if (typeEquals(inner, resultT.inner)) return true; if (!resultArms) return false; + // A Promise entry fulfills with undefined, which the result + // holds when the checker folded `void` into its undefined arm. + if (inner.kind === "void") return resultArms.some((a) => a.kind === "undefinedT"); if (inner.kind === "union") { const arms = lowerer.unions.get(inner.unionId)?.arms ?? []; return arms.every((a) => resultArms.some((b) => typeEquals(a, b))); } return resultArms.some((a) => typeEquals(a, inner)); }; + // A Promise entry (the deadline idiom: `new Promise((_, + // reject) => setTimeout(reject, ms))`) can only reject, so it races + // under any result type; its inner type maps to void. + const neverEntry = (el: ts.Expression): boolean => { + const t = lowerer.typeOf(el); + const arg = + t.getSymbol()?.name === "Promise" + ? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0] + : undefined; + return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0; + }; const entries = argNode.elements.nodes.map((el) => { const entry = lowerer.lowerExpr(el); if (entry.type.kind !== "promise") { @@ -80,7 +258,7 @@ export function lowerPromiseStaticCall( "JS would resolve plain values — wrap them: new Promise((r) => r(v))", ); } - if (!compatible(entry.type.inner)) { + if (!compatible(entry.type.inner) && !(entry.type.inner.kind === "void" && neverEntry(el))) { lowerer.unsupported( "SC1090", el, @@ -140,6 +318,10 @@ export function lowerPromiseStaticCall( return { kind: "intrinsic", name: "promise.all", args: [entriesArr], type: resultT, loc }; } + if (!isJsSourceFile(call.getSourceFile())) { + const tuple = lowerTuplePromiseAll(lowerer, call, elems); + if (tuple) return tuple; + } if ( isJsSourceFile(call.getSourceFile()) && elems.every((entry) => lowerer.dynConvertible(entry.type)) diff --git a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts new file mode 100644 index 000000000..cdb8a1fbb --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts @@ -0,0 +1,377 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { varRef } from "../../../ir/build.js"; +import { + BOOL, + type IrExpr, + type IrLocal, + type IrParam, + type IrStmt, + type IrType, + type SrcLoc, + VOID, + funcOf, + isUnitType, + typeEquals, + typeKey, +} from "../../../ir/ir.js"; + +const ERROR_T: IrType = { kind: "object", className: "%Error" }; + +/** The parts of `util.promisify(fn)` for a typed callback-last function: + * `fn(...args, (err, value) => void)` becomes `(...args) => Promise`. */ +export interface CallbackShape { + fnT: IrType & { kind: "func" }; + resultT: IrType & { kind: "func" }; + args: readonly IrType[]; + callbackT: IrType & { kind: "func" }; + /** The parameter's declared type: callbackT, or `callbackT | undefined` + * for an optional callback. */ + callbackParamT: IrType; + inner: IrType; + /** The error parameter's union and the tag of its Error arm, or null when + * the callback declares no error parameter. */ + error: { type: IrType; errorTag: number; unitTags: number[] } | null; +} + +function callbackShape( + lowerer: Lowerer, + fnT: IrType | null, + resultT: IrType | null, + loc: SrcLoc, +): CallbackShape | null { + if (fnT?.kind !== "func" || resultT?.kind !== "func") return null; + if (fnT.rest || fnT.restAbi || fnT.argumentsAll || resultT.rest || resultT.restAbi) return null; + const callbackParamT = fnT.params[fnT.params.length - 1]; + // An optional callback (`close(callback?: (err?: Error) => void)`) takes + // its function arm; the call passes the callback into the union. + const callbackT = + callbackParamT?.kind === "union" + ? (() => { + const arms = lowerer.unions.get(callbackParamT.unionId)?.arms ?? []; + const fns = arms.filter((arm) => arm.kind === "func"); + return fns.length === 1 && + arms.every((arm) => arm.kind === "func" || arm.kind === "undefinedT") + ? fns[0] + : undefined; + })() + : callbackParamT; + if (callbackT?.kind !== "func" || callbackT.ret.kind !== "void") return null; + if (callbackT.rest || callbackT.restAbi || callbackT.params.length > 2) return null; + const args = fnT.params.slice(0, -1); + if ( + resultT.ret.kind !== "promise" || + resultT.params.length !== args.length || + !resultT.params.every((type, i) => typeEquals(type, args[i]!)) + ) + return null; + const inner = resultT.ret.inner; + let error: CallbackShape["error"] = null; + const errorT = callbackT.params[0]; + if (errorT !== undefined) { + if (errorT.kind !== "union") return null; + const arms = lowerer.unions.get(errorT.unionId)?.arms ?? []; + const errorTags = arms.flatMap((arm, tag) => + arm.kind === "object" && + (arm.className === "%Error" || lowerer.isSubclassOf(arm.className, "%Error")) + ? [tag] + : [], + ); + const unitTags = arms.flatMap((arm, tag) => (isUnitType(arm) ? [tag] : [])); + if (errorTags.length !== 1 || errorTags.length + unitTags.length !== arms.length) return null; + error = { type: errorT, errorTag: errorTags[0]!, unitTags }; + } + if (inner.kind !== "void") { + const valueT = callbackT.params[1]; + if (valueT === undefined) return null; + const probe: IrExpr = { kind: "varRef", localId: "%probe", type: valueT, loc }; + if (!typeEquals(lowerer.coerceToExpected(probe, inner).type, inner)) return null; + } + return { fnT, resultT, args, callbackT, callbackParamT: callbackParamT!, inner, error }; +} + +/** The callback shape of a `promisify(fn)` call, or null. */ +export function callbackPromisifyShape( + lowerer: Lowerer, + call: ts.CallExpression, + loc: SrcLoc, +): CallbackShape | null { + if (call.questionDotToken || call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; + if (ts.isSpreadElement(argNode)) return null; + return callbackShape( + lowerer, + lowerer.mapTypeOf(lowerer.typeOf(argNode)), + lowerer.mapTypeOf(lowerer.typeOf(call)), + loc, + ); +} + +function captureParam(localId: string, type: IrType): IrParam { + return { localId, name: localId, type }; +} + +function boxedLocal(localId: string, type: IrType): IrLocal { + return { id: localId, name: localId, type, mutable: false, boxed: true }; +} + +function plainLocal(localId: string, type: IrType): IrLocal { + return { id: localId, name: localId, type, mutable: false }; +} + +/** `util.promisify(fn)` over a typed callback-last function value (a bound + * method, a function declaration, a closure): a native function that calls + * `fn` with the arguments and a callback, and settles its promise from the + * callback like Node's wrapper: a truthy error rejects, otherwise the first + * value fulfills. A synchronous throw rejects (the executor's rule). Null + * when the argument or result does not have that typed shape. */ +export function lowerCallbackPromisify( + lowerer: Lowerer, + call: ts.CallExpression, + loc: SrcLoc, +): IrExpr | null { + const shape = callbackPromisifyShape(lowerer, call, loc); + if (shape === null) return null; + const argNode = call.arguments.nodes[0]!; + let fn = lowerer.lowerExpr(argNode); + if (!typeEquals(fn.type, shape.fnT)) fn = lowerer.coerceToExpected(fn, shape.fnT); + if (!typeEquals(fn.type, shape.fnT)) return null; + return { + kind: "call", + callee: promisifyMaker(lowerer, shape, loc), + args: [fn], + type: shape.resultT, + loc, + }; +} + +/** Interns the helper family for one (function, result) type pair and + * answers the maker: `(fn) => closure(...args) => Promise`. */ +function promisifyMaker(lowerer: Lowerer, shape: CallbackShape, loc: SrcLoc): string { + const key = `promisify:${typeKey(shape.fnT)}=>${typeKey(shape.resultT)}`; + const existing = lowerer.arrHofHelpers.get(key); + if (existing) return existing; + const base = `%promisify.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, base); + const { fnT, resultT, args, callbackT, callbackParamT, inner, error } = shape; + const promiseT = resultT.ret; + const resolveT = funcOf(inner.kind === "void" ? [] : [inner], VOID); + const rejectT = funcOf([ERROR_T], VOID); + const executorT = funcOf([resolveT, rejectT], VOID); + const argIds = args.map((_, i) => `a${i}.0`); + + // The callback: (err, value) => err ? reject(err) : resolve(value). + const cbParams: IrParam[] = []; + callbackT.params.forEach((type, i) => + cbParams.push(captureParam(i === 0 ? "err.0" : "value.0", type)), + ); + const cbBody: IrStmt[] = []; + if (error !== null) { + const err = varRef("err.0", error.type, loc); + let truthy: IrExpr | null = null; + for (const tag of error.unitTags) { + const part: IrExpr = { + kind: "unionIsTag", + unionId: (error.type as IrType & { kind: "union" }).unionId, + tag, + negated: true, + value: err, + type: BOOL, + loc, + }; + truthy = + truthy === null + ? part + : { kind: "logical", op: "&&", left: truthy, right: part, type: BOOL, loc }; + } + const errorArm = lowerer.unions.get((error.type as IrType & { kind: "union" }).unionId)!.arms[ + error.errorTag + ]!; + let reason: IrExpr = { + kind: "unionNarrow", + unionId: (error.type as IrType & { kind: "union" }).unionId, + tag: error.errorTag, + value: err, + type: errorArm, + loc, + }; + if (!typeEquals(errorArm, ERROR_T)) + reason = { kind: "upcast", value: reason, type: ERROR_T, loc }; + const reject: IrStmt[] = [ + { + kind: "exprStmt", + expr: { + kind: "callValue", + callee: varRef("rej.0", rejectT, loc), + args: [reason], + type: VOID, + loc, + }, + loc, + }, + { kind: "return", value: null, loc }, + ]; + cbBody.push( + truthy === null ? reject[0]! : { kind: "if", cond: truthy, then: reject, else_: null, loc }, + ); + if (truthy === null) cbBody.push(reject[1]!); + } + const value = + inner.kind === "void" + ? [] + : [lowerer.coerceToExpected(varRef("value.0", callbackT.params[1]!, loc), inner)]; + cbBody.push({ + kind: "exprStmt", + expr: { + kind: "callValue", + callee: varRef("res.0", resolveT, loc), + args: value, + type: VOID, + loc, + }, + loc, + }); + lowerer.liftedFns.push({ + name: `${base}.cb`, + params: cbParams, + returnType: VOID, + captures: [captureParam("res.0", resolveT), captureParam("rej.0", rejectT)], + locals: [ + boxedLocal("res.0", resolveT), + boxedLocal("rej.0", rejectT), + ...cbParams.map((p) => plainLocal(p.localId, p.type)), + ], + body: cbBody, + loc, + }); + lowerer.liftedFns.push({ + name: `${base}.mkcb`, + params: [captureParam("res.0", resolveT), captureParam("rej.0", rejectT)], + returnType: callbackT, + locals: [boxedLocal("res.0", resolveT), boxedLocal("rej.0", rejectT)], + body: [ + { + kind: "return", + value: { + kind: "closure", + fnName: `${base}.cb`, + captures: ["res.0", "rej.0"], + type: callbackT, + loc, + }, + loc, + }, + ], + loc, + }); + + // The executor: (resolve, reject) => fn(...args, callback). + const captured: IrParam[] = [captureParam("f.0", fnT)]; + args.forEach((type, i) => captured.push(captureParam(argIds[i]!, type))); + lowerer.liftedFns.push({ + name: `${base}.exec`, + params: [captureParam("res.0", resolveT), captureParam("rej.0", rejectT)], + returnType: VOID, + captures: captured, + locals: [ + ...captured.map((p) => boxedLocal(p.localId, p.type)), + plainLocal("res.0", resolveT), + plainLocal("rej.0", rejectT), + ], + body: [ + { + kind: "exprStmt", + expr: { + kind: "callValue", + callee: varRef("f.0", fnT, loc), + args: [ + ...args.map((type, i) => varRef(argIds[i]!, type, loc)), + lowerer.coerceToExpected( + { + kind: "call", + callee: `${base}.mkcb`, + args: [varRef("res.0", resolveT, loc), varRef("rej.0", rejectT, loc)], + type: callbackT, + loc, + }, + callbackParamT, + ), + ], + // The function's own return type; the value is discarded. + type: fnT.ret, + loc, + }, + loc, + }, + ], + loc, + }); + lowerer.liftedFns.push({ + name: `${base}.mkexec`, + params: captured, + returnType: executorT, + locals: captured.map((p) => boxedLocal(p.localId, p.type)), + body: [ + { + kind: "return", + value: { + kind: "closure", + fnName: `${base}.exec`, + captures: captured.map((p) => p.localId), + type: executorT, + loc, + }, + loc, + }, + ], + loc, + }); + + // The promisified function: (...args) => new Promise(executor). + const implParams: IrParam[] = []; + args.forEach((type, i) => implParams.push(captureParam(argIds[i]!, type))); + lowerer.liftedFns.push({ + name: `${base}.impl`, + params: implParams, + returnType: promiseT, + captures: [captureParam("f.0", fnT)], + locals: [boxedLocal("f.0", fnT), ...implParams.map((p) => plainLocal(p.localId, p.type))], + body: [ + { + kind: "return", + value: { + kind: "newPromise", + executor: { + kind: "call", + callee: `${base}.mkexec`, + args: [ + varRef("f.0", fnT, loc), + ...implParams.map((p) => varRef(p.localId, p.type, loc)), + ], + type: executorT, + loc, + }, + type: promiseT, + loc, + }, + loc, + }, + ], + loc, + }); + lowerer.liftedFns.push({ + name: base, + params: [captureParam("f.0", fnT)], + returnType: resultT, + locals: [boxedLocal("f.0", fnT)], + body: [ + { + kind: "return", + value: { kind: "closure", fnName: `${base}.impl`, captures: ["f.0"], type: resultT, loc }, + loc, + }, + ], + loc, + }); + return base; +} diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 246cf1de4..7570fff88 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -27,6 +27,7 @@ import { isStreamUndefCallExpr } from "./lower-server.js"; import { nsPathPrefix } from "./lower-namespaces.js"; import { declSymbolOf } from "./lower-modules.js"; import { npmStaticPackageOfPath } from "../npm-static.js"; +import { restPackingAdapterFits } from "./coercions/function-adapters.js"; import { implicitMonoFile, implicitAnyParamSymbolsOf } from "./generic-functions.js"; /** How a parameter participates in CALL-SITE COMPLETION (the frontend @@ -525,6 +526,21 @@ export function requireExactArityValue( // optional/defaulted params spell their `T | undefined` slots (mapType's // completed-signature contract), so omitted trailing args complete with // the undefined arm like any direct call. + // A nested function declaration whose only non-required parameter is a + // typed rest: its value carries the typed-rest ABI its own type spells, + // and every use converts it at its slot (same signature, or the rest + // packing adapter for fixed parameters). + if ( + contextual === null && + ts.isFunctionDeclaration(blame) && + blame.name !== undefined && + funcType.kind === "func" && + funcType.restAbi === "typed" && + shapes.every((s) => s.mode === "required" || s.mode === "rest") + ) { + const own = lowerer.mapTypeOf(lowerer.typeOf(blame.name)); + if (own && typeEquals(own, funcType)) return; + } const target = contextual ? lowerer.checker.getContextualType(contextual) : undefined; const mapped = target ? lowerer.mapTypeOf(target) @@ -585,6 +601,16 @@ export function requireExactArityValue( ) { return; } + // A typed-rest function in a slot with fixed parameters (`log(...m: + // string[])` into `(m: string) => void`): the slot coercion's packing + // adapter collects the slot's arguments into the rest array. + if ( + mappedFn?.kind === "func" && + funcType.kind === "func" && + restPackingAdapterFits(lowerer, funcType, mappedFn) + ) { + return; + } // An uncontextualized npm-static implementation function can still // have a declaration-backed completed ABI: the package's .d.ts // projection supplies the body-facing parameter/return types while diff --git a/packages/compiler/src/frontend/lowering/checked-promise-all.ts b/packages/compiler/src/frontend/lowering/checked-promise-all.ts index c3be91833..6c24a8b2c 100644 --- a/packages/compiler/src/frontend/lowering/checked-promise-all.ts +++ b/packages/compiler/src/frontend/lowering/checked-promise-all.ts @@ -11,7 +11,7 @@ const COMBINATOR_LIB_CALLS = { /** `value.then` through the program's own property dispatch, so a class * instance's `then` method is visible to the runtime's thenable check. */ -function thenReader(lowerer: Lowerer, loc: SrcLoc): IrExpr { +export function thenReader(lowerer: Lowerer, loc: SrcLoc): IrExpr { const name = "%promise.thenOf"; const type = funcOf([DYN], DYN); if (!lowerer.liftedFns.some((fn) => fn.name === name)) { diff --git a/packages/compiler/src/frontend/lowering/class-attributes.ts b/packages/compiler/src/frontend/lowering/class-attributes.ts new file mode 100644 index 000000000..4e7bbe801 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/class-attributes.ts @@ -0,0 +1,440 @@ +import * as ts from "../ts7/adapter.js"; +import { + BOOL, + CLASS_ATTRIBUTES, + CLASS_FROZEN, + CLASS_NOT_EXTENSIBLE, + CLASS_SEALED, + DYN, + F64, + FIELD_NOT_WRITABLE, + STRING, + VOID, + canConvertToDyn, + canDynCheckTo, + classFieldAttributesName, + isClassOwnEnumerableFieldName, + isDynTypedRefType, + type IrExpr, + type IrFunction, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../ir/ir.js"; +import { numLit, strLit, varRef } from "../../ir/build.js"; +import { transformStmtList } from "../../ir/traverse.js"; +import { locOf } from "../program.js"; +import { type Lowerer, PoisonError } from "./lowerer.js"; +import type { ClassInfo } from "./lower-classes.js"; +import { classPropertiesHelper } from "./class-dynamic-dispatch.js"; +import { runtimeRooted } from "./class-descriptors.js"; + +/* Object.freeze, Object.seal, Object.preventExtensions and + * Object.defineProperty on class instances. Only the hierarchies these calls + * name pay for them: such a hierarchy gets a CLASS_ATTRIBUTES integrity word + * per instance, an attribute word per redefined field, and a check before + * each write to a declared field (guardClassAttributeWrites). The instance's + * checked view carries the attributes, so reflection answers from it. */ + +type ClassObjectType = Extract; + +function attributeClass(lowerer: Lowerer, target: IrExpr): ClassInfo | null { + if (!isDynTypedRefType(target.type)) return null; + const info = lowerer.classes.get(target.type.className); + if (!info || runtimeRooted(info) || info.decl == null) return null; + return info; +} + +/** Adds a zero-initialized f64 to every class of the hierarchy, at the same + * offset, like the property bag. */ +function addHiddenField(info: ClassInfo, name: string): void { + let root = info; + while (root.base && !root.base.def.runtime) root = root.base; + if (root.fields.has(name)) return; + const index = root.def.fields.length; + const add = (current: ClassInfo): void => { + if (!current.fields.has(name)) { + current.fields.set(name, F64); + current.def.fields.splice(index, 0, { name, type: F64 }); + } + current.subclasses.forEach(add); + }; + add(root); +} + +function hiddenFieldGet(receiver: IrExpr, className: string, field: string, loc: SrcLoc): IrExpr { + return { kind: "fieldGet", obj: receiver, className, field, type: F64, loc }; +} + +/** Object.freeze, Object.seal and Object.preventExtensions of a class + * instance: sets the instance's integrity bits, then applies the operation to + * the properties added to the instance. Answers the instance. */ +export function lowerClassIntegrity( + lowerer: Lowerer, + call: ts.CallExpression, + member: "freeze" | "seal" | "preventExtensions", + target: IrExpr, +): IrExpr | null { + const info = attributeClass(lowerer, target); + if (!info) return null; + addHiddenField(info, CLASS_ATTRIBUTES); + const loc = locOf(call); + const type = target.type as ClassObjectType; + const bits = + member === "freeze" + ? CLASS_NOT_EXTENSIBLE | CLASS_SEALED | CLASS_FROZEN + : member === "seal" + ? CLASS_NOT_EXTENSIBLE | CLASS_SEALED + : CLASS_NOT_EXTENSIBLE; + const slot = lowerer.declareHiddenLocal("%integrityReceiver", type); + const receiver = (): IrExpr => varRef(slot.id, type, loc); + const bag: IrExpr = { + kind: "call", + callee: classPropertiesHelper(lowerer, loc).name, + args: [lowerer.coerceToExpected(receiver(), DYN)], + type: DYN, + loc, + }; + const operation = { + freeze: "dyn.freeze", + seal: "dyn.seal", + preventExtensions: "dyn.preventExtensions", + } as const; + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: slot.id, init: target, loc }, + { + kind: "fieldSet", + obj: receiver(), + className: type.className, + field: CLASS_ATTRIBUTES, + value: { + kind: "bin", + op: "|", + left: hiddenFieldGet(receiver(), type.className, CLASS_ATTRIBUTES, loc), + right: numLit(bits, loc), + type: F64, + loc, + }, + attributesChecked: true, + loc, + }, + { + kind: "exprStmt", + expr: { kind: "libCall", fn: operation[member], args: [bag], type: DYN, loc }, + loc, + }, + ], + result: receiver(), + type, + loc, + }; +} + +/** An object literal with a get or set property. */ +function literalAccessorDescriptor(node: ts.Expression): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + return ( + ts.isObjectLiteralExpression(node) && + node.properties.nodes.some( + (property) => + property.name !== undefined && + (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && + (property.name.text === "get" || property.name.text === "set"), + ) + ); +} + +/** Object.defineProperty with a string-literal key naming a declared field + * of a class instance: a data descriptor changes the field's value and + * attributes, validated as Node validates ordinary objects. Answers the + * instance, or null when the call is not of that form. An object-literal + * accessor descriptor is refused at compile time; other accessor + * descriptors throw SC2020 when the call runs. */ +export function lowerClassFieldDefine( + lowerer: Lowerer, + call: ts.CallExpression, + target: IrExpr, +): IrExpr | null { + const info = attributeClass(lowerer, target); + const key = call.arguments.nodes[1]!; + if (!info || !ts.isStringLiteral(key)) return null; + const field = key.text; + const fieldType = info.fields.get(field); + if ( + fieldType === undefined || + !isClassOwnEnumerableFieldName(field) || + [...(info.symbolFields?.values() ?? [])].includes(field) + ) + return null; + if (literalAccessorDescriptor(call.arguments.nodes[2]!)) { + lowerer.pushDiag({ + code: "SC2020", + message: `Object.defineProperty with an accessor descriptor for the declared field '${field}' is not supported yet`, + loc: locOf(call), + }); + throw new PoisonError(); + } + const getRecord = (id: string) => lowerer.shapes.get(id); + const getUnion = (id: string) => lowerer.unions.get(id); + if ( + !canConvertToDyn(fieldType, getRecord, getUnion) || + !canDynCheckTo(fieldType, getRecord, getUnion) + ) + return null; + const attributesField = classFieldAttributesName(field); + addHiddenField(info, CLASS_ATTRIBUTES); + addHiddenField(info, attributesField); + const loc = locOf(call); + const type = target.type as ClassObjectType; + const receiverSlot = lowerer.declareHiddenLocal("%defineReceiver", type); + const descriptorSlot = lowerer.declareHiddenLocal("%defineDescriptor", DYN); + const holderSlot = lowerer.declareHiddenLocal("%defineHolder", DYN); + const receiver = (): IrExpr => varRef(receiverSlot.id, type, loc); + const descriptor = (): IrExpr => varRef(descriptorSlot.id, DYN, loc); + const holder = (): IrExpr => varRef(holderSlot.id, DYN, loc); + const name = (): IrExpr => strLit(field, loc); + const current: IrExpr = { + kind: "fieldGet", + obj: receiver(), + className: type.className, + field, + type: fieldType, + loc, + }; + const defined: IrExpr = { + kind: "libCall", + fn: "class.definedValue", + args: [holder(), name()], + type: DYN, + loc, + }; + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: receiverSlot.id, init: target, loc }, + { + kind: "varDecl", + localId: descriptorSlot.id, + init: lowerer.lowerExprExpecting(call.arguments.nodes[2]!, DYN), + loc, + }, + { + kind: "varDecl", + localId: holderSlot.id, + init: { + kind: "libCall", + fn: "class.defineField", + args: [ + lowerer.coerceToExpected(current, DYN), + hiddenFieldGet(receiver(), type.className, attributesField, loc), + hiddenFieldGet(receiver(), type.className, CLASS_ATTRIBUTES, loc), + name(), + descriptor(), + ], + type: DYN, + loc, + }, + loc, + }, + { + kind: "if", + cond: { + kind: "libCall", + fn: "class.descriptorHasValue", + args: [descriptor()], + type: BOOL, + loc, + }, + then: [ + { + kind: "fieldSet", + obj: receiver(), + className: type.className, + field, + value: lowerer.coerceToExpected(defined, fieldType), + attributesChecked: true, + loc, + }, + ], + else_: null, + loc, + }, + { + kind: "fieldSet", + obj: receiver(), + className: type.className, + field: attributesField, + value: { + kind: "libCall", + fn: "class.definedBits", + args: [holder(), name()], + type: F64, + loc, + }, + attributesChecked: true, + loc, + }, + ], + result: receiver(), + type, + loc, + }; +} + +function classDisplayName(info: ClassInfo): string { + return info.def.jsName ?? info.decl?.name?.text ?? info.def.name.replace(/^%/, ""); +} + +/** The instance's own class name for an error message: a helper testing the + * most derived classes of the hierarchy first. */ +function instanceClassName( + lowerer: Lowerer, + info: ClassInfo, + receiver: IrExpr, + helpers: Map, + loc: SrcLoc, +): IrExpr { + if (info.subclasses.length === 0) return strLit(classDisplayName(info), loc); + let helper = helpers.get(info.def.name); + if (!helper) { + const type: IrType = { kind: "object", className: info.def.name }; + const value = varRef("value", type, loc); + const descendants = (current: ClassInfo): ClassInfo[] => + current.subclasses.flatMap((child) => [...descendants(child), child]); + const body: IrStmt[] = descendants(info).map((child): IrStmt => ({ + kind: "if", + cond: { kind: "instanceOf", value, className: child.def.name, type: BOOL, loc }, + then: [{ kind: "return", value: strLit(classDisplayName(child), loc), loc }], + else_: null, + loc, + })); + body.push({ kind: "return", value: strLit(classDisplayName(info), loc), loc }); + helper = { + name: `%class.instanceName.${helpers.size}`, + params: [{ localId: "value", name: "value", type }], + returnType: STRING, + locals: [{ id: "value", name: "value", type, mutable: false }], + body, + loc, + }; + helpers.set(info.def.name, helper); + } + return { kind: "call", callee: helper.name, args: [receiver], type: STRING, loc }; +} + +/** Checks the integrity word, and the field's attribute word when it has one, + * before every write to a declared field of a hierarchy that has them. A + * read-only field throws Node's TypeError in strict mode code; a sloppy-mode + * write leaves it unchanged. Other classes keep their plain stores. */ +export function guardClassAttributeWrites(lowerer: Lowerer, functions: IrFunction[]): void { + const guarded = new Set( + [...lowerer.classes.values()] + .filter((info) => info.def.fields.some((field) => field.name === CLASS_ATTRIBUTES)) + .map((info) => info.def.name), + ); + if (guarded.size === 0) return; + const helpers = new Map(); + for (const fn of functions) { + const local = (type: IrType, loc: SrcLoc): Extract => { + const id = `%attributeWrite.${fn.locals.length}`; + fn.locals.push({ id, name: id, type, mutable: false }); + return { kind: "varRef", localId: id, type, loc }; + }; + fn.body = transformStmtList(fn.body, { + stmt: (statement) => { + if ( + statement.kind !== "fieldSet" || + statement.attributesChecked || + !guarded.has(statement.className) || + !isClassOwnEnumerableFieldName(statement.field) + ) + return statement; + const info = lowerer.classes.get(statement.className)!; + const loc = statement.loc; + const receiver = local(statement.obj.type, loc); + const value = local(statement.value.type, loc); + const masked = (field: string, bit: number): IrExpr => ({ + kind: "bin", + op: "&", + left: hiddenFieldGet(receiver, statement.className, field, loc), + right: numLit(bit, loc), + type: F64, + loc, + }); + const attributesField = classFieldAttributesName(statement.field); + let readOnly = masked( + CLASS_ATTRIBUTES, + statement.define ? CLASS_NOT_EXTENSIBLE : CLASS_FROZEN, + ); + if (!statement.define && info.fields.has(attributesField)) { + readOnly = { + kind: "bin", + op: "|", + left: readOnly, + right: masked(attributesField, FIELD_NOT_WRITABLE), + type: F64, + loc, + }; + } + const reject: IrStmt[] = statement.define + ? [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "class.throwNotExtensible", + args: [strLit(statement.field, loc)], + type: VOID, + loc, + }, + loc, + }, + ] + : statement.sloppy + ? [] + : [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "class.throwReadOnly", + args: [ + strLit(statement.field, loc), + instanceClassName(lowerer, info, receiver, helpers, loc), + ], + type: VOID, + loc, + }, + loc, + }, + ]; + return { + kind: "block", + body: [ + { kind: "varDecl", localId: receiver.localId, init: statement.obj, loc }, + { kind: "varDecl", localId: value.localId, init: statement.value, loc }, + { + kind: "if", + cond: { + kind: "bin", + op: "!==", + left: readOnly, + right: numLit(0, loc), + type: BOOL, + loc, + }, + then: reject, + else_: [{ ...statement, obj: receiver, value, attributesChecked: true }], + loc, + }, + ], + loc, + }; + }, + expr: (expression) => expression, + }); + } + for (const helper of helpers.values()) functions.push(helper); +} diff --git a/packages/compiler/src/frontend/lowering/class-construction.ts b/packages/compiler/src/frontend/lowering/class-construction.ts index 7f4a003c9..30f504b4a 100644 --- a/packages/compiler/src/frontend/lowering/class-construction.ts +++ b/packages/compiler/src/frontend/lowering/class-construction.ts @@ -18,6 +18,7 @@ import type { Lowerer } from "./lowerer.js"; import type { ParamShape } from "./call-signatures.js"; import { classPrototypeData } from "./class-prototypes.js"; import { classInstanceOf } from "./class-dynamic-dispatch.js"; +import { inheritedBuiltinCtorFence } from "./lower-classes.js"; const PREFIX = "%class.construct:"; const PACKED = `${PREFIX}packed`; @@ -380,6 +381,29 @@ export class ClassConstructionDispatch { changed = true; continue; } + const type: IrType = { kind: "classval", className: name }; + const callee = varRef("p.0", DYN, loc); + // A constructor inherited from a runtime stream or EventEmitter + // takes no arguments here: arguments report at run time, as they do + // at compile time for typed construction. + const fence = inheritedBuiltinCtorFence(lowerer, info, loc); + if (fence !== null && fn.name !== PACKED && fn.params.length > 1) { + fn.body.unshift({ + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.classIs", + args: [callee, { kind: "strLit", value: typeKey(type), type: STRING, loc }], + type: BOOL, + loc, + }, + then: [fence], + else_: null, + loc, + }); + changed = true; + continue; + } const args = constructorArguments( lowerer, info.ctorParams, @@ -388,8 +412,6 @@ export class ClassConstructionDispatch { fn.name === PACKED ? varRef("p.1", DYN, loc) : undefined, ); if (!args) continue; - const type: IrType = { kind: "classval", className: name }; - const callee = varRef("p.0", DYN, loc); const value = constructedValue(lowerer, info, { kind: "newValue", callee: { kind: "dynCheck", value: callee, type, loc }, @@ -400,6 +422,28 @@ export class ClassConstructionDispatch { const body: IrStmt[] = [ { kind: "return", value: lowerer.coerceToExpected(value, DYN), loc }, ]; + // A spread call's argument count is known only at run time. + if (fence !== null && fn.name === PACKED) + body.unshift({ + kind: "if", + cond: { + kind: "bin", + op: ">", + left: { + kind: "libCall", + fn: "dyn.arrLen", + args: [varRef("p.1", DYN, loc)], + type: F64, + loc, + }, + right: { kind: "numLit", value: 0, type: F64, loc }, + type: BOOL, + loc, + }, + then: [fence], + else_: null, + loc, + }); fn.body.unshift({ kind: "if", cond: { diff --git a/packages/compiler/src/frontend/lowering/class-descriptors.ts b/packages/compiler/src/frontend/lowering/class-descriptors.ts index 473ed2496..cb980f0d0 100644 --- a/packages/compiler/src/frontend/lowering/class-descriptors.ts +++ b/packages/compiler/src/frontend/lowering/class-descriptors.ts @@ -23,7 +23,7 @@ function bagOnlyProperty(owner: ClassInfo, name: string): boolean { return !owner.fields.has(name) && owner.subclasses.every((child) => bagOnlyProperty(child, name)); } -function runtimeRooted(info: ClassInfo): boolean { +export function runtimeRooted(info: ClassInfo): boolean { for (let current: ClassInfo | null = info; current; current = current.base) { if ( current.def.runtime || diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index d2ef7f6cd..5fc2a3cec 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -1,3 +1,4 @@ +import { primitiveConstructorOr } from "./lower-exprs.js"; import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js"; import { constructedValue } from "./constructor-returns.js"; import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js"; @@ -2070,6 +2071,49 @@ export class ClassDynamicDispatch { // A read compares the bag with the receiver and then reads it: // evaluate it once, so each read retains the receiver once. const bagRead = varRef("bag.0", DYN, loc); + const bagGet: IrExpr | null = read + ? { + kind: "libCall", + fn: "dyn.bagGet", + args: [bagRead, read.key, varRef("p.0", DYN, loc)], + type: DYN, + loc, + } + : null; + // A class instance whose class chain declares no member of an + // Object.prototype method's name (classes that do answer in + // their own branches) inherits it unless an own property + // shadows it; a class constructor's static bag reads as before. + const instanceRead: IrExpr | null = + read && INHERITED_OBJECT_METHODS.has(name) + ? { + kind: "ternary", + cond: { + kind: "dynTest", + test: "function", + value: varRef("p.0", DYN, loc), + type: BOOL, + loc, + }, + then: bagGet!, + else_: { + kind: "ternary", + cond: this.readBagHas(lowerer, "dyn.hasKey", bagRead, read.key, loc), + then: bagGet!, + else_: { + kind: "libCall", + fn: "dyn.builtinMethod", + args: [{ kind: "strLit", value: "Object", type: STRING, loc }, read.key], + type: DYN, + loc, + }, + type: DYN, + loc, + }, + type: DYN, + loc, + } + : bagGet; const fallback: IrExpr = read ? { kind: "ternary", @@ -2081,13 +2125,7 @@ export class ClassDynamicDispatch { loc, }, then: { ...read, value: bagRead }, - else_: { - kind: "libCall", - fn: "dyn.bagGet", - args: [bagRead, read.key, varRef("p.0", DYN, loc)], - type: DYN, - loc, - }, + else_: instanceRead!, type: DYN, loc, } @@ -2112,7 +2150,20 @@ export class ClassDynamicDispatch { ] : [ { kind: "varDecl", localId: "bag.0", init: bag, loc }, - { kind: "return", value: fallback, loc }, + { + kind: "return", + // Primitives answer their wrapper constructors. + value: + name === "constructor" + ? primitiveConstructorOr( + lowerer, + varRef("p.0", DYN, loc), + fallback, + loc, + ) + : fallback, + loc, + }, ], loc, }; @@ -2695,6 +2746,7 @@ export class ClassDynamicDispatch { !findGenericMethodOn(lowerer, info, dispatch.name) && dispatch.name !== "constructor" && !info.fields.has(dispatch.name) && + !(dispatch.name === "stack" && lowerer.isSubclassOf(info.def.name, "%Error")) && !findMethodOn(lowerer, info, `get:${dispatch.name}`) && !findMethodOn(lowerer, info, `set:${dispatch.name}`)) ) @@ -4525,6 +4577,110 @@ export class ClassDynamicDispatch { }, ]; } + // An Error's own `stack`: a string is the native error's stack, which + // typed reads share; any other value is kept with the instance's own + // properties, which a read checks first. A class whose chain declares + // a `stack` accessor answers through it (the accessor code below). + if ( + name === "stack" && + !info.fields.has(name) && + lowerer.isSubclassOf(info.def.name, "%Error") && + !findMethodOn(lowerer, info, "get:stack") && + !findMethodOn(lowerer, info, "set:stack") + ) { + const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc }; + const error = lowerer.upcastTo(receiver, "%Error"); + if (write) { + const helper = classPropertiesHelper(lowerer, loc); + this.generated.add(helper); + const store: IrStmt = { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.keySet", + args: [ + { + kind: "call", + callee: helper.name, + args: [lowerer.coerceToExpected(receiver, DYN)], + type: DYN, + loc, + }, + key, + varRef("p.1", DYN, loc), + ], + type: VOID, + loc, + }, + loc, + }; + return [ + { + kind: "if", + cond: { + kind: "dynTest", + test: "string", + value: varRef("p.1", DYN, loc), + type: BOOL, + loc, + }, + then: [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "error.setStack", + args: [error, lowerer.coerceToExpected(varRef("p.1", DYN, loc), STRING)], + type: VOID, + loc, + }, + loc, + }, + { + kind: "if", + cond: this.readBagHas( + lowerer, + "dyn.hasKey", + this.readPropertyBag(lowerer, receiver, loc), + key, + loc, + ), + then: [store], + else_: null, + loc, + }, + ], + else_: [store], + loc, + }, + { kind: "return", value: null, loc }, + ]; + } + const bag = this.readPropertyBag(lowerer, receiver, loc); + return [ + { + kind: "return", + value: { + kind: "ternary", + cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc), + then: { + kind: "libCall", + fn: "dyn.bagGet", + args: [bag, key, lowerer.coerceToExpected(receiver, DYN)], + type: DYN, + loc, + }, + else_: lowerer.coerceToExpected( + { kind: "libCall", fn: "error.stack", args: [error], type: STRING, loc }, + DYN, + ), + type: DYN, + loc, + }, + loc, + }, + ]; + } if ((name === "message" || name === "name") && lowerer.isSubclassOf(info.def.name, "%Error")) return write ? [ @@ -4715,19 +4871,40 @@ export class ClassDynamicDispatch { if (!field && !accessor) { const instance = lowerer.coerceToExpected(receiver, DYN); const bag = this.readPropertyBagOrEmpty(lowerer, instance, loc); - return [ - { - kind: "return", - value: { - kind: "libCall", - fn: "dyn.bagGet", - args: [bag, { kind: "strLit", value: name, type: STRING, loc }, instance], - type: DYN, + const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc }; + const own: IrExpr = { + kind: "libCall", + fn: "dyn.bagGet", + args: [bag, key, instance], + type: DYN, + loc, + }; + // An Object.prototype method no class in the chain declares: an + // own property shadows it, otherwise the read answers the same + // interned value `Object.prototype.` reads. + if (INHERITED_OBJECT_METHODS.has(name) && !findMethodOn(lowerer, info, name)) { + return [ + { + kind: "return", + value: { + kind: "ternary", + cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc), + then: own, + else_: { + kind: "libCall", + fn: "dyn.builtinMethod", + args: [{ kind: "strLit", value: "Object", type: STRING, loc }, key], + type: DYN, + loc, + }, + type: DYN, + loc, + }, loc, }, - loc, - }, - ]; + ]; + } + return [{ kind: "return", value: own, loc }]; } const type = field ?? accessor!.sig.ret; if (!canConvertToDyn(type, getRecord, getUnion)) return fence(); @@ -5250,3 +5427,12 @@ export class ClassDynamicDispatch { return body; } } + +/** Object.prototype methods every class instance inherits unless its class + * chain declares them. */ +const INHERITED_OBJECT_METHODS: ReadonlySet = new Set([ + "valueOf", + "toString", + "hasOwnProperty", + "propertyIsEnumerable", +]); diff --git a/packages/compiler/src/frontend/lowering/class-iterables.ts b/packages/compiler/src/frontend/lowering/class-iterables.ts new file mode 100644 index 000000000..8c58fc0bd --- /dev/null +++ b/packages/compiler/src/frontend/lowering/class-iterables.ts @@ -0,0 +1,35 @@ +import type { IrExpr, IrType } from "../../ir/ir.js"; +import type { Lowerer } from "./lowerer.js"; +import { accessorCall } from "./lower-classes.js"; + +/** The generator a class instance's `[Symbol.iterator]` returns, when that + * method is a generator method taking no arguments: `obj[Symbol.iterator]()`, + * called once, as for-of, spread and Array.from call it. Null for other + * values, and for classes whose iterator the program can replace at run + * time (an instance property, a prototype write). */ +export function classGeneratorIterable(lowerer: Lowerer, value: IrExpr): IrExpr | null { + if (value.type.kind !== "object" || !iteratesThroughGenerator(lowerer, value.type)) return null; + const className = value.type.className; + const ret = lowerer.findMethodOn(lowerer.classes.get(className)!, "sym:iterator")!.sig.ret; + return accessorCall(lowerer, className, "sym:iterator", value, [], ret, value.loc); +} + +/** Whether values of `type` iterate through a generator method (the check + * classGeneratorIterable makes, from the type alone). */ +export function iteratesThroughGenerator(lowerer: Lowerer, type: IrType): boolean { + if (type.kind !== "object") return false; + const info = lowerer.classes.get(type.className); + if (!info || info.def.runtime || info.fields.has("sym:iterator")) return false; + if ( + lowerer.prototypeMethodAccesses.has("sym:iterator") || + lowerer.replacedSymbolMethods.has("sym:iterator") + ) + return false; + const found = lowerer.findMethodOn(info, "sym:iterator"); + return ( + !!found && + found.sig.params.length === 0 && + found.sig.ret.kind === "generator" && + !found.sig.ret.async + ); +} diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index c07ec9136..981cd0c1a 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -43,9 +43,19 @@ export function collectClassMethodMutations( const jsAssignments: (ts.PropertyAccessExpression | ts.ElementAccessExpression)[] = []; const computedMethods: ts.Expression[] = []; const typedClasses: (ts.ClassDeclaration | ts.ClassExpression)[] = []; + const symbolMethods = new Set(); let unknownKey: ts.Node | undefined; let numericKey: ts.Node | undefined; let unknownOwnField = false; + // `sym:iterator` for `Symbol.iterator`: the name a class method declared + // with a well-known symbol key takes. + const wellKnownMethodName = (key: ts.Expression): string | null => { + while (ts.isParenthesizedExpression(key)) key = key.expression; + if (!ts.isPropertyAccessExpression(key)) return null; + const member = lowerer.stdlibGlobalMember(key, "Symbol"); + if (member === null || !(lowerer.typeOf(key).flags & ts.TypeFlags.ESSymbolLike)) return null; + return `sym:${member}`; + }; // JavaScript method inference and dynamic class factories keep their // existing dispatch contract. The native-call proof applies to typed // declarations (including JavaScript files TypeScript checks), while @@ -555,6 +565,19 @@ export function collectClassMethodMutations( lowerer.prototypeMethodAccesses.set(name, site); } for (const { key, receiver, site } of computedWrites) { + // A class method declared with a well-known symbol key is not a write; + // any other write with one can replace that method. + const symbolName = key ? wellKnownMethodName(unwrap(key)) : null; + if (symbolName) { + const declaration = + (ts.isMethodDeclaration(site) || + ts.isGetAccessorDeclaration(site) || + ts.isSetAccessorDeclaration(site)) && + (ts.isClassDeclaration(site.parent) || ts.isClassExpression(site.parent)); + if (declaration) symbolMethods.add(symbolName); + else lowerer.replacedSymbolMethods.add(symbolName); + continue; + } const type = key ? lowerer.typeOf(key) : undefined; const names = type && (literalValues(type) ?? (key ? literalLoopKeys(unwrap(key)) : null)); if (names) { @@ -585,8 +608,10 @@ export function collectClassMethodMutations( if (String(Number(name)) === name) markName(name, numericKey); } } - if (unknownKey) + if (unknownKey) { for (const method of methods) lowerer.prototypeMethodAccesses.set(method, unknownKey); + for (const method of symbolMethods) lowerer.replacedSymbolMethods.add(method); + } if (!unknownKey && !unknownOwnField) for (const field of arrowFields) { if (!receiverFields.has(field)) lowerer.receiverFreeCallbackFields.add(field); diff --git a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts index b99c24a15..445014b88 100644 --- a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts +++ b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts @@ -111,12 +111,15 @@ export function coercibleValue(lowerer: Lowerer, src: IrType, dst: IrType): bool } /** Whether all parameters and the result adapt without a trap-only path. - * Rest signatures are excluded; extra destination parameters are ignored. */ + * Rest signatures are excluded, except a typed-rest source in a slot with + * fixed parameters (the rest packing adapter); extra destination + * parameters are ignored. */ export function cleanFuncAdaptable( lowerer: Lowerer, src: IrType & { kind: "func" }, dst: IrType & { kind: "func" }, ): boolean { + if (restPackingAdapterFits(lowerer, src, dst)) return true; if (src.rest === true || dst.rest === true) return false; if (src.params.length > dst.params.length) return false; for (let i = 0; i < src.params.length; i++) { @@ -127,15 +130,145 @@ export function cleanFuncAdaptable( return src.ret.kind === "void" && dst.ret.kind === "dyn"; } +/** Whether a typed-rest function (`(...m: T[]) => R`) can serve a slot + * with fixed parameters (`(m: T) => void`): the slot's parameters convert + * to the function's fixed parameters, then to the rest element type. */ +export function restPackingAdapterFits( + lowerer: Lowerer, + fromT: IrType & { kind: "func" }, + toT: IrType & { kind: "func" }, +): boolean { + if (fromT.rest !== true || fromT.restAbi !== "typed" || toT.rest === true) return false; + const fixed = fromT.params.length - 1; + const restT = fromT.params[fixed]; + if (restT?.kind !== "array" || toT.params.length < fixed) return false; + for (let i = 0; i < toT.params.length; i++) { + if (!lowerer.coercibleValue(toT.params[i]!, i < fixed ? fromT.params[i]! : restT.elem)) + return false; + } + return toT.ret.kind === "void" || lowerer.coercibleValue(fromT.ret, toT.ret); +} + +/** The adapter for restPackingAdapterFits: forwards the fixed arguments and + * packs the remaining ones into the rest array, as a JavaScript call of the + * function with those arguments does. */ +function restPackingAdapter( + lowerer: Lowerer, + fromT: IrType & { kind: "func" }, + toT: IrType & { kind: "func" }, + loc: SrcLoc, +): string { + const key = `fnrestadapt:${typeKey(fromT)}:${typeKey(toT)}`; + const existing = lowerer.coercions.retags.get(key); + if (existing) return existing; + const name = `%fn.adapt.${lowerer.coercions.retags.size}`; + lowerer.coercions.retags.set(key, name); + lowerer.freshClosureAdapters.add(name); + const fixed = fromT.params.length - 1; + const restT = fromT.params[fixed] as IrType & { kind: "array" }; + const params: IrParam[] = toT.params.map((t, i) => ({ + localId: `a.${i}`, + name: `a${i}`, + type: t, + })); + const arg = (i: number, want: IrType): IrExpr => + lowerer.coerceToExpected( + { kind: "varRef", localId: `a.${i}`, type: toT.params[i]!, loc }, + want, + ); + const args: IrExpr[] = []; + for (let i = 0; i < fixed; i++) args.push(arg(i, fromT.params[i]!)); + const packed: IrExpr[] = []; + for (let i = fixed; i < toT.params.length; i++) packed.push(arg(i, restT.elem)); + args.push({ kind: "arrayLit", elems: packed, type: restT, loc }); + const call: IrExpr = { + kind: "callValue", + callee: { kind: "varRef", localId: "f.0", type: fromT, loc }, + receiver: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc }, + args, + type: fromT.ret, + loc, + }; + const body: IrStmt[] = + toT.ret.kind === "void" + ? [ + { kind: "exprStmt", expr: call, loc }, + { kind: "return", value: null, loc }, + ] + : [{ kind: "return", value: lowerer.coerceToExpected(call, toT.ret), loc }]; + lowerer.liftedFns.push(...buildFunctionAdapter(name, fromT, toT, params, [], body, loc)); + return name; +} + +/** Adapt a fixed-parameter function to a checked-dynamic rest signature + * without fixed parameters (`() => {}` into mock.fn()'s `(...args: any[]) + * => undefined`): the adapter receives the hidden argument array and + * passes its element i as parameter i, undefined when the caller passed + * fewer arguments, as JS does. Null unless every parameter is + * checked-dynamic, which holds a missing argument as undefined, and the + * result is discarded or converts. */ +function funcIntoDynRestAdapter( + lowerer: Lowerer, + fromT: IrType & { kind: "func" }, + toT: IrType & { kind: "func" }, + loc: SrcLoc, +): string | null { + if (fromT.rest === true || fromT.params.some((p) => p.kind !== "dyn")) return null; + if (toT.ret.kind !== "void" && !lowerer.coercibleValue(fromT.ret, toT.ret)) return null; + const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`; + const existing = lowerer.coercions.retags.get(key); + if (existing) return existing; + const name = `%fn.adapt.${lowerer.coercions.retags.size}`; + lowerer.coercions.retags.set(key, name); + lowerer.freshClosureAdapters.add(name); + const params: IrParam[] = [{ localId: "rest.0", name: "args", type: DYN }]; + const call: IrExpr = { + kind: "callValue", + callee: { kind: "varRef", localId: "f.0", type: fromT, loc }, + receiver: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc }, + args: fromT.params.map((_, i): IrExpr => ({ + kind: "dynKeyGet", + value: { kind: "varRef", localId: "rest.0", type: DYN, loc }, + key: { kind: "strLit", value: String(i), type: STRING, loc }, + type: DYN, + loc, + })), + type: fromT.ret, + loc, + }; + let body: IrStmt[]; + if (toT.ret.kind === "void") { + body = [ + { kind: "exprStmt", expr: call, loc }, + { kind: "return", value: null, loc }, + ]; + } else { + const result = lowerer.coerceToExpected(call, toT.ret); + if (!typeEquals(result.type, toT.ret)) + throw new InternalCompilerError("lowerer bug: probed fn-adapter return stopped coercing"); + body = [{ kind: "return", value: result, loc }]; + } + lowerer.liftedFns.push(...buildFunctionAdapter(name, fromT, toT, params, [], body, loc)); + return name; +} + /** Adapt a function value to a destination signature, forwarding and * converting its arguments and result. Unsupported parameter/result pairs - * produce checked traps when called; rest signatures remain refused. */ + * produce checked traps when called. Rest signatures are refused, except a + * typed-rest source in a slot with fixed parameters, which the rest + * packing adapter serves, and a fixed-parameter function into a + * checked-dynamic rest signature without fixed parameters + * (funcIntoDynRestAdapter). */ export function funcCoerceAdapter( lowerer: Lowerer, fromT: IrType & { kind: "func" }, toT: IrType & { kind: "func" }, loc: SrcLoc, ): string | null { + if (restPackingAdapterFits(lowerer, fromT, toT)) + return restPackingAdapter(lowerer, fromT, toT, loc); + if (toT.rest === true && toT.restAbi === undefined && toT.params.length === 0) + return funcIntoDynRestAdapter(lowerer, fromT, toT, loc); if (fromT.rest === true || toT.rest === true) return null; if (fromT.params.length > toT.params.length) return null; // Piece dispositions beyond coercibleValue, all CHECKER-APPROVED diff --git a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts index 2bc991c14..53b0748b4 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts @@ -23,7 +23,19 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width } if (dst.kind === "union") { if (src.kind === "union") { - return lowerer.unionRetagMappable(src.unionId, dst.unionId) ? { how: "retag" } : null; + if (lowerer.unionRetagMappable(src.unionId, dst.unionId)) return { how: "retag" }; + // A field the checker types `undefined` alone (an object literal's + // omitted member, normalized to `?: undefined` across a union of + // literal types) carries the unit-only union. Into a slot with an + // undefined arm it lifts as that arm; its null arm cannot occur. + const srcArms = lowerer.unions.get(src.unionId)?.arms ?? []; + if ( + srcArms.length > 0 && + srcArms.every(isUnitType) && + lowerer.armTag(dst.unionId, UNDEFINED_T) >= 0 + ) + return { how: "unitRetag" }; + return null; } // A unit-typed source can't wrap here (unionWrap requires the // LITERAL unit — and no lowered shape carries a bare unit field). diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index c85b9868d..496a29607 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -125,6 +125,19 @@ export function applyWidthLift( throw new InternalCompilerError("lowerer bug: planned retag lift failed to intern"); return { kind: "call", callee: retag, args: [value], type: dst, loc }; } + case "unitRetag": { + if (dst.kind !== "union" || value.type.kind !== "union") + throw new InternalCompilerError("lowerer bug: unit retag lift shape"); + const fromId = value.type.unionId; + const trappable = new Set(); + (lowerer.unions.get(fromId)?.arms ?? []).forEach((arm, tag) => { + if (lowerer.armTag(dst.unionId, arm) < 0) trappable.add(tag); + }); + const retag = lowerer.unionRetagHelper(fromId, dst.unionId, loc, trappable); + if (!retag) + throw new InternalCompilerError("lowerer bug: planned unit retag lift failed to intern"); + return { kind: "call", callee: retag, args: [value], type: dst, loc }; + } case "liftWrap": { if (dst.kind !== "union") throw new InternalCompilerError("lowerer bug: liftWrap lift against a non-union"); diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 2e93e2fd5..735361f54 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -44,6 +44,7 @@ import { lowerArrayCallback, requireProducedArrayElement, callbackArrayElement, + truthyFilterCallback, } from "./callback-arguments.js"; import { reverseArrayLoop, @@ -85,7 +86,9 @@ export function lowerArrayHofCall( const valueT = callbackValueType(lowerer, elem, present); if (booleanFilter) { if (valueT.kind === "union") lowerer.requireTruthyUnion(valueT.unionId, argNode); - if (valueT.kind === "dyn" || valueT.kind === "void" || isUnitType(valueT)) { + // Checked-dynamic elements (`anyText.split("\n").map(...).filter(Boolean)`) + // keep their runtime kind, so their truthiness is the runtime's. + if (valueT.kind === "void" || isUnitType(valueT)) { lowerer.badType(argNode, lowerer.typeOf(argNode)); } } @@ -231,33 +234,6 @@ function filterResultElem( return outElem; } -/** A native closure for the ambient Boolean callback. Reifying the - * JavaScript constructor itself is unnecessary; Array#filter only observes - * its one-argument ToBoolean result. */ -function truthyFilterCallback( - lowerer: Lowerer, - valueT: IrType, - loc: SrcLoc, -): IrExpr & { type: IrType & { kind: "func" } } { - const fnT: IrType & { kind: "func" } = { kind: "func", params: [valueT], ret: BOOL }; - const key = `filterBoolean:${typeKey(valueT)}`; - let name = lowerer.arrHofHelpers.get(key); - if (!name) { - name = `%arr.filterBoolean.${lowerer.arrHofHelpers.size}`; - lowerer.arrHofHelpers.set(key, name); - const value = varRef("v.0", valueT, loc); - lowerer.liftedFns.push({ - name, - params: [{ localId: "v.0", name: "v", type: valueT }], - returnType: BOOL, - locals: [{ id: "v.0", name: "v", type: valueT, mutable: false }], - body: [{ kind: "return", value: { kind: "toBool", operand: value, type: BOOL, loc }, loc }], - loc, - }); - } - return { kind: "closure", fnName: name, captures: [], type: fnT, loc }; -} - function filterPredicateOk(lowerer: Lowerer, ret: IrType): boolean { if (ret.kind === "bool") return true; // VOID is a TYPE erasure, not a runtime value: TS lets a value-returning diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 6072b58b7..110af2b56 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -44,6 +44,7 @@ import { lowerArrayCallback, callbackArrayElement, } from "./callback-arguments.js"; +import { classGeneratorIterable } from "../class-iterables.js"; /** Build the argument array for a mutating/copying operation while keeping * the receiver's scalar payload ABI. A missing read is represented as the @@ -265,6 +266,22 @@ function appendWidenedArrayValues( } /** The Array constructor's element and count forms, shared by calls and new. */ +/** The value of `.fill(v)` when `expr` is that call's receiver. */ +function fillArgumentOfReceiver(expr: ts.Expression): ts.Expression | null { + let receiver: ts.Node = expr; + let parent = expr.parent; + while (ts.isParenthesizedExpression(parent)) { + receiver = parent; + parent = parent.parent; + } + if (!ts.isPropertyAccessExpression(parent) || parent.expression !== receiver) return null; + if (parent.name.text !== "fill" || parent.questionDotToken) return null; + const call = parent.parent; + if (!ts.isCallExpression(call) || call.expression !== parent) return null; + const value = call.arguments.nodes[0]; + return value !== undefined && !ts.isSpreadElement(value) ? value : null; +} + export function lowerArrayConstructor( lowerer: Lowerer, expr: ts.CallExpression | ts.NewExpression, @@ -295,6 +312,15 @@ export function lowerArrayConstructor( const contextual = lowerer.checker.getContextualType(expr); if (contextual) result = lowerer.mapTypeOf(contextual); } + // `Array(n).fill(v)`: the checker types the length-constructed array as + // any[], but fill gives every element v's type. + if (result?.kind !== "array") { + const fillValue = fillArgumentOfReceiver(expr); + const elem = fillValue + ? lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(fillValue))) + : null; + if (elem !== null && isSupportedArrayElem(elem)) result = arrayOf(elem); + } if (isJsSourceFile(expr.getSourceFile()) && (result === null || result.kind === "dyn")) result = arrayOf(DYN); if (result?.kind !== "array" || !isSupportedArrayElem(result.elem)) { @@ -913,7 +939,7 @@ export function lowerArrayFromCall( // and materializes holes while keeping element reference identity. if ((args.length === 1 || args.length === 2) && !ts.isObjectLiteralExpression(args[0]!)) { const source = lowerer.lowerExpr(args[0]!); - const src = + let src = source.type.kind === "string" && args.length === 2 ? strCharsCall(lowerer, source, loc) : source; @@ -1008,6 +1034,9 @@ export function lowerArrayFromCall( if (src.type.kind === "searchParams" && args.length === 1) { return lowerCheckedArrayFrom(lowerer, lowerer.coerceInto(args[0]!, src, DYN), loc); } + // A class whose [Symbol.iterator] is a generator method iterates the + // generator that method returns. + src = classGeneratorIterable(lowerer, src) ?? src; // A generator (or an iterator adapted into one) drains in order. if (src.type.kind === "generator" && args.length === 1) { const arrT = lowerer.mapTypeOf(lowerer.typeOf(call)); diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index e87a5e1bc..dcd9934bd 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -1,12 +1,18 @@ import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; +import { isJsSourceFile, locOf } from "../../program.js"; +import { varRef } from "../../../ir/build.js"; +import { typeKey } from "../../type-mapper.js"; import { + BOOL, F64, type IrExpr, type IrType, + type SrcLoc, UNDEFINED_T, funcOf, isSupportedArrayElem, + isUnitType, typeEquals, } from "../../../ir/ir.js"; @@ -44,6 +50,24 @@ export function lowerArrayCallback( bindUntyped = false, expectedReturn?: IrType, ): { fnArg: IrExpr & { type: IrType & { kind: "func" } }; arity: number } { + // The ambient `Boolean` as the callback (`flags.every(Boolean)`, + // `xs.some(Boolean)`, `xs.map(Boolean)`): Boolean(value) answers the + // ToBoolean of its FIRST argument and ignores the rest, so the callback + // is the interned one-parameter truthiness closure over the lead slot. + // The constructor's own generic signature has no compiled value form. + // JavaScript files keep their primitive-constructor closure. + if ( + ts.isIdentifier(argNode) && + argNode.text === "Boolean" && + !isJsSourceFile(argNode.getSourceFile()) && + lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode) ?? undefined) + ) { + const valueT = lead[0]; + if (valueT === undefined || valueT.kind === "void" || isUnitType(valueT)) + lowerer.badType(argNode, lowerer.typeOf(argNode)); + if (valueT.kind === "union") lowerer.requireTruthyUnion(valueT.unionId, argNode); + return { fnArg: truthyFilterCallback(lowerer, valueT, locOf(argNode)), arity: 1 }; + } const full = [...lead, F64, arrT]; // A DYN-receiver HOF's callback (`parsed.flatMap((value) => ...)`): // the contextual signature types the unannotated param `any` (the @@ -248,3 +272,39 @@ function runtimeOptionalPairVariant(lowerer: Lowerer, expected: IrType, mapped: : t; return lowerer.isRuntimeOptionalRecordVariant(strip(expected), strip(mapped)); } + +/** A native closure for the ambient Boolean callback. Reifying the + * JavaScript constructor itself is unnecessary; Array#filter only observes + * its one-argument ToBoolean result. */ +export function truthyFilterCallback( + lowerer: Lowerer, + valueT: IrType, + loc: SrcLoc, +): IrExpr & { type: IrType & { kind: "func" } } { + const fnT: IrType & { kind: "func" } = { kind: "func", params: [valueT], ret: BOOL }; + const key = `filterBoolean:${typeKey(valueT)}`; + let name = lowerer.arrHofHelpers.get(key); + if (!name) { + name = `%arr.filterBoolean.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, name); + const value = varRef("v.0", valueT, loc); + lowerer.liftedFns.push({ + name, + params: [{ localId: "v.0", name: "v", type: valueT }], + returnType: BOOL, + locals: [{ id: "v.0", name: "v", type: valueT, mutable: false }], + body: [ + { + kind: "return", + value: + valueT.kind === "dyn" + ? { kind: "dynTest", test: "truthy", value, type: BOOL, loc } + : { kind: "toBool", operand: value, type: BOOL, loc }, + loc, + }, + ], + loc, + }); + } + return { kind: "closure", fnName: name, captures: [], type: fnT, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index a8b782b51..6a5290c0f 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -42,6 +42,8 @@ import { import { strCharsCall } from "./array-construction.js"; import { arrayElementRead } from "./array-iteration.js"; import { isSearchParamsIterable } from "../builtins/url.js"; +import { classGeneratorIterable } from "../class-iterables.js"; +import { generatorDrain } from "../iterator-adapters.js"; /** Ambient Map method calls. `get`/`set`/`has`/`delete`/`clear` lower to * mapIntrinsic; `forEach` desugars to a direct call of a synthetic loop @@ -1048,6 +1050,14 @@ function setFromSeedValue( } : null; } + // A generator, or a class whose [Symbol.iterator] is a generator method: + // the Set takes every value it yields, in order. + const generator = + classGeneratorIterable(lowerer, source) ?? (source.type.kind === "generator" ? source : null); + if (generator) { + const drained = generatorDrain(lowerer, generator, setT.elem, loc); + return drained ? { kind: "setNew", seed: drained, type: setT, loc } : null; + } let seed: IrExpr; if (source.type.kind === "set" && typeEquals(source.type, setT)) { return { kind: "setIntrinsic", method: "clone", receiver: source, args: [], type: setT, loc }; @@ -1246,6 +1256,24 @@ export function mapFromSeedValue( } : null; } + // A generator of entries, or a class whose [Symbol.iterator] is one: the + // Map takes every [key, value] pair it yields, in order. + const generator = + classGeneratorIterable(lowerer, seed) ?? (seed.type.kind === "generator" ? seed : null); + if (generator) { + const entry: IrType = { + kind: "record", + shapeId: lowerer.shapes.intern( + [ + { name: "0", type: mapT.key }, + { name: "1", type: mapT.value }, + ], + true, + ), + }; + const drained = generatorDrain(lowerer, generator, entry, loc); + return drained ? mapFromSeedValue(lowerer, drained, mapT) : null; + } if (seed.type.kind === "map") { if (!typeEquals(seed.type, mapT)) return null; return { kind: "mapIntrinsic", method: "clone", receiver: seed, args: [], type: mapT, loc }; diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index 450e64142..efc173ffc 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -28,6 +28,8 @@ import { tryLowerExpression } from "../expressions/try-lower-expression.js"; import { locOf } from "../../program.js"; import { typeKey } from "../../type-mapper.js"; import { type WidthLift } from "../lowerer.js"; +import { classGeneratorIterable, iteratesThroughGenerator } from "../class-iterables.js"; +import { generatorDrain } from "../iterator-adapters.js"; /** Object() and new Object() preserve known object inputs. Primitive * boxing and unproven checked-dynamic values retain their refusal. */ @@ -148,12 +150,29 @@ export function objectIterOverIndexShape( } // The overflow value must surface as the element type (identity, an // arm of a union element, or a dyn element over a dyn signature). + // A cast that narrows the signature's union (`Object.entries(headers as + // Record)` over a signature that + // also holds numbers): each overflow value checks into the element type, + // so a value of a dropped arm throws instead of mistyping. + let ivNarrows = false; if (valueT) { const ivSurfaces = typeEquals(iv, valueT) || (valueT.kind === "union" && lowerer.armTag(valueT.unionId, iv) >= 0) || (valueT.kind === "dyn" && iv.kind === "dyn"); - if (!ivSurfaces) { + if ( + !ivSurfaces && + iv.kind === "union" && + lowerer.dynConvertible(iv) && + lowerer.dynConvertible(valueT) + ) { + const ivUnion = iv; + const narrowArms = + valueT.kind === "union" ? (lowerer.unions.get(valueT.unionId)?.arms ?? []) : [valueT]; + ivNarrows = + narrowArms.length > 0 && narrowArms.every((a) => lowerer.armTag(ivUnion.unionId, a) >= 0); + } + if (!ivSurfaces && !ivNarrows) { lowerer.unsupported( "SC1090", node, @@ -267,6 +286,21 @@ export function objectIterOverIndexShape( } } } + // A cast that narrows the field's type (the element type keeps + // some of its arms): the value checks into the element type, so a + // dropped arm throws instead of mistyping. + if (lowerer.dynConvertible(f.type) && lowerer.dynConvertible(valueT)) { + const armsOf = (t: IrType): IrType[] => + t.kind === "union" ? (lowerer.unions.get(t.unionId)?.arms ?? []) : [t]; + const target = armsOf(valueT); + if (armsOf(f.type).some((a) => target.some((b) => typeEquals(a, b)))) + return { + kind: "dynCheck", + value: { kind: "dynFrom", value: raw, type: DYN, loc }, + type: valueT, + loc, + }; + } return null; }; const pushedOf = (element: IrExpr): IrExpr => @@ -337,17 +371,37 @@ export function objectIterOverIndexShape( const ksT = arrayOf(STRING); const ksRef = varRef("ks.0", ksT, loc); const kRef = varRef("k.0", STRING, loc); - const readValue: IrExpr | null = valueT - ? { - kind: "recordKeyGet", - obj: rRef, - shapeId: argIr.shapeId, - key: kRef, - overflowOnly: true, - type: valueT, - loc, - } - : null; + const readValue: IrExpr | null = !valueT + ? null + : ivNarrows + ? { + kind: "dynCheck", + value: { + kind: "dynFrom", + value: { + kind: "recordKeyGet", + obj: rRef, + shapeId: argIr.shapeId, + key: kRef, + overflowOnly: true, + type: iv, + loc, + }, + type: DYN, + loc, + }, + type: valueT, + loc, + } + : { + kind: "recordKeyGet", + obj: rRef, + shapeId: argIr.shapeId, + key: kRef, + overflowOnly: true, + type: valueT, + loc, + }; const loopPushed: IrExpr = member === "keys" ? kRef @@ -472,9 +526,37 @@ function lowerTypedObjectFromEntriesCall( if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) return null; const argNode = call.arguments.nodes[0]!; - const argIr = lowerer.mapTypeOf(lowerer.typeOf(argNode)); + let argIr = lowerer.mapTypeOf(lowerer.typeOf(argNode)); + // A generator of entries, or a class whose [Symbol.iterator] is one, + // drains into the entry array the object is built from. + let drained: IrExpr | null = null; + if (argIr?.kind === "generator" || (argIr && iteratesThroughGenerator(lowerer, argIr))) { + const value = lowerer.lowerExpr(argNode); + const generator = + classGeneratorIterable(lowerer, value) ?? (value.type.kind === "generator" ? value : null); + if (generator?.type.kind !== "generator") return null; + // The entries take the result's value type: `[string, V]` for an + // object `{ [k: string]: V }`. + const result = lowerer.mapTypeOf(lowerer.typeOf(call)); + const valueT = + result?.kind === "record" ? lowerer.shapes.get(result.shapeId)?.indexValue : undefined; + if (!valueT) return null; + const entry: IrType = { + kind: "record", + shapeId: lowerer.shapes.intern( + [ + { name: "0", type: STRING }, + { name: "1", type: valueT }, + ], + true, + ), + }; + drained = generatorDrain(lowerer, generator, entry, locOf(call)); + if (!drained) return null; + argIr = drained.type; + } if (argIr?.kind !== "array") return null; - const represented = tryLowerExpression(lowerer, argNode); + const represented = drained ?? tryLowerExpression(lowerer, argNode); if (represented && !typeEquals(represented.type, argIr)) return null; // `Object.fromEntries(rows)` over a `string[][]` VALUE — the env-line // idiom (`envArray.map((env) => env.split('='))`). The checker has no @@ -522,7 +604,7 @@ function lowerTypedObjectFromEntriesCall( // Enumeration/filtering may keep the array in checked-dynamic storage. // Validate its tuple layout before calling a helper with a native-array // ABI; opaque tuple values themselves retain their original references. - const receiver = lowerer.lowerExprExpecting(argNode, argIr); + const receiver = drained ?? lowerer.lowerExprExpecting(argNode, argIr); const key = `obj.fromEntries:${argIr.elem.shapeId}:${resultT.shapeId}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index 46074573d..4017335ae 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -34,6 +34,7 @@ import { genValue, stepDrivable, } from "../lower-generators.js"; +import { classGeneratorIterable, iteratesThroughGenerator } from "../class-iterables.js"; import { exitBeforeReturns } from "./for-of.js"; import { iteratorSlotAdapter, @@ -317,9 +318,11 @@ function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean { * it rather than a wrapper. A generator (including a helper object or an * array iterator) is itself; an array, string or typed array gives its * values iterator; a Set its values and a Map its entries, as a generator - * over the live collection. The result converts into the call's - * IteratorObject type. Null for other arguments (objects with their own - * next method, untyped values), which keep the Iterator fence. */ + * over the live collection; a class instance whose `[Symbol.iterator]` is + * a generator method gives the generator it returns. The result converts + * into the call's IteratorObject type. Null for other arguments (objects + * with their own next method, untyped values), which keep the Iterator + * fence. */ export function lowerIteratorFromCall( lowerer: Lowerer, call: ts.CallExpression, @@ -332,7 +335,9 @@ export function lowerIteratorFromCall( const argT = lowerer.mapTypeOf(lowerer.typeOf(argNode)); if (resultT?.kind !== "generator" || resultT.async || argT === null) return null; const iterableKinds = ["generator", "array", "string", "bytes", "set", "map"]; - if (!iterableKinds.includes(argT.kind) || (argT.kind === "generator" && argT.async)) return null; + const classIterable = iteratesThroughGenerator(lowerer, argT); + if (!classIterable && !iterableKinds.includes(argT.kind)) return null; + if (argT.kind === "generator" && argT.async) return null; const loc = locOf(call); const value = lowerer.lowerExpr(argNode); const own = (yieldT: IrType): GenType => ({ @@ -344,6 +349,7 @@ export function lowerIteratorFromCall( let iterator: IrExpr | null = null; const t = value.type; if (t.kind === "generator") iterator = value; + else if (classIterable) iterator = classGeneratorIterable(lowerer, value); else if (t.kind === "array") iterator = lowerArrayIteratorValue(lowerer, value, "values", own(t.elem), loc); else if (t.kind === "string") diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 39154a820..50693e5e4 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -302,6 +302,53 @@ function lowerMethodReceiver( return lowerer.coerceInto(node, lowered, expected); } +/** A `string | undefined | null` receiver of a string method: evaluated + * once, each unit arm throws the TypeError Node's member read throws + * ("Cannot read properties of undefined (reading 'replace')") before the + * arguments evaluate, and the string arm is the receiver. */ +function nullableStringReceiver(lowerer: Lowerer, node: ts.Expression, member: string): IrExpr { + const lowered = lowerer.lowerExpr(node); + if (lowered.type.kind !== "union") return lowerer.coerceInto(node, lowered, STRING); + const loc = locOf(node); + const unionId = lowered.type.unionId; + const local = lowerer.declareHiddenLocal("%nullableReceiver", lowered.type); + const ref: IrExpr = { kind: "varRef", localId: local.id, type: lowered.type, loc }; + const arms = lowerer.unions.get(unionId)?.arms ?? []; + let result: IrExpr = { + kind: "unionNarrow", + unionId, + tag: arms.findIndex((arm) => arm.kind === "string"), + value: ref, + type: STRING, + loc, + }; + for (const [tag, arm] of arms.entries()) { + if (!isUnitType(arm)) continue; + const unit = arm.kind === "nullT" ? "null" : "undefined"; + result = { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, value: ref, negated: false, type: BOOL, loc }, + then: nodeThrowExpr( + 1, + "", + `Cannot read properties of ${unit} (reading '${member}')`, + STRING, + loc, + ), + else_: result, + type: STRING, + loc, + }; + } + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: lowered, loc }], + result, + type: STRING, + loc, + }; +} + /** Relative string indexing and code-point decoding over the UTF-16 intrinsics. */ export function lowerStringIndexCall( lowerer: Lowerer, @@ -733,6 +780,15 @@ export function lowerStringPaddingCall( return { kind: "call", callee: helper, args: values, type: STRING, loc }; } +/** String methods whose regex-argument forms lower in lowerRegexMethodCall. */ +const REGEX_ARGUMENT_STRING_METHODS = new Set([ + "replace", + "replaceAll", + "split", + "search", + "matchAll", +]); + export function lowerRegexMethodCall( lowerer: Lowerer, call: ts.CallExpression, @@ -747,11 +803,30 @@ export function lowerRegexMethodCall( // by construction — the symbol gate doesn't apply to `any` receivers. if (dynReceiver === undefined && !lowerer.isStdlibMember(access)) return null; const name = access.name.text; - const receiverKind = dynReceiver - ? "string" - : lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind; + const mappedReceiver = dynReceiver ? null : lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + // A nullable string receiver of a REGEX-argument call + // (`part.replace(re, "")` where an unchecked index read typed `part` + // `string | undefined`) takes the string path, as + // lowerStringMethodCall's gate does for the string-argument forms: + // lowerMethodReceiver throws Node's "Cannot read properties of + // undefined" on the unit arms. (`match` keeps its own gate below.) + const regexArgument = call.arguments.nodes[0]; + const nullableString = + REGEX_ARGUMENT_STRING_METHODS.has(name) && + regexArgument !== undefined && + !ts.isSpreadElement(regexArgument) && + lowerer.mapTypeOf(lowerer.typeOf(regexArgument))?.kind === "regex" && + mappedReceiver?.kind === "union" && + (lowerer.unions.get(mappedReceiver.unionId)?.arms.some((arm) => arm.kind === "string") ?? + false) && + (lowerer.unions + .get(mappedReceiver.unionId) + ?.arms.every((arm) => arm.kind === "string" || isUnitType(arm)) ?? + false); + const receiverKind = dynReceiver || nullableString ? "string" : mappedReceiver?.kind; const lowerReceiver = (): IrExpr => { if (dynReceiver) return dynReceiver(); + if (nullableString) return nullableStringReceiver(lowerer, access.expression, name); if (receiverKind === "string") return lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text); if (receiverKind === "regex") diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index 36fbf48b9..a77f33c6b 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -14,7 +14,6 @@ import * as ts from "../ts7/adapter.js"; import { locOf } from "../program.js"; import { isNodeModulesPath } from "../resolve.js"; import { - dateLiteralDivergenceDiag, keyOrderDivergenceDiag, localeCompareDivergenceDiag, unrelatedClassCastDivergenceDiag, @@ -57,7 +56,6 @@ export function scanDivergences( const literals: ts.ObjectLiteralExpression[] = []; const observers: ts.Expression[] = []; const casts: (ts.AsExpression | ts.TypeAssertion)[] = []; - const dates: { call: ts.CallExpression | ts.NewExpression; arg: ts.Expression }[] = []; const localeCompares: ts.CallExpression[] = []; /** binding name → member names assigned through it (`q.a = 2`). */ const assignedMembers = new Map>(); @@ -99,20 +97,9 @@ export function scanDivergences( member === "dir") ) { for (const a of node.arguments.nodes) if (!isLiteralLike(a)) observers.push(a); - } else if (recv.text === "Date" && member === "parse") { - if (node.arguments.nodes[0] !== undefined) - dates.push({ call: node, arg: node.arguments.nodes[0] }); } } } - } else if (ts.isNewExpression(node)) { - if ( - ts.isIdentifier(node.expression) && - node.expression.text === "Date" && - node.arguments?.nodes.length === 1 - ) { - dates.push({ call: node, arg: node.arguments.nodes[0]! }); - } } else if (ts.isForInStatement(node)) { observers.push(node.expression); } else if (ts.isVariableDeclaration(node)) { @@ -399,55 +386,9 @@ export function scanDivergences( } } - // ── Date strings the native parser does not accept ──────────────── - for (const { call, arg } of dates) { - const callee = ts.isNewExpression(call) - ? call.expression - : ts.isPropertyAccessExpression(call.expression) - ? call.expression.expression - : undefined; - if (callee === undefined || !lowerer.isStdlibGlobal(callee, "Date")) continue; - for (const literal of dateStringLiterals(checker, arg)) { - const text = stringLiteralText(literal)!; - if (nativeDateParses(text) || !/\d/.test(text)) continue; - out.push(dateLiteralDivergenceDiag(text, locOf(literal))); - } - } - return out; } -/** The string literals a Date argument can be: the literal itself, or - * each element of a const array of literals a for-of binding iterates. */ -function dateStringLiterals(checker: ts.TypeChecker, arg: ts.Expression): ts.Expression[] { - if (stringLiteralText(arg) !== null) return [arg]; - if (!ts.isIdentifier(arg)) return []; - const symbol = safely(() => checker.getSymbolAtLocation(arg)); - const decl = symbol === undefined ? undefined : symbol.valueDeclaration; - const list = decl?.parent; - const loop = list?.parent; - if ( - decl === undefined || - !ts.isVariableDeclaration(decl) || - list === undefined || - loop === undefined || - !ts.isForOfStatement(loop) || - loop.initializer !== list - ) - return []; - let iterated: ts.Expression = loop.expression; - if (ts.isIdentifier(iterated)) { - const source = safely(() => checker.getSymbolAtLocation(iterated)); - const init = source === undefined ? undefined : source.valueDeclaration; - if (init === undefined || !ts.isVariableDeclaration(init) || init.initializer === undefined) - return []; - if (init.parent === undefined || (init.parent.flags & ts.NodeFlags.Const) === 0) return []; - iterated = init.initializer; - } - if (!ts.isArrayLiteralExpression(iterated)) return []; - return iterated.elements.nodes.filter((e) => stringLiteralText(e) !== null); -} - /** A checker query that fails here only loses a warning. */ function safely(f: () => T): T | undefined { try { @@ -870,20 +811,3 @@ function classExtends( function className(decl: ts.ClassLikeDeclaration): string { return decl.name?.text ?? "(anonymous class)"; } - -const MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"]; - -/** The date-string grammar the native runtime accepts (scr_lib.c - * scr_date_parse_get_time): ECMAScript's date-time format with an explicit - * offset (date-only forms are UTC) and the certificate-time shape - * "Mon DD HH:MM:SS YYYY GMT". Everything else parses to NaN natively. */ -export function nativeDateParses(text: string): boolean { - const iso = - /^(?:[+-]\d{6}|\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/.test(text) || - /^(?:[+-]\d{6}|\d{4})-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{3})?)?(?:Z|[+-]\d{2}:\d{2})$/.test( - text, - ); - if (iso) return true; - const cert = /^([A-Za-z]{3}) {1,2}\d{1,2} \d{2}:\d{2}:\d{2} \d{4} GMT$/.exec(text); - return cert !== null && MONTHS.includes(cert[1]!.toLowerCase()); -} diff --git a/packages/compiler/src/frontend/lowering/error-name-order.ts b/packages/compiler/src/frontend/lowering/error-name-order.ts new file mode 100644 index 000000000..7453d123e --- /dev/null +++ b/packages/compiler/src/frontend/lowering/error-name-order.ts @@ -0,0 +1,98 @@ +import * as ts from "../ts7/adapter.js"; +import { isClassOwnEnumerableFieldName } from "../../ir/ir.js"; +import type { ClassInfo } from "./lower-classes.js"; + +type ErrorClassLink = Pick & { + base: ErrorClassLink | null; +}; + +/** The property key a member or element-access name spells statically: + * an identifier, a quoted string, a number in its canonical spelling, or a + * computed string literal. Null for other keys. */ +function staticKeyText(name: ts.Node): string | null { + if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text; + if (ts.isNumericLiteral(name)) return String(Number(name.text)); + if (ts.isComputedPropertyName(name)) return staticKeyText(name.expression); + return null; +} + +/** Whether `node` is an assignment to `this.` or `this[""]`, + * and the name. */ +function thisAssignmentName(node: ts.Node): string | null { + if (!ts.isBinaryExpression(node) || node.operatorToken.kind !== ts.SyntaxKind.EqualsToken) + return null; + const target = node.left; + if (ts.isPropertyAccessExpression(target) && target.expression.kind === ts.SyntaxKind.ThisKeyword) + return ts.isIdentifier(target.name) ? target.name.text : null; + if (ts.isElementAccessExpression(target) && target.expression.kind === ts.SyntaxKind.ThisKeyword) + return staticKeyText(target.argumentExpression); + return null; +} + +/** Where an Error subclass instance's own `name` sits among its fields when + * a class of its chain gives it one during construction. A property's + * position is fixed when it is created: a `name` field takes its place + * among the fields (parameter properties first, then the declared ones), + * and `this.name = ...` in a constructor follows the fields created so far, + * including the ones that constructor created before it. The answer is the + * field `name` follows, null when it precedes every field, and undefined + * when no class of the chain sets it during construction. `fields` are the + * instance's own enumerable fields. */ +export function errorNameAfterField( + info: ErrorClassLink, + fields: ReadonlySet, +): string | null | undefined { + const chain: ErrorClassLink[] = []; + for (let current: ErrorClassLink | null | undefined = info; current; current = current.base) + if (current.decl) chain.unshift(current); + const created = new Set(); + let last: string | null = null; + const create = (name: string): void => { + if (!fields.has(name) || created.has(name)) return; + created.add(name); + last = name; + }; + for (const link of chain) { + for (const property of link.paramProps ?? []) create(property.name); + for (const member of link.decl!.members.nodes) { + if (!ts.isPropertyDeclaration(member)) continue; + const key = staticKeyText(member.name); + if (key === null) continue; + if ( + member.modifiers?.nodes.some( + (m) => m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.DeclareKeyword, + ) + ) + continue; + if (key === "name") return last; + create(key); + } + const body = link.ctor?.body; + if (!body) continue; + let found = false; + ts.walkPreorder(body, (node) => { + if (ts.isTypeNode(node)) return "skip"; + if (ts.isFunctionLike(node) && !ts.isArrowFunction(node)) return "skip"; + const name = thisAssignmentName(node); + if (name === "name") { + found = true; + return "stop"; + } + if (name !== null) create(name); + return undefined; + }); + if (found) return last; + } + return undefined; +} + +/** The fields an Error subclass's own-property view lists. */ +export function errorViewFields(fields: readonly { name: string }[]): Set { + return new Set( + fields + .map((field) => field.name) + .filter( + (name) => isClassOwnEnumerableFieldName(name) && name !== "name" && name !== "message", + ), + ); +} diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index ef2aa9778..d311d6d79 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -30,6 +30,7 @@ import { generatorDrain } from "../iterator-adapters.js"; import { isSearchParamsIterable } from "../builtins/url.js"; import { varRef } from "../../../ir/build.js"; import { arrayValueStore } from "../array-values.js"; +import { classGeneratorIterable } from "../class-iterables.js"; /** JavaScript: whether `expr` is or holds an existing checked-dynamic * object — a binding stored as a dyn value whose checker type is an @@ -431,6 +432,9 @@ export function lowerArrayLiteral( loc: locOf(el), }; } + // `[...obj]` over a class whose [Symbol.iterator] is a generator + // method spreads the generator that method returns. + src = classGeneratorIterable(lowerer, src) ?? src; // `[...it]` over a generator (or an iterator adapted into one): // drain it once, in order, into a fresh element array. if (src.type.kind === "generator") { diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 2ee56573c..e01c8f362 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -21,15 +21,23 @@ import { import { lowerNodeModuleIdentifier } from "../lower-node-module.js"; import { lowerCjsExportsObjectValue } from "../cjs-exports-object.js"; import { captureContextArguments } from "../function-context.js"; -import { isCjsExportTableLiteral, isJsSourceFile, isNodeEsmFile, locOf } from "../../program.js"; +import { + isCjsExportTableLiteral, + isCjsJsFile, + isJsSourceFile, + isNodeEsmFile, + locOf, +} from "../../program.js"; import { builtinClassAliasOwnsNoSlot, runtimeClassValueOf } from "../builtins/class-values.js"; import { cjsAccessorCall, + cjsAccessorSnapshotOf, primitiveCtorClosure, runtimeOptionalElementKey, runtimeOptionalUseOf, } from "../lower-exprs.js"; import { runtimeOptionalReceiverRead } from "./narrowing.js"; +import { builtinGlobalValueAt } from "./property-access.js"; import { varRef } from "../../../ir/build.js"; import { ambientUndefReadType, @@ -352,19 +360,25 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // MODULE compile-time constants, the containing file's location // (Node's exact values when the same tree runs in place; WASI uses // the equivalent guest-visible path; locals shadowing the names - // resolved above). In a file with ESM syntax Node never defines them - // (ReferenceError) — fence rather than invent a value there. + // resolved above), the same strings as import.meta.dirname/filename. + // In a JavaScript ES module Node never defines them, so the read throws + // Node's catchable ReferenceError. A .ts/.tsx file keeps them even when + // Node's strip-types rule would load it as ESM: TypeScript projects are + // commonly built as CommonJS bundles (tsc, esbuild, tsx) where these + // globals exist, and @types/node declares them for every file. if ( (expr.text === "__dirname" || expr.text === "__filename") && lowerer.isStdlibGlobal(expr, expr.text) ) { const sf = expr.getSourceFile(); - if (isNodeEsmFile(sf, lowerer.program)) { - lowerer.unsupported( - "SC1090", - expr, - `'${expr.text}' in an ES module (Node defines the CJS module globals only in CommonJS modules)`, - ); + if (isNodeEsmFile(sf, lowerer.program) && !/\.tsx?$/.test(sf.fileName)) { + return { + kind: "libCall", + fn: "global.undefRead", + args: [{ kind: "strLit", value: expr.text, type: STRING, loc }], + type: STRING, + loc, + }; } // The WASI runner mounts the host cwd at guest `/` and the host temp // directory at `/tmp`. Bake the matching GUEST spelling so resource @@ -710,8 +724,18 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: { const sym = lowerer.checker.getSymbolAtLocation(expr); if (lowerer.isStdlibSymbol(sym) || expr.text === "globalThis") { + const canonical = stdlibGlobalNameOf(lowerer, expr) ?? expr.text; + // Builtin constructors have real function values in both source + // languages (TypeScript reads them as dynamic values). + const builtinValue = builtinGlobalValueAt( + lowerer, + expr, + canonical, + loc, + isJsSourceFile(expr.getSourceFile()), + ); + if (builtinValue !== null) return builtinValue; if (isJsSourceFile(expr.getSourceFile())) { - const canonical = stdlibGlobalNameOf(lowerer, expr) ?? expr.text; return { kind: "strLit", value: `[builtin ${canonical}]`, type: STRING, loc }; } // The families with a WHY: each hint states what makes the @@ -924,5 +948,18 @@ function cjsExportAccessorRead(lowerer: Lowerer, ident: ts.Identifier): IrExpr | if (!ts.isObjectLiteralExpression(getter.parent) || !isCjsExportTableLiteral(getter.parent)) { return null; } + // An ES-module import binding reads the value Node copied when the + // importer linked the CommonJS module (cjsAccessorSnapshotStores). + const raw = lowerer.checker.getSymbolAtLocation(ident); + const binding = raw ? raw.declarations[0] : undefined; + if ( + binding !== undefined && + (ts.isImportSpecifier(binding) || ts.isExportSpecifier(binding)) && + !isCjsJsFile(binding.getSourceFile(), lowerer.program) + ) { + const snapshot = cjsAccessorSnapshotOf(lowerer, getter); + if (snapshot !== null) + return { kind: "varRef", localId: snapshot.id, type: snapshot.type, loc: locOf(ident) }; + } return cjsAccessorCall(lowerer, getter, locOf(ident)); } diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 6238479e0..77c4c8fc5 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -640,6 +640,74 @@ function literalUnionArmOf( /** Object spread treats nullish sources as empty. Preserve evaluation once, * then turn `Record | undefined` into either that record or a fresh * empty record of the same represented shape. */ +/** `{ ...unknownValue }` into a pure index-signature record (headers + * forwarded from an untyped parameter): Node copies the source's own + * enumerable keys in order, and spreads nothing for null, undefined, + * numbers and booleans. An object source converts through the checked + * extraction into the target's own shape, which keeps its keys and order + * and checks each value against the slot type (a value outside it throws + * the catchable TypeError of every unknown-to-typed conversion). A string + * source takes the same extraction, which rejects it, rather than + * silently dropping the characters Node would spread. Null for other + * sources and targets. */ +function dynIndexSpreadSource( + lowerer: Lowerer, + source: IrExpr, + target: IrType, + loc: SrcLoc, +): IrExpr | null { + if (source.type.kind !== "dyn" || target.kind !== "record") return null; + const shape = lowerer.shapes.get(target.shapeId); + if (!shape?.indexValue || shape.fields.length > 0 || shape.tuple) return null; + if ( + !canDynCheckTo( + target, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) + return null; + const local = lowerer.declareHiddenLocal("%dynSpread", DYN); + const ref = varRef(local.id, DYN, loc); + const test = (kind: "object" | "string" | "nullish"): IrExpr => ({ + kind: "dynTest", + test: kind, + value: ref, + type: BOOL, + loc, + }); + const extract: IrExpr = { kind: "dynCheck", value: ref, type: target, loc }; + const empty: IrExpr = { kind: "recordLit", fields: [], type: target, loc }; + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: source, loc }], + result: { + kind: "ternary", + cond: test("nullish"), + then: empty, + else_: { + kind: "ternary", + cond: { + kind: "logical", + op: "||", + left: test("object"), + right: test("string"), + type: BOOL, + loc, + }, + then: extract, + else_: empty, + type: target, + loc, + }, + type: target, + loc, + }, + type: target, + loc, + }; +} + function lowerOptionalIndexSpreadSource( lowerer: Lowerer, source: IrExpr, @@ -1372,7 +1440,7 @@ export function lowerObjectLiteral( vNode, ); contributors.push({ - kind: "field", + kind: "condField", name: csProp.name.text, value: { kind: "ternary", @@ -1387,6 +1455,7 @@ export function lowerObjectLiteral( } let src = lowerer.lowerExpr(prop.expression); src = lowerOptionalIndexSpreadSource(lowerer, src, locOf(prop)) ?? src; + src = dynIndexSpreadSource(lowerer, src, type, locOf(prop)) ?? src; if (src.type.kind !== "record") { lowerer.unsupported( "SC1090", @@ -1631,7 +1700,11 @@ export function lowerObjectLiteral( } shapeMismatch(prop); } - const absent = lowerer.absentFieldValue(fieldType, locOf(prop)); + // An 'unknown' slot holds the checked-dynamic undefined for the + // empty arm, the absent-property read the width copy uses too. + const absent = + lowerer.absentFieldValue(fieldType, locOf(prop)) ?? + (fieldType.kind === "dyn" ? dynUndefinedExpr(locOf(prop)) : null); if (!absent) { lowerer.unsupported( "SC1090", @@ -2559,6 +2632,14 @@ export function andSpreadOf( while (ts.isParenthesizedExpression(right)) right = right.expression; if (!ts.isObjectLiteralExpression(right)) return null; const spreadType = lowerer.mapTypeOf(lowerer.typeOf(e)); + // Every falsy value is a primitive, which spreads nothing, so `c && { + // ... }` contributes the literal's members exactly when c is truthy, + // whatever c's type. The checker's union of the two decides the + // representation where it maps; where it does not (a member typed by a + // checked-dynamic value such as an AbortSignal), the syntax decides. + if (spreadType === null || spreadType.kind === "dyn") { + return syntacticAndSpread(right, e.left); + } if (spreadType?.kind !== "union") return null; const arms = lowerer.unions.get(spreadType.unionId)?.arms ?? []; if ( @@ -2590,6 +2671,26 @@ export function andSpreadOf( return { cond: e.left, props, whenTrue: true }; } +function syntacticAndSpread( + right: ts.ObjectLiteralExpression, + cond: ts.Expression, +): { cond: ts.Expression; props: CondSpreadProp[]; whenTrue: boolean } | "unsupported" { + if (right.properties.nodes.length === 0) return "unsupported"; + const props: CondSpreadProp[] = []; + for (const p of right.properties.nodes) { + if ( + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + p.name && + (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) + ) { + props.push(p as CondSpreadProp); + } else { + return "unsupported"; + } + } + return { cond, props, whenTrue: true }; +} + /** A conditional spread's carrier property: `name: value` or shorthand, * with an identifier/string-literal name. */ export type CondSpreadProp = (ts.PropertyAssignment | ts.ShorthandPropertyAssignment) & { diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index a3154c45a..6b37b7a19 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -741,6 +741,12 @@ export function lowerPropertyAccessExpr( lowerStreamObjectProperty(lowerer, expr) ?? lowerEventObjectProperty(lowerer, expr) ?? lowerer.lowerUnionProperty(expr) ?? + // A builtin constructor reached through another binding (`const D = + // Date; D.prototype`) holds its interned function value, which + // models only name, length and constructor: a static member or the + // prototype that no lowering above claimed keeps the SC2020 fence + // rather than reading undefined through the field and dynamic reads. + fenceBuiltinConstructorStaticRead(lowerer, expr) ?? lowerer.lowerFieldRead(expr); // A union-typed field read narrows like an identifier when the // checker has narrowed this use to one arm. @@ -787,7 +793,14 @@ export function lowerPropertyAccessExpr( type: DYN, loc, }; - return { kind: "strLit", value: `[builtin ${canonical}]`, type: STRING, loc }; + return ( + builtinGlobalValueAt(lowerer, expr, canonical, loc, true) ?? { + kind: "strLit", + value: `[builtin ${canonical}]`, + type: STRING, + loc, + } + ); } } // A JS receiver whose CHECKER type has no mapping (`mustCallChecks @@ -866,6 +879,43 @@ export function lowerPropertyAccessExpr( }; } } + // A plain record's inherited Object.prototype members READ as values + // (`o.constructor`, `o.valueOf`, `({}).toString`, the isPlainObject and + // deep-equal idioms): the record boxes into its dynamic view, whose + // read answers the interned Object constructor and Object.prototype + // methods — the same values `Object` and `Object.prototype.valueOf` + // read, so identity comparisons are exact. Calls keep their own + // lowerings; a record declaring the name reads its field. + if ( + OBJECT_PROTOTYPE_VALUE_READS.has(expr.name.text) && + !isCalleeOf(expr) && + // `X.prototype.toString` keeps the builtin prototype-method lowering. + !(ts.isPropertyAccessExpression(expr.expression) && expr.expression.name.text === "prototype") + ) { + const receiver = tryLowerExpression(lowerer, expr.expression); + const name = expr.name.text; + const shape = + receiver?.type.kind === "record" ? lowerer.shapes.get(receiver.type.shapeId) : undefined; + const info = + receiver?.type.kind === "object" ? lowerer.classes.get(receiver.type.className) : undefined; + const inherits = + receiver?.type.kind === "dyn" || + (shape !== undefined && !shape.tuple && !shape.fields.some((field) => field.name === name)) || + (info !== undefined && + name !== "constructor" && + !info.fields.has(name) && + !lowerer.findMethodOn(info, name) && + !lowerer.findMethodOn(info, `get:${name}`)); + if (receiver !== null && receiver !== undefined && inherits) { + return { + kind: "dynKeyGet", + value: lowerer.coerceToExpected(receiver, DYN), + key: { kind: "strLit", value: name, type: STRING, loc }, + type: DYN, + loc, + }; + } + } // `f.name` / `f.length` / own properties on a FUNCTION-typed JS // value (the mustCall wrapper's function-instance members): read // through the dyn box — the closure's own-property table answers @@ -1255,6 +1305,8 @@ export function lowerPropertyAccessExpr( const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)); return mapped && mapped.kind !== "void" ? lowerer.coerceInto(expr, read, mapped) : read; } + const asserted = assertedMemberRead(lowerer, expr, recvLowered, loc); + if (asserted) return asserted; lowerer.unsupported( "SC1090", expr, @@ -1262,6 +1314,101 @@ export function lowerPropertyAccessExpr( ); } +/** `(source as { repoName: string }).repoName` where the assertion names a + * member some arms of the stored union of records do not declare: Node + * reads the property off whichever object it is, so arms that declare it + * answer their slot, and the others answer undefined (native records are + * closed, so they cannot hold it). The result keeps that undefined arm + * whatever the assertion claims; a consumer that needs the asserted type + * checks it like any other narrowing. The arms that declare the member + * must agree on its type. Null when the receiver is not such an + * assertion over records, or no arm declares the member. */ +function assertedMemberRead( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, + recv: IrExpr, + loc: SrcLoc, +): IrExpr | null { + let receiverNode: ts.Expression = expr.expression; + while (ts.isParenthesizedExpression(receiverNode)) receiverNode = receiverNode.expression; + if (!ts.isAsExpression(receiverNode) && !ts.isTypeAssertion(receiverNode)) return null; + if (recv.type.kind !== "union") return null; + const unionId = recv.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms ?? []; + const name = expr.name.text; + let resultType: IrType | null = null; + let records = 0; + const declaring: { tag: number; shapeId: string; type: IrType }[] = []; + const units: number[] = []; + for (let tag = 0; tag < arms.length; tag++) { + const arm = arms[tag]!; + if (isUnitType(arm)) { + units.push(tag); + continue; + } + if (arm.kind !== "record") return null; + records++; + const shape = lowerer.shapes.get(arm.shapeId); + if (!shape || shape.tuple || shape.indexValue) return null; + const field = shape.fields.find((f) => f.name === name); + if (!field) continue; + // The declaring arms agree up to an undefined arm (`string` beside + // `string | undefined`). + const armResult = lowerer.withUndefinedArmOf(field.type); + if (!armResult || (resultType && !typeEquals(resultType, armResult))) return null; + resultType = armResult; + declaring.push({ tag, shapeId: arm.shapeId, type: field.type }); + } + if (!resultType || declaring.length === records) return null; + const stable = lowerer.declareHiddenLocal("%assertedRecv", recv.type); + const ref = (): IrExpr => ({ kind: "varRef", localId: stable.id, type: recv.type, loc }); + let result: IrExpr | null = lowerer.wrappedUndefined(resultType, loc); + if (!result) return null; + // A unit arm the checker narrowed away reads like Node would read it. + for (const tag of units) { + const unit = arms[tag]!.kind === "nullT" ? "null" : "undefined"; + result = { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, negated: false, value: ref(), type: BOOL, loc }, + then: nodeThrowExpr( + 1, + "", + `Cannot read properties of ${unit} (reading '${name}')`, + resultType, + loc, + ), + else_: result, + type: resultType, + loc, + }; + } + for (const { tag, shapeId, type: fieldType } of declaring) { + const read: IrExpr = { + kind: "recordGet", + obj: { kind: "unionNarrow", unionId, tag, value: ref(), type: arms[tag]!, loc }, + shapeId, + field: name, + type: fieldType, + loc, + }; + result = { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, negated: false, value: ref(), type: BOOL, loc }, + then: lowerer.coerceToExpected(read, resultType), + else_: result, + type: resultType, + loc, + }; + } + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: stable.id, init: recv, loc }], + result, + type: resultType, + loc, + }; +} + function importMetaProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null { return isImportMeta(expr.expression) ? importMetaField(lowerer, expr, expr.name.text) : null; } @@ -1305,6 +1452,13 @@ export function lowerIntrinsicProperty( return { kind: "dynFrom", value, fnName: "ownKeys", type: DYN, loc: locOf(expr) }; } if (lowerer.chainBlocked(expr)) return null; + // Object.prototype.constructor: the Object constructor value. + if ( + expr.name.text === "constructor" && + ts.isPropertyAccessExpression(expr.expression) && + lowerer.stdlibGlobalMember(expr.expression, "Object") === "prototype" + ) + return builtinGlobalValue("Object", locOf(expr)); const builtinPrototype = builtinPrototypeMethod(lowerer, expr); if (builtinPrototype) return { @@ -2584,6 +2738,27 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi loc: locOf(expr), }; } + // A member the checker declares on the receiver, stored in a pure + // index-signature record (an object literal that spread one, see + // dictSpreadLiteralShape): the key is always present, so the keyed + // read answers the member through the slot type. + if ( + actual?.type.kind === "record" && + shape.fields.length === 0 && + declaredMemberOf(lowerer, expr.expression, expr.name.text) + ) { + return lowerer.maybeNarrow( + { + kind: "recordKeyGet", + obj: actual, + shapeId: actual.type.shapeId, + key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) }, + type: shape.indexValue, + loc: locOf(expr), + }, + expr, + ); + } lowerer.unsupported( "SC1090", expr, @@ -3557,6 +3732,223 @@ function lowerStreamObjectProperty( return lowerStreamProperty(lowerer, expr, info); } +/** The builtin constructors JavaScript sources read as real function + * values rather than identity tokens: the runtime interns one function per + * constructor, and dynamic values answer `.constructor` with the same one, + * so `o.constructor === Object` is an exact identity test. */ +const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ + "Object", + "Function", + "Date", + "RegExp", + "Map", + "Set", + "Promise", + "WeakMap", + "WeakSet", +]); + +/** The builtin global functions JavaScript sources read as interned + * function values (the `cond ? setImmediate : process.nextTick` feature + * probe of bcryptjs and async libraries): typeof answers "function", + * identity holds across spellings, and calls run the timer APIs. */ +const GLOBAL_FUNCTION_VALUES: ReadonlySet = new Set([ + "setTimeout", + "setInterval", + "setImmediate", + "clearTimeout", + "clearInterval", + "clearImmediate", + "queueMicrotask", +]); + +/** A builtin function value: one interned runtime function per name. */ +export function builtinFunctionValue(name: string, loc: SrcLoc): IrExpr { + return { + kind: "libCall", + fn: "dyn.builtinFunction", + args: [{ kind: "strLit", value: name, type: STRING, loc }], + type: DYN, + loc, + }; +} + +/** The value a source reads for the stdlib global `canonical` (builtin + * constructors in both languages, builtin functions in JavaScript), or + * null when it stays an identity token (JavaScript) or fenced. */ +export function builtinGlobalValue(canonical: string, loc: SrcLoc, js = false): IrExpr | null { + if (js && GLOBAL_FUNCTION_VALUES.has(canonical)) return builtinFunctionValue(canonical, loc); + if (GLOBAL_CONSTRUCTOR_VALUES.has(canonical)) { + return { + kind: "libCall", + fn: "dyn.globalConstructor", + args: [{ kind: "strLit", value: canonical, type: STRING, loc }], + type: DYN, + loc, + }; + } + return null; +} + +/** The members the interned builtin function values model (the runtime + * answers each one as Node does); any other member read through them would + * answer undefined, so it keeps its static lowering or its fence. */ +const BUILTIN_VALUE_MEMBERS: ReadonlySet = new Set(["name", "length", "constructor"]); + +/** The Object statics whose answers for an interned builtin function value + * match Node's (identity, and the empty enumerable own-key lists). */ +const BUILTIN_VALUE_REFLECTION: ReadonlySet = new Set(["is", "keys", "values", "entries"]); + +/** The value `builtinGlobalValue` gives the stdlib global `expr` read at its + * use site: null when the use reads the global's members (they keep their + * lowerings and fences), and SC2020 when it is the target of reflection or + * redefinition (`Object.hasOwn(Object, "hasOwn")`, + * `Object.defineProperty(Promise, ...)`, `hasOwnProperty.call(Map, ...)`), + * which would observe or change members the interned value does not + * model. */ +export function builtinGlobalValueAt( + lowerer: Lowerer, + expr: ts.Expression, + canonical: string, + loc: SrcLoc, + js: boolean, +): IrExpr | null { + if (readsBuiltinGlobalMembers(expr)) return null; + const value = builtinGlobalValue(canonical, loc, js); + if (value === null) return null; + let node: ts.Node = expr; + while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent; + const call = node.parent; + if ( + call !== undefined && + ts.isCallExpression(call) && + call.expression !== node && + ts.isPropertyAccessExpression(call.expression) + ) { + const callee = call.expression; + const owner = stdlibGlobalNameOf(lowerer, callee.expression); + const reflects = + owner === "Reflect" || + (owner === "Object" && !BUILTIN_VALUE_REFLECTION.has(callee.name.text)) || + ((callee.name.text === "call" || callee.name.text === "apply") && + call.arguments.nodes[0] === node && + lowerer.isStdlibSymbol( + lowerer.checker.getSymbolAtLocation( + ts.isPropertyAccessExpression(callee.expression) + ? callee.expression.name + : callee.expression, + ), + )); + if (reflects) { + const name = + owner !== null ? `${owner}.${callee.name.text}` : callee.getText().replace(/\s+/g, ""); + lowerer.noLowering( + `${name}(${canonical}, ...)`, + expr, + "builtin constructor and function values model only name, length and constructor, so reflecting on or redefining their members has no lowering", + ); + } + } + return value; +} + +/** Whether the stdlib global `expr` is read for its members (`Map.prototype`, + * `Date["UTC"]`, `"prototype" in Map`) rather than as a value. Those reads + * keep the global's member lowerings and fences: its interned function value + * models only BUILTIN_VALUE_MEMBERS. */ +function readsBuiltinGlobalMembers(expr: ts.Expression): boolean { + let node: ts.Node = expr; + while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + if (parent === undefined) return false; + if (ts.isPropertyAccessExpression(parent) && parent.expression === node) + return !BUILTIN_VALUE_MEMBERS.has(parent.name.text); + if (ts.isElementAccessExpression(parent) && parent.expression === node) return true; + return ( + ts.isBinaryExpression(parent) && + parent.operatorToken.kind === ts.SyntaxKind.InKeyword && + parent.right === node + ); +} + +/** Reports SC2020 when `access` reads a stdlib static member or the + * prototype of a builtin constructor with an interned function value through + * a receiver the checker types as the constructor's static side + * (`MapConstructor`); null otherwise. Calls that lower statically never + * reach the member read. */ +function fenceBuiltinConstructorStaticRead( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, +): null { + if (BUILTIN_VALUE_MEMBERS.has(access.name.text)) return null; + const symbol = lowerer.typeOf(access.expression).getSymbol(); + if ( + symbol !== undefined && + symbol.name.endsWith("Constructor") && + GLOBAL_CONSTRUCTOR_VALUES.has(symbol.name.slice(0, -"Constructor".length)) && + lowerer.isStdlibSymbol(symbol) + ) + lowerer.stdlibMemberFence(access); + return null; +} + +/** Object.prototype members a plain record inherits, read as values. */ +const OBJECT_PROTOTYPE_VALUE_READS: ReadonlySet = new Set([ + "constructor", + "valueOf", + "toString", + "hasOwnProperty", + "propertyIsEnumerable", +]); + +/** Whether `expr` is the callee of a call (`o.toString()`), which keeps its + * call lowering rather than reading the member as a value. */ +function isCalleeOf(expr: ts.PropertyAccessExpression): boolean { + let node: ts.Node = expr; + while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + return ( + parent !== undefined && + (ts.isCallExpression(parent) || ts.isNewExpression(parent)) && + parent.expression === node + ); +} + +/** The constructor a primitive dynamic value answers for `.constructor`: + * the same interned closure the `Number`/`String`/`Boolean`/`BigInt` + * identifiers read (dequal's `(ctor = foo.constructor) === bar.constructor` + * over numbers), or `otherwise` for non-primitives. `value` must be safe to + * read repeatedly. */ +export function primitiveConstructorOr( + lowerer: Lowerer, + value: IrExpr, + otherwise: IrExpr, + loc: SrcLoc, +): IrExpr { + let result = otherwise; + for (const [test, name] of [ + ["bigint", "BigInt"], + ["boolean", "Boolean"], + ["string", "String"], + ["number", "Number"], + ] as const) { + result = { + kind: "ternary", + cond: { kind: "dynTest", test, value, type: BOOL, loc }, + then: { + kind: "dynFrom", + value: primitiveCtorClosure(lowerer, name, loc, true), + type: DYN, + loc, + }, + else_: result, + type: DYN, + loc, + }; + } + return result; +} + /** True when `access` reads a member that JavaScript sources attach to a * function by assignment (`fn.member = value`): the receiver's type is * callable and every declaration of the member is such an assignment in a @@ -3581,3 +3973,10 @@ function jsFunctionExpandoMember(lowerer: Lowerer, access: ts.PropertyAccessExpr }) ); } + +/** Whether the checker declares `name` as a property of `receiver`'s type + * (as opposed to reading it through an index signature). */ +export function declaredMemberOf(lowerer: Lowerer, receiver: ts.Expression, name: string): boolean { + const symbol = lowerer.checker.getPropertyOfType(lowerer.typeOf(receiver), name); + return !!symbol && symbol.declarations.length > 0; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/typeof.ts b/packages/compiler/src/frontend/lowering/expressions/typeof.ts index 30d24e22a..c56ef4097 100644 --- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts +++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts @@ -13,6 +13,7 @@ import { } from "../../../ir/ir.js"; import { lowerWebAssemblyTypeof } from "../builtins/webassembly.js"; import { isNavigatorGlobal } from "../builtins/navigator.js"; +import { processHasIpc } from "../builtins/process.js"; import { lowerPerfHooksTypeof } from "../builtins/performance.js"; import { isNodeModuleValue } from "../lower-node-module.js"; import { builtinClassRefOf } from "../builtins/class-values.js"; @@ -27,7 +28,7 @@ import { typeofAnswer, } from "../lower-exprs.js"; import { tryLowerExpression } from "./try-lower-expression.js"; -import { isJsSourceFile } from "../../program.js"; +import { isJsSourceFile, isNodeEsmFile } from "../../program.js"; import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js"; import { varRef } from "../../../ir/build.js"; import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors.js"; @@ -38,8 +39,41 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc if (webAssembly) return webAssembly; if (lowerer.isStdlibGlobal(expr.expression, "process")) return { kind: "strLit", value: "object", type: STRING, loc }; + { + let operand = expr.expression; + while (ts.isParenthesizedExpression(operand)) operand = operand.expression; + const ipcMember = + ts.isPropertyAccessExpression(operand) && !operand.questionDotToken + ? lowerer.stdlibGlobalMember(operand, "process") + : null; + if (ipcMember === "send" || ipcMember === "disconnect") { + return { + kind: "ternary", + cond: processHasIpc(lowerer, loc), + then: { kind: "strLit", value: "function", type: STRING, loc }, + else_: { kind: "strLit", value: "undefined", type: STRING, loc }, + type: STRING, + loc, + }; + } + } if (lowerer.isStdlibGlobal(expr.expression, "fetch")) return { kind: "strLit", value: "function", type: STRING, loc }; + // `__dirname` / `__filename` in a JavaScript ES module, where Node never + // defines them: typeof answers "undefined" instead of throwing. + { + let operand = expr.expression; + while (ts.isParenthesizedExpression(operand)) operand = operand.expression; + if ( + ts.isIdentifier(operand) && + (operand.text === "__dirname" || operand.text === "__filename") && + lowerer.isStdlibGlobal(operand, operand.text) + ) { + const sf = operand.getSourceFile(); + if (isNodeEsmFile(sf, lowerer.program) && !/\.tsx?$/.test(sf.fileName)) + return { kind: "strLit", value: "undefined", type: STRING, loc }; + } + } if (isNavigatorGlobal(lowerer, expr.expression)) return { kind: "strLit", value: "object", type: STRING, loc }; const performance = lowerPerfHooksTypeof(lowerer, expr.expression); diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index 8b98389b6..2270f6043 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -1,5 +1,6 @@ /* Increment, decrement and prefix unary operators, and compound field updates. */ import * as ts from "../../ts7/adapter.js"; +import { isStrictCode } from "../strict-mode.js"; import { isPrivateWriteReceiver } from "../private-brands.js"; import type { Lowerer } from "../lowerer.js"; import { @@ -751,6 +752,7 @@ export function fieldSetStmt( className: target.className, field: target.field, value, + ...(isStrictCode(blame) ? {} : { sloppy: true as const }), loc, } : { diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index ed549e4ff..ae864c2cb 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -227,6 +227,7 @@ import { } from "./optional-arguments.js"; import { fenceSymbolFieldCopy } from "./symbol-fields.js"; import { lowerClassDataDescriptor, lowerClassDescriptorRead } from "./class-descriptors.js"; +import { lowerClassFieldDefine, lowerClassIntegrity } from "./class-attributes.js"; import { classStaticDataFor } from "./class-static-data.js"; import { lowerUrlFactory } from "./lower-url.js"; import { nullishValueUnitOf } from "./binding-analysis.js"; @@ -7717,6 +7718,8 @@ function lowerObjectStaticCall( if (target && declaredTarget && isDynTypedRefType(declaredTarget)) target = lowerer.coerceToExpected(target, declaredTarget); if (target && member === "defineProperty") { + const field = lowerClassFieldDefine(lowerer, call, target); + if (field) return field; const native = lowerClassDataDescriptor(lowerer, call, member, target); if (native) return native; } @@ -7859,6 +7862,19 @@ function lowerObjectStaticCall( loc, }; } + if (member === "preventExtensions" || member === "seal") { + const integrity = lowerClassIntegrity(lowerer, call, member, value); + if (integrity) return integrity; + } else if (isDynTypedRefType(value.type)) { + // The instance's checked view carries its integrity state. + return { + kind: "libCall", + fn: member === "isSealed" ? "dyn.isSealed" : "dyn.isExtensible", + args: [lowerer.coerceToExpected(value, DYN)], + type: BOOL, + loc: locOf(call), + }; + } if (value.type.kind === "dyn") { const operation = { preventExtensions: "dyn.preventExtensions", @@ -7929,6 +7945,8 @@ function lowerObjectStaticCall( ) ) return value; + const integrity = lowerClassIntegrity(lowerer, call, "freeze", value); + if (integrity) return integrity; lowerer.noLowering( "Object.freeze of a possibly-aliased value", call, diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 6589d7c04..3d234cfff 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -70,6 +70,7 @@ import { } from "./instantiation-path.js"; import { appendImplicitUndefinedReturn, hasExplicitJsDocReturn } from "./function-returns.js"; import { bodyReadsArguments } from "../arguments-usage.js"; +import { errorNameAfterField, errorViewFields } from "./error-name-order.js"; import { checkedJsDocumentedParameter, documentedParameter, @@ -88,7 +89,6 @@ import { isCjsJsFile, isJsSourceFile, isModuleExportsAccess, - isNodeTypesPath, locOf, } from "../program.js"; import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./lowerer.js"; @@ -1046,9 +1046,11 @@ export function builtinStreamInfoOf( if (rec.lib === symbol.name) irName = name; } if (!irName) return null; + // Both the bundled declarations and @types/node declare the classes + // inside `declare module "stream"` (@types/node nests them in the module's + // `internal` namespace). const declared = symbol.declarations.some((d) => { if (!ts.isClassDeclaration(d)) return false; - if (isNodeTypesPath(d.getSourceFile().fileName)) return false; let node: ts.Node | undefined = d.parent; while (node) { if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) { @@ -2212,11 +2214,7 @@ export function collectClassShapeInner( } if (lowerer.collectingExprClasses.has(decl)) { for (let parent = base; parent; parent = parent.base) { - if ( - parent.builtinEmitter || - parent.builtinStream !== undefined || - parent.builtinEventTarget !== undefined - ) { + if (parent.builtinEventTarget !== undefined) { lowerer.unsupported( "SC1090", decl, @@ -4259,6 +4257,12 @@ export function collectClassShapeInner( ...(deferredInitFields.size > 0 ? { deferredInitFields } : {}), ...(escape.escapes ? { constructionEscapes: true as const } : {}), }; + let root: ClassInfo | null = info; + while (root?.base) root = root.base; + if (root !== info && root?.def.name === "%Error") { + const after = errorNameAfterField(info, errorViewFields(info.def.fields)); + if (after !== undefined) info.def.errorNameAfter = after; + } // GENERIC members get their declaring-class backlink now that the // info exists (instance lowering reads it for `this` typing and the // generic-class binding merge). @@ -5182,16 +5186,11 @@ export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node) loc: locOf(blame), }; } - if ( - lowerer.inheritsBuiltinEmitterCtor(info) || - inheritsBuiltinStreamCtor(lowerer, info) || - eventTargetRootOf(info) !== null || - (() => { - for (let c = info.base; c; c = c.base) - if (c.builtinEmitter || c.builtinStream !== undefined) return true; - return false; - })() - ) { + // Classes extending EventEmitter or a stream class construct through + // their own (or synthesized) `%C.constructor`, whose super call runs the + // runtime initializer, so the class object's construct thunk serves them + // like any program class. EventTarget subclasses keep the refusal. + if (eventTargetRootOf(info) !== null) { lowerer.unsupported( "SC1090", blame, @@ -8062,6 +8061,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL className: info.def.name, field: f.name, value: dynUndefinedExpr(loc), + define: true, loc, }); } @@ -8077,6 +8077,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL className: info.def.name, field: f.name, value, + define: true, loc: locOf(f.initializer), }); } catch (e) { @@ -8734,6 +8735,60 @@ function inheritsBuiltinStreamCtor(lowerer: Lowerer, info: ClassInfo): boolean { return false; } +/** The run-time form of refuseInheritedBuiltinCtorArgs, for construction + * from checked-dynamic code (`new F(options)` with F an untyped value + * holding C), whose argument count is known only to the caller: a + * runtimeFence located at C, or null when C's constructor takes its + * arguments. */ +export function inheritedBuiltinCtorFence( + lowerer: Lowerer, + info: ClassInfo, + loc: SrcLoc, +): IrStmt | null { + let feature: string | null = null; + if (inheritsBuiltinStreamCtor(lowerer, info)) + feature = `new ${info.def.name.replace(/^%/, "")} with arguments through an inherited stream constructor (declare a constructor that passes an inline options object to super(...))`; + else if (lowerer.inheritsBuiltinEmitterCtor(info)) + feature = "EventEmitter constructor options ('captureRejections')"; + if (feature === null) return null; + // Located at the class, whose constructor takes no arguments. + const at = info.decl !== null ? locOf(info.decl) : loc; + const diag = unsupportedDiag("SC1090", at, feature); + lowerer.runtimeFences.push(diag); + const line = + info.decl !== null + ? ts.getLineAndCharacterOfPosition(info.decl.getSourceFile(), at.start).line + 1 + : null; + const where = line !== null ? ` [${diag.code} at ${at.file}:${line}]` : ""; + return { kind: "runtimeFence", code: diag.code, message: `${diag.message}${where}`, loc }; +} + +/** `new C(args)` where C's constructor is the inherited runtime stream or + * EventEmitter constructor (a ctor-less chain into the base): that + * constructor takes zero arguments here (the options bag fences, like the + * super() form), so arguments report at the first one. Applies to direct + * construction and to construction through a class value of C. Stream + * subclasses come first: their chain roots at the emitter too, but the + * message should name the stream story. */ +function refuseInheritedBuiltinCtorArgs( + lowerer: Lowerer, + info: ClassInfo, + expr: ts.NewExpression, +): void { + const first = expr.arguments?.nodes[0]; + if (first === undefined) return; + if (inheritsBuiltinStreamCtor(lowerer, info)) { + lowerer.noLowering( + `new ${info.def.name.replace(/^%/, "")} with arguments through an inherited stream constructor`, + first, + "declare a constructor that passes an inline options object to super(...)", + ); + } + if (lowerer.inheritsBuiltinEmitterCtor(info)) { + lowerer.unsupported("SC1090", first, "EventEmitter constructor options ('captureRejections')"); + } +} + /** Preserve argument evaluation order and option presence through builtin * and inherited Error constructors. Checked-dynamic conversion retains * Error identity and distinguishes a missing cause from undefined. */ @@ -8858,24 +8913,7 @@ function lowerProgramClassNew( "construction through a function-local class's internal name", ); lowerer.noteEdge(`%${info.def.name}.constructor`); - // A ctor-less chain into an EventEmitter base inherits `new C()` — - // zero arguments (the options bag fences, like the super() form). - // Stream subclasses come first: their chain roots at the emitter - // too, but the message should name the stream story. - if (inheritsBuiltinStreamCtor(lowerer, info) && (expr.arguments?.nodes ?? []).length > 0) { - lowerer.noLowering( - `new ${info.def.name.replace(/^%/, "")} with arguments through an inherited stream constructor`, - expr.arguments!.nodes[0]!, - "declare a constructor that passes an inline options object to super(...)", - ); - } - if (lowerer.inheritsBuiltinEmitterCtor(info) && (expr.arguments?.nodes ?? []).length > 0) { - lowerer.unsupported( - "SC1090", - expr.arguments!.nodes[0]!, - "EventEmitter constructor options ('captureRejections')", - ); - } + refuseInheritedBuiltinCtorArgs(lowerer, info, expr); // A ctor-less chain into a builtin error base inherits both arguments; // each synthesized constructor forwards the same pair to its base. const args = lowerer.inheritsBuiltinErrorCtor(info) @@ -9466,18 +9504,57 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // fenced, so copying the scalar cannot create an observable lie. if (symbol && symbol.name === "Date" && lowerer.isStdlibSymbol(symbol)) { const args = expr.arguments?.nodes ?? []; - if (args.some(ts.isSpreadElement) || args.length > 1) { + if (args.some(ts.isSpreadElement)) { lowerer.noLowering( - `new Date with ${args.length} arguments`, + `new Date with spread arguments`, expr, - "new Date(), new Date(milliseconds), and new Date(dateString) are supported; the local-time year/month field constructor has no lowering", + "new Date(), new Date(milliseconds), new Date(dateString), and new Date(year, monthIndex, ...) with listed arguments are supported", symbol, ); } + // new Date(year, monthIndex[, date[, hours[, minutes[, seconds[, ms]]]]]): + // local-time fields, converted to UTC in the host zone at run time. + // JavaScript callers pass their arguments to the runtime's ToNumber + // (in order, exactly the constructor's coercions); typed callers + // pass numbers, completed with the spec's defaults (date 1, time + // parts 0) for the seven-f64 ABI. + if (args.length > 1) { + if (isJsSourceFile(expr.getSourceFile())) { + return { + kind: "libCall", + fn: "date.nativeNewFields", + args: [ + { + kind: "dynArrLit", + elems: args.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }; + } + if (args.length > 7) { + lowerer.noLowering(`new Date with ${args.length} arguments`, expr, undefined, symbol); + } + const defaults = [0, 0, 1, 0, 0, 0, 0]; + const fields: IrExpr[] = []; + for (let i = 0; i < 7; i++) { + const arg = args[i]; + fields.push( + arg !== undefined + ? lowerer.lowerExprExpecting(arg, F64) + : { kind: "numLit", value: defaults[i]!, type: F64, loc }, + ); + } + return { kind: "libCall", fn: "date.newLocal", args: fields, type: DATE_T, loc }; + } if (isJsSourceFile(expr.getSourceFile())) { return { kind: "libCall", - fn: "date.nativeNew", + // One argument: a string parses with the full date parser. + fn: args.length === 1 ? "date.nativeNewValue" : "date.nativeNew", args: [ { kind: "dynArrLit", @@ -10123,6 +10200,9 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { `EventEmitter and stream class values constructed with options (construct with 'new ${info.def.name.slice(1)}({ ... })' directly)`, ); } + // A program class whose constructor is the inherited runtime one + // takes no arguments through its class value either. + refuseInheritedBuiltinCtorArgs(lowerer, info, expr); defineClassValueConstruct(lowerer, info); const args = lowerer.completeArgs( expr.arguments?.nodes ?? [], diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index 54f9d158b..d035b21c2 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -290,8 +290,17 @@ function lowerDnsModuleCall( loc: SrcLoc, ): IrExpr { const args = expr.arguments.nodes; - const dyn = (node: ts.Expression): IrExpr => - lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN); + const dyn = (node: ts.Expression): IrExpr => { + // `opts as dns.LookupOneOptions` over a checked-dynamic value (the + // overload-satisfying cast of a pass-through lookup): the cast changes + // no value, so the options reach the runtime as they are. + let inner = node; + while (ts.isAsExpression(inner) || ts.isParenthesizedExpression(inner)) + inner = inner.expression; + if (inner !== node && lowerer.mapTypeOf(lowerer.typeOf(inner))?.kind === "dyn") + return lowerer.lowerExpr(inner); + return lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN); + }; // A callback whose second parameter is typed (string, LookupAddress[]) // cannot take the undefined Node passes after an error. const typedResult = (node: ts.Expression): IrExpr => { diff --git a/packages/compiler/src/frontend/lowering/lower-enums.ts b/packages/compiler/src/frontend/lowering/lower-enums.ts index 159d72f32..cc2797fb9 100644 --- a/packages/compiler/src/frontend/lowering/lower-enums.ts +++ b/packages/compiler/src/frontend/lowering/lower-enums.ts @@ -29,7 +29,8 @@ import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { locOf } from "../program.js"; -import { F64, type IrExpr, STRING } from "../../ir/ir.js"; +import { BOOL, F64, type IrExpr, STRING, UNDEFINED_T } from "../../ir/ir.js"; +import { nodeThrowExpr } from "../../ir/build.js"; /** The member's source-name text (identifiers and string-literal names — * the only name forms tsc allows on enum members). */ @@ -182,6 +183,15 @@ function lowerEnumReverseRead( // existence tracking; nothing needs it — fence through the generic path. if (decls.some((d) => isAmbientEnumDecl(d))) return null; const idx = foldConstIndex(lowerer, key); + const keyType = lowerer.typeOf(key); + if ( + idx === null && + ((keyType.flags & ts.TypeFlags.StringLike) !== 0 || + (keyType.isUnionType() && + ts.constituentTypes(keyType).every((t) => (t.flags & ts.TypeFlags.StringLike) !== 0))) + ) { + return lowerEnumKeyRead(lowerer, expr, decls, key); + } if (idx === null) { lowerer.unsupported( "SC1090", @@ -206,6 +216,109 @@ function lowerEnumReverseRead( return { kind: "strLit", value: name, type: STRING, loc }; } +/** Object.prototype members every enum object inherits: a runtime key + * naming one reads a function, which the member value types cannot hold. */ +const OBJECT_PROTOTYPE_KEYS = new Set([ + "__defineGetter__", + "__defineSetter__", + "__lookupGetter__", + "__lookupSetter__", + "__proto__", + "constructor", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "toLocaleString", + "toString", + "valueOf", +]); + +/** `E[key]` with a RUNTIME string key (`Color[name as keyof typeof + * Color]` — the name-to-value lookup): the key evaluates once and is + * compared against every member NAME, each hit answering that member's + * constant, exactly the forward entries tsc's emit stores. A key naming no + * member is Node's undefined, so the read carries an undefined arm. Keys + * Node would answer from elsewhere on the object throw a named runtime + * refusal instead of a wrong value: a numeric member's reverse entry (its + * value spelled as a string — Node answers the member NAME) and the + * inherited Object.prototype members. */ +function lowerEnumKeyRead( + lowerer: Lowerer, + expr: ts.ElementAccessExpression, + decls: readonly ts.EnumDeclaration[], + key: ts.Expression, +): IrExpr { + const loc = locOf(expr); + const enumName = decls[0]!.name.text; + const valueType = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (valueType === null) lowerer.badType(expr, lowerer.typeOf(expr)); + const resultType = lowerer.withUndefinedArm(valueType); + const keyValue = lowerer.coerceToExpected(lowerer.lowerExpr(key), STRING); + const keyLocal = lowerer.declareHiddenLocal("%enumKey", STRING); + const keyRef: IrExpr = { kind: "varRef", localId: keyLocal.id, type: STRING, loc }; + const matches = (text: string): IrExpr => ({ + kind: "strEq", + negated: false, + left: keyRef, + right: { kind: "strLit", value: text, type: STRING, loc }, + type: BOOL, + loc, + }); + const refusal = (what: string): IrExpr => + nodeThrowExpr( + 0, + "SC1090", + `${what} through a runtime key ('${enumName}[key]') is not supported yet`, + resultType, + loc, + ); + const arms: { cond: IrExpr; value: IrExpr }[] = []; + const names = new Set(); + const reverse = new Set(); + for (const d of decls) { + for (const member of d.members.nodes) { + const name = memberNameText(member); + const constant = lowerEnumMemberRead(lowerer, expr, member); + if (constant === null) + lowerer.unsupported("SC1090", expr, `'${enumName}[key]' over this enum`); + if (!names.has(name)) { + names.add(name); + arms.push({ cond: matches(name), value: lowerer.coerceToExpected(constant, resultType) }); + } + const value = lowerer.checker.getConstantValue(member); + if (typeof value === "number") reverse.add(String(value)); + } + } + for (const text of reverse) { + if (names.has(text)) continue; + arms.push({ cond: matches(text), value: refusal("enum reverse lookups") }); + } + for (const text of OBJECT_PROTOTYPE_KEYS) { + if (names.has(text) || reverse.has(text)) continue; + arms.push({ cond: matches(text), value: refusal("inherited enum object members") }); + } + let result: IrExpr = { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }; + result = lowerer.coerceToExpected(result, resultType); + for (let i = arms.length - 1; i >= 0; i--) { + const arm = arms[i]!; + result = { + kind: "ternary", + cond: arm.cond, + then: arm.value, + else_: result, + type: resultType, + loc, + }; + } + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: keyLocal.id, init: keyValue, loc }], + result, + type: resultType, + loc, + }; +} + /** A compile-time-constant NUMERIC index: a numeric literal, its negation, * or an enum member read whose constant is a number. */ function foldConstIndex(lowerer: Lowerer, e: ts.Expression): number | null { diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 6cbe5d7a4..5cbf103d5 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -38,7 +38,14 @@ import { typeEquals, typeKey, } from "../../ir/ir.js"; -import { isJsSourceFile, locOf } from "../program.js"; +import { + cjsExportAssignmentOf, + cjsExportDiscardReason, + cjsExportTargetLiteral, + isJsSourceFile, + locOf, +} from "../program.js"; +import { npmStaticLexerVisibleNames } from "../npm-static.js"; import { type CompoundOp, NARROW_FIRST, @@ -84,6 +91,8 @@ import { lowerElementCompoundParts } from "./expressions/element-access.js"; export { DEFERRED_READ_MESSAGE, abstractPropertyDeclOf, + builtinFunctionValue, + builtinGlobalValue, fieldGetExpr, fieldTarget, isMatchSliceType, @@ -92,6 +101,7 @@ export { lowerIntrinsicProperty, lowerUnionProperty, matchResultNamedGroupsOf, + primitiveConstructorOr, } from "./expressions/property-access.js"; export { lowerAsExpression, @@ -2057,6 +2067,102 @@ function aliasTypeofArmOf( return null; } +/** The names Node's ES-module loader copies off a CommonJS module that a + * bundler rewrite spells as accessors (npmStaticLexerVisibleNames), with + * their getters in that order; empty for every other module. */ +function lexerVisibleAccessors(dep: ts.SourceFile): ts.GetAccessorDeclaration[] { + const names = npmStaticLexerVisibleNames(dep.fileName); + if (names.length === 0) return []; + let table: ts.ObjectLiteralExpression | null = null; + for (const stmt of dep.statements.nodes) { + const cjs = cjsExportAssignmentOf(stmt); + if (cjs?.kind === "table" && cjsExportDiscardReason(stmt) === null) { + table = cjsExportTargetLiteral(cjs.expr.right, dep); + } + } + if (table === null) return []; + const getters = new Map(); + for (const p of table.properties.nodes) { + if (ts.isGetAccessorDeclaration(p) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))) + getters.set(p.name.text, p); + } + return names.flatMap((n) => { + const g = getters.get(n); + return g === undefined ? [] : [g]; + }); +} + +/** The module global holding the value an accessor export had when an + * ES-module importer first linked its CommonJS module. */ +export function cjsAccessorSnapshotOf( + lowerer: Lowerer, + getter: ts.GetAccessorDeclaration, +): { id: string; type: IrType } | null { + const hit = lowerer.cjsAccessorSnapshots.get(getter); + if (hit !== undefined) return hit; + if (!lexerVisibleAccessors(getter.getSourceFile()).includes(getter)) return null; + const call = cjsAccessorCall(lowerer, getter, locOf(getter)); + if (call === null) return null; + const snapshot = { id: `%g.cjsSnapshot.${lowerer.cjsAccessorSnapshots.size}`, type: call.type }; + lowerer.globalsList.push({ + id: snapshot.id, + name: "%cjsSnapshot", + type: call.type, + mutable: true, + }); + lowerer.cjsAccessorSnapshots.set(getter, snapshot); + return snapshot; +} + +/** Node's ES-module loader reads every lexer-visible export of a CommonJS + * module once, when an importer first links it, and the import bindings + * keep those values. For the accessors a bundler rewrite introduced, the + * importer's link point runs each getter once, in the lexer's order, and + * stores the values its bindings read. Empty when the module has none. */ +export function cjsAccessorSnapshotStores( + lowerer: Lowerer, + dep: ts.SourceFile, + loc: SrcLoc, +): IrStmt[] { + const getters = lexerVisibleAccessors(dep); + if (getters.length === 0) return []; + let readyId = lowerer.cjsSnapshotReady.get(dep); + if (readyId === undefined) { + readyId = `%g.cjsSnapshotReady.${lowerer.cjsSnapshotReady.size}`; + lowerer.globalsList.push({ id: readyId, name: "%cjsSnapshotReady", type: BOOL, mutable: true }); + lowerer.cjsSnapshotReady.set(dep, readyId); + } + const stores: IrStmt[] = [ + { + kind: "assign", + localId: readyId, + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }, + ]; + for (const getter of getters) { + const snapshot = cjsAccessorSnapshotOf(lowerer, getter); + const call = cjsAccessorCall(lowerer, getter, loc); + if (snapshot === null || call === null) return []; + stores.push({ kind: "assign", localId: snapshot.id, value: call, loc }); + } + return [ + { + kind: "if", + cond: { + kind: "unary", + op: "!", + operand: { kind: "varRef", localId: readyId, type: BOOL, loc }, + type: BOOL, + loc, + }, + then: stores, + else_: null, + loc, + }, + ]; +} + /** The interned lifted-getter call both accessor-read paths share. */ export function cjsAccessorCall( lowerer: Lowerer, @@ -2588,6 +2694,24 @@ export function symbolFieldTarget( }; } +/** `return Promise.reject(reason)` in an async function: the checker's + * context is the awaited return type (or its union with PromiseLike), not + * a promise, but the async function adopts the returned promise, so the + * promise of its own result type is the honest type. Null elsewhere. */ +function asyncReturnPromise(lowerer: Lowerer, call: ts.CallExpression): IrType | null { + if (lowerer.ctx.isAsync !== true || lowerer.ctx.generator) return null; + let node: ts.Node = call; + while (ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + const returned = + (ts.isReturnStatement(parent) && parent.expression === node) || + (ts.isArrowFunction(parent) && parent.body === node); + if (!returned) return null; + const ret = lowerer.ctx.returnType; + if (ret.kind === "void" || ret.kind === "promise") return null; + return { kind: "promise", inner: ret }; +} + /** `Promise.reject(reason)` on THE Promise global: a pre-rejected * promise. The reason is pinned to the Error hierarchy (the executor * reject parameter's contract — rejection payloads share the @@ -2619,7 +2743,9 @@ export function lowerPromiseRejectCall( // A maybe-promise context (`return Promise.reject(e)` from a function // returning `T | Promise`) names its one promise arm. const maybePromise = contextual ? maybePromiseUnionOf(lowerer, contextual) : null; - const type = maybePromise ? maybePromise.promise : contextual; + const type = maybePromise + ? maybePromise.promise + : (asyncReturnPromise(lowerer, call) ?? contextual); const reasonNode = call.arguments.nodes[0]!; // A CHECKED-DYNAMIC reason (`Promise.reject(value)` where value rode // an untyped binding — the tracingChannel suite's shape): the checked-dynamic tree diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 6f01cee19..3be155402 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -21,6 +21,7 @@ import { type SrcLoc, canConvertToDyn, canDynCheckTo, + CLASS_FIELD_ATTRIBUTES_PREFIX, classDynViewSupported, DYN_CLASS_PROPERTIES, isClassOwnEnumerableFieldName, @@ -500,12 +501,16 @@ function classInspectsThroughView(lowerer: Lowerer, info: ClassInfo | undefined) ); } -/** Whether a class holds properties added outside its declared layout, which - * only its checked-dynamic view can list. Decided after lowering, when the +/** Whether a class holds properties added outside its declared layout, or + * fields Object.defineProperty can make non-enumerable, which only its + * checked-dynamic view lists as Node does. Decided after lowering, when the * class has its final fields. */ function classInspectsAddedProperties(lowerer: Lowerer, info: ClassInfo): boolean { return ( - info.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES) && + info.def.fields.some( + (field) => + field.name === DYN_CLASS_PROPERTIES || field.name.startsWith(CLASS_FIELD_ATTRIBUTES_PREFIX), + ) && classDynViewSupported( info.def.fields, (id) => lowerer.shapes.get(id), diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts index 4f446bb89..be68abaca 100644 --- a/packages/compiler/src/frontend/lowering/lower-mock.ts +++ b/packages/compiler/src/frontend/lowering/lower-mock.ts @@ -154,7 +154,12 @@ function mockWrapperFactory( const { fn, call, calls, once, onceEntry, result, args } = mockContextFields(lowerer, ctxType)!; const impl = `${name}.impl`; const ctx = () => varRef("ctx.0", ctxType, loc); + // A checked-dynamic rest function (mock.fn()'s default type) takes its + // arguments as one trailing dyn array, recorded as the call's arguments + // and spread into the implementation. + const dynRest = fn.rest === true && fn.restAbi === undefined; const params = fn.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type })); + if (dynRest) params.push({ localId: "rest.0", name: "args", type: DYN }); const paramRefs = () => params.map((p) => varRef(p.localId, p.type, loc)); const argumentsValue = (): IrExpr => fn.rest @@ -219,13 +224,23 @@ function mockWrapperFactory( loc, }; }; - const invoke: IrExpr = { - kind: "callValue", - callee: varRef("f.0", fn, loc), - args: paramRefs(), - type: fn.ret, - loc, - }; + const invoke: IrExpr = dynRest + ? { + kind: "dynCall", + callee: { kind: "dynFrom", value: varRef("f.0", fn, loc), type: DYN, loc }, + calleeName: "implementation", + args: paramRefs(), + spreads: [{ arg: 0, what: "args" }], + type: DYN, + loc, + } + : { + kind: "callValue", + callee: varRef("f.0", fn, loc), + args: paramRefs(), + type: fn.ret, + loc, + }; const tryBody: IrStmt[] = voidReturn ? [ { kind: "exprStmt", expr: invoke, loc }, @@ -629,7 +644,37 @@ function requireUndefinedResultArm( } } -/** mock.fn(original, implementation?) and t.mock.fn(...). */ +/** `%mock.noop.N`: mock.fn()'s original, Node's `function () {}` — it + * returns undefined. Null when the function type's return type cannot hold + * undefined. */ +function mockNoopOriginal( + lowerer: Lowerer, + ctxType: IrType, + fn: FuncType, + loc: SrcLoc, +): IrExpr | null { + const voidReturn = fn.ret.kind === "void" || fn.ret.kind === "undefinedT"; + const undefinedValue = voidReturn ? null : lowerer.wrappedUndefined(fn.ret, loc); + if (!voidReturn && !undefinedValue) return null; + const name = liftOnce(lowerer, ctxType, "noop", (name) => { + const params = fn.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type })); + if (fn.rest === true && fn.restAbi === undefined) + params.push({ localId: "rest.0", name: "args", type: DYN }); + lowerer.liftedFns.push({ + name, + params, + returnType: fn.ret, + locals: params.map((p) => local(p.localId, p.name, p.type)), + body: [{ kind: "return", value: undefinedValue, loc }], + loc, + }); + }); + return { kind: "closure", fnName: name, captures: [], type: fn, loc }; +} + +/** mock.fn(original?, implementation?) and t.mock.fn(...). Without an + * original, the mock's original and implementation are a function that + * returns undefined, as in Node. */ export function lowerMockFn( lowerer: Lowerer, call: ts.CallExpression, @@ -658,11 +703,19 @@ export function lowerMockFn( const { fn } = fields; requireUndefinedResultArm(lowerer, fields, call, what); const args = call.arguments.nodes; - if (args.length === 0 || args.length > 2 || args.some(ts.isSpreadElement)) { + if (args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `${what} with ${args.length} arguments`, call, - "the supported forms are mock.fn(original) and mock.fn(original, implementation); mock.fn() and mock options have no lowering", + "the supported forms are mock.fn(), mock.fn(original) and mock.fn(original, implementation); mock options have no lowering", + ); + } + const noop = args.length === 0 ? mockNoopOriginal(lowerer, ctxType!, fn, loc) : null; + if (args.length === 0 && !noop) { + lowerer.noLowering( + `${what}() of a function returning '${lowerer.fmt(fn.ret)}'`, + call, + "mock.fn() without an original returns undefined: pass an original, or use a function type whose return type includes undefined", ); } const stmts: IrStmt[] = []; @@ -670,7 +723,7 @@ export function lowerMockFn( stmts.push({ kind: "varDecl", localId: originalLocal.id, - init: lowerer.lowerExprExpecting(args[0]!, fn), + init: noop ?? lowerer.lowerExprExpecting(args[0]!, fn), loc, }); const implementation = diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index fa4c98372..ba33811f9 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -145,7 +145,7 @@ import { forkTargetPaths } from "../fork-target.js"; import { workerTargetPaths } from "../worker-target.js"; import { isNativeProxyInitializer } from "./expressions/native-proxy.js"; import { handleEventReaderPrelude } from "./lower-event-target.js"; -import { jsHoldsDynObject } from "./lower-exprs.js"; +import { cjsAccessorSnapshotStores, jsHoldsDynObject } from "./lower-exprs.js"; import { keptIdentityAssertion } from "./assertion-identity.js"; /** One file's declarations, split for collection and init-body lowering. */ @@ -2851,6 +2851,14 @@ export function lowerFileInit( }); } else { header.push({ kind: "exprStmt", expr: call, loc }); + // An ES-module importer links a CommonJS dependency: Node + // copies its lexer-visible exports here. + if ( + (ts.isImportDeclaration(stmt) || ts.isExportDeclaration(stmt)) && + !isCjsJsFile(sf, lowerer.program) && + isCjsJsFile(dep, lowerer.program) + ) + header.push(...cjsAccessorSnapshotStores(lowerer, dep, loc)); } } } diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 2f0e1c66f..79aaaf270 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -42,6 +42,8 @@ import { STATIC_RESPONSE_CALLS, STATIC_RESPONSE_READS, } from "../../compat/fetch-profile.js"; +import { classGeneratorIterable } from "./class-iterables.js"; +import { generatorDrain } from "./iterator-adapters.js"; export function refuseUnsupportedApi( lowerer: Lowerer, @@ -2436,6 +2438,10 @@ export function lowerStdlibMethodCall( src = lowerer.coerceInto(spread.expression, src, inferred); } } + // A generator, or a class whose [Symbol.iterator] is a generator + // method, drains into the number[] the fold reads. + src = classGeneratorIterable(lowerer, src) ?? src; + if (src.type.kind === "generator") src = generatorDrain(lowerer, src, F64, loc) ?? src; if (src.type.kind !== "array" || src.type.elem.kind !== "f64") { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 0e4f3c24c..f0ddc1570 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1,3 +1,4 @@ +import { declaredMemberOf } from "./expressions/property-access.js"; import { hostsAll } from "./expressions/hosted-narrowing.js"; import { matchAllRowsReadByElements, @@ -16,7 +17,7 @@ import { InternalCompilerError } from "../../errors.js"; * flow (if/while/for/for-of/do, switch, try/catch, abrupt completions), * explicit resource management, and blocked-binding poisoning. */ import * as ts from "../ts7/adapter.js"; -import { processDataMemberValue } from "./builtins/process.js"; +import { processDataMemberValue, programReadsExitCode } from "./builtins/process.js"; import { requireInitFlagStmt } from "./require-initialization.js"; import { isJsonRequireDecl } from "./json-require.js"; import { bindingSource } from "../binding-source.js"; @@ -32,6 +33,7 @@ import { import { cjsExportMemberWriteGlobalOf, isCjsExportsIdentifier, + isCjsModuleExports, isFreshExportsReset, } from "./cjs-exports-object.js"; import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js"; @@ -87,11 +89,11 @@ import { isCjsJsFile, isEsModuleStamp, isJsSourceFile, - isNodeEsmFile, locOf, requireEarlyReference, requireSpecOf, } from "../program.js"; +import { isStrictCode } from "./strict-mode.js"; import { COMPOUND_ASSIGN_OPS, type CompoundOp, @@ -195,6 +197,7 @@ import { import { globalSymbolKey } from "./expressions/global-symbols.js"; import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js"; import { + builtinGlobalValue, jsHoldsDynObject, lowerEnvironmentKey, lowerNativeFunctionAssignment, @@ -223,6 +226,7 @@ import { constituentTypes } from "../ts7/checker.js"; import { npmStaticPackageOfPath } from "../npm-static.js"; import { fenceNodeModuleMutation } from "./lower-node-module.js"; import { fencePerfEntryKeyWalk } from "./builtins/performance.js"; +import { classGeneratorIterable } from "./class-iterables.js"; interface UsingRegistration { acquire: IrStmt[]; @@ -3773,6 +3777,9 @@ function stdlibGlobalTokenDestructure( const canonical = member.name === "global" ? "globalThis" : member.name; token = `[builtin ${canonical}]`; alias = canonical; + // Builtin constructors read as their interned function values, the + // same values the bare and `globalThis.X` spellings read. + value = builtinGlobalValue(canonical, locOf(el), true); } else if (globalName === "console" && !CONSOLE_CALL_MEMBERS.has(propName)) { token = `[builtin console.${propName}]`; } else if (TOKEN_OPAQUE_GLOBALS.has(globalName)) { @@ -3816,6 +3823,10 @@ function stdlibGlobalTokenDestructure( const out: IrStmt[] = []; for (const b of binds) { const loc = locOf(b.name); + if (b.value !== undefined && b.alias !== null) { + const symbol = lowerer.checker.getSymbolAtLocation(b.name); + if (symbol) lowerer.stdlibGlobalAliases.set(symbol, b.alias); + } if (b.value !== undefined) { if (b.g !== undefined) { out.push({ @@ -6176,35 +6187,6 @@ export function lowerTry(lowerer: Lowerer, stmt: ts.TryStatement): IrStmt { }; } -function hasUseStrictDirective(statements: readonly ts.Statement[]): boolean { - for (const statement of statements) { - if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression)) - return false; - if (statement.expression.text === "use strict") return true; - } - return false; -} - -function isStrictDelete(node: ts.DeleteExpression): boolean { - for (let parent: ts.Node | undefined = node.parent; parent; parent = parent.parent) { - if (ts.isClassDeclaration(parent) || ts.isClassExpression(parent)) return true; - if ( - ts.isFunctionLike(parent) && - parent.body && - ts.isBlock(parent.body) && - hasUseStrictDirective(parent.body.statements.nodes) - ) - return true; - if (ts.isSourceFile(parent)) { - return ( - hasUseStrictDirective(parent.statements.nodes) || - (!isCjsJsFile(parent) && (isNodeEsmFile(parent) || ts.isExternalModule(parent))) - ); - } - } - return false; -} - /** Statement-position `delete`: process.env keys → process.envUnset * (unsetenv), pure `Record` keys → recordKeyDelete (the * overflow Map delete), declared OPTIONAL fields → the field's ABSENT @@ -6320,7 +6302,7 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt obj = lowerer.coerceToExpected(obj, DYN); } if (obj.type.kind === "dyn") { - const strict = { kind: "boolLit", value: isStrictDelete(expr), type: BOOL, loc } as const; + const strict = { kind: "boolLit", value: isStrictCode(expr), type: BOOL, loc } as const; if (ts.isElementAccessExpression(target)) { const key = lowerer.lowerExprExpecting(target.argumentExpression, DYN); return { @@ -6939,6 +6921,25 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm "require() outside the module's top level (move it to the top of the file)", ); } + // Replacing the export object (or rebinding `exports`) anywhere but a + // top-level statement makes the module's exports depend on which path + // ran (`cond ? module.exports = a : module.exports = b`, an + // environment-dependent branch); the static export table has one shape. + if ( + !topLevelJs && + isJsSourceFile(expr.getSourceFile()) && + ts.isBinaryExpression(expr) && + expr.operatorToken.kind === ts.SyntaxKind.EqualsToken && + (isCjsModuleExports(lowerer, expr.left) || isCjsExportsIdentifier(lowerer, expr.left)) + ) { + lowerer.unsupported( + "SC1090", + expr.left, + isCjsModuleExports(lowerer, expr.left) + ? "replacing module.exports outside a top-level statement (a conditional or environment-dependent export object)" + : "rebinding 'exports' outside a top-level statement (a conditional or environment-dependent export object)", + ); + } if (topLevelJs && ts.isExpressionStatement(stmtNode)) { if (isFreshExportsReset(lowerer, stmtNode)) return { kind: "block", body: [], loc: locOf(expr) }; @@ -7306,9 +7307,12 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm ) { const loc = locOf(expr); const value = lowerer.lowerExprExpecting(expr.right, F64); + const fn = programReadsExitCode(lowerer) + ? "process.setExitCodeTracked" + : "process.setExitCode"; return { kind: "exprStmt", - expr: { kind: "libCall", fn: "process.setExitCode", args: [value], type: VOID, loc }, + expr: { kind: "libCall", fn, args: [value], type: VOID, loc }, loc, }; } @@ -7501,6 +7505,31 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm const recvIr = lowerer.mapTypeOf(lowerer.typeOf(expr.left.expression)); if (recvIr?.kind === "record") { const shape = lowerer.shapes.get(recvIr.shapeId); + if ( + shape?.indexValue && + shape.fields.length === 0 && + declaredMemberOf(lowerer, expr.left.expression, expr.left.name.text) + ) { + // A declared member of a literal stored as a pure index-signature + // record (dictSpreadLiteralShape): a keyed write of that key. + const loc = locOf(expr); + const obj = lowerer.lowerExpr(expr.left.expression); + if (obj.type.kind === "record") { + const value = lowerer.intoIndexValueSlot( + lowerer.lowerExprExpecting(expr.right, shape.indexValue), + shape.indexValue, + expr.right, + ); + return { + kind: "recordKeySet", + obj, + shapeId: obj.type.shapeId, + key: { kind: "strLit", value: expr.left.name.text, type: STRING, loc }, + value, + loc, + }; + } + } if ( shape?.indexValue && !shape.fields.some( @@ -9175,6 +9204,16 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { ); } if (iterable.type.kind === "object") { + // A [Symbol.iterator] generator method: the loop drives the + // generator it returns. + const generator = classGeneratorIterable(lowerer, iterable); + if (generator) + return lowerForOfGenerator( + lowerer, + stmt, + generator as IrExpr & { type: IrType & { kind: "generator" } }, + labels, + ); // Class iterables ([Symbol.iterator]() + next() — classIteratorOf) // drive the same protocol with ordinary method calls. const cit = lowerer.classIteratorOf(iterable.type); diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 5769e1e0a..e9424aec0 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -999,6 +999,15 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C }; } +/** True when a value of Node's argument type `passed` widens implicitly + * into the declared parameter type `declared` (one of its union arms, or a + * sub-union of it). */ +function widensInto(lowerer: Lowerer, passed: IrType, declared: IrType, loc: SrcLoc): boolean { + if (declared.kind !== "union") return false; + const probe: IrExpr = { kind: "varRef", localId: "%probe", type: passed, loc }; + return typeEquals(lowerer.coerceToExpected(probe, declared).type, declared); +} + /* ── user subclasses (`extends Readable`, the underscore-override form) ── */ /** The wrapper closure binding one overridden underscore method as the @@ -1075,10 +1084,17 @@ function streamMethodWrapper( ); } } else if (!typeEquals(got.type, want)) { + // A declared supertype of what Node passes (`chunk: Buffer | string`) + // is sound: the wrapper takes Node's value and widens it into the + // declared parameter. + if (widensInto(lowerer, want, got.type, loc)) { + paramTypes.push(want); + continue; + } lowerer.noLowering( `a '${methodName}' method whose parameter ${i} is '${lowerer.fmt(got.type)}' where Node passes '${lowerer.fmt(want)}'`, blame, - "annotate the parameter with the Node signature's type (chunks are Buffers here)", + "annotate the parameter with the Node signature's type or a supertype of it (chunks are Buffers here)", ); } paramTypes.push(got.type); @@ -1094,7 +1110,11 @@ function streamMethodWrapper( for (let i = 0; i < paramTypes.length; i++) { const local = lowerer.declareHiddenLocal(`a${i}`, paramTypes[i]!); params.push({ localId: local.id, name: local.name, type: paramTypes[i]! }); - argRefs.push({ kind: "varRef", localId: local.id, type: paramTypes[i]!, loc }); + const arg: IrExpr = { kind: "varRef", localId: local.id, type: paramTypes[i]!, loc }; + const declaredType = declared[i]!.type; + argRefs.push( + typeEquals(arg.type, declaredType) ? arg : lowerer.coerceToExpected(arg, declaredType), + ); } const thisRef: IrExpr = { kind: "varRef", localId: thisLocal.id, type: thisType, loc }; let call: IrExpr; @@ -3654,8 +3674,10 @@ export function lowerStreamMethodCall( : lowerer.lowerExprExpecting(args[0]!, STRING); const fn = kind === "bytes" ? "writable.write" : "writable.writeStr"; if (cbArg !== undefined) { - const cb = lowerer.lowerExprExpecting(cbArg, funcOf([], VOID)); - if (cb.type.kind !== "func" || cb.type.params.length > 0) { + const cb = callbackDeclaresParameters(cbArg) + ? null + : lowerer.lowerExprExpecting(cbArg, funcOf([], VOID)); + if (cb === null || cb.type.kind !== "func" || cb.type.params.length > 0) { lowerer.noLowering( "write completion callbacks taking the error argument", cbArg, @@ -3712,8 +3734,10 @@ export function lowerStreamMethodCall( ); } if (cbArg !== undefined) { - const cb = lowerer.lowerExprExpecting(cbArg, funcOf([], VOID)); - if (cb.type.kind !== "func" || cb.type.params.length > 0) { + const cb = callbackDeclaresParameters(cbArg) + ? null + : lowerer.lowerExprExpecting(cbArg, funcOf([], VOID)); + if (cb === null || cb.type.kind !== "func" || cb.type.params.length > 0) { lowerer.noLowering( "end callbacks with parameters", cbArg, @@ -3978,3 +4002,17 @@ export function lowerStreamProperty( } return null; } + +/** Whether an inline completion callback declares a parameter. Node calls + * write and end callbacks with an error argument (null on success), but the + * native callback receives none, so the parameter would read undefined + * where Node passes null or the error. A function literal with parameters + * would otherwise adapt to the zero-parameter slot silently. */ +function callbackDeclaresParameters(node: ts.Expression): boolean { + let callback = node; + while (ts.isParenthesizedExpression(callback)) callback = callback.expression; + return ( + (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) && + callback.parameters.nodes.length > 0 + ); +} diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 4a8bee1fa..fd2ecfcf0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -117,6 +117,7 @@ import { InternalCompilerError } from "../../errors.js"; import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "./optional-arguments.js"; import { type ProtocolDispatch } from "./class-protocol-dispatch.js"; import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-dispatch.js"; +import { guardClassAttributeWrites } from "./class-attributes.js"; import { ClassProtocols } from "./class-protocols.js"; import { earlyReadVarSymbols } from "./var-hoisting.js"; import { recordTextCodecClass } from "../../ir/ir.js"; @@ -1969,6 +1970,8 @@ export class Lowerer { readonly symbolFieldIdentities = new Map(); readonly httpClientFnBindings = new Map(); streamPropMutatedSyms: Set | null = null; + /** Whether some source reads process.exitCode (processReadsExitCode). */ + readsExitCode: boolean | null = null; requestInitPropMutatedSyms: Set | null = null; emitterEventTable: Map | null = null; emitterComputedPatterns: ComputedEventPattern[] = []; @@ -2571,6 +2574,10 @@ export class Lowerer { * lifted lazily as module-level functions and interned per accessor * declaration: member reads call the getter (lower-exprs). */ readonly cjsAccessorFns = new Map(); + /** Accessor exports' link-time values for ES-module importers, and the + * per-module flag that the copy already ran (cjsAccessorSnapshotStores). */ + readonly cjsAccessorSnapshots = new Map(); + readonly cjsSnapshotReady = new Map(); /** Interned `%iter.drain.` helpers (classIteratorDrainCall): one per * receiver class — the eager drain of a class iterable's protocol into * a fresh element array, behind array/call spreads. */ @@ -2650,6 +2657,10 @@ 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(); + /** Well-known symbol methods (`sym:iterator`) the program may replace + * through a symbol-keyed write: an instance or prototype property, or a + * reflection call. */ + readonly replacedSymbolMethods = new Set(); /** Typed calls through class-backed protocols, by signature; class * dynamic dispatch adds their native arms (class-protocol-dispatch.ts). */ readonly protocolDispatches = new Map(); @@ -6934,6 +6945,7 @@ export class Lowerer { finalizeClassMethodValues(this, functions); finalizePrivateBrands(this, functions); trackClassFieldCreation(this, functions); + guardClassAttributeWrites(this, functions); // Globals typed by a class that never REGISTERED (a JS class whose // collection fenced — Symbol-keyed fields, an unsupported base): the @@ -8645,6 +8657,17 @@ export class Lowerer { * else (including a DIFFERENT union) is left for requireExactShape, which * rejects union mismatches with SC2003. */ coerceToExpected(expr: IrExpr, expected: IrType): IrExpr { + // A comma expression ending in a unit literal (`return log(), null`): + // its effects run first, unchanged, and the unit value takes the + // destination's representation (a bare unit literal has none). + if ( + expr.kind === "seqExpr" && + expr.result.kind === "unitLit" && + !typeEquals(expr.type, expected) + ) { + const result = this.coerceToExpected(expr.result, expected); + return { ...expr, result, type: result.type }; + } // A destination that holds undefined takes an unassigned deferred-init // field's slot as is (Node passes the undefined along). if (expected.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) { @@ -9019,6 +9042,10 @@ export class Lowerer { return this.applyWidthLift(lift, narrowed, expected, expr.loc); } } + if (expr.type.kind === "union" && expected.kind === "record") { + const lifted = unionArmsWidthLift(this, expr, expected); + if (lifted) return lifted; + } if (!typeEquals(expr.type, expected)) { const w = this.widthCoerce(expr, expected); if (w) return w; @@ -9356,6 +9383,7 @@ export class Lowerer { * lowering goes through here (via lowerExprExpecting) or calls this * directly when the expression was already lowered. */ coerceInto(node: ts.Node, expr: IrExpr, expected: IrType): IrExpr { + refuseInferredCallbackDrops(this, node, expr.type, expected); // A union destination can accept the binding's tagged storage directly. // The checker's single-arm flow type can be stale after a callback writes // the binding. Extracting that arm and wrapping it again would discard the @@ -10093,6 +10121,15 @@ export class Lowerer { e = this.returnedPromiseAwait(e); } if (e.kind === "unitLit") return { kind: "return", value: null, loc }; + // `return c ? f() : g()` over void calls: a void conditional has no + // value representation, so its branch selection becomes control flow. + if (e.kind === "ternary" && e.type.kind === "void") { + const branch = (x: IrExpr): IrStmt => + x.kind === "ternary" && x.type.kind === "void" + ? { kind: "if", cond: x.cond, then: [branch(x.then)], else_: [branch(x.else_)], loc } + : { kind: "exprStmt", expr: x, loc }; + return { kind: "block", body: [branch(e), { kind: "return", value: null, loc }], loc }; + } if (e.type.kind === "void") return { kind: "return", value: e, loc }; return { kind: "block", @@ -13092,3 +13129,180 @@ export class Lowerer { return lowerStdlibMethodCall(this, call, access); } } + +/** A union of record layouts flowing into one record slot: a function + * returning differently-shaped object literals (`{ ok: false }` and + * `{ ok: true, size }`, whose union the checker normalizes to arms with + * `size?: undefined`) into its declared result type. Each record arm + * width-lifts into the slot on its own tag; unit arms take the stranded + * TypeError (the checker excluded them). Null when some record arm does + * not lift, or when there is a non-record, non-unit arm. */ +function unionArmsWidthLift( + lowerer: Lowerer, + expr: IrExpr, + expected: IrType & { kind: "record" }, +): IrExpr | null { + if (expr.type.kind !== "union") return null; + const unionId = expr.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms ?? []; + const lifts: (WidthLift | null)[] = []; + let records = 0; + for (const arm of arms) { + if (isUnitType(arm)) { + lifts.push(null); + continue; + } + if (arm.kind !== "record") return null; + const lift = lowerer.widthLiftPlan(arm, expected); + if (!lift) return null; + lifts.push(lift); + records++; + } + if (records < 2) return null; + const loc = expr.loc; + const local = lowerer.declareHiddenLocal("%unionLift", expr.type); + const ref = (): IrExpr => ({ kind: "varRef", localId: local.id, type: expr.type, loc }); + let result: IrExpr | null = null; + for (let tag = arms.length - 1; tag >= 0; tag--) { + const lift = lifts[tag]; + const arm = arms[tag]!; + const branch: IrExpr = lift + ? lowerer.applyWidthLift( + lift, + { kind: "unionNarrow", unionId, tag, value: ref(), type: arm, loc }, + expected, + loc, + ) + : nodeThrowExpr( + 1, + "", + `${arm.kind === "nullT" ? "null" : "undefined"} is not representable in a '${lowerer.fmt(expected)}' slot (a value narrowed or asserted past the type still held it)`, + expected, + loc, + ); + result = + result === null + ? branch + : { + kind: "ternary", + cond: { + kind: "unionIsTag", + unionId, + tag, + negated: false, + value: ref(), + type: BOOL, + loc, + }, + then: branch, + else_: result, + type: expected, + loc, + }; + } + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: expr, loc }], + result: result!, + type: expected, + loc, + }; +} + +/** A callback passed to a generic call without a return annotation, whose + * returns the checker unified into a narrower result type: `trace(name, + * async () => (cond ? items : items.map((i) => ({ ...i, sum }))))` infers + * `T` from the narrower `Item[]`, though at run time the extended objects + * keep `sum`. Converting such a return into the inferred type copies the + * records and drops the members the narrower type lacks, which Node keeps + * and any later wider view (`Promise`) reads back. Refuse + * that conversion by name rather than lose the members silently; an + * annotation on the callback's return type or on the call's type argument + * names a type that keeps them. */ +function refuseInferredCallbackDrops( + lowerer: Lowerer, + node: ts.Node, + actual: IrType, + expected: IrType, +): void { + if (typeEquals(actual, expected)) return; + const structured = (t: IrType): boolean => + t.kind === "record" || t.kind === "array" || t.kind === "promise"; + if (!structured(actual) || !structured(expected)) return; + let site: ts.Node = node; + while (yieldsOperand(site)) site = site.parent!; + let fn: ts.Node | undefined; + if (ts.isReturnStatement(site.parent) && site.parent.expression === site) { + fn = site.parent.parent; + while (fn && !ts.isFunctionLike(fn)) fn = fn.parent; + } else if (ts.isArrowFunction(site.parent) && site.parent.body === site) { + fn = site.parent; + } + if (!fn || !(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) || fn.type) return; + let arg: ts.Node = fn; + while (ts.isParenthesizedExpression(arg.parent)) arg = arg.parent; + const call = arg.parent; + if (!(ts.isCallExpression(call) || ts.isNewExpression(call))) return; + if (!call.arguments?.nodes.includes(arg as ts.Expression)) return; + const dropped = droppedMembers(lowerer, actual, expected, new Set()); + if (dropped.length === 0) return; + lowerer.unsupported( + "SC1090", + node, + `returning '${lowerer.fmt(actual)}' from a callback whose result the checker infers as the narrower '${lowerer.fmt(expected)}' (the conversion would drop ${dropped.map((d) => `'${d}'`).join(", ")}, which the object keeps at run time — annotate the callback's return type or the call's type argument with a type that declares them)`, + ); +} + +/** True when the value of `node`'s parent can be `node`'s own value: + * parentheses, `await`, `satisfies`, a non-null assertion, a conditional's + * branch, either operand of `??`, `||` and `&&`, and the right operand of a + * comma. */ +function yieldsOperand(node: ts.Node): boolean { + const p = node.parent; + if (p === undefined) return false; + if ( + ts.isParenthesizedExpression(p) || + ts.isAwaitExpression(p) || + ts.isSatisfiesExpression(p) || + ts.isNonNullExpression(p) + ) + return true; + if (ts.isConditionalExpression(p)) return p.whenTrue === node || p.whenFalse === node; + if (ts.isBinaryExpression(p)) { + const op = p.operatorToken.kind; + if ( + op === ts.SyntaxKind.QuestionQuestionToken || + op === ts.SyntaxKind.BarBarToken || + op === ts.SyntaxKind.AmpersandAmpersandToken + ) + return true; + return op === ts.SyntaxKind.CommaToken && p.right === node; + } + return false; +} + +/** Member names a structural conversion from `from` into `to` drops: + * declared fields of a source record the target record lacks, through + * array elements and promise payloads. */ +function droppedMembers(lowerer: Lowerer, from: IrType, to: IrType, seen: Set): string[] { + if (typeEquals(from, to)) return []; + if (from.kind === "array" && to.kind === "array") + return droppedMembers(lowerer, from.elem, to.elem, seen); + if (from.kind === "promise" && to.kind === "promise") + return droppedMembers(lowerer, from.inner, to.inner, seen); + if (from.kind !== "record" || to.kind !== "record") return []; + const key = `${from.shapeId}>${to.shapeId}`; + if (seen.has(key)) return []; + seen.add(key); + const source = lowerer.shapes.get(from.shapeId); + const target = lowerer.shapes.get(to.shapeId); + if (!source || !target || source.tuple || target.tuple || target.indexValue) return []; + const out: string[] = []; + for (const f of source.fields) { + if (f.name.startsWith("%")) continue; + const t = target.fields.find((x) => x.name === f.name); + if (!t) out.push(f.name); + else out.push(...droppedMembers(lowerer, f.type, t.type, seen).map((n) => `${f.name}.${n}`)); + } + return out; +} diff --git a/packages/compiler/src/frontend/lowering/strict-mode.ts b/packages/compiler/src/frontend/lowering/strict-mode.ts new file mode 100644 index 000000000..86653c1d3 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/strict-mode.ts @@ -0,0 +1,34 @@ +import * as ts from "../ts7/adapter.js"; +import { isCjsJsFile, isNodeEsmFile } from "../program.js"; + +function hasUseStrictDirective(statements: readonly ts.Statement[]): boolean { + for (const statement of statements) { + if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression)) + return false; + if (statement.expression.text === "use strict") return true; + } + return false; +} + +/** Whether `node` runs in strict mode code: inside a class body, a function + * with a "use strict" directive, a module, or a script that starts with + * "use strict". CommonJS JavaScript is sloppy otherwise. */ +export function isStrictCode(node: ts.Node): boolean { + for (let parent: ts.Node | undefined = node.parent; parent; parent = parent.parent) { + if (ts.isClassDeclaration(parent) || ts.isClassExpression(parent)) return true; + if ( + ts.isFunctionLike(parent) && + parent.body && + ts.isBlock(parent.body) && + hasUseStrictDirective(parent.body.statements.nodes) + ) + return true; + if (ts.isSourceFile(parent)) { + return ( + hasUseStrictDirective(parent.statements.nodes) || + (!isCjsJsFile(parent) && (isNodeEsmFile(parent) || ts.isExternalModule(parent))) + ); + } + } + return false; +} diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 7ae1371c2..4c4d10f0e 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2221,14 +2221,22 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ kind: "stdlib", name: "Date.parse", fns: ["date.parse"], - note: "one date string, using the bounded parser shared with new Date(dateString).getTime()", + note: "one date string, using V8's date parser shared with new Date(dateString)", }, { id: "stdlib.date.constructor", kind: "stdlib", name: "Date constructor", - fns: ["date.newNow", "date.newMs", "date.newString", "date.nativeNew"], - note: "zero arguments, or one milliseconds/date-string argument; values are the read-only TimeClip scalar slice", + fns: [ + "date.newNow", + "date.newMs", + "date.newString", + "date.nativeNew", + "date.newLocal", + "date.nativeNewFields", + "date.nativeNewValue", + ], + note: "zero arguments, one milliseconds/date-string argument, or local-time year/month fields; values are the read-only TimeClip scalar slice", }, { id: "stdlib.date.UTC", @@ -2656,8 +2664,8 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ id: "node-builtin.process.exitCode", kind: "node-builtin", name: "process.exitCode", - fns: ["process.setExitCode"], - note: "numeric writes in statement position set the implicit exit status; process.exit() reads it", + fns: ["process.setExitCode", "process.setExitCodeTracked", "process.hasExitCode"], + note: "numeric writes in statement position set the implicit exit status; reads answer the assigned code or undefined; process.exit() reads it", }, // ── the tls CA store (lowerTlsCaCall / lowerTlsRootCertificates): the // host's trust anchors, read and replaced. Dedicated paths, and @@ -2969,7 +2977,7 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< }, util: { promisify: - "the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions, dns.lookup, dns.lookupService and the fs callback functions with an fs/promises counterpart; call the bound function directly", + "the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions, dns.lookup, dns.lookupService and the fs callback functions with an fs/promises counterpart, plus typed callback-last function values whose callback takes (error?: Error | null, value?); call the bound function directly", }, "stream/consumers": { blob: diff --git a/packages/compiler/src/frontend/lowering/symbol-methods.ts b/packages/compiler/src/frontend/lowering/symbol-methods.ts index f6a8e68e4..b02e95641 100644 --- a/packages/compiler/src/frontend/lowering/symbol-methods.ts +++ b/packages/compiler/src/frontend/lowering/symbol-methods.ts @@ -2,10 +2,11 @@ import * as ts from "../ts7/adapter.js"; import { locOf } from "../program.js"; import type { IrExpr } from "../../ir/ir.js"; import type { Lowerer } from "./lowerer.js"; -import { accessorCall } from "./lower-classes.js"; +import { accessorCall, type ClassInfo } from "./lower-classes.js"; import { classSymbolKeyOf } from "./symbol-fields.js"; -/** A stable symbol method uses the same completed arguments and virtual +/** A stable symbol method (a module-scope symbol, or a well-known symbol + * such as `Symbol.iterator`) uses the same completed arguments and virtual * dispatch as a string-named class method. Runtime-selected keys stay on * the checked-property path. */ export function lowerClassSymbolMethodCall( @@ -15,12 +16,13 @@ export function lowerClassSymbolMethodCall( const access = call.expression; if (!ts.isElementAccessExpression(access) || access.questionDotToken || call.questionDotToken) return null; - const key = classSymbolKeyOf(lowerer, access.argumentExpression); - if (!key) return null; const type = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); if (type?.kind !== "object") return null; const info = lowerer.classes.get(type.className); - const name = info?.symbolMethods?.get(key.identity); + const key = classSymbolKeyOf(lowerer, access.argumentExpression); + const name = key + ? info?.symbolMethods?.get(key.identity) + : wellKnownSymbolMethod(lowerer, info, access.argumentExpression); if (!name) return null; const method = lowerer.findMethodOn(info ?? null, name); if (!method) return null; @@ -29,3 +31,21 @@ export function lowerClassSymbolMethodCall( const args = lowerer.completeArgs(call.arguments.nodes, method.sig.params, loc, call); return accessorCall(lowerer, type.className, name, receiver, args, method.sig.ret, loc); } + +/** The `sym:` method a well-known symbol key such as `Symbol.iterator` + * names, when the class declares it as a method and the program cannot + * replace it at run time (an instance property of that name, a prototype + * write). */ +function wellKnownSymbolMethod( + lowerer: Lowerer, + info: ClassInfo | undefined, + key: ts.Expression, +): string | null { + while (ts.isParenthesizedExpression(key)) key = key.expression; + if (!info || info.def.runtime || !ts.isPropertyAccessExpression(key)) return null; + const member = lowerer.stdlibGlobalMember(key, "Symbol"); + if (member === null) return null; + const name = `sym:${member}`; + if (info.fields.has(name) || lowerer.replacedSymbolMethods.has(name)) return null; + return name; +} diff --git a/packages/compiler/src/frontend/lowering/width-lift.ts b/packages/compiler/src/frontend/lowering/width-lift.ts index d90f9ed35..e3722bc32 100644 --- a/packages/compiler/src/frontend/lowering/width-lift.ts +++ b/packages/compiler/src/frontend/lowering/width-lift.ts @@ -9,6 +9,7 @@ export type WidthLift = | { how: "discriminantWrap" } | { how: "discriminantRoute" } | { how: "retag" } + | { how: "unitRetag" } | { how: "liftWrap"; tag: number; arm: IrType } | { how: "width" } | { how: "unionWidth" } diff --git a/packages/compiler/src/frontend/mock-shapes.ts b/packages/compiler/src/frontend/mock-shapes.ts index 1baf2cd04..50781c272 100644 --- a/packages/compiler/src/frontend/mock-shapes.ts +++ b/packages/compiler/src/frontend/mock-shapes.ts @@ -12,10 +12,11 @@ * object; empty for mock.fn * * A MockFunctionCall is { arguments, error, result, this }: the - * arguments as a tuple of F's parameters, the result as F's return type or - * undefined (a checked-dynamic value for functions returning void or - * undefined, since a record field cannot have a unit type), and error and - * this as checked-dynamic values. */ + * arguments as a tuple of F's parameters (a checked-dynamic array for + * mock.fn()'s `(...args: any[]) => undefined`), the result as F's return + * type or undefined (a checked-dynamic value for functions returning void + * or undefined, since a record field cannot have a unit type), and error + * and this as checked-dynamic values. */ import { DYN, F64, NULL_T, UNDEFINED_T, VOID, typeKey, type IrType } from "../ir/ir.js"; import type { TypeMapperCtx } from "./type-mapper.js"; @@ -34,11 +35,17 @@ function orUndefined(t: IrType, reg: MockShapeRegistries): IrType { } /** The arguments type of a call record: a tuple of the parameters, or the - * rest array itself for a function whose only parameter is a rest array. - * Null for other rest shapes. A function without parameters records the + * rest array itself for a function whose only parameter is a rest array, + * or a checked-dynamic array for a function returning nothing whose only + * parameter is a checked-dynamic rest (mock.fn()'s default, `(...args: + * any[]) => undefined`). Null for other rest shapes. A function without parameters records the * empty tuple, which maps to an array of the unit-only union, as * type-mapper.ts maps `[]`. */ function mockArgumentsType(fn: IrType & { kind: "func" }, reg: MockShapeRegistries): IrType | null { + if (fn.rest && fn.restAbi === undefined) { + const returnsNothing = fn.ret.kind === "void" || fn.ret.kind === "undefinedT"; + return fn.params.length === 0 && returnsNothing ? DYN : null; + } if (fn.rest) { if (fn.params.length !== 1 || fn.params[0]!.kind !== "array") return null; return fn.params[0]!; diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts new file mode 100644 index 000000000..e6dfe9a56 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts @@ -0,0 +1,318 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { basename, join } from "node:path"; +import { afterAll, expect, test } from "vitest"; +import { nodeFrontendServices } from "./services-node.js"; +import { rewriteBundlerCjsExports } from "./npm-static-rewrite.js"; +import type { BundledChunkSplit } from "./npm-static-bundled-chunks.js"; + +const keyedHelper = `var __getOwnPropNames = Object.getOwnPropertyNames; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +`; + +const dirs: string[] = []; +afterAll(() => { + for (const dir of dirs) rmSync(dir, { recursive: true, force: true }); +}); + +function split(source: string, file = "/pkg/dist/index.js") { + return nodeFrontendServices().splitCjsChunks(source, file); +} + +function run(file: string) { + const result = spawnSync(process.execPath, [file], { encoding: "utf8" }); + return { status: result.status, stdout: result.stdout, stderr: result.stderr }; +} + +/** Runs the original bundle and its split modules side by side. */ +function expectSameBehavior(source: string): BundledChunkSplit { + const dir = mkdtempSync(join(tmpdir(), "scriptc-chunks-")); + dirs.push(dir); + const original = join(dir, "original.js"); + writeFileSync(original, source); + const bundle = join(dir, "index.js"); + const result = split(source, bundle); + expect(result).not.toBeNull(); + expect(result).not.toHaveProperty("degrade"); + const answer = result as BundledChunkSplit; + writeFileSync(bundle, answer.text); + for (const chunk of answer.chunks) writeFileSync(chunk.path, chunk.source); + // Respelled calls and blanked declarations keep the line structure. + expect(answer.text.split("\n")).toHaveLength(source.split("\n").length); + const oracle = run(original); + expect(oracle.status).toBe(0); + expect(run(bundle)).toEqual(oracle); + return answer; +} + +test("chunks run once, at their first require, with shared module instances", () => { + const answer = expectSameBehavior(`"use strict"; +${keyedHelper} +var require_b = __commonJS({ + "b.js"(exports2, module2) { + console.log("b init"); + module2.exports = { n: 1 }; + } +}); +var require_a = __commonJS({ + "a.js"(exports2) { + console.log("a init"); + var b = require_b(); + exports2.value = b.n + 1; + exports2.keys = Object.keys({ exports2 }); + } +}); +console.log("start"); +console.log(require_a().value, require_a().value, require_b().n, require_a().keys); +`); + expect(answer.chunks.map((c) => basename(c.path))).toEqual([ + "index.js.require_b.cjs", + "index.js.require_a.cjs", + ]); + expect(answer.text).not.toContain("require_a()"); +}); + +test("the arrow helper form and minified spellings split the same way", () => { + expectSameBehavior( + `var __commonJS=(cb,mod)=>()=>(mod||cb((mod={exports:{}}).exports,mod),mod.exports);` + + `var require_x=__commonJS((exports,module)=>{module.exports={v:"x"}});` + + `console.log(require_x().v,require_x()===require_x());`, + ); +}); + +test("esbuild 0.11's Object.keys spelling is recognized", () => { + expectSameBehavior(`var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[Object.keys(cb)[0]])((mod = {exports: {}}).exports, mod), mod.exports; +}; +var require_y = __commonJS({ + "y.js"(exports) { + exports.y = 3; + } +}); +console.log(require_y().y); +`); +}); + +test("pure helpers a chunk reads travel with it", () => { + const answer = expectSameBehavior(`${keyedHelper}var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +var require_named = __commonJS({ + "named.js"(exports2, module2) { + module2.exports = __name(function () {}, "renamed"); + } +}); +console.log(require_named().name); +`); + const chunk = answer.chunks[0]!.source; + expect(chunk).toContain("var __name ="); + expect(chunk).toContain("var __defProp = Object.defineProperty;"); +}); + +test("a helper travels with a chunk whose nested scopes declare its name", () => { + const answer = expectSameBehavior(`${keyedHelper}function label(n) { + return "label " + n; +} +function wrap(n) { + function inner() { + var label = "inner"; + return label; + } + return label(n) + " " + inner(); +} +var require_e = __commonJS({ + "e.js"(exports2, module2) { + function nested() { + var label = "nested"; + return label; + } + var seen = []; + for (let label of ["loop"]) seen.push(label); + try { + throw "caught"; + } catch (label) { + seen.push(label); + } + { + const label = "block"; + seen.push(label); + } + module2.exports = { outer: label(1), indirect: (0, label)(3), wrapped: wrap(2), nested: nested(), seen }; + } +}); +var e = require_e(); +console.log(e.outer, e.indirect, e.wrapped, e.nested, e.seen.join(",")); +`); + const chunk = answer.chunks[0]!.source; + expect(chunk).toContain("function label(n)"); + expect(chunk).toContain("function wrap(n)"); +}); + +test("a named function expression that only calls itself travels with a chunk", () => { + const answer = expectSameBehavior(`${keyedHelper}var fact = function self(n) { + return n <= 1 ? 1 : n * self(n - 1); +}; +var kind = function self(self) { + return typeof self; +}; +var outer = function self() { + var self = 2; + return self; +}; +var require_g = __commonJS({ + "g.js"(exports2, module2) { + module2.exports = [fact(5), kind(1), outer()]; + } +}); +console.log(require_g().join(",")); +`); + const chunk = answer.chunks[0]!.source; + expect(chunk).toContain("var fact = function self(n)"); +}); + +test("a helper a parameter default reads travels with a chunk whose body declares its name", () => { + const answer = expectSameBehavior(`${keyedHelper}var ten = function () { + return 10; +}; +var add = function (n, base = ten()) { + var ten = 1; + return n + base + ten; +}; +var require_g = __commonJS({ + "g.js"(exports2, module2) { + module2.exports = add(1); + } +}); +console.log(require_g()); +`); + const chunk = answer.chunks[0]!.source; + expect(chunk).toContain("var ten = function ()"); +}); + +test.each([ + ["exports it", `function tally() {}`, `module2.exports = { tally };`], + ["reads its properties", `function tally() {}`, `exports2.calls = tally.calls;`], + ["constructs it", `function tally() {}`, `exports2.made = new tally();`], + ["stores an arrow helper", `var tally = () => 1;`, `exports2.fn = tally;`], + [ + "calls a function that reads its own name as a value", + `var tally = function self() { self.n = (self.n || 0) + 1; return self.n; };`, + `exports2.count = tally();`, + ], + [ + "calls a function whose parameter default reads its own name", + `var tally = function self(box = self) { box.n = (box.n || 0) + 1; return box.n; };`, + `exports2.count = tally();`, + ], + [ + "calls a function whose parameter default reads its own name past a body var of that name", + `var tally = function self(box = self) { var self = 0; box.n = (box.n || 0) + 1; return box.n + self; };`, + `exports2.count = tally();`, + ], +])("a chunk that %s keeps the package refused", (_, declaration, statement) => { + const result = split(`${keyedHelper}${declaration} +var require_f = __commonJS({ + "f.js"(exports2, module2) { + ${statement} + } +}); +require_f(); +`); + expect(result).toEqual({ + degrade: expect.stringContaining( + "uses the bundle's top-level function 'tally' as a value", + ) as string, + }); +}); + +test.each([ + ["reads its factory's receiver", `console.log(this);`], + ["reads its factory's arguments object", `console.log(arguments.length);`], + ["returns from its top level", `if (exports2) return;`], + ["uses its module object beyond module.exports", `console.log(module2.id);`], + ["reads __filename", `console.log(__filename);`], + ["rebinds its module parameters", `function f(module2) { return module2; }`], + ["reads the bundle's top-level binding 'state'", `console.log(state);`], +])("a chunk that %s keeps the package refused", (reason, statement) => { + const result = split(`${keyedHelper}var state = { n: 1 }; +var require_c = __commonJS({ + "c.js"(exports2, module2) { + ${statement} + } +}); +require_c(); +`); + expect(result).toEqual({ degrade: expect.stringContaining(reason) as string }); +}); + +test("a factory used other than as a direct call keeps the package refused", () => { + const result = + split(`${keyedHelper}var require_d = __commonJS({ "d.js"(exports2) { exports2.d = 1; } }); +var load = require_d; +load(); +`); + expect(result).toEqual({ + degrade: expect.stringContaining("used other than as a direct call") as string, + }); +}); + +test("an unrecognized helper body keeps the package refused", () => { + const result = + split(`var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod); +var require_e = __commonJS((exports) => { exports.e = 1; }); +require_e(); +`); + expect(result).toEqual({ degrade: expect.stringContaining("__commonJS helper") as string }); +}); + +test("files without chunks and ES-module bundles are left alone", () => { + expect(split("exports.x = 1;\n")).toBeNull(); + expect( + split(`${keyedHelper}var require_f = __commonJS({ "f.js"(e) { e.f = 1; } }); +export default require_f(); +`), + ).toBeNull(); +}); + +test("an identifier-minified bundle names its lost helper names", () => { + // esbuild --minify output: the helper shapes survive, their names do not. + const result = split( + `var c=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports);` + + `var x=c((t,m)=>{m.exports={v:1}});console.log(x().v);`, + ); + expect(result).toEqual({ degrade: expect.stringContaining("identifier-minified") as string }); +}); + +test("an ES-module bundle of CommonJS chunks names the chunk interop", () => { + const result = rewriteBundlerCjsExports( + `${keyedHelper}var __toESM = (mod) => mod; +var require_g = __commonJS({ "g.js"(exports) { exports.g = 1; } }); +var import_g = __toESM(require_g()); +export const g = import_g.g; +`, + "/pkg/dist/index.js", + ); + expect(result).toEqual({ + degrade: expect.stringContaining("bundled CommonJS modules") as string, + }); +}); + +test("a chunk that only re-exports a builtin module becomes that module", () => { + const answer = expectSameBehavior(`${keyedHelper}var require_stream = __commonJS({ + "lib/stream.js"(exports2, module2) { + module2.exports = require("stream"); + } +}); +var require_user = __commonJS({ + "lib/user.js"(exports2) { + var Stream = require_stream(); + exports2.same = Stream === require("stream"); + } +}); +console.log(typeof require_stream().PassThrough, require_user().same); +`); + expect(answer.chunks.map((c) => basename(c.path))).toEqual(["index.js.require_user.cjs"]); + expect(answer.text).toContain('typeof require("stream").PassThrough'); +}); diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts new file mode 100644 index 000000000..c61f4a439 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts @@ -0,0 +1,907 @@ +/* Bundled CommonJS chunks: esbuild wraps every CommonJS module it bundles + * in a lazily evaluated factory, + * + * var require_x = __commonJS({ "src/x.js"(exports, module) { BODY } }); + * + * and each `require_x()` runs BODY once (later calls answer the cached + * module.exports, a call during BODY answers the partial one). That is + * exactly a CommonJS module that the bundle requires at the call's + * position, so each recognized chunk becomes its own module: a virtual + * sibling file `..cjs` holding BODY, its module + * parameters respelled as the CommonJS globals, and every `require_x()` + * call becomes `require("./..cjs")`. The program then + * compiles chunks with the ordinary CommonJS machinery: evaluation at the + * require's position, the run-once module cache, export tables, and the + * __toESM interop erasure. + * + * Only shapes whose meaning is verified move: the helper must be one of + * the recognized __commonJS spellings, every reference to a factory + * binding must be a direct call, and BODY must not observe the + * difference between a factory and a module (its `this`, `arguments`, + * `__filename`, the module object beyond `module.exports`, a top-level + * return, or a binding of the bundle other than the pure helpers copied + * beside it). Anything else answers a degrade reason. */ +import * as ts from "./ts7/syntax.js"; +import { helperTokens } from "./helper-tokens.js"; +import { canonicalBuiltinModule } from "./builtin-modules.js"; + +export interface BundledChunkSplit { + /** The bundle with chunk declarations blanked and calls respelled. */ + text: string; + /** Virtual sibling modules, in declaration order. */ + chunks: { path: string; source: string }[]; +} + +export interface Edit { + start: number; + end: number; + text: string; +} + +const CHUNK_SHAPE_DEGRADE = + "its shipped JS bundles CommonJS modules through the __commonJS helper in a shape the " + + "recognizer cannot verify"; + +/** The helper a counter-suffixed bundler name spells (`__commonJS2`). */ +export function helperBase(name: string): string { + return /^__[A-Za-z]+\d+$/.test(name) ? name.replace(/\d+$/, "") : name; +} + +export function unparen(e: ts.Expression): ts.Expression { + while (ts.isParenthesizedExpression(e)) e = e.expression; + return e; +} + +/** True for an identifier occurrence that reads or writes a binding (as + * opposed to a property name, a label, or a declaration's own name). + * Shorthand property values count: `{ x }` reads x. */ +export function isReference(id: ts.Identifier): boolean { + const p = id.parent; + if (p === undefined) return false; + if (ts.isPropertyAccessExpression(p)) return p.expression === id; + if (ts.isPropertyAssignment(p)) return p.initializer === id; + if ( + ts.isMethodDeclaration(p) || + ts.isPropertyDeclaration(p) || + ts.isGetAccessorDeclaration(p) || + ts.isSetAccessorDeclaration(p) + ) + return p.name !== id; + if (ts.isBindingElement(p)) return p.initializer === id; + if (ts.isVariableDeclaration(p) || ts.isParameter(p)) return p.initializer === id; + if ( + ts.isFunctionDeclaration(p) || + ts.isFunctionExpression(p) || + ts.isClassDeclaration(p) || + ts.isClassExpression(p) + ) + return p.name !== id; + if (ts.isLabeledStatement(p)) return p.label !== id; + if (ts.isBreakStatement(p) || ts.isContinueStatement(p)) return false; + if (ts.isMetaProperty(p)) return false; + return true; +} + +/** Names a subtree declares anywhere inside it: variables (destructuring + * included), parameters, function and class names, catch bindings. */ +export function declaredNames(root: ts.Node): Set { + const names = new Set(); + // Binding patterns nest BindingElements, which the walk visits too. + ts.walkPreorder(root, (n) => { + if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode) + return "skip"; + if (ts.isVariableDeclaration(n) || ts.isParameter(n) || ts.isBindingElement(n)) { + const name = n.name; + if (name !== undefined && ts.isIdentifier(name)) names.add(name.text); + } else if ( + (ts.isFunctionDeclaration(n) || + ts.isFunctionExpression(n) || + ts.isClassDeclaration(n) || + ts.isClassExpression(n)) && + n.name !== undefined + ) + names.add(n.name.text); + return undefined; + }); + return names; +} + +/** Adds the identifiers a binding name (an identifier or a destructuring + * pattern) declares. */ +function addBindingNames(name: ts.BindingName, out: Set): void { + if (ts.isIdentifier(name)) { + out.add(name.text); + return; + } + for (const element of name.elements.nodes) { + if (!ts.isOmittedExpression(element) && element.name !== undefined) + addBindingNames(element.name, out); + } +} + +function isFunctionScope(n: ts.Node): n is ts.SignatureDeclaration { + return ( + ts.isFunctionDeclaration(n) || + ts.isFunctionExpression(n) || + ts.isArrowFunction(n) || + ts.isMethodDeclaration(n) || + ts.isGetAccessorDeclaration(n) || + ts.isSetAccessorDeclaration(n) || + ts.isConstructorDeclaration(n) + ); +} + +/** The `var` names a function body declares, without entering nested + * functions. Function declarations in nested blocks count as block-scoped + * here; treating one as free only adds an unused helper copy. */ +function varScopedNames(body: ts.Node): Set { + const out = new Set(); + ts.walkPreorder(body, (n) => { + if (n !== body && (isFunctionScope(n) || ts.isClassDeclaration(n) || ts.isClassExpression(n))) + return "skip"; + if (ts.isVariableDeclarationList(n) && (n.flags & ts.NodeFlags.BlockScoped) === 0) { + for (const d of n.declarations.nodes) addBindingNames(d.name, out); + } + return undefined; + }); + return out; +} + +/** The names a function binds for references in its parameter list + * (default values, computed keys of destructuring patterns): its + * parameters and a function expression's own name. Parameter initializers + * run in their own scope, so the body's `var`s are not visible there. */ +function parameterScopeNames(scope: ts.SignatureDeclaration): Set { + const out = new Set(); + for (const param of scope.parameters.nodes) addBindingNames(param.name, out); + if (ts.isFunctionExpression(scope) && scope.name !== undefined) out.add(scope.name.text); + return out; +} + +const NO_NAMES: ReadonlySet = new Set(); + +/** True when `node` lies inside `ancestor` (or is it). */ +function within(node: ts.Node, ancestor: ts.Node): boolean { + for (let n: ts.Node | undefined = node; n !== undefined; n = n.parent) + if (n === ancestor) return true; + return false; +} + +/** The names one scope node binds for the references inside it. */ +function scopeNames(scope: ts.Node): Set { + const out = new Set(); + if (isFunctionScope(scope)) { + for (const param of scope.parameters.nodes) addBindingNames(param.name, out); + if (ts.isFunctionExpression(scope) && scope.name !== undefined) out.add(scope.name.text); + if (scope.body !== undefined) for (const name of varScopedNames(scope.body)) out.add(name); + } else if (ts.isBlock(scope) || ts.isSourceFile(scope) || ts.isCaseBlock(scope)) { + const statements = ts.isCaseBlock(scope) + ? scope.clauses.nodes.flatMap((clause) => clause.statements.nodes) + : scope.statements.nodes; + for (const stmt of statements) { + if ((ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) && stmt.name) + out.add(stmt.name.text); + else if ( + ts.isVariableStatement(stmt) && + (stmt.declarationList.flags & ts.NodeFlags.BlockScoped) !== 0 + ) { + for (const d of stmt.declarationList.declarations.nodes) addBindingNames(d.name, out); + } + } + } else if (ts.isCatchClause(scope) && scope.variableDeclaration !== undefined) { + addBindingNames(scope.variableDeclaration.name, out); + } else if ( + (ts.isForStatement(scope) || ts.isForInStatement(scope) || ts.isForOfStatement(scope)) && + scope.initializer !== undefined && + ts.isVariableDeclarationList(scope.initializer) && + (scope.initializer.flags & ts.NodeFlags.BlockScoped) !== 0 + ) { + for (const d of scope.initializer.declarations.nodes) addBindingNames(d.name, out); + } else if ( + (ts.isClassDeclaration(scope) || ts.isClassExpression(scope)) && + scope.name !== undefined + ) { + out.add(scope.name.text); + } + return out; +} + +/** Resolves references against one bundle's scopes, keeping each scope's + * names for the bundle's split. */ +class BundleScopes { + private readonly names = new Map>(); + private readonly parameterNames = new Map>(); + + private of(scope: ts.Node): Set { + let names = this.names.get(scope); + if (names === undefined) { + names = scopeNames(scope); + this.names.set(scope, names); + } + return names; + } + + /** The names `scope` binds for a reference reached through its child + * `from`: a function's parameter list sees its parameters and own name + * but not its body's `var`s, and a computed method name sees none of + * them. */ + private visible(scope: ts.Node, from: ts.Node): ReadonlySet { + if (!isFunctionScope(scope) || from === scope.body) return this.of(scope); + if (from === scope.name) return NO_NAMES; + let names = this.parameterNames.get(scope); + if (names === undefined) { + names = parameterScopeNames(scope); + this.parameterNames.set(scope, names); + } + return names; + } + + /** True when a scope between the reference and `root` (inclusive) + * binds its name. */ + boundWithin(id: ts.Identifier, root: ts.Node): boolean { + for (let from: ts.Node = id, n = id.parent; n !== undefined; from = n, n = n.parent) { + if (this.visible(n, from).has(id.text)) return true; + if (n === root) return false; + } + return false; + } + + /** True when a scope inside `root` (exclusive) binds the reference's + * name. */ + boundInside(id: ts.Identifier, root: ts.Node): boolean { + for ( + let from: ts.Node = id, n = id.parent; + n !== undefined && n !== root; + from = n, n = n.parent + ) { + if (this.visible(n, from).has(id.text)) return true; + } + return false; + } + + /** True when the reference resolves to a top-level binding of `sf`. */ + topLevel(id: ts.Identifier, sf: ts.SourceFile): boolean { + for ( + let from: ts.Node = id, n = id.parent; + n !== undefined && n !== sf; + from = n, n = n.parent + ) { + if (this.visible(n, from).has(id.text)) return false; + } + return true; + } +} + +/** Every binding reference inside a subtree. */ +export function references(root: ts.Node): ts.Identifier[] { + const out: ts.Identifier[] = []; + ts.walkPreorder(root, (n) => { + if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode) + return "skip"; + if (ts.isIdentifier(n) && isReference(n)) out.push(n); + return undefined; + }); + return out; +} + +/** True when the reference is written (assignment target, update operand, + * loop target, delete operand). */ +export function isWritten(id: ts.Identifier): boolean { + let child: ts.Node = id; + for (let p = child.parent; p !== undefined; child = p, p = p.parent) { + if (ts.isBinaryExpression(p)) { + return ( + p.left === child && + p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + p.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ); + } + if (ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) { + return ( + p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken + ); + } + if (ts.isForInStatement(p) || ts.isForOfStatement(p)) return p.initializer === child; + if (ts.isDeleteExpression(p)) return true; + if ( + !( + ts.isParenthesizedExpression(p) || + ts.isArrayLiteralExpression(p) || + ts.isObjectLiteralExpression(p) || + ts.isShorthandPropertyAssignment(p) || + ts.isPropertyAssignment(p) || + ts.isSpreadElement(p) + ) + ) + return false; + } + return false; +} + +/** The recognized __commonJS helper forms, as token templates over the + * helper's own parameter names (CB, MOD) and the own-key reader (KEYS: + * esbuild's `__getOwnPropNames` alias, or `Object.keys` in esbuild 0.11): + * keyed: `(CB, MOD) => function [name]() { return ; }` — the + * factory is the only method of an object literal; + * arrow: `(CB, MOD) => () => ()` — the factory itself. */ +const KEYED_RETURN = + "MOD || (0, CB[KEYS(CB)[0]])((MOD = { exports: {} }).exports, MOD), MOD.exports"; +const ARROW_BODY = "(MOD || CB((MOD = { exports: {} }).exports, MOD), MOD.exports)"; + +function template(text: string, cb: string, mod: string, keys: string): string | null { + return helperTokens( + text + .replace(/\bCB\b/g, cb) + .replace(/\bMOD\b/g, mod) + .replace(/\bKEYS\b/g, keys), + ); +} + +type HelperForm = "keyed" | "arrow"; + +/** The helper's form, or null when its initializer is not recognized. + * `ownNames` answers whether an identifier names the verified + * Object.getOwnPropertyNames alias. */ +function helperForm( + init: ts.Expression, + sf: ts.SourceFile, + ownNames: (name: string) => boolean, +): HelperForm | null { + const fn = unparen(init); + if (!ts.isArrowFunction(fn) || fn.parameters.nodes.length !== 2) return null; + const params = fn.parameters.nodes.map((p) => + ts.isIdentifier(p.name) && p.initializer === undefined && p.dotDotDotToken === undefined + ? p.name.text + : null, + ); + const [cb, mod] = params; + if (cb === null || mod === null || cb === undefined || mod === undefined || cb === mod) + return null; + if (ts.isBlock(fn.body)) return null; + const body = unparen(fn.body); + if (ts.isFunctionExpression(body)) { + if (body.parameters.nodes.length !== 0 || body.asteriskToken || body.modifiers?.nodes.length) + return null; + const stmts = body.body.statements.nodes; + const ret = stmts[0]; + if (stmts.length !== 1 || ret === undefined || !ts.isReturnStatement(ret) || !ret.expression) + return null; + const actual = helperTokens(ret.expression.getText(sf)); + if (actual === null) return null; + // The own-key reader: esbuild's alias or the 0.11 spelling. + const keysRef = /\[\s*([A-Za-z_$][\w$]*(?:\.keys)?)\s*\(/.exec(ret.expression.getText(sf)); + const keys = keysRef?.[1]; + if (keys === undefined || (keys !== "Object.keys" && !ownNames(keys))) return null; + return actual === template(KEYED_RETURN, cb, mod, keys) ? "keyed" : null; + } + if (ts.isArrowFunction(body) && body.parameters.nodes.length === 0 && !ts.isBlock(body.body)) { + const actual = helperTokens(body.body.getText(sf)); + return actual !== null && actual === template(ARROW_BODY, cb, mod, "") ? "arrow" : null; + } + return null; +} + +/** A top-level declaration the virtual chunk can copy verbatim: a pure + * helper whose evaluation runs nothing (a function, or a member read of + * Object such as `Object.create` / `Object.prototype.hasOwnProperty`). */ +function copyableInitializer(init: ts.Expression | undefined): boolean { + if (init === undefined) return false; + const e = unparen(init); + if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) return true; + let walk: ts.Expression = e; + let depth = 0; + while (ts.isPropertyAccessExpression(walk) && ts.isIdentifier(walk.name)) { + walk = walk.expression; + depth++; + } + return depth > 0 && ts.isIdentifier(walk) && walk.text === "Object"; +} + +/** A top-level declaration whose value is a function. */ +function isFunctionValued(node: ts.VariableDeclaration | ts.FunctionDeclaration): boolean { + if (ts.isFunctionDeclaration(node)) return true; + const init = node.initializer && unparen(node.initializer); + return init !== undefined && (ts.isArrowFunction(init) || ts.isFunctionExpression(init)); +} + +/** True when a named function expression initializer (`var f = function + * self() {}`) uses its own name only as a callee: `self` is the copied + * function, so it is as observable as the top-level binding. */ +function selfCalledOnly( + node: ts.VariableDeclaration | ts.FunctionDeclaration, + scopes: BundleScopes, +): boolean { + if (!ts.isVariableDeclaration(node) || node.initializer === undefined) return true; + const fn = unparen(node.initializer); + if (!ts.isFunctionExpression(fn) || fn.name === undefined || fn.body === undefined) return true; + const self = fn.name.text; + // A parameter of the same name hides the function's own name everywhere + // in it; a `var` of the same name hides it only in the body, since the + // parameter initializers do not see the body's `var`s. + if (fn.parameters.nodes.some((p) => bindsName(p.name, self))) return true; + const bodyVar = varScopedNames(fn.body).has(self); + const body = fn.body; + return references(fn).every( + (r) => + r.text !== self || scopes.boundInside(r, fn) || (bodyVar && within(r, body)) || isCallee(r), + ); +} + +/** True when a binding name (an identifier or a destructuring pattern) + * declares `name`. */ +function bindsName(binding: ts.BindingName, name: string): boolean { + const names = new Set(); + addBindingNames(binding, names); + return names.has(name); +} + +/** True when a reference is the function a call invokes: `f(...)`, or the + * `(0, f)(...)` form that drops the receiver. */ +function isCallee(id: ts.Identifier): boolean { + let callee: ts.Node = id; + const p = id.parent; + if ( + p !== undefined && + ts.isBinaryExpression(p) && + p.operatorToken.kind === ts.SyntaxKind.CommaToken && + p.right === id && + p.parent !== undefined && + ts.isParenthesizedExpression(p.parent) + ) + callee = p.parent; + while (callee.parent !== undefined && ts.isParenthesizedExpression(callee.parent)) + callee = callee.parent; + const call = callee.parent; + return call !== undefined && ts.isCallExpression(call) && call.expression === callee; +} + +/** One recognized chunk declaration. */ +interface Chunk { + name: string; + stmt: ts.VariableStatement; + params: string[]; + body: ts.Block; + path: string; + spec: string; +} + +interface TopLevel { + stmt: ts.Statement; + /** The declarator (or function declaration) itself. */ + node: ts.VariableDeclaration | ts.FunctionDeclaration; +} + +/** True when the file opens with a "use strict" directive prologue. */ +export function strictPrologue(stmts: readonly ts.Statement[]): boolean { + for (const s of stmts) { + if (!ts.isExpressionStatement(s) || !ts.isStringLiteral(s.expression)) return false; + if (s.expression.text === "use strict") return true; + } + return false; +} + +/** A cheap text probe: the helper's name, or its minified body's + * `(m={exports:{}}).exports,m` fragment. */ +export function mayBundleChunks(source: string): boolean { + return ( + source.includes("__commonJS") || + /\{\s*exports\s*:\s*\{\s*\}\s*\}\s*\)\s*\.exports\s*,/.test(source) + ); +} + +/** Splits a CommonJS bundle's __commonJS chunks into virtual modules (see + * the header). Null: no chunks (or not a CommonJS bundle) — serve the file + * unchanged. */ +export function splitBundledChunks( + sf: ts.SourceFile, + filePath: string, +): BundledChunkSplit | { degrade: string } | null { + const source = sf.text; + if (!mayBundleChunks(source)) return null; + // ES-module bundles have no require: their chunks keep the existing + // treatment (the Bun factory recognizer, or the ESM interop refusal). + if (!filePath.endsWith(".cjs") && ts.isExternalModule(sf)) return null; + + const top = new Map(); + const duplicate = new Set(); + for (const stmt of sf.statements.nodes) { + const add = (name: string, node: TopLevel["node"]): void => { + if (top.has(name)) duplicate.add(name); + else top.set(name, { stmt, node }); + }; + if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined) add(stmt.name.text, stmt); + else if (ts.isVariableStatement(stmt)) { + for (const d of stmt.declarationList.declarations.nodes) { + if (ts.isIdentifier(d.name)) add(d.name.text, d); + } + } + } + const helperNames = [...top.keys()].filter((n) => helperBase(n) === "__commonJS"); + + // Every name a recognizer relies on is written nowhere and declared + // once at the top level; `Object` itself is never rebound. + const allRefs = references(sf); + const written = new Set(); + for (const r of allRefs) if (isWritten(r)) written.add(r.text); + const declaredAnywhere = declaredNames(sf); + if (declaredAnywhere.has("Object") || written.has("Object")) + return { degrade: CHUNK_SHAPE_DEGRADE }; + const stable = (name: string): boolean => !duplicate.has(name) && !written.has(name); + // Names whose every reference in the bundle is a callee: `f(...)` or the + // `(0, f)(...)` form. + const scopes = new BundleScopes(); + const valueUsed = new Set(); + for (const r of allRefs) if (!isCallee(r) && scopes.topLevel(r, sf)) valueUsed.add(r.text); + const calledOnly = (name: string): boolean => !valueUsed.has(name); + const ownNames = (name: string): boolean => { + const t = top.get(name); + if (t === undefined || !stable(name) || !ts.isVariableDeclaration(t.node)) return false; + const init = t.node.initializer; + return ( + init !== undefined && + helperTokens(init.getText(sf)) === helperTokens("Object.getOwnPropertyNames") + ); + }; + if (helperNames.length === 0) { + // The helper's shape under another name: an identifier-minified bundle, + // whose other interop helpers lost their names as well. + for (const [name, t] of top) { + const init = ts.isVariableDeclaration(t.node) ? t.node.initializer : undefined; + if (init !== undefined && stable(name) && helperForm(init, sf, ownNames) !== null) + return { + degrade: + "its shipped JS is an identifier-minified bundle whose bundler-interop helpers " + + "(__commonJS and the rest) have lost the names the recognizers verify", + }; + } + return null; + } + const forms = new Map(); + for (const name of helperNames) { + const t = top.get(name)!; + const init = ts.isVariableDeclaration(t.node) ? t.node.initializer : undefined; + const form = init !== undefined && stable(name) ? helperForm(init, sf, ownNames) : null; + if (form === null) return { degrade: CHUNK_SHAPE_DEGRADE }; + forms.set(name, form); + } + + // The chunk declarations: `var NAME = HELPER(factory);`, one declarator. + const chunks = new Map(); + const base = filePath.replace(/^.*[\\/]/, ""); + for (const [name, t] of top) { + if (!ts.isVariableDeclaration(t.node) || t.node.initializer === undefined) continue; + const call = unparen(t.node.initializer); + if (!ts.isCallExpression(call) || !ts.isIdentifier(call.expression)) continue; + const form = forms.get(call.expression.text); + if (form === undefined) continue; + const stmt = t.stmt; + if ( + !ts.isVariableStatement(stmt) || + stmt.declarationList.declarations.nodes.length !== 1 || + call.arguments.nodes.length !== 1 || + !stable(name) + ) + return { degrade: CHUNK_SHAPE_DEGRADE }; + let factory: ts.Node = call.arguments.nodes[0]!; + if (form === "keyed") { + if (!ts.isObjectLiteralExpression(factory) || factory.properties.nodes.length !== 1) + return { degrade: CHUNK_SHAPE_DEGRADE }; + factory = factory.properties.nodes[0]!; + if (!ts.isMethodDeclaration(factory) || ts.isComputedPropertyName(factory.name)) + return { degrade: CHUNK_SHAPE_DEGRADE }; + } else { + factory = unparen(factory as ts.Expression); + if (!ts.isArrowFunction(factory) && !ts.isFunctionExpression(factory)) + return { degrade: CHUNK_SHAPE_DEGRADE }; + } + const fn = factory as ts.MethodDeclaration | ts.ArrowFunction | ts.FunctionExpression; + if ( + fn.body === undefined || + !ts.isBlock(fn.body) || + fn.asteriskToken !== undefined || + (fn.modifiers?.nodes.length ?? 0) > 0 || + fn.parameters.nodes.length > 2 + ) + return { degrade: CHUNK_SHAPE_DEGRADE }; + const params: string[] = []; + for (const p of fn.parameters.nodes) { + if (!ts.isIdentifier(p.name) || p.initializer !== undefined || p.dotDotDotToken) + return { degrade: CHUNK_SHAPE_DEGRADE }; + params.push(p.name.text); + } + const file = `${base}.${name}.cjs`; + chunks.set(name, { + name, + stmt, + params, + body: fn.body, + path: filePath.slice(0, filePath.length - base.length) + file, + spec: `./${file}`, + }); + } + if (chunks.size === 0) return null; + + // Every factory reference is a direct zero-argument call. Calls inside a + // chunk body belong to that chunk's text, the rest to the bundle's. + const callEdits: Edit[] = []; + for (const r of allRefs) { + const chunk = chunks.get(r.text); + if (chunk === undefined) continue; + const call = r.parent; + if ( + call === undefined || + !ts.isCallExpression(call) || + call.expression !== r || + call.arguments.nodes.length !== 0 || + call.questionDotToken !== undefined + ) + return { + degrade: `its bundled CommonJS module factory '${r.text}' is used other than as a direct call`, + }; + const forwarded = forwarderSpec(chunk); + callEdits.push({ + start: call.getStart(sf), + end: call.getEnd(), + text: `require(${JSON.stringify(forwarded ?? chunk.spec)})`, + }); + } + for (const name of chunks.keys()) { + if (declaredAnywhere.has(name) && countDeclarations(sf, name) > 1) + return { degrade: CHUNK_SHAPE_DEGRADE }; + } + + const bundleStrict = strictPrologue(sf.statements.nodes); + const out: BundledChunkSplit["chunks"] = []; + const bundleEdits: Edit[] = [...callEdits]; + for (const chunk of chunks.values()) { + if (forwarderSpec(chunk) !== null) { + // A chunk that only re-exports a builtin module is that module. + bundleEdits.push({ start: chunk.stmt.getStart(sf), end: chunk.stmt.getEnd(), text: "" }); + continue; + } + const result = chunkSource( + sf, + chunk, + chunks, + top, + stable, + calledOnly, + scopes, + callEdits, + bundleStrict, + ); + if (typeof result !== "string") return result; + out.push({ path: chunk.path, source: result }); + bundleEdits.push({ start: chunk.stmt.getStart(sf), end: chunk.stmt.getEnd(), text: "" }); + } + return { text: applyEdits(source, 0, source.length, bundleEdits), chunks: out }; +} + +export function countDeclarations(sf: ts.SourceFile, name: string): number { + let n = 0; + ts.walkPreorder(sf, (node) => { + if ( + (ts.isVariableDeclaration(node) || + ts.isParameter(node) || + ts.isBindingElement(node) || + ts.isFunctionDeclaration(node) || + ts.isFunctionExpression(node) || + ts.isClassDeclaration(node) || + ts.isClassExpression(node)) && + node.name !== undefined && + ts.isIdentifier(node.name) && + node.name.text === name + ) + n++; + return undefined; + }); + return n; +} + +/** Applies non-overlapping edits inside [from, to): replaced spans keep + * their newlines (blanked text stays line-aligned); edits nested in a + * blanked span vanish with it. */ +export function applyEdits(source: string, from: number, to: number, edits: Edit[]): string { + const inside = edits + .filter((e) => e.start >= from && e.end <= to) + .sort((a, b) => a.start - b.start || b.end - a.end); + let text = ""; + let at = from; + for (const e of inside) { + if (e.start < at) continue; // nested in an earlier edit + text += source.slice(at, e.start); + const replaced = source.slice(e.start, e.end); + // A respelled span keeps its line breaks after the replacement. + text += + e.text === "" ? replaced.replace(/[^\r\n]/g, " ") : e.text + replaced.replace(/[^\n]/g, ""); + at = e.end; + } + return text + source.slice(at, to); +} + +/** The builtin module a chunk forwards, or null: a body that is exactly + * `module.exports = require("")` (readable-stream's + * internal/streams/stream.js). Requiring the chunk is then requiring the + * builtin itself (same object, and loading a builtin runs no code). */ +function forwarderSpec(chunk: Chunk): string | null { + const moduleParam = chunk.params[1]; + if (moduleParam === undefined) return null; + const stmts = chunk.body.statements.nodes.filter( + (st) => !(ts.isExpressionStatement(st) && ts.isStringLiteral(st.expression)), + ); + const only = stmts[0]; + if (stmts.length !== 1 || only === undefined || !ts.isExpressionStatement(only)) return null; + const e = unparen(only.expression); + if ( + !ts.isBinaryExpression(e) || + e.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !ts.isPropertyAccessExpression(e.left) || + !ts.isIdentifier(e.left.expression) || + e.left.expression.text !== moduleParam || + e.left.name.text !== "exports" + ) + return null; + const call = unparen(e.right); + if ( + !ts.isCallExpression(call) || + !ts.isIdentifier(call.expression) || + call.expression.text !== "require" || + call.arguments.nodes.length !== 1 || + !ts.isStringLiteral(call.arguments.nodes[0]!) + ) + return null; + const spec = (call.arguments.nodes[0] as ts.StringLiteral).text; + return canonicalBuiltinModule(spec) !== null ? spec : null; +} + +/** How code in a factory body (a chunk or a lazy ES-module initializer) + * observes the factory's own frame, or null: its receiver, its arguments + * object, `super`, a meta property, or (when `returns`) a top-level + * return have no module-scope equivalent. */ +export function factoryFrameUse(body: ts.Block, returns: boolean): string | null { + let frameUse: string | null = null; + /** The innermost frame boundary between a node and the factory body. */ + const frameOf = (n: ts.Node): "factory" | "arrow" | "function" => { + let arrow = false; + for (let p = n.parent; p !== undefined && p !== body; p = p.parent) { + if ( + ts.isFunctionDeclaration(p) || + ts.isFunctionExpression(p) || + ts.isMethodDeclaration(p) || + ts.isGetAccessorDeclaration(p) || + ts.isSetAccessorDeclaration(p) || + ts.isConstructorDeclaration(p) || + ts.isClassDeclaration(p) || + ts.isClassExpression(p) + ) + return "function"; + if (ts.isArrowFunction(p)) arrow = true; + } + return arrow ? "arrow" : "factory"; + }; + ts.walkPreorder(body, (n) => { + if (frameUse !== null) return "stop"; + if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode) + return "skip"; + let use: string | null = null; + if (n.kind === ts.SyntaxKind.ThisKeyword) use = "reads its factory's receiver (`this`)"; + else if (n.kind === ts.SyntaxKind.SuperKeyword) use = "reads `super` at its top level"; + else if (ts.isIdentifier(n) && n.text === "arguments" && isReference(n)) + use = "reads its factory's arguments object"; + else if (ts.isMetaProperty(n)) use = "reads a meta property at its top level"; + else if (ts.isReturnStatement(n)) { + if (returns && frameOf(n) === "factory") frameUse = "returns from its top level"; + return undefined; + } + if (use !== null && frameOf(n) !== "function") frameUse = use; + return undefined; + }); + return frameUse; +} + +/** The virtual module text for one chunk, or a degrade reason. */ +function chunkSource( + sf: ts.SourceFile, + chunk: Chunk, + chunks: ReadonlyMap, + top: ReadonlyMap, + stable: (name: string) => boolean, + calledOnly: (name: string) => boolean, + scopes: BundleScopes, + callEdits: readonly Edit[], + bundleStrict: boolean, +): string | { degrade: string } { + const why = (reason: string): { degrade: string } => ({ + degrade: `its bundled CommonJS module '${chunk.name}' ${reason}`, + }); + const [exportsParam, moduleParam] = chunk.params; + const body = chunk.body; + const local = declaredNames(body); + for (const p of chunk.params) { + if ( + local.has(p) || + (p !== "exports" && local.has("exports")) || + (p !== "module" && local.has("module")) + ) + return why( + "rebinds its module parameters inside its body (an identifier-minified bundle the recognizer cannot respell)", + ); + } + + const frameUse = factoryFrameUse(body, true); + if (frameUse !== null) return why(frameUse); + + const edits: Edit[] = callEdits.filter((e) => e.start >= body.pos && e.end <= body.end); + const copied = new Set(); + const copyQueue: string[] = []; + for (const r of references(body)) { + const name = r.text; + if (chunks.has(name)) continue; // a call edit already respells it + if (name === exportsParam || name === moduleParam) { + if (name === moduleParam) { + const p = r.parent; + if (!ts.isPropertyAccessExpression(p) || p.expression !== r || p.name.text !== "exports") + return why("uses its module object beyond module.exports"); + } + const global = name === exportsParam ? "exports" : "module"; + if (name !== global) { + const p = r.parent; + edits.push( + ts.isShorthandPropertyAssignment(p) + ? { start: r.getStart(sf), end: r.getEnd(), text: `${name}: ${global}` } + : { start: r.getStart(sf), end: r.getEnd(), text: global }, + ); + } + continue; + } + // The factory function itself binds the parameters and `var`s. + if (scopes.boundWithin(r, body.parent ?? body)) continue; + if (name === "exports" || name === "module") + return why(`reads the bundle's own '${name}' binding`); + if (name === "__filename") return why("reads __filename, which names the bundle file"); + if (top.has(name)) { + if (!copied.has(name)) { + copied.add(name); + copyQueue.push(name); + } + } + } + + // The pure helpers BODY reads, closed over their own top-level reads. + const header: string[] = []; + for (let i = 0; i < copyQueue.length; i++) { + const name = copyQueue[i]!; + const t = top.get(name)!; + const node = t.node; + const pure = + stable(name) && + (ts.isFunctionDeclaration(node) || + (ts.isVariableDeclaration(node) && copyableInitializer(node.initializer))); + if (!pure || chunks.has(name)) + return why(`reads the bundle's top-level binding '${name}', which it cannot carry along`); + // Each chunk gets its own copy, so a function's identity and its + // properties must stay unobservable: the bundle only ever calls it. + if (isFunctionValued(node) && (!calledOnly(name) || !selfCalledOnly(node, scopes))) + return why( + `uses the bundle's top-level function '${name}' as a value, which its chunks cannot share`, + ); + for (const r of references(node)) { + if (scopes.boundWithin(r, node) || !top.has(r.text) || copied.has(r.text)) continue; + copied.add(r.text); + copyQueue.push(r.text); + } + } + // Declaration order of the bundle: a helper reads only earlier ones. + const ordered = [...copied].sort( + (a, b) => top.get(a)!.node.getStart(sf) - top.get(b)!.node.getStart(sf), + ); + for (const name of ordered) { + const node = top.get(name)!.node; + header.push(ts.isFunctionDeclaration(node) ? node.getText(sf) : `var ${node.getText(sf)};`); + } + + const bodyStrict = strictPrologue(body.statements.nodes); + const text = applyEdits(sf.text, body.getStart(sf) + 1, body.getEnd() - 1, edits); + const prologue = bundleStrict || bodyStrict ? '"use strict";\n' : ""; + return prologue + (header.length > 0 ? header.join("\n") + "\n" : "") + text + "\n"; +} diff --git a/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts b/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts new file mode 100644 index 000000000..381538087 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts @@ -0,0 +1,230 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, expect, test } from "vitest"; +import { nodeFrontendServices } from "./services-node.js"; + +const helpers = `var __getOwnPropNames = Object.getOwnPropertyNames; +var __esm = (fn, res) => function __init() { + return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; +}; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __create = Object.create; +var __getProtoOf = Object.getPrototypeOf; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +`; + +const dirs: string[] = []; +afterAll(() => { + for (const dir of dirs) rmSync(dir, { recursive: true, force: true }); +}); + +function lower(source: string, file = "/pkg/dist/index.js") { + return nodeFrontendServices().lowerEsmInits(source, file); +} + +function run(file: string) { + const result = spawnSync(process.execPath, [file], { encoding: "utf8" }); + return { status: result.status, stdout: result.stdout, stderr: result.stderr }; +} + +function expectSameBehavior(source: string): string { + const dir = mkdtempSync(join(tmpdir(), "scriptc-esm-inits-")); + dirs.push(dir); + const original = join(dir, "original.js"); + const lowered = join(dir, "index.js"); + writeFileSync(original, source); + const result = lower(source, lowered); + expect(typeof result).toBe("string"); + writeFileSync(lowered, result as string); + const oracle = run(original); + expect(oracle.status).toBe(0); + expect(run(lowered)).toEqual(oracle); + return result as string; +} + +const lazyModule = ` +function helper(x) { COUNT++; return "[" + x + "]"; } +var COUNT; +var init_helper = __esm({ + "src/helper.ts"() { + console.log("helper init"); + COUNT = 0; + } +}); +var lazy_exports = {}; +__export(lazy_exports, { + VALUE: () => VALUE, + value: () => value +}); +function value() { + return VALUE + ":" + COUNT + ":" + import_os.default.EOL.length; +} +var path, import_os, VALUE; +var init_lazy = __esm({ + "src/lazy.ts"() { + path = __toESM(require("path")); + import_os = __toESM(require("os")); + init_helper(); + console.log("lazy init"); + VALUE = helper(path.basename("/a/b.txt")); + } +}); +`; + +test("initializers run once, at their first call, and namespaces read live bindings", () => { + const lowered = expectSameBehavior(`"use strict"; +${helpers}${lazyModule} +console.log("start"); +function load() { + const lazy = (init_lazy(), __toCommonJS(lazy_exports)); + return lazy.value() + " " + lazy.VALUE; +} +console.log(load()); +COUNT = 5; +console.log(load(), (init_lazy(), __toCommonJS(lazy_exports)).value()); +`); + expect(lowered).not.toContain("__esm({"); + expect(lowered).not.toContain("import_os"); + expect(lowered).toContain('(require("os")).EOL'); +}); + +test("calls of exported functions that never read their own `this` still lower", () => { + const lowered = expectSameBehavior(`${helpers}var t_exports = {}; +__export(t_exports, { plain: () => plain, inner: () => inner }); +function plain(x) { return "plain " + x; } +function inner() { + function own() { return typeof this; } + return "inner " + own.call(1); +} +var init_t = __esm({ "t.ts"() {} }); +const t = (init_t(), __toCommonJS(t_exports)); +console.log(t.plain(1), (t.plain)(2), t.inner()); +`); + expect(lowered).not.toContain("__toCommonJS(t_exports)"); +}); + +test.each([ + ["a direct call", `NS.who()`], + ["a parenthesized callee", `(NS.who)()`], + ["a tagged template", "NS.who`x`"], + ["a call through a local", `(() => { const m = NS; return m.who(); })()`], + ["an arrow inside the function", `NS.arrow()()`], +])("%s that passes the namespace as `this` keeps the package refused", (_, use) => { + const result = lower(`${helpers}var w_exports = {}; +__export(w_exports, { who: () => who, arrow: () => arrow }); +function who() { return this.__esModule; } +function arrow() { return () => this; } +var init_w = __esm({ "w.ts"() {} }); +console.log(${use.replaceAll("NS", "(init_w(), __toCommonJS(w_exports))")}); +`); + expect(result).toEqual({ + degrade: expect.stringContaining( + "calls only of exported functions that do not read 'this'", + ) as string, + }); +}); + +test("an export that reads `this` in a parameter's default value keeps the package refused", () => { + const result = lower(`${helpers}var p_exports = {}; +__export(p_exports, { who: () => who }); +function who(value = this.__esModule) { return value; } +var init_p = __esm({ "p.ts"() {} }); +console.log((init_p(), __toCommonJS(p_exports)).who()); +`); + expect(result).toEqual({ + degrade: expect.stringContaining( + "calls only of exported functions that do not read 'this'", + ) as string, + }); +}); + +test("minified initializers sharing a var statement lower the same way", () => { + expectSameBehavior( + helpers + + `var x_exports={};__export(x_exports,{v:()=>v});var v,import_path,init_x=__esm({"x.ts"(){"use strict";import_path=__toESM(require("path")),v=import_path.default.sep}});` + + `console.log((init_x(),__toCommonJS(x_exports)).v);`, + ); +}); + +test("memoized lazy loaders sharing a var statement lower together", () => { + const lowered = expectSameBehavior(`${helpers}var a_exports = {}; +__export(a_exports, { v: () => v }); +var v, init_a = __esm({ "a.ts"() { console.log("a init"); v = 1; } }); +var b_exports = {}; +__export(b_exports, { default: () => w }); +var w, init_b = __esm({ "b.ts"() { console.log("b init"); w = 2; } }); +var nsA, loadA = () => nsA ??= (init_a(), __toCommonJS(a_exports)), nsB, loadB = () => nsB ??= __toESM((init_b(), __toCommonJS(b_exports))); +console.log(loadA().v, loadB().default, loadA().v); +`); + expect(lowered).not.toContain("loadA"); + expect(lowered).not.toContain("loadB"); +}); + +test("calls through a memoized lazy loader keep the receiver rule", () => { + const source = (use: string): string => `${helpers}var w_exports = {}; +__export(w_exports, { who: () => who, plain: () => plain }); +function who() { return this.__esModule; } +function plain(x) { return "plain " + x; } +var init_w = __esm({ "w.ts"() {} }); +var ns, load = () => ns ??= (init_w(), __toCommonJS(w_exports)); +console.log(${use}); +`; + expect(lower(source("load().who()"))).toEqual({ + degrade: expect.stringContaining( + "calls only of exported functions that do not read 'this'", + ) as string, + }); + const lowered = expectSameBehavior(source("load().plain(1), (load().plain)(2)")); + expect(lowered).not.toContain("load()"); +}); + +test.each([ + ["is asynchronous", `var init_a = __esm({ async "a.ts"() { await 0; } });\ninit_a();`], + [ + "is used other than as a statement call", + `var init_a = __esm({ "a.ts"() {} });\nvar r = init_a();`, + ], + [ + "reads its factory's receiver", + `var init_a = __esm({ "a.ts"() { console.log(this); } });\ninit_a();`, + ], + [ + "memoized lazy loader 'load' returns the namespace of a bundled ES module as a value", + `var a_exports = {};\n__export(a_exports, { v: () => v });\nvar v, init_a = __esm({ "a.ts"() { v = 1; } });\nvar ns, load = () => ns ??= __toESM((init_a(), __toCommonJS(a_exports)));\nload();`, + ], + [ + "reads the namespace of a bundled ES module ('a_exports')", + `var a_exports = {};\n__export(a_exports, { v: () => v });\nvar v, init_a = __esm({ "a.ts"() { v = 1; } });\nconsole.log(Object.keys((init_a(), __toCommonJS(a_exports))));`, + ], +])("an initializer that %s keeps the package refused", (reason, code) => { + expect(lower(`${helpers}${code}\n`)).toEqual({ + degrade: expect.stringContaining(reason) as string, + }); +}); + +test("files without __esm initializers and ES-module bundles are left alone", () => { + expect(lower("exports.x = 1;\n")).toBeNull(); + expect( + lower(`${helpers}var init_a = __esm({ "a.ts"() {} });\ninit_a();\nexport {};\n`), + ).toBeNull(); +}); diff --git a/packages/compiler/src/frontend/npm-static-bundled-esm.ts b/packages/compiler/src/frontend/npm-static-bundled-esm.ts new file mode 100644 index 000000000..166676b3a --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-bundled-esm.ts @@ -0,0 +1,825 @@ +/* Lazily initialized ES modules in a CommonJS bundle. esbuild scope-hoists + * every ES module it bundles into the bundle's own scope; one that some + * code require()s instead of importing runs from a lazy initializer, + * + * var COUNT, import_fs; + * var init_x = __esm({ "src/x.ts"() { import_fs = __toESM(require("fs")); COUNT = 0; } }); + * + * and the requiring site reads its namespace as + * `(init_x(), __toCommonJS(x_exports))`. `init_x()` runs the body once (a + * call during the body answers immediately). The pass below keeps that + * shape in the bundle's scope and respells only what has no static + * representation: + * + * - the initializer becomes a run-once closure over the same body; + * - an interop binding the body assigns once from require() (`import_fs` + * above) becomes that require at the assignment's position, and its + * member reads become member reads of the required module, exactly + * like the top-level interop erasure (npm-static-rewrite-syntax.ts); + * - a namespace read only through member reads becomes an object + * literal of getters over the module's live bindings (what + * __toCommonJS copies, minus the non-enumerable __esModule mark no + * member read can reach). + * + * The respelled interop reads resolve the required module even if they + * ran before the initializer, where Node would read a member of + * undefined and throw; esbuild only reaches them through the module's + * own code, which runs after its initializer. Shapes outside the + * recognized forms answer a degrade reason. */ +import * as ts from "./ts7/syntax.js"; +import { bareRequireSpecOf } from "./cjs-syntax.js"; +import { helperTokens } from "./helper-tokens.js"; +import { + applyEdits, + countDeclarations, + declaredNames, + factoryFrameUse, + helperBase, + isWritten, + references, + unparen, + type Edit, +} from "./npm-static-bundled-chunks.js"; + +export interface EsmInitHost { + requireTargetEsModuleStamped(fromFile: string, specifier: string): boolean; +} + +const ESM_SHAPE_DEGRADE = + "its shipped JS initializes bundled ES modules through the __esm helper in a shape the " + + "recognizer cannot verify"; + +/* The recognized __esm forms over its parameter names (FN, RES) and the + * own-key reader (KEYS): keyed `(FN, RES) => function [name]() { return + * ; }` with a one-method object argument, and arrow + * `(FN, RES) => () => ()` with a function argument. */ +const KEYED_RETURN = "FN && (RES = (0, FN[KEYS(FN)[0]])(FN = 0)), RES"; +const ARROW_BODY = "(FN && (RES = FN(FN = 0)), RES)"; + +function template(text: string, fn: string, res: string, keys: string): string | null { + return helperTokens( + text + .replace(/\bFN\b/g, fn) + .replace(/\bRES\b/g, res) + .replace(/\bKEYS\b/g, keys), + ); +} + +function esmHelperForm( + init: ts.Expression, + sf: ts.SourceFile, + ownNames: (name: string) => boolean, +): "keyed" | "arrow" | null { + const fn = unparen(init); + if (!ts.isArrowFunction(fn) || fn.parameters.nodes.length !== 2 || ts.isBlock(fn.body)) + return null; + const [p0, p1] = fn.parameters.nodes; + if ( + !p0 || + !p1 || + !ts.isIdentifier(p0.name) || + !ts.isIdentifier(p1.name) || + p0.initializer || + p1.initializer || + p0.name.text === p1.name.text + ) + return null; + const [f, r] = [p0.name.text, p1.name.text]; + const body = unparen(fn.body); + if (ts.isFunctionExpression(body)) { + if (body.parameters.nodes.length !== 0 || body.asteriskToken || body.modifiers?.nodes.length) + return null; + const stmts = body.body.statements.nodes; + const ret = stmts[0]; + if (stmts.length !== 1 || !ret || !ts.isReturnStatement(ret) || !ret.expression) return null; + const text = ret.expression.getText(sf); + const keys = /\[\s*([A-Za-z_$][\w$]*(?:\.keys)?)\s*\(/.exec(text)?.[1]; + if (keys === undefined || (keys !== "Object.keys" && !ownNames(keys))) return null; + return helperTokens(text) === template(KEYED_RETURN, f, r, keys) ? "keyed" : null; + } + if (ts.isArrowFunction(body) && body.parameters.nodes.length === 0 && !ts.isBlock(body.body)) { + const actual = helperTokens(body.body.getText(sf)); + return actual !== null && actual === template(ARROW_BODY, f, r, "") ? "arrow" : null; + } + return null; +} + +/** Pads the given declarators out of their statements (separating commas + * included; a statement whose declarators all go pads whole). */ +function removeDeclarators(sf: ts.SourceFile, dead: ReadonlySet): Edit[] { + const edits: Edit[] = []; + const stmts = new Set(); + for (const d of dead) { + const stmt = d.parent?.parent; + if (stmt !== undefined && ts.isVariableStatement(stmt)) stmts.add(stmt); + } + for (const stmt of stmts) { + const decls = stmt.declarationList.declarations.nodes; + const deadAt = decls.map((d) => dead.has(d)); + if (deadAt.every((x) => x)) { + edits.push({ start: stmt.getStart(sf), end: stmt.getEnd(), text: "" }); + continue; + } + const lastLive = deadAt.lastIndexOf(false); + for (let i = 0; i < decls.length; i++) { + if (!deadAt[i]) continue; + edits.push( + i < lastLive + ? { start: decls[i]!.getStart(sf), end: decls[i + 1]!.getStart(sf), text: "" } + : { start: decls[lastLive]!.getEnd(), end: decls[i]!.getEnd(), text: "" }, + ); + } + } + return edits; +} + +/** The elements of an expression statement's comma chain, in order. */ +function commaElements(e: ts.Expression): ts.Expression[] { + const out: ts.Expression[] = []; + const visit = (x: ts.Expression): void => { + if (ts.isBinaryExpression(x) && x.operatorToken.kind === ts.SyntaxKind.CommaToken) { + visit(x.left); + visit(x.right); + } else out.push(x); + }; + visit(e); + return out; +} + +/** True when a call's result is discarded: an expression statement or a + * non-final comma operand (parentheses included). */ +function resultUnused(call: ts.Expression): boolean { + let child: ts.Node = call; + let p = call.parent; + while (p !== undefined && ts.isParenthesizedExpression(p)) { + child = p; + p = p.parent; + } + if (p === undefined) return false; + if (ts.isExpressionStatement(p)) return true; + if (ts.isBinaryExpression(p) && p.operatorToken.kind === ts.SyntaxKind.CommaToken) { + if (p.left === child) return true; + return resultUnused(p); + } + return false; +} + +/** True when a member access is a plain read (not written, deleted or + * updated). */ +function plainRead(access: ts.Expression): boolean { + const p = access.parent; + if (p === undefined) return true; + if (ts.isDeleteExpression(p)) return false; + if ( + ts.isBinaryExpression(p) && + p.left === access && + p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + p.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ) + return false; + if ( + (ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) && + (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken) + ) + return false; + return true; +} + +/** True when a member read is the callee of a call or a tag, which receives + * the object it was read from as `this`. */ +function calledWithReceiver(access: ts.Expression): boolean { + let callee: ts.Node = access; + while (callee.parent !== undefined && ts.isParenthesizedExpression(callee.parent)) + callee = callee.parent; + const p = callee.parent; + return ( + p !== undefined && + ((ts.isCallExpression(p) && p.expression === callee) || + (ts.isTaggedTemplateExpression(p) && p.tag === callee)) + ); +} + +/** True when a function reads its own `this`: in its body or a parameter's + * default value, or in an arrow function inside them. */ +function readsThis(fn: ts.FunctionDeclaration): boolean { + let found = false; + if (fn.body === undefined) return false; + const visit = (n: ts.Node): "stop" | "skip" | undefined => { + if (found) return "stop"; + if ( + ts.isFunctionDeclaration(n) || + ts.isFunctionExpression(n) || + ts.isMethodDeclaration(n) || + ts.isGetAccessorDeclaration(n) || + ts.isSetAccessorDeclaration(n) || + ts.isConstructorDeclaration(n) || + ts.isClassDeclaration(n) || + ts.isClassExpression(n) + ) + return "skip"; + if (n.kind === ts.SyntaxKind.ThisKeyword) found = true; + return undefined; + }; + for (const parameter of fn.parameters.nodes) ts.walkPreorder(parameter, visit); + ts.walkPreorder(fn.body, visit); + return found; +} + +/** The `__export(T, { name: () => value, … })` getter table of T, or null. */ +function exportTableOf( + call: ts.CallExpression, +): { target: string; entries: [string, ts.Expression][] } | null { + if (!ts.isIdentifier(call.expression) || helperBase(call.expression.text) !== "__export") + return null; + const [target, table] = call.arguments.nodes; + if ( + call.arguments.nodes.length !== 2 || + !target || + !table || + !ts.isIdentifier(target) || + !ts.isObjectLiteralExpression(table) + ) + return null; + const entries: [string, ts.Expression][] = []; + for (const prop of table.properties.nodes) { + if (!ts.isPropertyAssignment(prop)) return null; + if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) return null; + const getter = unparen(prop.initializer); + if (!ts.isArrowFunction(getter) || getter.parameters.nodes.length !== 0) return null; + if (ts.isBlock(getter.body)) return null; + entries.push([prop.name.text, getter.body]); + } + return { target: target.text, entries }; +} + +/** Lowers a CommonJS bundle's __esm initializers (see the header). Null: + * nothing to lower. */ +export function lowerBundledEsmInits( + sf: ts.SourceFile, + filePath: string, + host: EsmInitHost, +): string | { degrade: string } | null { + const source = sf.text; + if (!source.includes("__esm")) return null; + if (!filePath.endsWith(".cjs") && ts.isExternalModule(sf)) return null; + + interface Top { + stmt: ts.Statement; + node: ts.VariableDeclaration | ts.FunctionDeclaration; + } + const top = new Map(); + const duplicate = new Set(); + for (const stmt of sf.statements.nodes) { + const add = (name: string, node: Top["node"]): void => { + if (top.has(name)) duplicate.add(name); + else top.set(name, { stmt, node }); + }; + if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined) add(stmt.name.text, stmt); + else if (ts.isVariableStatement(stmt)) { + for (const d of stmt.declarationList.declarations.nodes) { + if (ts.isIdentifier(d.name)) add(d.name.text, d); + } + } + } + const helpers = [...top.keys()].filter((n) => helperBase(n) === "__esm"); + if (helpers.length === 0) return null; + + const allRefs = references(sf); + const writes = new Map(); + for (const r of allRefs) if (isWritten(r)) writes.set(r.text, (writes.get(r.text) ?? 0) + 1); + const declaredAnywhere = declaredNames(sf); + if (declaredAnywhere.has("Object") || writes.has("Object")) return { degrade: ESM_SHAPE_DEGRADE }; + const stable = (name: string): boolean => !duplicate.has(name) && !writes.has(name); + const ownNames = (name: string): boolean => { + const t = top.get(name); + const init = + t !== undefined && ts.isVariableDeclaration(t.node) ? t.node.initializer : undefined; + return ( + init !== undefined && + stable(name) && + helperTokens(init.getText(sf)) === helperTokens("Object.getOwnPropertyNames") + ); + }; + const forms = new Map(); + for (const name of helpers) { + const t = top.get(name)!; + const init = ts.isVariableDeclaration(t.node) ? t.node.initializer : undefined; + const form = init !== undefined && stable(name) ? esmHelperForm(init, sf, ownNames) : null; + if (form === null) return { degrade: ESM_SHAPE_DEGRADE }; + forms.set(name, form); + } + + // The initializers: `NAME = HELPER(factory)` declarators. + interface Init { + name: string; + call: ts.CallExpression; + body: ts.Block; + } + const inits = new Map(); + for (const [name, t] of top) { + if (!ts.isVariableDeclaration(t.node) || t.node.initializer === undefined) continue; + const call = unparen(t.node.initializer); + if (!ts.isCallExpression(call) || !ts.isIdentifier(call.expression)) continue; + const form = forms.get(call.expression.text); + if (form === undefined) continue; + if (call.arguments.nodes.length !== 1 || !stable(name)) return { degrade: ESM_SHAPE_DEGRADE }; + let factory: ts.Node = call.arguments.nodes[0]!; + if (form === "keyed") { + if (!ts.isObjectLiteralExpression(factory) || factory.properties.nodes.length !== 1) + return { degrade: ESM_SHAPE_DEGRADE }; + factory = factory.properties.nodes[0]!; + if (!ts.isMethodDeclaration(factory) || ts.isComputedPropertyName(factory.name)) + return { degrade: ESM_SHAPE_DEGRADE }; + } else { + factory = unparen(factory as ts.Expression); + if (!ts.isArrowFunction(factory) && !ts.isFunctionExpression(factory)) + return { degrade: ESM_SHAPE_DEGRADE }; + } + const fn = factory as ts.MethodDeclaration | ts.ArrowFunction | ts.FunctionExpression; + if (fn.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) + return { + degrade: `its bundled ES module initializer '${name}' is asynchronous (top-level await), which the lazy initialization rewrite does not cover`, + }; + if ( + fn.body === undefined || + !ts.isBlock(fn.body) || + fn.asteriskToken !== undefined || + (fn.modifiers?.nodes.length ?? 0) > 0 || + fn.parameters.nodes.length > 0 + ) + return { degrade: ESM_SHAPE_DEGRADE }; + const frame = factoryFrameUse(fn.body, false); + if (frame !== null) return { degrade: `its bundled ES module initializer '${name}' ${frame}` }; + inits.set(name, { name, call, body: fn.body }); + } + if (inits.size === 0) return null; + + const edits: Edit[] = []; + // Every initializer reference is a direct call whose result is unused. + for (const r of allRefs) { + if (!inits.has(r.text)) continue; + const call = r.parent; + if ( + call === undefined || + !ts.isCallExpression(call) || + call.expression !== r || + call.arguments.nodes.length !== 0 || + !resultUnused(call) + ) + return { + degrade: `its bundled ES module initializer '${r.text}' is used other than as a statement call`, + }; + } + for (const name of inits.keys()) { + if (countDeclarations(sf, name) !== 1) return { degrade: ESM_SHAPE_DEGRADE }; + } + + /* Interop bindings an initializer assigns once from require(). */ + const refsByName = new Map(); + for (const r of allRefs) { + const list = refsByName.get(r.text); + if (list === undefined) refsByName.set(r.text, [r]); + else list.push(r); + } + const deadDeclarators = new Set(); + for (const init of inits.values()) { + for (const stmt of init.body.statements.nodes) { + if (!ts.isExpressionStatement(stmt)) continue; + for (const el of commaElements(stmt.expression)) { + if ( + !ts.isBinaryExpression(el) || + el.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !ts.isIdentifier(el.left) + ) + continue; + const name = el.left.text; + const decl = top.get(name)?.node; + if ( + decl === undefined || + !ts.isVariableDeclaration(decl) || + decl.initializer !== undefined || + duplicate.has(name) || + writes.get(name) !== 1 || + countDeclarations(sf, name) !== 1 + ) + continue; + let rhs = unparen(el.right); + let isNodeMode = false; + let interop = false; + if ( + ts.isCallExpression(rhs) && + ts.isIdentifier(rhs.expression) && + helperBase(rhs.expression.text) === "__toESM" + ) { + const [arg, mode] = rhs.arguments.nodes; + if (!arg || rhs.arguments.nodes.length > 2) continue; + if (mode !== undefined) { + if (ts.isNumericLiteral(mode)) isNodeMode = Number(mode.text) !== 0; + else if (mode.kind === ts.SyntaxKind.TrueKeyword) isNodeMode = true; + else if (mode.kind !== ts.SyntaxKind.FalseKeyword) continue; + } + rhs = unparen(arg); + interop = true; + } + const spec = bareRequireSpecOf(rhs); + if (spec === null) continue; + const req = rhs.getText(sf); + const defaultIsModule = + interop && (isNodeMode || !host.requireTargetEsModuleStamped(filePath, spec)); + // Every other reference must be a plain member read. + const uses: Edit[] = []; + let ok = true; + for (const r of refsByName.get(name) ?? []) { + if (r === el.left) continue; + const access = r.parent; + if ( + access === undefined || + !ts.isPropertyAccessExpression(access) || + access.expression !== r || + !plainRead(access) + ) { + ok = false; + break; + } + if (defaultIsModule && access.name.text === "default") + uses.push({ start: access.getStart(sf), end: access.getEnd(), text: `(${req})` }); + else uses.push({ start: r.getStart(sf), end: r.getEnd(), text: req }); + } + if (!ok) continue; + edits.push({ start: el.getStart(sf), end: el.getEnd(), text: req }, ...uses); + deadDeclarators.add(decl); + } + } + } + + /* Namespaces of lazily initialized modules read through members. */ + let mainTarget: string | null = null; + for (const stmt of sf.statements.nodes) { + if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue; + const e = stmt.expression; + const rhs = e.right; + if ( + e.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(e.left) && + ts.isIdentifier(e.left.expression) && + e.left.expression.text === "module" && + e.left.name.text === "exports" && + ts.isCallExpression(rhs) && + ts.isIdentifier(rhs.expression) && + helperBase(rhs.expression.text) === "__toCommonJS" && + rhs.arguments.nodes.length === 1 && + ts.isIdentifier(rhs.arguments.nodes[0]!) + ) + mainTarget = (rhs.arguments.nodes[0] as ts.Identifier).text; + } + const tables = new Map(); + for (const stmt of sf.statements.nodes) { + if (!ts.isExpressionStatement(stmt) || !ts.isCallExpression(stmt.expression)) continue; + const table = exportTableOf(stmt.expression); + if (table !== null && table.target !== mainTarget) { + if (tables.has(table.target)) return { degrade: ESM_SHAPE_DEGRADE }; + tables.set(table.target, { stmt, entries: table.entries }); + } + } + const lowered = new Set(); + /** Some binding between the use site and the top level rebinds a name + * the table's getters read. */ + const shadowed = (site: ts.Node, entries: readonly [string, ts.Expression][]): boolean => { + const read = new Set(); + for (const [, value] of entries) for (const x of references(value)) read.add(x.text); + for (let a = site.parent; a !== undefined && !ts.isSourceFile(a); a = a.parent) { + if ( + ts.isFunctionLike(a) || + ts.isBlock(a) || + ts.isClassDeclaration(a) || + ts.isClassExpression(a) + ) { + for (const n of declaredNames(a)) if (read.has(n)) return true; + } + } + return false; + }; + const getterText = (value: ts.Expression): string => + applyEdits(source, value.getStart(sf), value.getEnd(), edits); + /** A call through the namespace (`ns.fn()`, `L().fn()`) passes the object + * the read came from as `this`; the lowered object differs from Node's + * module object (`__esModule`, property attributes), so it must stay + * unobservable: the callee is a top-level function that never reads + * `this`. */ + const receiverHidden = (values: ReadonlyMap, name: string): boolean => { + const value = values.get(name); + if (value === undefined || !ts.isIdentifier(value) || !stable(value.text)) return false; + const decl = top.get(value.text)?.node; + return decl !== undefined && ts.isFunctionDeclaration(decl) && !readsThis(decl); + }; + + /* Memoized lazy loaders of those namespaces, `L = () => M ??= NS` (a + * bundle plugin's lazy re-exports): every `L().name` becomes the + * initializer call followed by the export's live read, so the first + * access still initializes the module, once, and later accesses read + * the binding. */ + const loaderSpans: { start: number; end: number }[] = []; + for (const [name, t] of top) { + const d = t.node; + if (!ts.isVariableDeclaration(d) || d.initializer === undefined) continue; + const fn = unparen(d.initializer); + if (!ts.isArrowFunction(fn) || fn.parameters.nodes.length !== 0 || ts.isBlock(fn.body)) + continue; + const memoize = unparen(fn.body); + if ( + !ts.isBinaryExpression(memoize) || + (memoize.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionEqualsToken && + memoize.operatorToken.kind !== ts.SyntaxKind.BarBarEqualsToken) || + !ts.isIdentifier(memoize.left) + ) + continue; + let w = unparen(memoize.right); + let interop = false; + let nodeMode = false; + if ( + ts.isCallExpression(w) && + ts.isIdentifier(w.expression) && + helperBase(w.expression.text) === "__toESM" + ) { + const [arg, mode] = w.arguments.nodes; + if (!arg || w.arguments.nodes.length > 2) continue; + if (mode !== undefined) { + if (ts.isNumericLiteral(mode)) nodeMode = Number(mode.text) !== 0; + else if (mode.kind === ts.SyntaxKind.TrueKeyword) nodeMode = true; + else if (mode.kind !== ts.SyntaxKind.FalseKeyword) continue; + } + interop = true; + w = unparen(arg); + } + let initCall: ts.CallExpression | null = null; + if (ts.isBinaryExpression(w) && w.operatorToken.kind === ts.SyntaxKind.CommaToken) { + const left = unparen(w.left); + if ( + !ts.isCallExpression(left) || + !ts.isIdentifier(left.expression) || + !inits.has(left.expression.text) || + left.arguments.nodes.length !== 0 + ) + continue; + initCall = left; + w = unparen(w.right); + } + if ( + !ts.isCallExpression(w) || + !ts.isIdentifier(w.expression) || + helperBase(w.expression.text) !== "__toCommonJS" || + w.arguments.nodes.length !== 1 || + !ts.isIdentifier(w.arguments.nodes[0]!) + ) + continue; + const target = (w.arguments.nodes[0] as ts.Identifier).text; + const table = tables.get(target); + if (table === undefined) continue; + const loaderWhy = { + degrade: `its memoized lazy loader '${name}' returns the namespace of a bundled ES module as a value, a module object the static rewrite cannot represent (only member reads of its result compile, and calls only of exported functions that do not read 'this')`, + }; + const memo = memoize.left.text; + const memoDecl = top.get(memo)?.node; + if ( + memoDecl === undefined || + !ts.isVariableDeclaration(memoDecl) || + memoDecl.initializer !== undefined || + writes.get(memo) !== 1 || + countDeclarations(sf, memo) !== 1 || + (refsByName.get(memo) ?? []).some((x) => x !== memoize.left) || + writes.has(name) || + countDeclarations(sf, name) !== 1 + ) + return loaderWhy; + const entries = new Map(table.entries); + for (const ref of refsByName.get(name) ?? []) { + const call = ref.parent; + if ( + call === undefined || + !ts.isCallExpression(call) || + call.expression !== ref || + call.arguments.nodes.length !== 0 + ) + return loaderWhy; + let access: ts.Node | undefined = call.parent; + if ( + access === undefined || + !ts.isPropertyAccessExpression(access) || + access.expression !== call || + !plainRead(access) + ) + return loaderWhy; + // Node mode binds `default` to the namespace object itself. + if (interop && nodeMode && access.name.text === "default") { + const outer: ts.Node | undefined = access.parent; + if ( + outer === undefined || + !ts.isPropertyAccessExpression(outer) || + outer.expression !== access || + !plainRead(outer) + ) + return loaderWhy; + access = outer; + } + const member = (access as ts.PropertyAccessExpression).name.text; + const value = entries.get(member); + if (value === undefined || shadowed(access, table.entries)) return loaderWhy; + // The rewrite reads the export without the namespace object, so a + // call through it (`L().fn()`) loses its receiver. + if (calledWithReceiver(access as ts.Expression) && !receiverHidden(entries, member)) + return loaderWhy; + const init = initCall === null ? "" : `${initCall.getText(sf)}, `; + edits.push({ + start: access.getStart(sf), + end: access.getEnd(), + text: `(${init}${getterText(value)})`, + }); + } + deadDeclarators.add(d); + deadDeclarators.add(memoDecl); + loaderSpans.push({ start: d.getStart(sf), end: d.getEnd() }); + lowered.add(target); + } + + for (const r of allRefs) { + const table = tables.get(r.text); + if (table === undefined) continue; + if (loaderSpans.some((span) => r.getStart(sf) >= span.start && r.getEnd() <= span.end)) + continue; + const p = r.parent; + // The table's own declaration and __export call are handled below. + if (p !== undefined && ts.isVariableDeclaration(p) && p.name === r) continue; + if ( + p !== undefined && + ts.isCallExpression(p) && + p.arguments.nodes[0] === r && + ts.isExpressionStatement(p.parent) && + p.parent === table.stmt + ) + continue; + const why = `reads the namespace of a bundled ES module ('${r.text}') in a way the static rewrite cannot follow (only member reads of its exports compile, and calls only of exported functions that do not read 'this')`; + if ( + p === undefined || + !ts.isCallExpression(p) || + !ts.isIdentifier(p.expression) || + helperBase(p.expression.text) !== "__toCommonJS" || + p.arguments.nodes.length !== 1 + ) + return { degrade: `its shipped JS ${why}` }; + // The namespace expression: `(init_x(), __toCommonJS(T))` or the call. + let ns: ts.Expression = p; + for (;;) { + const up = ns.parent; + if (up !== undefined && ts.isParenthesizedExpression(up)) ns = up; + else if ( + up !== undefined && + ts.isBinaryExpression(up) && + up.operatorToken.kind === ts.SyntaxKind.CommaToken && + up.right === ns + ) + ns = up; + else break; + } + const values = new Map(table.entries); + const memberRead = (access: ts.Node | undefined, of: ts.Node): boolean => + access !== undefined && + ts.isPropertyAccessExpression(access) && + access.expression === of && + values.has(access.name.text) && + plainRead(access) && + (!calledWithReceiver(access) || receiverHidden(values, access.name.text)); + const holder = ns.parent; + // `__reExport(main, ns, module.exports)`: a star re-export of the + // module into the bundle's own exports (`export * from` in its entry). + // The initializer runs at the statement; the names become live getter + // entries of the export table, after its explicit ones (first wins) + // and without `default`, exactly the names __reExport copies. + if ( + holder !== undefined && + ts.isCallExpression(holder) && + ts.isIdentifier(holder.expression) && + helperBase(holder.expression.text) === "__reExport" && + holder.arguments.nodes[1] === ns && + holder.arguments.nodes.length <= 3 && + ts.isExpressionStatement(holder.parent) && + mainTarget !== null && + ts.isIdentifier(holder.arguments.nodes[0]!) && + (holder.arguments.nodes[0] as ts.Identifier).text === mainTarget + ) { + const inner = unparen(ns); + const init = + ts.isBinaryExpression(inner) && inner.operatorToken.kind === ts.SyntaxKind.CommaToken + ? `${unparen(inner.left).getText(sf)}; ` + : ""; + if (shadowed(holder, table.entries)) return { degrade: `its shipped JS ${why}` }; + const entries = table.entries + .filter(([n]) => n !== "default") + .map(([n, value]) => `${JSON.stringify(n)}: () => ${getterText(value)}`) + .join(", "); + edits.push({ + start: holder.parent.getStart(sf), + end: holder.parent.getEnd(), + text: `${init}__export(${mainTarget}, { ${entries} });`, + }); + lowered.add(r.text); + continue; + } + let readable = memberRead(holder, ns); + if ( + !readable && + holder !== undefined && + ts.isVariableDeclaration(holder) && + holder.initializer === ns && + ts.isIdentifier(holder.name) + ) { + const local = holder.name.text; + const localRefs = (refsByName.get(local) ?? []).filter((x) => x !== holder.name); + readable = + countDeclarations(sf, local) === 1 && + !writes.has(local) && + localRefs.every((x) => memberRead(x.parent, x)); + } + if (!readable) { + // A namespace kept for later (`x ??= __toESM(ns)`, a hand-rolled + // lazy loader) is a value with no static representation. + let outer: ts.Node | undefined = holder; + while ( + outer !== undefined && + (ts.isParenthesizedExpression(outer) || + (ts.isCallExpression(outer) && + ts.isIdentifier(outer.expression) && + helperBase(outer.expression.text) === "__toESM")) + ) + outer = outer.parent; + if ( + outer !== undefined && + ts.isBinaryExpression(outer) && + outer.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + outer.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ) + return { + degrade: `its shipped JS stores the namespace of a bundled ES module ('${r.text}') in a variable (a memoized lazy loader), a module object the static rewrite cannot represent`, + }; + return { degrade: `its shipped JS ${why}` }; + } + // The getters read top-level bindings: nothing between the use site + // and the top level may rebind one of their names. + if (shadowed(p, table.entries)) return { degrade: `its shipped JS ${why}` }; + const getters = table.entries + .map( + ([n, value]) => + `get ${/^[A-Za-z_$][\w$]*$/.test(n) ? n : JSON.stringify(n)}() { return ${getterText(value)}; }`, + ) + .join(", "); + edits.push({ start: p.getStart(sf), end: p.getEnd(), text: `{ ${getters} }` }); + lowered.add(r.text); + } + // A table nothing reads (its module is never required) goes too: its + // names are no exports of the bundle. + for (const [target, table] of tables) { + if (lowered.has(target)) continue; + const live = (refsByName.get(target) ?? []).some((x) => { + const p = x.parent; + if (p !== undefined && ts.isVariableDeclaration(p) && p.name === x) return false; + return !(x.getStart(sf) >= table.stmt.getStart(sf) && x.getEnd() <= table.stmt.getEnd()); + }); + if (!live) lowered.add(target); + } + for (const target of lowered) { + const table = tables.get(target)!; + const decl = top.get(target)?.node; + if ( + decl === undefined || + !ts.isVariableDeclaration(decl) || + decl.initializer === undefined || + !ts.isObjectLiteralExpression(unparen(decl.initializer)) || + (unparen(decl.initializer) as ts.ObjectLiteralExpression).properties.nodes.length !== 0 || + writes.has(target) + ) + return { degrade: ESM_SHAPE_DEGRADE }; + edits.push({ start: table.stmt.getStart(sf), end: table.stmt.getEnd(), text: "" }); + deadDeclarators.add(decl); + } + // Every removed declarator in one pass: declarators of one statement + // removed separately would leave overlapping edits, and applyEdits keeps + // only the first of them. + edits.push(...removeDeclarators(sf, deadDeclarators)); + + /* The run-once closures. */ + for (const init of inits.values()) { + const { call, body } = init; + edits.push( + { + start: call.getStart(sf), + end: body.getStart(sf) + 1, + text: "(() => { let __scriptc_done = false; function __scriptc_init() {", + }, + { + start: body.getEnd() - 1, + end: call.getEnd(), + text: "} return function () { if (__scriptc_done) return; __scriptc_done = true; __scriptc_init(); }; })()", + }, + ); + } + return applyEdits(source, 0, source.length, edits); +} diff --git a/packages/compiler/src/frontend/npm-static-cjs-exports-alias.test.ts b/packages/compiler/src/frontend/npm-static-cjs-exports-alias.test.ts new file mode 100644 index 000000000..c3704ea29 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-cjs-exports-alias.test.ts @@ -0,0 +1,81 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, expect, test } from "vitest"; +import { nodeFrontendServices } from "./services-node.js"; + +const dirs: string[] = []; +afterAll(() => { + for (const dir of dirs) rmSync(dir, { recursive: true, force: true }); +}); + +function rewrite(source: string): string | null { + return nodeFrontendServices().cjsFunctionExports(source, "/pkg/index.js"); +} + +function run(file: string) { + const result = spawnSync(process.execPath, [file], { encoding: "utf8" }); + return { status: result.status, stdout: result.stdout, stderr: result.stderr }; +} + +/** The rewritten module prints what the original prints. */ +function expectSameBehavior(source: string): string { + const result = rewrite(source); + expect(typeof result).toBe("string"); + const dir = mkdtempSync(join(tmpdir(), "scriptc-cjs-function-exports-")); + dirs.push(dir); + const original = join(dir, "original.cjs"); + const rewritten = join(dir, "rewritten.cjs"); + writeFileSync(original, source); + writeFileSync(rewritten, result as string); + const oracle = run(original); + expect(oracle.status).toBe(0); + expect(run(rewritten)).toEqual(oracle); + return result as string; +} + +const report = ` +console.log(typeof module.exports, module.exports === Version, Object.keys(Version).join(",")); +`; + +test("members of a function export object read and write the function", () => { + const rewritten = expectSameBehavior(`exports = module.exports = Version; +exports.parse = parse; +module.exports.major = (v) => parse(v)[0]; +function Version(v) { return { v }; } +function parse(v) { return v.split(".").map(Number); } +console.log(exports.major("2.1.0"), module.exports.parse("1.2").length); +${report}`); + expect(rewritten).toContain("Version.parse = parse"); + expect(rewritten).not.toContain("exports = module.exports"); +}); + +test.each([ + ["a parameter", "function inner(Version) { return exports.parse; }"], + ["a local", "function inner() { const Version = {}; return exports.parse; }"], + ["a nested function", "function inner() { function Version() {} return exports.parse; }"], + [ + "a catch binding", + "function inner() { try { throw 0; } catch (Version) { return exports.parse; } }", + ], +])("a nested binding of the function's name (%s) keeps the module as it is", (_, inner) => { + expect( + rewrite(`exports = module.exports = Version; +exports.parse = parse; +function Version() {} +function parse() {} +${inner} +`), + ).toBeNull(); +}); + +test("an `exports` shorthand property keeps its key", () => { + const rewritten = expectSameBehavior(`exports = module.exports = Version; +exports.tag = "v"; +function Version() {} +const holder = { exports }; +console.log(Object.keys(holder).join(","), holder.exports === Version, holder.exports.tag); +${report}`); + expect(rewritten).toContain("{ exports: Version }"); +}); diff --git a/packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts b/packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts new file mode 100644 index 000000000..dfe45b862 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts @@ -0,0 +1,191 @@ +/* CommonJS modules whose export object is one of their own functions: + * + * exports = module.exports = SemVer; // or module.exports = SemVer; + * exports.parse = parse; // or module.exports.parse = … + * function SemVer(version) { … } + * + * After that statement `module.exports` (and, after the rebinding form, + * `exports`) IS the function, so every member read or write through them + * is a member of the function itself. The pass respells those accesses + * through the function's name, the expando form both resolvers model + * (members on a function declaration), and drops the `exports =` + * rebinding the static export protocol has no storage for. + * + * Exact only while nothing can observe the module before the statement + * runs and nothing replaces the export object again: the statement is a + * top-level statement preceded only by inert declarations, it is the + * file's only write of `module.exports` and `exports`, the function is a + * top-level declaration nothing reassigns and the file's only binding of + * its name, and `exports`/`module` are the CommonJS globals (never + * declared in the file). Other shapes answer + * null and keep their existing treatment. */ +import * as ts from "./ts7/syntax.js"; +import { + applyEdits, + countDeclarations, + declaredNames, + isWritten, + references, + unparen, + type Edit, +} from "./npm-static-bundled-chunks.js"; + +function isModuleExports(e: ts.Expression): e is ts.PropertyAccessExpression { + const x = unparen(e); + return ( + ts.isPropertyAccessExpression(x) && + !x.questionDotToken && + ts.isIdentifier(x.expression) && + x.expression.text === "module" && + x.name.text === "exports" + ); +} + +/** True for a statement whose evaluation runs no code that could observe + * the export object: declarations and member writes (`F.prototype.m = + * function …`) without calls, and directives. */ +function inert(stmt: ts.Statement): boolean { + if (ts.isFunctionDeclaration(stmt) || ts.isEmptyStatement(stmt)) return true; + if (ts.isExpressionStatement(stmt) && ts.isStringLiteral(stmt.expression)) return true; + if (ts.isExpressionStatement(stmt)) { + const e = unparen(stmt.expression); + if ( + !ts.isBinaryExpression(e) || + e.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !(ts.isPropertyAccessExpression(e.left) || ts.isElementAccessExpression(e.left)) + ) + return false; + let root: ts.Expression = e.left; + while (ts.isPropertyAccessExpression(root) || ts.isElementAccessExpression(root)) + root = root.expression; + if (!ts.isIdentifier(root) || root.text === "module" || root.text === "exports") return false; + } else if (!ts.isVariableStatement(stmt)) return false; + let calls = false; + ts.walkPreorder(stmt, (n) => { + if (calls) return "stop"; + if (ts.isArrowFunction(n) || ts.isFunctionExpression(n)) return "skip"; + if (ts.isCallExpression(n) || ts.isNewExpression(n) || ts.isTaggedTemplateExpression(n)) + calls = true; + return undefined; + }); + return !calls; +} + +export function normalizeCjsFunctionExports(sf: ts.SourceFile): string | null { + const source = sf.text; + if (!source.includes("module.exports")) return null; + if (!sf.fileName.endsWith(".cjs") && ts.isExternalModule(sf)) return null; + const declared = declaredNames(sf); + if (declared.has("exports") || declared.has("module")) return null; + + // The whole-export statement: `[exports =] module.exports = W;`. + let whole: { stmt: ts.ExpressionStatement; fn: string; rebinds: boolean } | null = null; + const stmts = sf.statements.nodes; + for (let i = 0; i < stmts.length; i++) { + const stmt = stmts[i]!; + if (!ts.isExpressionStatement(stmt)) continue; + const top = unparen(stmt.expression); + if (!ts.isBinaryExpression(top) || top.operatorToken.kind !== ts.SyntaxKind.EqualsToken) + continue; + let e: ts.BinaryExpression = top; + let rebinds = false; + if (ts.isIdentifier(top.left) && top.left.text === "exports") { + const inner = unparen(top.right); + if (!ts.isBinaryExpression(inner) || inner.operatorToken.kind !== ts.SyntaxKind.EqualsToken) + continue; + rebinds = true; + e = inner; + } + if (!isModuleExports(e.left)) continue; + const value = unparen(e.right); + if (!ts.isIdentifier(value)) return null; + if (whole !== null || !stmts.slice(0, i).every(inert)) return null; + whole = { stmt, fn: value.text, rebinds }; + } + if (whole === null) return null; + const fnName = whole.fn; + const fnDecls = stmts.filter( + (st) => ts.isFunctionDeclaration(st) && st.name?.text === fnName && st.body !== undefined, + ); + // The respelled reads name the function from wherever they sit, so no + // other binding of its name (a parameter, a local, a nested function) + // may exist anywhere in the file. + if (fnDecls.length !== 1 || countDeclarations(sf, fnName) !== 1) return null; + + const edits: Edit[] = []; + let unsafe = false; + const refs = references(sf); + for (const r of refs) { + if (r.text === fnName && isWritten(r)) unsafe = true; + if (r.text === "exports") { + if (isWritten(r)) { + // Only the whole statement's own rebinding. + const assign = r.parent; + if ( + !whole.rebinds || + assign === undefined || + !ts.isBinaryExpression(assign) || + assign.parent !== whole.stmt || + assign.left !== r + ) + unsafe = true; + continue; + } + // Without the rebinding, `exports` keeps the replaced object. + if (!whole.rebinds || r.getStart(sf) < whole.stmt.getEnd()) { + unsafe = true; + continue; + } + // `{ exports }` keeps its key: the shorthand spells out the value. + const shorthand = + r.parent !== undefined && ts.isShorthandPropertyAssignment(r.parent) && r.parent.name === r; + edits.push({ + start: r.getStart(sf), + end: r.getEnd(), + text: shorthand ? `exports: ${fnName}` : fnName, + }); + continue; + } + if (r.text === "module") { + const access = r.parent; + if (access === undefined || !isModuleExports(access as ts.Expression)) continue; + const outer = access.parent; + if (outer !== undefined && ts.isBinaryExpression(outer) && outer.left === access) { + if ( + outer.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + outer.operatorToken.kind <= ts.SyntaxKind.LastAssignment && + !( + (outer.parent === whole.stmt && !whole.rebinds) || + (whole.rebinds && outer.parent?.parent === whole.stmt) + ) + ) + unsafe = true; + continue; + } + // A member access through module.exports: a member of the function. + if ( + outer !== undefined && + (ts.isPropertyAccessExpression(outer) || ts.isElementAccessExpression(outer)) && + outer.expression === access + ) { + if (access.getStart(sf) < whole.stmt.getEnd() && !insideFunction(access)) unsafe = true; + edits.push({ start: access.getStart(sf), end: access.getEnd(), text: fnName }); + } + } + } + if (unsafe) return null; + if (whole.rebinds) { + // `exports = module.exports = W` → `module.exports = W`. + const e = unparen(whole.stmt.expression) as ts.BinaryExpression; + edits.push({ start: e.getStart(sf), end: unparen(e.right).getStart(sf), text: "" }); + } + if (edits.length === 0) return null; + return applyEdits(source, 0, source.length, edits); +} + +function insideFunction(node: ts.Node): boolean { + for (let p = node.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) { + if (ts.isFunctionLike(p)) return true; + } + return false; +} diff --git a/packages/compiler/src/frontend/npm-static-cjs-exports-var.ts b/packages/compiler/src/frontend/npm-static-cjs-exports-var.ts new file mode 100644 index 000000000..1af4ea1ed --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-cjs-exports-var.ts @@ -0,0 +1,238 @@ +/* CommonJS modules that name their export object through a variable: + * + * var ip = exports; + * ip.isPrivate = function (addr) { … }; + * ip.isPublic = function (addr) { return !ip.isPrivate(addr); }; + * + * `ip` IS the export object, so every member read or write through it is + * an export. The pass respells those accesses as `exports.`, the + * form the static export protocol models, and drops the alias declaration + * (the export object has no storage of its own to bind). + * + * Exact only while the variable is nothing but that alias: one top-level + * declaration initialized with the CommonJS `exports` global, never + * reassigned, never declared again anywhere in the file (so no nested + * binding shadows it), and used only as the object of member accesses. + * `exports` itself must be the global (never declared in the file) and is + * never reassigned. Other shapes answer null and keep their existing + * treatment. */ +import * as ts from "./ts7/syntax.js"; + +function unparen(e: ts.Expression): ts.Expression { + let x = e; + while (ts.isParenthesizedExpression(x)) x = x.expression; + return x; +} + +function isDeclarationName(id: ts.Identifier): boolean { + const p = id.parent; + return ( + ((ts.isVariableDeclaration(p) || + ts.isParameter(p) || + ts.isBindingElement(p) || + ts.isFunctionDeclaration(p) || + ts.isFunctionExpression(p) || + ts.isClassDeclaration(p) || + ts.isClassExpression(p)) && + p.name === id) || + (ts.isPropertyAccessExpression(p) && p.name === id) || + ((ts.isPropertyAssignment(p) || ts.isMethodDeclaration(p) || ts.isPropertyDeclaration(p)) && + p.name === id) + ); +} + +function isFunctionScope(n: ts.Node): boolean { + return ( + ts.isFunctionDeclaration(n) || + ts.isFunctionExpression(n) || + ts.isArrowFunction(n) || + ts.isMethodDeclaration(n) || + ts.isGetAccessorDeclaration(n) || + ts.isSetAccessorDeclaration(n) || + ts.isConstructorDeclaration(n) + ); +} + +function bindsName(name: ts.BindingName | undefined, text: string): boolean { + if (name === undefined) return false; + if (ts.isIdentifier(name)) return name.text === text; + for (const element of name.elements.nodes) { + if (!ts.isOmittedExpression(element) && bindsName(element.name, text)) return true; + } + return false; +} + +/** True when one of `statements` declares `text` lexically: a let/const, + * or a class or function declaration. */ +function declaresLexically(statements: readonly ts.Statement[], text: string): boolean { + for (const stmt of statements) { + if ( + ts.isVariableStatement(stmt) && + (stmt.declarationList.flags & ts.NodeFlags.BlockScoped) !== 0 && + stmt.declarationList.declarations.nodes.some((d) => bindsName(d.name, text)) + ) + return true; + if ((ts.isClassDeclaration(stmt) || ts.isFunctionDeclaration(stmt)) && stmt.name?.text === text) + return true; + } + return false; +} + +/** True when a function body declares `text` with `var` (or a function + * declaration in a nested block) anywhere outside nested functions. */ +function declaresVar(body: ts.Node, text: string): boolean { + let found = false; + ts.walkPreorder(body, (n) => { + if (found) return "stop"; + if (n === body) return undefined; + if (isFunctionScope(n)) { + if (ts.isFunctionDeclaration(n) && n.name?.text === text) found = true; + return "skip"; + } + if ( + ts.isVariableDeclarationList(n) && + (n.flags & ts.NodeFlags.BlockScoped) === 0 && + n.declarations.nodes.some((d) => bindsName(d.name, text)) + ) + found = true; + return undefined; + }); + return found; +} + +/** True when `scope` declares `text` itself, so references inside it name + * that binding instead of the module-level alias: a function's parameters, + * own name, `var`s and the let/const, class and function declarations of + * its body; a block's or case block's own let/const, class and function + * declarations; a catch binding; a for loop's let/const. A let/const in a + * nested block shadows only that block, which the walk reaches as its own + * scope. */ +function shadows(scope: ts.Node, text: string): boolean { + if (isFunctionScope(scope)) { + const fn = scope as ts.SignatureDeclaration; + if (fn.parameters.nodes.some((p) => bindsName(p.name, text))) return true; + if (ts.isFunctionExpression(scope) && scope.name?.text === text) return true; + const body = (scope as { body?: ts.Node }).body; + if (body === undefined || !ts.isBlock(body)) return false; + return declaresVar(body, text) || declaresLexically(body.statements.nodes, text); + } + if (ts.isBlock(scope)) return declaresLexically(scope.statements.nodes, text); + if (ts.isCaseBlock(scope)) + return declaresLexically( + scope.clauses.nodes.flatMap((clause) => clause.statements.nodes), + text, + ); + if (ts.isCatchClause(scope)) return bindsName(scope.variableDeclaration?.name, text); + if (ts.isForStatement(scope) || ts.isForInStatement(scope) || ts.isForOfStatement(scope)) { + const init = scope.initializer; + return ( + init !== undefined && + ts.isVariableDeclarationList(init) && + (init.flags & ts.NodeFlags.BlockScoped) !== 0 && + init.declarations.nodes.some((d) => bindsName(d.name, text)) + ); + } + if (ts.isClassExpression(scope)) return scope.name?.text === text; + return false; +} + +export function normalizeCjsExportsVar(sf: ts.SourceFile): string | null { + const source = sf.text; + if (!/\b(?:var|let|const)\s+[A-Za-z_$][\w$]*\s*=\s*exports\s*[;,\n]/.test(source)) return null; + if (!sf.fileName.endsWith(".cjs") && ts.isExternalModule(sf)) return null; + let alias: { name: string; stmt: ts.VariableStatement } | null = null; + for (const stmt of sf.statements.nodes) { + if (!ts.isVariableStatement(stmt)) continue; + const decls = stmt.declarationList.declarations.nodes; + if (decls.length !== 1) continue; + const decl = decls[0]!; + const init = decl.initializer !== undefined ? unparen(decl.initializer) : undefined; + if ( + ts.isIdentifier(decl.name) && + init !== undefined && + ts.isIdentifier(init) && + init.text === "exports" + ) { + if (alias !== null) return null; /* two aliases: keep the plain treatment */ + alias = { name: decl.name.text, stmt }; + } + } + if (alias === null) return null; + const name = alias.name; + const aliasDecl = alias.stmt.declarationList.declarations.nodes[0]!; + const edits: { start: number; end: number; text: string }[] = []; + let ok = true; + ts.walkPreorder(sf, (n) => { + if (!ok) return "stop"; + if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode) + return "skip"; + if ( + ts.isBinaryExpression(n) && + n.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(unparen(n.left)) && + (unparen(n.left) as ts.PropertyAccessExpression).name.text === "exports" && + ts.isIdentifier((unparen(n.left) as ts.PropertyAccessExpression).expression) && + ((unparen(n.left) as ts.PropertyAccessExpression).expression as ts.Identifier).text === + "module" + ) { + ok = false; /* module.exports replaced: the alias no longer names the exports */ + return "stop"; + } + // A scope that declares the alias's name again: its references name + // that binding, not the export object. + if ( + n !== sf && + (isFunctionScope(n) || + ts.isBlock(n) || + ts.isCaseBlock(n) || + ts.isCatchClause(n) || + ts.isForStatement(n) || + ts.isForInStatement(n) || + ts.isForOfStatement(n) || + ts.isClassExpression(n)) && + shadows(n, name) + ) + return "skip"; + if (!ts.isIdentifier(n)) return undefined; + if (n.text === "exports" && n !== aliasDecl.initializer) { + // `exports` declared in the file, or the export object replaced + if (isDeclarationName(n) && !ts.isPropertyAccessExpression(n.parent)) ok = false; + const p = n.parent; + if ( + ts.isBinaryExpression(p) && + p.left === n && + p.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) + ok = false; + return undefined; + } + if (n.text !== name) return undefined; + if (n === aliasDecl.name) return undefined; + const p = n.parent; + if (ts.isPropertyAccessExpression(p) && p.name === n) return undefined; + if ((ts.isPropertyAssignment(p) || ts.isMethodDeclaration(p)) && p.name === n) return undefined; + if (isDeclarationName(n)) { + ok = false; /* declared again (a shadowing binding): not a plain alias */ + return undefined; + } + if ( + (ts.isPropertyAccessExpression(p) || ts.isElementAccessExpression(p)) && + p.expression === n + ) { + edits.push({ start: n.getStart(sf), end: n.getEnd(), text: "exports" }); + return undefined; + } + ok = false; /* used as a value, assigned, or passed along */ + return undefined; + }); + if (!ok || edits.length === 0) return null; + edits.push({ start: alias.stmt.getStart(sf), end: alias.stmt.getEnd(), text: "" }); + edits.sort((a, b) => a.start - b.start); + let out = ""; + let at = 0; + for (const e of edits) { + out += source.slice(at, e.start) + e.text; + at = e.end; + } + return out + source.slice(at); +} diff --git a/packages/compiler/src/frontend/npm-static-rewrite-host.ts b/packages/compiler/src/frontend/npm-static-rewrite-host.ts index ee36f58d1..4e0c92e31 100644 --- a/packages/compiler/src/frontend/npm-static-rewrite-host.ts +++ b/packages/compiler/src/frontend/npm-static-rewrite-host.ts @@ -2,10 +2,16 @@ import { dirname, join, resolve as resolvePath } from "node:path"; import { cjsLexedExportsOfFile, cjsVisibleNames } from "./cjs-syntax.js"; import type { Ts7SourceParser } from "./ts7/source-parser.js"; -import { trackedDirectoryExists, trackedFileExists, trackedReadFile } from "./input-tracker.js"; +import { + isVirtualSource, + trackedDirectoryExists, + trackedFileExists, + trackedReadFile, +} from "./input-tracker.js"; import { resolveExports } from "./resolve.js"; import { packageNameOfSpecifier } from "./workspace-registry.js"; import { isBundlerCjsCandidate, rewriteBundlerCjsSyntax } from "./npm-static-rewrite-syntax.js"; +import { lowerBundledEsmInits } from "./npm-static-bundled-esm.js"; const NODE_REQUIRE_CONDITIONS = new Set(["require", "node", "default"]); @@ -134,7 +140,7 @@ function requireTargetEsModuleStamped( } catch { /* unlexable — keep probing */ } - if (src.includes("__toCommonJS(")) return true; + if (/__toCommonJS\d*\(/.test(src)) return true; const fwd = /module\.exports\s*=\s*require\(\s*["']([^"']+)["']\s*\)/.exec(src); if (fwd !== null) return requireTargetEsModuleStamped(parser, file, fwd[1]!, depth + 1); return false; @@ -159,13 +165,29 @@ function starTargetNames(parser: Ts7SourceParser, file: string): Set { } } -export function rewriteBundlerCjsWithParser( +export function lowerEsmInitsWithParser( parser: Ts7SourceParser, source: string, filePath: string, +): string | { degrade: string } | null { + if (!source.includes("__esm")) return null; + return lowerBundledEsmInits(parser.parse(filePath, source, "js"), filePath, { + requireTargetEsModuleStamped: (from, spec) => requireTargetEsModuleStamped(parser, from, spec), + }); +} + +export function rewriteBundlerCjsWithParser( + parser: Ts7SourceParser, + source: string, + filePath: string, + liveExportTable?: (filePath: string) => void, + lexerVisibleNames?: (filePath: string, names: readonly string[]) => void, ): string | { degrade: string } | null { if (!isBundlerCjsCandidate(source)) return null; return rewriteBundlerCjsSyntax(parser.parse(filePath, source, "js"), filePath, { + liveExportTable, + lexerVisibleNames, + requireOnly: isVirtualSource, requireTargetEsModuleStamped: (from, spec) => requireTargetEsModuleStamped(parser, from, spec), starTargetNames: (from, spec) => { const target = resolveRelativeCjs(from, spec); diff --git a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts index 7938471f3..348b04817 100644 --- a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts +++ b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts @@ -12,6 +12,73 @@ import { rewriteBundledFunctionImports } from "./npm-static-bundled-cjs.js"; export interface CjsRewriteHost { requireTargetEsModuleStamped(fromFile: string, specifier: string): boolean; starTargetNames(fromFile: string, specifier: string): Set; + /** Told when the rewritten file's tail export table stands for a live + * getter table the original installed before it could run another + * module's code (liveExportTableFirst): a module of a require cycle that + * captured the export object early observes exactly that table. */ + liveExportTable?: ((filePath: string) => void) | undefined; + /** True for a module only ever loaded through require() (a bundle's + * CommonJS chunk split into its own module): no ES-module importer + * exists, so Node's lexer-visible name set bounds nothing and every + * name the export plumbing defines is part of its surface. */ + requireOnly?: ((filePath: string) => boolean) | undefined; + /** Told the names Node's lexer detects in the original file that the + * rewritten text spells only as accessors (which the lexer skips). */ + lexerVisibleNames?: ((filePath: string, names: readonly string[]) => void) | undefined; +} + +/** True when esbuild's export object (`module.exports = __toCommonJS(T)` + * over a getter table `__export(T, {...})`) is installed before anything + * that can run another module's code, and nothing else writes the export + * object. Every statement before the installation is then a declaration, + * a directive, or the getter-table call (whose getters run only when + * read), and variable initializers evaluate no calls outside function + * bodies. The getters read live bindings, so the object a cycle partner + * captures early answers every later read exactly like the final table. */ +function liveExportTableFirst( + sf: ts.SourceFile, + installAt: number, + target: string, + exportTables: readonly { target: string }[], +): boolean { + if (!exportTables.some((t) => t.target === target)) return false; + let callsOutsideFunctions = false; + const noCalls = (root: ts.Node): boolean => { + callsOutsideFunctions = false; + ts.walkPreorder(root, (n) => { + if (callsOutsideFunctions) return "stop"; + if (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n)) + return "skip"; + if (ts.isCallExpression(n) || ts.isNewExpression(n) || ts.isTaggedTemplateExpression(n)) + callsOutsideFunctions = true; + return undefined; + }); + return !callsOutsideFunctions; + }; + let sawTable = false; + for (const stmt of sf.statements.nodes) { + if (stmt.getStart(sf) >= installAt) return sawTable; + if (ts.isFunctionDeclaration(stmt) || ts.isEmptyStatement(stmt)) continue; + if (ts.isExpressionStatement(stmt) && ts.isStringLiteral(stmt.expression)) continue; + if (ts.isVariableStatement(stmt)) { + if ( + stmt.declarationList.declarations.nodes.every( + (d) => ts.isIdentifier(d.name) && (d.initializer === undefined || noCalls(d.initializer)), + ) + ) + continue; + return false; + } + if (ts.isExpressionStatement(stmt) && ts.isCallExpression(stmt.expression)) { + const table = exportCallOf(stmt.expression); + if (table !== null && table.target === target) { + sawTable = true; + continue; + } + } + return false; + } + return false; } export function isBundlerCjsCandidate(source: string): boolean { @@ -24,24 +91,43 @@ export function isBundlerCjsCandidate(source: string): boolean { ); } +/** The helper a bundler-emitted name spells. esbuild suffixes a helper + * that a bundled module declares again with a counter (a bundled esbuild + * dist inside another bundle carries `__export2`, `__toCommonJS2`, …); the + * recognizers below verify each call's shape, so the counter carries no + * meaning beyond the name collision it avoided. */ +export function bundlerHelperBase(name: string): string { + return /^__[A-Za-z]+\d+$/.test(name) ? name.replace(/\d+$/, "") : name; +} + /** The last NAME of a callee: bare identifier or any member chain's final - * name (`tslib_1.__exportStar` → "__exportStar"). */ + * name (`tslib_1.__exportStar` → "__exportStar"), helper counters dropped. */ function calleeNameOf(call: ts.CallExpression): string | null { const callee = call.expression; - if (ts.isIdentifier(callee)) return callee.text; + if (ts.isIdentifier(callee)) return bundlerHelperBase(callee.text); if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) - return callee.name.text; + return bundlerHelperBase(callee.name.text); return null; } +/** True for an identifier spelling the named helper (counter-suffixed + * spellings included). */ +function isHelperIdent(node: ts.Node, base: string): node is ts.Identifier { + return ts.isIdentifier(node) && bundlerHelperBase(node.text) === base; +} + /** An entry the canonical table can carry. */ interface EntrySpec { /** The export name (may need string-key spelling). */ name: string; /** "ident": VALUE is a plain identifier spelled directly in the table. * "hoist": VALUE is an expression text hoisted to a tail const first. + * "getter": VALUE is a getter body's returned expression the table + * keeps as a live accessor (read on every access, like the original + * getter, so a lazily initialized module still initializes at the + * first read). * "any": the checked-dynamic fallback (`__scriptc_any`). */ - kind: "ident" | "hoist" | "any"; + kind: "ident" | "hoist" | "getter" | "any"; value?: string; } @@ -79,6 +165,26 @@ function chaseValue(e: ts.Expression): { kind: "ident" | "hoist"; text: string } return null; } +/** An object literal of side-effect-free member values (`exports.default + * = { context: context_api_1.context, metrics: metrics_api_1.metrics }`, + * tsc's default-export object): the tail const can build it once, like a + * chased value. Null for any other shape (spreads, methods, accessors, + * computed keys, or a value chaseValue rejects). */ +function chaseObjectLiteral(e: ts.Expression): { kind: "hoist"; text: string } | null { + let cur: ts.Expression = e; + while (ts.isParenthesizedExpression(cur)) cur = cur.expression; + if (!ts.isObjectLiteralExpression(cur)) return null; + for (const prop of cur.properties.nodes) { + if ( + !ts.isPropertyAssignment(prop) || + !(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) || + (chaseValue(prop.initializer) === null && chaseObjectLiteral(prop.initializer) === null) + ) + return null; + } + return { kind: "hoist", text: cur.getText() }; +} + /** The getter body's returned expression for the two descriptor getter * spellings (`get() { return X }` / `get: function () { return X }`) and * the arrow forms the esbuild table uses (`() => X`, `() => { return X }`). */ @@ -194,13 +300,8 @@ interface ToEsmPlan { * __copyProps(…, mod) call copying onto a conditional that tests * isNodeMode/__esModule and defines "default" with value mod. Comments and * formatting are free; the SHAPE is the contract. */ -function recognizedToEsmDecl(stmt: ts.Statement): boolean { - if (!ts.isVariableStatement(stmt)) return false; - const decls = stmt.declarationList.declarations.nodes; - if (decls.length !== 1) return false; - const d = decls[0]!; - if (!ts.isIdentifier(d.name) || d.name.text !== "__toESM" || d.initializer === undefined) - return false; +function recognizedToEsmDecl(d: ts.VariableDeclaration): boolean { + if (!isHelperIdent(d.name, "__toESM") || d.initializer === undefined) return false; let init: ts.Expression = d.initializer; while (ts.isParenthesizedExpression(init)) init = init.expression; if (!ts.isArrowFunction(init)) return false; @@ -218,12 +319,7 @@ function recognizedToEsmDecl(stmt: ts.Statement): boolean { expr = expr.right; } while (ts.isParenthesizedExpression(expr)) expr = expr.expression; - if ( - !ts.isCallExpression(expr) || - !ts.isIdentifier(expr.expression) || - expr.expression.text !== "__copyProps" - ) - return false; + if (!ts.isCallExpression(expr) || !isHelperIdent(expr.expression, "__copyProps")) return false; if (expr.arguments.nodes.length < 2) return false; const from = expr.arguments.nodes[1]!; if (!ts.isIdentifier(from) || from.text !== mod) return false; @@ -234,12 +330,7 @@ function recognizedToEsmDecl(stmt: ts.Statement): boolean { if (!condText.includes(isNodeMode) || !condText.includes("__esModule")) return false; let stamp: ts.Expression = to.whenTrue; while (ts.isParenthesizedExpression(stamp)) stamp = stamp.expression; - if ( - !ts.isCallExpression(stamp) || - !ts.isIdentifier(stamp.expression) || - stamp.expression.text !== "__defProp" - ) - return false; + if (!ts.isCallExpression(stamp) || !isHelperIdent(stamp.expression, "__defProp")) return false; if (stamp.arguments.nodes.length !== 3) return false; const nameArg = stamp.arguments.nodes[1]!; if (!ts.isStringLiteral(nameArg) || nameArg.text !== "default") return false; @@ -278,19 +369,23 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH degrade: reason, }); - const declStmts = sf.statements.nodes.filter( - (s) => - ts.isVariableStatement(s) && - s.declarationList.declarations.nodes.some( - (d) => ts.isIdentifier(d.name) && d.name.text === "__toESM", - ), - ); + // Top-level declarators of the helper, any statement shape (minified + // dists share one `var` among several helpers), by spelling. + const decls = new Map(); + for (const s of sf.statements.nodes) { + if (!ts.isVariableStatement(s)) continue; + for (const d of s.declarationList.declarations.nodes) { + if (!isHelperIdent(d.name, "__toESM")) continue; + const list = decls.get(d.name.text); + if (list === undefined) decls.set(d.name.text, [d]); + else list.push(d); + } + } const refs: ts.Identifier[] = []; const calls: ts.CallExpression[] = []; const collect = (n: ts.Node): void => { - if (ts.isIdentifier(n) && n.text === "__toESM") refs.push(n); - if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "__toESM") - calls.push(n); + if (isHelperIdent(n, "__toESM")) refs.push(n); + if (ts.isCallExpression(n) && isHelperIdent(n.expression, "__toESM")) calls.push(n); }; ts.walkPreorder(sf, collect); if (calls.length === 0 && refs.length === 0) return plan; @@ -298,15 +393,16 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH // a helper that escapes as a VALUE is outside the recognized shape. const callees = new Set(calls.map((c) => c.expression)); const declNames = new Set(); - for (const s of declStmts) { - for (const d of (s as ts.VariableStatement).declarationList.declarations.nodes) { - if (ts.isIdentifier(d.name) && d.name.text === "__toESM") declNames.add(d.name); - } - } + for (const list of decls.values()) for (const d of list) declNames.add(d.name); if (refs.some((r) => !callees.has(r) && !declNames.has(r))) return fail(TO_ESM_ESCAPE_DEGRADE); if (calls.length === 0) return plan; // declared but never called — dead helper - if (declStmts.length !== 1 || !recognizedToEsmDecl(declStmts[0]!)) - return fail(TO_ESM_SHAPE_DEGRADE); + // Every spelling a call uses must be declared exactly once, in the + // recognized shape. + for (const call of calls) { + const list = decls.get((call.expression as ts.Identifier).text); + if (list === undefined || list.length !== 1 || !recognizedToEsmDecl(list[0]!)) + return fail(TO_ESM_SHAPE_DEGRADE); + } /** Module-scope interop bindings: name → whether `.default` IS the * module (pad the access) rather than a member read of its `default`. */ @@ -467,16 +563,20 @@ export function rewriteBundlerCjsSyntax( // ESM bundles can contain closed CommonJS function factories. Recognize // those before refusing interop that needs a dynamic module namespace. - if (ts.isExternalModule(sf)) { + // TypeScript forces every `.cjs` file to be an external module + // (isFileForcedToBeModuleByFormat), yet Node always loads one as + // CommonJS: the extension, not that flag, decides the format there. + if (!filePath.endsWith(".cjs") && ts.isExternalModule(sf)) { const bundled = rewriteBundledFunctionImports(source, sf); if (bundled !== null) return bundled; - return source.includes("__toESM(") - ? { - degrade: - "its ES-module dist routes an external dependency through the __toESM " + - "bundler-interop helper, which cannot compile natively in ESM output", - } - : null; + if (!source.includes("__toESM(")) return null; + return { + degrade: source.includes("__commonJS") + ? "its ES-module bundle reaches bundled CommonJS modules through the __commonJS and " + + "__toESM bundler-interop helpers, which cannot compile natively in ESM output" + : "its ES-module dist routes an external dependency through the __toESM " + + "bundler-interop helper, which cannot compile natively in ESM output", + }; } // The __toESM interop pass (see the section header): wrapper call sites @@ -644,6 +744,11 @@ export function rewriteBundlerCjsSyntax( } const lexed = cjsLexedExportsOfFile(sf); + if (host.requireOnly?.(filePath) === true) { + for (const t of exportTables) for (const [name] of t.entries) lexed.exports.add(name); + for (const d of definePropEntries) lexed.exports.add(d.name); + for (const m of memberStmts) for (const name of m.names) lexed.exports.add(name); + } let emitTable = sawBundleShape && (lexed.exports.size > 0 || lexed.reexports.length > 0); const starSpecs: string[] = []; @@ -682,7 +787,14 @@ export function rewriteBundlerCjsSyntax( ...exportTables.filter((t) => t.target !== esbuildMainTarget), ]; for (const t of tablesOrdered) { - for (const [name, value] of t.entries) claim(name, classify(name, value)); + for (const [name, value] of t.entries) { + const spec = classify(name, value); + // The table feeding module.exports keeps a getter it cannot chase + // as a live accessor rather than a snapshot. + if (spec.kind === "any" && value !== null && t.target === esbuildMainTarget) + claim(name, { name, kind: "getter", value: value.getText() }); + else claim(name, spec); + } } for (const d of definePropEntries) claim(d.name, classify(d.name, d.value)); @@ -714,7 +826,7 @@ export function rewriteBundlerCjsSyntax( // anything else keeps the file untouched (the offender path owns // it: neutralizing would drop an evaluation, keeping it meets the // lowering's discard fence at load). - const chased = chaseValue(cur); + const chased = chaseValue(cur) ?? chaseObjectLiteral(cur); if (chased === null) { return toEsm.pads.length > 0 ? { degrade: TO_ESM_MIXED_DEGRADE } : null; } @@ -814,21 +926,36 @@ export function rewriteBundlerCjsSyntax( "__reExport", "__toESM", "__toCommonJS", + "__commonJS", + "__esm", "__exportStar", "__createBinding", "__setModuleDefault", "__importStar", "__importDefault", ]); + /* Helper declarators by name. A minified dist shares one `var` among + * several helpers (`var __getProtoOf=…,__hasOwnProp=…;`): each + * declarator is its own candidate, and the statement pads whole only + * once every declarator in it is dead. */ const helperDecls = new Map(); + const helperStmts: ts.VariableStatement[] = []; for (const stmt of sf.statements.nodes) { if (!ts.isVariableStatement(stmt)) continue; const decls = stmt.declarationList.declarations.nodes; - if (decls.length !== 1) continue; - const d = decls[0]!; - if (ts.isIdentifier(d.name) && HELPER_NAMES.has(d.name.text)) { - helperDecls.set(d.name.text, { start: stmt.getStart(sf), end: stmt.getEnd() }); + let anyHelper = false; + for (const d of decls) { + if (!ts.isIdentifier(d.name) || !HELPER_NAMES.has(bundlerHelperBase(d.name.text))) continue; + if (helperDecls.has(d.name.text)) continue; // redeclared: keep the first, never sweep twice + anyHelper = true; + helperDecls.set( + d.name.text, + decls.length === 1 + ? { start: stmt.getStart(sf), end: stmt.getEnd() } + : { start: d.getStart(sf), end: d.getEnd() }, + ); } + if (anyHelper && decls.length > 1) helperStmts.push(stmt); } // every identifier occurrence of a helper name outside its own // declarator name node, position-recorded for the span test @@ -862,8 +989,39 @@ export function rewriteBundlerCjsSyntax( } } } + // A swept declarator of a shared statement pads with its separating + // comma, and the statement pads whole once every declarator is dead: + // padding the bare declarator span would leave a dangling comma. + const declaratorSpans = new Set<{ start: number; end: number }>(); + const declaratorPads: { start: number; end: number }[] = []; + for (const stmt of helperStmts) { + const decls = stmt.declarationList.declarations.nodes; + const deadAt = decls.map((d) => { + if (!ts.isIdentifier(d.name) || !removed.has(d.name.text)) return false; + const span = helperDecls.get(d.name.text)!; + if (span.start !== d.getStart(sf)) return false; + declaratorSpans.add(span); + return true; + }); + if (deadAt.every((x) => x)) { + declaratorPads.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + continue; + } + // Dead declarators before the last live one take their trailing + // comma; the dead tail after it pads from that declarator's end. + const lastLive = deadAt.lastIndexOf(false); + for (let i = 0; i < decls.length; i++) { + if (!deadAt[i]) continue; + const d = decls[i]!; + declaratorPads.push( + i < lastLive + ? { start: d.getStart(sf), end: decls[i + 1]!.getStart(sf) } + : { start: decls[lastLive]!.getEnd(), end: d.getEnd() }, + ); + } + } neutralize.length = 0; - neutralize.push(...dead); + neutralize.push(...dead.filter((span) => !declaratorSpans.has(span)), ...declaratorPads); } /* ── emission ─────────────────────────────────────────────────────── */ @@ -875,6 +1033,15 @@ export function rewriteBundlerCjsSyntax( if (!emitTable) return text; // interop pads only — nothing appends + if ( + esbuildMainTarget !== null && + tablePos !== null && + memberStmts.length === 0 && + definePropEntries.length === 0 && + liveExportTableFirst(sf, tablePos, esbuildMainTarget, exportTables) + ) + host.liveExportTable?.(filePath); + const lines: string[] = [""]; const hasAnyFallback = [...entries.values()].some((s) => s.kind === "any"); if (hasAnyFallback) { @@ -906,6 +1073,8 @@ export function rewriteBundlerCjsSyntax( const v = `__scriptc_e${hoistN++}`; hoists.push(`const ${v} = ${dropInteropDefault(spec.value!)};`); plainParts.push(`${keyOf(spec.name)}: ${v},`); + } else if (spec.kind === "getter") { + plainParts.push(`get ${keyOf(spec.name)}() { return ${dropInteropDefault(spec.value!)}; },`); } else { plainParts.push(`${keyOf(spec.name)}: __scriptc_any,`); } @@ -931,5 +1100,12 @@ export function rewriteBundlerCjsSyntax( lines.push( `module.exports = {${spreadParts.join("")}${plainParts.join("")}${esModulePart}${memberParts.join("")}};`, ); + /* Node's lexer skips accessors (and stops reading an object literal at + * the first one), so a canonical table with accessor entries hides + * names the original showed it (esbuild's dead annotation): the host + * keeps the original's lexer answer, so ES-module importers link + * exactly as they do against the original. */ + if ([...entries.values()].some((spec) => spec.kind === "getter")) + host.lexerVisibleNames?.(filePath, [...lexed.exports]); return text + lines.join("\n") + "\n"; } diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index 3c8fa22e9..84bb2347a 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -13,7 +13,13 @@ import { registerWorkspacePackage, workspacePackageOfPath, } from "./workspace-registry.js"; -import { trackedExists, trackedReadFile, trackedRealpath } from "./input-tracker.js"; +import { + clearVirtualSources, + setVirtualSource, + trackedExists, + trackedReadFile, + trackedRealpath, +} from "./input-tracker.js"; let activePackages: ReadonlySet = new Set(); let declarationOverloads: ReadonlyMap = new Map(); @@ -28,14 +34,36 @@ const offenders = new Map(); * the host probes the same file many times, and the rewrite parses. */ const rewriteCache = new Map(); +/** Rewritten files whose tail export table stands for a live getter table + * installed before the file could run another module's code (the rewrite + * certifies it; require-cycle admission consults it). */ +const liveExportFiles = new Set(); + +export function npmStaticLiveExportTable(path: string): boolean { + return liveExportFiles.has(path.split("\\").join("/")); +} + +/** Names Node's lexer detects in a rewritten file's original text that + * its rewrite spells only as accessors (the lexer skips those). */ +const lexerNames = new Map(); + +export function npmStaticLexerVisibleNames(path: string): readonly string[] { + return lexerNames.get(path.split("\\").join("/")) ?? []; +} + export function setNpmStaticPackages>(packages: T): void { activePackages = new Set(packages); declarationOverloads = new Map(); declarationProperties = new Map(); offenders.clear(); + fileDegrades.clear(); rewriteCache.clear(); + liveExportFiles.clear(); + lexerNames.clear(); + clearVirtualSources(); untypedPkgCache.clear(); realpathProbed.clear(); + aliasNames.clear(); } export function setNpmStaticDeclarationOverloads( @@ -81,6 +109,21 @@ export function npmStaticOffenders(): ReadonlyMap { return offenders; } +/** Files whose bundler interop the rewrite could not finish this attempt, + * with their package and reason. A file refuses its package only when + * the compiled program can load it (npmStaticLoadableFiles): a package is + * judged by what its imported subpaths reach, not by every file it ships. */ +const fileDegrades = new Map(); + +export function npmStaticFileDegrades(): ReadonlyMap { + return fileDegrades; +} + +function reportFileDegrade(path: string, pkg: string, reason: string): void { + const key = path.split("\\").join("/"); + if (!fileDegrades.has(key)) fileDegrades.set(key, { pkg, reason }); +} + /** The DefinitelyTyped name mangling ("@scope/pkg" → "scope__pkg") — the * @types twin that must hide alongside the package's own declarations. */ function mangledTypesName(name: string): string { @@ -135,9 +178,32 @@ function shadowTargetOf(path: string): { pkg: string; viaTypes: boolean } | null if (mangledTypesName(pkg) === mangled) return { pkg, viaTypes: true }; } } + // An npm alias (`"x-updated": "npm:x@6.3.0"`) installs the package under + // another directory name: its own manifest names the package. + const aliased = aliasedPackageName(`${norm.slice(0, idx)}/node_modules/${dirName}`); + if (aliased !== null && aliased !== dirName && activePackages.has(aliased)) + return { pkg: aliased, viaTypes: false }; return null; } +/** The "name" a node_modules directory's own package.json declares, or + * null (cached per load). */ +const aliasNames = new Map(); +function aliasedPackageName(pkgDir: string): string | null { + const hit = aliasNames.get(pkgDir); + if (hit !== undefined) return hit; + let name: string | null = null; + try { + const text = trackedReadFile(`${pkgDir}/package.json`); + const declared = text === null ? undefined : (JSON.parse(text) as { name?: unknown }).name; + if (typeof declared === "string") name = declared; + } catch { + name = null; + } + aliasNames.set(pkgDir, name); + return name; +} + /** The opted-in package.json transform, applied identically in BOTH * resolution worlds (the tsgo host's fs shadow and resolve.ts): * 1. every types claim goes — the top-level "types"/"typings" fields @@ -362,7 +428,36 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow if (hit !== undefined) return hit ?? undefined; let rewritten: string | null = null; try { - const original = trackedReadFile(path); + let original = trackedReadFile(path); + // A bundle's CommonJS chunks move to their own modules first, so + // every later pass (and this shadow, for each chunk) sees plain + // CommonJS module text. + const split = original === null ? null : services.splitCjsChunks(original, path); + if (split !== null && "degrade" in split) { + reportFileDegrade(path, target.pkg, split.degrade); + original = null; + } else if (split !== null) { + for (const chunk of split.chunks) setVirtualSource(chunk.path, chunk.source); + original = split.text; + } + // Then its lazily initialized ES modules become run-once closures. + const esm = original === null ? null : services.lowerEsmInits(original, path); + if (esm !== null && typeof esm === "object") { + reportFileDegrade(path, target.pkg, esm.degrade); + original = null; + } else if (esm !== null) { + original = esm; + } + // A module exporting one of its functions reaches its members + // through that function. + const fnExports = original === null ? null : services.cjsFunctionExports(original, path); + if (fnExports !== null) original = fnExports; + // A module that names its export object through a variable reaches + // its members through `exports`. + const exportsVar = original === null ? null : services.cjsExportsVar(original, path); + if (exportsVar !== null) original = exportsVar; + const respelled = + split !== null || esm !== null || fnExports !== null || exportsVar !== null; if (original !== null) { const namepaths = services.jsDocNamepaths(path, original); const source = namepaths ?? original; @@ -385,9 +480,11 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow classFields?.text ?? source, path, + (live) => liveExportFiles.add(live.split("\\").join("/")), + (file, names) => lexerNames.set(file.split("\\").join("/"), names), ); if (answer !== null && typeof answer === "object") { - reportNpmStaticOffender(target.pkg, answer.degrade); + reportFileDegrade(path, target.pkg, answer.degrade); } else { rewritten = answer ?? @@ -395,7 +492,8 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow propertyProjected?.text ?? findWidened?.text ?? classFields?.text ?? - namepaths; + namepaths ?? + (respelled ? original : null); } } } catch { @@ -434,10 +532,32 @@ function looksUnminified(source: string): boolean { return source.length / lines.length <= 200; } +/** A whitespace/syntax-minified bundler dist whose interop helpers keep + * their names (`var __toESM=…,__toCommonJS=mod=>…`): the recognizers + * verify those shapes and refuse the package precisely when they cannot, + * so the minification heuristic need not refuse it up front. */ +function namedBundlerHelpers(source: string): boolean { + return /\b__(?:toCommonJS|commonJS|toESM)\d*\s*=\s*(?:\(|[A-Za-z_$][\w$]*\s*=>)/.test(source); +} + function hasTransformMarkers(source: string): boolean { return TRANSFORM_MARKERS.some((m) => source.includes(m)); } +/** Why the declarations at `typesFile` cannot stand for the runtime + * package `pkg`, or null when they are its DefinitelyTyped twin + * (`@types/`). The declarations only check the importing + * program; the JavaScript compiles with its inferred types, and an + * inferred surface the program does not typecheck against refuses the + * package afterwards, whatever release the twin describes. */ +export function definitelyTypedMismatch(pkg: string, typesFile: string): string | null { + const owner = npmPackageNameOf(typesFile.split("\\").join("/")); + const twin = `@types/${mangledTypesName(pkg)}`; + return owner === twin + ? null + : `its declared types come from ${owner ?? typesFile}, which is not ${pkg}'s DefinitelyTyped package ${twin}`; +} + /** Auto-mode eligibility for one candidate package: `typesFile` is what * the ordinary (types-first) resolution answered for the import, * `jsEntry` what the js-only resolution answers. Returns null when @@ -446,17 +566,23 @@ export function npmStaticIneligibleReason( pkgName: string, typesFile: string, jsEntry: string | null, + /** A dependency another package's JavaScript imports: no declarations + * check that importer, so the package need not ship its own. */ + untypedDependency = false, ): string | null { - if (npmPackageNameOf(typesFile) !== pkgName) { - return "its declared types come from a third-party @types package, not the package itself"; - } - if (!/\.d\.(ts|mts|cts)$/.test(typesFile)) { - return "it ships no own .d.ts declaration surface"; + if (!untypedDependency) { + if (npmPackageNameOf(typesFile) !== pkgName) { + return "its declared types come from a third-party @types package, not the package itself"; + } + if (!/\.d\.(ts|mts|cts)$/.test(typesFile)) { + return "it ships no own .d.ts declaration surface"; + } } if (jsEntry === null) return "no runtime JS entry resolves"; const source = trackedReadFile(jsEntry); if (source === null) return `its runtime entry ${jsEntry} cannot be read`; - if (!looksUnminified(source)) return "its shipped JS looks minified"; + if (!looksUnminified(source) && !namedBundlerHelpers(source)) + return "its shipped JS looks minified"; if (hasTransformMarkers(source)) { return "its shipped JS carries build-transform markers (bundled/transpiled dist)"; } diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 024c69d98..5081fc7be 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -10,11 +10,14 @@ import { checkPreflight, isNodeTypesPath, locOf, + npmStaticLoadableFiles, requiresOf, resolveNpmImport, type LoadResult, } from "./program.js"; import { + definitelyTypedMismatch, + npmStaticFileDegrades, npmStaticIneligibleReason, npmStaticOffenders, npmStaticPackageOfPath, @@ -217,13 +220,26 @@ function detectAutoPackages( chosen.push(runtimePkg); continue; } + // Both answers compile the JavaScript with inferred types; the + // declarations only check the importer, and an inferred surface + // the program does not typecheck against still refuses the package + // below. A DefinitelyTyped twin qualifies; a package without + // declarations qualifies as a dependency of another package, whose + // JavaScript importer has no declarations to check. + const refusal = + npm === null + ? owner === null && !sf.fileName.includes("/node_modules/") + ? "it ships no own .d.ts declaration surface" + : null + : definitelyTypedMismatch(runtimePkg, npm.typesFile); + if (refusal === null) { + chosen.push(runtimePkg); + continue; + } statuses.push({ package: runtimePkg, status: "refused", - detail: - npm === null - ? "it ships no own .d.ts declaration surface" - : `its declared types come from the separate ${npm.packageName} package, not the package itself`, + detail: refusal, hint: npmStaticOptInHint(runtimePkg, owner), }); continue; @@ -254,6 +270,7 @@ function detectAutoPackages( pkg, typesFile, jsEntry !== null && isJsSourceFileName(jsEntry.typesFile) ? jsEntry.typesFile : null, + mode === "lib" && isJsSourceFileName(typesFile) && fromFile.includes("/node_modules/"), ); if (reason === null) chosen.push(pkg); else @@ -458,6 +475,15 @@ function loadFrontend( const typecheckRefused = new Set(); while (effective.size > 0) { const reasons = new Map(npmStaticOffenders()); + // Bundler interop the rewrite could not finish refuses its package + // only where the compiled program can load the file. + if (npmStaticFileDegrades().size > 0) { + const loadable = npmStaticLoadableFiles(load.program, load.moduleOrder); + for (const [file, { pkg, reason }] of npmStaticFileDegrades()) { + if (!reasons.has(pkg) && (loadable === null || loadable.has(file))) + reasons.set(pkg, reason); + } + } for (const d of preflight) { const pkg = npmStaticPackageOfPath(d.loc.file); if (pkg !== null && !reasons.has(pkg)) diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 906d901fe..4bd895c44 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -68,6 +68,8 @@ import { isNpmStaticPackage, npmStaticActive, npmStaticFsShadow, + npmStaticLexerVisibleNames, + npmStaticLiveExportTable, npmStaticPackageOfPath, reportNpmStaticOffender, setNpmStaticDeclarationOverloads, @@ -1438,6 +1440,8 @@ export function requireBindingEarlyUse7( k: number, decl: ts.VariableDeclaration, precedingDecls: readonly ts.VariableDeclaration[], + /** A reference of the binding whose early evaluation is known exact. */ + exactEarlyRead?: (reference: ts.Identifier) => boolean, ): ts.Identifier | null { const checker = program.getTypeChecker(); const bindings = new Set(); @@ -1669,6 +1673,7 @@ export function requireBindingEarlyUse7( const sym = symbolOf(n); if (sym === undefined) return undefined; if (bindings.has(sym)) { + if (exactEarlyRead?.(n) === true) return undefined; hit = n; return "stop"; } @@ -3119,6 +3124,16 @@ function cjsCycleRefusal7( // anything else can change what a module of the cycle observes. let stray: ts.Node | null = null; const settled = new Set(stmts.slice(0, firstEntry).filter(exportStatement)); + // A bundler export table the rewrite certified (npmStaticLiveExportTable) + // stands for the live getter object the original installed before its + // first require: the tail table settles as if it ran there. + const tail = stmts[stmts.length - 1]; + if ( + tail !== undefined && + npmStaticLiveExportTable(m.fileName) && + cjsExportAssignmentOf(tail)?.kind === "table" + ) + settled.add(tail); ts.walkPreorder(m, (n) => { if (stray !== null) return "stop"; if (ts.isStatement(n) && settled.has(n)) return "skip"; @@ -3171,7 +3186,9 @@ function cjsCycleRefusal7( if (req.decl === null) continue; const dep = resolveImport7(program, m, req.spec, "require"); if (dep === null || !members.has(dep)) continue; - const reached = requireBindingEarlyUse7(program, m, stmts.length, req.decl, []); + const reached = requireBindingEarlyUse7(program, m, stmts.length, req.decl, [], (id) => + functionExportRead7(id, dep), + ); if (reached !== null) { return `the top-level code of ${m.fileName} can use its binding '${reached.text}' of a module of the cycle (${lineOf(reached)}) while the cycle initializes`; } @@ -3181,6 +3198,161 @@ function cjsCycleRefusal7( return null; } +/** True when `use` initializes a top-level const whose every other + * reference is a value of an export statement (`module.exports = { … }` + * or `exports.n = …`). */ +function publishedAlias7(use: ts.Node, decl: ts.Node | undefined): boolean { + if ( + decl === undefined || + !ts.isVariableDeclaration(decl) || + decl.initializer !== use || + !ts.isIdentifier(decl.name) + ) + return false; + const list = decl.parent; + const stmt = list?.parent; + const sf = stmt?.parent; + if ( + list === undefined || + (list.flags & ts.NodeFlags.Const) === 0 || + stmt === undefined || + !ts.isVariableStatement(stmt) || + sf === undefined || + !ts.isSourceFile(sf) + ) + return false; + const alias = decl.name; + const published = new Set(); + for (const st of sf.statements.nodes) { + const cjs = cjsExportAssignmentOf(st); + if (cjs === null) continue; + if (cjs.kind === "member") published.add(cjs.value); + else if (cjs.obj !== null) { + for (const p of cjs.obj.properties.nodes) { + if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) + published.add(p.initializer); + else if (ts.isShorthandPropertyAssignment(p)) published.add(p.name); + } + } + } + let kept = false; + ts.walkPreorder(sf, (n) => { + if (kept) return "stop"; + if (!ts.isIdentifier(n) || n === alias || n.text !== alias.text) return undefined; + const p = n.parent; + if ( + p !== undefined && + ((ts.isPropertyAccessExpression(p) && p.name === n) || + (ts.isPropertyAssignment(p) && p.name === n)) + ) + return undefined; + if (!published.has(n)) kept = true; + return undefined; + }); + return !kept; +} + +/** True for a plain read `binding.name` (not a call, not a write, not a + * value that is stored or passed on) of the cycle member `dep` whose + * export `name` is a top-level function declaration nothing reassigns. + * Every member settles its exports before entering the cycle, so Node + * reads that same function whenever the read runs, and the compiled read + * resolves to the declaration itself. */ +function functionExportRead7(id: ts.Identifier, dep: ts.SourceFile): boolean { + const access = id.parent; + if (!ts.isPropertyAccessExpression(access) || access.expression !== id) return false; + // The value the read yields: through parentheses and the right operand + // of a comma, so `(0, binding.name)(...)` is the call it spells. + let use: ts.Node = access; + for (;;) { + const p = use.parent; + if (ts.isParenthesizedExpression(p)) use = p; + else if ( + ts.isBinaryExpression(p) && + p.operatorToken.kind === ts.SyntaxKind.CommaToken && + p.right === use + ) + use = p; + else break; + } + const parent = use.parent; + // Only a use that neither runs the function nor keeps it is a read: a + // stored alias (`const f = binding.name`, `{ f: binding.name }`) or an + // argument can be called while the cycle initializes. A top-level const + // that only publishes it (`module.exports = { name: f }`, the static + // rewrite's re-export spelling) runs nothing: a module that reads the + // export back checks that read itself. + const observes = + publishedAlias7(use, parent) || + ts.isExpressionStatement(parent) || + ts.isTypeOfExpression(parent) || + ts.isVoidExpression(parent) || + (ts.isPrefixUnaryExpression(parent) && parent.operator === ts.SyntaxKind.ExclamationToken) || + (ts.isBinaryExpression(parent) && + (parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken || + parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken || + parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken || + parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken)) || + ((ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent)) && + parent.expression === use) || + (ts.isConditionalExpression(parent) && parent.condition === use); + if (!observes) return false; + const name = access.name.text; + // The exported value's identifier: a table entry or a member export. + const values: ts.Expression[] = []; + for (const stmt of dep.statements.nodes) { + if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue; + const e = stmt.expression; + if (e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) continue; + if (isModuleExportsAccess(e.left)) { + if (!ts.isObjectLiteralExpression(e.right)) return false; + for (const p of e.right.properties.nodes) { + if (ts.isSpreadAssignment(p)) return false; + if (ts.isShorthandPropertyAssignment(p) && p.name.text === name) + values.push(p.name as ts.Identifier); + else if ( + ts.isPropertyAssignment(p) && + (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && + p.name.text === name + ) + values.push(p.initializer); + } + } else if ( + ts.isPropertyAccessExpression(e.left) && + e.left.name.text === name && + ((ts.isIdentifier(e.left.expression) && e.left.expression.text === "exports") || + isModuleExportsAccess(e.left.expression)) + ) { + values.push(e.right); + } + } + const value = values.length === 1 ? values[0]! : null; + if (value === null || !ts.isIdentifier(value)) return false; + const fnName = value.text; + if ( + !dep.statements.nodes.some( + (st) => ts.isFunctionDeclaration(st) && st.name?.text === fnName && st.body !== undefined, + ) + ) + return false; + let rebound = false; + ts.walkPreorder(dep, (n) => { + if (rebound) return "stop"; + if ( + (ts.isBinaryExpression(n) && + ts.isIdentifier(n.left) && + n.left.text === fnName && + n.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + n.operatorToken.kind <= ts.SyntaxKind.LastAssignment) || + (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name) && n.name.text === fnName) || + (ts.isFunctionDeclaration(n) && n.name?.text === fnName && !ts.isSourceFile(n.parent)) + ) + rebound = true; + return undefined; + }); + return !rebound; +} + /* ── exact cyclic initialization ───────────────────────────────────────── * Cycles that fail the declaration-only rule still evaluate exactly like * Node when their shape is representable: the guarded %init calls already @@ -5349,12 +5521,18 @@ function cjsNamedImportLinkCheck( npmStaticDepSf7(program, from, spec, "require"); return dep !== null && isCjsJsFile7(dep, program) ? dep : null; }; + // A bundler rewrite spells some lexer-visible names as accessors. + const lexWithAccessorNames = (sf: ts.SourceFile): ReturnType => { + const lexed = cjsLexedExportsOfFile(sf); + for (const name of npmStaticLexerVisibleNames(sf.fileName)) lexed.exports.add(name); + return lexed; + }; const visible = (dep: ts.SourceFile, name: string): boolean => // `default` is the module.exports binding itself — always provided. // The program already owns native source ASTs; reuse them without // starting another syntax session or reparsing their source text. name === "default" || - cjsVisibleNames(dep, cjsLexedExportsOfFile, resolveCjsDep, lexMemo).has(name); + cjsVisibleNames(dep, lexWithAccessorNames, resolveCjsDep, lexMemo).has(name); /** The statement's resolved LOCAL CommonJS dependency, when it is an * import/re-export from one. */ @@ -5930,6 +6108,58 @@ export function orderedImportsOf( return out; } +/** The files a compiled program can load: the module order closed over + * every literal require() and import() call it contains (deferred and + * nested ones included) and, for opted-in package JavaScript, over every + * import and export declaration as well. A package file outside this set + * is reachable only through types, so whatever it spells cannot affect + * the compiled program. Null when a reachable file loads a computed + * specifier: then any file may load. */ +export function npmStaticLoadableFiles( + program: ts.Program, + moduleOrder: readonly ts.SourceFile[], +): Set | null { + const seen = new Set(); + const work: ts.SourceFile[] = []; + const add = (sf: ts.SourceFile | null | undefined): void => { + if (sf === null || sf === undefined || seen.has(sf) || sf.isDeclarationFile) return; + seen.add(sf); + work.push(sf); + }; + for (const sf of moduleOrder) add(sf); + let computed = false; + while (work.length > 0 && !computed) { + const sf = work.pop()!; + const packageJs = + npmStaticPackageOfPath(sf.fileName) !== null && isJsSourceFileName(sf.fileName); + const follow = (spec: string, kind: "import" | "require"): void => { + if (canonicalBuiltinModule(spec) !== null) return; + add(resolveImport7(program, sf, spec, kind) ?? npmStaticDepSf7(program, sf, spec, kind)); + }; + ts.walkPreorder(sf, (n) => { + if (computed) return "stop"; + if (ts.isCallExpression(n)) { + const isImport = n.expression.kind === ts.SyntaxKind.ImportKeyword; + const isRequire = ts.isIdentifier(n.expression) && n.expression.text === "require"; + if (!isImport && !isRequire) return undefined; + const arg = n.arguments.nodes[0]; + if (arg !== undefined && ts.isStringLiteralLike(arg)) + follow(arg.text, isImport ? "import" : "require"); + else if (packageJs || isImport) computed = true; + } else if ( + packageJs && + (ts.isImportDeclaration(n) || ts.isExportDeclaration(n)) && + n.moduleSpecifier !== undefined && + ts.isStringLiteral(n.moduleSpecifier) + ) { + follow(n.moduleSpecifier.text, "import"); + } + return undefined; + }); + } + return computed ? null : new Set([...seen].map((sf) => sf.fileName.split("\\").join("/"))); +} + /** The program source file a bare specifier reaches when it names an * opted-in --npm-static package (the shipped-JS entry preflight admitted * as a module edge), else null. No offender reporting here — preflight diff --git a/packages/compiler/src/frontend/resolve.ts b/packages/compiler/src/frontend/resolve.ts index 463400475..36470a643 100644 --- a/packages/compiler/src/frontend/resolve.ts +++ b/packages/compiler/src/frontend/resolve.ts @@ -18,6 +18,7 @@ import { dirname, isAbsolute, join, resolve } from "node:path"; import { isNpmStaticPackage, + npmStaticActive, npmStaticPackageOfPath, npmStaticTransformPkgJson, } from "./npm-static.js"; @@ -868,7 +869,11 @@ export function resolveBareModule( // never consulted — mirroring the shadowed world the tsgo host serves. const runtimeImport = mode === "runtime-js" || mode === "runtime-source"; const npmStatic = - mode === "js-only" || (!runtimeImport && mode !== "types-only" && isNpmStaticPackage(pkgName)); + mode === "js-only" || + (!runtimeImport && + mode !== "types-only" && + (isNpmStaticPackage(pkgName) || + (npmStaticActive() && isNpmStaticPackage(aliasedPackageOf(fromFile, pkgName))))); const conditions = runtimeImport ? RUNTIME_IMPORT_CONDITIONS : npmStatic @@ -970,6 +975,23 @@ export function resolveBareModule( return npmStatic ? passOnce("js") : (passOnce("types") ?? passOnce("js")); } +/** The package an npm alias directory installs (`"x-updated": + * "npm:x@6.3.0"` puts package x under node_modules/x-updated): the name + * the nearest node_modules//package.json declares when it + * differs from the directory name, else null. */ +function aliasedPackageOf(fromFile: string, dirName: string): string | null { + for (let dir = dirname(resolve(fromFile)); ;) { + const pkgDir = join(dir, "node_modules", dirName); + if (isDirectory(pkgDir)) { + const name = pkgJsonOf(pkgDir)?.name; + return typeof name === "string" && name !== dirName ? name : null; + } + const parent = dirname(dir); + if (parent === dir) return null; + dir = parent; + } +} + export function resolveWorkspaceSourceModule( fromFile: string, specifier: string, diff --git a/packages/compiler/src/frontend/services.ts b/packages/compiler/src/frontend/services.ts index ca4dd2b36..07bc8ca69 100644 --- a/packages/compiler/src/frontend/services.ts +++ b/packages/compiler/src/frontend/services.ts @@ -1,8 +1,15 @@ import { Ts7SourceParser, type Ts7SourceKind } from "./ts7/source-parser.js"; import { Ts7Host, type Ts7ApiFactory, type Ts7HostOptions } from "./ts7/program-host.js"; -import { rewriteBundlerCjsWithParser } from "./npm-static-rewrite-host.js"; +import { lowerEsmInitsWithParser, rewriteBundlerCjsWithParser } from "./npm-static-rewrite-host.js"; import { isBundlerCjsCandidate } from "./npm-static-rewrite-syntax.js"; +import { normalizeCjsFunctionExports } from "./npm-static-cjs-exports-alias.js"; +import { + mayBundleChunks, + splitBundledChunks, + type BundledChunkSplit, +} from "./npm-static-bundled-chunks.js"; import * as syntax from "./npm-static-declaration-syntax.js"; +import { normalizeCjsExportsVar } from "./npm-static-cjs-exports-var.js"; import type { SourceFile } from "./ts7/ast-types.js"; /** Evaluate an isolated, self-contained TypeScript callback under a finite @@ -55,10 +62,51 @@ export class FrontendServices { this.ensureOpen(); return new Ts7Host(this.createApi, { ...options, cwd: options?.cwd ?? this.cwd }); } - rewriteCjs(source: string, path: string): string | { degrade: string } | null { + rewriteCjs( + source: string, + path: string, + liveExportTable?: (path: string) => void, + lexerVisibleNames?: (path: string, names: readonly string[]) => void, + ): string | { degrade: string } | null { this.ensureOpen(); if (!isBundlerCjsCandidate(source)) return null; - return rewriteBundlerCjsWithParser(this.sourceParser(), source, path); + return rewriteBundlerCjsWithParser( + this.sourceParser(), + source, + path, + liveExportTable, + lexerVisibleNames, + ); + } + /** A CommonJS bundle's __commonJS chunks as their own modules (null: no + * chunks; a degrade reason: chunks the recognizer cannot move). */ + splitCjsChunks(source: string, path: string): BundledChunkSplit | { degrade: string } | null { + this.ensureOpen(); + if (!mayBundleChunks(source)) return null; + return splitBundledChunks(this.parse(path, source, "js"), path); + } + /** A CommonJS bundle's lazily initialized ES modules (__esm) lowered to + * run-once closures (null: none; a degrade reason: shapes the rewrite + * cannot lower). */ + lowerEsmInits(source: string, path: string): string | { degrade: string } | null { + this.ensureOpen(); + if (!source.includes("__esm")) return null; + return lowerEsmInitsWithParser(this.sourceParser(), source, path); + } + /** A CommonJS module whose export object is one of its functions, with + * its member accesses respelled through that function (null: none). */ + cjsFunctionExports(source: string, path: string): string | null { + this.ensureOpen(); + if (!source.includes("module.exports")) return null; + return normalizeCjsFunctionExports(this.parse(path, source, "js")); + } + /** A CommonJS module that names its export object through a variable + * (`var ip = exports`), its member accesses respelled through `exports` + * (null: none). */ + cjsExportsVar(source: string, path: string): string | null { + this.ensureOpen(); + if (!source.includes("exports")) return null; + return normalizeCjsExportsVar(this.parse(path, source, "js")); } nullableClassFields(path: string, source: string): syntax.NpmStaticOverloadRewrite | null { return syntax.applyNpmStaticNullableClassFields(this.parse(path, source, "js"), source); diff --git a/packages/compiler/src/frontend/ts7/ast-node.ts b/packages/compiler/src/frontend/ts7/ast-node.ts index 92ab5e4b8..3760fff8d 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.ts @@ -19,13 +19,22 @@ import { SyntaxKind, NodeFlags } from "./enums.js"; /** A list of child nodes with the list's own position, the shape of tsc-ts's * NodeList. The file keeps the nodes array, not this object: each read of a * list field returns a new AstNodeList over the same array, so `nodes` keeps - * its identity and a decoded file holds no list objects. */ + * its identity and a decoded file holds no list objects. The position is + * decoded from the wire when it is read; most readers only use `nodes`. */ export class AstNodeList { constructor( - readonly pos: number, - readonly end: number, + private readonly wire: AstWireFile, + private readonly index: number, readonly nodes: readonly T[], ) {} + + get pos(): number { + return this.wire.pos(this.index) >>> 0; + } + + get end(): number { + return this.wire.end(this.index) >>> 0; + } } /** One immutable AST response owns one object per node and one array per @@ -83,7 +92,7 @@ export class AstFile { this.lists[index] = nodes; this.materialized?.(); } - return new AstNodeList(this.wire.pos(index) >>> 0, this.wire.end(index) >>> 0, nodes); + return new AstNodeList(this.wire, index, nodes); } namedChild(node: AstNode, name: string): number { diff --git a/packages/compiler/src/frontend/ts7/checker-backend.test.ts b/packages/compiler/src/frontend/ts7/checker-backend.test.ts new file mode 100644 index 000000000..e37357afa --- /dev/null +++ b/packages/compiler/src/frontend/ts7/checker-backend.test.ts @@ -0,0 +1,79 @@ +import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { checkerBackend, tscTsExecutable, tscTsPlatformPackage } from "./checker-backend.js"; + +const roots: string[] = []; + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +function tempRoot(): string { + const root = realpathSync(mkdtempSync(join(tmpdir(), "scriptc-checker-"))); + roots.push(root); + return root; +} + +function writePackage(directory: string, name: string, executable?: string): void { + mkdirSync(directory, { recursive: true }); + writeFileSync(join(directory, "package.json"), JSON.stringify({ name })); + if (executable === undefined) return; + mkdirSync(join(directory, "lib"), { recursive: true }); + writeFileSync(join(directory, "lib", executable), ""); +} + +test("tsc-ts is the default checker except on Windows, and SCRIPTC_CHECKER selects one", () => { + expect(checkerBackend({}, "linux")).toBe("tsc-ts"); + expect(checkerBackend({ SCRIPTC_CHECKER: "" }, "linux")).toBe("tsc-ts"); + expect(checkerBackend({}, "darwin")).toBe("tsc-ts"); + expect(checkerBackend({}, "win32")).toBe("tsgo"); + expect(checkerBackend({ SCRIPTC_CHECKER: "" }, "win32")).toBe("tsgo"); + expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" }, "linux")).toBe("tsc-ts"); + expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" }, "win32")).toBe("tsc-ts"); + expect(checkerBackend({ SCRIPTC_CHECKER: "tsgo" }, "linux")).toBe("tsgo"); + expect(() => checkerBackend({ SCRIPTC_CHECKER: "tsc" })).toThrow( + 'SCRIPTC_CHECKER must be tsgo or tsc-ts, not "tsc"', + ); +}); + +test("tsc-ts platform packages are named for the host", () => { + expect(tscTsPlatformPackage("linux", "x64", false)).toBe("@tsc-ts/linux-x64-gnu"); + expect(tscTsPlatformPackage("linux", "arm64", true)).toBe("@tsc-ts/linux-arm64-musl"); + expect(tscTsPlatformPackage("darwin", "arm64", false)).toBe("@tsc-ts/darwin-arm64"); + expect(tscTsPlatformPackage("win32", "x64", false)).toBe("@tsc-ts/win32-x64-msvc"); +}); + +test("the tsc-ts executable is found next to an npm install of tsc-ts", () => { + const root = tempRoot(); + writePackage(join(root, "node_modules/tsc-ts"), "tsc-ts"); + writePackage(join(root, "node_modules/@tsc-ts/linux-x64-gnu"), "@tsc-ts/linux-x64-gnu", "tsc-ts"); + mkdirSync(join(root, "src/deep"), { recursive: true }); + expect(tscTsExecutable([join(root, "src/deep")], "@tsc-ts/linux-x64-gnu", "linux")).toBe( + join(root, "node_modules/@tsc-ts/linux-x64-gnu/lib/tsc-ts"), + ); +}); + +test("the tsc-ts executable is found in a pnpm install of tsc-ts", () => { + const root = tempRoot(); + const store = join(root, "node_modules/.pnpm/tsc-ts@0.0.3/node_modules"); + writePackage(join(store, "tsc-ts"), "tsc-ts"); + writePackage(join(store, "@tsc-ts/win32-x64-msvc"), "@tsc-ts/win32-x64-msvc", "tsc-ts.exe"); + mkdirSync(join(root, "app/node_modules"), { recursive: true }); + symlinkSync(join(store, "tsc-ts"), join(root, "app/node_modules/tsc-ts")); + expect(tscTsExecutable([join(root, "app")], "@tsc-ts/win32-x64-msvc", "win32")).toBe( + join(store, "@tsc-ts/win32-x64-msvc/lib/tsc-ts.exe"), + ); +}); + +test("a missing tsc-ts install or platform package is an error", () => { + const root = tempRoot(); + expect(() => tscTsExecutable([root], "@tsc-ts/linux-x64-gnu", "linux")).toThrow( + "the tsc-ts package is not installed", + ); + writePackage(join(root, "node_modules/tsc-ts"), "tsc-ts"); + expect(() => tscTsExecutable([root], "@tsc-ts/linux-x64-gnu", "linux")).toThrow( + "has no @tsc-ts/linux-x64-gnu executable", + ); +}); diff --git a/packages/compiler/src/frontend/ts7/checker-backend.ts b/packages/compiler/src/frontend/ts7/checker-backend.ts new file mode 100644 index 000000000..14418cc94 --- /dev/null +++ b/packages/compiler/src/frontend/ts7/checker-backend.ts @@ -0,0 +1,111 @@ +import { existsSync, realpathSync, statSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** The TypeScript 7 API servers a compile can check with: tsc-ts, the default, + * and tsgo from the `typescript` package, selected with `SCRIPTC_CHECKER=tsgo`. + * Both serve the same API and give the same results. On Windows the default is + * tsgo: the compiler connects to the server over a named pipe there, and tsc-ts's + * API server has no named-pipe transport yet. */ +export type CheckerBackend = "tsgo" | "tsc-ts"; + +export function checkerBackend( + env: NodeJS.ProcessEnv = process.env, + platform: string = process.platform, +): CheckerBackend { + const value = env["SCRIPTC_CHECKER"]; + if (value === undefined || value === "") return platform === "win32" ? "tsgo" : "tsc-ts"; + if (value === "tsc-ts") return "tsc-ts"; + if (value === "tsgo") return "tsgo"; + throw new Error(`SCRIPTC_CHECKER must be tsgo or tsc-ts, not ${JSON.stringify(value)}`); +} + +/** tsc-ts's platform package for a host: `@tsc-ts/linux-x64-gnu`, + * `@tsc-ts/darwin-arm64`, `@tsc-ts/win32-x64-msvc`, and so on. */ +export function tscTsPlatformPackage( + platform: string, + architecture: string, + musl: boolean, +): string { + const abi = + platform === "linux" ? (musl ? "-musl" : "-gnu") : platform === "win32" ? "-msvc" : ""; + return `@tsc-ts/${platform}-${architecture}${abi}`; +} + +let hostMusl: boolean | undefined; + +/** Whether this process runs on musl, the libc of tsc-ts's `-musl` Linux packages: Node reports no glibc + * runtime version there. The libc cannot change within a process, and a report is costly to generate. */ +export function hostIsMusl(): boolean { + if (process.platform !== "linux") return false; + if (hostMusl === undefined) { + const report = process.report?.getReport() as + | { header?: { glibcVersionRuntime?: unknown } } + | undefined; + hostMusl = typeof report?.header?.glibcVersionRuntime !== "string"; + } + return hostMusl; +} + +/** The checker's part of an executable cache key: the identity of the tsc-ts + * executable, or undefined for tsgo, whose keys predate the choice. */ +export function checkerCacheIdentity(env: NodeJS.ProcessEnv = process.env): string | undefined { + if (checkerBackend(env) === "tsgo") return undefined; + const path = hostTscTsExecutable(); + const info = statSync(path); + return JSON.stringify({ + path, + canonical: realpathSync(path), + dev: info.dev, + ino: info.ino, + size: info.size, + mtime: info.mtimeMs, + ctime: info.ctimeMs, + }); +} + +/** The Node host's tsc-ts executable: that of the `tsc-ts` package installed + * with this compiler, or else for the working directory. */ +export function hostTscTsExecutable(): string { + return tscTsExecutable( + [dirname(fileURLToPath(import.meta.url)), process.cwd()], + tscTsPlatformPackage(process.platform, process.arch, hostIsMusl()), + process.platform, + ); +} + +/** The tsc-ts executable of the `tsc-ts` package in the nearest + * `node_modules` above each of `starts`, in order. The platform package is + * installed next to `tsc-ts` (npm) or in the directory that holds it (pnpm). */ +export function tscTsExecutable( + starts: readonly string[], + platformPackage: string, + platform: string, +): string { + const name = platform === "win32" ? "tsc-ts.exe" : "tsc-ts"; + for (const start of starts) { + const packageRoot = installedPackage(start, "tsc-ts"); + if (packageRoot === undefined) continue; + const root = realpathSync(packageRoot); + for (const candidate of [ + join(root, "node_modules", platformPackage), + join(dirname(root), platformPackage), + ]) { + const executable = join(candidate, "lib", name); + if (existsSync(executable)) return executable; + } + throw new Error(`tsc-ts checker: ${root} has no ${platformPackage} executable`); + } + throw new Error("tsc-ts checker: the tsc-ts package is not installed"); +} + +function installedPackage(start: string, name: string): string | undefined { + let directory = resolve(start); + for (;;) { + const candidate = join(directory, "node_modules", name); + if (existsSync(join(candidate, "package.json"))) return candidate; + const parent = dirname(directory); + if (parent === directory) return undefined; + directory = parent; + } +} diff --git a/packages/compiler/src/frontend/ts7/rpc-api.ts b/packages/compiler/src/frontend/ts7/rpc-api.ts index a6ae609d4..a21ba6279 100644 --- a/packages/compiler/src/frontend/ts7/rpc-api.ts +++ b/packages/compiler/src/frontend/ts7/rpc-api.ts @@ -1,6 +1,7 @@ import { createRequire } from "node:module"; import { dirname, join } from "node:path"; import { Ts7Api as ConnectedTs7Api } from "./api.js"; +import { checkerBackend, hostTscTsExecutable } from "./checker-backend.js"; import { Ts7RpcClient } from "./rpc-client.js"; import { TS7_FILE_SYSTEM_CALLBACKS, type Ts7FileSystem } from "./rpc-filesystem.js"; import { spawnTs7Wire } from "./rpc-process.js"; @@ -12,8 +13,15 @@ const { default: getExePath } = require(join(packageRoot, "lib/getExePath.js")) }; /** Platform-package discovery and process creation remain at the host - * boundary. The session itself uses no SDK implementation or Node process. */ + * boundary. The session itself uses no SDK implementation or Node process. + * The executable is tsc-ts's (`hostTscTsExecutable`), or tsgo's with + * `SCRIPTC_CHECKER=tsgo`. */ export function ts7Executable(): string { + return checkerBackend() === "tsgo" ? getExePath() : hostTscTsExecutable(); +} + +/** tsgo's executable, from the `typescript` package, whichever checker compiles. */ +export function tsgoExecutable(): string { return getExePath(); } diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts index cb05db50c..5c5f2e2fc 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts @@ -6,7 +6,7 @@ import { API } from "typescript/unstable/sync"; import type { Node as SdkNode } from "typescript/unstable/ast"; import { AstNode } from "./ast-node.js"; import { Ts7RpcClient } from "./rpc-client.js"; -import { ts7Executable } from "./rpc-api.js"; +import { tsgoExecutable } from "./rpc-api.js"; import { spawnTs7Wire } from "./rpc-process.js"; import { Ts7Session } from "./session.js"; import { SyntaxKind } from "./enums.js"; @@ -45,8 +45,9 @@ beforeAll(() => { files: [file], }), ); + // Compared with the pinned SDK, which runs tsgo. session = new Ts7Session( - new Ts7RpcClient(spawnTs7Wire(ts7Executable(), ["--api", "--cwd", directory])), + new Ts7RpcClient(spawnTs7Wire(tsgoExecutable(), ["--api", "--cwd", directory])), ); oracle = new API({ cwd: directory }); }); diff --git a/packages/compiler/src/frontend/ts7/session.test.ts b/packages/compiler/src/frontend/ts7/session.test.ts index ceaceb113..522b72fe6 100644 --- a/packages/compiler/src/frontend/ts7/session.test.ts +++ b/packages/compiler/src/frontend/ts7/session.test.ts @@ -11,7 +11,7 @@ import { type Ts7FileSystem, } from "./rpc-filesystem.js"; import { spawnTs7Wire } from "./rpc-process.js"; -import { ts7Executable } from "./rpc-api.js"; +import { tsgoExecutable } from "./rpc-api.js"; import { Ts7Session, Ts7SessionProject, Ts7SessionSnapshot } from "./session.js"; import { AstNode } from "./ast-node.js"; @@ -25,9 +25,10 @@ const fallback: Ts7FileSystem = { getAccessibleEntries: () => undefined, }; +// These tests compare the session with the pinned SDK, which runs tsgo, so the session runs tsgo too. function transport(cwd: string, fs: Ts7FileSystem = fallback, timing = false) { const rpc = new Ts7RpcClient( - spawnTs7Wire(ts7Executable(), [ + spawnTs7Wire(tsgoExecutable(), [ "--api", "--cwd", cwd, diff --git a/packages/compiler/src/frontend/ts7/source-parser.test.ts b/packages/compiler/src/frontend/ts7/source-parser.test.ts index 1fae05ff6..d5b54f080 100644 --- a/packages/compiler/src/frontend/ts7/source-parser.test.ts +++ b/packages/compiler/src/frontend/ts7/source-parser.test.ts @@ -208,3 +208,30 @@ test("JSDoc callable types cannot replace method body views", () => { expect(member.body?.parent).toBe(member); } }); + +test("a parser starts a new server once it has parsed its source budget, and earlier results stay usable", () => { + const directory = mkdtempSync( + join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-source-parser-"), + ); + directories.push(directory); + const servers: Ts7Api[] = []; + const parser = new Ts7SourceParser( + (options) => { + const api = new Ts7Api(options); + servers.push(api); + return api; + }, + directory, + 64, + ); + parsers.push(parser); + const first = parser.parse("a.ts", `export const a = ${"1 + ".repeat(20)}1;`, "ts"); + expect(servers).toHaveLength(1); + const second = parser.parse("b.ts", "export const b = 2;", "ts"); + expect(servers).toHaveLength(2); + const third = parser.parse("b.ts", "export const b = 3;", "ts"); + expect(servers).toHaveLength(2); + expect(first.statements.nodes[0]!.getText()).toBe(`export const a = ${"1 + ".repeat(20)}1;`); + expect(second.statements.nodes[0]!.getText()).toBe("export const b = 2;"); + expect(third.statements.nodes[0]!.getText()).toBe("export const b = 3;"); +}); diff --git a/packages/compiler/src/frontend/ts7/source-parser.ts b/packages/compiler/src/frontend/ts7/source-parser.ts index 38605cd64..f9029774c 100644 --- a/packages/compiler/src/frontend/ts7/source-parser.ts +++ b/packages/compiler/src/frontend/ts7/source-parser.ts @@ -7,12 +7,21 @@ import { tsgoPath } from "./session-path.js"; export type Ts7SourceKind = "js" | "jsx" | "ts" | "tsx"; export type Ts7SourceApiFactory = (options: { cwd: string; fs: Ts7FileSystem }) => Ts7Api; +/** The source text, in UTF-16 code units, one parser server parses before a + * new server replaces it. tsc-ts keeps the syntax trees it builds for the + * life of its process, so a server that parses many files grows with them; a + * new one starts empty. Results are detached ASTs and outlive the server that + * made them. */ +const SERVER_SOURCE_BUDGET = 256 * 1024; + /** A syntax-only session with a closed, in-memory filesystem. Parsing a * source string never reads imports, project configuration, or libraries * from disk. The returned AST owns its wire data and outlives the session. * Only one project and one input are retained between requests. */ export class Ts7SourceParser { - private readonly api: Ts7Api; + private api: Ts7Api; + private readonly createApi: Ts7SourceApiFactory; + private parsedSource = 0; private readonly files = new Map(); private readonly configPath: string; private previousPath: string | undefined; @@ -21,10 +30,19 @@ export class Ts7SourceParser { private closed = false; private readonly cwd: string; - constructor(createApi: Ts7SourceApiFactory, cwd = process.cwd()) { + constructor( + createApi: Ts7SourceApiFactory, + cwd = process.cwd(), + private readonly sourceBudget = SERVER_SOURCE_BUDGET, + ) { this.cwd = resolve(cwd); this.configPath = tsgoPath(resolve(this.cwd, "__scriptc-source-parser.tsconfig.json")); - this.api = createApi({ + this.createApi = createApi; + this.api = this.connect(); + } + + private connect(): Ts7Api { + return this.createApi({ cwd: this.cwd, fs: { readFile: (path) => this.files.get(tsgoPath(path)) ?? null, @@ -61,7 +79,14 @@ export class Ts7SourceParser { this.previousFile !== undefined ) return this.previousFile; - const previousPath = this.previousPath; + let previousPath = this.previousPath; + if (this.parsedSource >= this.sourceBudget) { + this.api.close(); + this.api = this.connect(); + this.parsedSource = 0; + previousPath = undefined; + } + this.parsedSource += source.length; this.files.clear(); this.files.set(path, source); this.files.set( diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 947791fe6..8d6ef2980 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -6,6 +6,7 @@ import type { IrRecordShape, IrType, IrUnionDef, IrUnionDiscriminant } from "../ import { BYTES_ELEMENT_NAME, arrayOf, + isDynTypedRefType, BOOL, bytesOf, canConvertToDyn, @@ -125,6 +126,14 @@ export function jsOpenObjectType( : type; } +/** The modules whose RequestOptions interface maps to the checked dynamic. */ +const HTTP_REQUEST_OPTION_MODULES: ReadonlySet = new Set([ + "http", + "node:http", + "https", + "node:https", +]); + /** The WebCrypto interfaces whose values are checked-dynamic: the crypto * global, SubtleCrypto, CryptoKey handles and the algorithm dictionaries * (scr_webcrypto.c). */ @@ -1346,6 +1355,26 @@ function isWorkerHandleSymbol( /** The `{ message: any }` object type (or its union with undefined) that * worker_threads declares as receiveMessageOnPort's result. */ +/** A union of dns lookup option bags with numbers and nullish values that + * includes LookupAllOptions (whose `all: true` arm has no record form). */ +function isLookupOptionsUnion(type: ts.Type, ctx: Pick): boolean { + if (!type.isUnionType()) return false; + let all = false; + for (const member of ts.constituentTypes(type)) { + if (member.flags & (ts.TypeFlags.NumberLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined)) + continue; + const symbol = member.getSymbol(); + if ( + symbol === undefined || + !/^Lookup(One|All)?Options$/.test(symbol.name) || + !symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) + ) + return false; + if (symbol.name === "LookupAllOptions") all = true; + } + return all; +} + function isReceiveMessageResult( type: ts.Type, ctx: Pick, @@ -1536,6 +1565,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // worker_threads.receiveMessageOnPort's `{ message: any } | undefined`: // the runtime's { message } object, a checked-dynamic value. if (isReceiveMessageResult(type, ctx)) return DYN; + // The lookup-function option union (`LookupOneOptions | LookupAllOptions + // | number | undefined`): with a LookupAllOptions arm it is an option bag + // dns.lookup reads at run time, so it rides checked-dynamic storage. + if (isLookupOptionsUnion(type, ctx)) return DYN; // @types/node spells child_process.ExecFileException as an intersection // type alias over two Error-derived metadata interfaces. The value is // still one ordinary Error; recognize the owned alias before generic @@ -1590,6 +1623,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { contextResolutions++; return DYN; } + // Readonly over a program class (Object.freeze's result type) is the + // same instance. + const readonlyClass = readonlyClassType(type, ctx); + if (readonlyClass !== null) return readonlyClass; if (resolveTypeParam && type.flags & ts.TypeFlags.TypeParameter) { const bound = resolveTypeParam(type); if (bound) { @@ -2498,6 +2535,26 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // spawn consumes a presence-sensitive option bag, including optional // stdio and env values. Keep it checked-dynamic for native normalization. if (isStdlibInterface("SpawnOptions")) return DYN; + // dns.LookupAllOptions (`all: true`, the overload whose callback takes + // an address list): a lookup option bag dns.lookup reads at run time. + // It rides checked-dynamic storage, so the lookup function type that + // takes `LookupOneOptions | LookupAllOptions | number` compiles. + if (isStdlibInterface("LookupAllOptions")) return DYN; + // http.RequestOptions / https.RequestOptions as a declared type: an + // option record whose members include functions, agents and signals. + // Values ride checked-dynamic storage, like SpawnOptions. + if ( + psym?.name === "RequestOptions" && + psym.declarations.some((d) => { + if (!ts.isInterfaceDeclaration(d) || !ctx.isStdlibFile(d.getSourceFile())) return false; + for (let p: ts.Node | undefined = d.parent; p !== undefined; p = p.parent) { + if (ts.isModuleDeclaration(p) && ts.isStringLiteral(p.name)) + return HTTP_REQUEST_OPTION_MODULES.has(p.name.text); + } + return false; + }) + ) + return DYN; // exactly like npm-declared types. Static fetch has one deliberately // narrower native representation: Response is an opaque checked- @@ -4371,10 +4428,13 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null // params). Declaration files keep their existing paths: library // callback slots such as String#replace's `(substring, ...args: any[])` // have intrinsic lowerings that inspect the unmapped type. + // node:test's default mock function type, `(...args: any[]) => + // undefined` (the type parameter default of mock.fn), takes the same + // ABI: mock.fn() without an original records whatever its callers pass. if ( rest && decl !== undefined && - !decl.getSourceFile().isDeclarationFile && + (!decl.getSourceFile().isDeclarationFile || isNodeTestDefaultFunctionParam(decl, ctx)) && (pt?.kind === "dyn" || (!pt && isAnyArrayType(checker.getTypeOfSymbol(p), checker))) ) { dynRest = true; @@ -5106,6 +5166,19 @@ function mentionsMappedParams( return false; } +function readonlyClassType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { + const alias = type.getAliasSymbol(); + if (alias?.name !== "Readonly") return null; + const args = type.getAliasTypeArguments(); + if (args.length !== 1 || !args[0]) return null; + const isLibAlias = alias.declarations.some( + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ); + if (!isLibAlias) return null; + const mapped = mapType(args[0], ctx); + return mapped !== null && isDynTypedRefType(mapped) ? mapped : null; +} + function mapGenericUtilityAlias(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { const { resolveTypeParam, shapes, unions } = ctx; if (!resolveTypeParam) return null; @@ -5854,6 +5927,7 @@ interface RecordShapeParts { fields: { name: string; type: IrType }[]; indexValue?: IrType; declaredOrder?: string[]; + accessorOrder?: string[]; } function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { @@ -5890,15 +5964,14 @@ function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { // (generic type parameters, mixin instantiations) cannot intern by // checker-type identity — fence recursive generic-open shapes. if (contextResolutions !== sensitivityAtEntry) return null; - return { - kind: "record", - shapeId: shapes.finalizeRecursive( - widened, - inner.fields, - inner.indexValue, - inner.declaredOrder, - ), - }; + const recursive = shapes.finalizeRecursive( + widened, + inner.fields, + inner.indexValue, + inner.declaredOrder, + ); + adoptAccessorOrder(shapes.get(recursive), inner.accessorOrder); + return { kind: "record", shapeId: recursive }; } // A record with no typed payload contract is a view of an existing // object, including across parameters and returns. Building a native @@ -5913,12 +5986,14 @@ function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { return DYN; const covered = coveredOptionalFieldsShape(widened, inner, ctx); if (covered !== null) return { kind: "record", shapeId: covered }; + const merged = dictSpreadLiteralShape(widened, inner, ctx); + if (merged === "dyn") return DYN; + if (merged !== null) return { kind: "record", shapeId: merged }; const refined = refinedConstituentShape(widened, inner, ctx); if (refined !== null) return { kind: "record", shapeId: refined }; - return { - kind: "record", - shapeId: shapes.intern(inner.fields, false, inner.indexValue, inner.declaredOrder), - }; + const shapeId = shapes.intern(inner.fields, false, inner.indexValue, inner.declaredOrder); + adoptAccessorOrder(shapes.get(shapeId), inner.accessorOrder); + return { kind: "record", shapeId }; } finally { shapes.inProgress.delete(widened); } @@ -5969,6 +6044,236 @@ function coveredOptionalFieldsShape( return shapes.intern([], false, optionalSlot, []); } +/** The type of an object literal that spreads an index-signature record + * (`{ ...tags, bucket: b, status }`, the metric-tags idiom). The checker + * drops the signature and types the literal by its explicit members only, + * but at run time the object holds every key the spread copied, in one + * insertion order. Such a literal maps to the pure index-signature record + * whose slot joins the spread slots and the member types, so the merge, + * every enumeration and keyed writes see the same keys Node does. A + * member read still answers its member type: members written after the + * spreads always win, and a member written before a spread must have that + * spread's slot type, so an override cannot change its type. The join + * covers scalar and unit types only; other literals keep their shape and + * the spread fence. */ +function dictSpreadLiteralShape( + widened: ts.Type, + parts: RecordShapeParts, + ctx: TypeMapperCtx, +): string | "dyn" | null { + if (parts.indexValue) return null; + const symbol = widened.getSymbol(); + if (!symbol || (symbol.flags & ts.SymbolFlags.ObjectLiteral) === 0) return null; + const literal = symbol.declarations.find((d): d is ts.ObjectLiteralExpression => + ts.isObjectLiteralExpression(d), + ); + if (!literal) return null; + const { checker, shapes, unions } = ctx; + const props = literal.properties.nodes; + if (!props.some((p) => ts.isSpreadAssignment(p))) return null; + // Decide on checker types alone before mapping anything: object literals + // with ordinary spreads are everywhere, and mapping their sources here + // would intern types out of their usual order. + // A literal written into a typed index-signature slot already lowers into + // it (the merge into the contextual record); its own type keeps its + // mapping. + const contextualRaw = checker.getContextualType(literal); + const contextual = contextualRaw ? checker.getNonNullableType(contextualRaw) : undefined; + // A literal written into a declared slot lowers into that slot (a typed + // index-signature record merges into it, a declared record converts), + // and its own type keeps its mapping. Only a literal with no declared + // destination (an unannotated binding, a type parameter, `any` or + // `unknown`, an unknown-valued record) is built by its own type. + // A generic argument's context is the instantiated parameter: the + // literal's own type when the type argument was inferred from it. + const ownContext = + contextual !== undefined && + (contextual === widened || contextual.getSymbol()?.declarations.includes(literal) === true); + if ( + contextual && + !ownContext && + !(contextual.flags & (ts.TypeFlags.TypeParameter | ts.TypeFlags.Any | ts.TypeFlags.Unknown)) + ) { + const infos = checker.getIndexInfosOfType(contextual); + const unknownValued = + infos.length > 0 && + infos.every( + (info) => (info.valueType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0, + ); + if (!unknownValued) return null; + } + const kinds = props.map((p) => + ts.isSpreadAssignment(p) ? spreadSourceKindTs(checker, p.expression) : "member", + ); + if (kinds.some((k) => k === null) || !kinds.includes("dict")) return null; + const dictSlotOf = (e: ts.Expression): IrType | null => { + const t = checker.getNonNullableType(checker.getTypeAtLocation(e)); + if (checker.getPropertiesOfType(t).length === 0) { + const infos = checker.getIndexInfosOfType(t); + if (infos.length !== 1 || !(infos[0]!.keyType.flags & ts.TypeFlags.String)) return null; + return mapType(infos[0]!.valueType, ctx); + } + // Another such literal's type (`{ ...tags, repo }` over a binding + // initialized by `{ ...base, plan }`) is stored as the same kind of + // pure index-signature record. + if (t === widened) return null; + const mapped = mapType(t, ctx); + if (mapped?.kind === "dyn") return DYN; + const shape = mapped?.kind === "record" ? shapes.get(mapped.shapeId) : undefined; + return shape?.indexValue && shape.fields.length === 0 && !shape.tuple ? shape.indexValue : null; + }; + const arms = new Map(); + const addArms = (t: IrType): boolean => { + const list = t.kind === "union" ? (unions.get(t.unionId)?.arms ?? []) : [t]; + for (const arm of list) { + if (!OVERLOAD_JOIN_SCALARS.has(arm.kind)) return false; + arms.set(typeKey(arm), arm); + } + return true; + }; + const before: string[] = []; + // An unknown-valued spread (`Record`, or another such + // literal) makes the object the checked-dynamic value: it is built like + // a JavaScript object literal (copy each spread's own keys in order), and + // member reads check their declared types. + let unknownValued = false; + for (const [i, p] of props.entries()) { + // A fixed-shape or conditional spread contributes its own members, + // which the checker already types exactly (they are in `parts`). + if (kinds[i] === "record") { + // Its members may be overwritten by a later dict spread, like members + // written before one. + before.push(...spreadMemberNamesTs(checker, (p as ts.SpreadAssignment).expression)); + continue; + } + if (ts.isSpreadAssignment(p)) { + const slot = dictSlotOf(p.expression); + if (slot?.kind === "dyn") { + unknownValued = true; + continue; + } + if (!slot) return null; + if (!addArms(slot)) { + // A slot outside the scalar join still has the checked-dynamic + // representation when it converts to it. + if ( + !canConvertToDyn( + slot, + (id) => shapes.get(id), + (id) => unions.get(id), + ) + ) + return null; + unknownValued = true; + } + // A member written before this spread may be overwritten by it: the + // typed join keeps member types only where the slot is that type; + // otherwise the checked-dynamic object (whose member reads check + // their types) holds whatever the spread wrote. + for (const name of before) { + const field = parts.fields.find((f) => f.name === name); + if (!field) return null; + if (!typeEquals(field.type, slot)) unknownValued = true; + } + continue; + } + if (!(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p))) return null; + const name = p.name; + if (!(ts.isIdentifier(name) || ts.isStringLiteral(name))) return null; + before.push(name.text); + } + if (parts.fields.some((f) => f.name.startsWith("%"))) return null; + // Members outside the scalar join (an array of labels beside the tags) + // make the object the checked-dynamic value too. + if (!unknownValued && !parts.fields.every((f) => addArms(f.type))) unknownValued = true; + if (unknownValued) { + const getRecord = (id: string) => shapes.get(id); + const getUnion = (id: string) => unions.get(id); + return parts.fields.every( + (f) => f.type.kind === "dyn" || canConvertToDyn(f.type, getRecord, getUnion), + ) + ? "dyn" + : null; + } + for (const field of parts.fields) if (!addArms(field.type)) return null; + if (![...arms.values()].some((arm) => !isUnitType(arm))) return null; + const sorted = [...arms.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + const slot: IrType = + sorted.length === 1 ? sorted[0]! : { kind: "union", unionId: unions.intern(sorted) }; + return shapes.intern([], false, slot, []); +} + +/** How a spread source contributes to dictSpreadLiteralShape, judged on + * checker types only: "dict" for a pure string index signature or another + * literal that spreads one, "record" for a fixed object shape or a + * conditional spread (`...(c && { k: v })`), null otherwise. */ +function spreadSourceKindTs(checker: ts.TypeChecker, e: ts.Expression): "dict" | "record" | null { + if (dictSpreadSourceTs(checker, e, 0)) return "dict"; + let inner = e; + while (ts.isParenthesizedExpression(inner)) inner = inner.expression; + if ( + (ts.isBinaryExpression(inner) && + inner.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) || + ts.isConditionalExpression(inner) + ) + return "record"; + const t = checker.getNonNullableType(checker.getTypeAtLocation(e)); + if ((t.flags & ts.TypeFlags.Object) === 0 || t.isUnionType()) return null; + if (checker.getIndexInfosOfType(t).length > 0) return null; + if (checker.getCallSignatures(t).length > 0 || checker.getConstructSignatures(t).length > 0) + return null; + const symbol = t.getSymbol(); + if (symbol && (symbol.flags & ts.SymbolFlags.Class) !== 0) return null; + return "record"; +} + +/** The member names a fixed-shape or conditional spread contributes. */ +function spreadMemberNamesTs(checker: ts.TypeChecker, e: ts.Expression): string[] { + let inner = e; + while (ts.isParenthesizedExpression(inner)) inner = inner.expression; + const literals: ts.Expression[] = []; + if (ts.isBinaryExpression(inner)) literals.push(inner.right); + else if (ts.isConditionalExpression(inner)) literals.push(inner.whenTrue, inner.whenFalse); + else literals.push(inner); + const names: string[] = []; + for (const l of literals) { + const t = checker.getNonNullableType(checker.getTypeAtLocation(l)); + for (const property of checker.getPropertiesOfType(t)) names.push(property.name); + } + return names; +} + +/** A spread source the checker types as a pure string index signature, + * or as another object literal that spreads one (dictSpreadLiteralShape), + * judged on checker types only. */ +function dictSpreadSourceTs(checker: ts.TypeChecker, e: ts.Expression, depth: number): boolean { + const t = checker.getNonNullableType(checker.getTypeAtLocation(e)); + if (checker.getPropertiesOfType(t).length === 0) { + const infos = checker.getIndexInfosOfType(t); + return infos.length === 1 && (infos[0]!.keyType.flags & ts.TypeFlags.String) !== 0; + } + if (depth > 4) return false; + const symbol = t.getSymbol(); + if (!symbol || (symbol.flags & ts.SymbolFlags.ObjectLiteral) === 0) return false; + const literal = symbol.declarations.find((d): d is ts.ObjectLiteralExpression => + ts.isObjectLiteralExpression(d), + ); + if (!literal) return false; + const spreads = literal.properties.nodes.filter((p) => ts.isSpreadAssignment(p)); + return ( + spreads.length > 0 && + spreads.every((p) => + dictSpreadSourceTs(checker, (p as ts.SpreadAssignment).expression, depth + 1), + ) + ); +} + +/** The first-seen accessor key order of a shape with accessor slots. */ +function adoptAccessorOrder(shape: IrRecordShape | undefined, order: string[] | undefined): void { + if (shape !== undefined && order !== undefined && shape.accessorOrder === undefined) + shape.accessorOrder = order; +} + /** A refinement intersection (`Expr & { kind: "mul" }`, which the checker * reduces to `Mul & { kind: "mul" }`) keeps the shape of the constituent it * refines when the refinement changes no field's representation. Recursive @@ -6328,8 +6633,18 @@ function mapRecordTypeInner( const declaredOrder = esOwnKeyOrder( fields.filter((f) => accessorSlotProp(f.name) === null).map((f) => f.name), ); + // With accessors: every own key in that order, an accessor property + // named once for its get/set pair (the checked-dynamic copy lists it). + const accessorOrder = fields.some((f) => accessorSlotProp(f.name) !== null) + ? esOwnKeyOrder([...new Set(fields.map((f) => accessorSlotProp(f.name)?.prop ?? f.name))]) + : undefined; fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); - return { fields, ...(indexValue ? { indexValue } : {}), declaredOrder }; + return { + fields, + ...(indexValue ? { indexValue } : {}), + declaredOrder, + ...(accessorOrder ? { accessorOrder } : {}), + }; } } @@ -6387,6 +6702,23 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean { } } +/** A parameter of a function type that is the default of a type parameter + * declared in node:test's standard-library declarations (MockTracker.fn's + * `F extends Function = (...args: any[]) => undefined`). */ +function isNodeTestDefaultFunctionParam(decl: ts.Declaration, ctx: TypeMapperCtx): boolean { + const fnType = decl.parent; + return ( + ts.isParameter(decl) && + fnType !== undefined && + ts.isFunctionTypeNode(fnType) && + fnType.parent !== undefined && + ts.isTypeParameterDeclaration(fnType.parent) && + fnType.parent.defaultType === fnType && + ctx.isStdlibFile(decl.getSourceFile()) && + isDeclaredInAmbientModule(decl, "test") + ); +} + /** `any[]` (or `readonly any[]`): an array type whose element is `any`. */ export function isAnyArrayType(t: ts.Type, checker: ts.TypeChecker): boolean { if (!checker.isArrayType(t)) return false; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 7d8d88889..49c296a28 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -8,6 +8,7 @@ import { libraryWasmRefusal, } from "./library/prepare.js"; import { analyzeWithFrontend } from "./frontend/analysis.js"; +import { checkerCacheIdentity } from "./frontend/ts7/checker-backend.js"; import { analyzeWithVerdictCache, fileVerdictStore } from "./coverage/verdict-cache.js"; import { coverageCompilerIdentity } from "./coverage/verdict-startup.js"; import { commentOnlyReplay } from "./coverage/verdict-semantic.js"; @@ -379,6 +380,12 @@ export async function analyzeCached( const nodeFrontend: FrontendFactory = (entry, npmStatic, externalTypes, libraryNpmStatic) => runFrontend(entry, loadProgram, npmStatic, externalTypes, libraryNpmStatic); +/** The checker's part of the executable cache key (`checkerCacheIdentity`), as the CLI's startup route computes it. */ +function checkerField(): { checker?: string } { + const checker = checkerCacheIdentity(); + return checker === undefined ? {} : { checker }; +} + /** The whole pipeline: load → preflight → lower → validate → emit LLVM → link. */ function clearCompileSessionCaches(): void { clearResolveCaches(); @@ -1052,6 +1059,7 @@ async function compileTracked( ...(opts.strip ? { strip: true as const } : {}), ...(opts.windowsSubsystem === "gui" ? { windowsSubsystem: "gui" as const } : {}), ...(opts.detachedExit === true ? { detachedExit: true as const } : {}), + ...checkerField(), ...(youngNurseryRequested() ? { youngNursery: true as const } : {}), npmStatic: opts.npmStatic ?? null, ffiProfile: diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 826151009..20969d9b8 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -346,6 +346,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "process.kill", "process.killNum", "process.setExitCode", + "process.setExitCodeTracked", // Node's argument grammar throws ERR_INVALID_ARG_TYPE at the call. "process.emitWarning", // cpuUsage(prev)'s field validation: negative/non-finite prev fields @@ -490,6 +491,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "json.stringifyReplacer", "json.stringifyValue", "date.nativeNew", + "date.nativeNewFields", + "date.nativeNewValue", "date.checkedValue", "util.parseArgs", "util.parseEnv", @@ -597,6 +600,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.mapSeedEntry", "dyn.freeze", "dyn.isFrozen", + "class.defineField", + "class.throwReadOnly", + "class.throwNotExtensible", "dyn.toString", "dyn.valueOf", "string.fromCodePoint", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index c55444e60..381258b07 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -206,12 +206,47 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.in": { argTypes: [DYN, STRING], result: BOOL }, "dyn.freeze": { argTypes: [DYN], result: DYN }, "dyn.isFrozen": { argTypes: [DYN], result: BOOL }, + /** Object.defineProperty on a declared class field (scr_json.c): the + * field's current value (borrowed), its FIELD_* attribute word, the + * instance's CLASS_* integrity word, the field name and the descriptor. + * Validates as Node does (TypeError "Cannot redefine property: f") and + * answers a one-property holder of the defined value and attributes (+1), + * read back with class.definedBits and class.definedValue. */ + "class.defineField": { argTypes: [DYN, F64, F64, STRING, DYN], result: DYN }, + "class.definedBits": { argTypes: [DYN, STRING], result: F64 }, + "class.definedValue": { argTypes: [DYN, STRING], result: DYN }, + /** Whether a property descriptor has a `value` member. */ + "class.descriptorHasValue": { argTypes: [DYN], result: BOOL }, + /** Throws Node's TypeError for a write to a read-only class field: the + * field name and the instance's class name. */ + "class.throwReadOnly": { argTypes: [STRING, STRING], result: VOID }, + /** Throws Node's TypeError for a class field initializer on a + * non-extensible instance: the field name. */ + "class.throwNotExtensible": { argTypes: [STRING], result: VOID }, "dyn.nativeSetNew": { argTypes: [DYN], result: DYN }, "dyn.nativeSetIs": { argTypes: [DYN], result: BOOL }, "dyn.nativeMapIs": { argTypes: [DYN], result: BOOL }, "dyn.nativeUrlIs": { argTypes: [DYN], result: BOOL }, "dyn.nativeDateIs": { argTypes: [DYN], result: BOOL }, + /** A builtin constructor (`Object`, `Date`, `RegExp`, `Map`, `Set`, + * `Promise`, `WeakMap`, `WeakSet`) read as a JavaScript value: one + * interned function per name, the same one dynamic values answer for + * `.constructor`. The name is a compiler-chosen literal. */ + "dyn.globalConstructor": { argTypes: [STRING], result: DYN }, + /** A builtin global function (setTimeout, setInterval, setImmediate, + * the clear functions, queueMicrotask, process.nextTick) read as a + * JavaScript value: one interned function per name whose calls run the + * timer APIs (it pulls in the event loop). The name is a compiler-chosen + * literal. */ + "dyn.builtinFunction": { argTypes: [STRING], result: DYN }, "date.nativeNew": { argTypes: [DYN], result: DYN }, + /** `new Date(year, monthIndex, ...)` in JavaScript: the argument array's + * first seven values pass through ToNumber in order (later ones are + * evaluated, never converted), then the local-time field constructor. */ + "date.nativeNewFields": { argTypes: [DYN], result: DYN }, + /** `new Date(value)` in JavaScript: a string argument parses with V8's + * full date parser; every other value constructs as date.nativeNew. */ + "date.nativeNewValue": { argTypes: [DYN], result: DYN }, "date.checkedValue": { argTypes: [DYN], result: DATE_T }, "dyn.nativeRegexIs": { argTypes: [DYN], result: BOOL }, /** toString() on a checked-dynamic receiver: runtime kind dispatch @@ -239,6 +274,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.promiseAll": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, "dyn.promiseAllSettled": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, "dyn.promiseAny": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, + /** PromiseResolve over one checked-dynamic value: the promise it holds, + * a promise that adopts a thenable's settlement, or a promise fulfilled + * with the value. The second argument reads `then` as above. */ + "dyn.promiseResolve": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, /** Bare `typeof v` on a dyn value AS A STRING (arg: the dyn value, * borrowed; result: an owned string) — the dyn kind's JS answer: * undefined→"undefined", null/object/array/bytes→"object" (JS's oldest @@ -2391,6 +2430,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** Rest listeners use the checked function bridge to pack event arguments. */ "child.onDyn": { argTypes: [CHILD_T, STRING, DYN], result: VOID }, "process.connected": { argTypes: [], result: BOOL }, + /** Whether this fork child opened its channel; part of process.send's + * presence in programs with fork targets. Never throws. */ + "process.ipcOpen": { argTypes: [], result: BOOL }, "process.send": { argTypes: [STRING], result: BOOL }, "process.sendCb": { argTypes: [STRING, null], result: BOOL }, "process.disconnect": { argTypes: [], result: VOID }, @@ -3025,8 +3067,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "process.exit": { argTypes: [F64], result: VOID }, /** Numeric process.exitCode write (integer validation, implicit exit status). */ "process.setExitCode": { argTypes: [F64], result: VOID }, + /** The process.exitCode write of a program that also reads the + * property: the same validation, and the code counts as assigned. */ + "process.setExitCodeTracked": { argTypes: [F64], result: VOID }, /** The code for process.exit() with no argument, or zero when unset. */ "process.currentExitCode": { argTypes: [], result: F64 }, + /** Whether process.exitCode reads a code rather than undefined. Never throws. */ + "process.hasExitCode": { argTypes: [], result: BOOL }, "process.cwd": { argTypes: [], result: STRING }, /** getpid(2) / getuid(2): zero args → f64. POSIX-only target, so both * always answer (the checker's `getuid?` optionality covers Windows — @@ -3231,6 +3278,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** SameValue over checked values, including NaN, signed zero and reference identity. */ "dyn.sameValue": { argTypes: [DYN, DYN], result: BOOL }, "error.stack": { argTypes: [null], result: STRING }, + "error.setStack": { argTypes: [null, STRING], result: VOID }, "error.stackLimitGet": { argTypes: [], result: F64 }, "error.stackLimitSet": { argTypes: [F64], result: VOID }, // Receiver (any error-hierarchy object) and the program-dependent @@ -4313,13 +4361,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "date.toDateString": { argTypes: [DATE_T], result: STRING }, "date.toTimeString": { argTypes: [DATE_T], result: STRING }, /** The composed `new Date(dateString).getTime()` read: one borrowed - * string, f64 milliseconds since epoch. The parsed grammar is BOUNDED - * (documented divergence — V8's parser accepts far more): the ASN.1 - * validity shape X509Certificate.validFrom/validTo answer ("Jul 1 - * 00:00:00 2026 GMT" — the portless cert-expiry read), and ECMA's own - * date-time string format (YYYY-MM-DD[THH:mm[:ss[.sss]]][Z|±HH:MM] — - * date-only forms are UTC, exactly the spec). Anything else is NaN, - * Node's invalid-date getTime. Never throws. */ + * string, f64 milliseconds since epoch, with V8's date-string grammar + * (the ES5 format, then the legacy forms: RFC 1123/2822, Date#toString, + * "Jan 2, 2024", ...). Zone-less dates are local time; anything V8 + * rejects is NaN, Node's invalid-date getTime. Never throws. */ "date.parseGetTime": { argTypes: [STRING], result: F64 }, /** `Date.UTC(...)` — seven f64 arguments (the frontend completes the * spec's defaults for omitted trailing parts: month 0, date 1, time @@ -4327,6 +4372,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * map to 1900+year, out-of-range months/dates roll over, non-finite * parts and out-of-range results answer NaN. Never throws. */ "date.utc": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: F64 }, + /** `new Date(year, monthIndex[, date, hours, minutes, seconds, ms])` — + * the same seven-f64 completion as date.utc, but the fields are LOCAL + * time: V8's MakeDay/MakeTime, then UTC(t) in the host zone (DST gaps + * read with the offset before the transition, overlaps answer the + * earlier instant), then TimeClip. Never throws. */ + "date.newLocal": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: DATE_T }, /** WHATWG TextDecoder.decode over u8 bytes (scr_bytes.c): utf-8 with * default options — the same maximal-subpart replacement decode as * Buffer.toString("utf8"), with the leading BOM stripped (the one diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index c2d461e09..23bcb86ef 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -154,6 +154,7 @@ const MODULE_FIELDS: Record>, Role> = { "classes[].jsName": "other", "classes[].genericOf": "other", "classes[].baseValueGlobal": "other", + "classes[].errorNameAfter": "other", "classes[].prototypeDataHelper": "reference", "classes[].instancePrototypeHelper": "reference", "classes[].localCaptures[].name": "other", @@ -170,6 +171,7 @@ const MODULE_FIELDS: Record>, Role> = { "records[].id": "other", "records[].fields[].name": "other", "records[].declaredOrder[]": "other", + "records[].accessorOrder[]": "other", "unions[].id": "other", "unions[].discriminant.field": "other", "unions[].discriminant.cases[].values[]": "other", diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index fe0467fac..a08693013 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -347,6 +347,8 @@ const DYN_READ_ONLY = new Set([ "arrayBuffer.viewU8", "arrayBuffer.viewU8C", "date.nativeNew", + "date.nativeNewFields", + "date.nativeNewValue", // The dynamic operators run the ToPrimitive and coercion helpers of // dyn.compare, dyn.numberConstructor and dyn.toStringCoerce, then build a // fresh number, string or bigint. dyn.increment steps dyn.toNumeric's result. diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 9a8f1121c..6d63007fb 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1406,6 +1406,11 @@ export interface IrClassDef { * bag. Its descriptors preserve presence and insertion order independently * of native layout order; field values remain in their native slots. */ tracksOwnFields?: boolean; + /** Error subclasses: the field an instance's own `name` follows among its + * own properties, or null when it precedes every field. Absent when no + * class of the chain sets `name` during construction, and then an own + * `name` follows every other own property. */ + errorNameAfter?: string | null; /** Stable module symbol identities for public instance layout fields. */ symbolFields?: { field: string; globalId: string }[]; /** RUNTIME-PROVIDED class (the builtin Error hierarchy): the struct, RC @@ -1621,6 +1626,10 @@ export interface IrRecordShape { * fields carry that declaredOrder OMITS are internal '%'-fields (Dirent's * %dtype) — hidden from every key-order surface, JSON included. */ declaredOrder?: string[]; + /** Shapes with accessor slots: every own key in declaration order, with + * an accessor property named once (see accessorSlotProp). The + * checked-dynamic copy lists its keys in this order. */ + accessorOrder?: string[]; } /** fs.Dirent rows: the hidden %dtype entry kind (libuv's UV_DIRENT @@ -1932,8 +1941,23 @@ export type IrStmt = | { kind: "return"; value: IrExpr | null; awaited?: true; loc: SrcLoc } /** Field write `obj.f = v` — statement-only, like `assign`/`arraySet`. * Evaluation order: obj, then value. The old value is released; ownership - * of a refcounted new value MOVES into the object. */ - | { kind: "fieldSet"; obj: IrExpr; className: string; field: string; value: IrExpr; loc: SrcLoc } + * of a refcounted new value MOVES into the object. `sloppy` marks a write + * in sloppy-mode code, which skips a read-only field instead of throwing. + * `define` marks a class field initializer, which defines the property + * (a non-extensible instance rejects it). `attributesChecked` marks a + * write that already honors the instance's property attributes + * (CLASS_ATTRIBUTES). */ + | { + kind: "fieldSet"; + obj: IrExpr; + className: string; + field: string; + value: IrExpr; + sloppy?: true; + define?: true; + attributesChecked?: true; + loc: SrcLoc; + } /** Record field write `r.f = v` — mirrors `fieldSet` exactly (evaluation * order obj then value; old value released; refcounted new value moved * in), with a shape id in place of a class name. */ @@ -3331,9 +3355,10 @@ export type IrExpr = /** Keyed read on a dyn value — `pkg.name` / `pkg["k"]` / the * `pkg?.scripts` chain step on a JSON.parse result. `key` is * string-typed (a strLit for the dot form); `type` is always dyn. An - * OBJ receiver answers the member (+1) or the undefined singleton (the - * own-property answer — prototype members like `toString` answer - * undefined, SEMANTICS.md); ARR answers `length` and canonical + * OBJ receiver answers the member (+1), an inherited Object.prototype + * member (the Object constructor and the interned valueOf/toString/ + * hasOwnProperty/propertyIsEnumerable values), or the undefined singleton; + * ARR answers `length` and canonical * in-range indices, STR answers `length` (UTF-16-exact), both * undefined otherwise; NUM/BOOL/BYTES answer undefined. An * undefined/null receiver THROWS the catchable Node-shaped TypeError @@ -4078,6 +4103,27 @@ export function isClassOwnEnumerableFieldName(name: string): boolean { /** Compiler-owned storage for properties added through untyped references. */ export const DYN_CLASS_PROPERTIES = "%dynProperties"; +/** Compiler-owned integrity state of a class instance whose hierarchy is + * passed to Object.freeze, Object.seal, Object.preventExtensions or + * Object.defineProperty: an f64 holding CLASS_NOT_EXTENSIBLE, CLASS_SEALED + * and CLASS_FROZEN bits. Other hierarchies have no such field. */ +export const CLASS_ATTRIBUTES = "%attributes"; +export const CLASS_NOT_EXTENSIBLE = 1; +export const CLASS_SEALED = 2; +export const CLASS_FROZEN = 4; + +/** Compiler-owned attributes of one declared field that Object.defineProperty + * changes: an f64 holding FIELD_NOT_WRITABLE, FIELD_NOT_ENUMERABLE and + * FIELD_NOT_CONFIGURABLE bits (zero is an ordinary data property). */ +export const CLASS_FIELD_ATTRIBUTES_PREFIX = "%attr:"; +export const FIELD_NOT_WRITABLE = 1; +export const FIELD_NOT_ENUMERABLE = 2; +export const FIELD_NOT_CONFIGURABLE = 4; + +export function classFieldAttributesName(field: string): string { + return CLASS_FIELD_ATTRIBUTES_PREFIX + field; +} + /** A class capsule can always preserve its exact native identity. Its * optional property view additionally needs converters in both directions; * fields such as Maps may remain opaque without preventing the round trip. */ @@ -4231,9 +4277,6 @@ function canBoxDynCompositeAt( const shape = getRecord(t.shapeId); if (!shape) return false; if (recordTextCodecClass(shape) !== null) return false; - // Accessor slots hold the getter and setter closures, not property - // values (see accessorSlotProp). - if (shapeHasAccessorSlots(shape)) return false; // Recursive shapes answer coinductively, like isJsonSafeType. if (visiting.has(t.shapeId)) return true; visiting.add(t.shapeId); diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 9abe15345..223be72a9 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -21,6 +21,7 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "dyn.promiseAll", "dyn.promiseAllSettled", "dyn.promiseAny", + "dyn.promiseResolve", "process.onUncaughtException", "process.offUncaughtException", "process.onUnhandledRejection", @@ -198,12 +199,24 @@ const READABLE_DYN_ASYNC_PREFIXES: readonly string[] = ["readable.fromGen", "rea const DYN_INVOKE_LIB_FNS: ReadonlySet = new Set([ "dyn.arrayProtoCall", "dyn.arrayPrototype", + "dyn.builtinFunction", "dyn.builtinMethod", "dyn.defineProperty", "dyn.defineProps", "dyn.definePrototypeProps", "dyn.functionApply", + "dyn.globalConstructor", "dyn.objCreateWithProperties", + "dyn.objectTag", +]); + +/** Member names whose constant-key dynamic reads answer the inherited Object.prototype member through the dyn-invoke unit. Mirrors expr-dynamic.ts INHERITED_OBJECT_MEMBERS (the IR layer does not import the backend). */ +const INHERITED_MEMBER_NAMES: ReadonlySet = new Set([ + "constructor", + "hasOwnProperty", + "propertyIsEnumerable", + "toString", + "valueOf", ]); /** Entry points that link the child-process unit (with CHILD_PROCESS_LIB_PREFIXES): the fork IPC channel on the process object. */ @@ -211,6 +224,7 @@ const CHILD_PROCESS_LIB_FNS: ReadonlySet = new Set([ "process.connected", "process.disconnect", "process.forkTarget", + "process.ipcOpen", "process.onDisconnect", "process.onMessage", "process.send", @@ -493,7 +507,13 @@ const RUNTIME_FEATURE_RULES = { dynInvoke: rule({ initial: (mod) => mod.workers === true, fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS], - expr: (node, _tables) => node.kind === "dynInvoke", + // Constant-key reads of Object.prototype member names answer the + // inherited member through scr_dyn_invoke.c. + expr: (node, _tables) => + node.kind === "dynInvoke" || + (node.kind === "dynKeyGet" && + node.key.kind === "strLit" && + INHERITED_MEMBER_NAMES.has(node.key.value)), }), /** The program has dynamic-tier jsval operations (js.* builtins, the * jsval pass): links the scr_js_* units (two-tier design I3). */ diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 4a961dfca..716774b03 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4109,6 +4109,7 @@ function validateFunction( if (checkLibEmitterCall(e, sig.result)) return; if ( e.fn === "error.stack" || + e.fn === "error.setStack" || e.fn === "error.cause" || e.fn === "error.hasCause" || e.fn === "error.setCause" || @@ -6199,7 +6200,10 @@ function validateFunction( if (expectsArg && (rp.params.length !== 1 || !typeEquals(rp.params[0]!, inner))) { err(`newPromise resolve param must be (${inner.kind}) => void`, e.loc); } - if (!expectsArg && rp.params.length !== 0) { + // A Promise executor types resolve as `(value: never) => + // void` (one parameter over a void inner): the void resolve never + // reads an argument, and a never value cannot be produced. + if (!expectsArg && rp.params.length > 1) { err("newPromise resolve takes no argument", e.loc); } } diff --git a/packages/compiler/src/native/toolchain.test.ts b/packages/compiler/src/native/toolchain.test.ts index 9afccdc18..0ee434300 100644 --- a/packages/compiler/src/native/toolchain.test.ts +++ b/packages/compiler/src/native/toolchain.test.ts @@ -14,6 +14,7 @@ test("native toolchain installation paths relocate with the manifest", () => { compiler_version: "1.2.3", target: "macos-arm64", ts7: "typescript/tsc", + tsc_ts: "tsc-ts/lib/tsc-ts", llvm_package: "llvm", runtime_pack: "runtime", linker: "tools/linker", @@ -23,7 +24,10 @@ test("native toolchain installation paths relocate with the manifest", () => { try { writeFileSync(path, JSON.stringify(manifest)); const toolchain = loadNativeToolchain(path); - expect(toolchain.ts7Executable).toBe(join(root, "typescript/tsc")); + expect(toolchain.ts7Executable).toBe(join(root, "tsc-ts/lib/tsc-ts")); + expect(loadNativeToolchain(path, { SCRIPTC_CHECKER: "tsgo" }).ts7Executable).toBe( + join(root, "typescript/tsc"), + ); expect(toolchain.helperExecutable).toBe(join(root, "llvm/bin/scriptc-llvm-codegen")); expect(toolchain.runtimePackRoot).toBe(join(root, "runtime")); expect(toolchain.linker).toBe(join(root, "tools/linker")); @@ -39,6 +43,7 @@ test("native toolchain installation paths relocate with the manifest", () => { { ...manifest, schema: "other" }, { ...manifest, target: "other" }, { ...manifest, ts7: "" }, + { ...manifest, tsc_ts: "" }, { ...manifest, linker_args: [1] }, ]) { writeFileSync(path, JSON.stringify(invalid)); @@ -71,6 +76,7 @@ test("native toolchain assets resolve from the physical manifest through package compiler_version: "1.2.3", target: "windows-x64-msvc", ts7: asset("typescript/lib/tsc.exe"), + tsc_ts: asset("tsc-ts/lib/tsc-ts.exe"), llvm_package: asset("llvm"), runtime_pack: asset("runtime"), runtime_packs: [{ target: "windows-x64-msvc", path: asset("runtime") }], @@ -94,6 +100,10 @@ test("native toolchain assets resolve from the physical manifest through package const viaLink = loadNativeToolchain(join(linked, "bin", "scriptc.exe.json")); expect(viaLink).toEqual(direct); expect(viaLink.ts7Executable).toBe(join(assets, "typescript/lib/tsc.exe")); + expect( + loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { SCRIPTC_CHECKER: "tsc-ts" }) + .ts7Executable, + ).toBe(join(assets, "tsc-ts/lib/tsc-ts.exe")); expect(viaLink.helperExecutable).toBe(join(assets, "llvm/bin/scriptc-llvm-codegen.exe")); expect( loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { @@ -126,6 +136,7 @@ test("cross-target packs can be added to a project after the compiler is install compiler_version: "1.2.3", target: "macos-arm64", ts7: "../lib/typescript/tsc", + tsc_ts: "../lib/tsc-ts/lib/tsc-ts", llvm_package: "../lib/llvm", runtime_pack: "../lib/runtime-darwin-arm64", linker: "clang", @@ -174,6 +185,7 @@ test("versioned GNU targets select Zig while ordinary host builds keep the insta compiler_version: "1.2.3", target: "linux-x64-gnu", ts7: "tsc", + tsc_ts: "tsc-ts", llvm_package: "llvm", runtime_pack: "runtime", linker: "clang", @@ -192,3 +204,41 @@ test("versioned GNU targets select Zig while ordinary host builds keep the insta rmSync(root, { recursive: true, force: true }); } }); + +test("a manifest without tsc_ts takes the host's tsc-ts executable from the installed package", () => { + const root = realpathSync( + mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-checker-tools-")), + ); + const path = join(root, "toolchain.json"); + const manifest: NativeToolchainManifest = { + schema: "scriptc.native-toolchain.v1", + compiler_version: "1.2.3", + target: "linux-x64-musl", + ts7: "typescript/tsc", + llvm_package: "llvm", + runtime_pack: "runtime", + linker: "tools/linker", + linker_args: ["cc"], + dsymutil: "dsymutil", + }; + try { + writeFileSync(path, JSON.stringify(manifest)); + const project = join(root, "project"); + const platformPackage = join(project, "node_modules/@tsc-ts/linux-x64-musl"); + mkdirSync(join(project, "node_modules/tsc-ts"), { recursive: true }); + writeFileSync(join(project, "node_modules/tsc-ts/package.json"), '{"name":"tsc-ts"}'); + mkdirSync(join(platformPackage, "lib"), { recursive: true }); + writeFileSync(join(platformPackage, "lib/tsc-ts"), ""); + expect(loadNativeToolchain(path, {}, project).ts7Executable).toBe( + join(platformPackage, "lib/tsc-ts"), + ); + expect(loadNativeToolchain(path, { SCRIPTC_CHECKER: "tsgo" }, project).ts7Executable).toBe( + join(root, "typescript/tsc"), + ); + expect(() => loadNativeToolchain(path, {}, root)).toThrow( + "the tsc-ts package is not installed", + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/src/native/toolchain.ts b/packages/compiler/src/native/toolchain.ts index 651d87027..fbcd94251 100644 --- a/packages/compiler/src/native/toolchain.ts +++ b/packages/compiler/src/native/toolchain.ts @@ -6,6 +6,11 @@ import { type NativeTargetSpec, type NativeHelperSpec, } from "../backend/targets.js"; +import { + checkerBackend, + tscTsExecutable, + tscTsPlatformPackage, +} from "../frontend/ts7/checker-backend.js"; export interface NativeToolchain { compilerVersion: string; @@ -34,6 +39,8 @@ export interface NativeToolchainManifest { compiler_version: string; target: string; ts7: string; + /** The tsc-ts executable, the default checker; without it, an installed `tsc-ts` package's. */ + tsc_ts?: string; llvm_package: string; runtime_pack: string; runtime_sources?: string; @@ -106,6 +113,7 @@ export function loadNativeToolchain( } const manifest = raw as NativeToolchainManifest; for (const name of [ + "tsc_ts", "runtime_sources", "declarations", "archiver", @@ -169,7 +177,19 @@ export function loadNativeToolchain( : [...target.defaultLinkerArgs]; return { compilerVersion: manifest.compiler_version, - ts7Executable: pathFrom(root, manifest.ts7), + // tsc-ts, the default checker except on Windows, comes with the distribution; + // a development build takes it from the tsc-ts package installed with the + // compiler or for the working directory. SCRIPTC_CHECKER selects a checker. + ts7Executable: + checkerBackend(env, host.platform) === "tsgo" + ? pathFrom(root, manifest.ts7) + : manifest.tsc_ts !== undefined + ? pathFrom(root, manifest.tsc_ts) + : tscTsExecutable( + [root, cwd], + tscTsPlatformPackage(host.platform, host.architecture, host.name.endsWith("-musl")), + host.platform, + ), target, helper: host.helper, helperPackageRoot, diff --git a/packages/compiler/src/startup-cache.ts b/packages/compiler/src/startup-cache.ts index fedc36fd5..c2f82fdad 100644 --- a/packages/compiler/src/startup-cache.ts +++ b/packages/compiler/src/startup-cache.ts @@ -22,5 +22,6 @@ export function precompiledRuntimePackTarget( ): NativeTargetSpec | null { return nativeCodegenTarget(env); } +export { checkerCacheIdentity } from "./frontend/ts7/checker-backend.js"; export { readCoverageVerdict } from "./coverage/verdict-startup.js"; export { coverageEnvelope, coveragePasses, renderCoverage } from "./coverage/report.js"; diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 01d12dedc..5f96ab8c0 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1988,7 +1988,7 @@ "kind": "node-builtin", "name": "process.exitCode", "status": "static", - "note": "numeric writes in statement position set the implicit exit status; process.exit() reads it" + "note": "numeric writes in statement position set the implicit exit status; reads answer the assigned code or undefined; process.exit() reads it" }, { "id": "node-builtin.process.getBuiltinModule", @@ -2532,7 +2532,7 @@ "name": "util.promisify", "status": "unsupported", "code": "SC2020", - "note": "the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions, dns.lookup, dns.lookupService and the fs callback functions with an fs/promises counterpart; call the bound function directly" + "note": "the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions, dns.lookup, dns.lookupService and the fs callback functions with an fs/promises counterpart, plus typed callback-last function values whose callback takes (error?: Error | null, value?); call the bound function directly" }, { "id": "node-builtin.util.stripVTControlCharacters", @@ -4097,7 +4097,7 @@ "kind": "stdlib", "name": "Date constructor", "status": "static", - "note": "zero arguments, or one milliseconds/date-string argument; values are the read-only TimeClip scalar slice" + "note": "zero arguments, one milliseconds/date-string argument, or local-time year/month fields; values are the read-only TimeClip scalar slice" }, { "id": "stdlib.date.getDate", @@ -4220,7 +4220,7 @@ "kind": "stdlib", "name": "Date.parse", "status": "static", - "note": "one date string, using the bounded parser shared with new Date(dateString).getTime()" + "note": "one date string, using V8's date parser shared with new Date(dateString)" }, { "id": "stdlib.date.toDateString", diff --git a/packages/compiler/test/class-attributes.test.ts b/packages/compiler/test/class-attributes.test.ts new file mode 100644 index 000000000..1a6fea654 --- /dev/null +++ b/packages/compiler/test/class-attributes.test.ts @@ -0,0 +1,84 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import type { IrModule } from "../src/ir/ir.js"; + +async function lowerModule(source: string, file = "main.ts"): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-class-attributes-")); + try { + const entry = join(dir, file); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function fieldsOf(mod: IrModule, className: string): string[] { + const cls = (mod.classes ?? []).find( + (c) => c.name === className || c.name.endsWith(`.${className}`), + ); + if (!cls) throw new Error(`no class ${className}`); + return cls.fields.map((f) => f.name); +} + +function functionsCalling(mod: IrModule, fn: string): string[] { + return mod.functions + .filter((f) => JSON.stringify(f.body).includes(`"fn":"${fn}"`)) + .map((f) => f.name); +} + +test("only hierarchies passed to an integrity call get attribute state and write checks", async () => { + const mod = await lowerModule(` +export {}; +class Point { x = 1; } +class Other { x = 1; } +function movePoint(p: Point): void { p.x = 2; } +function moveOther(o: Other): void { o.x = 2; } +const p = new Point(); +Object.freeze(p); +movePoint(p); +moveOther(new Other()); +`); + expect(fieldsOf(mod, "Point")).toContain("%attributes"); + expect(fieldsOf(mod, "Other")).not.toContain("%attributes"); + const checked = functionsCalling(mod, "class.throwReadOnly"); + expect(checked).toContain("movePoint"); + expect(checked).not.toContain("moveOther"); +}); + +test("a redefined field gets its own attribute word", async () => { + const mod = await lowerModule(` +export {}; +class Config { mode = "dev"; level = 1; } +const c = new Config(); +Object.defineProperty(c, "mode", { writable: false }); +`); + const fields = fieldsOf(mod, "Config"); + expect(fields).toContain("%attributes"); + expect(fields).toContain("%attr:mode"); + expect(fields).not.toContain("%attr:level"); +}); + +test("sloppy-mode writes skip a read-only field without throwing", async () => { + const mod = await lowerModule( + ` +class Box { constructor() { this.x = 1; } } +const b = new Box(); +Object.freeze(b); +b.x = 2; +`, + "main.cjs", + ); + expect(fieldsOf(mod, "Box")).toContain("%attributes"); + // Class bodies are strict mode code; the top-level write is not. + const throwing = functionsCalling(mod, "class.throwReadOnly"); + expect(throwing.length).toBeGreaterThan(0); + expect(throwing.every((name) => name.includes("constructor"))).toBe(true); +}); diff --git a/packages/compiler/test/class-method-mutations.test.ts b/packages/compiler/test/class-method-mutations.test.ts index 2258bd54a..b508a3949 100644 --- a/packages/compiler/test/class-method-mutations.test.ts +++ b/packages/compiler/test/class-method-mutations.test.ts @@ -114,3 +114,24 @@ test("a computed key in an Object.assign source keeps checked dispatch", async ( .replace("LITERAL", "Object.assign(target, { [key]: value })"); expect(await callersOf(source, "%dyn.class.properties")).not.toEqual([]); }); + +const range = ` +class Range { + hi: number; + constructor(hi: number) { this.hi = hi; } + *[Symbol.iterator](): Generator { for (let i = 0; i < this.hi; i++) yield i; } +} +const r = new Range(3); +console.log(r[Symbol.iterator]().next().value); +`; + +test("a well-known symbol method call lowers to the class's method", async () => { + expect(await callersOf(range, "%Range.sym:iterator")).not.toEqual([]); +}); + +test("a reflection call with a well-known symbol key keeps checked dispatch", async () => { + const write = "Object.defineProperty(r, Symbol.iterator, { value: function* () {} });\n"; + // The checked path reads the declared method through its value thunk. + const callers = await callersOf(`${range}${write}`, "%Range.sym:iterator"); + expect(callers.filter((name) => !name.startsWith("%method.value:"))).toEqual([]); +}); diff --git a/packages/compiler/test/native-class-descriptors.test.ts b/packages/compiler/test/native-class-descriptors.test.ts index d4aff3949..bd0a06dac 100644 --- a/packages/compiler/test/native-class-descriptors.test.ts +++ b/packages/compiler/test/native-class-descriptors.test.ts @@ -12,7 +12,7 @@ function fixture(source: string) { } test.each([ - "Object.defineProperty(value, 'x', {value: 2, writable: false});", + "Object.defineProperty(value, 'x', {get() { return 2; }});", "Object.defineProperties(value, {x: {value: 2}});", ])("keeps unsafe native descriptor changes fenced: %s", (operation) => { const { dir, entry } = fixture( @@ -30,6 +30,42 @@ test.each([ } }); +test("data descriptor changes on native fields match Node", async () => { + const { dir, entry } = fixture(` +class Value { constructor() { this.x = 1; } } +const value = new Value(); +Object.defineProperty(value, 'x', { value: 2, writable: false }); +console.log(value.x, JSON.stringify(Object.getOwnPropertyDescriptor(value, 'x'))); +value.x = 3; +console.log(value.x, Object.keys(value)); +`); + try { + const result = await compile(entry, { + outDir: dir, + outPath: join(dir, "program"), + sanitize: process.env["SCRIPTC_SAN"] === "1", + }); + if (!result.ok) throw new Error(JSON.stringify(result.diagnostics)); + const run = spawnSync(result.binaryPath, { encoding: "utf8", timeout: 30_000 }); + const oracle = spawnSync(process.execPath, [entry], { encoding: "utf8", timeout: 30_000 }); + expect(run.error).toBeUndefined(); + expect(oracle.status).toBe(0); + expect({ + status: run.status, + signal: run.signal, + stdout: run.stdout, + stderr: run.stderr, + }).toEqual({ + status: oracle.status, + signal: oracle.signal, + stdout: oracle.stdout, + stderr: oracle.stderr, + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + test("JS overrides with different return types match Node", async () => { const { dir, entry } = fixture(` class Base { copy(value = 1) { return value; } } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5779-sha512-lengths.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5779-sha512-lengths.ts.json new file mode 100644 index 000000000..60582ec60 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5779-sha512-lengths.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5779-sha512-lengths.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5816-fixed-into-unknown-rest.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5816-fixed-into-unknown-rest.ts.json new file mode 100644 index 000000000..8354f5c48 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5816-fixed-into-unknown-rest.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5816-fixed-into-unknown-rest.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5916-dyn-error-stack-property.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5916-dyn-error-stack-property.ts.json new file mode 100644 index 000000000..fa5b5460f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5916-dyn-error-stack-property.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5916-dyn-error-stack-property.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6004-class-instance-integrity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6004-class-instance-integrity.ts.json new file mode 100644 index 000000000..4d21236c0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6004-class-instance-integrity.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6004-class-instance-integrity.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6005-class-instance-integrity-sloppy.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6005-class-instance-integrity-sloppy.cjs.json new file mode 100644 index 000000000..1101d2180 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6005-class-instance-integrity-sloppy.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6005-class-instance-integrity-sloppy.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6055-class-generator-iterables.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6055-class-generator-iterables.ts.json new file mode 100644 index 000000000..661995e3f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6055-class-generator-iterables.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6055-class-generator-iterables.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6056-class-symbol-method-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6056-class-symbol-method-calls.ts.json new file mode 100644 index 000000000..445389b5e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6056-class-symbol-method-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6056-class-symbol-method-calls.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6230-date-parse-legacy.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6230-date-parse-legacy.ts.json new file mode 100644 index 000000000..b7cad9619 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6230-date-parse-legacy.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6230-date-parse-legacy.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6231-date-local-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6231-date-local-fields.ts.json new file mode 100644 index 000000000..6f078d295 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6231-date-local-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6231-date-local-fields.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6232-date-fields-js.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6232-date-fields-js.cjs.json new file mode 100644 index 000000000..10f48c83a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6232-date-fields-js.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6232-date-fields-js.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6233-object-identity-dynamic.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6233-object-identity-dynamic.cjs.json new file mode 100644 index 000000000..a5dd87184 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6233-object-identity-dynamic.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6233-object-identity-dynamic.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6234-object-prototype-values.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6234-object-prototype-values.cjs.json new file mode 100644 index 000000000..bce305944 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6234-object-prototype-values.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6234-object-prototype-values.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6235-object-identity-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6235-object-identity-typed.ts.json new file mode 100644 index 000000000..c48f4164b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6235-object-identity-typed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6235-object-identity-typed.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6236-builtin-function-values.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6236-builtin-function-values.cjs.json new file mode 100644 index 000000000..21b9f85c2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6236-builtin-function-values.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6236-builtin-function-values.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6400-dyn-error-stack-property-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6400-dyn-error-stack-property-js.js.json new file mode 100644 index 000000000..7bea48883 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6400-dyn-error-stack-property-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6400-dyn-error-stack-property-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6401-dyn-error-own-property-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6401-dyn-error-own-property-names.ts.json new file mode 100644 index 000000000..cf96c4f96 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6401-dyn-error-own-property-names.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6401-dyn-error-own-property-names.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6402-dyn-error-own-property-names-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6402-dyn-error-own-property-names-js.js.json new file mode 100644 index 000000000..49cf88884 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6402-dyn-error-own-property-names-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6402-dyn-error-own-property-names-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6403-error-own-name-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6403-error-own-name-order.ts.json new file mode 100644 index 000000000..da39be0a0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6403-error-own-name-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6403-error-own-name-order.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6404-error-own-name-order-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6404-error-own-name-order-js.js.json new file mode 100644 index 000000000..b19997ac3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6404-error-own-name-order-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6404-error-own-name-order-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6420-process-send-presence.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6420-process-send-presence.ts.json new file mode 100644 index 000000000..69db221e2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6420-process-send-presence.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6420-process-send-presence.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6421-process-send-presence-fork/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6421-process-send-presence-fork/main.ts.json new file mode 100644 index 000000000..abde055bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6421-process-send-presence-fork/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6421-process-send-presence-fork/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6422-process-exitcode-read.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6422-process-exitcode-read.ts.json new file mode 100644 index 000000000..15abd3cdc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6422-process-exitcode-read.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6422-process-exitcode-read.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6423-process-exitcode-default.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6423-process-exitcode-default.ts.json new file mode 100644 index 000000000..31bf2584e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6423-process-exitcode-default.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6423-process-exitcode-default.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6424-process-exit-keeps-exitcode.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6424-process-exit-keeps-exitcode.ts.json new file mode 100644 index 000000000..ce051e3fa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6424-process-exit-keeps-exitcode.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6424-process-exit-keeps-exitcode.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6425-stream-hook-wider-chunk.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6425-stream-hook-wider-chunk.ts.json new file mode 100644 index 000000000..4c6db1400 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6425-stream-hook-wider-chunk.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6425-stream-hook-wider-chunk.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6426-promise-race-void-entry.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6426-promise-race-void-entry.ts.json new file mode 100644 index 000000000..e4d66ecf4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6426-promise-race-void-entry.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6426-promise-race-void-entry.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6427-http-request-options-type.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6427-http-request-options-type.ts.json new file mode 100644 index 000000000..3e403ece6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6427-http-request-options-type.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6427-http-request-options-type.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6428-promisify-callback-functions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6428-promisify-callback-functions.ts.json new file mode 100644 index 000000000..76c3e65d0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6428-promisify-callback-functions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6428-promisify-callback-functions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6429-esm-js-dirname.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6429-esm-js-dirname.mjs.json new file mode 100644 index 000000000..0168f85ee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6429-esm-js-dirname.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6429-esm-js-dirname.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6445-rest-function-fixed-slot.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6445-rest-function-fixed-slot.ts.json new file mode 100644 index 000000000..b39c2fa38 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6445-rest-function-fixed-slot.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6445-rest-function-fixed-slot.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6446-rest-function-nested-declaration.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6446-rest-function-nested-declaration.ts.json new file mode 100644 index 000000000..41c034395 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6446-rest-function-nested-declaration.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6446-rest-function-nested-declaration.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-builtin-base-class-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-builtin-base-class-values.ts.json new file mode 100644 index 000000000..bdd8fcbc8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-builtin-base-class-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6450-builtin-base-class-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-promise-race-never-entry.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-promise-race-never-entry.ts.json new file mode 100644 index 000000000..bb709e7f3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-promise-race-never-entry.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6450-promise-race-never-entry.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6451-array-length-fill-spread.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6451-array-length-fill-spread.ts.json new file mode 100644 index 000000000..0302374fc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6451-array-length-fill-spread.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6451-array-length-fill-spread.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6451-builtin-base-class-expressions.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6451-builtin-base-class-expressions.cjs.json new file mode 100644 index 000000000..34cde5811 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6451-builtin-base-class-expressions.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6451-builtin-base-class-expressions.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-promise-reject-async-return.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-promise-reject-async-return.ts.json new file mode 100644 index 000000000..dda9bd1e8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-promise-reject-async-return.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6470-promise-reject-async-return.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-union-literal-results-into-record.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-union-literal-results-into-record.ts.json new file mode 100644 index 000000000..1a02071a1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-union-literal-results-into-record.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6471-union-literal-results-into-record.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-asserted-member-read.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-asserted-member-read.ts.json new file mode 100644 index 000000000..dfb88018b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-asserted-member-read.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6472-asserted-member-read.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6473-unknown-spread-into-index-record.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6473-unknown-spread-into-index-record.ts.json new file mode 100644 index 000000000..abf47b7db --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6473-unknown-spread-into-index-record.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6473-unknown-spread-into-index-record.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6474-index-record-spread-literals.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6474-index-record-spread-literals.ts.json new file mode 100644 index 000000000..8991f227d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6474-index-record-spread-literals.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6474-index-record-spread-literals.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6475-dynamic-spread-literals.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6475-dynamic-spread-literals.ts.json new file mode 100644 index 000000000..29207c929 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6475-dynamic-spread-literals.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6475-dynamic-spread-literals.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-boolean-callbacks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-boolean-callbacks.ts.json new file mode 100644 index 000000000..3279178b8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-boolean-callbacks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6480-boolean-callbacks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-catch-typeof-untyped-throws.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-catch-typeof-untyped-throws.ts.json new file mode 100644 index 000000000..38b99f9fc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-catch-typeof-untyped-throws.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6480-catch-typeof-untyped-throws.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-generic-callback-extended-results.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-generic-callback-extended-results.ts.json new file mode 100644 index 000000000..d3b5c41b7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-generic-callback-extended-results.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6480-generic-callback-extended-results.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6481-enum-runtime-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6481-enum-runtime-keys.ts.json new file mode 100644 index 000000000..7fc1885aa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6481-enum-runtime-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6481-enum-runtime-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6482-promise-all-tuples.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6482-promise-all-tuples.ts.json new file mode 100644 index 000000000..5ceea99a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6482-promise-all-tuples.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6482-promise-all-tuples.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6483-nullable-regex-receivers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6483-nullable-regex-receivers.ts.json new file mode 100644 index 000000000..6a6263bbc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6483-nullable-regex-receivers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6483-nullable-regex-receivers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/comma-null-union-return.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/comma-null-union-return.cjs.json new file mode 100644 index 000000000..1fad3c559 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/comma-null-union-return.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/comma-null-union-return.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/local-class-detached-method.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/local-class-detached-method.ts.json new file mode 100644 index 000000000..6d311fce9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/local-class-detached-method.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/local-class-detached-method.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-literal-accessors-dyn.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-literal-accessors-dyn.ts.json new file mode 100644 index 000000000..ffe0c75d3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-literal-accessors-dyn.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/object-literal-accessors-dyn.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/required-module-truthiness.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/required-module-truthiness.cjs.json new file mode 100644 index 000000000..101bb2d57 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/required-module-truthiness.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/required-module-truthiness.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/strict-arguments-parameters.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/strict-arguments-parameters.cjs.json new file mode 100644 index 000000000..8c9cc9279 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/strict-arguments-parameters.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/strict-arguments-parameters.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/boot-path-node-surface-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/boot-path-node-surface-fences.ts.json new file mode 100644 index 000000000..de58c3423 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/boot-path-node-surface-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/boot-path-node-surface-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-base-class-value-arguments.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-base-class-value-arguments.ts.json new file mode 100644 index 000000000..622882c24 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-base-class-value-arguments.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/builtin-base-class-value-arguments.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-constructor-members.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-constructor-members.ts.json new file mode 100644 index 000000000..ae3a963f3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-constructor-members.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/builtin-constructor-members.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-field-accessor-descriptor.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-field-accessor-descriptor.ts.json new file mode 100644 index 000000000..3d6639169 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-field-accessor-descriptor.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/class-field-accessor-descriptor.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inferred-callback-drops-conditional.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inferred-callback-drops-conditional.ts.json new file mode 100644 index 000000000..2014c8620 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inferred-callback-drops-conditional.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/inferred-callback-drops-conditional.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inferred-callback-drops-satisfies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inferred-callback-drops-satisfies.ts.json new file mode 100644 index 000000000..44c5e8bbc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inferred-callback-drops-satisfies.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/inferred-callback-drops-satisfies.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inferred-callback-drops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inferred-callback-drops.ts.json new file mode 100644 index 000000000..a6b592336 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inferred-callback-drops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/inferred-callback-drops.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-completion-callback-error.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-completion-callback-error.ts.json new file mode 100644 index 000000000..af31bb8cf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-completion-callback-error.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/stream-completion-callback-error.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/lookup-and-header-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/lookup-and-header-entries.ts.json new file mode 100644 index 000000000..13a440abb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/lookup-and-header-entries.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/lookup-and-header-entries.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/promisify-callback-functions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/promisify-callback-functions.ts.json new file mode 100644 index 000000000..a4a753c3d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/promisify-callback-functions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/promisify-callback-functions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/stream-subclass-hooks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/stream-subclass-hooks.ts.json new file mode 100644 index 000000000..295d7deda --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/stream-subclass-hooks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/stream-subclass-hooks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2558-bundled-chunks/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2558-bundled-chunks/main.ts.json new file mode 100644 index 000000000..cc5bc69c8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2558-bundled-chunks/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2558-bundled-chunks/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2559-cjs-cycle/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2559-cjs-cycle/main.ts.json new file mode 100644 index 000000000..382eef7b4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2559-cjs-cycle/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2559-cjs-cycle/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2560-cjs-cycle-partner-first/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2560-cjs-cycle-partner-first/main.ts.json new file mode 100644 index 000000000..f609f128e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2560-cjs-cycle-partner-first/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2560-cjs-cycle-partner-first/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2561-cjs-cycle-early-read/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2561-cjs-cycle-early-read/main.ts.json new file mode 100644 index 000000000..be3b2af16 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2561-cjs-cycle-early-read/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2561-cjs-cycle-early-read/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2562-subpath-leaf/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2562-subpath-leaf/main.ts.json new file mode 100644 index 000000000..68cde3de1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2562-subpath-leaf/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2562-subpath-leaf/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2563-bundled-esm-inits/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2563-bundled-esm-inits/main.ts.json new file mode 100644 index 000000000..953a75d7d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2563-bundled-esm-inits/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2563-bundled-esm-inits/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2564-chunk-helper-shadowing/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2564-chunk-helper-shadowing/main.ts.json new file mode 100644 index 000000000..90e304cbe --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2564-chunk-helper-shadowing/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2564-chunk-helper-shadowing/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2565-chunk-shared-function/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2565-chunk-shared-function/main.ts.json new file mode 100644 index 000000000..6443d170b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2565-chunk-shared-function/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2565-chunk-shared-function/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2566-cjs-cycle-indirect-call/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2566-cjs-cycle-indirect-call/main.ts.json new file mode 100644 index 000000000..f51a29951 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2566-cjs-cycle-indirect-call/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2566-cjs-cycle-indirect-call/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2567-esm-namespace-this/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2567-esm-namespace-this/main.ts.json new file mode 100644 index 000000000..0739a6f0c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2567-esm-namespace-this/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2567-esm-namespace-this/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2571-cjs-cycle-stored-function/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2571-cjs-cycle-stored-function/main.ts.json new file mode 100644 index 000000000..c3e0442f9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2571-cjs-cycle-stored-function/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2571-cjs-cycle-stored-function/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2580-lazy-barrel/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2580-lazy-barrel/main.ts.json new file mode 100644 index 000000000..08a17ffe0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2580-lazy-barrel/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2580-lazy-barrel/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2581-lazy-barrel-require/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2581-lazy-barrel-require/main.ts.json new file mode 100644 index 000000000..6fdd9687e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2581-lazy-barrel-require/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2581-lazy-barrel-require/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2582-npm-alias-two-releases/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2582-npm-alias-two-releases/main.ts.json new file mode 100644 index 000000000..8ab899372 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2582-npm-alias-two-releases/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2582-npm-alias-two-releases/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2583-cjs-function-exports/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2583-cjs-function-exports/main.ts.json new file mode 100644 index 000000000..caf2144ad --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2583-cjs-function-exports/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2583-cjs-function-exports/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2584-cjs-conditional-exports/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2584-cjs-conditional-exports/main.ts.json new file mode 100644 index 000000000..e255c754a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2584-cjs-conditional-exports/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2584-cjs-conditional-exports/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/cjs-exports-var/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/cjs-exports-var/main.ts.json new file mode 100644 index 000000000..9ec4bbdeb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/cjs-exports-var/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/cjs-exports-var/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/tsc-default-object/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/tsc-default-object/main.ts.json new file mode 100644 index 000000000..7ce71c66f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/tsc-default-object/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/tsc-default-object/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/rpc-api.test.ts b/packages/compiler/test/ts7/rpc-api.test.ts index 28b63ec34..a8b4aebce 100644 --- a/packages/compiler/test/ts7/rpc-api.test.ts +++ b/packages/compiler/test/ts7/rpc-api.test.ts @@ -71,7 +71,16 @@ test("production RPC bridge preserves pinned SDK parser/checker behavior", () => JSON.stringify({ compilerOptions: { strict: true, noEmit: true, types: [] }, files: [path] }), ); writeFileSync(path, 'export const answer = 42;\nexport const invalid: number = "wrong";\n'); - const nativeClient = new Ts7Api({ cwd: dir, fs: fallback }); + // Compared with the pinned SDK, which runs tsgo, so the bridge runs tsgo too. + const checker = process.env["SCRIPTC_CHECKER"]; + process.env["SCRIPTC_CHECKER"] = "tsgo"; + let nativeClient: Ts7Api; + try { + nativeClient = new Ts7Api({ cwd: dir, fs: fallback }); + } finally { + if (checker === undefined) delete process.env["SCRIPTC_CHECKER"]; + else process.env["SCRIPTC_CHECKER"] = checker; + } const sdk = new API({ cwd: dir }); try { expect(nativeClient.parseConfigFile(config)).toEqual(sdk.parseConfigFile(config)); diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index 246d27f0a..935736612 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -935,12 +935,19 @@ static void scr_sa_trim(void) { * interrupts every running thread of the process to flush its TLB, and * glibc's trim locks the arenas other threads allocate from, so discards * wait for the last worker of a group, which hands them to one helper; - * - the helper waits SCR_SA_DISCARD_DELAY_MS (100 by default) and stops if + * - the helper waits a delay (scr_sa_discard_delay_ms: 1 s to start, + * doubled each time workers start again soon after a discard) and stops if * a worker started meanwhile: a program that starts its next workers or * exits within that time does not return pages it would fault back in or * never touch again, and the last worker of the next group hands the * discards to a new helper; a thread that finds every arena claimed * discards one waiting arena and claims it; + * - once the event loop of the thread that is not a worker runs, the + * helper then discards one arena at a time, and only while that loop + * waits with nothing ready (scr_sa_loop_waiting): a program that does + * its next work on that thread alone (a compiler rebuilding one file in + * watch mode) would otherwise have it slowed by the discards. A loop that + * does not wait gets them SCR_SA_DISCARD_DELAY_MAX_MS after the delay; * - the helper then returns the pages * it carved (MADV_DONTNEED; the start of the first page, which it may * share with older blocks, is cleared), frees the recorded system blocks, @@ -1214,12 +1221,20 @@ void scr_sa_thread_shares(void) { /* Worker threads that started and have not parked. */ static _Atomic(unsigned) scr_sa_workers_live; +#ifdef SCR_SA_TRIM +static void scr_sa_group_starting(void); +#endif + /* Library archives run on their embedder's threads and keep their memory. */ static void scr_sa_worker_starting(void) { #ifndef SCR_LIB scr_sa_worker = true; +#ifdef SCR_SA_TRIM + if (atomic_fetch_add_explicit(&scr_sa_workers_live, 1, memory_order_relaxed) == 0) scr_sa_group_starting(); +#else atomic_fetch_add_explicit(&scr_sa_workers_live, 1, memory_order_relaxed); #endif +#endif } #define SCR_SA_WORKER_STARTING() scr_sa_worker_starting() #endif @@ -1309,28 +1324,91 @@ static bool scr_sa_drain_some(bool one) { static bool scr_sa_drain(void) { return scr_sa_drain_some(false); } -/* SCR_SA_DISCARD_DELAY_MS: how long the trim helper waits after the last - * worker parks before it returns their memory (default 100). */ -static long scr_sa_discard_delay_ms(void) { - const char *delay = getenv("SCR_SA_DISCARD_DELAY_MS"); - if (!delay) return 100; - long ms = strtol(delay, NULL, 10); +/* How long the trim helper waits after the last worker parks before it + * returns their memory: SCR_SA_DISCARD_DELAY_MS (1000 by default) to start. + * Each time workers start again within ten delays of a discard, the delay + * doubles, up to SCR_SA_DISCARD_DELAY_MAX_MS (10000 by default): a program + * that keeps starting workers (a compiler rebuilding in watch mode, a + * server) would fault the returned pages straight back in. Memory that + * stays idle for the grown delay still goes back. */ +static _Atomic(long) scr_sa_delay_grown_ms; /* 0 until a discard proved too early */ +static _Atomic(long) scr_sa_discarded_ms; /* when the helper last discarded; 0: not since */ + +static long scr_sa_env_ms(const char *name, long fallback) { + const char *value = getenv(name); + if (!value) return fallback; + long ms = strtol(value, NULL, 10); return ms < 0 ? 0 : ms; } +static long scr_sa_discard_delay_ms(void) { + long start = scr_sa_env_ms("SCR_SA_DISCARD_DELAY_MS", 1000); + long max = scr_sa_env_ms("SCR_SA_DISCARD_DELAY_MAX_MS", 10000); + long grown = atomic_load_explicit(&scr_sa_delay_grown_ms, memory_order_relaxed); + long delay = grown > start ? grown : start; + return max >= start && delay > max ? max : delay; +} + +static long scr_sa_now_ms(void) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (long)now.tv_sec * 1000 + now.tv_nsec / 1000000 + 1; +} + +/* The first worker of a group starts: a discard shortly before means the + * delay was too short for this program. */ +static void scr_sa_group_starting(void) { + long at = atomic_exchange_explicit(&scr_sa_discarded_ms, 0, memory_order_relaxed); + if (!at) return; + long delay = scr_sa_discard_delay_ms(); + if (scr_sa_now_ms() - at >= 10 * (delay > 10 ? delay : 10)) return; + long max = scr_sa_env_ms("SCR_SA_DISCARD_DELAY_MAX_MS", 10000); + long grown = delay > 0 ? 2 * delay : 1; + if (grown > max) grown = max; + if (grown > atomic_load_explicit(&scr_sa_delay_grown_ms, memory_order_relaxed)) + atomic_store_explicit(&scr_sa_delay_grown_ms, grown, memory_order_relaxed); +} + +/* The event loop of the thread that is not a worker: an odd count while it + * waits with nothing ready, an even one while it runs, 0 until it first + * waits. Only that thread writes it. */ +static _Atomic(unsigned long) scr_sa_loop_turns; + +void scr_sa_loop_waiting(bool waiting) { + if (scr_sa_worker) return; + unsigned long turns = atomic_load_explicit(&scr_sa_loop_turns, memory_order_relaxed); + if ((bool)(turns & 1) != waiting) atomic_store_explicit(&scr_sa_loop_turns, turns + 1, memory_order_relaxed); +} + /* Waits out the delay first, and stops if a worker starts meanwhile: the * discards then wait for the last worker of that group. A program that exits * within the delay does not spend its last milliseconds returning pages, and * one that starts its next workers within it does not return pages while - * they start. */ + * they start. Once the event loop runs, each discard then waits until the + * loop has been in one wait for a step, or until the maximum delay has + * passed. */ static void *scr_sa_drain_main(void *arg) { (void)arg; - for (long left = scr_sa_discard_delay_ms(); left > 0; left -= 5) { - struct timespec pause = {0, (left < 5 ? left : 5) * 1000000L}; + long delay = scr_sa_discard_delay_ms(), step = delay > 200 ? 20 : 5; + for (long left = delay; left > 0; left -= step) { + struct timespec pause = {0, (left < step ? left : step) * 1000000L}; nanosleep(&pause, NULL); if (atomic_load_explicit(&scr_sa_workers_live, memory_order_acquire) != 0) return NULL; } - if (atomic_load_explicit(&scr_sa_workers_live, memory_order_acquire) == 0) scr_sa_drain(); + long max = scr_sa_env_ms("SCR_SA_DISCARD_DELAY_MAX_MS", 10000); + unsigned long seen = 0; + for (long waited = 0; atomic_load_explicit(&scr_sa_workers_live, memory_order_acquire) == 0;) { + unsigned long turns = atomic_load_explicit(&scr_sa_loop_turns, memory_order_relaxed); + if (turns == 0 || ((turns & 1) && turns == seen) || waited >= max) { + if (!scr_sa_drain_some(true)) break; + atomic_store_explicit(&scr_sa_discarded_ms, scr_sa_now_ms(), memory_order_relaxed); + continue; + } + seen = turns; + struct timespec pause = {0, step * 1000000L}; + nanosleep(&pause, NULL); + waited += step; + } return NULL; } @@ -1361,6 +1439,7 @@ void scr_sa_wait_trims(void) { #else void scr_sa_wait_trims(void) {} void scr_sa_thread_shares(void) {} +void scr_sa_loop_waiting(bool waiting) { (void)waiting; } #endif static void scr_sa_park_arena(void); @@ -1564,6 +1643,7 @@ void *scr_mem_realloc(void *p, size_t n) { return realloc(p, n); } void scr_sa_thread_park(void) {} void scr_sa_wait_trims(void) {} void scr_sa_thread_shares(void) {} +void scr_sa_loop_waiting(bool waiting) { (void)waiting; } void scr_sa_free_foreign(void *p) { free(p); } #endif #endif diff --git a/packages/runtime/src/scr_alloc.test.c b/packages/runtime/src/scr_alloc.test.c index aa6b0b60b..6c83afdf9 100644 --- a/packages/runtime/src/scr_alloc.test.c +++ b/packages/runtime/src/scr_alloc.test.c @@ -700,11 +700,91 @@ static void check_discard_delay(void) { assert(pthread_join(holder, NULL) == 0); assert(pthread_join(plain, NULL) == 0); scr_sa_wait_trims(); - unsetenv("SCR_SA_DISCARD_DELAY_MS"); + setenv("SCR_SA_DISCARD_DELAY_MS", "100", 1); assert(resident_pages(first, discard_hi) < total / 2); discard_in_old_arena = true; } +/* A discard that workers follow within ten delays was too early: the next + * delay is twice as long, and memory idle for that long still goes back. */ +static void *plain_discard_worker(void *arg) { + (void)arg; + scr_sa_thread_starting(); + allocate_discardable(); + scr_sa_thread_park(); + return NULL; +} + +static void check_discard_delay_grows(void) { + size_t page = (size_t)sysconf(_SC_PAGESIZE); + setenv("SCR_SA_DISCARD_DELAY_MS", "100", 1); + setenv("SCR_SA_DISCARD_DELAY_MAX_MS", "10000", 1); + discard_blocks = malloc(DISCARD_BLOCKS * sizeof *discard_blocks); + assert(discard_blocks); + /* A plain thread holds the arena whose slice the earlier checks filled. */ + atomic_store(&discard_step, 0); + pthread_t plain; + assert(pthread_create(&plain, NULL, holding_thread, NULL) == 0); + wait_step(1); + run(plain_discard_worker); /* returned after the delay */ + pthread_t t; + assert(pthread_create(&t, NULL, plain_discard_worker, NULL) == 0); + assert(pthread_join(t, NULL) == 0); + uintptr_t first = (discard_lo + page - 1) & ~(uintptr_t)(page - 1); + size_t total = (discard_hi - first) / page; + struct timespec pause = {0, 140000000L}; + nanosleep(&pause, NULL); + assert(resident_pages(first, discard_hi) > total / 2); /* the delay is 200 ms or more now */ + scr_sa_wait_trims(); + assert(resident_pages(first, discard_hi) < total / 2); + setenv("SCR_SA_DISCARD_DELAY_MS", "100", 1); + setenv("SCR_SA_DISCARD_DELAY_MAX_MS", "0", 1); + atomic_store(&discard_step, 3); + assert(pthread_join(plain, NULL) == 0); + free(discard_blocks); +} + +/* Once the event loop runs, discards wait for it to wait with nothing + * ready; a loop that does not wait gets them SCR_SA_DISCARD_DELAY_MAX_MS + * after the delay. The test's thread plays the loop's. */ +static void check_discard_waits_for_loop(void) { + size_t page = (size_t)sysconf(_SC_PAGESIZE); + setenv("SCR_SA_DISCARD_DELAY_MS", "100", 1); + setenv("SCR_SA_DISCARD_DELAY_MAX_MS", "10000", 1); + discard_blocks = malloc(DISCARD_BLOCKS * sizeof *discard_blocks); + assert(discard_blocks); + atomic_store(&discard_step, 0); + pthread_t plain; + assert(pthread_create(&plain, NULL, holding_thread, NULL) == 0); + wait_step(1); + scr_sa_loop_waiting(true); + scr_sa_loop_waiting(false); /* the loop runs */ + pthread_t t; + assert(pthread_create(&t, NULL, plain_discard_worker, NULL) == 0); + assert(pthread_join(t, NULL) == 0); + uintptr_t first = (discard_lo + page - 1) & ~(uintptr_t)(page - 1); + size_t total = (discard_hi - first) / page; + struct timespec pause = {0, 900000000L}; + nanosleep(&pause, NULL); + assert(resident_pages(first, discard_hi) > total / 2); /* past the delay */ + scr_sa_loop_waiting(true); + scr_sa_wait_trims(); + assert(resident_pages(first, discard_hi) < total / 2); + /* A loop that keeps running. */ + scr_sa_loop_waiting(false); + setenv("SCR_SA_DISCARD_DELAY_MAX_MS", "300", 1); + assert(pthread_create(&t, NULL, plain_discard_worker, NULL) == 0); + assert(pthread_join(t, NULL) == 0); + first = (discard_lo + page - 1) & ~(uintptr_t)(page - 1); + total = (discard_hi - first) / page; + scr_sa_wait_trims(); + assert(resident_pages(first, discard_hi) < total / 2); + setenv("SCR_SA_DISCARD_DELAY_MAX_MS", "0", 1); + atomic_store(&discard_step, 3); + assert(pthread_join(plain, NULL) == 0); + free(discard_blocks); +} + static void check_discard(void) { discard_blocks = malloc(DISCARD_BLOCKS * sizeof *discard_blocks); assert(discard_blocks); @@ -819,10 +899,15 @@ int main(void) { check_heap_huge_pages(); #endif #if defined(SCR_WORKERS) && SCR_SMALL_ALLOC + /* A short discard delay, which stays at its start until check_discard_delay_grows. */ + setenv("SCR_SA_DISCARD_DELAY_MS", "100", 1); + setenv("SCR_SA_DISCARD_DELAY_MAX_MS", "0", 1); check_threads(); #ifdef __linux__ check_trim(); check_discard(); + check_discard_delay_grows(); + check_discard_waits_for_loop(); #endif printf("allocator checks passed: size-class workers\n"); #else diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 155f91be4..8185422af 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -4410,6 +4410,9 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { #endif #if !defined(_WIN32) && !defined(__wasi__) if (due > now) scr_fiber_stacks_clear(); +#endif +#ifdef SCR_WORKERS + if (due > now) scr_sa_loop_waiting(true); #endif bool evw = scr_events_watching_fn != NULL && scr_events_watching_fn(); #ifdef SCR_WORKERS @@ -4564,6 +4567,9 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { now = due; } } +#ifdef SCR_WORKERS + scr_sa_loop_waiting(false); +#endif while (scr_ntimers > 0 && scr_timers[0].deadline_ms <= now) { ScrTimer t = scr_timer_pop(); /* Timer callbacks are plain sync closures, run on the main stack. diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index 1cdd466c3..ec4dad794 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -1437,24 +1437,29 @@ static void scr_dyn_thenable_schedule(ScrPromise *target, const ScrDyn *thenable * a thenable gets a fresh promise and its resolve job, anything else a * fulfilled promise (a throwing `then` read rejects it). The element * joins the combinator right after, so a thenable's job sits between - * the jobs of the entries around it, exactly Node's iteration. */ -static ScrPromise *scr_dyn_promise_entry(ScrCombine *c, ScrDyn *value, const ScrDyn *reader) { + * the jobs of the entries around it, exactly Node's iteration. Without a + * combinator (`c` NULL) this is PromiseResolve for one value. */ +static ScrPromise *scr_dyn_promise_entry(ScrCombine *c, const ScrDyn *value, const ScrDyn *reader) { if (value->kind == SCR_DYN_PROMISE) return scr_promise_retain(value->v.promise); ScrPromise *source = scr_promise_new(); ScrDyn *then = scr_dyn_then_of(value, reader); if (scr_exc_pending()) { scr_promise_reject_pending(source); } else if (then) { - scr_promise_combine_flush(c); + if (c) scr_promise_combine_flush(c); scr_dyn_thenable_schedule(source, value, then, reader); } else { - scr_promise_fulfill_ref(source, scr_dyn_retain(value), scr_dyn_retain_v, scr_dyn_release_v, - scr_dyn_trace_v); + scr_promise_fulfill_ref(source, scr_dyn_retain((ScrDyn *)value), scr_dyn_retain_v, + scr_dyn_release_v, scr_dyn_trace_v); } scr_dyn_release(then); return source; } +ScrPromise *scr_dyn_promise_resolve(const ScrDyn *value, const ScrDyn *reader) { + return scr_dyn_promise_entry(NULL, value, reader); +} + static void scr_dyn_settled_store(ScrArr *a, double i, ScrPromise *src) { ScrDyn *record = scr_dyn_new_obj(); bool fulfilled = !scr_promise_is_rejected(src); diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index 184e3be9b..ec685656d 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -6410,6 +6410,7 @@ void scr_child_ipc_on_disconnect(ScrChild *c, ScrClosure *cb, bool once) { bool scr_process_ipc_connected(void) { return scr_process_ipc != NULL && scr_process_ipc->connected; } +bool scr_process_ipc_open(void) { return scr_process_ipc != NULL; } bool scr_process_ipc_send(ScrStr *json) { return scr_ipc_send(scr_process_ipc, json, NULL, NULL); } diff --git a/packages/runtime/src/scr_child.h b/packages/runtime/src/scr_child.h index 6492aefe8..f59e02e74 100644 --- a/packages/runtime/src/scr_child.h +++ b/packages/runtime/src/scr_child.h @@ -182,6 +182,9 @@ void scr_child_ipc_on_disconnect(ScrChild *c, ScrClosure *cb /*moves*/, bool once); double scr_process_fork_target(double target_count); bool scr_process_ipc_connected(void); +/* Whether this fork child opened its channel (process.send's presence in + * a program with fork targets; disconnecting keeps it). */ +bool scr_process_ipc_open(void); bool scr_process_ipc_send(ScrStr *json); bool scr_process_ipc_send_cb(ScrStr *json, ScrClosure *cb /*moves*/, ScrIpcSendFn fn); diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index a3d506a10..fb1bc1548 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -646,6 +646,9 @@ ScrDyn *scr_dyn_native_search_params(ScrSearchParams *value); bool scr_dyn_native_search_params_is(const ScrDyn *value); bool scr_dyn_native_date_is(const ScrDyn *value); ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments); +ScrDyn *scr_dyn_native_date_new_fields(const ScrDyn *arguments); +ScrDyn *scr_dyn_native_date_new_value(const ScrDyn *arguments); +ScrStr *scr_dyn_object_tag_of(const ScrDyn *d); ScrDyn *scr_dyn_native_date_from_ms(double ms); double scr_dyn_native_date_value(const ScrDyn *value); ScrDyn *scr_dyn_native_regex(ScrRegex *value); @@ -697,6 +700,19 @@ ScrDyn *scr_dyn_symbol_ref(ScrSym *value, void (*release)(ScrSym *), ScrStr *(*r ScrDyn *scr_dyn_new_str(ScrStr *s); ScrDyn *scr_dyn_new_arr(void); ScrDyn *scr_dyn_array_constructor(void); +/* The interned builtin constructor `name` (Object, Date, RegExp, Map, Set, + * Promise, WeakMap, WeakSet) as a dynamic function value: +1. */ +ScrDyn *scr_dyn_global_constructor(const ScrStr *name); +/* A builtin global function (setTimeout, setImmediate, process.nextTick, ...) + * as an interned dynamic function value: +1. */ +ScrDyn *scr_dyn_builtin_function(const ScrStr *name); +/* A constant-key read of an Object.prototype member name: takes the + * ordinary read's +1 result and answers the inherited member in place of + * an undefined one (scr_dyn_invoke.c). */ +ScrDyn *scr_dyn_inherited_read(const ScrDyn *d, const ScrStr *key, ScrDyn *read); +/* The member `key` read on Object.prototype itself, or NULL for any other + * receiver (scr_dyn_invoke.c). */ +ScrDyn *scr_dyn_object_prototype_member(const ScrDyn *d, const ScrStr *key); bool scr_dyn_arr_has_index(const ScrDyn *arr, size_t index); void scr_dyn_arr_push_hole(ScrDyn *arr); ScrDyn *scr_dyn_new_obj(void); @@ -937,7 +953,17 @@ double scr_dyn_arr_len(const ScrDyn *d); ScrDyn *scr_dyn_arr_at(const ScrDyn *d, double i); ScrDyn *scr_dyn_arr_named_get(const ScrDyn *d, const ScrStr *key); void scr_dyn_obj_set(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value); +void scr_dyn_obj_set_field(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value); ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor); +void scr_dyn_class_field_attributes(ScrDyn *view, ScrStr *name, double bits); +void scr_dyn_class_view_attributes(ScrDyn *view, double bits); +ScrDyn *scr_class_define_field(ScrDyn *current, double field_bits, double object_bits, ScrStr *name, + ScrDyn *descriptor); +double scr_class_defined_bits(ScrDyn *holder, ScrStr *name); +ScrDyn *scr_class_defined_value(ScrDyn *holder, ScrStr *name); +bool scr_class_descriptor_has_value(ScrDyn *descriptor); +void scr_class_throw_read_only(ScrStr *field, ScrStr *owner); +void scr_class_throw_not_extensible(ScrStr *field); ScrDyn *scr_dyn_get_own_property_descriptor(ScrDyn *target, ScrDyn *key); ScrDyn *scr_dyn_array_proto_call(ScrDyn *target, ScrStr *method, ScrDyn *args); ScrDyn *scr_dyn_array_prototype(void); /* +1, installs native method adapters */ @@ -1066,6 +1092,12 @@ extern SCR_TL size_t scr_uncloneable_count; ScrDyn *scr_dyn_from_error(const ScrError *e); void scr_error_register_dyn(const ScrVt *vt, ScrDyn *(*box)(void *)); ScrDyn *scr_error_dyn_fields(const ScrError *e); +void scr_error_dyn_place_name(ScrDyn *view, const char *after, size_t after_len); +/* An accessor property of a fresh object: enumerable and configurable, + * with the given getter and setter (either may be NULL). Moves both. */ +void scr_dyn_obj_set_accessor(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *getter, ScrDyn *setter); +/* The getter or setter of an own accessor property, +1 (undefined when none). */ +ScrDyn *scr_dyn_obj_accessor_fn(const ScrDyn *obj, const char *key, size_t key_len, bool setter); void scr_error_commit_dyn(ScrError *e, const ScrDyn *view); /* The reverse extraction, riding the same identity cache: a dyn error * that came from a runtime ScrError answers THAT instance (+1; @@ -1309,6 +1341,10 @@ extern SCR_TL ScrDyn *(*scr_dyn_promise_member_fn)(const char *key, size_t len); ScrPromise *scr_dyn_promise_all(const ScrDyn *entries, const ScrDyn *reader); ScrPromise *scr_dyn_promise_all_settled(const ScrDyn *entries, const ScrDyn *reader); ScrPromise *scr_dyn_promise_any(const ScrDyn *entries, const ScrDyn *reader); +/* PromiseResolve over one dyn value (borrowed): a native promise is + * itself, a thenable adopts through its resolve job, a throwing `then` + * read rejects, and any other value fulfills. +1. */ +ScrPromise *scr_dyn_promise_resolve(const ScrDyn *value, const ScrDyn *reader); /* Native predicates for typed reference capsules and boxed errors. */ bool scr_dyn_ref_typeof_is(const ScrDyn *d, const char *name); diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 90e8d0363..782fab829 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -1671,7 +1671,7 @@ static ScrDyn *dyn_builtin_method_call(ScrClosure *closure, ScrDyn *const *args, if (!strcmp(prototype->data, "Object")) { ScrDyn *key = argc ? args[0] : scr_dyn_undefined(); if (!strcmp(method->data, "toString")) { - ScrStr *tag = scr_dyn_object_tag(receiver); + ScrStr *tag = scr_dyn_object_tag_of(receiver); if (tag) { result = scr_dyn_new_str(tag); scr_str_release(tag); } } else if (!strcmp(method->data, "hasOwnProperty")) { bool own = scr_dyn_has_own_computed(receiver, key); @@ -1684,7 +1684,8 @@ static ScrDyn *dyn_builtin_method_call(ScrClosure *closure, ScrDyn *const *args, scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); } else if (receiver->kind == SCR_DYN_OBJ || receiver->kind == SCR_DYN_ARR || receiver->kind == SCR_DYN_FUNC || receiver->kind == SCR_DYN_HANDLE || - receiver->kind == SCR_DYN_TYPED_REF || receiver->kind == SCR_DYN_PROXY) result = scr_dyn_retain(receiver); + receiver->kind == SCR_DYN_TYPED_REF || receiver->kind == SCR_DYN_PROXY || + receiver->kind == SCR_DYN_PROMISE || receiver->kind == SCR_DYN_BYTES) result = scr_dyn_retain(receiver); else dyn_throw_unsupported("Object", method->data); } else { bool compatible = false; @@ -1757,6 +1758,358 @@ ScrDyn *scr_dyn_builtin_method(const ScrStr *prototype, const ScrStr *method) { return scr_dyn_retain(value); } +/* ── Builtin constructors, inherited Object.prototype members and builtin + * functions as dynamic values ────────────────────────────────────────── + * This unit links only into programs that use them (the frontend marks + * dyn.globalConstructor, dyn.builtinFunction and constant-key reads of the + * inherited names), so programs that never read them keep their exact + * executables. + * + * JavaScript reads `Object`, `Date`, `RegExp`, `Map`, `Set`, `Promise`, + * `WeakMap` and `WeakSet` as interned functions, and the same functions + * answer `.constructor` on the matching dynamic values, so + * `o.constructor === Object` and `a.constructor === b.constructor` are + * exact identity tests. `Object(v)` works through the stored value; calling + * the others through it is fenced. */ +/* String constants live in named arrays in their own section rather than + * in the unit's merged string pool (the explicit section also keeps the + * compiler from folding the unit's existing literals into them), so + * programs that link this unit for other reasons keep their exact bytes. */ +#if defined(__APPLE__) +#define SCR_DYN_COMPAT_STR __attribute__((section("__TEXT,__scr_dyn_compat"))) +#elif defined(_WIN32) +#define SCR_DYN_COMPAT_STR +#else +#define SCR_DYN_COMPAT_STR __attribute__((section(".rodata.scr_dyn_compat"))) +#endif +static const char dyn_s_error_tag[] SCR_DYN_COMPAT_STR = "[object Error]"; +static const char dyn_s0[] SCR_DYN_COMPAT_STR = "calling the %s constructor%s through a stored value is not supported yet"; +static const char dyn_s1[] SCR_DYN_COMPAT_STR = "calling '%s' through a stored value is not supported yet"; +static const char dyn_s3[] SCR_DYN_COMPAT_STR = "scriptc: invalid global constructor name\n"; +static const char dyn_s4[] SCR_DYN_COMPAT_STR = "propertyIsEnumerable"; +static const char dyn_s5[] SCR_DYN_COMPAT_STR = "ERR_INVALID_ARG_TYPE"; +static const char dyn_s6[] SCR_DYN_COMPAT_STR = "native:constructor"; +static const char dyn_s7[] SCR_DYN_COMPAT_STR = "process.nextTick"; +static const char dyn_s8[] SCR_DYN_COMPAT_STR = " on a primitive"; +static const char dyn_s9[] SCR_DYN_COMPAT_STR = "hasOwnProperty"; +static const char dyn_s10[] SCR_DYN_COMPAT_STR = "native:builtin"; +static const char dyn_s11[] SCR_DYN_COMPAT_STR = "queueMicrotask"; +static const char dyn_s12[] SCR_DYN_COMPAT_STR = "clearImmediate"; +static const char dyn_s13[] SCR_DYN_COMPAT_STR = "setImmediate"; +static const char dyn_s14[] SCR_DYN_COMPAT_STR = "constructor"; +static const char dyn_s15[] SCR_DYN_COMPAT_STR = "setInterval"; +static const char dyn_s16[] SCR_DYN_COMPAT_STR = "setTimeout"; +static const char dyn_s17[] SCR_DYN_COMPAT_STR = "callback"; +static const char dyn_s18[] SCR_DYN_COMPAT_STR = "toString"; +static const char dyn_s20[] SCR_DYN_COMPAT_STR = "Function"; +static const char dyn_s21[] SCR_DYN_COMPAT_STR = "Promise"; +static const char dyn_s22[] SCR_DYN_COMPAT_STR = "WeakMap"; +static const char dyn_s23[] SCR_DYN_COMPAT_STR = "valueOf"; +static const char dyn_s24[] SCR_DYN_COMPAT_STR = "WeakSet"; +static const char dyn_s25[] SCR_DYN_COMPAT_STR = "RegExp"; +static const char dyn_s26[] SCR_DYN_COMPAT_STR = "Object"; +static const char dyn_s27[] SCR_DYN_COMPAT_STR = "SC2020"; +static const char dyn_s28[] SCR_DYN_COMPAT_STR = "clear"; +static const char dyn_s29[] SCR_DYN_COMPAT_STR = "Date"; +static const char dyn_s30[] SCR_DYN_COMPAT_STR = "Map"; +static const char dyn_s31[] SCR_DYN_COMPAT_STR = "Set"; +static const char dyn_s32[] SCR_DYN_COMPAT_STR = ""; + +static const char *const dyn_global_ctor_names[] = { + dyn_s26, dyn_s29, dyn_s25, dyn_s30, dyn_s31, dyn_s21, dyn_s22, dyn_s24, +}; +#define DYN_GLOBAL_CTORS (sizeof dyn_global_ctor_names / sizeof dyn_global_ctor_names[0]) +static SCR_TL ScrDyn *dyn_global_ctors[DYN_GLOBAL_CTORS]; +static SCR_TL bool dyn_global_ctors_registered; + +static void dyn_global_ctors_cleanup(void) { + for (size_t i = 0; i < DYN_GLOBAL_CTORS; i++) { + scr_dyn_release(dyn_global_ctors[i]); + dyn_global_ctors[i] = NULL; + } +} + +static ScrDyn *dyn_global_ctor_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + const char *name = closure->fn; + bool object = strcmp(name, dyn_s26) == 0; + if (object) { + const ScrDyn *value = argc ? args[0] : scr_dyn_undefined(); + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) return scr_dyn_new_obj(); + if (value->kind != SCR_DYN_NUM && value->kind != SCR_DYN_STR && value->kind != SCR_DYN_BOOL && + value->kind != SCR_DYN_BIGINT && value->kind != SCR_DYN_SYMBOL) { + return scr_dyn_retain((ScrDyn *)value); + } + } + char message[128]; + int length = snprintf(message, sizeof message, dyn_s0, + name, object ? dyn_s8 : dyn_s32); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, dyn_s27); + return NULL; +} + +static ScrDyn *dyn_global_ctor(const char *name) { + for (size_t i = 0; i < DYN_GLOBAL_CTORS; i++) { + if (strcmp(dyn_global_ctor_names[i], name) != 0) continue; + if (!dyn_global_ctors_registered) { + dyn_global_ctors_registered = true; + scr_atexit(dyn_global_ctors_cleanup); + } + if (!dyn_global_ctors[i]) { + /* Each constructor's own length: Object 1, Date 7, RegExp 2, Promise 1, + * the collections 0. */ + uint32_t length = i == 0 || i == 5 ? 1 : i == 1 ? 7 : i == 2 ? 2 : 0; + dyn_global_ctors[i] = scr_dyn_new_func(scr_closure_new((void *)dyn_global_ctor_names[i], 0), + dyn_global_ctor_call, length, dyn_s6, + dyn_global_ctor_names[i]); + } + return scr_dyn_retain(dyn_global_ctors[i]); + } + return NULL; +} + +static ScrDyn *dyn_function_probe_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_dyn_retain(scr_dyn_undefined()); +} + +/* The Function constructor: the value any ordinary function answers for + * `.constructor` (scr_json.c keeps its singletons). */ +static ScrDyn *dyn_function_constructor(void) { + ScrDyn *probe = scr_dyn_new_func(scr_closure_new(NULL, 0), dyn_function_probe_call, 0, dyn_s10, dyn_s20); + ScrDyn *ctor = scr_dyn_fn_get(probe, dyn_s14, 11); + scr_dyn_release(probe); + return ctor; +} + +ScrDyn *scr_dyn_global_constructor(const ScrStr *name) { + if (strcmp(name->data, dyn_s20) == 0) return dyn_function_constructor(); + ScrDyn *ctor = dyn_global_ctor(name->data); + if (!ctor) scr_trap(dyn_s3); + return ctor; +} + +/* Object.prototype.toString's tag: errors answer "[object Error]" (the + * shared tag walk in scr_json.c answers "[object Object]" for them). */ +ScrStr *scr_dyn_object_tag_of(const ScrDyn *d) { + if (d->kind == SCR_DYN_OBJ && scr_dyn_err_instanceof(d, 0)) return scr_str_new(dyn_s_error_tag, sizeof dyn_s_error_tag - 1); + return scr_dyn_object_tag(d); +} + +/* Which Object.prototype members a value inherits when no nearer prototype + * defines them: types with their own toString or valueOf keep theirs. */ +enum { DYN_INHERIT_VALUE_OF = 1, DYN_INHERIT_TO_STRING = 2, DYN_INHERIT_OWN = 4, DYN_INHERIT_ALL = 7 }; + +static ScrDyn *dyn_inherited_from(const char *ctor, int methods, const ScrStr *key) { + if (strcmp(key->data, dyn_s14) == 0) return ctor ? dyn_global_ctor(ctor) : NULL; + bool applies = (strcmp(key->data, dyn_s23) == 0 && (methods & DYN_INHERIT_VALUE_OF)) || + (strcmp(key->data, dyn_s18) == 0 && (methods & DYN_INHERIT_TO_STRING)) || + ((strcmp(key->data, dyn_s9) == 0 || strcmp(key->data, dyn_s4) == 0) && + (methods & DYN_INHERIT_OWN)); + if (!applies) return NULL; + ScrStr *prototype = scr_str_new(dyn_s26, 6); + ScrDyn *method = scr_dyn_builtin_method(prototype, key); + scr_str_release(prototype); + return method; +} + +/* The inherited member `key` of `d` when its own chain does not define it, + * or NULL (the read's undefined stands). */ +static ScrDyn *dyn_inherited_member(const ScrDyn *d, const ScrStr *key) { + switch (d->kind) { + case SCR_DYN_TYPED_REF: { + ScrDyn *view = scr_dyn_typed_ref_materialize(d); + ScrDyn *member = view && !scr_exc_pending() ? dyn_inherited_member(view, key) : NULL; + scr_dyn_release(view); + return member; + } + case SCR_DYN_OBJ: { + /* A plain object, or an Object.create chain ending in Object.prototype; + * a member present anywhere on the chain (even as undefined) stands. */ + ScrDyn *object_prototype = scr_dyn_object_prototype(); + const ScrDyn *tail = d; + bool present = false; + for (const ScrDyn *link = d; link; link = link->prototype) { + tail = link; + if (link == object_prototype) break; + if (link->kind != SCR_DYN_OBJ || scr_dyn_has_own(link, key)) { + present = true; + break; + } + } + scr_dyn_release(object_prototype); + if (present || (tail != object_prototype && tail->null_proto)) return NULL; + /* Errors keep Error.prototype.toString and their constructor values. */ + if (scr_dyn_err_instanceof(d, 0)) return dyn_inherited_from(NULL, DYN_INHERIT_VALUE_OF | DYN_INHERIT_OWN, key); + return dyn_inherited_from(dyn_s26, DYN_INHERIT_ALL, key); + } + case SCR_DYN_ARR: + /* Array.prototype defines toString and answers constructor itself. */ + if (d->null_proto || d->prototype || scr_dyn_has_own(d, key)) return NULL; + return dyn_inherited_from(NULL, DYN_INHERIT_VALUE_OF | DYN_INHERIT_OWN, key); + case SCR_DYN_FUNC: + /* Function.prototype defines toString and answers constructor itself. */ + if (scr_dyn_has_own(d, key)) return NULL; + return dyn_inherited_from(NULL, DYN_INHERIT_VALUE_OF | DYN_INHERIT_OWN, key); + case SCR_DYN_PROMISE: + return dyn_inherited_from(dyn_s21, DYN_INHERIT_ALL, key); + case SCR_DYN_HANDLE: + switch (d->v.handle.tag) { + case SCR_DYNH_DATE: return dyn_inherited_from(dyn_s29, DYN_INHERIT_OWN, key); + case SCR_DYNH_REGEXP: return dyn_inherited_from(dyn_s25, DYN_INHERIT_VALUE_OF | DYN_INHERIT_OWN, key); + case SCR_DYNH_MAP: return dyn_inherited_from(dyn_s30, DYN_INHERIT_ALL, key); + case SCR_DYNH_SET: return dyn_inherited_from(dyn_s31, DYN_INHERIT_ALL, key); + case SCR_DYNH_WEAK_MAP: return dyn_inherited_from(dyn_s22, DYN_INHERIT_ALL, key); + case SCR_DYNH_WEAK_SET: return dyn_inherited_from(dyn_s24, DYN_INHERIT_ALL, key); + default: return NULL; + } + default: + return NULL; + } +} + +/* Object.prototype's own constructor and shared methods, read on the + * prototype itself (`const { hasOwnProperty } = Object.prototype`, the + * `proto.constructor` of an isPlainObject check): the member, or NULL when + * `d` is another value and the ordinary read applies. */ +ScrDyn *scr_dyn_object_prototype_member(const ScrDyn *d, const ScrStr *key) { + ScrDyn *object_prototype = scr_dyn_object_prototype(); + bool prototype = d == object_prototype; + scr_dyn_release(object_prototype); + return prototype ? dyn_inherited_from(dyn_s26, DYN_INHERIT_ALL, key) : NULL; +} + +/* A constant-key read of constructor/valueOf/toString/hasOwnProperty/ + * propertyIsEnumerable: `read` is the ordinary read's +1 result (NULL when + * it threw). Takes it; answers the inherited member in place of an + * undefined read, the read otherwise. */ +ScrDyn *scr_dyn_inherited_read(const ScrDyn *d, const ScrStr *key, ScrDyn *read) { + if (!read || read->kind != SCR_DYN_UNDEF) return read; + ScrDyn *member = dyn_inherited_member(d, key); + if (scr_exc_pending()) { + scr_dyn_release(member); + scr_dyn_release(read); + return NULL; + } + if (!member) return read; + scr_dyn_release(read); + return member; +} + +/* Builtin global functions stored in JavaScript (the `var nextTick = typeof + * setImmediate === "function" ? setImmediate : setTimeout` probe): one + * interned function per name, so typeof answers "function" and `===` is + * identity. Calls run the timer and tick APIs with Node's argument + * contract — a function callback (else ERR_INVALID_ARG_TYPE), the delay + * through ToNumber, the remaining arguments passed to the callback — and + * timer functions answer the numeric handles the direct calls answer, + * which the clear functions accept. */ +static SCR_TL ScrDyn *dyn_builtin_fns; + +static void dyn_builtin_fns_cleanup(void) { + scr_dyn_release(dyn_builtin_fns); + dyn_builtin_fns = NULL; +} + +/* Library builds have no event loop (a library that schedules timers is + * refused at SC4005 before emission), so the calls that schedule are left + * out of them. */ +#ifndef SCR_LIB +static bool dyn_builtin_fn_callback(ScrDyn *const *args, size_t argc) { + if (argc > 0 && args[0]->kind == SCR_DYN_FUNC) return true; + static const char message[] = "The \"callback\" argument must be of type function."; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, dyn_s5); + return false; +} + +static void dyn_builtin_fn_fire(ScrClosure *c) { + ScrDyn *fn = (ScrDyn *)scr_box_get_ref(c->caps[0]); + ScrDyn *args = (ScrDyn *)scr_box_get_ref(c->caps[1]); + ScrDyn *r = scr_dyn_call(fn, args->v.arr.items, args->v.arr.len, dyn_s17); + if (r != NULL) scr_dyn_release(r); + scr_dyn_release(fn); + scr_dyn_release(args); +} + +/* The closure that calls args[0] with args[first..] when the loop runs it. */ +static ScrClosure *dyn_builtin_fn_deferred(ScrDyn *const *args, size_t argc, size_t first) { + ScrDyn *rest = scr_dyn_new_arr(); + for (size_t i = first; i < argc; i++) scr_dyn_arr_push(rest, scr_dyn_retain(args[i])); + ScrClosure *c = scr_closure_new((void *)dyn_builtin_fn_fire, 2); + c->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[0], scr_dyn_retain(args[0])); + c->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[1], rest); /* moves */ + return c; +} + +#endif + +static ScrDyn *dyn_builtin_fn_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) { +#ifdef SCR_LIB + (void)clo; + (void)args; + (void)argc; + static const char message[] = "Builtin timer functions require an executable event loop"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC4005"); + return NULL; +#else + ScrStr *name = scr_box_get_ref(clo->caps[0]); + ScrDyn *result = NULL; + bool timeout = strcmp(name->data, dyn_s16) == 0; + if (timeout || strcmp(name->data, dyn_s15) == 0) { + double ms = 0; + if (dyn_builtin_fn_callback(args, argc) && (argc < 2 || scr_dyn_number_coerce_js(args[1], &ms))) { + ScrClosure *c = dyn_builtin_fn_deferred(args, argc, 2); + result = scr_dyn_new_num(timeout ? scr_set_timeout_handle(c, ms) : scr_set_interval(c, ms)); + } + } else if (strcmp(name->data, dyn_s13) == 0) { + if (dyn_builtin_fn_callback(args, argc)) + result = scr_dyn_new_num(scr_set_immediate(dyn_builtin_fn_deferred(args, argc, 1))); + } else if (strcmp(name->data, dyn_s7) == 0) { + if (dyn_builtin_fn_callback(args, argc)) { + scr_next_tick(dyn_builtin_fn_deferred(args, argc, 1)); + result = scr_dyn_retain(scr_dyn_undefined()); + } + } else if (strcmp(name->data, dyn_s11) == 0) { + if (dyn_builtin_fn_callback(args, argc)) { + scr_queue_microtask_dyn(args[0]); + result = scr_dyn_retain(scr_dyn_undefined()); + } + } else if (strncmp(name->data, dyn_s28, 5) == 0) { + /* Node ignores anything that is not a live handle. */ + if (argc > 0 && args[0]->kind == SCR_DYN_NUM) { + if (strcmp(name->data, dyn_s12) == 0) scr_clear_immediate(args[0]->v.num); + else scr_clear_interval(args[0]->v.num); + } + result = scr_dyn_retain(scr_dyn_undefined()); + } else { + char message[160]; + int length = snprintf(message, sizeof message, dyn_s1, name->data); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, dyn_s27); + } + scr_str_release(name); + return result; +#endif +} + +ScrDyn *scr_dyn_builtin_function(const ScrStr *name) { + if (!dyn_builtin_fns) { + dyn_builtin_fns = scr_dyn_new_obj_null_proto(); + scr_atexit(dyn_builtin_fns_cleanup); + } + ScrDyn *value = scr_dyn_obj_get(dyn_builtin_fns, name->data, name->len); + if (!value) { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new(SCR_BOX_STR); + scr_box_set_ref(closure->caps[0], scr_str_retain((ScrStr *)name)); + const char *dot = memchr(name->data, '.', name->len); + bool two = strcmp(name->data, dyn_s16) == 0 || strcmp(name->data, dyn_s15) == 0; + value = scr_dyn_new_func(closure, dyn_builtin_fn_call, two ? 2 : 1, dyn_s10, dot ? dot + 1 : name->data); + scr_dyn_obj_set(dyn_builtin_fns, name->data, name->len, value); /* the table owns it */ + } + return scr_dyn_retain(value); +} + ScrDyn *scr_dyn_array_prototype(void) { static SCR_TL bool initialized; ScrDyn *prototype = scr_dyn_array_prototype_base(); diff --git a/packages/runtime/src/scr_error.c b/packages/runtime/src/scr_error.c index 6f616c35a..4f514681b 100644 --- a/packages/runtime/src/scr_error.c +++ b/packages/runtime/src/scr_error.c @@ -354,6 +354,12 @@ ScrStr *scr_error_stack(ScrError *e) { return scr_str_retain(e->stack); } +void scr_error_set_stack(ScrError *e, ScrStr *stack) { + ScrStr *replacement = scr_str_retain(stack); + scr_str_release(e->stack); + e->stack = replacement; +} + bool scr_error_is(const void *obj) { const ScrVt *vt = ((const ScrError *)obj)->vt; return scr_error_vts[SCR_ERR_ERROR].pre <= vt->pre && diff --git a/packages/runtime/src/scr_error.h b/packages/runtime/src/scr_error.h index ce60dac23..453ebf3e1 100644 --- a/packages/runtime/src/scr_error.h +++ b/packages/runtime/src/scr_error.h @@ -252,6 +252,9 @@ void scr_error_install_message_reader(ScrStr *(*reader)(ScrError *)); void scr_error_install_name_reader(ScrStr *(*reader)(ScrError *)); ScrStr *scr_error_default_name(const ScrError *e); ScrStr *scr_error_stack(ScrError *e); +/* Replaces the error's stack string, as an assignment to `stack` does. + * Borrowed operands. */ +void scr_error_set_stack(ScrError *e, ScrStr *stack); ScrError *scr_error_retain(ScrError *e); void scr_error_release(ScrError *e); /* dispatches through e->vt */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 27168dad1..f2b469667 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1585,8 +1585,12 @@ static void scr_dyn_obj_put(ScrDyn *obj, const char *key, size_t key_len, ScrDyn } if (obj->non_extensible) { scr_dyn_release(value); - static const char message[] = "Cannot add property, object is not extensible"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, "Cannot add property "); + scr_jb_write(&buffer, key, key_len); + scr_jb_puts(&buffer, ", object is not extensible"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); return; } if (obj->v.obj.len == obj->v.obj.cap) { @@ -1922,9 +1926,40 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { return scr_dyn_retain(object); } +/* A class instance's integrity state lives in its native layout, which only + * statically typed calls can reach (the compiler adds the state to the + * hierarchies they name). Through an untyped value the operation throws. */ +static bool scr_dyn_class_integrity_fence(const ScrDyn *object, const char *operation) { + if (object->kind != SCR_DYN_TYPED_REF) return false; + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, operation); + scr_jb_puts(&buffer, " of a class instance held in an untyped value is not supported yet"); + ScrStr *message = scr_jb_finish(&buffer); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + scr_str_release(message); + return true; +} + +/* Integrity queries on a class instance answer from its checked view, which + * carries the instance's attributes (scr_dyn_class_view_attributes). */ +static bool scr_dyn_typed_ref_view_answer(ScrDyn *object, bool (*query)(const ScrDyn *), + bool *answer) { + if (object->kind != SCR_DYN_TYPED_REF) return false; + ScrDyn *view = scr_dyn_typed_ref_materialize(object); + *answer = scr_exc_pending() ? false : query(view); + scr_dyn_release(view); + return true; +} + +static bool scr_dyn_is_extensible_view(const ScrDyn *view) { + return (view->kind == SCR_DYN_OBJ || view->kind == SCR_DYN_ARR) && !view->non_extensible; +} + ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) { if (scr_dyn_class_reflection_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.preventExtensions"); return NULL; } + if (scr_dyn_class_integrity_fence(object, "Object.preventExtensions")) return NULL; if (object->kind == SCR_DYN_OBJ || object->kind == SCR_DYN_ARR) { object->non_extensible = true; if (object->kind == SCR_DYN_ARR && object->v.arr.properties) @@ -1936,12 +1971,15 @@ ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) { bool scr_dyn_is_extensible(ScrDyn *object) { if (scr_dyn_class_reflection_fence(object)) return false; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.isExtensible"); return false; } - return (object->kind == SCR_DYN_OBJ || object->kind == SCR_DYN_ARR) && !object->non_extensible; + bool answer; + if (scr_dyn_typed_ref_view_answer(object, scr_dyn_is_extensible_view, &answer)) return answer; + return scr_dyn_is_extensible_view(object); } ScrDyn *scr_dyn_seal(ScrDyn *object) { if (scr_dyn_class_reflection_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.seal"); return NULL; } + if (scr_dyn_class_integrity_fence(object, "Object.seal")) return NULL; if (object->symbol_properties) scr_dyn_release(scr_dyn_seal(object->symbol_properties)); if (object->kind == SCR_DYN_OBJ) { object->non_extensible = true; @@ -1958,6 +1996,8 @@ ScrDyn *scr_dyn_seal(ScrDyn *object) { bool scr_dyn_is_sealed(const ScrDyn *object) { if (scr_dyn_class_reflection_fence(object)) return false; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.isSealed"); return false; } + bool answer; + if (scr_dyn_typed_ref_view_answer((ScrDyn *)object, scr_dyn_is_sealed, &answer)) return answer; if (object->symbol_properties && !scr_dyn_is_sealed(object->symbol_properties)) return false; if (object->kind == SCR_DYN_ARR) return object->non_extensible && (object->v.arr.len == 0 || object->v.arr.sealed) && @@ -4316,6 +4356,50 @@ void scr_dyn_obj_set(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value scr_dyn_obj_put(obj, key, key_len, value); } +/* The getter (or, with `setter`, the setter) of the own accessor property + * `key`: +1, or the undefined value when the property is not an accessor + * or has none. */ +ScrDyn *scr_dyn_obj_accessor_fn(const ScrDyn *obj, const char *key, size_t key_len, bool setter) { + const ScrDynEntry *entry = obj->kind == SCR_DYN_OBJ ? scr_dyn_find_entry(obj, key, key_len) : NULL; + ScrDyn *fn = entry && entry->accessor ? (setter ? entry->setter : entry->getter) : NULL; + return scr_dyn_retain(fn ? fn : scr_dyn_undefined()); +} + +/* An object literal's get/set accessor property on a fresh copy: + * enumerable and configurable, with the given getter and setter functions + * (either NULL when the literal declares none). Moves both. */ +void scr_dyn_obj_set_accessor(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *getter, ScrDyn *setter) { + scr_dyn_obj_put(obj, key, key_len, scr_dyn_retain(scr_dyn_undefined())); + ScrDynEntry *entry = scr_dyn_find_entry(obj, key, key_len); + if (!entry) { + scr_dyn_release(getter); + scr_dyn_release(setter); + return; + } + entry->accessor = true; + entry->getter = getter; + entry->setter = setter; +} + +/* A class instance's view takes its native field values whatever the + * attributes copied into it (a frozen instance's view is built the same way + * as any other). Consumes value. */ +void scr_dyn_obj_set_field(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value) { + ScrDynEntry *existing = obj->v.obj.index ? scr_dyn_find_entry(obj, key, key_len) + : scr_dyn_find_linear_entry(obj, key, key_len); + if (existing && !existing->accessor) { + scr_dyn_region_store(obj, value); + ScrDyn *old = existing->value; + existing->value = value; + scr_dyn_release(old); + return; + } + bool non_extensible = obj->non_extensible; + obj->non_extensible = false; + scr_dyn_obj_put(obj, key, key_len, value); + obj->non_extensible = non_extensible; +} + static ScrStr *scr_dyn_property_key(const ScrDyn *key) { return scr_dyn_string_coerce_js(key); } @@ -6177,9 +6261,12 @@ ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) (!accessor && ((!prior->writable && writable) || (!prior->writable && value && !scr_dyn_property_same_value(prior->value, value)))))) { scr_dyn_descriptor_fields_drop(fields); + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, "Cannot redefine property: "); + scr_jb_write(&buffer, name->data, name->len); scr_str_release(name); - static const char msg[] = "Cannot redefine property"; - scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); return NULL; } if (prior) { @@ -6359,6 +6446,28 @@ void scr_dyn_throw(ScrDyn *d) { scr_dyn_release(d); return; } + /* A checked-dynamic primitive throws as the primitive itself, as a + * statically typed `throw 5` does: catch bindings test and extract the + * same snapshot kinds (`typeof e === "number"`), and a primitive has no + * identity to keep. */ + if (d->kind == SCR_DYN_NUM) { + double value = d->v.num; + scr_dyn_release(d); + scr_throw_f64(value); + return; + } + if (d->kind == SCR_DYN_BOOL) { + bool value = d->v.b; + scr_dyn_release(d); + scr_throw_bool(value); + return; + } + if (d->kind == SCR_DYN_STR) { + ScrStr *value = scr_str_retain(d->v.str); + scr_dyn_release(d); + scr_throw_str(value); + return; + } scr_throw_ref_classified(d, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, scr_dyn_is_object(d)); } @@ -6792,14 +6901,111 @@ static void scr_errdyn_teardown(void) { free(cache); } +/* An error's own `stack`: a non-enumerable accessor shared by every error, + * as V8 defines it. The getter formats the stack on first access, so its + * heading is the name and message at that point, and later reads answer + * the same string. Both functions find the error through the receiver or + * its prototype chain; on any other receiver the getter answers undefined + * and the setter changes nothing but the receiver's own accessor. */ +static SCR_TL ScrDyn *scr_error_stack_accessors[2]; +static SCR_TL bool scr_error_stack_accessors_registered; + +static void scr_error_stack_accessors_cleanup(void) { + for (size_t i = 0; i < 2; i++) { + scr_dyn_release(scr_error_stack_accessors[i]); + scr_error_stack_accessors[i] = NULL; + } + scr_error_stack_accessors_registered = false; +} + +/* The native error behind an accessor receiver. +1 or NULL. */ +static ScrError *scr_error_stack_holder(const ScrDyn *receiver) { + for (const ScrDyn *current = receiver; current; current = scr_dyn_proto_next(current)) { + ScrError *error = scr_errdyn_err_of(current); + if (error) return error; + if (current->kind != SCR_DYN_OBJ) return NULL; + } + return NULL; +} + +static ScrDyn *scr_error_stack_get_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + ScrDyn *self = scr_dyn_this_get(); + ScrError *error = scr_error_stack_holder(self); + scr_dyn_release(self); + if (!error) return scr_dyn_retain(scr_dyn_undefined()); + ScrStr *stack = scr_error_stack(error); + scr_error_release(error); + /* A name or message getter that throws leaves its exception pending. */ + if (!stack) return NULL; + ScrDyn *out = scr_dyn_new_str(stack); + scr_str_release(stack); + return out; +} + +static ScrDyn *scr_error_stack_set_call(ScrClosure *closure, ScrDyn *const *args, size_t argc); + +/* Whether an own property is an error's `stack` accessor. */ +static bool scr_dyn_entry_is_error_stack(const ScrDynEntry *entry) { + return entry->accessor && entry->setter && entry->setter->kind == SCR_DYN_FUNC && + entry->setter->v.fn.thunk == scr_error_stack_set_call; +} + +static ScrDyn *scr_error_stack_set_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *self = scr_dyn_this_get(); + ScrError *error = scr_error_stack_holder(self); + ScrDyn *value = argc > 0 ? args[0] : scr_dyn_undefined(); + if (error && value->kind == SCR_DYN_STR) { + scr_error_set_stack(error, value->v.str); + } else if (self->kind == SCR_DYN_OBJ) { + /* The native slot holds strings only, and a typed reference's + * snapshot names no error: the receiver's own accessor becomes a data + * property holding the value, which a snapshot's commit stores. */ + ScrStr *key = scr_str_new("stack", 5); + ScrDynEntry *entry = scr_dyn_entry(self, key); + scr_str_release(key); + if (entry && scr_dyn_entry_is_error_stack(entry)) { + ScrDyn *old_value = entry->value, *old_getter = entry->getter, *old_setter = entry->setter; + entry->value = scr_dyn_retain(value); + entry->getter = entry->setter = NULL; + entry->accessor = false; + entry->writable = true; + scr_dyn_release(old_value); + scr_dyn_release(old_getter); + scr_dyn_release(old_setter); + } + } + scr_error_release(error); + scr_dyn_release(self); + return scr_dyn_retain(scr_dyn_undefined()); +} + +static void scr_error_dyn_add_stack(ScrDyn *d) { + if (!scr_error_stack_accessors_registered) { + scr_error_stack_accessors_registered = true; + scr_atexit(scr_error_stack_accessors_cleanup); + } + if (!scr_error_stack_accessors[0]) { + scr_error_stack_accessors[0] = + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_stack_get_call, 0, "native:error.stack", NULL); + scr_error_stack_accessors[1] = + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_stack_set_call, 1, "native:error.stack", NULL); + } + scr_dyn_obj_set(d, "stack", 5, scr_dyn_retain(scr_dyn_undefined())); + ScrDynEntry *entry = &d->v.obj.entries[d->v.obj.len - 1]; + entry->accessor = true; + entry->getter = scr_dyn_retain(scr_error_stack_accessors[0]); + entry->setter = scr_dyn_retain(scr_error_stack_accessors[1]); + entry->enumerable = false; +} + ScrDyn *scr_error_dyn_fields(const ScrError *e) { ScrDyn *d = scr_dyn_new_obj(); scr_dyn_obj_set(d, "%error", 6, scr_dyn_new_bool(true)); /* the checked-dynamic tree's error marker */ d->v.obj.entries[d->v.obj.len - 1].enumerable = false; - if (e->name_present) { - scr_dyn_obj_set(d, "name", 4, scr_dyn_new_str(e->name)); - d->v.obj.entries[d->v.obj.len - 1].enumerable = e->name_enumerable; - } + /* Node lists `stack` first among an error's own properties. */ + scr_error_dyn_add_stack(d); if (!e->name_present || !e->message_present) { ScrDyn *prototype = scr_dyn_new_obj(); if (!e->name_present) { @@ -6817,11 +7023,6 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) { scr_dyn_obj_set(d, "message", 7, scr_dyn_new_str(e->message)); d->v.obj.entries[d->v.obj.len - 1].enumerable = e->message_enumerable; } - if (e->stack) { - /* Node's own `stack` is non-enumerable. */ - scr_dyn_obj_set(d, "stack", 5, scr_dyn_new_str(e->stack)); - d->v.obj.entries[d->v.obj.len - 1].enumerable = false; - } if (e->system_message) { /* SystemError: code, the read-only `info` context ({ message, path, * syscall, errno, code }), then errno, syscall, path and dest. */ @@ -6880,9 +7081,37 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) { scr_dyn_obj_set(d, "errors", 6, scr_aggregate_errors((ScrError *)e)); d->v.obj.entries[d->v.obj.len - 1].enumerable = false; } + /* An own `name` is assigned after construction defines the others. */ + if (e->name_present) { + scr_dyn_obj_set(d, "name", 4, scr_dyn_new_str(e->name)); + d->v.obj.entries[d->v.obj.len - 1].enumerable = e->name_enumerable; + } return d; } +/* Moves an Error subclass view's own `name` to follow the property + * `after`, or every other property when `after` is NULL: the position + * the class's construction creates it at. Nothing changes when the view + * has no own `name` or no `after` property. */ +void scr_error_dyn_place_name(ScrDyn *view, const char *after, size_t after_len) { + if (view->kind != SCR_DYN_OBJ) return; + ScrDynEntry *entries = view->v.obj.entries; + size_t n = view->v.obj.len, from = n, to = n; + for (size_t i = 0; i < n; i++) { + if (entries[i].key_len == 4 && memcmp(entries[i].key, "name", 4) == 0) from = i; + else if (after && entries[i].key_len == after_len && memcmp(entries[i].key, after, after_len) == 0) to = i; + } + if (from == n || (after && to == n)) return; + size_t at = after ? (to > from ? to : to + 1) : n - 1; + if (at == from) return; + ScrDynEntry name = entries[from]; + memmove(&entries[from], &entries[from + 1], (n - from - 1) * sizeof *entries); + memmove(&entries[at + 1], &entries[at], (n - 1 - at) * sizeof *entries); + entries[at] = name; + free(view->v.obj.index); + view->v.obj.index = NULL; +} + ScrDyn *scr_dyn_from_error(const ScrError *e) { size_t hit = scr_errdyn_find(e, true); if (hit != SIZE_MAX) return scr_dyn_retain(scr_errdyn_cache[hit].dyn); @@ -10936,6 +11165,17 @@ static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up, ScrDynRegion ScrDyn *out = scr_dyn_new_obj(); for (size_t i = 0; i < v->v.obj.len; i++) { const ScrDynEntry *e = &v->v.obj.entries[i]; + if (scr_dyn_entry_is_error_stack(e)) { + /* A stack that was already formatted copies as a non-enumerable + * data property; an unformatted one has nothing to copy. */ + ScrError *error = scr_errdyn_err_of(v); + if (error && error->stack) { + scr_dyn_obj_set(out, "stack", 5, scr_dyn_new_str(error->stack)); + out->v.obj.entries[out->v.obj.len - 1].enumerable = false; + } + scr_error_release(error); + continue; + } ScrDyn *c = scr_sc_clone(e->value, &self, region); if (c == NULL) { scr_dyn_release(out); @@ -11459,7 +11699,20 @@ ScrDyn *scr_dyn_get_own_property_names(const ScrDyn *value) { scr_dyn_release(table); return out; } - if (value->kind == SCR_DYN_OBJ) return scr_dyn_obj_own_keys(value); + if (value->kind == SCR_DYN_OBJ) { + ScrDyn *keys = scr_dyn_obj_own_keys(value); + if (!scr_dyn_obj_get(value, "%error", 6)) return keys; + /* The checked-dynamic tree's error marker is not a property. */ + ScrDyn *names = scr_dyn_new_arr(); + scr_dyn_arr_reserve(names, keys->v.arr.len); + for (size_t i = 0; i < keys->v.arr.len; i++) { + const ScrStr *key = keys->v.arr.items[i]->v.str; + if (key->len == 6 && memcmp(key->data, "%error", 6) == 0) continue; + scr_dyn_arr_push(names, scr_dyn_retain(keys->v.arr.items[i])); + } + scr_dyn_release(keys); + return names; + } if (value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BYTES) { ScrDyn *keys = scr_dyn_new_arr(); size_t length = value->kind == SCR_DYN_ARR ? value->v.arr.len : @@ -14031,6 +14284,7 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt ScrDyn *scr_dyn_freeze(ScrDyn *value) { if (scr_dyn_class_reflection_fence(value)) return NULL; + if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL; if (value->symbol_properties) scr_dyn_release(scr_dyn_freeze(value->symbol_properties)); if (value->kind == SCR_DYN_ARR) { value->non_extensible = true; @@ -14058,6 +14312,8 @@ ScrDyn *scr_dyn_freeze(ScrDyn *value) { bool scr_dyn_is_frozen(const ScrDyn *value) { if (scr_dyn_class_reflection_fence(value)) return false; + bool answer; + if (scr_dyn_typed_ref_view_answer((ScrDyn *)value, scr_dyn_is_frozen, &answer)) return answer; if (value->symbol_properties && !scr_dyn_is_frozen(value->symbol_properties)) return false; if (value->kind == SCR_DYN_ARR) return scr_dyn_is_sealed(value) && (value->v.arr.len == 0 || value->v.arr.frozen) && @@ -14112,6 +14368,130 @@ ScrDyn *scr_caught_to_dyn(const ScrCaught *c) { } } +/* ── class instance attributes ─────────────────────────────────────────── + * A hierarchy passed to Object.freeze, Object.seal, Object.preventExtensions + * or Object.defineProperty keeps an integrity state per instance and an + * attribute word per redefined field (CLASS_* and FIELD_* bits in the + * compiler's ir.ts). The checked view of an instance carries them, so + * reflection over the view answers as Node does. */ +enum { + SCR_CLASS_NOT_EXTENSIBLE = 1, + SCR_CLASS_SEALED = 2, + SCR_CLASS_FROZEN = 4, + SCR_FIELD_NOT_WRITABLE = 1, + SCR_FIELD_NOT_ENUMERABLE = 2, + SCR_FIELD_NOT_CONFIGURABLE = 4, +}; + +void scr_dyn_class_field_attributes(ScrDyn *view, ScrStr *name, double bits) { + unsigned b = (unsigned)bits; + if (b == 0 || view->kind != SCR_DYN_OBJ) return; + ScrDynEntry *entry = scr_dyn_entry(view, name); + if (!entry) return; + if (b & SCR_FIELD_NOT_WRITABLE) entry->writable = false; + if (b & SCR_FIELD_NOT_ENUMERABLE) entry->enumerable = false; + if (b & SCR_FIELD_NOT_CONFIGURABLE) entry->configurable = false; +} + +void scr_dyn_class_view_attributes(ScrDyn *view, double bits) { + unsigned b = (unsigned)bits; + if (b == 0 || view->kind != SCR_DYN_OBJ) return; + view->non_extensible = true; + for (size_t i = 0; i < view->v.obj.len; i++) { + ScrDynEntry *entry = &view->v.obj.entries[i]; + if (b & (SCR_CLASS_SEALED | SCR_CLASS_FROZEN)) entry->configurable = false; + if ((b & SCR_CLASS_FROZEN) && !entry->accessor) entry->writable = false; + } +} + +/* Object.defineProperty on a declared class field. The descriptor applies to + * a one-property object holding the field's current value and attributes, + * so validation and errors match ordinary objects. Answers that object (+1), + * or NULL with a pending exception. */ +ScrDyn *scr_class_define_field(ScrDyn *current, double field_bits, double object_bits, ScrStr *name, + ScrDyn *descriptor) { + unsigned f = (unsigned)field_bits, o = (unsigned)object_bits; + ScrDyn *holder = scr_dyn_new_obj(); + scr_dyn_obj_set(holder, name->data, name->len, scr_dyn_retain(current)); + ScrDynEntry *entry = scr_dyn_entry(holder, name); + entry->writable = !(f & SCR_FIELD_NOT_WRITABLE) && !(o & SCR_CLASS_FROZEN); + entry->enumerable = !(f & SCR_FIELD_NOT_ENUMERABLE); + entry->configurable = + !(f & SCR_FIELD_NOT_CONFIGURABLE) && !(o & (SCR_CLASS_SEALED | SCR_CLASS_FROZEN)); + ScrDyn *key = scr_dyn_new_str(name); + ScrDyn *defined = scr_dyn_define_property(holder, key, descriptor); + scr_dyn_release(key); + if (!defined) { + scr_dyn_release(holder); + return NULL; + } + scr_dyn_release(defined); + entry = scr_dyn_entry(holder, name); + if (entry->accessor) { + scr_dyn_release(holder); + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, "Object.defineProperty with an accessor descriptor for the declared field '"); + scr_jb_write(&buffer, name->data, name->len); + scr_jb_puts(&buffer, "' is not supported yet"); + ScrStr *message = scr_jb_finish(&buffer); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + scr_str_release(message); + return NULL; + } + return holder; +} + +/* The field attribute word of a scr_class_define_field result. */ +double scr_class_defined_bits(ScrDyn *holder, ScrStr *name) { + const ScrDynEntry *entry = scr_dyn_entry(holder, name); + unsigned bits = 0; + if (!entry->writable) bits |= SCR_FIELD_NOT_WRITABLE; + if (!entry->enumerable) bits |= SCR_FIELD_NOT_ENUMERABLE; + if (!entry->configurable) bits |= SCR_FIELD_NOT_CONFIGURABLE; + return (double)bits; +} + +/* The value a scr_class_define_field result holds (+1). */ +ScrDyn *scr_class_defined_value(ScrDyn *holder, ScrStr *name) { + return scr_dyn_retain(scr_dyn_entry(holder, name)->value); +} + +/* Whether a property descriptor names a value (Node's ToPropertyDescriptor + * reads `value` with [[HasProperty]]). */ +bool scr_class_descriptor_has_value(ScrDyn *descriptor) { + if (descriptor->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(descriptor); + bool has = !scr_exc_pending() && scr_class_descriptor_has_value(view); + scr_dyn_release(view); + return has; + } + return descriptor->kind == SCR_DYN_OBJ && scr_dyn_obj_get(descriptor, "value", 5) != NULL; +} + +/* Node's TypeError for a write to a read-only field of a class instance. */ +void scr_class_throw_read_only(ScrStr *field, ScrStr *owner) { + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, "Cannot assign to read only property '"); + scr_jb_write(&buffer, field->data, field->len); + scr_jb_puts(&buffer, "' of object '#<"); + scr_jb_write(&buffer, owner->data, owner->len); + scr_jb_puts(&buffer, ">'"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); +} + +/* Node's TypeError for a class field initializer on a non-extensible + * instance (a base constructor froze or sealed `this`). */ +void scr_class_throw_not_extensible(ScrStr *field) { + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, "Cannot define property "); + scr_jb_write(&buffer, field->data, field->len); + scr_jb_puts(&buffer, ", object is not extensible"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); +} + /* ── compiled generators through untyped values ─────────────────────── */ static SCR_TL ScrDyn *scr_generator_next_getter; diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index bd7d599ce..99f0128c9 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -2037,6 +2037,19 @@ double scr_process_exit_code_or_zero(void) { return (double)scr_exit_code_hint_get(); } +static SCR_TL bool scr_exit_code_assigned = false; + +void scr_process_set_exit_code_tracked(double code) { + int converted; + if (!scr_process_exit_code_of(code, &converted)) return; + scr_exit_code_note(converted); + scr_exit_code_assigned = true; +} + +bool scr_process_has_exit_code(void) { + return scr_exit_code_assigned || scr_exit_code_hint_get() != 0; +} + #if defined(__linux__) #include #include @@ -7472,29 +7485,319 @@ static uint64_t scr_sha512_rotr(uint64_t x, unsigned n) { return (x >> n) | (x << (64 - n)); } -SCR_DIGEST_ALIGNED static void scr_sha512_block(uint64_t h[8], const unsigned char *data) { - uint64_t w[80]; - for (size_t i = 0; i < 16; i++) { - w[i] = 0; - for (size_t b = 0; b < 8; b++) w[i] = (w[i] << 8) | data[i * 8 + b]; +#define SCR_SHA512_BSIG0(x) \ + (scr_sha512_rotr(x, 28) ^ scr_sha512_rotr(x, 34) ^ scr_sha512_rotr(x, 39)) +#define SCR_SHA512_BSIG1(x) \ + (scr_sha512_rotr(x, 14) ^ scr_sha512_rotr(x, 18) ^ scr_sha512_rotr(x, 41)) +#define SCR_SHA512_SSIG0(x) (scr_sha512_rotr(x, 1) ^ scr_sha512_rotr(x, 8) ^ ((x) >> 7)) +#define SCR_SHA512_SSIG1(x) (scr_sha512_rotr(x, 19) ^ scr_sha512_rotr(x, 61) ^ ((x) >> 6)) + +/* One round with the eight working variables passed in rotated order, so + * an unrolled sequence of rounds needs no register moves. */ +#define SCR_SHA512_ROUND(a, b, c, d, e, f, g, h, k, w) \ + do { \ + uint64_t t1 = (h) + SCR_SHA512_BSIG1(e) + ((g) ^ ((e) & ((f) ^ (g)))) + (k) + (w); \ + (d) += t1; \ + (h) = t1 + SCR_SHA512_BSIG0(a) + (((a) & (b)) | ((c) & ((a) | (b)))); \ + } while (0) + +static uint64_t scr_sha512_load(const unsigned char *p) { + uint64_t v = 0; + for (size_t b = 0; b < 8; b++) v = (v << 8) | p[b]; + return v; +} + +/* The message schedule is kept as a 16-word ring and extended 16 words + * at a time between groups of 16 rounds. */ +SCR_DIGEST_ALIGNED static void scr_sha512_blocks_portable(uint64_t state[8], const unsigned char *p, + size_t blocks) { + uint64_t a = state[0], b = state[1], c = state[2], d = state[3]; + uint64_t e = state[4], f = state[5], g = state[6], h = state[7]; + for (; blocks != 0; blocks--, p += 128) { + uint64_t w[16]; + for (size_t i = 0; i < 16; i++) w[i] = scr_sha512_load(p + i * 8); + for (size_t i = 0; i < 80; i += 16) { + if (i != 0) { + for (size_t j = 0; j < 16; j++) { + w[j] += SCR_SHA512_SSIG1(w[(j + 14) & 15]) + w[(j + 9) & 15] + + SCR_SHA512_SSIG0(w[(j + 1) & 15]); + } + } + const uint64_t *k = scr_sha512_k + i; + SCR_SHA512_ROUND(a, b, c, d, e, f, g, h, k[0], w[0]); + SCR_SHA512_ROUND(h, a, b, c, d, e, f, g, k[1], w[1]); + SCR_SHA512_ROUND(g, h, a, b, c, d, e, f, k[2], w[2]); + SCR_SHA512_ROUND(f, g, h, a, b, c, d, e, k[3], w[3]); + SCR_SHA512_ROUND(e, f, g, h, a, b, c, d, k[4], w[4]); + SCR_SHA512_ROUND(d, e, f, g, h, a, b, c, k[5], w[5]); + SCR_SHA512_ROUND(c, d, e, f, g, h, a, b, k[6], w[6]); + SCR_SHA512_ROUND(b, c, d, e, f, g, h, a, k[7], w[7]); + SCR_SHA512_ROUND(a, b, c, d, e, f, g, h, k[8], w[8]); + SCR_SHA512_ROUND(h, a, b, c, d, e, f, g, k[9], w[9]); + SCR_SHA512_ROUND(g, h, a, b, c, d, e, f, k[10], w[10]); + SCR_SHA512_ROUND(f, g, h, a, b, c, d, e, k[11], w[11]); + SCR_SHA512_ROUND(e, f, g, h, a, b, c, d, k[12], w[12]); + SCR_SHA512_ROUND(d, e, f, g, h, a, b, c, k[13], w[13]); + SCR_SHA512_ROUND(c, d, e, f, g, h, a, b, k[14], w[14]); + SCR_SHA512_ROUND(b, c, d, e, f, g, h, a, k[15], w[15]); + } + a = state[0] += a; b = state[1] += b; c = state[2] += c; d = state[3] += d; + e = state[4] += e; f = state[5] += f; g = state[6] += g; h = state[7] += h; } - for (size_t i = 16; i < 80; i++) { - uint64_t s0 = scr_sha512_rotr(w[i - 15], 1) ^ scr_sha512_rotr(w[i - 15], 8) ^ (w[i - 15] >> 7); - uint64_t s1 = scr_sha512_rotr(w[i - 2], 19) ^ scr_sha512_rotr(w[i - 2], 61) ^ (w[i - 2] >> 6); - w[i] = w[i - 16] + s0 + w[i - 7] + s1; +} + +#ifdef SCR_SHA_NI +/* ── SHA-512 kernels (x86-64) ───────────────────────────────────────── + * With AVX2 and BMI2, the message schedule is computed two words at a + * time in vector registers, interleaved with the scalar rounds, and the + * rounds rotate with RORX. With AVX-512VL the schedule uses vector + * rotates and three-input XOR. The CPU and OS state checks run once and + * are cached. */ +static int scr_sha512_x86_state; /* 0 unknown, 1 portable, 2 AVX2, 3 AVX-512VL */ + +static int scr_sha512_x86(void) { + if (scr_sha512_x86_state == 0) { + unsigned a, b, c, d; + int kind = 1; + if (__get_cpuid(1, &a, &b, &c, &d) && (c & bit_OSXSAVE) && (c & bit_AVX)) { + unsigned lo, hi; + __asm__ volatile("xgetbv" : "=a"(lo), "=d"(hi) : "c"(0)); + (void)hi; + /* XMM and YMM state enabled. */ + if ((lo & 6) == 6 && __get_cpuid_count(7, 0, &a, &b, &c, &d) && (b & bit_AVX2) && + (b & bit_BMI2)) { + kind = 2; + /* Opmask, ZMM0-15 upper halves and ZMM16-31 state enabled. */ + if ((b & bit_AVX512F) && (b & bit_AVX512VL) && (lo & 0xe0) == 0xe0) kind = 3; + } + } + scr_sha512_x86_state = kind; } - uint64_t a = h[0], b = h[1], c = h[2], d = h[3]; - uint64_t e = h[4], f = h[5], g = h[6], v = h[7]; - for (size_t i = 0; i < 80; i++) { - uint64_t s1 = scr_sha512_rotr(e, 14) ^ scr_sha512_rotr(e, 18) ^ scr_sha512_rotr(e, 41); - uint64_t t1 = v + s1 + ((e & f) ^ (~e & g)) + scr_sha512_k[i] + w[i]; - uint64_t s0 = scr_sha512_rotr(a, 28) ^ scr_sha512_rotr(a, 34) ^ scr_sha512_rotr(a, 39); - uint64_t t2 = s0 + ((a & b) ^ (a & c) ^ (b & c)); - v = g; g = f; f = e; e = d + t1; - d = c; c = b; b = a; a = t1 + t2; + return scr_sha512_x86_state; +} + +__attribute__((target("avx2,bmi2"))) static inline __m128i scr_sha512_ssig0_avx2(__m128i x) { + return _mm_xor_si128(_mm_xor_si128(_mm_or_si128(_mm_srli_epi64(x, 1), _mm_slli_epi64(x, 63)), + _mm_or_si128(_mm_srli_epi64(x, 8), _mm_slli_epi64(x, 56))), + _mm_srli_epi64(x, 7)); +} + +__attribute__((target("avx2,bmi2"))) static inline __m128i scr_sha512_ssig1_avx2(__m128i x) { + return _mm_xor_si128(_mm_xor_si128(_mm_or_si128(_mm_srli_epi64(x, 19), _mm_slli_epi64(x, 45)), + _mm_or_si128(_mm_srli_epi64(x, 61), _mm_slli_epi64(x, 3))), + _mm_srli_epi64(x, 6)); +} + +__attribute__((target("avx512f,avx512vl,bmi2"))) static inline __m128i +scr_sha512_ssig0_avx512(__m128i x) { + return _mm_ternarylogic_epi64(_mm_ror_epi64(x, 1), _mm_ror_epi64(x, 8), _mm_srli_epi64(x, 7), + 0x96); +} + +__attribute__((target("avx512f,avx512vl,bmi2"))) static inline __m128i +scr_sha512_ssig1_avx512(__m128i x) { + return _mm_ternarylogic_epi64(_mm_ror_epi64(x, 19), _mm_ror_epi64(x, 61), _mm_srli_epi64(x, 6), + 0x96); +} + +/* A round for the vector kernels. wk is the schedule word plus its + * constant. Maj(a, b, c) is ((a ^ b) & (b ^ c)) ^ b, where b ^ c is the + * previous round's a ^ b. The empty asm statements keep the compiler from + * reassociating the additions: adding Sigma1(e) last keeps the sum of h, + * wk and Ch off the dependency chain through e. */ +#define SCR_SHA512_PIN(v) __asm__("" : "+r"(v)) +#define SCR_SHA512_ROUND_WK(a, b, c, d, e, f, g, h, wk, ab, bc) \ + do { \ + uint64_t t1 = (h) + (wk); \ + SCR_SHA512_PIN(t1); \ + t1 += (g) ^ ((e) & ((f) ^ (g))); \ + SCR_SHA512_PIN(t1); \ + t1 += SCR_SHA512_BSIG1(e); \ + SCR_SHA512_PIN(t1); \ + (d) += t1; \ + (ab) = (a) ^ (b); \ + uint64_t m = t1 + (((ab) & (bc)) ^ (b)); \ + SCR_SHA512_PIN(m); \ + (h) = m + SCR_SHA512_BSIG0(a); \ + } while (0) + +/* Words 2j and 2j+1 of the schedule, in the ring slot of words 2j-16 and + * 2j-15, and their sum with the round constants into wk. */ +#define SCR_SHA512_SCHEDULE(j, ssig0, ssig1) \ + do { \ + if ((j) < 40) { \ + __m128i s15 = _mm_alignr_epi8(w[((j) + 1) & 7], w[(j) & 7], 8); \ + __m128i s7 = _mm_alignr_epi8(w[((j) + 5) & 7], w[((j) + 4) & 7], 8); \ + __m128i next = _mm_add_epi64(_mm_add_epi64(w[(j) & 7], ssig0(s15)), \ + _mm_add_epi64(s7, ssig1(w[((j) + 7) & 7]))); \ + w[(j) & 7] = next; \ + _mm_store_si128((__m128i *)&wk[(j) * 2], \ + _mm_add_epi64(next, _mm_loadu_si128((const __m128i *)&scr_sha512_k[(j) * 2]))); \ + } \ + } while (0) + +/* Rounds 2i to 2i+7 compute the schedule words 2i+16 to 2i+23. */ +#define SCR_SHA512_VECTOR_BODY(ssig0, ssig1) \ + const __m128i bswap = _mm_set_epi64x(0x08090a0b0c0d0e0fULL, 0x0001020304050607ULL); \ + uint64_t a = state[0], b = state[1], c = state[2], d = state[3]; \ + uint64_t e = state[4], f = state[5], g = state[6], h = state[7]; \ + for (; blocks != 0; blocks--, p += 128) { \ + __m128i w[8]; \ + uint64_t wk[80] __attribute__((aligned(16))); \ + _Pragma("GCC unroll 8") for (int i = 0; i < 8; i++) { \ + w[i] = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i *)(p + i * 16)), bswap); \ + _mm_store_si128((__m128i *)&wk[i * 2], \ + _mm_add_epi64(w[i], _mm_loadu_si128((const __m128i *)&scr_sha512_k[i * 2]))); \ + } \ + uint64_t x = b ^ c, y; \ + _Pragma("GCC unroll 10") for (int i = 0; i < 40; i += 4) { \ + SCR_SHA512_SCHEDULE(i + 8, ssig0, ssig1); \ + SCR_SHA512_ROUND_WK(a, b, c, d, e, f, g, h, wk[2 * i + 0], y, x); \ + SCR_SHA512_ROUND_WK(h, a, b, c, d, e, f, g, wk[2 * i + 1], x, y); \ + SCR_SHA512_SCHEDULE(i + 9, ssig0, ssig1); \ + SCR_SHA512_ROUND_WK(g, h, a, b, c, d, e, f, wk[2 * i + 2], y, x); \ + SCR_SHA512_ROUND_WK(f, g, h, a, b, c, d, e, wk[2 * i + 3], x, y); \ + SCR_SHA512_SCHEDULE(i + 10, ssig0, ssig1); \ + SCR_SHA512_ROUND_WK(e, f, g, h, a, b, c, d, wk[2 * i + 4], y, x); \ + SCR_SHA512_ROUND_WK(d, e, f, g, h, a, b, c, wk[2 * i + 5], x, y); \ + SCR_SHA512_SCHEDULE(i + 11, ssig0, ssig1); \ + SCR_SHA512_ROUND_WK(c, d, e, f, g, h, a, b, wk[2 * i + 6], y, x); \ + SCR_SHA512_ROUND_WK(b, c, d, e, f, g, h, a, wk[2 * i + 7], x, y); \ + } \ + a = state[0] += a; b = state[1] += b; c = state[2] += c; d = state[3] += d; \ + e = state[4] += e; f = state[5] += f; g = state[6] += g; h = state[7] += h; \ } - h[0] += a; h[1] += b; h[2] += c; h[3] += d; - h[4] += e; h[5] += f; h[6] += g; h[7] += v; + +__attribute__((target("avx2,bmi2"), aligned(64))) static void +scr_sha512_blocks_avx2(uint64_t state[8], const unsigned char *p, size_t blocks) { + SCR_SHA512_VECTOR_BODY(scr_sha512_ssig0_avx2, scr_sha512_ssig1_avx2) +} + +__attribute__((target("avx512f,avx512vl,bmi2"), aligned(64))) static void +scr_sha512_blocks_avx512(uint64_t state[8], const unsigned char *p, size_t blocks) { + SCR_SHA512_VECTOR_BODY(scr_sha512_ssig0_avx512, scr_sha512_ssig1_avx512) +} +#endif + + +#if defined(__aarch64__) && (defined(__clang__) || defined(__GNUC__)) && !defined(_WIN32) +/* ── SHA-512 kernel (arm64) ─────────────────────────────────────────── + * On CPUs with the SHA-512 extension (FEAT_SHA512), the blocks run with + * SHA512H, SHA512H2, SHA512SU0 and SHA512SU1; other CPUs use the portable + * blocks. The CPU check runs once and is cached. */ +#include +#if defined(__APPLE__) +#include +#else +#include +#endif +#define SCR_SHA512_ARM 1 +#if defined(__clang__) +#define SCR_SHA512_ARM_TARGET __attribute__((target("sha3"))) +#else +#define SCR_SHA512_ARM_TARGET __attribute__((target("+sha3"))) +#endif + +static int scr_sha512_arm_state; /* 0 unknown, 1 available, 2 absent */ + +static bool scr_sha512_arm(void) { + if (scr_sha512_arm_state == 0) { + bool ok; +#if defined(__APPLE__) + int value = 0; + size_t size = sizeof value; + ok = sysctlbyname("hw.optional.armv8_2_sha512", &value, &size, NULL, 0) == 0 && value != 0; +#else + ok = (getauxval(AT_HWCAP) & (1ul << 21)) != 0; /* HWCAP_SHA512 */ +#endif + scr_sha512_arm_state = ok ? 1 : 2; + } + return scr_sha512_arm_state == 1; +} + +/* Two rounds per step. With the state pairs in the roles (A, B, C, D) = + * (ab, cd, ef, gh), a step computes I = SHA512H(swap(K + W) + D, ext(C, D), + * ext(B, C)), D = SHA512H2(I, B, A) and B += I; the roles then rotate to + * (D, A, B, C). */ +#define SCR_SHA512_ARM_STEP(A, B, C, D, msg, k) \ + do { \ + uint64x2_t kw = vaddq_u64((msg), vld1q_u64(k)); \ + uint64x2_t sum = vaddq_u64(vextq_u64(kw, kw, 1), (D)); \ + uint64x2_t i = vsha512hq_u64(sum, vextq_u64((C), (D), 1), vextq_u64((B), (C), 1)); \ + (D) = vsha512h2q_u64(i, (B), (A)); \ + (B) = vaddq_u64((B), i); \ + } while (0) + +/* Message words 2j and 2j+1 from the eight pairs before them. */ +#define SCR_SHA512_ARM_SCHEDULE(m0, m1, m4, m5, m7) \ + (m0) = vsha512su1q_u64(vsha512su0q_u64((m0), (m1)), (m7), vextq_u64((m4), (m5), 1)) + +SCR_SHA512_ARM_TARGET static void scr_sha512_blocks_arm(uint64_t state[8], const unsigned char *p, + size_t blocks) { + uint64x2_t ab = vld1q_u64(&state[0]), cd = vld1q_u64(&state[2]); + uint64x2_t ef = vld1q_u64(&state[4]), gh = vld1q_u64(&state[6]); + for (; blocks != 0; blocks--, p += 128) { + uint64x2_t ab0 = ab, cd0 = cd, ef0 = ef, gh0 = gh; + uint64x2_t m0 = vreinterpretq_u64_u8(vrev64q_u8(vld1q_u8(p + 0))); + uint64x2_t m1 = vreinterpretq_u64_u8(vrev64q_u8(vld1q_u8(p + 16))); + uint64x2_t m2 = vreinterpretq_u64_u8(vrev64q_u8(vld1q_u8(p + 32))); + uint64x2_t m3 = vreinterpretq_u64_u8(vrev64q_u8(vld1q_u8(p + 48))); + uint64x2_t m4 = vreinterpretq_u64_u8(vrev64q_u8(vld1q_u8(p + 64))); + uint64x2_t m5 = vreinterpretq_u64_u8(vrev64q_u8(vld1q_u8(p + 80))); + uint64x2_t m6 = vreinterpretq_u64_u8(vrev64q_u8(vld1q_u8(p + 96))); + uint64x2_t m7 = vreinterpretq_u64_u8(vrev64q_u8(vld1q_u8(p + 112))); + const uint64_t *k = scr_sha512_k; + for (int i = 0; i < 5; i++, k += 16) { + SCR_SHA512_ARM_STEP(ab, cd, ef, gh, m0, k + 0); + SCR_SHA512_ARM_STEP(gh, ab, cd, ef, m1, k + 2); + SCR_SHA512_ARM_STEP(ef, gh, ab, cd, m2, k + 4); + SCR_SHA512_ARM_STEP(cd, ef, gh, ab, m3, k + 6); + SCR_SHA512_ARM_STEP(ab, cd, ef, gh, m4, k + 8); + SCR_SHA512_ARM_STEP(gh, ab, cd, ef, m5, k + 10); + SCR_SHA512_ARM_STEP(ef, gh, ab, cd, m6, k + 12); + SCR_SHA512_ARM_STEP(cd, ef, gh, ab, m7, k + 14); + if (i == 4) break; + SCR_SHA512_ARM_SCHEDULE(m0, m1, m4, m5, m7); + SCR_SHA512_ARM_SCHEDULE(m1, m2, m5, m6, m0); + SCR_SHA512_ARM_SCHEDULE(m2, m3, m6, m7, m1); + SCR_SHA512_ARM_SCHEDULE(m3, m4, m7, m0, m2); + SCR_SHA512_ARM_SCHEDULE(m4, m5, m0, m1, m3); + SCR_SHA512_ARM_SCHEDULE(m5, m6, m1, m2, m4); + SCR_SHA512_ARM_SCHEDULE(m6, m7, m2, m3, m5); + SCR_SHA512_ARM_SCHEDULE(m7, m0, m3, m4, m6); + } + ab = vaddq_u64(ab, ab0); + cd = vaddq_u64(cd, cd0); + ef = vaddq_u64(ef, ef0); + gh = vaddq_u64(gh, gh0); + } + vst1q_u64(&state[0], ab); + vst1q_u64(&state[2], cd); + vst1q_u64(&state[4], ef); + vst1q_u64(&state[6], gh); +} +#endif + +static void scr_sha512_blocks(uint64_t h[8], const unsigned char *p, size_t blocks) { +#ifdef SCR_SHA_NI + switch (scr_sha512_x86()) { + case 3: + scr_sha512_blocks_avx512(h, p, blocks); + return; + case 2: + scr_sha512_blocks_avx2(h, p, blocks); + return; + default: + break; + } +#endif +#ifdef SCR_SHA512_ARM + if (scr_sha512_arm()) { + scr_sha512_blocks_arm(h, p, blocks); + return; + } +#endif + scr_sha512_blocks_portable(h, p, blocks); } static void scr_sha512_init(ScrSha512Ctx *ctx, bool sha384) { @@ -7523,14 +7826,15 @@ static void scr_sha512_update(ScrSha512Ctx *ctx, const unsigned char *data, size data += take; len -= take; if (ctx->tail_len == 128) { - scr_sha512_block(ctx->h, ctx->tail); + scr_sha512_blocks(ctx->h, ctx->tail, 1); ctx->tail_len = 0; } } - while (len >= 128) { - scr_sha512_block(ctx->h, data); - data += 128; - len -= 128; + if (len >= 128) { + size_t blocks = len / 128; + scr_sha512_blocks(ctx->h, data, blocks); + data += blocks * 128; + len -= blocks * 128; } if (len != 0) { memcpy(ctx->tail, data, len); @@ -7549,8 +7853,7 @@ static size_t scr_sha512_final(const ScrSha512Ctx *source, unsigned char out[64] tail[pad - 1 - b] = (unsigned char)(low >> (8 * b)); tail[pad - 9 - b] = (unsigned char)(high >> (8 * b)); } - scr_sha512_block(ctx.h, tail); - if (pad == 256) scr_sha512_block(ctx.h, tail + 128); + scr_sha512_blocks(ctx.h, tail, pad / 128); size_t len = sha384 ? 48 : 64; for (size_t i = 0; i < len; i++) out[i] = (unsigned char)(ctx.h[i / 8] >> (56 - 8 * (i % 8))); return len; @@ -10635,9 +10938,10 @@ ScrStr *scr_date_to_utc_string(double ms) { /* ── new Date(dateString).getTime() ────────────────────────────────────── * - * The BOUNDED date-string parse (documented divergence — V8's parser - * accepts far more): two grammars answer a time value, everything else is - * NaN (Node's invalid-date getTime). + * The BOUNDED date-string parse, the fast path: two grammars answer a time + * value directly; scr_date_parse falls back to V8's full parser + * (scr_date_parse_v8 below), which also answers everything they accept, + * for the strings they reject. * * 1. The ASN1_TIME_print shape X509Certificate.validFrom/validTo answer * ("Jul 1 00:00:00 2026 GMT", "Jul 17 17:52:11 2026 GMT") — the @@ -10646,8 +10950,8 @@ ScrStr *scr_date_to_utc_string(double ms) { * 2. ECMA's own date-time string format with an EXPLICIT offset: * YYYY[-MM[-DD]] date-only forms (UTC, per the spec) and full * date-times YYYY-MM-DDTHH:mm[:ss[.sss]] ending in Z or ±HH:MM. - * Offset-less date-times are LOCAL time in JS; that arm answers NaN - * here rather than guessing a zone (the divergence note). + * Offset-less date-times are LOCAL time in JS; they take the + * fallback, which converts them in the host zone. */ /* Howard Hinnant's days_from_civil (the to_iso walk inverted). */ @@ -10781,6 +11085,16 @@ double scr_date_parse_get_time(ScrStr *s) { } } +static double scr_date_parse_v8(ScrStr *s); + +/* Date.parse and new Date(string): the bounded grammars answer first; only + * a string they reject reaches V8's full parser, so the timestamps they + * accept cost nothing more. */ +double scr_date_parse(ScrStr *s) { + double t = scr_date_parse_get_time(s); + return isnan(t) ? scr_date_parse_v8(s) : t; +} + /* Date.UTC(year, month, date, hours, minutes, seconds, ms) — the spec's * MakeDay/MakeTime/TimeClip pipeline over ALREADY-NUMBER arguments (tsc * pins them; the frontend completes omitted trailing arguments with the @@ -10989,6 +11303,657 @@ double scr_date_get_timezone_offset(double ms) { return scr_date_local_parts(ms, &p) ? p.timezone_offset : NAN; } +/* ── Local time to UTC, and the year/month field constructor ─────────── + * The spec's UTC(t) over the host zone: t is a local wall-clock time + * value. The offset in effect a day before and a day after t brackets any + * transition (none come closer together than that in tzdata). A time in + * a DST overlap has two instants and answers the earlier; a time in a gap + * has none and is read with the offset BEFORE the transition, exactly + * V8's ICU getOffsetFromLocal(FORMER, FORMER). Offsets come from the same + * localtime/surrogate-year walk as the local getters, so a Date built + * from local fields reads those fields back. */ + +/* The host zone's offset (local - UTC) in ms at UTC instant `ms` (an + * integral time value within about ±8.65e15). */ +static bool scr_date_offset_at(double ms, double *out) { + long long cms = (long long)ms; + double secd = (double)(cms / 1000 - (cms % 1000 < 0)); + struct tm tmv; + double basis_secd = secd; + if (!scr_date_localtime(basis_secd, &tmv)) { + ScrDateParts utc; + scr_date_utc_parts_unchecked(ms, &utc); + long long cycle_year = (utc.year - 2000) % 400; + if (cycle_year < 0) cycle_year += 400; + basis_secd = + scr_days_from_civil(2000 + cycle_year, utc.month + 1, utc.date) * 86400.0 + + utc.hours * 3600.0 + utc.minutes * 60.0 + utc.seconds; + if (!scr_date_localtime(basis_secd, &tmv)) return false; + } + double local_as_utc = + scr_days_from_civil((long long)tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday) * 86400.0 + + tmv.tm_hour * 3600.0 + tmv.tm_min * 60.0 + tmv.tm_sec; + *out = (local_as_utc - basis_secd) * 1000.0; + return true; +} + +static volatile const double scr_date_day_ms = 86400000.0; + +static double scr_date_local_to_utc(double t) { + double before, after, at_first, at_second, day = scr_date_day_ms; + if (!scr_date_offset_at(t - day, &before) || !scr_date_offset_at(t + day, &after)) { + return NAN; + } + double first = t - before; + if (!scr_date_offset_at(first, &at_first)) return NAN; + if (before == after) { + if (at_first == before) return first; + /* Two transitions inside the window: settle on the instant's own offset. */ + double again = t - at_first, at_again; + if (scr_date_offset_at(again, &at_again) && at_again == at_first) return again; + return first; + } + double second = t - after; + if (!scr_date_offset_at(second, &at_second)) return NAN; + bool first_ok = at_first == before, second_ok = at_second == after; + if (first_ok && second_ok) return first < second ? first : second; /* overlap */ + if (second_ok && !first_ok) return second; + return first; /* the earlier offset's instant, or a gap */ +} + +/* V8's MakeDay (date.cc): bounds the UNtruncated year and month, then + * truncates (FastD2I) and normalizes the month into the year. */ +static double scr_date_v8_make_day(double year, double month, double date) { + if (!(fabs(year) <= 1000000.0 && fabs(month) <= 10000000.0 && isfinite(date))) return NAN; + int y = (int)year, m = (int)month; + y += m / 12; + m %= 12; + if (m < 0) { + m += 12; + y -= 1; + } + static const int year_delta = 399999; + static const int base_day = 365 * (1970 + year_delta) + (1970 + year_delta) / 4 - + (1970 + year_delta) / 100 + (1970 + year_delta) / 400; + int day_from_year = 365 * (y + year_delta) + (y + year_delta) / 4 - + (y + year_delta) / 100 + (y + year_delta) / 400 - base_day; + static const int common[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; + static const int leap[12] = {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335}; + bool is_leap = !((y % 4 != 0) || (y % 100 == 0 && y % 400 != 0)); + day_from_year += is_leap ? leap[m] : common[m]; + return (double)(day_from_year - 1) + trunc(date); +} + +static double scr_date_v8_make_time(double h, double m, double s, double ms) { + if (!isfinite(h) || !isfinite(m) || !isfinite(s) || !isfinite(ms)) return NAN; + return trunc(h) * 3600000.0 + trunc(m) * 60000.0 + trunc(s) * 1000.0 + trunc(ms); +} + +/* V8's bound on a local time value before UTC(t): TimeClip's range plus + * ten days. Read from memory so the value stays in its own section instead + * of this unit's shared constant pool, which every program linking the + * unit carries. */ +static volatile const double scr_date_local_limit = 8640000864000000.0; + +/* MakeDate over local fields, then UTC(t) and TimeClip. V8 refuses a local + * time value more than ten days past TimeClip's range before converting. */ +static double scr_date_from_local(double day, double time) { + if (!isfinite(day) || !isfinite(time)) return NAN; + double t = time + day * 86400000.0; + if (!(fabs(t) <= scr_date_local_limit)) return NAN; + return scr_date_new_ms(scr_date_local_to_utc(trunc(t))); +} + +/* new Date(year, monthIndex[, date[, hours[, minutes[, seconds[, ms]]]]]) + * over already-number arguments (the frontend completes omitted trailing + * arguments with date 1 and time parts 0): years 0–99 map to 1900+year, + * fields roll over through MakeDay/MakeTime, and the local time converts + * to UTC in the host zone. Never throws. */ +double scr_date_new_local(double y, double mo, double d, + double h, double mi, double s, double ms) { + if (!isnan(y)) { + double yi = trunc(y); + if (yi >= 0 && yi <= 99) y = 1900 + yi; + } + return scr_date_from_local(scr_date_v8_make_day(y, mo, d), scr_date_v8_make_time(h, mi, s, ms)); +} + +/* ── V8's date-string parser, the fallback ───────────────────────────── + * scr_date_parse answers the bounded grammars directly and asks + * this parser only when they produce NaN. It follows V8's DateParser + * (dateparser.cc, dateparser-inl.h) step for step: the ES5 date-time + * format first, then the Safari-compatible legacy grammar for whatever + * is left (RFC 1123/2822 HTTP dates, Date#toString output, "Jan 2, 2024", + * "2024/01/02 10:00", ...). Strings are read as UTF-16 code units, and a + * NUL unit ends the input, as in V8. A date without a zone is local time. */ + +enum { + SCR_DT_INVALID = -6, SCR_DT_UNKNOWN = -5, SCR_DT_SPACE = -4, + SCR_DT_NUMBER = -3, SCR_DT_SYMBOL = -2, SCR_DT_END = -1, + /* keyword tags are their keyword type (>= 0) */ + SCR_DK_WORD = 0, SCR_DK_MONTH = 1, SCR_DK_ZONE = 2, SCR_DK_T = 3, SCR_DK_AMPM = 4, +}; +#define SCR_DT_NONE INT_MAX + +typedef struct { + int tag, length, value; +} ScrDateTok; + +typedef struct { + const unsigned char *p, *end; + uint32_t ch, low; + ScrDateTok next; +} ScrDateScan; + +static void scr_date_scan_advance(ScrDateScan *in) { + if (in->low) { + in->ch = in->low; + in->low = 0; + return; + } + if (in->p >= in->end) { + in->ch = 0; + return; + } + const unsigned char *p = in->p; + size_t left = (size_t)(in->end - p); + uint32_t cp = p[0]; + size_t n = 1; + if (cp >= 0xF0 && left >= 4) { + cp = ((cp & 0x07u) << 18) | ((p[1] & 0x3Fu) << 12) | ((p[2] & 0x3Fu) << 6) | (p[3] & 0x3Fu); + n = 4; + } else if (cp >= 0xE0 && left >= 3) { + cp = ((cp & 0x0Fu) << 12) | ((p[1] & 0x3Fu) << 6) | (p[2] & 0x3Fu); + n = 3; + } else if (cp >= 0xC0 && left >= 2) { + cp = ((cp & 0x1Fu) << 6) | (p[1] & 0x3Fu); + n = 2; + } else if (cp >= 0x80) { + cp = 0xFFFD; + } + in->p += n; + if (cp > 0xFFFF) { + cp -= 0x10000; + in->ch = 0xD800 + (cp >> 10); + in->low = 0xDC00 + (cp & 0x3FF); + } else { + in->ch = cp; + } +} + +/* ECMAScript WhiteSpace, and WhiteSpace or LineTerminator. */ +static bool scr_date_is_space(uint32_t c) { + return c == 0x09 || c == 0x0B || c == 0x0C || c == 0x20 || c == 0xA0 || c == 0xFEFF || + c == 0x1680 || (c >= 0x2000 && c <= 0x200A) || c == 0x202F || c == 0x205F || + c == 0x3000; +} + +static bool scr_date_is_space_or_newline(uint32_t c) { + return scr_date_is_space(c) || c == 0x0A || c == 0x0D || c == 0x2028 || c == 0x2029; +} + +static const struct { + char prefix[3]; + signed char type, value; +} scr_date_keywords[] = { + {{'j', 'a', 'n'}, SCR_DK_MONTH, 1}, {{'f', 'e', 'b'}, SCR_DK_MONTH, 2}, + {{'m', 'a', 'r'}, SCR_DK_MONTH, 3}, {{'a', 'p', 'r'}, SCR_DK_MONTH, 4}, + {{'m', 'a', 'y'}, SCR_DK_MONTH, 5}, {{'j', 'u', 'n'}, SCR_DK_MONTH, 6}, + {{'j', 'u', 'l'}, SCR_DK_MONTH, 7}, {{'a', 'u', 'g'}, SCR_DK_MONTH, 8}, + {{'s', 'e', 'p'}, SCR_DK_MONTH, 9}, {{'o', 'c', 't'}, SCR_DK_MONTH, 10}, + {{'n', 'o', 'v'}, SCR_DK_MONTH, 11}, {{'d', 'e', 'c'}, SCR_DK_MONTH, 12}, + {{'a', 'm', 0}, SCR_DK_AMPM, 0}, {{'p', 'm', 0}, SCR_DK_AMPM, 12}, + {{'u', 't', 0}, SCR_DK_ZONE, 0}, {{'u', 't', 'c'}, SCR_DK_ZONE, 0}, + {{'z', 0, 0}, SCR_DK_ZONE, 0}, {{'g', 'm', 't'}, SCR_DK_ZONE, 0}, + {{'c', 'd', 't'}, SCR_DK_ZONE, -5}, {{'c', 's', 't'}, SCR_DK_ZONE, -6}, + {{'e', 'd', 't'}, SCR_DK_ZONE, -4}, {{'e', 's', 't'}, SCR_DK_ZONE, -5}, + {{'m', 'd', 't'}, SCR_DK_ZONE, -6}, {{'m', 's', 't'}, SCR_DK_ZONE, -7}, + {{'p', 'd', 't'}, SCR_DK_ZONE, -7}, {{'p', 's', 't'}, SCR_DK_ZONE, -8}, + {{'t', 0, 0}, SCR_DK_T, 0}, +}; + +static ScrDateTok scr_date_scan_token(ScrDateScan *in) { + ScrDateTok t; + t.tag = SCR_DT_UNKNOWN; + t.length = 1; + t.value = -1; + uint32_t c = in->ch; + if (c == 0) { + t.tag = SCR_DT_END; + return t; + } + if (c >= '0' && c <= '9') { + /* Leading zeros count toward the length; past nine significant + * digits the value stops growing but the digits are consumed. */ + int n = 0, digits = 0, length = 0; + while (in->ch == '0') { + scr_date_scan_advance(in); + length++; + } + while (in->ch >= '0' && in->ch <= '9') { + if (digits < 9) n = n * 10 + (int)(in->ch - '0'); + digits++; + length++; + scr_date_scan_advance(in); + } + t.tag = SCR_DT_NUMBER; + t.value = n; + t.length = length; + return t; + } + if (c == ':' || c == '-' || c == '+' || c == '.' || c == ')') { + scr_date_scan_advance(in); + t.tag = SCR_DT_SYMBOL; + t.value = (int)c; + return t; + } + if (c >= 'A' && !scr_date_is_space(c)) { + uint32_t prefix[3]; + prefix[0] = prefix[1] = prefix[2] = 0; + int length = 0; + while (in->ch >= 'A' && !scr_date_is_space(in->ch)) { + if (length < 3) prefix[length] = in->ch | 0x20; + length++; + scr_date_scan_advance(in); + } + t.tag = SCR_DK_WORD; + t.value = 0; + t.length = length; + for (size_t i = 0; i < sizeof scr_date_keywords / sizeof scr_date_keywords[0]; i++) { + const char *k = scr_date_keywords[i].prefix; + if (prefix[0] == (uint32_t)(unsigned char)k[0] && prefix[1] == (uint32_t)(unsigned char)k[1] && + prefix[2] == (uint32_t)(unsigned char)k[2] && + (length <= 3 || scr_date_keywords[i].type == SCR_DK_MONTH)) { + t.tag = scr_date_keywords[i].type; + t.value = scr_date_keywords[i].value; + break; + } + } + return t; + } + if (scr_date_is_space_or_newline(c)) { + scr_date_scan_advance(in); + t.tag = SCR_DT_SPACE; + return t; + } + if (c == '(') { + int balance = 0; + do { + if (in->ch == ')') balance--; + else if (in->ch == '(') balance++; + scr_date_scan_advance(in); + } while (balance > 0 && in->ch); + return t; + } + scr_date_scan_advance(in); + return t; +} + +static ScrDateTok scr_date_next(ScrDateScan *in) { + ScrDateTok t = in->next; + in->next = scr_date_scan_token(in); + return t; +} + +static bool scr_date_tok_symbol(ScrDateTok t, int c) { + return t.tag == SCR_DT_SYMBOL && t.value == c; +} + +static bool scr_date_skip(ScrDateScan *in, int c) { + if (!scr_date_tok_symbol(in->next, c)) return false; + in->next = scr_date_scan_token(in); + return true; +} + +static bool scr_date_tok_sign(ScrDateTok t) { + return t.tag == SCR_DT_SYMBOL && (t.value == '-' || t.value == '+'); +} + +static bool scr_date_tok_fixed(ScrDateTok t, int length) { + return t.tag == SCR_DT_NUMBER && t.length == length; +} + +static bool scr_date_tok_z(ScrDateTok t) { + return t.tag == SCR_DK_ZONE && t.length == 1 && t.value == 0; +} + +static bool scr_date_between(int x, int lo, int hi) { return x >= lo && x <= hi; } + +typedef struct { + int comp[3], index, named_month; + bool iso; +} ScrDateDay; + +typedef struct { + int comp[4], index, hour_offset; +} ScrDateTime; + +typedef struct { + int sign, hour, minute; +} ScrDateZone; + +static bool scr_date_day_add(ScrDateDay *d, int n) { + if (d->index >= 3) return false; + d->comp[d->index++] = n; + return true; +} + +static bool scr_date_time_add(ScrDateTime *t, int n) { + if (t->index >= 4) return false; + t->comp[t->index++] = n; + return true; +} + +static bool scr_date_time_add_final(ScrDateTime *t, int n) { + if (!scr_date_time_add(t, n)) return false; + while (t->index < 4) t->comp[t->index++] = 0; + return true; +} + +static bool scr_date_time_expecting(const ScrDateTime *t, int n) { + return (t->index == 1 && scr_date_between(n, 0, 59)) || + (t->index == 2 && scr_date_between(n, 0, 59)) || + (t->index == 3 && scr_date_between(n, 0, 999)); +} + +static void scr_date_zone_set(ScrDateZone *z, int hours) { + z->sign = hours < 0 ? -1 : 1; + z->hour = hours * z->sign; + z->minute = 0; +} + +static bool scr_date_zone_expecting(const ScrDateZone *z, int n) { + return z->hour != SCR_DT_NONE && z->minute == SCR_DT_NONE && scr_date_between(n, 0, 59); +} + +/* ReadMilliseconds: the first three significant digits of the numeral. */ +static int scr_date_millis(ScrDateTok t) { + int number = t.value, length = t.length; + if (length == 1) number *= 100; + else if (length == 2) number *= 10; + else if (length > 3) { + static const int factors[] = {10, 100, 1000, 10000, 100000, 1000000}; + if (length > 9) length = 9; + number /= factors[length - 4]; + } + return number; +} + +/* ParseES5DateTime: answers the first token it could not place (the + * legacy grammar resumes there), END after a complete ES5 string, or + * INVALID for a malformed ES5 time part. */ +static ScrDateTok scr_date_parse_es5(ScrDateScan *in, ScrDateDay *day, ScrDateTime *time, + ScrDateZone *tz) { + ScrDateTok invalid; + invalid.tag = SCR_DT_INVALID; + invalid.length = 0; + invalid.value = -1; + if (scr_date_tok_sign(in->next)) { + ScrDateTok sign = scr_date_next(in); + if (!scr_date_tok_fixed(in->next, 6)) return sign; + int s = 44 - sign.value; + int year = scr_date_next(in).value; + if (s < 0 && year == 0) return sign; + scr_date_day_add(day, s * year); + } else if (scr_date_tok_fixed(in->next, 4)) { + scr_date_day_add(day, scr_date_next(in).value); + } else { + return scr_date_next(in); + } + if (scr_date_skip(in, '-')) { + if (!scr_date_tok_fixed(in->next, 2) || !scr_date_between(in->next.value, 1, 12)) { + return scr_date_next(in); + } + scr_date_day_add(day, scr_date_next(in).value); + if (scr_date_skip(in, '-')) { + if (!scr_date_tok_fixed(in->next, 2) || !scr_date_between(in->next.value, 1, 31)) { + return scr_date_next(in); + } + scr_date_day_add(day, scr_date_next(in).value); + } + } + if (in->next.tag != SCR_DK_T) { + if (in->next.tag != SCR_DT_END) return scr_date_next(in); + } else { + scr_date_next(in); + if (!scr_date_tok_fixed(in->next, 2) || !scr_date_between(in->next.value, 0, 24)) return invalid; + bool hour_is_24 = in->next.value == 24; + scr_date_time_add(time, scr_date_next(in).value); + if (!scr_date_skip(in, ':')) return invalid; + if (!scr_date_tok_fixed(in->next, 2) || !scr_date_between(in->next.value, 0, 59) || + (hour_is_24 && in->next.value > 0)) { + return invalid; + } + scr_date_time_add(time, scr_date_next(in).value); + if (scr_date_skip(in, ':')) { + if (!scr_date_tok_fixed(in->next, 2) || !scr_date_between(in->next.value, 0, 59) || + (hour_is_24 && in->next.value > 0)) { + return invalid; + } + scr_date_time_add(time, scr_date_next(in).value); + if (scr_date_skip(in, '.')) { + if (in->next.tag != SCR_DT_NUMBER || (hour_is_24 && in->next.value > 0)) return invalid; + scr_date_time_add(time, scr_date_millis(scr_date_next(in))); + } + } + if (scr_date_tok_z(in->next)) { + scr_date_next(in); + scr_date_zone_set(tz, 0); + } else if (scr_date_tok_sign(in->next)) { + tz->sign = scr_date_next(in).value == '+' ? 1 : -1; + if (scr_date_tok_fixed(in->next, 4)) { + int hourmin = scr_date_next(in).value; + int hour = hourmin / 100, minute = hourmin % 100; + if (!scr_date_between(hour, 0, 23) || !scr_date_between(minute, 0, 59)) return invalid; + tz->hour = hour; + tz->minute = minute; + } else { + if (!scr_date_tok_fixed(in->next, 2) || !scr_date_between(in->next.value, 0, 23)) { + return invalid; + } + tz->hour = scr_date_next(in).value; + if (!scr_date_skip(in, ':')) return invalid; + if (!scr_date_tok_fixed(in->next, 2) || !scr_date_between(in->next.value, 0, 59)) { + return invalid; + } + tz->minute = scr_date_next(in).value; + } + } + if (in->next.tag != SCR_DT_END) return invalid; + } + /* Date-only forms without a zone are UTC; date-times are local. */ + if (tz->hour == SCR_DT_NONE && time->index == 0) scr_date_zone_set(tz, 0); + day->iso = true; + ScrDateTok end; + end.tag = SCR_DT_END; + end.length = 0; + end.value = -1; + return end; +} + +static bool scr_date_day_write(ScrDateDay *d, double *year_out, double *month_out, double *day_out) { + if (d->index < 1) return false; + while (d->index < 3) d->comp[d->index++] = 1; + int year = 0, month, day; + if (d->named_month == SCR_DT_NONE) { + if (d->iso || !scr_date_between(d->comp[0], 1, 31)) { + year = d->comp[0]; + month = d->comp[1]; + day = d->comp[2]; + } else { + month = d->comp[0]; + day = d->comp[1]; + year = d->comp[2]; + } + } else { + month = d->named_month; + /* V8 pads to three components before choosing an order, so a lone + * day after a month name reads the padding 1 as the year (2001). */ + if (!scr_date_between(d->comp[0], 1, 31)) { + year = d->comp[0]; + day = d->comp[1]; + } else { + day = d->comp[0]; + year = d->comp[1]; + } + } + if (!d->iso) { + if (scr_date_between(year, 0, 49)) year += 2000; + else if (scr_date_between(year, 50, 99)) year += 1900; + } + if (!scr_date_between(month, 1, 12) || !scr_date_between(day, 1, 31)) return false; + *year_out = year; + *month_out = month - 1; + *day_out = day; + return true; +} + +static bool scr_date_time_write(ScrDateTime *t, double out[4]) { + while (t->index < 4) t->comp[t->index++] = 0; + int hour = t->comp[0], minute = t->comp[1], second = t->comp[2], millisecond = t->comp[3]; + if (t->hour_offset != SCR_DT_NONE) { + if (!scr_date_between(hour, 0, 12)) return false; + hour %= 12; + hour += t->hour_offset; + } + if (!scr_date_between(hour, 0, 23) || !scr_date_between(minute, 0, 59) || + !scr_date_between(second, 0, 59) || !scr_date_between(millisecond, 0, 999)) { + if (hour != 24 || minute != 0 || second != 0 || millisecond != 0) return false; + } + out[0] = hour; + out[1] = minute; + out[2] = second; + out[3] = millisecond; + return true; +} + +static bool scr_date_zone_write(ScrDateZone *z, double *offset_ms) { + if (z->sign == SCR_DT_NONE) { + *offset_ms = NAN; + return true; + } + if (z->hour == SCR_DT_NONE) z->hour = 0; + if (z->minute == SCR_DT_NONE) z->minute = 0; + unsigned total = (unsigned)z->hour * 3600u + (unsigned)z->minute * 60u; + if (total > (unsigned)INT_MAX) return false; + long long ms = (long long)total * 1000; + *offset_ms = (double)(z->sign < 0 ? -ms : ms); + return true; +} + +static double scr_date_parse_v8(ScrStr *s) { + /* Field-by-field initialization keeps vector constants out of the + * unit's shared constant pool (see scr_date_local_limit). */ + ScrDateScan in; + in.p = (const unsigned char *)s->data; + in.end = in.p + s->len; + in.ch = 0; + in.low = 0; + scr_date_scan_advance(&in); + in.next = scr_date_scan_token(&in); + ScrDateDay day; + day.index = 0; + day.named_month = SCR_DT_NONE; + day.iso = false; + ScrDateTime time; + time.index = 0; + time.hour_offset = SCR_DT_NONE; + ScrDateZone tz; + tz.sign = SCR_DT_NONE; + tz.hour = SCR_DT_NONE; + tz.minute = SCR_DT_NONE; + ScrDateTok token = scr_date_parse_es5(&in, &day, &time, &tz); + if (token.tag == SCR_DT_INVALID) return NAN; + bool has_read_number = day.index != 0; + for (; token.tag != SCR_DT_END; token = scr_date_next(&in)) { + if (token.tag == SCR_DT_NUMBER) { + has_read_number = true; + int n = token.value; + if (scr_date_skip(&in, ':')) { + if (scr_date_skip(&in, ':')) { + /* n:: */ + if (time.index != 0) return NAN; + scr_date_time_add(&time, n); + scr_date_time_add(&time, 0); + } else { + if (!scr_date_time_add(&time, n)) return NAN; + if (scr_date_tok_symbol(in.next, '.')) scr_date_next(&in); + } + } else if (scr_date_skip(&in, '.') && scr_date_time_expecting(&time, n)) { + scr_date_time_add(&time, n); + if (in.next.tag != SCR_DT_NUMBER) return NAN; + int ms = scr_date_millis(scr_date_next(&in)); + if (ms < 0) return NAN; + scr_date_time_add_final(&time, ms); + } else if (scr_date_zone_expecting(&tz, n)) { + tz.minute = n; + } else if (scr_date_time_expecting(&time, n)) { + scr_date_time_add_final(&time, n); + ScrDateTok peek = in.next; + if (peek.tag != SCR_DT_END && peek.tag != SCR_DT_SPACE && !scr_date_tok_z(peek) && + !scr_date_tok_sign(peek)) { + return NAN; + } + } else { + if (!scr_date_day_add(&day, n)) return NAN; + scr_date_skip(&in, '-'); + } + } else if (token.tag >= 0) { + if (token.tag == SCR_DK_AMPM && time.index != 0) { + time.hour_offset = token.value; + } else if (token.tag == SCR_DK_MONTH) { + day.named_month = token.value; + scr_date_skip(&in, '-'); + } else if (token.tag == SCR_DK_ZONE && has_read_number) { + scr_date_zone_set(&tz, token.value); + } else { + /* Garbage words are illegal once a number has been read, and the + * first number must be separated from them. */ + if (has_read_number) return NAN; + if (in.next.tag == SCR_DT_NUMBER) return NAN; + } + } else if (scr_date_tok_sign(token) && + ((tz.hour == 0 && tz.minute == 0) || time.index != 0)) { + /* A UTC offset, only after UTC or a time. */ + tz.sign = token.value == '-' ? -1 : 1; + int n = 0, length = 0; + if (in.next.tag == SCR_DT_NUMBER) { + ScrDateTok number = scr_date_next(&in); + length = number.length; + n = number.value; + } + has_read_number = true; + if (scr_date_tok_symbol(in.next, ':')) { + tz.hour = n; + tz.minute = SCR_DT_NONE; + } else if (length == 2 || length == 1) { + tz.hour = n; + tz.minute = 0; + } else if (length == 4 || length == 3) { + tz.hour = n / 100; + tz.minute = n % 100; + } else { + return NAN; + } + } else if ((scr_date_tok_sign(token) || scr_date_tok_symbol(token, ')')) && has_read_number) { + return NAN; + } + /* Anything else (whitespace, unknown characters) is ignored. */ + } + double year, month, date, hms[4], offset; + if (!scr_date_day_write(&day, &year, &month, &date) || !scr_date_time_write(&time, hms) || + !scr_date_zone_write(&tz, &offset)) { + return NAN; + } + double dayv = scr_date_v8_make_day(year, month, date); + double timev = scr_date_v8_make_time(hms[0], hms[1], hms[2], hms[3]); + if (isnan(offset)) return scr_date_from_local(dayv, timev); + if (!isfinite(dayv) || !isfinite(timev)) return NAN; + double t = timev + dayv * 86400000.0 - offset; + if (!(fabs(t) <= 8640000000000000.0)) return NAN; + return scr_date_new_ms(t); +} + /* ── Date time zone names ─────────────────────────────────────────────── * Long time zone names for Date.prototype.toString and toTimeString. * @@ -11878,6 +12843,29 @@ ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments) { return scr_dyn_native_date_from_ms(ms); } +/* new Date(value) in JavaScript: a string parses with the full parser; + * every other value takes scr_dyn_native_date_new. */ +ScrDyn *scr_dyn_native_date_new_value(const ScrDyn *arguments) { + if (arguments->v.arr.len == 1 && arguments->v.arr.items[0]->kind == SCR_DYN_STR) + return scr_dyn_native_date_from_ms(scr_date_new_ms(scr_date_parse(arguments->v.arr.items[0]->v.str))); + return scr_dyn_native_date_new(arguments); +} + +/* new Date(year, monthIndex, ...) in JavaScript: ToNumber over the first + * seven arguments in order (a throwing conversion stops there), the + * spec's defaults for the rest, then the local-time field constructor. */ +ScrDyn *scr_dyn_native_date_new_fields(const ScrDyn *arguments) { + double fields[7]; + fields[0] = fields[1] = fields[3] = fields[4] = fields[5] = fields[6] = 0; + fields[2] = 1; + size_t n = arguments->v.arr.len < 7 ? arguments->v.arr.len : 7; + for (size_t i = 0; i < n; i++) { + if (!scr_dyn_number_coerce_js(arguments->v.arr.items[i], &fields[i])) return NULL; + } + return scr_dyn_native_date_from_ms( + scr_date_new_local(fields[0], fields[1], fields[2], fields[3], fields[4], fields[5], fields[6])); +} + /* The Date.prototype methods scr_native_date_invoke answers. */ static const char *const scr_native_date_methods[] = { "getTime", "valueOf", "toISOString", "toJSON", "toString", scr_date_to_date_string_name, diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index 4c973e3e5..690f84875 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -149,6 +149,11 @@ void scr_process_exit(double code); extern const unsigned char scr_detached_exit; void scr_process_set_exit_code(double code); double scr_process_exit_code_or_zero(void); +/* The writer and the presence test of programs that read process.exitCode: + * the code reads undefined until an assignment (or a nonzero exit status + * the runtime chose, such as an uncaught exception's 1). */ +void scr_process_set_exit_code_tracked(double code); +bool scr_process_has_exit_code(void); /* process._exiting: true once the exit sequence began (process.exit or * the exit-listener runner set the flag). Never throws. */ extern SCR_TL bool scr_process_in_exit; diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index 1d01e5573..374b5469a 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -810,6 +810,12 @@ ScrDyn *scr_message_decode(const ScrMessage *message) { } if (!values[i]->v.arr.properties) values[i]->v.arr.properties = scr_dyn_new_obj(); scr_dyn_obj_set(values[i]->v.arr.properties, property->key, property->length, value); + } else if (node->kind == SCR_MESSAGE_ERROR && property->length == 5 && !memcmp(property->key, "stack", 5)) { + /* The new error's own `stack` accessor stores the value. */ + ScrStr *key = scr_str_new(property->key, property->length); + scr_dyn_key_set(values[i], key, value); + scr_str_release(key); + scr_dyn_release(value); } else scr_dyn_obj_set(values[i], property->key, property->length, value); } if (node->kind == SCR_MESSAGE_ERROR) { diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index 1b85fb0fe..78d19614b 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -118,9 +118,18 @@ ScrStr *scr_date_to_iso(double ms); /* Node's Date.prototype.toUTCString ("Thu, 01 Jan 1970 00:00:00 GMT"; * "Invalid Date" for NaN). Never throws; +1. */ ScrStr *scr_date_to_utc_string(double ms); -/* new Date(dateString).getTime(): the BOUNDED parse (the X509 validity - * shape + ECMA's format with an explicit offset); NaN elsewhere. */ +/* The BOUNDED parse (the X509 validity shape + ECMA's format with an + * explicit offset); NaN elsewhere. */ double scr_date_parse_get_time(ScrStr *s); +/* Date.parse and new Date(dateString): V8's date parser (the ES5 format, + * then its legacy grammar: RFC 1123/2822, Date#toString, "Jan 2, 2024", + * ...). Zone-less dates are local time. Never throws. */ +double scr_date_parse(ScrStr *s); +/* new Date(year, monthIndex, date, hours, minutes, seconds, ms) over + * already-number arguments: V8's MakeDay/MakeTime over LOCAL fields, then + * UTC(t) in the host zone and TimeClip. Never throws. */ +double scr_date_new_local(double y, double mo, double d, + double h, double mi, double s, double ms); /* Date.UTC over already-number arguments (frontend completes the spec's * defaults): MakeDay/MakeTime/TimeClip exactly — 0–99 maps to 1900+year, * month/date rollover, NaN for non-finite parts, out-of-range results, diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index eb28fb2e7..8611741da 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -352,6 +352,10 @@ void scr_sa_thread_starting(void); * pointer, or a process-wide cache keeps an immortal object. Its memory * then stays when it exits (scr_alloc.c). */ void scr_sa_thread_shares(void); +/* The event loop of the calling thread is about to wait with nothing ready + * (true), or is done waiting (false); worker threads are ignored. The + * memory of parked workers goes back while the loop waits (scr_alloc.c). */ +void scr_sa_loop_waiting(bool waiting); #define SCR_SA_FREE_FOREIGN(p) scr_sa_free_foreign(p) #else #define SCR_SA_FREE_FOREIGN(p) free(p) diff --git a/packages/runtime/test/sha-kernels.test.ts b/packages/runtime/test/sha-kernels.test.ts index 724841fb1..6f7759348 100644 --- a/packages/runtime/test/sha-kernels.test.ts +++ b/packages/runtime/test/sha-kernels.test.ts @@ -42,7 +42,7 @@ const runtimeSources = [ ]; // Compiled once with ASan, UBSan and the RC audit. The test runs the SHA-NI and portable -// SHA-1/SHA-256 blocks over the same inputs and checks known vectors. +// SHA-1/SHA-256 blocks and the SHA-512 kernels over the same inputs and checks known vectors. beforeAll(async () => { await mkdir(join(testDir, "build"), { recursive: true }); await execFileAsync("clang", [ @@ -64,7 +64,7 @@ beforeAll(async () => { ]); }, 300_000); -test("SHA-1 and SHA-256: the SHA-NI blocks match the portable blocks", async () => { +test("SHA-1, SHA-256, SHA-384 and SHA-512: the CPU kernels match the portable blocks", async () => { const { stderr } = await execFileAsync(bin, [], { env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" }, }); diff --git a/packages/runtime/test/test_sha_kernels.c b/packages/runtime/test/test_sha_kernels.c index 7f854216e..da7d79235 100644 --- a/packages/runtime/test/test_sha_kernels.c +++ b/packages/runtime/test/test_sha_kernels.c @@ -1,9 +1,11 @@ -/* SHA-1 and SHA-256 block kernels (scr_lib.c), built by +/* SHA-1, SHA-256, SHA-384 and SHA-512 block kernels (scr_lib.c), built by * sha-kernels.test.ts. The unit is included directly so the test can run - * the SHA-NI blocks and the portable blocks over the same inputs: known - * vectors, every length from 0 to 1100 bytes, and 1 MiB, each fed whole - * and in 1-, 7-, 64- and 100-byte updates. On CPUs without the SHA - * extensions only the portable blocks run. */ + * each kernel the CPU supports and the portable blocks over the same + * inputs: known vectors, every length from 0 to 1100 bytes, and 1 MiB, + * each fed whole and in 1-, 7-, 64- and 100-byte updates. The SHA-1 and + * SHA-256 kernels use the SHA extensions; the SHA-512 kernels use AVX2 or + * AVX-512VL on x86-64 and the SHA-512 extension on arm64. On CPUs without + * them only the portable blocks run. */ #include "../src/scr_lib.c" #include @@ -45,6 +47,71 @@ static void sha1_of(const unsigned char *data, size_t len, size_t chunk, unsigne scr_sha1_final(&ctx, out); } +static void sha512_of(const unsigned char *data, size_t len, size_t chunk, bool sha384, + unsigned char out[64]) { + ScrSha512Ctx ctx; + scr_sha512_init(&ctx, sha384); + for (size_t at = 0; at < len;) { + size_t take = chunk == 0 || chunk > len - at ? len - at : chunk; + scr_sha512_update(&ctx, data + at, take); + at += take; + } + scr_sha512_final(&ctx, out, sha384); +} + +/* FIPS 180-4 example vectors for SHA-384 and SHA-512. */ +static void vectors512(const char *path) { + unsigned char d[64]; + char text[129]; + static const char two_block[] = + "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu"; + sha512_of((const unsigned char *)"abc", 3, 0, false, d); + hex(d, 64, text); + check(strcmp(text, "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f") == 0, path, 3, 0); + sha512_of((const unsigned char *)"", 0, 0, false, d); + hex(d, 64, text); + check(strcmp(text, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") == 0, path, 0, 0); + sha512_of((const unsigned char *)two_block, sizeof two_block - 1, 0, false, d); + hex(d, 64, text); + check(strcmp(text, "8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909") == 0, path, sizeof two_block - 1, 0); + sha512_of((const unsigned char *)"abc", 3, 0, true, d); + hex(d, 48, text); + check(strcmp(text, "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7") == 0, path, 3, 0); + sha512_of((const unsigned char *)two_block, sizeof two_block - 1, 0, true, d); + hex(d, 48, text); + check(strcmp(text, "09330c33f71147e83d192fc782cd1b4753111b173b3b05d22fa08086e3b0f712fcc7c71a557e2db966c3e9fa91746039") == 0, path, sizeof two_block - 1, 0); + static unsigned char million[1000000]; + memset(million, 'a', sizeof million); + sha512_of(million, sizeof million, 0, false, d); + hex(d, 64, text); + check(strcmp(text, "e718483d0ce769644e2e42c7bc15b4638e1f98b13b2044285632a803afa973ebde0ff244877ea60a4cb0432ce577c31beb009c5c2c49aa2e4eadb217ad8cc09b") == 0, path, sizeof million, 0); + sha512_of(million, sizeof million, 0, true, d); + hex(d, 48, text); + check(strcmp(text, "9d0e1809716474cb086e834e310a4a1ced149e9c00f248527972cec5704c2a5b07b8b3dc38ecc4ebae97ddd87f3d8985") == 0, path, sizeof million, 0); +} + +/* Selects the SHA-512 kernel: 0 the portable blocks, otherwise the CPU's + * kernel. Returns false when the CPU has none. */ +static bool select512(int kernel) { +#ifdef SCR_SHA_NI + scr_sha512_x86_state = 0; + int best = scr_sha512_x86(); + if (kernel > best - 1) return false; + scr_sha512_x86_state = kernel == 0 ? 1 : kernel + 1; + return true; +#elif defined(SCR_SHA512_ARM) + scr_sha512_arm_state = 0; + bool available = scr_sha512_arm(); + if (kernel > (available ? 1 : 0)) return false; + scr_sha512_arm_state = kernel == 0 ? 2 : 1; + return true; +#else + return kernel == 0; +#endif +} + +static const char *const kernel512_names[] = {"portable", "avx2-or-arm", "avx512"}; + static void vectors(const char *path) { unsigned char d[32]; char text[65]; @@ -108,8 +175,31 @@ int main(void) { vectors("ni"); } #endif + /* SHA-512 and SHA-384: every kernel against the portable blocks, and + * the known vectors through each. */ + int kernels512 = 0; + for (int k = 0; k < 3 && select512(k); k++) { + kernels512 = k + 1; + vectors512(kernel512_names[k]); + } + for (int k = 1; k < kernels512; k++) { + for (size_t len = 0; len <= 1100 + 1; len++) { + size_t n = len == 1101 ? BIG : len; + for (size_t c = 0; c < sizeof chunks / sizeof chunks[0]; c++) { + for (int sha384 = 0; sha384 < 2; sha384++) { + unsigned char a[64], b[64]; + select512(k); + sha512_of(data, n, chunks[c], sha384, a); + select512(0); + sha512_of(data, n, chunks[c], sha384, b); + check(memcmp(a, b, sha384 ? 48 : 64) == 0, kernel512_names[k], n, chunks[c]); + } + } + } + } free(data); fprintf(stderr, "sha-ni %s\n", ni ? "available" : "absent"); + fprintf(stderr, "sha-512 kernels %d\n", kernels512); fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total); return failed != 0; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41d9e02ac..cbe529577 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,6 +106,9 @@ importers: '@scriptc/runtime': specifier: workspace:* version: link:../runtime + tsc-ts: + specifier: 0.0.3 + version: 0.0.3 typescript: specifier: 7.0.2 version: 7.0.2 @@ -1367,6 +1370,45 @@ packages: '@ts-morph/common@0.11.1': resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==} + '@tsc-ts/darwin-arm64@0.0.3': + resolution: {integrity: sha512-NSP4IhLx658tY21ub3ruraZwUwAC3GCbIf1eM0eOKUHfOBBXqq7LU/Loj3zvlvn/tJUyv2VdIDmzrWI/mxp5sA==} + cpu: [arm64] + os: [darwin] + + '@tsc-ts/darwin-x64@0.0.3': + resolution: {integrity: sha512-+BNvqyygkMx0/oHkjK8AmsPrE2+OcE/hGzT0wa2dndiH8lCSDBs+77C7Kmy/FSTWi9ItJShp1+AuWKgQPEOztg==} + cpu: [x64] + os: [darwin] + + '@tsc-ts/linux-arm64-gnu@0.0.3': + resolution: {integrity: sha512-I3v0J0L387TEkau0BTGiVx2GdstYfOrwVYwb0pJ/LbFURhrcGcsUODP5bdSOIYCtsp3aAX6e2afdqSIlpsxtbw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tsc-ts/linux-arm64-musl@0.0.3': + resolution: {integrity: sha512-f7wD9D6fC1xL7+ifvMAbJ0TqxOuZP04R2/7c8lb7Gor5SFKe85udJoxf0gocK7mbzWzmEiOJ53J1PujJLBVIxA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tsc-ts/linux-x64-gnu@0.0.3': + resolution: {integrity: sha512-BL9zownW4tHv/qqQkF8OlurHNDXi7xtGtJ6PV4qMeRYPMKsivTIGFRoZLW023KcLOr1Zptaof6X3zpv1nJEIpA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tsc-ts/linux-x64-musl@0.0.3': + resolution: {integrity: sha512-g4/lE5sULNGUVLM0Px/jrsjLsQQAYSiL+Rl+JZ+l5JE2k1dXroCejT2+IRB/NucVcz6N8EOUiCRZ13zl+Wtpcg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tsc-ts/win32-x64-msvc@0.0.3': + resolution: {integrity: sha512-oGtboSHgpuQP1dHAHqWyGjAWCpvHDpciYCi/2ywcp6MT+oOHxC99X48H/PZr29fzYVxCVeaEqWZ/VFU9qLZaKg==} + cpu: [x64] + os: [win32] + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -2524,6 +2566,11 @@ packages: ts-toolbelt@6.15.5: resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} + tsc-ts@0.0.3: + resolution: {integrity: sha512-Qr0i/BxP3ULIPDfr38vRb02x7sj6Q0r70Gubgu4ty2J4CEDCUKzT8RT2KMNqLXI3ZAhl7cncJq7a4LjKYZ1+0g==} + engines: {node: '>=20'} + hasBin: true + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -3394,6 +3441,27 @@ snapshots: mkdirp: 1.0.4 path-browserify: 1.0.1 + '@tsc-ts/darwin-arm64@0.0.3': + optional: true + + '@tsc-ts/darwin-x64@0.0.3': + optional: true + + '@tsc-ts/linux-arm64-gnu@0.0.3': + optional: true + + '@tsc-ts/linux-arm64-musl@0.0.3': + optional: true + + '@tsc-ts/linux-x64-gnu@0.0.3': + optional: true + + '@tsc-ts/linux-x64-musl@0.0.3': + optional: true + + '@tsc-ts/win32-x64-msvc@0.0.3': + optional: true + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -4730,6 +4798,16 @@ snapshots: ts-toolbelt@6.15.5: {} + tsc-ts@0.0.3: + optionalDependencies: + '@tsc-ts/darwin-arm64': 0.0.3 + '@tsc-ts/darwin-x64': 0.0.3 + '@tsc-ts/linux-arm64-gnu': 0.0.3 + '@tsc-ts/linux-arm64-musl': 0.0.3 + '@tsc-ts/linux-x64-gnu': 0.0.3 + '@tsc-ts/linux-x64-musl': 0.0.3 + '@tsc-ts/win32-x64-msvc': 0.0.3 + tslib@2.8.1: optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8f10bfca8..d4d370721 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,6 +7,14 @@ minimumReleaseAgeExclude: - "effect@4.0.0" - "@effect/platform-node@4.0.0" - "@effect/platform-node-shared@4.0.0" + - "@tsc-ts/darwin-arm64@0.0.3" + - "@tsc-ts/darwin-x64@0.0.3" + - "@tsc-ts/linux-arm64-gnu@0.0.3" + - "@tsc-ts/linux-arm64-musl@0.0.3" + - "@tsc-ts/linux-x64-gnu@0.0.3" + - "@tsc-ts/linux-x64-musl@0.0.3" + - "@tsc-ts/win32-x64-msvc@0.0.3" + - "tsc-ts@0.0.3" allowBuilds: esbuild: true overrides: diff --git a/scripts/build-native-cli.mts b/scripts/build-native-cli.mts index fa4cb74a7..c8a14adbd 100644 --- a/scripts/build-native-cli.mts +++ b/scripts/build-native-cli.mts @@ -5,7 +5,7 @@ import { basename, dirname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { compile } from "../packages/compiler/src/index.js"; import { NATIVE_TARGETS, nativeCodegenTarget } from "../packages/compiler/src/backend/targets.js"; -import { ts7Executable } from "../packages/compiler/src/frontend/ts7/rpc-api.js"; +import { tsgoExecutable } from "../packages/compiler/src/frontend/ts7/rpc-api.js"; import { stageNativeCliAssets } from "./native-cli-assets.mjs"; import type { NativeToolchainManifest } from "../packages/compiler/src/native/toolchain.js"; @@ -108,7 +108,7 @@ const assets = stageNativeCliAssets({ repository: root, output, target, - ts7: ts7Executable(), + ts7: tsgoExecutable(), compilerVersion, }); // A cross-libc seed may use Zig on the build host. The installed compiler diff --git a/scripts/checker-identity.mjs b/scripts/checker-identity.mjs new file mode 100644 index 000000000..a710d5946 --- /dev/null +++ b/scripts/checker-identity.mjs @@ -0,0 +1,156 @@ +#!/usr/bin/env node +// Compiles every corpus program to LLVM IR with each TypeScript 7 API server (tsc-ts, the default, and tsgo with +// SCRIPTC_CHECKER=tsgo) and fails unless both give the same IR and the same diagnostics. The text of a checker panic +// is the checker's own (Go's runtime error for tsgo, tsc-ts's for tsc-ts), so it is compared as `panic: …`. +// +// Usage: node scripts/checker-identity.mjs [--jobs ] [--optimization release|dev] [--filter ] +// +// It needs `pnpm build`. Each checker compiles in its own worker processes, which read SCRIPTC_CHECKER when they +// start their servers. +import { fork } from "node:child_process"; +import { globSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { availableParallelism, tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { parseArgs } from "node:util"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const corpusDir = join(repoRoot, "tests/corpus"); +const checkers = ["tsgo", "tsc-ts"]; + +if (process.argv[2] === "--worker") await worker(process.argv.slice(3)); +else await main(); + +async function main() { + const { values } = parseArgs({ + options: { + jobs: { + type: "string", + default: String(Math.max(1, Math.floor(availableParallelism() / 2))), + }, + optimization: { type: "string", default: "release" }, + filter: { type: "string", default: "" }, + }, + }); + if (values.optimization !== "release" && values.optimization !== "dev") + throw new Error("--optimization must be release or dev"); + // The entries llvm-differential.test.ts compiles. + const entries = ["ts", "js", "mjs", "cjs"] + .flatMap((ext) => [ + ...globSync(join(corpusDir, `*.${ext}`)), + ...globSync(join(corpusDir, `*/main.${ext}`)), + ]) + .filter((file) => file.includes(values.filter)) + .sort(); + const jobs = Math.max(1, Number(values.jobs)); + const outRoot = mkdtempSync(join(tmpdir(), "scriptc-checker-identity-")); + try { + const results = new Map(checkers.map((checker) => [checker, new Map()])); + const runs = []; + for (const checker of checkers) + for (let index = 0; index < jobs; index++) + runs.push( + new Promise((done, fail) => { + const child = fork( + fileURLToPath(import.meta.url), + [ + "--worker", + join(outRoot, checker), + values.optimization, + ...entries.filter((_, i) => i % jobs === index), + ], + { env: { ...process.env, SCRIPTC_CHECKER: checker } }, + ); + child.on("message", (record) => results.get(checker).set(record.file, record)); + child.on("error", fail); + child.on("exit", (code) => + code === 0 ? done() : fail(new Error(`${checker} worker exited with ${code}`)), + ); + }), + ); + await Promise.all(runs); + const differences = []; + for (const file of entries) { + const [a, b] = checkers.map((checker) => results.get(checker).get(file)); + const name = file.slice(corpusDir.length + 1); + if (a === undefined || b === undefined) differences.push(`${name}: not compiled`); + else if (a.ok !== b.ok || withoutPanicText(a.diagnostics) !== withoutPanicText(b.diagnostics)) + differences.push( + `${name}: tsgo ${a.ok ? "compiles" : a.diagnostics}; tsc-ts ${b.ok ? "compiles" : b.diagnostics}`, + ); + else if (a.ok && irWithoutPanicText(a.llvm) !== irWithoutPanicText(b.llvm)) + differences.push(`${name}: LLVM IR differs`); + } + if (differences.length > 0) { + console.error( + `${differences.length} of ${entries.length} programs differ between tsgo and tsc-ts:`, + ); + for (const line of differences) console.error(` ${line}`); + process.exitCode = 1; + } else { + console.log( + `${entries.length} programs: tsgo and tsc-ts give the same LLVM IR and diagnostics (${values.optimization}).`, + ); + } + } finally { + rmSync(outRoot, { recursive: true, force: true }); + } +} + +/** A diagnostic list with each SC0004 checker panic's text replaced by `…`. */ +function withoutPanicText(diagnostics) { + return diagnostics?.replace(/(SC0004: [^;]*?: panic: )[^;]*/g, "$1…"); +} + +/** A module's LLVM IR with each SC0004 checker panic's text replaced by `…`. A fence statement keeps the + * diagnostic's message in a string constant named for a hash of its contents, and passes its length. */ +function irWithoutPanicText(llvm) { + let ir = readFileSync(llvm, "utf8"); + const constant = + /^(@sc_cs_\d+) = internal constant \[\d+ x i8\] c"(the TypeScript checker crashed answering a query here [^"]*?: panic: )[^"]*?( \[SC0004 at [^"]*)"$/gm; + const names = []; + ir = ir.replace(constant, (_, name, head, tail) => { + names.push(name); + return `@checker_panic_${names.length} = internal constant c"${head}…${tail}"`; + }); + names.forEach((name, index) => { + ir = ir.replace( + new RegExp(`${name}\\b(, i64 \\d+)?`, "g"), + (_, length) => `@checker_panic_${index + 1}${length === undefined ? "" : ", i64 …"}`, + ); + }); + return ir; +} + +async function worker([outDir, optimization, ...files]) { + const { compile } = await import( + pathToFileURL(join(repoRoot, "packages/compiler/dist/index.js")).href + ); + for (const file of files) { + const name = file.slice(corpusDir.length + 1); + const llvm = join(outDir, `${name}.ll`); + let record; + try { + const result = await compile(file, { + outputKind: "llvm", + outPath: llvm, + outDir: join(outDir, ".work", name), + optimization, + }); + record = result.ok + ? { file, ok: true, llvm } + : { + file, + ok: false, + diagnostics: result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("; "), + }; + } catch (error) { + record = { + file, + ok: false, + diagnostics: `threw ${error instanceof Error ? error.message : String(error)}`, + }; + } + process.send(record); + } +} diff --git a/scripts/native-cli-assets.mts b/scripts/native-cli-assets.mts index 614fe52d5..b3d1436bd 100644 --- a/scripts/native-cli-assets.mts +++ b/scripts/native-cli-assets.mts @@ -2,6 +2,10 @@ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync import { basename, dirname, join, relative } from "node:path"; import type { NativeTargetSpec } from "../packages/compiler/src/backend/targets.js"; import type { NativeToolchainManifest } from "../packages/compiler/src/native/toolchain.js"; +import { + tscTsExecutable, + tscTsPlatformPackage, +} from "../packages/compiler/src/frontend/ts7/checker-backend.js"; /** Copy release payloads explicitly; development caches and build trees never ship. */ function copyPackage(source: string, destination: string, members: readonly string[]): void { @@ -28,6 +32,7 @@ export function stageNativeCliAssets(options: { }): Pick< NativeToolchainManifest, | "ts7" + | "tsc_ts" | "llvm_package" | "runtime_pack" | "runtime_packs" @@ -54,6 +59,14 @@ export function stageNativeCliAssets(options: { ); const typescript = join(lib, "typescript"); copyPackage(dirname(dirname(options.ts7)), typescript, ["lib"]); + // tsc-ts, the default checker, from the tsc-ts package the compiler depends on. + const tscTsSource = tscTsExecutable( + [join(repository, "packages/compiler")], + tscTsPlatformPackage(target.platform, target.architecture, target.name.endsWith("-musl")), + target.platform, + ); + const tscTs = join(lib, "tsc-ts"); + copyPackage(dirname(dirname(tscTsSource)), tscTs, ["lib"]); const sources = join(lib, "runtime-sources"); copyPackage(join(repository, "packages/runtime"), sources, ["src", "vendor"]); const declarations = join(lib, "declarations"); @@ -74,11 +87,15 @@ export function stageNativeCliAssets(options: { const identity = JSON.parse(readFileSync(join(typescript, "package.json"), "utf8")) as { version: string; }; + const tscTsIdentity = JSON.parse(readFileSync(join(tscTs, "package.json"), "utf8")) as { + version: string; + }; writeFileSync( join(output, "THIRD_PARTY_NOTICES"), [ - `scriptc ${options.compilerVersion} includes the existing TypeScript ${identity.version}, LLVM, and C runtime dependencies.`, + `scriptc ${options.compilerVersion} includes the existing TypeScript ${identity.version}, tsc-ts ${tscTsIdentity.version}, LLVM, and C runtime dependencies.`, "TypeScript licensing: lib/typescript/LICENSE and lib/typescript/NOTICE.txt.", + "tsc-ts licensing and notices: lib/tsc-ts/LICENSE and lib/tsc-ts/THIRD_PARTY_NOTICES.md.", "LLVM licensing and notices: lib/llvm/LICENSE and lib/llvm/THIRD_PARTY_NOTICES.", "Runtime dependency licenses: lib/runtime-sources/vendor/*/LICENSE*.", "", @@ -86,6 +103,7 @@ export function stageNativeCliAssets(options: { ); return { ts7: relative(bin, join(typescript, "lib", basename(options.ts7))), + tsc_ts: relative(bin, join(tscTs, "lib", basename(tscTsSource))), llvm_package: relative(bin, helper), runtime_pack: relative(bin, runtime), runtime_packs: [{ target: target.name, path: relative(bin, runtime) }], diff --git a/scripts/package-native-cli.mjs b/scripts/package-native-cli.mjs index dd42b8eaa..6d1c05991 100644 --- a/scripts/package-native-cli.mjs +++ b/scripts/package-native-cli.mjs @@ -44,6 +44,7 @@ for (const name of readdirSync(packages) for (const path of [ binary, resolve(bin, hostPath(manifest.ts7)), + resolve(bin, hostPath(manifest.tsc_ts)), resolve(bin, hostPath(manifest.comptime)), join( resolve(bin, hostPath(manifest.llvm_package)), diff --git a/scripts/verify-native-cli.mjs b/scripts/verify-native-cli.mjs index b1e061664..115573b5c 100644 --- a/scripts/verify-native-cli.mjs +++ b/scripts/verify-native-cli.mjs @@ -28,6 +28,7 @@ const within = (path) => { }; for (const name of [ "ts7", + "tsc_ts", "llvm_package", "runtime_pack", "runtime_sources", diff --git a/tests/corpus/5779-sha512-lengths.ts b/tests/corpus/5779-sha512-lengths.ts new file mode 100644 index 000000000..277408409 --- /dev/null +++ b/tests/corpus/5779-sha512-lengths.ts @@ -0,0 +1,35 @@ +// SHA-384 and SHA-512 over every length from 0 to 520 bytes, fed whole and +// in 1-, 7-, 100- and 128-byte updates, then over 1 MiB, and HMAC-SHA512 +// over a few lengths. Each length's digests are folded into one running +// SHA-512 so the output stays short. The CPU's SHA-512 kernel (vector or +// SHA-512 instructions) and the portable blocks give the same digests. +import { createHash, createHmac } from "node:crypto"; + +const data = Buffer.alloc(1 << 20); +let x = 0x9e3779b9; +for (let i = 0; i < data.length; i++) { + x ^= x << 13; + x ^= x >>> 17; + x ^= x << 5; + data[i] = x & 0xff; +} + +function digest(algorithm: string, input: Buffer, chunk: number): Buffer { + const hash = createHash(algorithm); + if (chunk === 0) hash.update(input); + else for (let at = 0; at < input.length; at += chunk) hash.update(input.subarray(at, at + chunk)); + return hash.digest(); +} + +for (const algorithm of ["sha384", "sha512"]) { + for (const chunk of [0, 1, 7, 100, 128]) { + const fold = createHash("sha512"); + for (let length = 0; length <= 520; length++) fold.update(digest(algorithm, data.subarray(0, length), chunk)); + console.log(algorithm, "chunk", chunk, fold.digest("hex").slice(0, 32)); + } + console.log(algorithm, "1 MiB", digest(algorithm, data, 0).toString("hex").slice(0, 32), digest(algorithm, data, 4096).toString("hex").slice(0, 32)); +} +console.log("abc", createHash("sha512").update("abc").digest("hex")); +for (const length of [0, 1, 111, 112, 127, 128, 129, 255, 256, 1000]) { + console.log("hmac", length, createHmac("sha512", "key").update(data.subarray(0, length)).digest("hex").slice(0, 32)); +} diff --git a/tests/corpus/5816-fixed-into-unknown-rest.ts b/tests/corpus/5816-fixed-into-unknown-rest.ts new file mode 100644 index 000000000..f6d287021 --- /dev/null +++ b/tests/corpus/5816-fixed-into-unknown-rest.ts @@ -0,0 +1,39 @@ +// A function with only `unknown` parameters, or none, flows into a +// `(...args: unknown[]) => R` slot: each call passes the callers' +// arguments in order, undefined for a parameter the caller left out, and +// drops the extra ones, as JS calls do. Sync and async functions, stored +// in variables and arrays and passed as arguments. +type Handler = (...args: unknown[]) => void; +type AsyncHandler = (...args: unknown[]) => Promise; + +const none: Handler = () => console.log("none"); +const one: Handler = (a: unknown) => console.log("one", a); +const two: Handler = (a: unknown, b: unknown) => console.log("two", a, b); + +none(); +none(1, 2, 3); +one(); +one("x"); +one("x", "y"); +two(1); +two(1, [2], { three: 3 }); + +const handlers: Handler[] = [none, one, two]; +for (const h of handlers) h("all", true); + +function dispatch(h: Handler, ...args: unknown[]): void { + h(...args); +} +dispatch(two, "spread", null); +dispatch(one); + +const first: AsyncHandler = async (value: unknown) => value; +const pair: AsyncHandler = async (a: unknown, b: unknown) => [a, b]; + +async function main(): Promise { + console.log(await first(10, 20)); + console.log(await first()); + console.log(await pair("p")); + console.log(await pair("p", "q", "r")); +} +main().then(() => console.log("done")); diff --git a/tests/corpus/5916-dyn-error-stack-property.ts b/tests/corpus/5916-dyn-error-stack-property.ts new file mode 100644 index 000000000..2e1b42ef3 --- /dev/null +++ b/tests/corpus/5916-dyn-error-stack-property.ts @@ -0,0 +1,147 @@ +// Errors held as `any` or `unknown` have an own `stack` like Node's: a +// non-enumerable accessor that every error shares. The first read formats +// "Name: message" from the error's name and message at that point, and +// later reads answer the same string. Assigning a value replaces it, +// `delete` removes it, Object.keys, spread, Object.assign and +// JSON.stringify leave it out, `in` and Object.hasOwn find it, objects +// that inherit from an error read the error's stack. Typed and dynamic +// reads answer the same string, and a stack read through `any` captures +// frames. Only the first line is printed: frame lines differ between +// runtimes. + +class ParseError extends Error { + constructor(message: string) { + super(message); + this.name = "ParseError"; + } +} + +class PlainSubclass extends Error {} + +function dyn(value: unknown): any { + return value; +} + +function firstLine(value: unknown): string { + return typeof value === "string" ? JSON.stringify(value.split("\n")[0]) : `(${typeof value} ${String(value)})`; +} + +function describe(label: string, error: any): void { + console.log( + label, + "|", + typeof error.stack, + firstLine(error.stack), + "|", + Object.keys(error).join(","), + JSON.stringify(error), + "|", + "stack" in error, + Object.hasOwn(error, "stack"), + error.propertyIsEnumerable("stack"), + ); +} + +try { + throw new RangeError("thrown"); +} catch (error) { + describe("caught", error); +} + +const unknownError: unknown = new TypeError("bad thing"); +describe("unknown", unknownError); +describe("plain", dyn(new Error("plain"))); +describe("no message", dyn(new SyntaxError())); +describe("subclass", dyn(new ParseError("line 3"))); +describe("plain subclass", dyn(new PlainSubclass("inherited name"))); + +const coded = dyn(new Error("with code")); +coded.code = "E_CODE"; +describe("own property", coded); + +const descriptor = Object.getOwnPropertyDescriptor(dyn(new Error("a")), "stack")!; +const other = Object.getOwnPropertyDescriptor(dyn(new TypeError("b")), "stack")!; +console.log("descriptor", Object.keys(descriptor).join(","), descriptor.enumerable, descriptor.configurable); +console.log("shared accessors", typeof descriptor.get, descriptor.get === other.get, descriptor.set === other.set); +console.log("foreign receiver", descriptor.get!.call({}), firstLine(descriptor.get!.call(dyn(new TypeError("b"))))); +const subclassDescriptor = Object.getOwnPropertyDescriptor(dyn(new PlainSubclass("s")), "stack")!; +console.log("subclass descriptor", Object.keys(subclassDescriptor).join(","), subclassDescriptor.get === descriptor.get); + +const renamed = dyn(new Error("before")); +renamed.message = "after"; +renamed.name = "Renamed"; +console.log("first read", firstLine(renamed.stack)); +renamed.message = "later"; +console.log("later read", firstLine(renamed.stack)); +const subclassRenamed = dyn(new PlainSubclass("before")); +subclassRenamed.message = "after"; +console.log("subclass first read", firstLine(subclassRenamed.stack)); + +const replaced = dyn(new Error("replaced")); +replaced.stack = "custom stack\n at nowhere"; +describe("assigned", replaced); +console.log("assigned descriptor", Object.keys(Object.getOwnPropertyDescriptor(replaced, "stack")!).join(",")); +const subclassReplaced = dyn(new ParseError("replaced")); +subclassReplaced.stack = "subclass stack"; +console.log("subclass assigned", subclassReplaced.stack); +const typedReplaced = new ParseError("typed write"); +typedReplaced.stack = "typed subclass stack"; +console.log("typed subclass assigned", typedReplaced.stack, dyn(typedReplaced).stack); +const subclassNumbered = dyn(new PlainSubclass("numbered")); +subclassNumbered.stack = 7; +console.log("subclass number", subclassNumbered.stack, typeof subclassNumbered.stack); +subclassNumbered.stack = "string again"; +console.log("subclass string again", subclassNumbered.stack); +const numbered = dyn(new Error("numbered")); +numbered.stack = 5; +console.log("number", numbered.stack, typeof numbered.stack); +const removed = dyn(new Error("removed")); +delete removed.stack; +console.log("deleted", removed.stack, "stack" in removed, Object.hasOwn(removed, "stack")); + +const spreadSource = dyn(new Error("spread")); +console.log("spread", Object.keys({ ...spreadSource }).length, Object.keys(Object.assign({}, spreadSource)).length); +console.log("inherited", firstLine(Object.create(dyn(new RangeError("prototype"))).stack)); + +const typed = new Error("typed"); +const view = dyn(typed); +console.log("typed and dynamic", view.stack === typed.stack, firstLine(typed.stack)); +const typedSubclass = new ParseError("typed subclass"); +console.log("typed subclass", firstLine(dyn(typedSubclass).stack), dyn(typedSubclass).stack === typedSubclass.stack); +const assignedThroughView = new Error("typed"); +dyn(assignedThroughView).stack = "through the view"; +console.log("assigned through view", assignedThroughView.stack); + +function inner(): any { + return dyn(new Error("deep")); +} +function outer(): any { + return inner(); +} +const deep: string = outer().stack; +console.log("frames", deep.split("\n").length > 1, deep.includes("at inner"), deep.includes("at outer")); +function makeSubclass(): any { + return dyn(new PlainSubclass("deep")); +} +console.log("subclass frames", (makeSubclass().stack as string).includes("at makeSubclass")); + +const formatted = dyn(new Error("formatted")); +console.log("formatted before clone", firstLine(formatted.stack)); +console.log("clone keeps a formatted stack", firstLine(dyn(structuredClone(formatted)).stack)); + +class GrumpyError extends Error { + override get name(): string { + throw new Error("no name today"); + } +} +try { + console.log("throwing name getter", firstLine(dyn(new GrumpyError("grumpy")).stack)); +} catch (error) { + console.log("throwing name getter threw", (error as Error).message); +} + +function readKey(target: any, key: string): unknown { + return target[key]; +} +const computed = readKey(inner(), ["sta", "ck"].join("")) as string; +console.log("computed key", firstLine(computed), computed.includes("at inner")); diff --git a/tests/corpus/6004-class-instance-integrity.ts b/tests/corpus/6004-class-instance-integrity.ts new file mode 100644 index 000000000..7df516802 --- /dev/null +++ b/tests/corpus/6004-class-instance-integrity.ts @@ -0,0 +1,169 @@ +export {}; +import { inspect } from "node:util"; + +const message = (run: () => void): string => { + try { + run(); + return "no error"; + } catch (e) { + return `${e instanceof TypeError ? "TypeError" : "Error"}: ${(e as Error).message}`; + } +}; + +class Point { + x = 1; + y = 2; + label = "p"; + move(dx: number): void { + this.x += dx; + } +} + +const p = new Point(); +console.log(Object.isFrozen(p), Object.isSealed(p), Object.isExtensible(p)); +const same = Object.freeze(p); +console.log(same === p, Object.isFrozen(p), Object.isSealed(p), Object.isExtensible(p)); +console.log(message(() => { + p.x = 5; +})); +console.log(message(() => p.move(1))); +console.log(p.x, Object.keys(p)); +const q = new Point(); +q.x = 9; +console.log(q.x, Object.isFrozen(q)); + +class Point3 extends Point { + z = 3; +} +const p3 = new Point3(); +Object.freeze(p3); +console.log(message(() => { + p3.z = 1; +})); +const asBase: Point = new Point3(); +Object.freeze(asBase); +console.log(message(() => { + asBase.y = 0; +})); +console.log(asBase.y); + +class Counter { + n = 0; +} +const c = Object.seal(new Counter()); +c.n = 5; +console.log(c.n, Object.isSealed(c), Object.isFrozen(c), Object.isExtensible(c)); +const d = new Counter(); +Object.preventExtensions(d); +d.n = 2; +console.log(d.n, Object.isExtensible(d), Object.isSealed(d), Object.isFrozen(d)); +const loose: any = d; +console.log(message(() => { + loose.extra = 1; +})); + +class Config { + mode = "dev"; + level = 1; +} +const cfg = new Config(); +Object.defineProperty(cfg, "mode", { value: "prod", writable: false }); +console.log(cfg.mode, Object.keys(cfg), Object.values(cfg)); +console.log(message(() => { + cfg.mode = "x"; +})); +Object.defineProperty(cfg, "level", { enumerable: false }); +console.log(Object.keys(cfg), Object.entries(cfg), cfg.level); +Object.defineProperty(cfg, "level", { configurable: false }); +console.log(message(() => Object.defineProperty(cfg, "level", { enumerable: true }))); +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(cfg, "mode"))); +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(cfg, "level"))); +cfg.level = 7; +console.log(cfg.level); + +const fz = Object.freeze(new Config()); +console.log(message(() => Object.defineProperty(fz, "level", { value: 2 }))); +console.log(message(() => Object.defineProperty(fz, "level", { value: 1 }))); +console.log(fz.level, JSON.stringify(Object.getOwnPropertyDescriptor(fz, "level"))); + +class Frozen { + a = 1; + constructor() { + Object.freeze(this); + } +} +const frozenInCtor = new Frozen(); +console.log(frozenInCtor.a, Object.isFrozen(frozenInCtor)); +console.log(message(() => { + frozenInCtor.a = 2; +})); + +class Extended extends Frozen { + b = 2; +} +console.log(message(() => new Extended())); + +class Empty {} +const empty = new Empty(); +console.log(Object.isExtensible(empty), Object.isSealed(empty), Object.isFrozen(empty)); +Object.preventExtensions(empty); +console.log(Object.isExtensible(empty), Object.isSealed(empty), Object.isFrozen(empty)); + +class Totals { + count = 1; + items: string[] = []; +} +const t = new Totals(); +Object.freeze(t); +console.log(message(() => { + t.count += 1; +})); +console.log(message(() => { + t.count++; +})); +console.log(message(() => { + ({ value: t.count } = { value: 5 }); +})); +t.items.push("still mutable"); +console.log(t.count, t.items, Object.isFrozen(t.items)); +console.log(JSON.stringify(Object.getOwnPropertyDescriptor(t, "count"))); + +class Holder { + value: T; + constructor(value: T) { + this.value = value; + } +} +const h = Object.freeze(new Holder("text")); +console.log(h.value, Object.isFrozen(h)); +const h2 = new Holder(4); +Object.seal(h2); +h2.value = 5; +console.log(h2.value, Object.isSealed(h2)); + +class Settings { + mode = "dev"; + level = 1; +} +const settings = new Settings(); +console.log(settings); +Object.defineProperty(settings, "level", { enumerable: false }); +console.log(settings, inspect(settings)); +console.log([settings], { settings }); + +class Pair { + x = 1; + y = 2; +} +const pair = new Pair(); +Object.freeze(pair); +console.log(pair, Object.isFrozen(pair)); +const sealed = new Pair(); +(sealed as any).extra = "added"; +Object.seal(sealed); +console.log(sealed, Object.isSealed(sealed)); + +// JSON.stringify reads the same view: non-enumerable fields are skipped. +console.log(JSON.stringify(cfg), JSON.stringify(settings)); +console.log(JSON.stringify({ settings, pair, sealed })); +console.log(JSON.stringify([fz, t])); diff --git a/tests/corpus/6005-class-instance-integrity-sloppy.cjs b/tests/corpus/6005-class-instance-integrity-sloppy.cjs new file mode 100644 index 000000000..92c959d34 --- /dev/null +++ b/tests/corpus/6005-class-instance-integrity-sloppy.cjs @@ -0,0 +1,40 @@ +class Box { + constructor() { + this.x = 1; + } + bump() { + this.x = 2; + } +} + +const b = new Box(); +Object.freeze(b); +b.x = 5; +console.log(b.x, Object.isFrozen(b)); + +function strictWrite() { + "use strict"; + try { + b.x = 6; + } catch (e) { + console.log(e instanceof TypeError, e.message); + } +} +strictWrite(); + +try { + b.bump(); +} catch (e) { + console.log("class body:", e.message); +} +console.log(b.x); + +const s = new Box(); +Object.seal(s); +s.x = 3; +console.log(s.x, Object.isSealed(s), Object.isFrozen(s)); + +const plain = new Box(); +Object.defineProperty(plain, "x", { writable: false }); +plain.x = 9; +console.log(plain.x, JSON.stringify(Object.getOwnPropertyDescriptor(plain, "x"))); diff --git a/tests/corpus/6055-class-generator-iterables.ts b/tests/corpus/6055-class-generator-iterables.ts new file mode 100644 index 000000000..50641c261 --- /dev/null +++ b/tests/corpus/6055-class-generator-iterables.ts @@ -0,0 +1,86 @@ +// Classes whose [Symbol.iterator] is a generator method iterate the +// generator it returns, called once: in for-of (an early exit closes it), +// array and Math.max spreads, Array.from, new Set, new Map, +// Object.fromEntries and yield*. +class Counter implements Iterable { + n: number; + constructor(n: number) { + this.n = n; + } + *[Symbol.iterator](): Generator { + for (let i = 0; i < this.n; i++) yield i; + } +} +const c = new Counter(3); +for (const v of c) console.log("of", v); +console.log([...c], Array.from(c), Math.max(...c)); +for (const v of c) { + if (v === 1) break; + console.log("early", v); +} + +class Pairs implements Iterable<[string, number]> { + *[Symbol.iterator](): Generator<[string, number]> { + yield ["a", 1]; + yield ["b", 2]; + } +} +class Nums implements Iterable { + *[Symbol.iterator](): Generator { + yield 1; + yield 2; + yield 2; + } +} +const p = new Pairs(); +const n = new Nums(); +console.log(new Set(n).size); +console.log(new Map(p).get("b")); + +console.log(Object.fromEntries(p)); +function* outer(): Generator { + yield 0; + yield* n; +} +console.log([...outer()]); +function* plain(): Generator { + yield 3; + yield 3; + yield 4; +} +console.log(new Set(plain()).size, [...new Set(n)]); + +class Tracked implements Iterable { + log: string[] = []; + *[Symbol.iterator](): Generator { + try { + for (const word of ["a", "b", "c"]) { + this.log.push("yield " + word); + yield word; + } + } finally { + this.log.push("closed"); + } + } +} +const t = new Tracked(); +for (const w of t) { + if (w === "b") break; + t.log.push("saw " + w); +} +console.log(t.log.join(", ")); +class Tree implements Iterable { + value: number; + children: Tree[]; + constructor(value: number, children: Tree[] = []) { + this.value = value; + this.children = children; + } + *[Symbol.iterator](): Generator { + yield this.value; + for (const child of this.children) yield* child; + } +} +class Leafy extends Tree {} +const tree = new Tree(1, [new Tree(2, [new Tree(3)]), new Leafy(4)]); +console.log([...tree], Array.from(new Leafy(9)), Math.min(...tree)); diff --git a/tests/corpus/6056-class-symbol-method-calls.ts b/tests/corpus/6056-class-symbol-method-calls.ts new file mode 100644 index 000000000..2829dc067 --- /dev/null +++ b/tests/corpus/6056-class-symbol-method-calls.ts @@ -0,0 +1,78 @@ +// Well-known symbol methods called through element access, and +// Iterator.from over a class whose [Symbol.iterator] is a generator method: +// both give the class's own generator, so iterator helpers apply to it. +class Range { + lo: number; + hi: number; + constructor(lo: number, hi: number) { + this.lo = lo; + this.hi = hi; + } + *[Symbol.iterator](): Generator { + for (let i = this.lo; i < this.hi; i++) yield i; + } +} +class Labeled extends Range { + label = "l"; +} +class Words { + text: string; + constructor(text: string) { + this.text = text; + } + *[Symbol.iterator](): Generator { + for (const w of this.text.split(" ")) yield w; + } +} +class Money { + cents: number; + constructor(cents: number) { + this.cents = cents; + } + [Symbol.toPrimitive](hint: string): string | number { + return hint === "number" ? this.cents / 100 : `$${(this.cents / 100).toFixed(2)}`; + } +} +class Handle { + name: string; + constructor(name: string) { + this.name = name; + } + [Symbol.dispose](): void { + console.log("dispose", this.name); + } +} + +const r = new Range(1, 6); +const direct = r[Symbol.iterator](); +console.log(direct.next(), direct.next()); +console.log(r[Symbol.iterator]().map((x) => x + 1).toArray()); +const ranges: Range[] = [new Range(0, 3), new Labeled(5, 7)]; +for (const x of ranges) console.log(x[Symbol.iterator]().toArray()); +console.log(r[Symbol.iterator]().filter((x) => x > 3).toArray()); + +const it = Iterator.from(r); +console.log(it.next().value, it.next().value); +console.log([...it]); +console.log(Iterator.from(r).map((x) => x * 10).toArray()); +console.log(Iterator.from(r).filter((x) => x % 2 === 1).toArray()); +console.log(Iterator.from(r).take(2).toArray(), Iterator.from(r).drop(3).toArray()); +console.log(Iterator.from(r).reduce((a, b) => a + b, 0)); +console.log(Iterator.from(r).some((x) => x > 4), Iterator.from(r).every((x) => x > 0)); +console.log(Iterator.from(r).find((x) => x > 2)); +console.log(Iterator.from(new Words("a bb ccc")).map((w) => w.length).toArray()); +Iterator.from(r).forEach((x) => console.log("each", x)); +for (const x of Iterator.from(r).flatMap((x) => [x, -x]).take(4)) console.log(x); +const g = Iterator.from(r); +for (const x of g) { + if (x === 2) break; +} +console.log(g.next()); + +console.log(Iterator.from(r), r[Symbol.iterator](), Iterator.from(r).map((x) => x)); +console.log(Iterator.from(r).flatMap((x) => new Set([x, x + 10])).toArray()); +console.log(r[Symbol.iterator]().flatMap((x) => new Map([[x, String(x)]])).toArray()); + +const m = new Money(1234); +console.log(m[Symbol.toPrimitive]("number"), m[Symbol.toPrimitive]("default")); +new Handle("h")[Symbol.dispose](); diff --git a/tests/corpus/6230-date-parse-legacy.ts b/tests/corpus/6230-date-parse-legacy.ts new file mode 100644 index 000000000..cc71961c0 --- /dev/null +++ b/tests/corpus/6230-date-parse-legacy.ts @@ -0,0 +1,113 @@ +// Date.parse and new Date(string) follow V8's date-string grammar: the ES5 +// date-time format first, then the legacy forms Node accepts (RFC 1123 and +// RFC 2822 HTTP dates, Date#toString output, month names, slashes, AM/PM, +// zone names and numeric offsets). Strings without a zone are local time, +// so every input is parsed under several zones, including DST transitions. +const fixed = [ + "Mon, 01 Jan 2024 00:00:00 GMT", + "Wed, 21 Oct 2015 07:28:00 GMT", + "Sun, 06 Nov 1994 08:49:37 GMT", + "Sunday, 06-Nov-94 08:49:37 GMT", + "Sun Nov 6 08:49:37 1994", + "Tue, 2 Jan 2024 03:04:05 +0100", + "2 Jan 2024 03:04 -0800", + "Mon Jan 01 2024 00:00:00 GMT+0000 (Coordinated Universal Time)", + "Thu Jan 01 1970 00:00:00 GMT-0500 (EST)", + "Jan 5", + "1/2", + "1/2/3", + "2024/01/02", + "2024/01/02 10:00", + "2024-01-02 10:00:00", + "2024-01-02T10:00:00", + "2024-01-02T10:00:00.5", + "2024-01-02T10:00:00.123456Z", + "2024-01-02T10:00:00+0530", + "2024-01-02T24:00:00Z", + "2024-01-02T24:00:01Z", + "2024-01-02T1:00Z", + " 2024-01-01", + "2024-01-01 ", + "Jan 2, 2024", + "January 2, 2024 10:00 PM", + "12:00 am Jan 1 2024", + "12:00 pm Jan 1 2024", + "13:00 pm Jan 1 2024", + "Jan 1 2024 10:00 GMT+5", + "Jan 1 2024 10:00 UTC+0530", + "Jan 1 2024 10:00 +05:30", + "Jan 1 2024 10:00 PST", + "Jan 1 2024 10:00 edt", + "Jan 1 2024 10:00 XYZ", + "Jan 1 2024 10.30", + "10:20:30 Jan 1 2024", + "Jan 1 2024 10:: GMT", + "Jan 1 2024 (a (nested) comment) 10:00", + "Jan 1 2024 10:00)", + "foo 2024-01-01", + "foo2024", + "May 2024", + "2024 May 3", + "3 May 2024", + "May 3 99", + "May 3 49", + "May 3 50", + "Feb 29 2023", + "Feb 30 2024 GMT", + "Dec 31 1969 23:59:59.999 GMT", + "2024-13-01", + "2024-02-31T00:00:00Z", + "+002024-01-01", + "-000000-01-01", + "-000001-01-01T00:00:00Z", + "+275760-09-13T00:00:00.000Z", + "+275760-09-13T00:00:00.001Z", + "Sat, 13 Sep 275760 00:00:00 GMT", + "Sat, 13 Sep 275760 00:00:01 GMT", + "-271821-04-20T00:00:00Z", + "Jan 1 9999999 GMT", + "2024-01-01T10:00:00\u0000junk", + "Jan 1
2024", + "\uFEFFJan 1 2024", + "Jan 1 2024 été", + "0", + "12", + "2024", + "20240", + "", + "garbage", +]; +// A deterministic generator of date-like strings over V8's token classes. +let seed = 20240101; +function next(n: number): number { + seed = (seed * 1103515245 + 12345) % 2147483648; + return seed % n; +} +const pieces = ["Mon", "Tue,", "Wednesday", "Jan", "january", "FEB", "Sept", "dec", "ma", "1", "01", "2", "12", "13", "24", "31", "32", "59", "60", "99", "100", "2024", "1970", "0", "000", "0000", "123456", "-", "+", ":", "::", ".", ",", "/", " ", "\t", "(", ")", "(x)", "GMT", "UTC", "UT", "Z", "T", "t", "PST", "edt", "AM", "pm", "+0100", "-0800", "+01:00", "-8", "GMT+2", "GMT-0530", "999", "1000000", "x", "10:00", "23:59:59", "24:00", "12:30:45.123", "2024-01-15", "2024-01-15T10:00", "T10:00:00Z", "Jan1", "1Jan"]; +const generated: string[] = []; +for (let i = 0; i < 1500; i++) { + let s = ""; + const n = 1 + next(6); + for (let j = 0; j < n; j++) s += pieces[next(pieces.length)]! + (next(3) === 0 ? " " : ""); + generated.push(s); +} +for (const zone of ["UTC", "America/New_York", "Asia/Kolkata", "Australia/Lord_Howe"]) { + process.env.TZ = zone; + console.log("zone", zone); + for (const s of fixed) console.log(JSON.stringify(s), Date.parse(s), new Date(s).getTime()); + let valid = 0; + let sum = 0; + let hash = 0; + for (const s of generated) { + const t = Date.parse(s); + if (Number.isNaN(t)) continue; + valid++; + sum = (sum + (t % 1000003)) % 1000000007; + hash = (hash * 31 + (t % 65521)) % 2147483647; + } + console.log("generated", generated.length, "valid", valid, sum, hash); +} +// An HTTP conditional GET (the fresh package's comparison). +const lastModified = "Sun, 31 Dec 2023 00:00:00 GMT"; +const ifModifiedSince = "Mon, 01 Jan 2024 00:00:00 GMT"; +console.log("fresh", Date.parse(lastModified) <= Date.parse(ifModifiedSince)); diff --git a/tests/corpus/6231-date-local-fields.ts b/tests/corpus/6231-date-local-fields.ts new file mode 100644 index 000000000..6ff18c964 --- /dev/null +++ b/tests/corpus/6231-date-local-fields.ts @@ -0,0 +1,36 @@ +// new Date(year, monthIndex, date, hours, minutes, seconds, ms) builds a +// LOCAL time: fields roll over through MakeDay/MakeTime, years 0-99 mean +// 1900-1999, and the result converts to UTC in the host zone. A time in a +// DST gap reads with the offset before the transition; a time in an +// overlap answers the earlier instant. +const zones = ["UTC", "America/New_York", "Europe/Berlin", "Australia/Lord_Howe", "Asia/Kolkata", "America/St_Johns", "Pacific/Apia"]; +const years = [1800, 1969, 1970, 2000, 2024, 2038, 2100, 0, 49, 99, 100, -1, -271821, 275760]; +for (const zone of zones) { + process.env.TZ = zone; + const out: string[] = []; + for (const y of years) { + for (const m of [0, 2, 9, 11, 12, -1]) { + for (const d of [1, 31, 0]) out.push(`${y}/${m}/${d}=${new Date(y, m, d, 1, 30, 15, 500).getTime()}`); + } + } + console.log(zone, out.join(" ")); + // Every quarter hour across a spring-forward and a fall-back day. + const y = 2024; + const m = zone === "America/New_York" || zone === "Europe/Berlin" ? 2 : 9; + const d = zone === "America/New_York" ? 10 : zone === "Europe/Berlin" ? 31 : 6; + const day: string[] = []; + for (let minute = 0; minute < 24 * 60; minute += 15) { + const spring = new Date(y, m, d, 0, minute); + const fall = new Date(y, m + 7, d, 0, minute); + day.push(`${spring.getTime()}:${spring.getHours()}:${fall.getTime()}:${fall.getMinutes()}`); + } + console.log(zone, day.join(" ")); + const date = new Date(2020, 0, 15, 12, 30, 45, 123); + console.log(zone, date.getTime(), date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds(), date.getTimezoneOffset()); +} +process.env.TZ = "America/New_York"; +console.log(new Date(2020, 0).getTime(), new Date(2020, 0, 1, 0, 0, 0, 0.9).getTime(), new Date(2020.7, 0.9).getTime(), new Date(-0.5, 0).getTime()); +console.log(new Date(1e6, 0).getTime(), new Date(1e6 + 0.5, 0).getTime(), new Date(2020, 1e7).getTime(), new Date(2020, 1e7 + 1).getTime()); +console.log(new Date(NaN, 0).getTime(), new Date(2020, 0, Infinity).getTime(), new Date(2020, 0, 1, -Infinity).getTime()); +console.log(new Date(275760, 8, 13).getTime(), new Date(275760, 8, 12, 20).getTime(), new Date(-271821, 3, 19, 20).getTime()); +console.log(new Date(2021, 6, 4, 9, 5).toISOString(), new Date(99, 11, 31, 23, 59, 59, 999).toISOString()); diff --git a/tests/corpus/6232-date-fields-js.cjs b/tests/corpus/6232-date-fields-js.cjs new file mode 100644 index 000000000..4ae8d94d2 --- /dev/null +++ b/tests/corpus/6232-date-fields-js.cjs @@ -0,0 +1,50 @@ +// JavaScript callers of the Date field constructor and the date-string +// parser: arguments pass through ToNumber in order (valueOf runs, strings +// and booleans convert, a throwing conversion stops the rest), arguments +// past the seventh are evaluated but never converted, and RFC 1123 dates +// parse the way HTTP caching middleware reads them. +process.env.TZ = "Europe/Berlin"; +const log = []; +const arg = (name, value) => ({ valueOf() { log.push(name); return value; } }); +const d = new Date(arg("y", 2024), arg("m", 1), arg("d", 29), arg("h", 13), arg("mi", 5), arg("s", 6), arg("ms", 7), arg("extra", 8)); +console.log(d.getTime(), d.toISOString(), log.join(",")); +console.log(new Date("2024", "0", "15").getTime(), new Date(2024, true, null).getTime(), new Date(2024, undefined).getTime()); +console.log(new Date(2024, 0, "x").getTime(), new Date(99, 0).getFullYear(), new Date(2024, 0, 1, 2, 3).getHours()); +try { + new Date(2024, arg("m", 0), { valueOf() { throw new Error("conversion stopped"); } }, arg("h", 1)); +} catch (e) { + console.log(e.message, log.slice(-1).join(",")); +} +function isFresh(reqHeaders, resHeaders) { + const modifiedSince = reqHeaders["if-modified-since"]; + const lastModified = resHeaders["last-modified"]; + if (!modifiedSince || !lastModified) return false; + const lm = Date.parse(lastModified); + const ms = Date.parse(modifiedSince); + return !(isNaN(lm) || isNaN(ms) || lm > ms); +} +console.log(isFresh({ "if-modified-since": "Mon, 01 Jan 2024 00:00:00 GMT" }, { "last-modified": "Sun, 31 Dec 2023 00:00:00 GMT" })); +console.log(isFresh({ "if-modified-since": "Sun, 31 Dec 2023 00:00:00 GMT" }, { "last-modified": "Mon, 01 Jan 2024 00:00:00 GMT" })); +console.log(isFresh({ "if-modified-since": "not a date" }, { "last-modified": "Mon, 01 Jan 2024 00:00:00 GMT" })); +const stamps = ["Tue, 15 Nov 1994 08:12:31 GMT", "Tuesday, 15-Nov-94 08:12:31 GMT", "Tue Nov 15 08:12:31 1994", "2024-03-31 02:30"]; +for (const s of stamps) console.log(s, Date.parse(s), new Date(s).getTime()); +// Date#toUTCString's IMF-fixdate (cookie Expires, Last-Modified) round-trips +// through the parser. Where toUTCString has no native lowering yet the same +// string comes from fields; `same` checks that string against Node's. +const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; +const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +const pad = (n, w) => String(n).padStart(w, "0"); +function imf(d) { + const y = d.getUTCFullYear(); + const year = y < 0 ? "-" + pad(-y, 4) : pad(y, 4); + return days[d.getUTCDay()] + ", " + pad(d.getUTCDate(), 2) + " " + months[d.getUTCMonth()] + " " + year + " " + + pad(d.getUTCHours(), 2) + ":" + pad(d.getUTCMinutes(), 2) + ":" + pad(d.getUTCSeconds(), 2) + " GMT"; +} +const stampsMs = [0, 1445412480000, 951782400000, -1, -62135596800000, -61851600000000, -59011459200000, 253402300799000, 1709251199999, -2208988800001, 8640000000000000, -8640000000000000]; +for (const ms of stampsMs) { + const d = new Date(ms); + const method = d[["toUTC", "String"].join("")]; + const s = typeof method === "function" ? method.call(d) : imf(d); + console.log(JSON.stringify(s), Date.parse(s), Date.parse(s) === ms - (ms % 1000 + 1000) % 1000, s === imf(d)); +} +console.log(Date.parse("Wed, 21 Oct 2015 07:28:00 GMT"), new Date("Wed, 21 Oct 2015 07:28:00 GMT").getTime()); diff --git a/tests/corpus/6233-object-identity-dynamic.cjs b/tests/corpus/6233-object-identity-dynamic.cjs new file mode 100644 index 000000000..41701d96b --- /dev/null +++ b/tests/corpus/6233-object-identity-dynamic.cjs @@ -0,0 +1,105 @@ +// Plain objects, arrays, functions and the builtin collections inherit +// Object.prototype's constructor and methods: `o.constructor === Object`, +// `o.valueOf === Object.prototype.valueOf` and the isPlainObject and +// deep-equal idioms built on them (fast-deep-equal, is-plain-obj, lodash) +// answer exactly what Node answers. +function equal(a, b) { + if (a === b) return true; + if (a && b && typeof a == "object" && typeof b == "object") { + if (a.constructor !== b.constructor) return false; + var length, i, keys; + if (Array.isArray(a)) { + length = a.length; + if (length != b.length) return false; + for (i = length; i-- !== 0; ) if (!equal(a[i], b[i])) return false; + return true; + } + if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; + if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf(); + if (a.toString !== Object.prototype.toString) return a.toString() === b.toString(); + keys = Object.keys(a); + length = keys.length; + if (length !== Object.keys(b).length) return false; + for (i = length; i-- !== 0; ) if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; + for (i = length; i-- !== 0; ) { + var key = keys[i]; + if (!equal(a[key], b[key])) return false; + } + return true; + } + return a !== a && b !== b; +} +class Point { + constructor(x) { + this.x = x; + } +} +console.log(equal({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }), equal({ a: 1 }, { a: "1" }), equal(new Date(0), new Date(0)), equal(new Date(0), new Date(1))); +console.log(equal(/a/g, /a/g), equal(/a/g, /a/i), equal([1, 2], [1, 2, 3]), equal(new Map(), new Set()), equal(new Point(1), new Point(1)), equal(new Point(1), { x: 1 })); +console.log(equal(JSON.parse('{"x":{"y":[1,2]}}'), { x: { y: [1, 2] } }), equal(NaN, NaN), equal(null, {})); + +const values = { + plain: { a: 1 }, + parsed: JSON.parse('{"x":{"y":1}}').x, + array: [1, 2], + date: new Date(0), + regexp: /a/g, + map: new Map([[1, 2]]), + set: new Set([1]), + fn: function () {}, + arrow: () => 1, + error: new TypeError("x"), + promise: Promise.resolve(1), + nullProto: Object.create(null), + created: Object.create({ inherited: 1 }), + instance: new Point(2), +}; +const ctors = { Object: Object, Array: Array, Date: Date, RegExp: RegExp, Map: Map, Set: Set, Function: Function, TypeError: TypeError, Promise: Promise, Point: Point }; +for (const name of Object.keys(values)) { + const v = values[name]; + const c = v.constructor; + const hits = Object.keys(ctors).filter((k) => c === ctors[k]); + console.log(name, hits.join("|") || "-", v.valueOf === Object.prototype.valueOf, + name === "regexp" ? "-" : v.toString === Object.prototype.toString, + v.hasOwnProperty === Object.prototype.hasOwnProperty, + v.propertyIsEnumerable === Object.prototype.propertyIsEnumerable, + Object.prototype.toString.call(v)); +} +const o = { a: 1 }; +console.log(typeof Object, typeof Date, typeof Map, Object.name, Date.name, o.constructor.name, typeof o.constructor); +console.log(o.toString(), o.valueOf() === o, Object(o) === o, typeof Object(), Object(null).constructor === Object); +const { Object: O, Map: M } = globalThis; +console.log(O === Object, M === Map, globalThis.Object === Object, O.keys({ z: 1 }).join()); +const shadow = { constructor: "mine", valueOf() { return 7; }, toString: 3 }; +console.log(shadow.constructor, shadow.valueOf === Object.prototype.valueOf, shadow.toString, shadow.valueOf()); + +function isPlainObj(value) { + if (typeof value !== "object" || value === null) return false; + const prototype = Object.getPrototypeOf(value); + return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in value) && !(Symbol.iterator in value); +} +function isPlainTagged(value) { + if (value == null || Object.prototype.toString.call(value) != "[object Object]") return false; + const proto = Object.getPrototypeOf(value); + return proto === null || proto.constructor === Object; +} +function isPlainCtor(value) { + return !!value && typeof value === "object" && (value.constructor === Object || value.constructor === undefined); +} +for (const name of ["plain", "parsed", "array", "nullProto", "created", "instance"]) { + console.log(name, isPlainObj(values[name]), isPlainTagged(values[name]), isPlainCtor(values[name])); +} +// Primitives answer their wrapper constructors (dequal compares +// `(ctor = a.constructor) === b.constructor` before anything else). +function ctorOf(v) { + return v.constructor; +} +for (const v of [1, "s", true, 10n]) { + const c = ctorOf(v); + console.log(typeof v, c === Number, c === String, c === Boolean, c === BigInt, c === Object, typeof c); +} +function sameCtor(a, b) { + let c; + return !!(a && b && (c = a.constructor) === b.constructor) && (c === Number || c === String); +} +console.log(sameCtor(1, 2), sameCtor(1, "1"), sameCtor("a", "b"), sameCtor({}, {})); diff --git a/tests/corpus/6234-object-prototype-values.cjs b/tests/corpus/6234-object-prototype-values.cjs new file mode 100644 index 000000000..17dc6ff91 --- /dev/null +++ b/tests/corpus/6234-object-prototype-values.cjs @@ -0,0 +1,25 @@ +// Object.prototype as a value: destructured and aliased methods, the +// prototype's own constructor and members, `{}.valueOf`, and the +// `Object.prototype.toString.call(x)` tag idiom (acorn, ini, qs, +// safe-stable-stringify, rfdc). +const { hasOwnProperty, propertyIsEnumerable } = Object.prototype; +const toStr = Object.prototype.toString; +const has = Object.prototype.hasOwnProperty; +const o = { a: 1 }; +console.log(hasOwnProperty.call(o, "a"), hasOwnProperty.call(o, "b"), has.call(o, "a"), propertyIsEnumerable.call(o, "a")); +console.log(toStr.call([]), toStr.call(null), toStr.call(undefined), toStr.call(new Date(0)), toStr.call(/x/), toStr.call(1), toStr.call("s"), toStr.call(true), toStr.call(o), toStr.call(new Error("e")), toStr.call(new RangeError("r")), toStr.call(() => 1), toStr.call(new Map())); +console.log({}.valueOf === Object.prototype.valueOf, ({}).toString === toStr, typeof Object.prototype.toString, Object.prototype.constructor === Object); +console.log(hasOwnProperty === Object.prototype.hasOwnProperty, o.hasOwnProperty === hasOwnProperty, [].hasOwnProperty === has); +function kind(value) { + switch (toStr.call(value)) { + case "[object Date]": return "date:" + value.getTime(); + case "[object Array]": return "array:" + value.length; + case "[object RegExp]": return "regexp:" + value.source; + case "[object Error]": return "error:" + value.message; + case "[object Object]": return "object:" + Object.keys(value).join(","); + default: return "other"; + } +} +console.log([new Date(5), [1, 2], /ab/, new Error("boom"), { p: 1, q: 2 }, 3].map(kind).join(" ")); +const config = JSON.parse('{"name":"app","db":{"host":"h"}}'); +for (const key in config) if (has.call(config, key)) console.log(key, typeof config[key], toStr.call(config[key])); diff --git a/tests/corpus/6235-object-identity-typed.ts b/tests/corpus/6235-object-identity-typed.ts new file mode 100644 index 000000000..70386d17f --- /dev/null +++ b/tests/corpus/6235-object-identity-typed.ts @@ -0,0 +1,24 @@ +// TypeScript records and class instances read their inherited +// Object.prototype members as values, so the identity tests agree with +// Node: `o.constructor === Object`, `o.valueOf === Object.prototype.valueOf`. +interface Shape { + kind: string; + size: number; +} +class Box { + width = 2; + describe(): string { + return `box ${this.width}`; + } +} +const record: Shape = { kind: "square", size: 3 }; +const box = new Box(); +const empty = {}; +console.log(record.constructor === Object, record.valueOf === Object.prototype.valueOf, record.toString === Object.prototype.toString); +console.log(record.hasOwnProperty === Object.prototype.hasOwnProperty, empty.valueOf === Object.prototype.valueOf, typeof record.constructor); +console.log(box.valueOf === Object.prototype.valueOf, box.toString === Object.prototype.toString, box.describe()); +const parsed: unknown = JSON.parse('{"a":1}'); +if (typeof parsed === "object" && parsed !== null) { + console.log(parsed.constructor === Object, Object.getPrototypeOf(parsed) === Object.prototype); +} +console.log(Object.getPrototypeOf(record) === Object.prototype, record.toString(), record.constructor === empty.constructor); diff --git a/tests/corpus/6236-builtin-function-values.cjs b/tests/corpus/6236-builtin-function-values.cjs new file mode 100644 index 000000000..703495b0f --- /dev/null +++ b/tests/corpus/6236-builtin-function-values.cjs @@ -0,0 +1,55 @@ +// Builtin timer functions stored as values (bcryptjs's `nextTick` feature +// probe, async libraries' scheduler selection): conditional, logical and +// assigned values are real functions — typeof answers "function", identity +// holds across spellings, and calls schedule with Node's argument contract +// and ordering. +var nextTick = + typeof setImmediate === "function" + ? setImmediate + : typeof queueMicrotask === "function" + ? queueMicrotask + : setTimeout; +const pick = process.argv.length > 100; +const later = pick ? setImmediate : process.nextTick; +const timeout = pick ? setImmediate : setTimeout; +const micro = pick || queueMicrotask; +let assigned = setTimeout; +assigned = pick ? assigned : setImmediate; +const { setInterval: every, clearInterval: stop } = globalThis; +console.log(typeof nextTick, typeof later, typeof timeout, typeof micro, typeof assigned, typeof every); +console.log(nextTick === setImmediate, later === process.nextTick, timeout === setTimeout, micro === queueMicrotask, assigned === globalThis.setImmediate, every === setInterval, stop === clearInterval); +console.log(nextTick.name, later.name, timeout.name, setTimeout.length, queueMicrotask.length); + +const order = []; +nextTick(function () { order.push("immediate:" + arguments.length); }); +later((a, b) => order.push("tick:" + a + b), 1, 2); +// Microtask and nextTick queues order differently in CommonJS and ESM +// entry evaluation, so the microtask reports through its own flag. +let microRan = false; +micro(() => { microRan = true; }); +timeout((x) => order.push("timeout:" + x), 60, "x"); +assigned(() => order.push("assigned")); +const cancelled = timeout(() => order.push("cancelled"), 1); +(pick ? clearImmediate : clearTimeout)(cancelled); +// The interval counts its runs apart from `order`, and the report waits until +// it has stopped, so slow machines print the same line. +let count = 0; +const handle = every(() => { + count++; + if (count === 2) { + stop(handle); + timeout(() => console.log(order.join(" "), "interval:" + count, "micro:" + microRan), 120); + } +}, 20); + +for (const bad of [undefined, 1, "fn"]) { + try { + later(bad); + } catch (e) { + console.log("rejected", typeof bad, e.code, e instanceof TypeError); + } +} +const log = pick ? console.error : console.log; +log("log", typeof log); +const mx = pick ? Math.min : Math.max; +console.log(mx(1, 3, 2), typeof mx); diff --git a/tests/corpus/6400-dyn-error-stack-property-js.js b/tests/corpus/6400-dyn-error-stack-property-js.js new file mode 100644 index 000000000..f0f666103 --- /dev/null +++ b/tests/corpus/6400-dyn-error-stack-property-js.js @@ -0,0 +1,148 @@ +// In JavaScript sources, errors have an own `stack` like Node's: a +// non-enumerable accessor that every error shares. The first read formats +// "Name: message" from the error's name and message at that point, and +// later reads answer the same string. Assigning a value replaces it, +// `delete` removes it, Object.keys, spread, Object.assign and +// JSON.stringify leave it out, `in` and Object.hasOwn find it, objects +// that inherit from an error read the error's stack. Typed and dynamic +// reads answer the same string, and a stack read through `any` captures +// frames. Only the first line is printed: frame lines differ between +// runtimes. + +class ParseError extends Error { + constructor(message) { + super(message); + this.name = "ParseError"; + } +} + +class PlainSubclass extends Error {} + +function dyn(value) { + return value; +} + +function firstLine(value) { + return typeof value === "string" ? JSON.stringify(value.split("\n")[0]) : `(${typeof value} ${String(value)})`; +} + +function describe(label, error) { + console.log( + label, + "|", + typeof error.stack, + firstLine(error.stack), + "|", + Object.keys(error).join(","), + JSON.stringify(error), + "|", + "stack" in error, + Object.hasOwn(error, "stack"), + error.propertyIsEnumerable("stack"), + ); +} + +try { + throw new RangeError("thrown"); +} catch (error) { + describe("caught", error); +} + +const unknownError = new TypeError("bad thing"); +describe("unknown", unknownError); +describe("plain", dyn(new Error("plain"))); +describe("no message", dyn(new SyntaxError())); +describe("subclass", dyn(new ParseError("line 3"))); +describe("plain subclass", dyn(new PlainSubclass("inherited name"))); + +const coded = dyn(new Error("with code")); +coded.code = "E_CODE"; +describe("own property", coded); + +const descriptor = dyn(Object.getOwnPropertyDescriptor(dyn(new Error("a")), "stack")); +const other = dyn(Object.getOwnPropertyDescriptor(dyn(new TypeError("b")), "stack")); +console.log("descriptor", Object.keys(descriptor).join(","), descriptor.enumerable, descriptor.configurable); +console.log("shared accessors", typeof descriptor.get, descriptor.get === other.get, descriptor.set === other.set); +console.log("foreign receiver", descriptor.get.call({}), firstLine(descriptor.get.call(dyn(new TypeError("b"))))); +const subclassDescriptor = dyn(Object.getOwnPropertyDescriptor(dyn(new PlainSubclass("s")), "stack")); +console.log("subclass descriptor", Object.keys(subclassDescriptor).join(","), subclassDescriptor.get === descriptor.get); + +const renamed = dyn(new Error("before")); +renamed.message = "after"; +renamed.name = "Renamed"; +console.log("first read", firstLine(renamed.stack)); +renamed.message = "later"; +console.log("later read", firstLine(renamed.stack)); +const subclassRenamed = dyn(new PlainSubclass("before")); +subclassRenamed.message = "after"; +console.log("subclass first read", firstLine(subclassRenamed.stack)); + +const replaced = dyn(new Error("replaced")); +replaced.stack = "custom stack\n at nowhere"; +describe("assigned", replaced); +console.log("assigned descriptor", Object.keys(Object.getOwnPropertyDescriptor(replaced, "stack")).join(",")); +const subclassReplaced = dyn(new ParseError("replaced")); +subclassReplaced.stack = "subclass stack"; +console.log("subclass assigned", subclassReplaced.stack); +const typedReplaced = new ParseError("typed write"); +typedReplaced.stack = "typed subclass stack"; +console.log("typed subclass assigned", typedReplaced.stack, dyn(typedReplaced).stack); +const subclassNumbered = dyn(new PlainSubclass("numbered")); +subclassNumbered.stack = 7; +console.log("subclass number", subclassNumbered.stack, typeof subclassNumbered.stack); +subclassNumbered.stack = "string again"; +console.log("subclass string again", subclassNumbered.stack); +const numbered = dyn(new Error("numbered")); +numbered.stack = 5; +console.log("number", numbered.stack, typeof numbered.stack); +const removed = dyn(new Error("removed")); +delete removed.stack; +console.log("deleted", removed.stack, "stack" in removed, Object.hasOwn(removed, "stack")); + +const spreadSource = dyn(new Error("spread")); +console.log("spread", Object.keys({ ...spreadSource }).length, Object.keys(Object.assign({}, spreadSource)).length); +console.log("inherited", firstLine(Object.create(dyn(new RangeError("prototype"))).stack)); + +const typed = new Error("typed"); +const view = dyn(typed); +console.log("typed and dynamic", view.stack === typed.stack, firstLine(typed.stack)); +const typedSubclass = new ParseError("typed subclass"); +console.log("typed subclass", firstLine(dyn(typedSubclass).stack), dyn(typedSubclass).stack === typedSubclass.stack); +const assignedThroughView = new Error("typed"); +dyn(assignedThroughView).stack = "through the view"; +console.log("assigned through view", assignedThroughView.stack); + +function inner() { + return dyn(new Error("deep")); +} +function outer() { + return inner(); +} +const deep = outer().stack; +console.log("frames", deep.split("\n").length > 1, deep.includes("at inner"), deep.includes("at outer")); +function makeSubclass() { + return dyn(new PlainSubclass("deep")); +} +console.log("subclass frames", makeSubclass().stack.includes("at makeSubclass")); + +const formatted = dyn(new Error("formatted")); +console.log("formatted before clone", firstLine(formatted.stack)); +console.log("clone keeps a formatted stack", firstLine(dyn(structuredClone(formatted)).stack)); + +class GrumpyError extends Error { + /** @returns {string} */ + get name() { + throw new Error("no name today"); + } +} +try { + console.log("throwing name getter", firstLine(dyn(new GrumpyError("grumpy")).stack)); +} catch (error) { + console.log("throwing name getter threw", error.message); +} + +function readKey(target, key) { + return target[key]; +} +const computed = readKey(inner(), ["sta", "ck"].join("")); +console.log("computed key", firstLine(computed), computed.includes("at inner")); diff --git a/tests/corpus/6401-dyn-error-own-property-names.ts b/tests/corpus/6401-dyn-error-own-property-names.ts new file mode 100644 index 000000000..1020cf014 --- /dev/null +++ b/tests/corpus/6401-dyn-error-own-property-names.ts @@ -0,0 +1,44 @@ +// The own property names of errors held as `any` or `unknown` match +// Node's: Object.getOwnPropertyNames, Reflect.ownKeys and +// Object.getOwnPropertyDescriptors list `stack`, `message` and the +// properties the error has, in order, and nothing else. + +class PlainSubclass extends Error {} + +function dyn(value: unknown): any { + return value; +} + +function names(label: string, error: any): void { + console.log( + label, + "|", + Object.getOwnPropertyNames(error).join(","), + "|", + Reflect.ownKeys(error).map(String).join(","), + "|", + Object.keys(Object.getOwnPropertyDescriptors(error)).join(","), + ); +} + +names("error", dyn(new Error("plain"))); +names("type error", dyn(new TypeError("bad thing"))); +names("no message", dyn(new RangeError())); +names("cause", dyn(new Error("outer", { cause: new Error("inner") }))); +names("subclass", dyn(new PlainSubclass("sub"))); + +const unknownError: unknown = new SyntaxError("unexpected"); +names("unknown", unknownError); + +const coded = dyn(new Error("with code")); +coded.code = "E_CODE"; +coded.detail = 1; +names("own properties", coded); + +try { + throw new URIError("thrown"); +} catch (error) { + names("caught", error); +} + +console.log("marker", Object.getOwnPropertyNames(dyn(new Error("x"))).some((name) => name.startsWith("%"))); diff --git a/tests/corpus/6402-dyn-error-own-property-names-js.js b/tests/corpus/6402-dyn-error-own-property-names-js.js new file mode 100644 index 000000000..6f6ca7fb7 --- /dev/null +++ b/tests/corpus/6402-dyn-error-own-property-names-js.js @@ -0,0 +1,44 @@ +// In JavaScript sources, the own property names of errors match +// Node's: Object.getOwnPropertyNames, Reflect.ownKeys and +// Object.getOwnPropertyDescriptors list `stack`, `message` and the +// properties the error has, in order, and nothing else. + +class PlainSubclass extends Error {} + +function dyn(value) { + return value; +} + +function names(label, error) { + console.log( + label, + "|", + Object.getOwnPropertyNames(error).join(","), + "|", + Reflect.ownKeys(error).map(String).join(","), + "|", + Object.keys(Object.getOwnPropertyDescriptors(error)).join(","), + ); +} + +names("error", dyn(new Error("plain"))); +names("type error", dyn(new TypeError("bad thing"))); +names("no message", dyn(new RangeError())); +names("cause", dyn(new Error("outer", { cause: new Error("inner") }))); +names("subclass", dyn(new PlainSubclass("sub"))); + +const unknownError = new SyntaxError("unexpected"); +names("unknown", unknownError); + +const coded = dyn(new Error("with code")); +coded.code = "E_CODE"; +coded.detail = 1; +names("own properties", coded); + +try { + throw new URIError("thrown"); +} catch (error) { + names("caught", error); +} + +console.log("marker", Object.getOwnPropertyNames(dyn(new Error("x"))).some((name) => name.startsWith("%"))); diff --git a/tests/corpus/6403-error-own-name-order.ts b/tests/corpus/6403-error-own-name-order.ts new file mode 100644 index 000000000..d1a5200c4 --- /dev/null +++ b/tests/corpus/6403-error-own-name-order.ts @@ -0,0 +1,108 @@ +// An error's own `name` is listed where it was created, as in Node: +// after `message` and `cause`, after the fields created before it, and +// before the fields created after it. A `name` field takes its place among +// the fields, `this.name = ...` in a constructor follows that class's +// fields, and a name assigned after construction follows every other own +// property. Object.keys, JSON.stringify and Object.getOwnPropertyNames +// list the same order, for typed values and values held as `any`. + +class AssignedName extends Error { + extra = 1; + constructor(message: string) { + super(message); + this.name = "AssignedName"; + } +} + +class NameField extends Error { + override name = "NameField"; + extra = 1; +} + +class LaterNameField extends Error { + extra = 1; + override name = "LaterNameField"; + last = 2; +} + +class CodeThenName extends Error { + code: string; + constructor(message: string) { + super(message); + this.code = "E_CODE"; + this.name = "CodeThenName"; + } +} + +class NamedBase extends Error { + constructor(message: string) { + super(message); + this.name = "NamedBase"; + } +} + +class ChildOfNamed extends NamedBase { + child = true; +} + +class FieldBase extends Error { + base = "base"; +} + +class NamedChild extends FieldBase { + child = "child"; + constructor(message: string) { + super(message); + this.name = "NamedChild"; + } +} + +class QuotedName extends Error { + override "name" = "QuotedName"; + "quoted-extra" = 1; +} + +class Unnamed extends Error { + extra = 1; +} + +function dyn(value: unknown): any { + return value; +} + +function show(label: string, error: Error): void { + console.log( + label, + "|", + Object.keys(dyn(error)).join(","), + "|", + JSON.stringify(dyn(error)), + "|", + Object.getOwnPropertyNames(dyn(error)).join(","), + ); +} + +show("assigned", new AssignedName("a")); +show("field", new NameField("b")); +show("later field", new LaterNameField("c")); +show("code then name", new CodeThenName("d")); +show("child of named", new ChildOfNamed("e")); +show("named child", new NamedChild("f")); +show("quoted name field", new QuotedName("q")); + +const late = new Unnamed("g"); +late.name = "Late"; +show("assigned later", late); + +const builtin = new TypeError("h", { cause: 1 }); +builtin.name = "Renamed"; +show("builtin", builtin); + +const viaAny = dyn(new RangeError("i")); +viaAny.code = "E_RANGE"; +viaAny.name = "Custom"; +viaAny.after = true; +console.log("through any", Object.keys(viaAny).join(","), JSON.stringify(viaAny)); + +const typed = new AssignedName("j"); +console.log("typed keys", Object.keys(typed).join(","), JSON.stringify({ error: typed })); diff --git a/tests/corpus/6404-error-own-name-order-js.js b/tests/corpus/6404-error-own-name-order-js.js new file mode 100644 index 000000000..b5f9b4d32 --- /dev/null +++ b/tests/corpus/6404-error-own-name-order-js.js @@ -0,0 +1,94 @@ +// In JavaScript sources, an error's own `name` is listed where it was +// created, as in Node: among the properties a constructor assigns, in +// assignment order, after the fields declared before it, and after every +// other own property when it is assigned after construction. + +class AssignedBetween extends Error { + constructor(message) { + super(message); + this.code = "E_CODE"; + this.name = "AssignedBetween"; + this.detail = { at: 1 }; + } +} + +class AssignedFirst extends Error { + constructor(message) { + super(message); + this.name = "AssignedFirst"; + this.status = 500; + } +} + +class DeclaredField extends Error { + kind = "declared"; + constructor(message) { + super(message); + this.name = "DeclaredField"; + this.after = true; + } +} + +class NameField extends Error { + name = "NameField"; + extra = 1; +} + +class Base extends Error { + constructor(message) { + super(message); + this.base = 1; + } +} + +class Derived extends Base { + constructor(message) { + super(message); + this.name = "Derived"; + this.derived = 2; + } +} + +class ElementName extends Error { + constructor(message) { + super(message); + this.first = 1; + this["name"] = "ElementName"; + this.second = 2; + } +} + +class Unnamed extends Error { + constructor(message) { + super(message); + this.extra = 1; + } +} + +function show(label, error) { + console.log( + label, + "|", + Object.keys(error).join(","), + "|", + JSON.stringify(error), + "|", + Object.getOwnPropertyNames(error).join(","), + ); +} + +show("between", new AssignedBetween("a")); +show("first", new AssignedFirst("b")); +show("declared field", new DeclaredField("c")); +show("name field", new NameField("d")); +show("derived", new Derived("e")); +show("element name", new ElementName("g")); + +const late = new Unnamed("f"); +late.name = "Late"; +show("assigned later", late); + +const builtin = new Error("g", { cause: "reason" }); +builtin.name = "Renamed"; +builtin.code = "E_AFTER"; +show("builtin", builtin); diff --git a/tests/corpus/6420-process-send-presence.ts b/tests/corpus/6420-process-send-presence.ts new file mode 100644 index 000000000..59773ab44 --- /dev/null +++ b/tests/corpus/6420-process-send-presence.ts @@ -0,0 +1,24 @@ +// process.send exists only with an IPC channel: presence checks in every +// truthiness position, typeof, and guarded calls. +const viaIpc = Boolean(process.send); +console.log("Boolean", viaIpc); +console.log("not", !process.send, !!process.disconnect); +console.log("typeof", typeof process.send, typeof process.disconnect); +console.log("ternary", process.send ? "ipc" : "stdout"); +if (process.send) { + process.send({ kind: "log", line: "never" }); +} else { + console.log("if: no channel"); +} +let polls = 0; +while (process.send && polls < 3) polls++; +console.log("while", polls); +process.send && process.send({ unused: true }); +const sink = !process.send || !process.disconnect ? "console" : "ipc"; +console.log("or", sink); +console.log("optional", process.send?.({ value: 1 })); +try { + process.send!({ value: 2 }); +} catch (e) { + console.log("direct", (e as Error).name, (e as Error).message); +} diff --git a/tests/corpus/6421-process-send-presence-fork/main.ts b/tests/corpus/6421-process-send-presence-fork/main.ts new file mode 100644 index 000000000..11a634672 --- /dev/null +++ b/tests/corpus/6421-process-send-presence-fork/main.ts @@ -0,0 +1,13 @@ +// process.send and process.disconnect exist in a forked child (an IPC +// channel) and not in the parent; the child's presence checks pick the +// channel and its guarded send reaches the parent. +import { fork } from "node:child_process"; + +console.log("parent", Boolean(process.send), typeof process.send, typeof process.disconnect); +const child = fork(new URL("./worker.ts", import.meta.url), [], { + stdio: ["ignore", "inherit", "inherit", "ipc"], +}); +child.on("message", (message: { sink: string; send: string; disconnect: string }) => { + console.log("child says", message.sink, message.send, message.disconnect); +}); +child.on("exit", (code) => console.log("child exit", code)); diff --git a/tests/corpus/6421-process-send-presence-fork/worker.ts b/tests/corpus/6421-process-send-presence-fork/worker.ts new file mode 100644 index 000000000..81e8288a7 --- /dev/null +++ b/tests/corpus/6421-process-send-presence-fork/worker.ts @@ -0,0 +1,6 @@ +const sink = process.send ? "ipc" : "stdout"; +if (process.send) { + process.send({ sink, send: typeof process.send, disconnect: typeof process.disconnect }); +} +if (!process.send) console.log("unreachable"); +process.disconnect(); diff --git a/tests/corpus/6422-process-exitcode-read.ts b/tests/corpus/6422-process-exitcode-read.ts new file mode 100644 index 000000000..c320b4a0d --- /dev/null +++ b/tests/corpus/6422-process-exitcode-read.ts @@ -0,0 +1,22 @@ +// @exit: 7 +// process.exitCode: unset reads undefined, assignments (including 0) read back, +// and process.exit(n) assigns the code before 'exit' listeners run. +console.log("initial", process.exitCode); +process.on("exit", (code) => { + console.log("exit listener", code, process.exitCode); +}); +process.exitCode = 0; +console.log("after 0", process.exitCode); +process.exitCode = 4; +const seen = process.exitCode; +console.log("after 4", seen, typeof process.exitCode); +process.exitCode = process.exitCode ?? 9; +console.log("kept", process.exitCode); +try { + process.exitCode = 1.5; +} catch (e) { + console.log("rejected", (e as Error).message, process.exitCode); +} +setTimeout(() => { + process.exit(7); +}, 1); diff --git a/tests/corpus/6423-process-exitcode-default.ts b/tests/corpus/6423-process-exitcode-default.ts new file mode 100644 index 000000000..5dbc72294 --- /dev/null +++ b/tests/corpus/6423-process-exitcode-default.ts @@ -0,0 +1,10 @@ +// @exit: 2 +// The exit status follows process.exitCode when the program ends normally. +function settle(failed: boolean): void { + if (failed) process.exitCode = process.exitCode ?? 2; +} +console.log("start", process.exitCode === undefined); +settle(true); +settle(true); +console.log("code", process.exitCode); +process.on("exit", () => console.log("exiting with", process.exitCode)); diff --git a/tests/corpus/6424-process-exit-keeps-exitcode.ts b/tests/corpus/6424-process-exit-keeps-exitcode.ts new file mode 100644 index 000000000..e5a0b86d2 --- /dev/null +++ b/tests/corpus/6424-process-exit-keeps-exitcode.ts @@ -0,0 +1,5 @@ +// @exit: 5 +// process.exit() without an argument keeps process.exitCode. +process.exitCode = 5; +process.on("exit", (code) => console.log("exit", code, process.exitCode)); +process.exit(); diff --git a/tests/corpus/6425-stream-hook-wider-chunk.ts b/tests/corpus/6425-stream-hook-wider-chunk.ts new file mode 100644 index 000000000..be46b18be --- /dev/null +++ b/tests/corpus/6425-stream-hook-wider-chunk.ts @@ -0,0 +1,46 @@ +// Stream hooks may declare a supertype of what Node passes: chunks typed +// Buffer | string still arrive as Buffers. +import { Transform, Writable } from "node:stream"; + +class LineSink extends Writable { + private rest = ""; + readonly lines: string[] = []; + _write(chunk: Buffer | string, encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log("chunk", typeof chunk, Buffer.isBuffer(chunk), encoding); + const parts = (this.rest + chunk.toString()).split("\n"); + this.rest = parts.pop() ?? ""; + this.lines.push(...parts); + cb(); + } + _final(cb: (err?: Error | null) => void): void { + if (this.rest !== "") this.lines.push(this.rest); + cb(); + } +} + +class Upper extends Transform { + _transform(chunk: string | Buffer, _enc: string, cb: (err?: Error | null, data?: string) => void): void { + cb(null, chunk.toString().toUpperCase()); + } +} + +class Failing extends Writable { + _write(chunk: Uint8Array | string, _enc: BufferEncoding, cb: (err?: Error | null) => void): void { + cb(new Error(`refused ${chunk.toString().length} bytes`)); + } +} + +const sink = new LineSink(); +sink.write("a\nb"); +sink.write(Buffer.from("c\nd")); +sink.end("e"); +sink.on("finish", () => console.log("lines", JSON.stringify(sink.lines))); + +const up = new Upper(); +up.on("data", (d: Buffer) => console.log("upper", d.toString())); +up.write("hello "); +up.end("world"); + +const bad = new Failing(); +bad.on("error", (e) => console.log("error", e.message)); +bad.write("xyz"); diff --git a/tests/corpus/6426-promise-race-void-entry.ts b/tests/corpus/6426-promise-race-void-entry.ts new file mode 100644 index 000000000..b33f3b664 --- /dev/null +++ b/tests/corpus/6426-promise-race-void-entry.ts @@ -0,0 +1,31 @@ +// Promise.race over Promise and Promise: the void +// entry fulfills the race with undefined. +interface Msg { id: string } +function delay(ms: number, value: T): Promise { + return new Promise((resolve) => setTimeout(() => resolve(value), ms)); +} +function poll(ms: number, id: string | null): Promise { + return delay(ms, id === null ? undefined : { id }); +} +function exited(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} +async function main(): Promise { + const a = await Promise.race([poll(5, "m1"), exited(50)]); + console.log("message first:", a ? a.id : "none"); + const b = await Promise.race([poll(50, "m2"), exited(5)]); + console.log("exit first:", b === undefined, b ? b.id : "none"); + const c = await Promise.race([exited(5), poll(1, null)]); + console.log("empty poll first:", c); + const settled = Promise.resolve(); + const d = await Promise.race([settled, poll(0, "m3")]); + console.log("settled void entry wins:", d); + const n = await Promise.race([delay(20, 42), exited(1)]); + console.log("number or void:", n); + try { + await Promise.race([poll(20, "late"), Promise.reject(new Error("boom"))]); + } catch (e) { + console.log("rejected:", (e as Error).message); + } +} +void main(); diff --git a/tests/corpus/6427-http-request-options-type.ts b/tests/corpus/6427-http-request-options-type.ts new file mode 100644 index 000000000..17cb2d855 --- /dev/null +++ b/tests/corpus/6427-http-request-options-type.ts @@ -0,0 +1,10 @@ +// http.RequestOptions as a declared type: a checked-dynamic option record. +import type http from "node:http"; +const socket = process.env.NO_SUCH_SOCKET_VAR; +const base: http.RequestOptions = socket ? { socketPath: socket } : { host: "127.0.0.1", port: 8080 }; +const full: http.RequestOptions = { ...base, path: "/x", method: "POST", headers: { "x-a": "1" } }; +console.log(JSON.stringify(base), JSON.stringify(full)); +function describe(o: http.RequestOptions): string { + return `${String(o.host)}:${String(o.port)}${String(o.path ?? "/")}`; +} +console.log(describe(full), describe({ host: "h" })); diff --git a/tests/corpus/6428-promisify-callback-functions.ts b/tests/corpus/6428-promisify-callback-functions.ts new file mode 100644 index 000000000..b899dd292 --- /dev/null +++ b/tests/corpus/6428-promisify-callback-functions.ts @@ -0,0 +1,80 @@ +// util.promisify over typed callback-only functions (bound methods, one with +// an optional callback, a function declaration, an inline arrow, a function +// that returns a value): the error argument rejects, the value fulfills, the +// return value is ignored, a synchronous throw rejects, and settlement is +// async. +import { promisify } from "node:util"; + +class Connection { + private closed = false; + private readonly name: string; + constructor(name: string) { + this.name = name; + } + close(callback: (error?: Error) => void): void { + setTimeout(() => { + if (this.closed) { + callback(new Error(`${this.name} already closed`)); + return; + } + this.closed = true; + callback(); + }, 1); + } + flush(callback?: (error?: Error) => void): void { + setImmediate(() => callback?.(this.closed ? new Error("flushed after close") : undefined)); + } + stats(callback: (error: Error | null, open: number) => void): void { + setImmediate(() => (this.closed ? callback(new RangeError("closed"), 0) : callback(null, 3))); + } +} + +function boom(callback: (error: Error | null) => void): void { + if (callback !== undefined) throw new TypeError("synchronous failure"); +} + +let pending = 0; +function size(callback: (error: Error | null, size: number) => void): number { + setImmediate(() => callback(null, 4)); + return ++pending; +} + +async function main(): Promise { + const c = new Connection("db"); + const stats = promisify(c.stats.bind(c)); + console.log("open", await stats()); + await promisify(c.flush.bind(c))(); + console.log("flushed"); + const closeAsync = promisify(c.close.bind(c)); + await closeAsync(); + console.log("closed once"); + try { + await closeAsync(); + } catch (e) { + console.log("second close:", (e as Error).message); + } + try { + await stats(); + } catch (e) { + console.log("stats:", (e as Error).name, (e as Error).message); + } + await promisify(c.flush.bind(c))().catch((e) => console.log("flush:", (e as Error).message)); + try { + await promisify(boom)(); + } catch (e) { + console.log("sync throw:", (e as Error).name, (e as Error).message); + } + const order: string[] = []; + const tick = promisify((done: (error?: Error) => void) => { + order.push("called"); + done(); + }); + const p = tick().then(() => order.push("settled")); + order.push("returned"); + await p; + console.log(order.join(", ")); + await promisify(new Connection("inline").close.bind(new Connection("inline")))(); + console.log("inline done"); + console.log("size", await promisify(size)(), pending); +} +void main(); diff --git a/tests/corpus/6429-esm-js-dirname.mjs b/tests/corpus/6429-esm-js-dirname.mjs new file mode 100644 index 000000000..2ecc4088a --- /dev/null +++ b/tests/corpus/6429-esm-js-dirname.mjs @@ -0,0 +1,15 @@ +// A JavaScript ES module has no __dirname or __filename (Node throws a +// ReferenceError where they are read); import.meta carries the location. +// TypeScript files keep the CommonJS globals as the source path. +try { + console.log(__dirname); +} catch (error) { + console.log("no __dirname:", error instanceof Error); +} +console.log(import.meta.filename.endsWith("6429-esm-js-dirname.mjs")); +console.log(typeof __dirname, typeof __filename); +try { + console.log(__filename.length); +} catch (error) { + console.log(error.name, error.message); +} diff --git a/tests/corpus/6445-rest-function-fixed-slot.ts b/tests/corpus/6445-rest-function-fixed-slot.ts new file mode 100644 index 000000000..e78e83836 --- /dev/null +++ b/tests/corpus/6445-rest-function-fixed-slot.ts @@ -0,0 +1,49 @@ +// A rest-parameter function in a slot with fixed parameters: the slot's +// arguments arrive in the rest array, as in a JavaScript call. +function log(...messages: string[]): void { + console.log(`log(${messages.length}):`, messages.join(" | ")); +} +function tagged(tag: string, ...values: number[]): number { + console.log(tag, values.join(",")); + return values.length; +} + +class Line { + private readonly sink: (m: string) => void; + constructor(sink: (m: string) => void) { + this.sink = sink; + } + emit(m: string): void { + this.sink(m); + } +} + +new Line(log).emit("one"); +const pair: (a: string, b: string) => void = log; +pair("x", "y"); +const none: () => void = log; +none(); +const count: (tag: string, a: number, b: number) => number = tagged; +console.log("count", count("t", 1, 2)); +["a", "b"].forEach((m) => new Line(log).emit(m)); + +// Nullable and optional slots: the value takes the slot's function arm. +class OptionalLine { + private readonly sink: ((m: string) => void) | null; + constructor(sink: ((m: string) => void) | null) { + this.sink = sink; + } + emit(m: string): void { + if (this.sink) this.sink(m); + else console.log("no sink:", m); + } +} +new OptionalLine(log).emit("nullable"); +new OptionalLine(null).emit("dropped"); +function relay(m: string, sink?: (m: string) => void): void { + sink?.(m); +} +relay("optional", log); +relay("absent"); +const maybe: ((a: string, b: string) => void) | undefined = log; +maybe?.("p", "q"); diff --git a/tests/corpus/6446-rest-function-nested-declaration.ts b/tests/corpus/6446-rest-function-nested-declaration.ts new file mode 100644 index 000000000..1b2d7aa17 --- /dev/null +++ b/tests/corpus/6446-rest-function-nested-declaration.ts @@ -0,0 +1,26 @@ +// Nested rest-parameter function declarations passed to a fixed slot. +class Line { + private readonly sink: (m: string) => void; + constructor(sink: (m: string) => void) { + this.sink = sink; + } + emit(m: string): void { + this.sink(m); + } +} +interface Logger { + line: () => Line; + log: (...messages: string[]) => void; +} +function makeLogger(prefix: string): Logger { + return { + line: () => new Line(log), + log, + }; + function log(...messages: string[]) { + console.log(prefix, messages.join(" ")); + } +} +const logger = makeLogger(">"); +logger.log("a", "b"); +logger.line().emit("c"); diff --git a/tests/corpus/6450-builtin-base-class-values.ts b/tests/corpus/6450-builtin-base-class-values.ts new file mode 100644 index 000000000..0dafa5d82 --- /dev/null +++ b/tests/corpus/6450-builtin-base-class-values.ts @@ -0,0 +1,63 @@ +// Classes extending Node builtin bases (Writable, Transform, EventEmitter) +// used as values: through a variable, a generic factory, an object table and +// an array, with instanceof against the class and its base. +import { EventEmitter } from "node:events"; +import { Readable, Transform, Writable } from "node:stream"; + +class Sink extends Writable { + readonly lines: string[] = []; + private readonly tag: string; + constructor(tag: string) { + super(); + this.tag = tag; + } + _write(chunk: Buffer, _enc: BufferEncoding, cb: (err?: Error | null) => void): void { + this.lines.push(this.tag + chunk.toString()); + cb(); + } +} + +class Upper extends Transform { + _transform(chunk: Buffer, _enc: BufferEncoding, cb: (err?: Error | null, data?: string) => void): void { + cb(null, chunk.toString().toUpperCase()); + } +} + +class Ticker extends EventEmitter { + count = 0; + tick(): void { + this.count++; + this.emit("tick", this.count); + } +} + +class Plain extends EventEmitter {} + +function build(ctor: new () => T): T { + return new ctor(); +} + +const SinkClass = Sink; +const s = new SinkClass("> "); +s.write("a"); +s.end("b", () => console.log("sink", JSON.stringify(s.lines), s instanceof Sink, s instanceof Writable)); + +const t = build(Ticker); +t.on("tick", (n: number) => console.log("tick", n)); +t.tick(); +t.tick(); +console.log("ticker", t instanceof Ticker, t instanceof EventEmitter, t.count); + +const p = build(Plain); +p.on("x", () => console.log("plain x")); +p.emit("x"); + +const registry = { upper: Upper }; +const u = new registry.upper(); +u.on("data", (d: Buffer) => console.log("upper", d.toString())); +u.end("hello"); + +const kinds = [Ticker, Ticker]; +console.log("names", kinds.map((k) => k.name).join(","), typeof Sink, Sink.name); +const r = Readable.from(["x"]); +console.log("readable", r instanceof Readable); diff --git a/tests/corpus/6450-promise-race-never-entry.ts b/tests/corpus/6450-promise-race-never-entry.ts new file mode 100644 index 000000000..412c5f817 --- /dev/null +++ b/tests/corpus/6450-promise-race-never-entry.ts @@ -0,0 +1,22 @@ +// Promise.race with a Promise deadline entry under a typed result: +// the never entry can only reject, so it races with any result type. A +// read that settles first wins; a deadline that fires first rejects. +class Deadline extends Error {} +async function readWithDeadline(read: () => Promise<{ done: boolean; value?: Uint8Array }>, ms: number): Promise<{ done: boolean; value?: Uint8Array }> { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + read(), + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Deadline()), ms); + }), + ]); + } catch (error) { + if (error instanceof Deadline) return { done: true }; + throw error; + } finally { + if (timer !== undefined) clearTimeout(timer); + } +} +readWithDeadline(async () => ({ done: false, value: new Uint8Array([1, 2]) }), 1000).then((r) => console.log(r.done, r.value?.length)); +readWithDeadline(() => new Promise((res) => setTimeout(() => res({ done: false }), 200)), 20).then((r) => console.log("deadline", r.done)); diff --git a/tests/corpus/6451-array-length-fill-spread.ts b/tests/corpus/6451-array-length-fill-spread.ts new file mode 100644 index 000000000..03b80c300 --- /dev/null +++ b/tests/corpus/6451-array-length-fill-spread.ts @@ -0,0 +1,15 @@ +// Array(n).fill(v): the checker types the length-constructed array as +// any[], but fill gives every element v's type, so the array (and an +// array literal spreading it) holds strings. +function expand(head: string, tail: string): number[] | null { + const headParts = head ? head.split(":") : []; + const tailParts = tail ? tail.split(":") : []; + const missing = 8 - headParts.length - tailParts.length; + if (missing < 0) return null; + const all = [...headParts, ...Array(missing).fill("0"), ...tailParts]; + if (all.length !== 8) return null; + return all.map((part) => Number.parseInt(part || "0", 16)); +} +console.log(JSON.stringify(expand("2001:db8", "1")), JSON.stringify(expand("fe80", ""))); +const pad: string[] = Array(3).fill("-"); +console.log(pad.join(""), [..."ab", ...new Array(2).fill("c")].join("")); diff --git a/tests/corpus/6451-builtin-base-class-expressions.cjs b/tests/corpus/6451-builtin-base-class-expressions.cjs new file mode 100644 index 000000000..6d9241b97 --- /dev/null +++ b/tests/corpus/6451-builtin-base-class-expressions.cjs @@ -0,0 +1,46 @@ +// Class expressions extending Node builtin bases (a bundler's output shape). +"use strict"; +const { Readable, Transform } = require("stream"); +const { EventEmitter } = require("events"); + +const Wrap = class extends Readable { + constructor(parts) { + super(); + this.parts = parts; + } + _read() { + const next = this.parts.shift(); + this.push(next === undefined ? null : next); + } +}; +const Bus = class extends EventEmitter {}; +const Lower = class extends Transform { + _transform(chunk, _enc, cb) { + cb(null, chunk.toString().toLowerCase()); + } +}; + +const r = new Wrap(["a", "b"]); +const chunks = []; +r.on("data", (c) => chunks.push(c.toString())); +r.on("end", () => console.log("wrap", chunks.join("+"), r instanceof Readable)); +const bus = new Bus(); +bus.on("ping", (n) => console.log("ping", n)); +bus.emit("ping", 1); +const l = new Lower(); +l.on("data", (d) => console.log("lower", d.toString())); +l.end("HeLLo"); +console.log(Wrap.name, Bus.name, typeof Lower); +{ + // A class declared in a block is collected like a class expression. + class Once extends Readable { + _read() { + this.push(null); + } + } + function onReadable() { + console.log("block", stream.read() === null); + } + const stream = new Once(); + stream.once("readable", onReadable); +} diff --git a/tests/corpus/6470-promise-reject-async-return.ts b/tests/corpus/6470-promise-reject-async-return.ts new file mode 100644 index 000000000..4d9a62eab --- /dev/null +++ b/tests/corpus/6470-promise-reject-async-return.ts @@ -0,0 +1,17 @@ +// `return Promise.reject(err)` in an async function returns a promise of the +// function's own result type, which the async function adopts. +type Ctx = { id: string; endpoint: string; meta?: { [key: string]: string } }; +async function context(endpoint: string | undefined): Promise { + if (!endpoint || endpoint.length === 0) { + return Promise.reject(new Error("invalid endpoint")); + } + return Promise.resolve({ id: "c1", endpoint }); +} +function sync(flag: boolean): Promise { + if (flag) return Promise.reject(new TypeError("nope")); + return Promise.resolve(3); +} +context("http://x").then((c) => console.log(c.id, c.endpoint)); +context(undefined).catch((e) => console.log("rejected", String(e))); +sync(false).then((n) => console.log(n)); +sync(true).catch((e) => console.log(e instanceof TypeError, String(e))); diff --git a/tests/corpus/6471-union-literal-results-into-record.ts b/tests/corpus/6471-union-literal-results-into-record.ts new file mode 100644 index 000000000..0f6c158d1 --- /dev/null +++ b/tests/corpus/6471-union-literal-results-into-record.ts @@ -0,0 +1,37 @@ +// A function returning differently shaped object literals (the checker +// normalizes omitted members to `?: undefined`) flows into a declared record +// result: each literal shape converts on its own. +class Tracer { + trace(_name: string, fn: () => T): T { + return fn(); + } +} +const tracer = new Tracer(); +type CacheResult = { downloaded: boolean; size?: number; cacheType?: string; dir?: string }; +async function download(n: number, dir: string | undefined): Promise { + const pending = tracer.trace("download", async () => { + if (n === 0) { + return { downloaded: false, dir }; + } + if (n < 0) { + return { downloaded: false }; + } + return { downloaded: true, size: n, cacheType: "full", dir }; + }); + return pending.finally(() => { + console.log("done", n); + }); +} +type Details = { obtain: string; url?: string; kind?: string }; +async function details(flag: number): Promise
{ + return tracer.trace("details", async () => { + if (flag === 0) return { obtain: "skipped" }; + return { obtain: "found", url: "u", kind: "k" }; + }); +} +async function main() { + console.log(await download(0, "/d"), await download(5, undefined), await download(-1, "z")); + const d0 = await details(0); + console.log(d0, "url" in d0, JSON.stringify(await details(1))); +} +main(); diff --git a/tests/corpus/6472-asserted-member-read.ts b/tests/corpus/6472-asserted-member-read.ts new file mode 100644 index 000000000..498a9bb57 --- /dev/null +++ b/tests/corpus/6472-asserted-member-read.ts @@ -0,0 +1,16 @@ +// `(source as { repoName: string }).repoName` on a union of records: arms +// that declare the member answer it, the others answer undefined, as Node +// reads a missing property. +type GithubSource = { type: string; repoId: number; repoName: string; ref: string }; +type GitlabSource = { type: string; projectId: number; ref: string }; +type BitbucketSource = { type: string; repoName: string; owner: string; ref: string }; +type Source = GithubSource | GitlabSource | BitbucketSource; +function label(source: Source): string { + return (source as { repoName: string }).repoName || "???"; +} +const sources: Source[] = [ + { type: "github", repoId: 1, repoName: "r1", ref: "main" }, + { type: "gitlab", projectId: 2, ref: "dev" }, + { type: "bitbucket", repoName: "", owner: "o", ref: "x" }, +]; +console.log(sources.map(label)); diff --git a/tests/corpus/6473-unknown-spread-into-index-record.ts b/tests/corpus/6473-unknown-spread-into-index-record.ts new file mode 100644 index 000000000..c5cd4f6ad --- /dev/null +++ b/tests/corpus/6473-unknown-spread-into-index-record.ts @@ -0,0 +1,21 @@ +// Spreading an unknown value into an index-signature record copies its own +// keys in order and spreads nothing for null and undefined. +function build(extra: unknown, attempt: number): Record { + return { + "content-type": "application/json", + ...(extra as Record), + "x-attempt": String(attempt), + }; +} +function headersOf(params: { headers?: T }, attempt: number): Record { + return { + ...params.headers, + "x-attempt": String(attempt), + }; +} +const parsed: unknown = JSON.parse('{"authorization":"t","content-type":"text/plain"}'); +console.log(build(parsed, 1)); +console.log(headersOf({ headers: parsed }, 3), headersOf({}, 4)); +const nothing: unknown = JSON.parse("null"); +const five: unknown = JSON.parse("5"); +console.log(headersOf({ headers: nothing }, 5), headersOf({ headers: five }, 6)); diff --git a/tests/corpus/6474-index-record-spread-literals.ts b/tests/corpus/6474-index-record-spread-literals.ts new file mode 100644 index 000000000..59e1603a9 --- /dev/null +++ b/tests/corpus/6474-index-record-spread-literals.ts @@ -0,0 +1,59 @@ +// An object literal that spreads an index-signature record holds every key +// the spread copied, in insertion order, though the checker types it by its +// explicit members only. A conditional spread that does not apply adds no +// key. +function bucket(n: number | undefined): string { + return n === undefined ? "unknown" : n < 100 ? "small" : "large"; +} +function emit(name: string, tags: Record): void { + console.log(name, JSON.stringify(tags), Object.keys(tags).length); +} +function measure(base: Record, count: number | undefined, ok: boolean) { + const metricTags = { + ...base, + fileCountBucket: bucket(count), + status: ok ? "succeeded" : "failed", + }; + emit("files", metricTags); + console.log(metricTags.status.toUpperCase(), metricTags.fileCountBucket); + return metricTags; +} +const base = { plan: "pro", region: "iad1", status: "pending" }; +const m = measure(base, 42, true); +console.log(Object.entries(m)); +function walker(enable: boolean, metricTags: Record = {}) { + const tags = { + plan: "unknown", + ...metricTags, + largeCliDplPerformance: String(enable), + walkerFallback: "false", + }; + tags.walkerFallback = "true"; + console.log(tags, tags.plan); +} +walker(true, { plan: "hobby", extra: "x" }); +walker(false); +function counts(tags: Record, n: number) { + const t = { ...tags, attempts: n, retried: n > 1 }; + console.log(t, t.attempts + 1, t.retried); +} +counts({ a: "1" }, 2); +function depthTags(deep: boolean): { depth: number | undefined; kind: "shallow" | "deep"; useDeep: boolean } { + return { depth: deep ? undefined : 1, kind: deep ? "deep" : "shallow", useDeep: deep }; +} +function attempt(base: Record, n: number, deep: boolean, bytes: number | undefined) { + const tags = { + ...base, + git_attempt: n, + repo: "r", + ...(bytes !== undefined && { packBytes: bytes }), + ...depthTags(deep), + }; + console.log(JSON.stringify(tags), Object.keys(tags).join(","), tags.git_attempt, tags.useDeep); +} +attempt({ plan: "pro" }, 1, false, 10); +attempt({ plan: "pro", repo: "x" }, 2, true, undefined); +function addTags(tags: Record): void { + console.log(Object.entries(tags).map(([k, v]) => `${k}=${String(v)}`).join(" ")); +} +addTags({ ...{ plan: "pro", team: "t" } as Record, count: 3, ok: true }); diff --git a/tests/corpus/6475-dynamic-spread-literals.ts b/tests/corpus/6475-dynamic-spread-literals.ts new file mode 100644 index 000000000..eaf76598e --- /dev/null +++ b/tests/corpus/6475-dynamic-spread-literals.ts @@ -0,0 +1,63 @@ +// Object literals spreading index-signature records whose members are not +// all scalars, or whose spread values are unknown, are checked-dynamic +// objects built like JavaScript's: each spread copies its own keys in +// order, conditional spreads add nothing when they do not apply, and later +// keyed writes land on the same object. +function flatten(obj: unknown, prefix: string): Record { + const out: Record = {}; + if (typeof obj === "object" && obj !== null) { + for (const [k, v] of Object.entries(obj)) out[`${prefix}.${k}`] = v; + } + return out; +} +function summary(status: string, ms: number, bytes: number | undefined, err: string | undefined) { + const milestones = { connect: 3, done: 9 }; + const resultTags = { + "git.fetch.status": status, + "git.fetch.durationMs": ms, + ...flatten(milestones, "git.fetch"), + ...(bytes !== undefined && { "git.fetch.packBytes": bytes }), + ...(err !== undefined && { "git.fetch.error": err }), + }; + console.log(JSON.stringify({ event: "git.fetch.summary", version: "v1", ...resultTags })); +} +summary("ok", 12, 100, undefined); +summary("failed", 3, undefined, "boom"); +type SendOptions = { method: string; headersTimeout?: number; signal?: AbortSignal }; +function send(url: string, options: SendOptions): string { + return `${url} ${options.method} ${options.headersTimeout ?? "-"} ${options.signal ? options.signal.aborted : "none"}`; +} +function callWith(timeout: number | undefined, attemptTimeout: number | undefined): string { + return send("u", { + headersTimeout: timeout, + method: "GET", + ...(attemptTimeout && { signal: AbortSignal.timeout(attemptTimeout) }), + }); +} +console.log(callWith(5, 1000)); +console.log(callWith(undefined, undefined)); +console.log(callWith(5, 0)); +function report(tags: string[], from: string, logs: [string, string][]) { + const ddTags = { + "errClass.from": from, + "errClass.tagLabels": Array.from(new Set(tags)).sort(), + "errClass.tags": "", + ...tags.reduce((obj: Record, tag: string) => { + obj[`errClass.tags.${tag}`] = true; + return obj; + }, {}), + "errClass.logLinesSent": logs.length, + "errClass.lastRule": "none", + }; + if (tags.length > 1) { + ddTags["errClass.lastRule"] = "rule-2"; + } + for (const tag of ["late"]) { + // @ts-expect-error -- keys outside the literal's declared ones + ddTags[`errClass.tags.${tag}`] = true; + } + ddTags["errClass.tags"] = tags.join(","); + console.log(JSON.stringify(ddTags), ddTags["errClass.logLinesSent"] + 1); +} +report(["b", "a"], "build", [["stdout", "x"]]); +report([], "init", []); diff --git a/tests/corpus/6480-boolean-callbacks.ts b/tests/corpus/6480-boolean-callbacks.ts new file mode 100644 index 000000000..b9ac4b4ab --- /dev/null +++ b/tests/corpus/6480-boolean-callbacks.ts @@ -0,0 +1,34 @@ +// The ambient Boolean as an array callback beyond filter: every, some, +// find, findIndex and map call it with the element first, so each answers +// ToBoolean of the element (later arguments are ignored). filter(Boolean) +// also covers elements the checker types `any`. +const flags = [true, true, false]; +console.log(flags.every(Boolean), flags.some(Boolean), flags.slice(0, 2).every(Boolean)); +const counts = [0, 3, 0, 7]; +console.log(counts.find(Boolean), counts.findIndex(Boolean), counts.map(Boolean).join(",")); +const names = ["", "a", "", "b"]; +console.log(names.some(Boolean), names.every(Boolean), names.findIndex(Boolean)); +const maybe: (string | undefined)[] = [undefined, "x", undefined]; +console.log(maybe.some(Boolean), maybe.find(Boolean), maybe.map(Boolean).join(",")); + +// Elements the checker types `any` keep their runtime kind; filter(Boolean) +// answers their runtime truthiness. +function untyped(value: unknown): any { + return value; +} +const configFiles = [untyped("app.json"), untyped(""), untyped(undefined), untyped("site.json")].filter( + Boolean, +) as string[]; +console.log(configFiles.length, configFiles.join(",")); + +async function check(n: number): Promise { + return n % 2 === 1; +} + +async function main(): Promise { + const all = (await Promise.all([1, 3, 5].map(async (n) => check(n)))).every(Boolean); + const any = (await Promise.all([2, 4].map(async (n) => check(n)))).some(Boolean); + console.log(all, any); +} + +main(); diff --git a/tests/corpus/6480-catch-typeof-untyped-throws.ts b/tests/corpus/6480-catch-typeof-untyped-throws.ts new file mode 100644 index 000000000..98122f851 --- /dev/null +++ b/tests/corpus/6480-catch-typeof-untyped-throws.ts @@ -0,0 +1,40 @@ +// Values thrown from `unknown` storage read back through typeof tests on +// the catch binding, like the same values thrown with static types. +const thrown: unknown[] = [5, 0, -1.5, "text", "", true, false, null, undefined, { kind: "box" }]; +function describe(value: unknown): string { + try { + throw value; + } catch (e) { + const same = e === value; + if (typeof e === "number") return `${same} number ${e + 1}`; + if (typeof e === "string") return `${same} string ${e.length}`; + if (typeof e === "boolean") return `${same} boolean ${!e}`; + if (typeof e === "object") return `${same} object ${e === null ? "null" : "value"}`; + return `${same} ${typeof e}`; + } +} +for (const value of thrown) console.log(describe(value)); +function rethrow(value: unknown): void { + try { + throw value; + } catch (e) { + if (typeof e !== "string") throw e; + console.log("kept string", e); + } +} +for (const value of ["s", 7, false]) { + try { + rethrow(value); + } catch (e) { + console.log("rethrown", typeof e, String(e)); + } +} +let count = 0; +for (const value of thrown) { + try { + throw value; + } catch (e) { + if (typeof e === "number" && e > 0) count += e; + } +} +console.log(count); diff --git a/tests/corpus/6480-generic-callback-extended-results.ts b/tests/corpus/6480-generic-callback-extended-results.ts new file mode 100644 index 000000000..0d75a1f53 --- /dev/null +++ b/tests/corpus/6480-generic-callback-extended-results.ts @@ -0,0 +1,41 @@ +// A callback whose returns extend the elements it was given keeps the +// added members once its result type names them (an annotation on the +// callback or on the type argument), as Node keeps them on the objects. +type Plain = { name: string; n: number }; +type Item = Plain & { extra?: boolean }; +const plain: Plain[] = [ + { name: "a", n: 1 }, + { name: "b", n: 2 }, +]; + +function run(fn: () => T): T { + return fn(); +} +async function runAsync(fn: () => Promise): Promise { + return fn(); +} + +const annotated = run((): Item[] => { + if (plain.length === 0) return plain; + return plain.map((x) => ({ ...x, extra: x.n > 1 })); +}); +console.log(JSON.stringify(annotated)); + +const viaTypeArgument = run(() => { + if (plain.length > 5) return plain; + return plain.map((x) => ({ ...x, extra: true })); +}); +console.log(JSON.stringify(viaTypeArgument), viaTypeArgument[0]!.extra); + +runAsync(async (): Promise => { + if (plain.length === 0) return plain; + return plain.map((x) => ({ ...x, extra: false })); +}).then((items) => console.log(items)); + +// Callbacks whose results the checker types by their own returns keep +// every member already. +const mapped = plain.map((x) => (x.n > 1 ? x : { ...x, extra: true })); +const asItems: Item[] = mapped; +console.log(JSON.stringify(asItems)); +const summed = plain.reduce((acc, x) => ({ ...acc, total: acc.total + x.n }), { total: 0, label: "sum" }); +console.log(JSON.stringify(summed)); diff --git a/tests/corpus/6481-enum-runtime-keys.ts b/tests/corpus/6481-enum-runtime-keys.ts new file mode 100644 index 000000000..183d509b2 --- /dev/null +++ b/tests/corpus/6481-enum-runtime-keys.ts @@ -0,0 +1,29 @@ +// @transform-types +// Enum member lookup through a runtime string key (`E[name as keyof +// typeof E]`): each member name answers its constant — string, numeric and +// renamed members alike — and a name no member carries reads undefined. +enum Color { + red = "red", + "dark-blue" = "dark-blue", + green = "light-green", +} + +enum Level { + low, + mid = 5, + high, +} + +function color(name: string | undefined): Color | undefined { + return name ? Color[name as keyof typeof Color] : undefined; +} + +for (const name of ["red", "dark-blue", "green", "light-green", "", undefined]) { + console.log(JSON.stringify(name), JSON.stringify(color(name))); +} + +const levelName = process.argv.length > 99 ? "unused" : "high"; +const level: Level | undefined = Level[levelName as keyof typeof Level]; +console.log(level, level === Level.high); +const missing: Level | undefined = Level[("med" + "ium") as keyof typeof Level]; +console.log(missing === undefined); diff --git a/tests/corpus/6482-promise-all-tuples.ts b/tests/corpus/6482-promise-all-tuples.ts new file mode 100644 index 000000000..3c5e38e29 --- /dev/null +++ b/tests/corpus/6482-promise-all-tuples.ts @@ -0,0 +1,97 @@ +// Promise.all over an array literal of DIFFERENT promise types (the +// checker's tuple overload): destructuring with holes and nested +// patterns, void entries, plain values, `any` entries that may or may not +// hold a promise or a thenable, tuples of more than ten entries, entry +// identity, and rejection — +// the first rejection in settlement order wins, later ones count as +// handled. +interface Version { + major: number; + range: string; +} + +class Scan { + tool: string; + schema: number | undefined; + constructor(tool: string, schema?: number) { + this.tool = tool; + this.schema = schema; + } +} + +const shared: Version = { major: 22, range: "22.x" }; +const scanned = new Scan("make", 9); + +async function version(): Promise { + return shared; +} +async function scan(): Promise { + return scanned; +} +async function patch(): Promise { + console.log("patched"); +} +async function creds(ok: boolean): Promise<{ key: string } | undefined> { + return ok ? { key: "k" } : undefined; +} +function delay(ms: number, value: T, fail?: string): Promise { + return new Promise((resolve, reject) => + setTimeout(() => (fail ? reject(new Error(fail)) : resolve(value)), ms), + ); +} + +// JavaScript-style results the checker types `any`: one holds a promise, +// one a plain value. +function untypedCount(): any { + return 41; +} +function untypedRecord(): any { + return Promise.resolve({ label: "from a promise" }); +} + +// A thenable that is not a native promise: Promise.all adopts its +// settlement, calling `then` one microtask after the call. +class Deferred { + then(resolve: (value: string) => void): void { + console.log("then called"); + resolve("adopted"); + } +} +function untypedThenable(): any { + return new Deferred(); +} + +async function main(): Promise { + const [runtime, { tool: kind, schema: level }] = await Promise.all([version(), scan()]); + console.log(runtime.major, kind, level, runtime === shared); + + const first = scan(); + const second = patch(); + const third = creds(true); + const [found, , c] = await Promise.all([first, second, third]); + console.log(found === scanned, c?.key); + + const both = await Promise.all([version(), 5, "x", creds(false)]); + console.log(both[0].range, both[1] + 1, both[2], both[3]); + + const [count, { label }, plain] = await Promise.all([untypedCount(), untypedRecord(), "p"]); + console.log(count + 1, label, plain); + + const pending = Promise.all([untypedThenable(), version()]); + console.log("after the call"); + const [adopted, latest] = await pending; + console.log(adopted, latest.major); + + const many = await Promise.all([version(), 1, 2, 3, 4, 5, 6, 7, 8, 9, "ten", scan()]); + console.log(many[0].major, many[2], many[9], many[10], many[11].tool); + + try { + await Promise.all([delay(20, 1, "slow failure"), delay(5, "s", "fast failure"), delay(1, true)]); + } catch (e) { + console.log("rejected:", (e as Error).message); + } + await delay(40, 0); + console.log("done"); +} + +main(); diff --git a/tests/corpus/6483-nullable-regex-receivers.ts b/tests/corpus/6483-nullable-regex-receivers.ts new file mode 100644 index 000000000..d6b190a30 --- /dev/null +++ b/tests/corpus/6483-nullable-regex-receivers.ts @@ -0,0 +1,23 @@ +// Regex-argument string methods on a receiver the checker types +// `string | undefined` (an unchecked index read, ts-ignored): a string +// takes the regex lowering, undefined throws Node's TypeError. +const lines = "100 /srv/node1/a.txt\n2048 /srv/node22/b/c.bin"; +const pairs = lines.split("\n").map((line) => { + const [count, file] = line.trim().split(/\s+/) as [string | undefined, string | undefined]; + // @ts-ignore possibly undefined + return `${count.replace(/0+$/, "")}:${file.replace(/^\/srv\/node\d+\//, "")}`; +}); +console.log(pairs); + +function parts(text: string): (string | undefined)[] { + return text.split(",").map((s) => (s === "" ? undefined : s)); +} + +for (const value of parts("a-b-c,")) { + try { + // @ts-ignore possibly undefined + console.log(value.replaceAll(/-/g, "+"), value.split(/-/).length, value.search(/c/)); + } catch (e) { + console.log(e instanceof TypeError, (e as Error).message); + } +} diff --git a/tests/corpus/comma-null-union-return.cjs b/tests/corpus/comma-null-union-return.cjs new file mode 100644 index 000000000..52cef027e --- /dev/null +++ b/tests/corpus/comma-null-union-return.cjs @@ -0,0 +1,15 @@ +// `return log(), null` from a function that otherwise returns a list: the +// comma expression's effects run, then null takes the result's +// representation. +"use strict"; +async function promote(list) { + if (list.length === 0) return console.log("no static mounts"), null; + return list.map((x) => x * 2); +} +function first(list) { + if (list.length === 0) return console.log("empty"), undefined; + return list[0]; +} +promote([]).then((v) => console.log("promoted", v)); +promote([1, 2]).then((v) => console.log("promoted", v)); +console.log(first([]), first([7])); diff --git a/tests/corpus/local-class-detached-method.ts b/tests/corpus/local-class-detached-method.ts new file mode 100644 index 000000000..528539c48 --- /dev/null +++ b/tests/corpus/local-class-detached-method.ts @@ -0,0 +1,16 @@ +// A method that never reads `this`, taken off an instance of a class +// declared inside a function: the detached method runs without a receiver. +function helper(): string { + return "helper"; +} +function make() { + return class Local { + tag = "t"; + plain(): string { + return helper(); + } + }; +} +const C = make(); +const detached = new C().plain; +console.log(detached()); diff --git a/tests/corpus/object-literal-accessors-dyn.ts b/tests/corpus/object-literal-accessors-dyn.ts new file mode 100644 index 000000000..3b7dc95e4 --- /dev/null +++ b/tests/corpus/object-literal-accessors-dyn.ts @@ -0,0 +1,31 @@ +// Object literals with get/set accessors held as `any` or `unknown` keep +// their accessors, as in Node: a read calls the getter, a write calls the +// setter, Object.keys, Object.entries and JSON.stringify list the accessor +// in declaration order and call the getter, and the property descriptor +// has a get and set function. Writes to data properties reach the +// original object. + +function dyn(value: unknown): any { + return value; +} + +let count = 0; +const lit = { + a: 1, + get g(): number { count++; return 42; }, + set g(v: number) { console.log("set", v); }, + b: 2, + get only(): string { return "read-only"; }, +}; +const d = dyn(lit); +console.log("read", d.g, d.only, count); +console.log("keys", Object.keys(d).join(","), JSON.stringify(d), count); +console.log("entries", Object.entries(d).map((e) => e.join("=")).join(";")); +console.log("in", "g" in d, Object.hasOwn(d, "g")); +d.g = 7; +d.a = 5; +console.log("write", lit.a, d.a); +const desc = Object.getOwnPropertyDescriptor(d, "g")!; +console.log("descriptor", typeof desc.get, typeof desc.set, desc.enumerable, desc.configurable); +const u: unknown = lit; +console.log("unknown", (u as any).g, JSON.stringify(u)); diff --git a/tests/corpus/required-module-truthiness.cjs b/tests/corpus/required-module-truthiness.cjs new file mode 100644 index 000000000..626d1cd4d --- /dev/null +++ b/tests/corpus/required-module-truthiness.cjs @@ -0,0 +1,10 @@ +// A required builtin module object in a boolean context is truthy, as any +// object: readable-stream's `process.env.READABLE_STREAM === "disable" && +// Stream` and `Stream || exports` guards. +"use strict"; +var Stream = require("stream"); +var os = require("os"); +console.log(Stream ? "stream" : "none", !!os, !os); +console.log(process.env.SCRIPTC_UNSET_FLAG === "disable" && Stream ? "disabled" : "default"); +var fallback = Stream || null; +console.log(fallback === Stream); diff --git a/tests/corpus/strict-arguments-parameters.cjs b/tests/corpus/strict-arguments-parameters.cjs new file mode 100644 index 000000000..463cb316a --- /dev/null +++ b/tests/corpus/strict-arguments-parameters.cjs @@ -0,0 +1,15 @@ +// A CommonJS file under "use strict": a function with named parameters +// reads `arguments`, which in strict code never aliases those parameters. +"use strict"; +function wrap(fn, label) { + label = label + "!"; + return [label, arguments.length, arguments[1], typeof arguments[0]].join(" "); +} +function sum(a, b) { + a = 100; + let total = 0; + for (let i = 0; i < arguments.length; i++) total += arguments[i]; + return total; +} +console.log(wrap(() => 1, "tag")); +console.log(sum(1, 2, 3)); diff --git a/tests/coverage-fixtures/divergences.ts b/tests/coverage-fixtures/divergences.ts index 10b0a12e7..83c3ede49 100644 --- a/tests/coverage-fixtures/divergences.ts +++ b/tests/coverage-fixtures/divergences.ts @@ -47,7 +47,7 @@ function sizeOf(s: Shape): number { } console.log(sizeOf(new Circle())); -// The native date parser takes ISO 8601 with an explicit offset. +// Date strings parse with V8's grammar natively, so none of these diverge. console.log(Date.parse("Jan 2, 2024"), new Date("2024/01/02").getTime(), Date.parse("2024-01-02T03:04:05Z")); const stamps = ["2024-03-04T05:06:07Z", "March 4, 2024"]; for (const stamp of stamps) console.log(Date.parse(stamp)); diff --git a/tests/diagnostics/boot-path-node-surface-fences.ts b/tests/diagnostics/boot-path-node-surface-fences.ts new file mode 100644 index 000000000..146a32aa1 --- /dev/null +++ b/tests/diagnostics/boot-path-node-surface-fences.ts @@ -0,0 +1,23 @@ +// The boot-path Node surface keeps its boundaries: process.send answers +// presence checks but stays fenced as a stored value, a stream hook must +// declare Node's argument type or a supertype of it, and util.promisify +// over a function value needs an Error-typed callback error. +import { promisify } from "node:util"; +import { Writable } from "node:stream"; + +const send = process.send; +console.log(typeof send); + +class Counter extends Writable { + _write(chunk: number, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log(chunk); + cb(); + } +} +new Counter().end("x"); + +function legacy(cb: (code: number, value: string) => void): void { + cb(0, "v"); +} +const legacyAsync = promisify(legacy); +legacyAsync().then((v) => console.log(v)); diff --git a/tests/diagnostics/builtin-base-class-value-arguments.ts b/tests/diagnostics/builtin-base-class-value-arguments.ts new file mode 100644 index 000000000..db5be873b --- /dev/null +++ b/tests/diagnostics/builtin-base-class-value-arguments.ts @@ -0,0 +1,14 @@ +// A class without a constructor that extends a stream class inherits the +// runtime constructor, which takes no arguments here. Passing arguments +// through the class's value reports at the first argument, as direct +// construction does. +import { Transform } from "node:stream"; + +class Upper extends Transform {} +const registry = { upper: Upper }; +const upper = new registry.upper({ objectMode: true }); +upper.destroy(); + +const Kind = Upper; +const other = new Kind({ highWaterMark: 4 }); +other.destroy(); diff --git a/tests/diagnostics/builtin-constructor-members.ts b/tests/diagnostics/builtin-constructor-members.ts new file mode 100644 index 000000000..294fe397b --- /dev/null +++ b/tests/diagnostics/builtin-constructor-members.ts @@ -0,0 +1,14 @@ +// Builtin constructors have interned function values, but those values model +// only name, length and constructor. Every other static member or prototype +// read as a value reports SC2020 at the read, whether it is spelled directly, +// through globalThis or through an alias; it never reads undefined. +const mapPrototype = Map.prototype; +const datePrototype = Date.prototype; +const regExpPrototype = RegExp.prototype; +const promisePrototype = Promise.prototype; +const weakSetPrototype = WeakSet.prototype; +const frozen = Object.isFrozen; +const setPrototype = globalThis.Set.prototype; +const D = Date; +const aliasPrototype = D.prototype; +// End with a nonblank context line so diagnostic snapshots carry no trailing whitespace. diff --git a/tests/diagnostics/class-field-accessor-descriptor.ts b/tests/diagnostics/class-field-accessor-descriptor.ts new file mode 100644 index 000000000..66078f6c2 --- /dev/null +++ b/tests/diagnostics/class-field-accessor-descriptor.ts @@ -0,0 +1,14 @@ +// Object.defineProperty can change a declared class field's value and +// attributes, but not turn it into an accessor: a literal get or set +// descriptor is refused at compile time, naming the field. +class Value { + x = 1; +} + +const value = new Value(); +Object.defineProperty(value, "x", { + get() { + return 2; + }, +}); +console.log(value.x); diff --git a/tests/diagnostics/inferred-callback-drops-conditional.ts b/tests/diagnostics/inferred-callback-drops-conditional.ts new file mode 100644 index 000000000..f124beed9 --- /dev/null +++ b/tests/diagnostics/inferred-callback-drops-conditional.ts @@ -0,0 +1,12 @@ +type Plain = { name: string; n: number }; +type Item = Plain & { extra?: boolean }; +const plain: Plain[] = [{ name: "a", n: 1 }]; +function run(fn: () => T): T { + return fn(); +} +// The checker unifies both branches into Plain[]; the extended objects keep +// `extra` in Node, so converting them into Plain would drop it silently. +const items: Item[] = run(() => + plain.length === 0 ? plain : plain.map((x) => ({ ...x, extra: true })), +); +console.log(items.length); diff --git a/tests/diagnostics/inferred-callback-drops-satisfies.ts b/tests/diagnostics/inferred-callback-drops-satisfies.ts new file mode 100644 index 000000000..ad58237b8 --- /dev/null +++ b/tests/diagnostics/inferred-callback-drops-satisfies.ts @@ -0,0 +1,13 @@ +type Plain = { name: string; n: number }; +type Item = Plain & { extra?: boolean }; +const plain: Plain[] = [{ name: "a", n: 1 }]; +function run(fn: () => T): T { + return fn(); +} +// `satisfies` checks the return against Plain[] without changing it; the +// extended objects keep `extra` in Node. +const items: Item[] = run(() => { + if (plain.length === 0) return plain; + return plain.map((x) => ({ ...x, extra: true })) satisfies Plain[]; +}); +console.log(items.length); diff --git a/tests/diagnostics/inferred-callback-drops.ts b/tests/diagnostics/inferred-callback-drops.ts new file mode 100644 index 000000000..0d4808b6a --- /dev/null +++ b/tests/diagnostics/inferred-callback-drops.ts @@ -0,0 +1,13 @@ +type Plain = { name: string; n: number }; +type Item = Plain & { extra?: boolean }; +const plain: Plain[] = [{ name: "a", n: 1 }]; +function run(fn: () => T): T { + return fn(); +} +// The checker unifies both returns into Plain[]; the extended objects keep +// `extra` in Node, so converting them into Plain would drop it silently. +const items: Item[] = run(() => { + if (plain.length === 0) return plain; + return plain.map((x) => ({ ...x, extra: true })); +}); +console.log(items.length); diff --git a/tests/diagnostics/maybe-promise-fences.ts b/tests/diagnostics/maybe-promise-fences.ts index efe541c9e..1dcccaf08 100644 --- a/tests/diagnostics/maybe-promise-fences.ts +++ b/tests/diagnostics/maybe-promise-fences.ts @@ -30,8 +30,3 @@ console.log(typeof widened); // `instanceof Promise` on a value that is never a promise. const record = { v: 1 }; console.log(record instanceof Promise); - -// A variadic `unknown[]` slot takes closures with the same variadic ABI. -type HandlerFn = (...args: unknown[]) => Promise; -const fixed: HandlerFn = async (first: unknown) => first; -fixed(1, 2).then((v) => console.log(v)); diff --git a/tests/diagnostics/mock-fences.ts b/tests/diagnostics/mock-fences.ts index 4425dfa5f..ef92f366f 100644 --- a/tests/diagnostics/mock-fences.ts +++ b/tests/diagnostics/mock-fences.ts @@ -1,8 +1,9 @@ -// node:test mocks lower for mock.fn(original[, implementation]), +// node:test mocks lower for mock.fn([original[, implementation]]), // mock.method(object, methodName[, implementation]) on records, their // t.mock twins, and the trackers' restoreAll and reset. The rest of the // mock surface is refused, each with a named diagnostic: mock.fn() without -// an original; mock.method on a class instance or a builtin object, with +// an original for a function type whose return type does not include +// undefined; mock.method on a class instance or a builtin object, with // an options object, or with an implementation of another type when the // result is used; and functions whose union return type has no undefined // arm (the calls log records undefined for calls that throw). @@ -11,8 +12,11 @@ import { mock, test } from "node:test"; const add = mock.fn((a: number, b: number) => a + b); // lowers add(1, 2); -const empty = mock.fn(); // no original function -empty(); +const empty = mock.fn(); // lowers +empty(1, "two"); + +const counted = mock.fn<() => number>(); // no original, returns number +counted(); const obj = { value: () => 1 }; mock.method(obj, "value"); // lowers diff --git a/tests/diagnostics/object-literal-accessor-values.ts b/tests/diagnostics/object-literal-accessor-values.ts index 107ab1f69..e4c456a76 100644 --- a/tests/diagnostics/object-literal-accessor-values.ts +++ b/tests/diagnostics/object-literal-accessor-values.ts @@ -1,11 +1,6 @@ // Object literals with get/set accessors keep their closures in reserved -// slots, so their typed values have no JSON or checked-dynamic form: Node's -// JSON.stringify and dynamic reads invoke the getter, which neither a typed -// serializer nor a converted copy could do. - -function hold(value: unknown): unknown { - return value; -} +// slots, so their typed values have no JSON form: Node's JSON.stringify +// invokes the getter, which the typed serializer cannot do. const counter = { count: 1, @@ -17,11 +12,6 @@ const counter = { // JSON.stringify of the typed value would omit the getter's property. console.log(JSON.stringify(counter)); -// Converting the typed value to `unknown` or `any` would lose the getter. -hold(counter); -const viaAny: any = counter; -console.log(viaAny); - // A literal written into an `any` slot directly keeps its accessor. const direct: any = { get value(): number { diff --git a/tests/diagnostics/promise-all-fences.ts b/tests/diagnostics/promise-all-fences.ts index 89c29c308..c7ddc74bd 100644 --- a/tests/diagnostics/promise-all-fences.ts +++ b/tests/diagnostics/promise-all-fences.ts @@ -1,14 +1,8 @@ -// Promise.all, Promise.allSettled and Promise.any lower over ONE promise -// type (any Promise[] expression); heterogeneous array literals land -// on the checker's tuple overload and fence with the shared-type hint, -// and non-array arguments fence on their shape. -async function hetero(): Promise { - const pair = await Promise.all([ - new Promise((resolve) => resolve("s")), - new Promise((resolve) => resolve(1)), - ]); - console.log(pair.length); -} +// Promise.allSettled and Promise.any lower over ONE promise type (any +// Promise[] expression); their heterogeneous array literals land on +// the checker's tuple overload and fence with the shared-type hint, and +// non-array arguments fence on their shape. (Promise.all's tuple overload +// lowers: corpus 6482.) async function settled(): Promise { const results = await Promise.allSettled([ new Promise((resolve) => resolve("s")), @@ -22,6 +16,5 @@ function* pending(): Generator> { async function anyOfGenerator(): Promise { console.log(await Promise.any(pending())); } -hetero(); settled(); anyOfGenerator(); diff --git a/tests/diagnostics/stdlib-fence.ts b/tests/diagnostics/stdlib-fence.ts index 11b516b99..ae456af90 100644 --- a/tests/diagnostics/stdlib-fence.ts +++ b/tests/diagnostics/stdlib-fence.ts @@ -20,7 +20,7 @@ const parsedMs = Date.parse; const fmt = new Intl.NumberFormat(); // Constructors. -const d = new Date(2024, 0); // the local-time year/month field constructor stays fenced +const d = new Date(...([2024, 0] as const)); // spread field arguments stay fenced const mutableDate = new Date(0); mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity diff --git a/tests/diagnostics/stream-completion-callback-error.ts b/tests/diagnostics/stream-completion-callback-error.ts new file mode 100644 index 000000000..eb73b485d --- /dev/null +++ b/tests/diagnostics/stream-completion-callback-error.ts @@ -0,0 +1,11 @@ +import { Writable } from "node:stream"; + +// Node passes write and end callbacks an error argument (null on success); +// the native callbacks receive none, so a callback that names it refuses. +const sink = new Writable({ + write(_chunk, _encoding, done) { + done(); + }, +}); +sink.write("a", (err) => console.log("written", err)); +sink.end((err?: Error | null) => console.log("ended", err)); diff --git a/tests/fixtures/esm-path-globals/lib/helper.ts b/tests/fixtures/esm-path-globals/lib/helper.ts new file mode 100644 index 000000000..61960c097 --- /dev/null +++ b/tests/fixtures/esm-path-globals/lib/helper.ts @@ -0,0 +1,2 @@ +export const helperDir: string = __dirname; +export const helperFile: string = __filename; diff --git a/tests/fixtures/esm-path-globals/main.ts b/tests/fixtures/esm-path-globals/main.ts new file mode 100644 index 000000000..d3d3c3b8a --- /dev/null +++ b/tests/fixtures/esm-path-globals/main.ts @@ -0,0 +1,12 @@ +// __dirname and __filename in TypeScript files with import syntax: each +// module reads its own source location, the same strings as +// import.meta.dirname and import.meta.filename. +import { basename, join } from "node:path"; +import { helperDir, helperFile } from "./lib/helper.ts"; + +console.log(typeof __dirname, typeof __filename); +console.log(basename(__dirname), basename(__filename)); +console.log(__dirname === import.meta.dirname, __filename === import.meta.filename); +console.log(join(__dirname, "lib") === helperDir, basename(helperFile)); +const home = typeof __dirname === "string" && __dirname.endsWith("/lib") ? join(__dirname, "..") : "/tmp"; +console.log(home); diff --git a/tests/fixtures/node-test/cases/mock-fn-default.ts b/tests/fixtures/node-test/cases/mock-fn-default.ts new file mode 100644 index 000000000..4666dfa5e --- /dev/null +++ b/tests/fixtures/node-test/cases/mock-fn-default.ts @@ -0,0 +1,72 @@ +// mock.fn() and t.mock.fn() without an original: the mock returns +// undefined and records the arguments of each call, whatever its callers +// pass. mockImplementation and mockImplementationOnce replace its +// behavior, and restore() brings back the function that returns undefined. +// mock.fn() records F's parameters, for a function type whose return +// type is void or includes undefined. +import assert from "node:assert"; +import { mock, test } from "node:test"; + +test("records calls with any arguments", () => { + const fn = mock.fn(); + fn(1, "two"); + fn(); + fn(true, null, [3]); + assert.strictEqual(fn.mock.callCount(), 3); + const [first, second, third] = fn.mock.calls; + assert.deepStrictEqual(first!.arguments, [1, "two"]); + assert.strictEqual(second!.arguments.length, 0); + assert.deepStrictEqual(third!.arguments, [true, null, [3]]); + assert.ok(first!.result === undefined); + assert.ok(first!.error === undefined); + assert.ok(first!.this === undefined); +}); + +test("changes implementations", () => { + const seen: string[] = []; + const fn = mock.fn(); + fn.mock.mockImplementation((...args: unknown[]) => { + seen.push(`impl ${args.length}`); + return undefined; + }); + fn.mock.mockImplementationOnce(() => { + seen.push("once"); + return undefined; + }); + fn("a"); + fn("a", "b"); + fn.mock.restore(); + fn("c"); + assert.deepStrictEqual(seen, ["once", "impl 2"]); + assert.strictEqual(fn.mock.callCount(), 3); + fn.mock.resetCalls(); + assert.strictEqual(fn.mock.callCount(), 0); +}); + +test("records a throwing implementation", () => { + const fn = mock.fn(); + fn.mock.mockImplementation(() => { + throw new Error("boom"); + }); + assert.throws(() => fn(7), { message: "boom" }); + const call = fn.mock.calls[0]!; + assert.deepStrictEqual(call.arguments, [7]); + assert.ok(call.error instanceof Error); +}); + +test("t.mock.fn() and typed mock.fn()", (t) => { + const fn = t.mock.fn(); + fn("x"); + assert.strictEqual(fn.mock.callCount(), 1); + const typed = t.mock.fn<(a: number, b: string) => void>(); + typed(1, "x"); + typed(2, "y"); + assert.strictEqual(typed.mock.callCount(), 2); + assert.strictEqual(typed.mock.calls[1]!.arguments[0], 2); + assert.strictEqual(typed.mock.calls[1]!.arguments[1], "y"); + const maybe = t.mock.fn<() => number | undefined>(); + assert.ok(maybe() === undefined); + maybe.mock.mockImplementation(() => 4); + assert.ok(maybe() === 4); + assert.ok(maybe.mock.calls[1]!.result === 4); +}); diff --git a/tests/fixtures/node-types/lookup-and-header-entries.ts b/tests/fixtures/node-types/lookup-and-header-entries.ts new file mode 100644 index 000000000..a87f612d7 --- /dev/null +++ b/tests/fixtures/node-types/lookup-and-header-entries.ts @@ -0,0 +1,47 @@ +// A dns lookup function whose options parameter is Node's lookup-option +// union (`LookupOneOptions | LookupAllOptions | number | undefined`) passes +// the options to dns.lookup as they are, and Object.entries over a header +// record cast to a narrower value type reads every entry. +import * as dns from "node:dns"; +import type { OutgoingHttpHeaders } from "node:http"; + +type LookupCallback = ( + err: NodeJS.ErrnoException | null, + address: string | dns.LookupAddress[], + family?: number, +) => void; + +function createLookup(): ( + hostname: string, + opts: dns.LookupOneOptions | dns.LookupAllOptions | number | undefined, + callback: LookupCallback, +) => void { + return function lookup(hostname, opts, callback) { + dns.lookup(hostname, opts as dns.LookupOneOptions, (err, address, family) => { + if (err) return callback(err, "", 0); + callback(null, address, family); + }); + }; +} + +type HeadersLike = Headers | [string, string][] | OutgoingHttpHeaders; + +function flatten(input: HeadersLike | undefined): string[] { + const out: string[] = []; + if (!input || input instanceof Headers || Array.isArray(input)) return out; + for (const [name, value] of Object.entries(input as Record)) { + if (value === undefined) continue; + if (Array.isArray(value)) for (const v of value) out.push(`${name}=${v}`); + else out.push(`${name}=${value}`); + } + return out; +} + +const lookup = createLookup(); +lookup("localhost", { family: 4 }, (err, address, family) => { + console.log("one", err, address, family); + lookup("127.0.0.1", 4, (err3, address3, family3) => { + console.log("family", err3, address3, family3); + console.log(flatten({ accept: "text/plain", "x-list": ["a", "b"], "x-skip": undefined }).join(",")); + }); +}); diff --git a/tests/fixtures/node-types/promisify-callback-functions.ts b/tests/fixtures/node-types/promisify-callback-functions.ts new file mode 100644 index 000000000..78be5e3bd --- /dev/null +++ b/tests/fixtures/node-types/promisify-callback-functions.ts @@ -0,0 +1,67 @@ +// util.promisify over typed callback-last functions with leading arguments +// under @types/node: bound methods, a function declaration, rejection from +// the callback error, a synchronous throw, and asynchronous settlement. +import { promisify } from "node:util"; + +class Client { + private closed = false; + private readonly name: string; + constructor(name: string) { + this.name = name; + } + close(callback: (error?: Error) => void): void { + setTimeout(() => { + if (this.closed) { + callback(new Error(`${this.name} already closed`)); + return; + } + this.closed = true; + callback(); + }, 1); + } + fetch(key: string, callback: (error: Error | null, value?: string) => void): void { + setImmediate(() => { + if (key === "") callback(new RangeError("empty key")); + else callback(null, `${this.name}:${key}`); + }); + } +} + +function sum(a: number, b: number, done: (err: Error | null, total: number) => void): void { + if (a < 0) throw new TypeError("negative"); + process.nextTick(() => done(null, a + b)); +} + +async function main(): Promise { + const c = new Client("db"); + const closeAsync = promisify(c.close.bind(c)); + await closeAsync(); + console.log("closed once"); + try { + await closeAsync(); + } catch (e) { + console.log("second close:", (e as Error).message); + } + const fetchAsync = promisify(c.fetch.bind(c)); + console.log(await fetchAsync("k")); + try { + await fetchAsync(""); + } catch (e) { + console.log("fetch:", (e as Error).name, (e as Error).message); + } + const sumAsync = promisify(sum); + console.log("sum", await sumAsync(2, 3)); + try { + await sumAsync(-1, 3); + } catch (e) { + console.log("sync throw:", (e as Error).name, (e as Error).message); + } + await promisify(new Client("inline").close.bind(new Client("inline")))(); + console.log("inline done"); + const order: string[] = []; + const p = sumAsync(1, 1).then((v) => order.push(`then ${v}`)); + order.push("after call"); + await p; + console.log(order.join(", ")); +} +void main(); diff --git a/tests/fixtures/node-types/stream-subclass-hooks.ts b/tests/fixtures/node-types/stream-subclass-hooks.ts new file mode 100644 index 000000000..0e7cfbad5 --- /dev/null +++ b/tests/fixtures/node-types/stream-subclass-hooks.ts @@ -0,0 +1,46 @@ +// Stream subclasses under @types/node: a Writable whose hooks declare a +// supertype of Node's chunk type, a Transform, and an erroring Writable. +import { Transform, Writable } from "node:stream"; + +class LineSink extends Writable { + private rest = ""; + readonly lines: string[] = []; + _write(chunk: Buffer | string, encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log("chunk", typeof chunk, Buffer.isBuffer(chunk), encoding); + const parts = (this.rest + chunk.toString()).split("\n"); + this.rest = parts.pop() ?? ""; + this.lines.push(...parts); + cb(); + } + _final(cb: (err?: Error | null) => void): void { + if (this.rest !== "") this.lines.push(this.rest); + cb(); + } +} + +class Upper extends Transform { + _transform(chunk: string | Buffer, _enc: string, cb: (err?: Error | null, data?: string) => void): void { + cb(null, chunk.toString().toUpperCase()); + } +} + +class Failing extends Writable { + _write(chunk: Uint8Array | string, _enc: BufferEncoding, cb: (err?: Error | null) => void): void { + cb(new Error(`refused ${chunk.toString().length} bytes`)); + } +} + +const sink = new LineSink(); +sink.write("a\nb"); +sink.write(Buffer.from("c\nd")); +sink.end("e"); +sink.on("finish", () => console.log("lines", JSON.stringify(sink.lines))); + +const up = new Upper(); +up.on("data", (d: Buffer) => console.log("upper", d.toString())); +up.write("hello "); +up.end("world"); + +const bad = new Failing(); +bad.on("error", (e) => console.log("error", e.message)); +bad.write("xyz"); diff --git a/tests/fixtures/npm-static/node_modules/@types/twinstale/index.d.ts b/tests/fixtures/npm-static/node_modules/@types/twinstale/index.d.ts new file mode 100644 index 000000000..47f003c12 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/@types/twinstale/index.d.ts @@ -0,0 +1 @@ +export function stale(text: string): string; diff --git a/tests/fixtures/npm-static/node_modules/@types/twinstale/package.json b/tests/fixtures/npm-static/node_modules/@types/twinstale/package.json new file mode 100644 index 000000000..7f7850ce6 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/@types/twinstale/package.json @@ -0,0 +1,5 @@ +{ + "name": "@types/twinstale", + "version": "2.0.4", + "types": "index.d.ts" +} diff --git a/tests/fixtures/npm-static/node_modules/twinstale/index.js b/tests/fixtures/npm-static/node_modules/twinstale/index.js new file mode 100644 index 000000000..6efa90723 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/twinstale/index.js @@ -0,0 +1,7 @@ +// The 3.x release renamed its export: its @types twin still describes the +// 2.x name. +'use strict'; + +exports.render = function render(text) { + return 'stale:' + text; +}; diff --git a/tests/fixtures/npm-static/node_modules/twinstale/package.json b/tests/fixtures/npm-static/node_modules/twinstale/package.json new file mode 100644 index 000000000..9c104d6bf --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/twinstale/package.json @@ -0,0 +1,5 @@ +{ + "name": "twinstale", + "version": "3.1.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/twinstale-cli.ts b/tests/fixtures/npm-static/twinstale-cli.ts new file mode 100644 index 000000000..f6c8a679c --- /dev/null +++ b/tests/fixtures/npm-static/twinstale-cli.ts @@ -0,0 +1,6 @@ +// twinstale's @types twin describes an older release than the installed +// package: the program typechecks against the twin, but not against the +// JavaScript's inferred surface (under Node the import is undefined). +import { stale } from "twinstale"; + +console.log(stale("ready")); diff --git a/tests/fixtures/npm/cases/2558-bundled-chunks/main.ts b/tests/fixtures/npm/cases/2558-bundled-chunks/main.ts new file mode 100644 index 000000000..5cbbc3dbc --- /dev/null +++ b/tests/fixtures/npm/cases/2558-bundled-chunks/main.ts @@ -0,0 +1,29 @@ +// esbuild's bundled CommonJS chunks: each package below is real esbuild +// output (0.25 unminified, 0.25 whitespace+syntax minified, 0.17) of one +// source that bundles CommonJS modules through the __commonJS helper. +// Every chunk compiles as its own CommonJS module: it runs once, at its +// first require (the top-level interop imports run while the bundle +// loads; `lazy` requires its chunk only when called), chunk-to-chunk +// requires share one module instance, an esbuild-built dist bundled as a +// chunk keeps its suffixed helpers, and the external dependency passes +// through the __toESM interop. gtcjsdist is a "type": "module" package +// whose CommonJS entry is a .cjs bundle using the node-mode interop. +import { describe, external, lazy, nested } from "gtchunk"; +import { describe as dws, external as ews, lazy as lws, nested as nws } from "gtchunkws"; +import { describe as d17, external as e17, lazy as l17, nested as n17 } from "gtchunk17"; +import { viaNodeMode } from "gtcjsdist"; + +console.log("main start"); +console.log("plain", describe()); +console.log("plain", external()); +console.log("plain", nested()); +console.log("plain", lazy(), lazy()); +console.log("minified", dws()); +console.log("minified", ews()); +console.log("minified", nws()); +console.log("minified", lws(), lws()); +console.log("older", d17()); +console.log("older", e17()); +console.log("older", n17()); +console.log("older", l17(), l17()); +console.log(viaNodeMode()); diff --git a/tests/fixtures/npm/cases/2559-cjs-cycle/main.ts b/tests/fixtures/npm/cases/2559-cjs-cycle/main.ts new file mode 100644 index 000000000..042102b64 --- /dev/null +++ b/tests/fixtures/npm/cases/2559-cjs-cycle/main.ts @@ -0,0 +1,11 @@ +// A require cycle between esbuild's per-file CommonJS outputs (index ↔ +// merge, index ↔ append). Each file installs its live getter table +// before its first require, so a partner that loads while index.js is +// still initializing sees index's partially initialized exports, and both +// partners call back into index from their functions. index.js re-exports +// mergeAll from merge.js through its getter table. +import { run, mergeAll } from "gtcycle"; + +console.log(run([" a ", "", "@x"], ["b", "@keep"])); +console.log(run(["c"], [])); +console.log(mergeAll(["@keep", " d"], ["@drop"]).join("|")); diff --git a/tests/fixtures/npm/cases/2560-cjs-cycle-partner-first/main.ts b/tests/fixtures/npm/cases/2560-cjs-cycle-partner-first/main.ts new file mode 100644 index 000000000..1ed90d432 --- /dev/null +++ b/tests/fixtures/npm/cases/2560-cjs-cycle-partner-first/main.ts @@ -0,0 +1,11 @@ +// The same esbuild CommonJS cycle entered through a partner: merge.js +// loads first, requires index.js, which requires merge.js back while +// merge is still initializing (its getter table is already installed) and +// re-exports mergeAll from it: the re-export reads merge's hoisted +// function while merge is still initializing. +import { mergeAll } from "gtcycle/dist/merge.js"; +import { run, mergeAll as reexported } from "gtcycle"; + +console.log(mergeAll([" x "], ["@keep"]).join(",")); +console.log(reexported === mergeAll); +console.log(run(["y"], ["@z"])); diff --git a/tests/fixtures/npm/cases/2561-cjs-cycle-early-read/main.ts b/tests/fixtures/npm/cases/2561-cjs-cycle-early-read/main.ts new file mode 100644 index 000000000..3efeb9eb1 --- /dev/null +++ b/tests/fixtures/npm/cases/2561-cjs-cycle-early-read/main.ts @@ -0,0 +1,6 @@ +// A require cycle whose partner reads a const export of index.js while +// index is still initializing: Node throws a ReferenceError (the binding +// is in its temporal dead zone), and scriptc refuses the cycle. +import { show } from "gtcyclebad"; + +console.log(show()); diff --git a/tests/fixtures/npm/cases/2562-subpath-leaf/main.ts b/tests/fixtures/npm/cases/2562-subpath-leaf/main.ts new file mode 100644 index 000000000..b34359cc7 --- /dev/null +++ b/tests/fixtures/npm/cases/2562-subpath-leaf/main.ts @@ -0,0 +1,10 @@ +// The program reaches gtsubpath only through its leaf data module (and a +// type from its main entry). The main entry reaches a file whose bundler +// interop the recognizer cannot verify, but no compiled code can load it, +// so the package compiles: a package is judged by what its imported +// subpaths reach. +import type { Manager } from "gtsubpath"; +import { managers } from "gtsubpath/dist/data.js"; + +const names: Manager[] = managers.filter((m) => m.lockfile.endsWith(".yaml")); +console.log(managers.length, names.map((m) => m.name).join(",")); diff --git a/tests/fixtures/npm/cases/2563-bundled-esm-inits/main.ts b/tests/fixtures/npm/cases/2563-bundled-esm-inits/main.ts new file mode 100644 index 000000000..02ac60cba --- /dev/null +++ b/tests/fixtures/npm/cases/2563-bundled-esm-inits/main.ts @@ -0,0 +1,16 @@ +// esbuild's lazily initialized ES modules: each package is real esbuild +// output (0.25 plain, and whitespace/syntax minified) of a source whose +// load() require()s an ES module. That module and the module it imports +// run behind the __esm helper: once, at the first load(), assigning +// their interop bindings (a builtin and an external CommonJS package) +// and live exports that the namespace's getters read. +import { describe, load } from "gtesm"; +import { describe as dws, load as lws } from "gtesmws"; + +console.log("main start"); +console.log(describe()); +console.log(load()); +console.log(load()); +console.log(dws()); +console.log(lws()); +console.log(lws()); diff --git a/tests/fixtures/npm/cases/2564-chunk-helper-shadowing/main.ts b/tests/fixtures/npm/cases/2564-chunk-helper-shadowing/main.ts new file mode 100644 index 000000000..d52b3731d --- /dev/null +++ b/tests/fixtures/npm/cases/2564-chunk-helper-shadowing/main.ts @@ -0,0 +1,7 @@ +// A bundled CommonJS chunk that calls the bundle's top-level `label` while +// a nested function of the chunk declares its own `label`: the chunk +// carries the top-level function along, and each reference keeps its own +// binding. +import { inner, outer } from "gtchunkshadow"; + +console.log(outer(), inner()); diff --git a/tests/fixtures/npm/cases/2565-chunk-shared-function/main.ts b/tests/fixtures/npm/cases/2565-chunk-shared-function/main.ts new file mode 100644 index 000000000..21ca81b40 --- /dev/null +++ b/tests/fixtures/npm/cases/2565-chunk-shared-function/main.ts @@ -0,0 +1,7 @@ +// Two bundled CommonJS chunks export the bundle's top-level `tally` and +// count calls on a property of it. Node prints "1 2 true": both chunks +// share one function. A per-chunk copy would split its identity and its +// properties, so the package stays refused. +import { counts, same } from "gtchunkshared"; + +console.log(counts(), same()); diff --git a/tests/fixtures/npm/cases/2566-cjs-cycle-indirect-call/main.ts b/tests/fixtures/npm/cases/2566-cjs-cycle-indirect-call/main.ts new file mode 100644 index 000000000..3cca145e0 --- /dev/null +++ b/tests/fixtures/npm/cases/2566-cjs-cycle-indirect-call/main.ts @@ -0,0 +1,7 @@ +// A require cycle whose partner calls an export of index.js with esbuild's +// `(0, import_index.peek)()` spelling while index is still initializing: +// peek reads a const in its temporal dead zone, so Node throws a +// ReferenceError, and scriptc refuses the cycle. +import { show } from "gtcyclecall"; + +console.log(show()); diff --git a/tests/fixtures/npm/cases/2567-esm-namespace-this/main.ts b/tests/fixtures/npm/cases/2567-esm-namespace-this/main.ts new file mode 100644 index 000000000..d6f084bc0 --- /dev/null +++ b/tests/fixtures/npm/cases/2567-esm-namespace-this/main.ts @@ -0,0 +1,7 @@ +// A lazily initialized ES module export that reads `this`, called through +// the module's namespace (`lazy.who()`): Node passes the namespace object, +// whose `__esModule` is true. A lowered namespace object would differ, so +// the package stays refused. +import { load } from "gtesmthis"; + +console.log(load()); diff --git a/tests/fixtures/npm/cases/2571-cjs-cycle-stored-function/main.ts b/tests/fixtures/npm/cases/2571-cjs-cycle-stored-function/main.ts new file mode 100644 index 000000000..ae2be341a --- /dev/null +++ b/tests/fixtures/npm/cases/2571-cjs-cycle-stored-function/main.ts @@ -0,0 +1,8 @@ +// Require cycles whose partner stores an export of index.js (in a const, +// or in an object literal) and calls it while index is still +// initializing: the export reads a const in its temporal dead zone, so +// Node throws a ReferenceError, and scriptc refuses both cycles. +import { show } from "gtcyclealias"; +import { show as showBox } from "gtcyclebox"; + +console.log(show(), showBox()); diff --git a/tests/fixtures/npm/cases/2580-lazy-barrel/main.ts b/tests/fixtures/npm/cases/2580-lazy-barrel/main.ts new file mode 100644 index 000000000..31c88db9b --- /dev/null +++ b/tests/fixtures/npm/cases/2580-lazy-barrel/main.ts @@ -0,0 +1,19 @@ +// A barrel whose re-exports load their modules lazily: a build plugin +// (real esbuild output, plain and whitespace/syntax minified) turns each +// re-export into a getter over a memoized loader of a lazily initialized +// ES module. Node's ES-module import of a CommonJS module reads every +// lexer-visible export when it links, so each module initializes once, +// before main runs, and the imports are snapshots (greet reassigns +// GREETING afterwards). +import { greet, GREETING, Counter, describe } from "gtlazy"; +import { describe as wsDescribe, GREETING as wsGreeting } from "gtlazyws"; + +console.log("main start"); +console.log(GREETING); +console.log(greet("a")); +console.log(GREETING); +const c = new Counter(); +console.log(c.next(), c.next()); +console.log(describe()); +console.log(wsDescribe()); +console.log(wsGreeting); diff --git a/tests/fixtures/npm/cases/2581-lazy-barrel-require/main.ts b/tests/fixtures/npm/cases/2581-lazy-barrel-require/main.ts new file mode 100644 index 000000000..42bed4781 --- /dev/null +++ b/tests/fixtures/npm/cases/2581-lazy-barrel-require/main.ts @@ -0,0 +1,9 @@ +// The lazy barrel (gtlazy) required from CommonJS: require() hands out the +// export object itself, so each re-export's getter runs at its read. A +// module initializes at the first read of one of its exports, once, and +// later reads see its live bindings (greet reassigns GREETING). +import { first, again } from "gtlazyuser"; + +console.log("main start"); +console.log(first()); +console.log(again()); diff --git a/tests/fixtures/npm/cases/2582-npm-alias-two-releases/main.ts b/tests/fixtures/npm/cases/2582-npm-alias-two-releases/main.ts new file mode 100644 index 000000000..6ccf96cdb --- /dev/null +++ b/tests/fixtures/npm/cases/2582-npm-alias-two-releases/main.ts @@ -0,0 +1,7 @@ +// gtroute requires two releases of one package: gtpattern 1.0.0 under its +// own name and 2.0.0 through an npm alias directory (gtpattern-next), +// whose manifest still names gtpattern. Both compile from their own +// JavaScript, as Node loads them. +import { compile } from "gtroute"; + +console.log(compile("/users/:id")); diff --git a/tests/fixtures/npm/cases/2582-npm-alias-two-releases/node_modules/gtroute b/tests/fixtures/npm/cases/2582-npm-alias-two-releases/node_modules/gtroute new file mode 120000 index 000000000..e0ddd1565 --- /dev/null +++ b/tests/fixtures/npm/cases/2582-npm-alias-two-releases/node_modules/gtroute @@ -0,0 +1 @@ +../../../store/gtroute@1.0.0/node_modules/gtroute \ No newline at end of file diff --git a/tests/fixtures/npm/cases/2583-cjs-function-exports/main.ts b/tests/fixtures/npm/cases/2583-cjs-function-exports/main.ts new file mode 100644 index 000000000..29e65cbfc --- /dev/null +++ b/tests/fixtures/npm/cases/2583-cjs-function-exports/main.ts @@ -0,0 +1,12 @@ +// CommonJS modules whose export object is one of their own functions: +// semver 5/6's `exports = module.exports = Version` with members attached +// through both `exports` and `module.exports` (and read back through +// `exports` inside the module), and a constructor function exported whole +// with function and value members attached afterwards. +import Version from "gtfnexports"; +import Point from "gtfnmembers"; + +const v = Version.parse("2.4.1"); +console.log(v === null ? "null" : v.major(), String(new Version("3.0.0"))); +console.log(Version.compare("5.0.0", "3.2.0"), Version.gt("1.0.0", "2.0.0"), Version.MAX_LENGTH); +console.log(Point.origin().sum(), new Point(2, 3).sum(), Point.dimensions); diff --git a/tests/fixtures/npm/cases/2584-cjs-conditional-exports/main.ts b/tests/fixtures/npm/cases/2584-cjs-conditional-exports/main.ts new file mode 100644 index 000000000..2c117e914 --- /dev/null +++ b/tests/fixtures/npm/cases/2584-cjs-conditional-exports/main.ts @@ -0,0 +1,7 @@ +// An environment-dependent export object (readable-stream 2.x's +// READABLE_STREAM=disable switch): module.exports is replaced inside an +// if/else, so the module's exports depend on which branch ran. scriptc +// names the construct; Node picks the branch at load. +import rs from "gtcondexports"; + +console.log(rs.Readable.name); diff --git a/tests/fixtures/npm/cases/cjs-exports-var/main.ts b/tests/fixtures/npm/cases/cjs-exports-var/main.ts new file mode 100644 index 000000000..ec72ba751 --- /dev/null +++ b/tests/fixtures/npm/cases/cjs-exports-var/main.ts @@ -0,0 +1,16 @@ +// A CommonJS module that names its export object through a variable +// (`var ip = exports; ip.isPrivate = function …`), the ip package's +// shape, imported by default as Node does: its members are the exports, +// including the ones it calls on itself through the alias. +import ip from "exportsvar"; + +for (const addr of ["10.0.0.1", "192.168.1.20", "127.0.0.1", "::1", "8.8.8.8"]) { + console.log(addr, ip.isPrivate(addr), ip.isLoopback(addr), ip.isPublic(addr)); +} +console.log(ip.toLong("192.168.1.1")); +// Members whose parameter shadows the alias name. +console.log(ip.isV4Format("1.2.3.4"), ip.isV4Format("::1"), ip.normalize(" FE80::1 ")); +// Members with a block-local `let` or a loop `const` of the alias name: +// only that block or loop names the local binding. +console.log(ip.classify("FE80::1"), ip.classify("10.1.2.3"), ip.classify("8.8.4.4")); +console.log(ip.countPrivate(["127.0.0.1", "10.0.0.2", "10.9.9.9"]), ip.countPrivate(["10.0.0.1"])); diff --git a/tests/fixtures/npm/cases/tsc-default-object/main.ts b/tests/fixtures/npm/cases/tsc-default-object/main.ts new file mode 100644 index 000000000..bcc650c06 --- /dev/null +++ b/tests/fixtures/npm/cases/tsc-default-object/main.ts @@ -0,0 +1,7 @@ +// tsc's CommonJS output for a module with named re-exports AND a default +// export object (`exports.default = { registry: registry_api_1.registry }`, +// the @opentelemetry/api index shape): the named exports still link, read +// through their defineProperty getters. +import { registry, VERSION } from "tscdefault"; + +console.log(VERSION, registry.count("a", 2), registry.count("a", 3), registry.count("b", 1)); diff --git a/tests/fixtures/npm/node_modules/exportsvar/index.d.ts b/tests/fixtures/npm/node_modules/exportsvar/index.d.ts new file mode 100644 index 000000000..fa65c9064 --- /dev/null +++ b/tests/fixtures/npm/node_modules/exportsvar/index.d.ts @@ -0,0 +1,8 @@ +export function isPrivate(addr: string): boolean; +export function isLoopback(addr: string): boolean; +export function isPublic(addr: string): boolean; +export function toLong(addr: string): number; +export function isV4Format(ip: string): boolean; +export function normalize(ip: string): string; +export function classify(addr: string): string; +export function countPrivate(addrs: string[]): number; diff --git a/tests/fixtures/npm/node_modules/exportsvar/lib/ip.js b/tests/fixtures/npm/node_modules/exportsvar/lib/ip.js new file mode 100644 index 000000000..074ed8318 --- /dev/null +++ b/tests/fixtures/npm/node_modules/exportsvar/lib/ip.js @@ -0,0 +1,49 @@ +'use strict'; + +var ip = exports; + +ip.isPrivate = function(addr) { + return /^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) || + /^(::f{4}:)?192\.168\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr); +}; + +ip.isLoopback = function(addr) { + return /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/.test(addr) || + /^::1$/.test(addr); +}; + +ip.isPublic = function(addr) { + return !ip.isPrivate(addr) && !ip.isLoopback(addr); +}; + +ip.toLong = function(addr) { + var n = 0; + addr.split('.').forEach(function(octet) { + n <<= 8; + n += parseInt(octet, 10); + }); + return n >>> 0; +}; + +ip.isV4Format = function(ip) { + return /^(\d{1,3}\.){3,3}\d{1,3}$/.test(ip); +}; + +ip.normalize = function(ip) { + var trimmed = ip.trim(); + return trimmed.toLowerCase(); +}; + +ip.classify = function(addr) { + if (addr.indexOf(':') !== -1) { + let ip = addr.toLowerCase(); + return 'v6 ' + ip; + } + return ip.isPrivate(addr) ? 'private' : 'public'; +}; + +ip.countPrivate = function(addrs) { + var n = 0; + for (const ip of addrs) if (/^10\./.test(ip)) n++; + return n + (ip.isLoopback(addrs[0]) ? 100 : 0); +}; diff --git a/tests/fixtures/npm/node_modules/exportsvar/package.json b/tests/fixtures/npm/node_modules/exportsvar/package.json new file mode 100644 index 000000000..04881bdbd --- /dev/null +++ b/tests/fixtures/npm/node_modules/exportsvar/package.json @@ -0,0 +1,6 @@ +{ + "name": "exportsvar", + "version": "1.0.0", + "main": "lib/ip", + "types": "index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtchunk/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtchunk/dist/index.d.ts new file mode 100644 index 000000000..bd3d4e582 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunk/dist/index.d.ts @@ -0,0 +1,4 @@ +export declare function describe(): string; +export declare function external(): string; +export declare function nested(): string; +export declare function lazy(): string; diff --git a/tests/fixtures/npm/node_modules/gtchunk/dist/index.js b/tests/fixtures/npm/node_modules/gtchunk/dist/index.js new file mode 100644 index 000000000..7820e3ae4 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunk/dist/index.js @@ -0,0 +1,132 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// chunk/vendor/settings/counter.js +var require_counter = __commonJS({ + "chunk/vendor/settings/counter.js"(exports2, module2) { + var n = 0; + console.log("counter init"); + module2.exports = { + next() { + n += 1; + return n; + } + }; + } +}); + +// chunk/vendor/settings/index.js +var require_settings = __commonJS({ + "chunk/vendor/settings/index.js"(exports2) { + "use strict"; + console.log("settings init"); + var counter = require_counter(); + exports2.LABEL = "bundled"; + exports2.VERSION = 2; + exports2.bump = function() { + return counter.next(); + }; + } +}); + +// chunk/vendor/prebuilt/index.js +var require_prebuilt = __commonJS({ + "chunk/vendor/prebuilt/index.js"(exports2, module2) { + var __defProp2 = Object.defineProperty; + var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; + var __getOwnPropNames2 = Object.getOwnPropertyNames; + var __hasOwnProp2 = Object.prototype.hasOwnProperty; + var __export2 = (target, all) => { + for (var name in all) + __defProp2(target, name, { get: all[name], enumerable: true }); + }; + var __copyProps2 = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames2(from)) + if (!__hasOwnProp2.call(to, key) && key !== except) + __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable }); + } + return to; + }; + var __toCommonJS2 = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod); + var prebuilt_exports = {}; + __export2(prebuilt_exports, { + PREBUILT_TAG: () => PREBUILT_TAG2, + greet: () => greet2 + }); + module2.exports = __toCommonJS2(prebuilt_exports); + function greet2(name) { + return "hello " + name; + } + var PREBUILT_TAG2 = "prebuilt"; + } +}); + +// chunk/vendor/lazy.js +var require_lazy = __commonJS({ + "chunk/vendor/lazy.js"(exports2, module2) { + console.log("lazy init"); + module2.exports = { value: "lazy-value" }; + } +}); + +// chunk/src/index.ts +var index_exports = {}; +__export(index_exports, { + describe: () => describe, + external: () => external, + lazy: () => lazy, + nested: () => nested +}); +module.exports = __toCommonJS(index_exports); +var import_settings = __toESM(require_settings()); +var core = __toESM(require("gtcore")); +var import_gtcore = __toESM(require("gtcore")); +var import_prebuilt = __toESM(require_prebuilt()); +function describe() { + return import_settings.LABEL + "@" + import_settings.default.VERSION + " bump " + (0, import_settings.bump)() + " " + (0, import_settings.bump)(); +} +function external() { + return core.tag + ":" + core.label(1) + ":" + import_gtcore.default.label(2); +} +function nested() { + return (0, import_prebuilt.greet)("chunk") + " " + import_prebuilt.PREBUILT_TAG; +} +function lazy() { + return require_lazy().value; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + describe, + external, + lazy, + nested +}); diff --git a/tests/fixtures/npm/node_modules/gtchunk/package.json b/tests/fixtures/npm/node_modules/gtchunk/package.json new file mode 100644 index 000000000..2812a5188 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunk/package.json @@ -0,0 +1,10 @@ +{ + "name": "gtchunk", + "version": "1.0.0", + "description": "esbuild 0.25.10 output (--bundle --platform=node --format=cjs --external:gtcore): bundled CommonJS modules behind the __commonJS helper, including an esbuild-built dist with suffixed helpers", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "dependencies": { + "gtcore": "1.0.0" + } +} diff --git a/tests/fixtures/npm/node_modules/gtchunk17/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtchunk17/dist/index.d.ts new file mode 100644 index 000000000..bd3d4e582 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunk17/dist/index.d.ts @@ -0,0 +1,4 @@ +export declare function describe(): string; +export declare function external(): string; +export declare function nested(): string; +export declare function lazy(): string; diff --git a/tests/fixtures/npm/node_modules/gtchunk17/dist/index.js b/tests/fixtures/npm/node_modules/gtchunk17/dist/index.js new file mode 100644 index 000000000..afda6af56 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunk17/dist/index.js @@ -0,0 +1,132 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// chunk/vendor/settings/counter.js +var require_counter = __commonJS({ + "chunk/vendor/settings/counter.js"(exports, module2) { + var n = 0; + console.log("counter init"); + module2.exports = { + next() { + n += 1; + return n; + } + }; + } +}); + +// chunk/vendor/settings/index.js +var require_settings = __commonJS({ + "chunk/vendor/settings/index.js"(exports) { + "use strict"; + console.log("settings init"); + var counter = require_counter(); + exports.LABEL = "bundled"; + exports.VERSION = 2; + exports.bump = function() { + return counter.next(); + }; + } +}); + +// chunk/vendor/prebuilt/index.js +var require_prebuilt = __commonJS({ + "chunk/vendor/prebuilt/index.js"(exports, module2) { + var __defProp2 = Object.defineProperty; + var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; + var __getOwnPropNames2 = Object.getOwnPropertyNames; + var __hasOwnProp2 = Object.prototype.hasOwnProperty; + var __export2 = (target, all) => { + for (var name in all) + __defProp2(target, name, { get: all[name], enumerable: true }); + }; + var __copyProps2 = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames2(from)) + if (!__hasOwnProp2.call(to, key) && key !== except) + __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable }); + } + return to; + }; + var __toCommonJS2 = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod); + var prebuilt_exports = {}; + __export2(prebuilt_exports, { + PREBUILT_TAG: () => PREBUILT_TAG2, + greet: () => greet2 + }); + module2.exports = __toCommonJS2(prebuilt_exports); + function greet2(name) { + return "hello " + name; + } + var PREBUILT_TAG2 = "prebuilt"; + } +}); + +// chunk/vendor/lazy.js +var require_lazy = __commonJS({ + "chunk/vendor/lazy.js"(exports, module2) { + console.log("lazy init"); + module2.exports = { value: "lazy-value" }; + } +}); + +// chunk/src/index.ts +var src_exports = {}; +__export(src_exports, { + describe: () => describe, + external: () => external, + lazy: () => lazy, + nested: () => nested +}); +module.exports = __toCommonJS(src_exports); +var import_settings = __toESM(require_settings()); +var core = __toESM(require("gtcore")); +var import_gtcore = __toESM(require("gtcore")); +var import_prebuilt = __toESM(require_prebuilt()); +function describe() { + return import_settings.LABEL + "@" + import_settings.default.VERSION + " bump " + (0, import_settings.bump)() + " " + (0, import_settings.bump)(); +} +function external() { + return core.tag + ":" + core.label(1) + ":" + import_gtcore.default.label(2); +} +function nested() { + return (0, import_prebuilt.greet)("chunk") + " " + import_prebuilt.PREBUILT_TAG; +} +function lazy() { + return require_lazy().value; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + describe, + external, + lazy, + nested +}); diff --git a/tests/fixtures/npm/node_modules/gtchunk17/package.json b/tests/fixtures/npm/node_modules/gtchunk17/package.json new file mode 100644 index 000000000..73761f181 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunk17/package.json @@ -0,0 +1,10 @@ +{ + "name": "gtchunk17", + "version": "1.0.0", + "description": "the gtchunk source bundled by esbuild 0.17.19: chunk factories take (exports, module2)", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "dependencies": { + "gtcore": "1.0.0" + } +} diff --git a/tests/fixtures/npm/node_modules/gtchunkshadow/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtchunkshadow/dist/index.d.ts new file mode 100644 index 000000000..238cf1744 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkshadow/dist/index.d.ts @@ -0,0 +1,2 @@ +export declare function outer(): string; +export declare function inner(): string; diff --git a/tests/fixtures/npm/node_modules/gtchunkshadow/dist/index.js b/tests/fixtures/npm/node_modules/gtchunkshadow/dist/index.js new file mode 100644 index 000000000..0f89c7ec5 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkshadow/dist/index.js @@ -0,0 +1,52 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +function label(n) { + return "label " + n; +} + +// chunk/vendor/shadow.js +var require_shadow = __commonJS({ + "chunk/vendor/shadow.js"(exports2, module2) { + function nested() { + var label = "nested"; + return label; + } + module2.exports = { outer: label(1), inner: nested() }; + } +}); + +// chunk/src/index.ts +var index_exports = {}; +__export(index_exports, { + inner: () => inner, + outer: () => outer +}); +module.exports = __toCommonJS(index_exports); +function outer() { + return require_shadow().outer; +} +function inner() { + return require_shadow().inner; +} +0 && (module.exports = { + inner, + outer +}); diff --git a/tests/fixtures/npm/node_modules/gtchunkshadow/package.json b/tests/fixtures/npm/node_modules/gtchunkshadow/package.json new file mode 100644 index 000000000..6830fbd2c --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkshadow/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtchunkshadow", + "version": "1.0.0", + "description": "esbuild's CommonJS bundle shape: a chunk calls a top-level function whose name a nested scope of the chunk also declares", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtchunkshared/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtchunkshared/dist/index.d.ts new file mode 100644 index 000000000..3a47a0495 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkshared/dist/index.d.ts @@ -0,0 +1,2 @@ +export declare function same(): boolean; +export declare function counts(): string; diff --git a/tests/fixtures/npm/node_modules/gtchunkshared/dist/index.js b/tests/fixtures/npm/node_modules/gtchunkshared/dist/index.js new file mode 100644 index 000000000..0d3aacec8 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkshared/dist/index.js @@ -0,0 +1,56 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +function tally() { + tally.calls = (tally.calls || 0) + 1; + return tally.calls; +} + +// chunk/vendor/a.js +var require_a = __commonJS({ + "chunk/vendor/a.js"(exports2, module2) { + module2.exports = { tally, first: tally() }; + } +}); + +// chunk/vendor/b.js +var require_b = __commonJS({ + "chunk/vendor/b.js"(exports2, module2) { + module2.exports = { tally, second: tally() }; + } +}); + +// chunk/src/index.ts +var index_exports = {}; +__export(index_exports, { + counts: () => counts, + same: () => same +}); +module.exports = __toCommonJS(index_exports); +function same() { + return require_a().tally === require_b().tally; +} +function counts() { + return require_a().first + " " + require_b().second; +} +0 && (module.exports = { + counts, + same +}); diff --git a/tests/fixtures/npm/node_modules/gtchunkshared/package.json b/tests/fixtures/npm/node_modules/gtchunkshared/package.json new file mode 100644 index 000000000..bfc90c5d2 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkshared/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtchunkshared", + "version": "1.0.0", + "description": "esbuild's CommonJS bundle shape: two chunks export one top-level function and count calls on a property of it", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtchunkws/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtchunkws/dist/index.d.ts new file mode 100644 index 000000000..bd3d4e582 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkws/dist/index.d.ts @@ -0,0 +1,4 @@ +export declare function describe(): string; +export declare function external(): string; +export declare function nested(): string; +export declare function lazy(): string; diff --git a/tests/fixtures/npm/node_modules/gtchunkws/dist/index.js b/tests/fixtures/npm/node_modules/gtchunkws/dist/index.js new file mode 100644 index 000000000..f022baf09 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkws/dist/index.js @@ -0,0 +1 @@ +var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf,__hasOwnProp=Object.prototype.hasOwnProperty;var __commonJS=(cb,mod)=>function(){return mod||(0,cb[__getOwnPropNames(cb)[0]])((mod={exports:{}}).exports,mod),mod.exports};var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toESM=(mod,isNodeMode,target)=>(target=mod!=null?__create(__getProtoOf(mod)):{},__copyProps(isNodeMode||!mod||!mod.__esModule?__defProp(target,"default",{value:mod,enumerable:!0}):target,mod)),__toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var require_counter=__commonJS({"chunk/vendor/settings/counter.js"(exports2,module2){var n=0;console.log("counter init");module2.exports={next(){return n+=1,n}}}});var require_settings=__commonJS({"chunk/vendor/settings/index.js"(exports2){"use strict";console.log("settings init");var counter=require_counter();exports2.LABEL="bundled";exports2.VERSION=2;exports2.bump=function(){return counter.next()}}});var require_prebuilt=__commonJS({"chunk/vendor/prebuilt/index.js"(exports2,module2){var __defProp2=Object.defineProperty,__getOwnPropDesc2=Object.getOwnPropertyDescriptor,__getOwnPropNames2=Object.getOwnPropertyNames,__hasOwnProp2=Object.prototype.hasOwnProperty,__export2=(target,all)=>{for(var name in all)__defProp2(target,name,{get:all[name],enumerable:!0})},__copyProps2=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames2(from))!__hasOwnProp2.call(to,key)&&key!==except&&__defProp2(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc2(from,key))||desc.enumerable});return to},__toCommonJS2=mod=>__copyProps2(__defProp2({},"__esModule",{value:!0}),mod),prebuilt_exports={};__export2(prebuilt_exports,{PREBUILT_TAG:()=>PREBUILT_TAG2,greet:()=>greet2});module2.exports=__toCommonJS2(prebuilt_exports);function greet2(name){return"hello "+name}var PREBUILT_TAG2="prebuilt"}});var require_lazy=__commonJS({"chunk/vendor/lazy.js"(exports2,module2){console.log("lazy init");module2.exports={value:"lazy-value"}}});var index_exports={};__export(index_exports,{describe:()=>describe,external:()=>external,lazy:()=>lazy,nested:()=>nested});module.exports=__toCommonJS(index_exports);var import_settings=__toESM(require_settings()),core=__toESM(require("gtcore")),import_gtcore=__toESM(require("gtcore")),import_prebuilt=__toESM(require_prebuilt());function describe(){return import_settings.LABEL+"@"+import_settings.default.VERSION+" bump "+(0,import_settings.bump)()+" "+(0,import_settings.bump)()}function external(){return core.tag+":"+core.label(1)+":"+import_gtcore.default.label(2)}function nested(){return(0,import_prebuilt.greet)("chunk")+" "+import_prebuilt.PREBUILT_TAG}function lazy(){return require_lazy().value}0&&(module.exports={describe,external,lazy,nested}); diff --git a/tests/fixtures/npm/node_modules/gtchunkws/package.json b/tests/fixtures/npm/node_modules/gtchunkws/package.json new file mode 100644 index 000000000..5502f80a0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtchunkws/package.json @@ -0,0 +1,10 @@ +{ + "name": "gtchunkws", + "version": "1.0.0", + "description": "the gtchunk source bundled by esbuild 0.25.10 with --minify-whitespace --minify-syntax: helpers share var statements", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "dependencies": { + "gtcore": "1.0.0" + } +} diff --git a/tests/fixtures/npm/node_modules/gtcjsdist/dist/index.cjs b/tests/fixtures/npm/node_modules/gtcjsdist/dist/index.cjs new file mode 100644 index 000000000..8836b5691 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcjsdist/dist/index.cjs @@ -0,0 +1,43 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// cjsdist/src/index.ts +var index_exports = {}; +__export(index_exports, { + viaNodeMode: () => viaNodeMode +}); +module.exports = __toCommonJS(index_exports); +var core = __toESM(require("gtcore"), 1); +var import_gtcore = __toESM(require("gtcore"), 1); +function viaNodeMode() { + return import_gtcore.default.tag + "/" + core.label(7) + "/" + import_gtcore.default.label(8); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + viaNodeMode +}); diff --git a/tests/fixtures/npm/node_modules/gtcjsdist/dist/index.d.cts b/tests/fixtures/npm/node_modules/gtcjsdist/dist/index.d.cts new file mode 100644 index 000000000..6f77b9943 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcjsdist/dist/index.d.cts @@ -0,0 +1 @@ +export declare function viaNodeMode(): string; diff --git a/tests/fixtures/npm/node_modules/gtcjsdist/package.json b/tests/fixtures/npm/node_modules/gtcjsdist/package.json new file mode 100644 index 000000000..8b416fca1 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcjsdist/package.json @@ -0,0 +1,11 @@ +{ + "name": "gtcjsdist", + "version": "1.0.0", + "description": "a type-module package whose CommonJS entry is an esbuild 0.25.10 .cjs bundle using the node-mode __toESM interop", + "type": "module", + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "dependencies": { + "gtcore": "1.0.0" + } +} diff --git a/tests/fixtures/npm/node_modules/gtcondexports/index.d.ts b/tests/fixtures/npm/node_modules/gtcondexports/index.d.ts new file mode 100644 index 000000000..ba94e150b --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcondexports/index.d.ts @@ -0,0 +1,2 @@ +declare const exported: { Readable: Function; Stream: unknown; name: string }; +export = exported; diff --git a/tests/fixtures/npm/node_modules/gtcondexports/index.js b/tests/fixtures/npm/node_modules/gtcondexports/index.js new file mode 100644 index 000000000..b4017d055 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcondexports/index.js @@ -0,0 +1,14 @@ +var Stream = require("stream"); +function Local(name) { + this.name = name; +} +if (process.env.GT_STREAMS === "native" && Stream) { + module.exports = Stream; + exports = module.exports = Stream.Readable; + exports.Readable = Stream.Readable; + exports.Stream = Stream; +} else { + exports = module.exports = Local; + exports.Stream = Stream || exports; + exports.Readable = exports; +} diff --git a/tests/fixtures/npm/node_modules/gtcondexports/package.json b/tests/fixtures/npm/node_modules/gtcondexports/package.json new file mode 100644 index 000000000..eab4647ed --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcondexports/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtcondexports", + "version": "1.0.0", + "description": "readable-stream 2.x's environment-dependent export object: module.exports is replaced (and exports rebound) inside an if/else on an environment variable", + "main": "index.js", + "types": "index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtcycle/dist/append.d.ts b/tests/fixtures/npm/node_modules/gtcycle/dist/append.d.ts new file mode 100644 index 000000000..e33c31f90 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcycle/dist/append.d.ts @@ -0,0 +1 @@ +export declare function appendAll(xs: string[], tail: string): string[]; diff --git a/tests/fixtures/npm/node_modules/gtcycle/dist/append.js b/tests/fixtures/npm/node_modules/gtcycle/dist/append.js new file mode 100644 index 000000000..cdfeb6c6c --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcycle/dist/append.js @@ -0,0 +1,30 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var append_exports = {}; +__export(append_exports, { + appendAll: () => appendAll +}); +module.exports = __toCommonJS(append_exports); +var import_index = require("./index"); +function appendAll(xs, tail) { + return xs.some(import_index.isHandler) ? xs : xs.concat([tail]); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + appendAll +}); diff --git a/tests/fixtures/npm/node_modules/gtcycle/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtcycle/dist/index.d.ts new file mode 100644 index 000000000..4b86308a9 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcycle/dist/index.d.ts @@ -0,0 +1,4 @@ +export { mergeAll } from "./merge"; +export declare function isHandler(x: string): boolean; +export declare function normalize(xs: string[]): string[]; +export declare function run(a: string[], b: string[]): string; diff --git a/tests/fixtures/npm/node_modules/gtcycle/dist/index.js b/tests/fixtures/npm/node_modules/gtcycle/dist/index.js new file mode 100644 index 000000000..92f6d2a39 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcycle/dist/index.js @@ -0,0 +1,44 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var index_exports = {}; +__export(index_exports, { + isHandler: () => isHandler, + mergeAll: () => import_merge2.mergeAll, + normalize: () => normalize, + run: () => run +}); +module.exports = __toCommonJS(index_exports); +var import_append = require("./append"); +var import_merge = require("./merge"); +var import_merge2 = require("./merge"); +function isHandler(x) { + return x.startsWith("@"); +} +function normalize(xs) { + return xs.map((x) => x.trim()).filter((x) => x.length > 0); +} +function run(a, b) { + return (0, import_append.appendAll)((0, import_merge.mergeAll)(a, b), "@end").join(","); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + isHandler, + mergeAll, + normalize, + run +}); diff --git a/tests/fixtures/npm/node_modules/gtcycle/dist/merge.d.ts b/tests/fixtures/npm/node_modules/gtcycle/dist/merge.d.ts new file mode 100644 index 000000000..bbe6b6a4d --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcycle/dist/merge.d.ts @@ -0,0 +1 @@ +export declare function mergeAll(a: string[], b: string[]): string[]; diff --git a/tests/fixtures/npm/node_modules/gtcycle/dist/merge.js b/tests/fixtures/npm/node_modules/gtcycle/dist/merge.js new file mode 100644 index 000000000..114e7d8cf --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcycle/dist/merge.js @@ -0,0 +1,30 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var merge_exports = {}; +__export(merge_exports, { + mergeAll: () => mergeAll +}); +module.exports = __toCommonJS(merge_exports); +var import_index = require("./index"); +function mergeAll(a, b) { + return (0, import_index.normalize)(a.concat(b)).filter((x) => !(0, import_index.isHandler)(x) || x === "@keep"); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + mergeAll +}); diff --git a/tests/fixtures/npm/node_modules/gtcycle/package.json b/tests/fixtures/npm/node_modules/gtcycle/package.json new file mode 100644 index 000000000..5b41b8a52 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcycle/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtcycle", + "version": "1.0.0", + "description": "esbuild 0.25.10 per-file CommonJS output (no bundling) of three modules that import each other", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtcyclealias/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtcyclealias/dist/index.d.ts new file mode 100644 index 000000000..5f4373e2f --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclealias/dist/index.d.ts @@ -0,0 +1,3 @@ +export declare const LIMIT = 3; +export declare function show(): string; +export declare function peek(): number; diff --git a/tests/fixtures/npm/node_modules/gtcyclealias/dist/index.js b/tests/fixtures/npm/node_modules/gtcyclealias/dist/index.js new file mode 100644 index 000000000..77aca3c9b --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclealias/dist/index.js @@ -0,0 +1,37 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var index_exports = {}; +__export(index_exports, { + LIMIT: () => LIMIT, + peek: () => peek, + show: () => show +}); +module.exports = __toCommonJS(index_exports); +var import_reader = require("./reader"); +const LIMIT = 3; +function peek() { + return LIMIT; +} +function show() { + return (0, import_reader.describe)(); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + LIMIT, + show +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclealias/dist/reader.js b/tests/fixtures/npm/node_modules/gtcyclealias/dist/reader.js new file mode 100644 index 000000000..943e61a0a --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclealias/dist/reader.js @@ -0,0 +1,34 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var reader_exports = {}; +__export(reader_exports, { + captured: () => captured, + describe: () => describe +}); +module.exports = __toCommonJS(reader_exports); +var import_index = require("./index"); +const read = import_index.peek; +const captured = read(); +function describe() { + return "limit " + captured; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + captured, + describe +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclealias/package.json b/tests/fixtures/npm/node_modules/gtcyclealias/package.json new file mode 100644 index 000000000..9ee7441e0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclealias/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtcyclealias", + "version": "1.0.0", + "description": "esbuild 0.25.10 per-file CommonJS output shape of a cycle whose partner stores an exported function in a const and calls it while that function's const binding is uninitialized", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtcyclebad/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtcyclebad/dist/index.d.ts new file mode 100644 index 000000000..0fa366319 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebad/dist/index.d.ts @@ -0,0 +1,2 @@ +export declare const LIMIT = 3; +export declare function show(): string; diff --git a/tests/fixtures/npm/node_modules/gtcyclebad/dist/index.js b/tests/fixtures/npm/node_modules/gtcyclebad/dist/index.js new file mode 100644 index 000000000..5bd708a37 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebad/dist/index.js @@ -0,0 +1,33 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var index_exports = {}; +__export(index_exports, { + LIMIT: () => LIMIT, + show: () => show +}); +module.exports = __toCommonJS(index_exports); +var import_reader = require("./reader"); +const LIMIT = 3; +function show() { + return (0, import_reader.describe)(); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + LIMIT, + show +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclebad/dist/reader.js b/tests/fixtures/npm/node_modules/gtcyclebad/dist/reader.js new file mode 100644 index 000000000..a58bdaa51 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebad/dist/reader.js @@ -0,0 +1,33 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var reader_exports = {}; +__export(reader_exports, { + captured: () => captured, + describe: () => describe +}); +module.exports = __toCommonJS(reader_exports); +var import_index = require("./index"); +const captured = import_index.LIMIT; +function describe() { + return "limit " + captured; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + captured, + describe +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclebad/package.json b/tests/fixtures/npm/node_modules/gtcyclebad/package.json new file mode 100644 index 000000000..793c97fc9 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebad/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtcyclebad", + "version": "1.0.0", + "description": "esbuild 0.25.10 per-file CommonJS output of a cycle whose partner reads a const binding before its declaration runs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtcyclebox/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtcyclebox/dist/index.d.ts new file mode 100644 index 000000000..5f4373e2f --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebox/dist/index.d.ts @@ -0,0 +1,3 @@ +export declare const LIMIT = 3; +export declare function show(): string; +export declare function peek(): number; diff --git a/tests/fixtures/npm/node_modules/gtcyclebox/dist/index.js b/tests/fixtures/npm/node_modules/gtcyclebox/dist/index.js new file mode 100644 index 000000000..77aca3c9b --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebox/dist/index.js @@ -0,0 +1,37 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var index_exports = {}; +__export(index_exports, { + LIMIT: () => LIMIT, + peek: () => peek, + show: () => show +}); +module.exports = __toCommonJS(index_exports); +var import_reader = require("./reader"); +const LIMIT = 3; +function peek() { + return LIMIT; +} +function show() { + return (0, import_reader.describe)(); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + LIMIT, + show +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclebox/dist/reader.js b/tests/fixtures/npm/node_modules/gtcyclebox/dist/reader.js new file mode 100644 index 000000000..b8f8a927e --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebox/dist/reader.js @@ -0,0 +1,34 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var reader_exports = {}; +__export(reader_exports, { + captured: () => captured, + describe: () => describe +}); +module.exports = __toCommonJS(reader_exports); +var import_index = require("./index"); +const box = { read: import_index.peek }; +const captured = box.read(); +function describe() { + return "limit " + captured; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + captured, + describe +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclebox/package.json b/tests/fixtures/npm/node_modules/gtcyclebox/package.json new file mode 100644 index 000000000..67f943195 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclebox/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtcyclebox", + "version": "1.0.0", + "description": "esbuild 0.25.10 per-file CommonJS output shape of a cycle whose partner stores an exported function in an object literal and calls it while that function's const binding is uninitialized", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtcyclecall/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtcyclecall/dist/index.d.ts new file mode 100644 index 000000000..5f4373e2f --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclecall/dist/index.d.ts @@ -0,0 +1,3 @@ +export declare const LIMIT = 3; +export declare function show(): string; +export declare function peek(): number; diff --git a/tests/fixtures/npm/node_modules/gtcyclecall/dist/index.js b/tests/fixtures/npm/node_modules/gtcyclecall/dist/index.js new file mode 100644 index 000000000..77aca3c9b --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclecall/dist/index.js @@ -0,0 +1,37 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var index_exports = {}; +__export(index_exports, { + LIMIT: () => LIMIT, + peek: () => peek, + show: () => show +}); +module.exports = __toCommonJS(index_exports); +var import_reader = require("./reader"); +const LIMIT = 3; +function peek() { + return LIMIT; +} +function show() { + return (0, import_reader.describe)(); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + LIMIT, + show +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclecall/dist/reader.js b/tests/fixtures/npm/node_modules/gtcyclecall/dist/reader.js new file mode 100644 index 000000000..c98d1ae46 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclecall/dist/reader.js @@ -0,0 +1,33 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var reader_exports = {}; +__export(reader_exports, { + captured: () => captured, + describe: () => describe +}); +module.exports = __toCommonJS(reader_exports); +var import_index = require("./index"); +const captured = (0, import_index.peek)(); +function describe() { + return "limit " + captured; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + captured, + describe +}); diff --git a/tests/fixtures/npm/node_modules/gtcyclecall/package.json b/tests/fixtures/npm/node_modules/gtcyclecall/package.json new file mode 100644 index 000000000..cc1069a6f --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtcyclecall/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtcyclecall", + "version": "1.0.0", + "description": "esbuild 0.25.10 per-file CommonJS output shape of a cycle whose partner calls an exported function through (0, binding.fn)() while that function's const binding is uninitialized", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtesm/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtesm/dist/index.d.ts new file mode 100644 index 000000000..af4369397 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtesm/dist/index.d.ts @@ -0,0 +1,2 @@ +export declare function describe(): string; +export declare function load(): string; diff --git a/tests/fixtures/npm/node_modules/gtesm/dist/index.js b/tests/fixtures/npm/node_modules/gtesm/dist/index.js new file mode 100644 index 000000000..241c5e048 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtesm/dist/index.js @@ -0,0 +1,90 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __esm = (fn, res) => function __init() { + return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/helper.ts +function helper(x) { + COUNT++; + return "[" + x + "]"; +} +var COUNT; +var init_helper = __esm({ + "src/helper.ts"() { + console.log("helper init"); + COUNT = 0; + } +}); + +// src/lazy.ts +var lazy_exports = {}; +__export(lazy_exports, { + VALUE: () => VALUE, + value: () => value +}); +function value() { + return VALUE + ":" + import_gtcore.default.tag + ":" + COUNT; +} +var path, import_gtcore, VALUE; +var init_lazy = __esm({ + "src/lazy.ts"() { + path = __toESM(require("path")); + import_gtcore = __toESM(require("gtcore")); + init_helper(); + console.log("lazy init"); + VALUE = helper(path.basename("/a/b.txt")); + } +}); + +// src/index.ts +var index_exports = {}; +__export(index_exports, { + describe: () => describe, + load: () => load +}); +module.exports = __toCommonJS(index_exports); + +// src/greet.ts +function greet(name) { + return "hello " + name; +} + +// src/index.ts +function describe() { + return greet("main"); +} +function load() { + const lazy = (init_lazy(), __toCommonJS(lazy_exports)); + return lazy.value() + " " + lazy.VALUE; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + describe, + load +}); diff --git a/tests/fixtures/npm/node_modules/gtesm/package.json b/tests/fixtures/npm/node_modules/gtesm/package.json new file mode 100644 index 000000000..3913402f9 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtesm/package.json @@ -0,0 +1,10 @@ +{ + "name": "gtesm", + "version": "1.0.0", + "description": "esbuild 0.25.10 output (--bundle --platform=node --format=cjs --external:gtcore) of a source that require()s an ES module: lazily initialized ES modules behind the __esm helper", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "dependencies": { + "gtcore": "1.0.0" + } +} diff --git a/tests/fixtures/npm/node_modules/gtesmthis/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtesmthis/dist/index.d.ts new file mode 100644 index 000000000..af4369397 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtesmthis/dist/index.d.ts @@ -0,0 +1,2 @@ +export declare function describe(): string; +export declare function load(): string; diff --git a/tests/fixtures/npm/node_modules/gtesmthis/dist/index.js b/tests/fixtures/npm/node_modules/gtesmthis/dist/index.js new file mode 100644 index 000000000..46b81aa7d --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtesmthis/dist/index.js @@ -0,0 +1,94 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __esm = (fn, res) => function __init() { + return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/helper.ts +function helper(x) { + COUNT++; + return "[" + x + "]"; +} +var COUNT; +var init_helper = __esm({ + "src/helper.ts"() { + console.log("helper init"); + COUNT = 0; + } +}); + +// src/lazy.ts +var lazy_exports = {}; +__export(lazy_exports, { + VALUE: () => VALUE, + value: () => value, + who: () => who +}); +function who() { + return [typeof this, this.__esModule, Object.getOwnPropertyNames(this).join(","), Object.keys(this).join(",")].join(" "); +} +function value() { + return VALUE + ":" + import_gtcore.default.tag + ":" + COUNT; +} +var path, import_gtcore, VALUE; +var init_lazy = __esm({ + "src/lazy.ts"() { + path = __toESM(require("path")); + import_gtcore = __toESM(require("gtcore")); + init_helper(); + console.log("lazy init"); + VALUE = helper(path.basename("/a/b.txt")); + } +}); + +// src/index.ts +var index_exports = {}; +__export(index_exports, { + describe: () => describe, + load: () => load +}); +module.exports = __toCommonJS(index_exports); + +// src/greet.ts +function greet(name) { + return "hello " + name; +} + +// src/index.ts +function describe() { + return greet("main"); +} +function load() { + const lazy = (init_lazy(), __toCommonJS(lazy_exports)); + return lazy.value() + " " + lazy.VALUE + " | " + lazy.who() + " | " + (init_lazy(), __toCommonJS(lazy_exports)).who(); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + describe, + load +}); diff --git a/tests/fixtures/npm/node_modules/gtesmthis/package.json b/tests/fixtures/npm/node_modules/gtesmthis/package.json new file mode 100644 index 000000000..c6344fa8c --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtesmthis/package.json @@ -0,0 +1,10 @@ +{ + "name": "gtesmthis", + "version": "1.0.0", + "description": "gtesm's esbuild 0.25.10 output shape with a lazily initialized ES module export that reads `this`, called through the module's namespace", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "dependencies": { + "gtcore": "1.0.0" + } +} diff --git a/tests/fixtures/npm/node_modules/gtesmws/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtesmws/dist/index.d.ts new file mode 100644 index 000000000..af4369397 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtesmws/dist/index.d.ts @@ -0,0 +1,2 @@ +export declare function describe(): string; +export declare function load(): string; diff --git a/tests/fixtures/npm/node_modules/gtesmws/dist/index.js b/tests/fixtures/npm/node_modules/gtesmws/dist/index.js new file mode 100644 index 000000000..cef009700 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtesmws/dist/index.js @@ -0,0 +1 @@ +var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf,__hasOwnProp=Object.prototype.hasOwnProperty;var __esm=(fn,res)=>function(){return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res};var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toESM=(mod,isNodeMode,target)=>(target=mod!=null?__create(__getProtoOf(mod)):{},__copyProps(isNodeMode||!mod||!mod.__esModule?__defProp(target,"default",{value:mod,enumerable:!0}):target,mod)),__toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);function helper(x){return COUNT++,"["+x+"]"}var COUNT,init_helper=__esm({"src/helper.ts"(){console.log("helper init");COUNT=0}});var lazy_exports={};__export(lazy_exports,{VALUE:()=>VALUE,value:()=>value});function value(){return VALUE+":"+import_gtcore.default.tag+":"+COUNT}var path,import_gtcore,VALUE,init_lazy=__esm({"src/lazy.ts"(){path=__toESM(require("path")),import_gtcore=__toESM(require("gtcore"));init_helper();console.log("lazy init");VALUE=helper(path.basename("/a/b.txt"))}});var index_exports={};__export(index_exports,{describe:()=>describe,load:()=>load});module.exports=__toCommonJS(index_exports);function greet(name){return"hello "+name}function describe(){return greet("main")}function load(){let lazy=(init_lazy(),__toCommonJS(lazy_exports));return lazy.value()+" "+lazy.VALUE}0&&(module.exports={describe,load}); diff --git a/tests/fixtures/npm/node_modules/gtesmws/package.json b/tests/fixtures/npm/node_modules/gtesmws/package.json new file mode 100644 index 000000000..3dfe90e90 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtesmws/package.json @@ -0,0 +1,10 @@ +{ + "name": "gtesmws", + "version": "1.0.0", + "description": "the gtesm source bundled by esbuild 0.25.10 with --minify-whitespace --minify-syntax", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "dependencies": { + "gtcore": "1.0.0" + } +} diff --git a/tests/fixtures/npm/node_modules/gtfnexports/index.d.ts b/tests/fixtures/npm/node_modules/gtfnexports/index.d.ts new file mode 100644 index 000000000..c2ee082d3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtfnexports/index.d.ts @@ -0,0 +1,13 @@ +declare class Version { + constructor(raw: string); + raw: string; + major(): number; + toString(): string; +} +declare namespace Version { + const MAX_LENGTH: number; + function parse(raw: string): Version | null; + function compare(a: string, b: string): number; + function gt(a: string, b: string): boolean; +} +export = Version; diff --git a/tests/fixtures/npm/node_modules/gtfnexports/index.js b/tests/fixtures/npm/node_modules/gtfnexports/index.js new file mode 100644 index 000000000..d64b58f0c --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtfnexports/index.js @@ -0,0 +1,25 @@ +exports = module.exports = Version; + +var label = process.env.GT_LABEL ? "[" + process.env.GT_LABEL + "]" : ""; + +function Version(raw) { + this.raw = raw; + this.parts = raw.split("."); +} +Version.prototype.major = function () { + return Number(this.parts[0]); +}; +Version.prototype.toString = function () { + return label + this.raw; +}; + +exports.MAX_LENGTH = 256; +exports.parse = function parse(raw) { + return raw.length > exports.MAX_LENGTH ? null : new Version(raw); +}; +exports.compare = function compare(a, b) { + return exports.parse(a).major() - exports.parse(b).major(); +}; +module.exports.gt = function gt(a, b) { + return exports.compare(a, b) > 0; +}; diff --git a/tests/fixtures/npm/node_modules/gtfnexports/package.json b/tests/fixtures/npm/node_modules/gtfnexports/package.json new file mode 100644 index 000000000..4524cd1bc --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtfnexports/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtfnexports", + "version": "1.0.0", + "description": "semver 5/6's export idiom: exports = module.exports = Version, then members attached through exports and module.exports", + "main": "index.js", + "types": "index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtfnmembers/index.d.ts b/tests/fixtures/npm/node_modules/gtfnmembers/index.d.ts new file mode 100644 index 000000000..ea1888e0b --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtfnmembers/index.d.ts @@ -0,0 +1,11 @@ +declare class Point { + constructor(x: number, y: number); + x: number; + y: number; + sum(): number; +} +declare namespace Point { + function origin(): Point; + const dimensions: number; +} +export = Point; diff --git a/tests/fixtures/npm/node_modules/gtfnmembers/index.js b/tests/fixtures/npm/node_modules/gtfnmembers/index.js new file mode 100644 index 000000000..6eaf003f7 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtfnmembers/index.js @@ -0,0 +1,13 @@ +"use strict"; +function Point(x, y) { + this.x = x; + this.y = y; +} +Point.prototype.sum = function () { + return this.x + this.y; +}; +module.exports = Point; +module.exports.origin = function origin() { + return new Point(0, 0); +}; +module.exports.dimensions = 2; diff --git a/tests/fixtures/npm/node_modules/gtfnmembers/package.json b/tests/fixtures/npm/node_modules/gtfnmembers/package.json new file mode 100644 index 000000000..5179c6caf --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtfnmembers/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtfnmembers", + "version": "1.0.0", + "description": "a constructor function as module.exports with function and value members attached through module.exports afterwards", + "main": "index.js", + "types": "index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtlazy/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtlazy/dist/index.d.ts new file mode 100644 index 000000000..d3b4bac73 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtlazy/dist/index.d.ts @@ -0,0 +1,7 @@ +export declare let GREETING: string; +export declare function greet(name: string): string; +export declare class Counter { + n: number; + next(): number; +} +export declare function describe(): string; diff --git a/tests/fixtures/npm/node_modules/gtlazy/dist/index.js b/tests/fixtures/npm/node_modules/gtlazy/dist/index.js new file mode 100644 index 000000000..9a94faa11 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtlazy/dist/index.js @@ -0,0 +1,116 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __esm = (fn, res) => function __init() { + return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/greet.ts +var greet_exports = {}; +__export(greet_exports, { + GREETING: () => GREETING, + greet: () => greet +}); +function greet(name) { + GREETING = "hi"; + return GREETING + " " + name; +} +var GREETING; +var init_greet = __esm({ + "src/greet.ts"() { + console.log("greet init"); + GREETING = "hello"; + } +}); + +// src/counter.ts +var counter_exports = {}; +__export(counter_exports, { + default: () => Counter +}); +var Counter; +var init_counter = __esm({ + "src/counter.ts"() { + console.log("counter init"); + Counter = class { + n = 0; + next() { + return ++this.n; + } + }; + } +}); + +// src/describe.ts +var describe_exports = {}; +__export(describe_exports, { + describe: () => describe +}); +function describe() { + return "described: " + greet("d"); +} +var init_describe = __esm({ + "src/describe.ts"() { + init_greet(); + console.log("describe init"); + } +}); + +// src/index.ts +var __create = Object.create; +var __defProp2 = Object.defineProperty; +var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; +var __getOwnPropNames2 = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp2 = Object.prototype.hasOwnProperty; +var __export2 = (target, all) => { + for (var name in all) + __defProp2(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps2 = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames2(from)) + if (!__hasOwnProp2.call(to, key) && key !== except) + __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps2( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp2(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS2 = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod); +var stdin_exports = {}; +__export2(stdin_exports, { + Counter: () => load_import_counter().default, + GREETING: () => load_import_greet().GREETING, + describe: () => load_import_describe().describe, + greet: () => load_import_greet().greet +}); +module.exports = __toCommonJS2(stdin_exports); +var import_greet2; +var load_import_greet = () => import_greet2 ??= (init_greet(), __toCommonJS(greet_exports)); +var import_counter; +var load_import_counter = () => import_counter ??= __toESM((init_counter(), __toCommonJS(counter_exports))); +var import_describe; +var load_import_describe = () => import_describe ??= (init_describe(), __toCommonJS(describe_exports)); + +0 && (module.exports = {"Counter": null,"GREETING": null,"describe": null,"greet": null}); diff --git a/tests/fixtures/npm/node_modules/gtlazy/package.json b/tests/fixtures/npm/node_modules/gtlazy/package.json new file mode 100644 index 000000000..e63f696a6 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtlazy/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtlazy", + "version": "1.0.0", + "description": "esbuild 0.25.10 output (--bundle --platform=node --format=cjs) of a barrel whose re-exports a build plugin turns into memoized lazy loaders: `load_x = () => x ??= (init_x(), __toCommonJS(x_exports))`", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtlazyuser/index.d.ts b/tests/fixtures/npm/node_modules/gtlazyuser/index.d.ts new file mode 100644 index 000000000..948fec847 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtlazyuser/index.d.ts @@ -0,0 +1,2 @@ +export declare function first(): string; +export declare function again(): string; diff --git a/tests/fixtures/npm/node_modules/gtlazyuser/index.js b/tests/fixtures/npm/node_modules/gtlazyuser/index.js new file mode 100644 index 000000000..d5666d6e1 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtlazyuser/index.js @@ -0,0 +1,10 @@ +"use strict"; +const lazy = require("gtlazy"); + +console.log("user loaded"); +exports.first = function first() { + return lazy.GREETING + "/" + lazy.greet("u"); +}; +exports.again = function again() { + return lazy.GREETING + "/" + lazy.describe(); +}; diff --git a/tests/fixtures/npm/node_modules/gtlazyuser/package.json b/tests/fixtures/npm/node_modules/gtlazyuser/package.json new file mode 100644 index 000000000..841bdc7b2 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtlazyuser/package.json @@ -0,0 +1,10 @@ +{ + "name": "gtlazyuser", + "version": "1.0.0", + "description": "a CommonJS package that require()s the gtlazy barrel: its lazy re-exports initialize at the first read and read live bindings", + "main": "index.js", + "types": "index.d.ts", + "dependencies": { + "gtlazy": "1.0.0" + } +} diff --git a/tests/fixtures/npm/node_modules/gtlazyws/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtlazyws/dist/index.d.ts new file mode 100644 index 000000000..d3b4bac73 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtlazyws/dist/index.d.ts @@ -0,0 +1,7 @@ +export declare let GREETING: string; +export declare function greet(name: string): string; +export declare class Counter { + n: number; + next(): number; +} +export declare function describe(): string; diff --git a/tests/fixtures/npm/node_modules/gtlazyws/dist/index.js b/tests/fixtures/npm/node_modules/gtlazyws/dist/index.js new file mode 100644 index 000000000..a472466b0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtlazyws/dist/index.js @@ -0,0 +1,3 @@ +var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __esm=(fn,res)=>function(){return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res};var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var greet_exports={};__export(greet_exports,{GREETING:()=>GREETING,greet:()=>greet});function greet(name){return GREETING="hi",GREETING+" "+name}var GREETING,init_greet=__esm({"src/greet.ts"(){console.log("greet init");GREETING="hello"}});var counter_exports={};__export(counter_exports,{default:()=>Counter});var Counter,init_counter=__esm({"src/counter.ts"(){console.log("counter init");Counter=class{n=0;next(){return++this.n}}}});var describe_exports={};__export(describe_exports,{describe:()=>describe});function describe(){return"described: "+greet("d")}var init_describe=__esm({"src/describe.ts"(){init_greet();console.log("describe init")}});var __create=Object.create,__defProp2=Object.defineProperty,__getOwnPropDesc2=Object.getOwnPropertyDescriptor,__getOwnPropNames2=Object.getOwnPropertyNames,__getProtoOf=Object.getPrototypeOf,__hasOwnProp2=Object.prototype.hasOwnProperty,__export2=(target,all)=>{for(var name in all)__defProp2(target,name,{get:all[name],enumerable:!0})},__copyProps2=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames2(from))!__hasOwnProp2.call(to,key)&&key!==except&&__defProp2(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc2(from,key))||desc.enumerable});return to},__toESM=(mod,isNodeMode,target)=>(target=mod!=null?__create(__getProtoOf(mod)):{},__copyProps2(isNodeMode||!mod||!mod.__esModule?__defProp2(target,"default",{value:mod,enumerable:!0}):target,mod)),__toCommonJS2=mod=>__copyProps2(__defProp2({},"__esModule",{value:!0}),mod),stdin_exports={};__export2(stdin_exports,{Counter:()=>load_import_counter().default,GREETING:()=>load_import_greet().GREETING,describe:()=>load_import_describe().describe,greet:()=>load_import_greet().greet});module.exports=__toCommonJS2(stdin_exports);var import_greet2,load_import_greet=()=>import_greet2??=(init_greet(),__toCommonJS(greet_exports)),import_counter,load_import_counter=()=>import_counter??=__toESM((init_counter(),__toCommonJS(counter_exports))),import_describe,load_import_describe=()=>import_describe??=(init_describe(),__toCommonJS(describe_exports)); + +0 && (module.exports = {"Counter": null,"GREETING": null,"describe": null,"greet": null}); diff --git a/tests/fixtures/npm/node_modules/gtlazyws/package.json b/tests/fixtures/npm/node_modules/gtlazyws/package.json new file mode 100644 index 000000000..bf5ec5b93 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtlazyws/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtlazyws", + "version": "1.0.0", + "description": "the gtlazy source and plugin bundled by esbuild 0.25.10 with --minify-whitespace --minify-syntax", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/fixtures/npm/node_modules/gtsubpath/dist/data.d.ts b/tests/fixtures/npm/node_modules/gtsubpath/dist/data.d.ts new file mode 100644 index 000000000..356c8bbc9 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtsubpath/dist/data.d.ts @@ -0,0 +1,2 @@ +import type { Manager } from "./index.js"; +export declare const managers: Manager[]; diff --git a/tests/fixtures/npm/node_modules/gtsubpath/dist/data.js b/tests/fixtures/npm/node_modules/gtsubpath/dist/data.js new file mode 100644 index 000000000..918a296a8 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtsubpath/dist/data.js @@ -0,0 +1,6 @@ +"use strict"; +exports.managers = [ + { name: "npm", lockfile: "package-lock.json" }, + { name: "pnpm", lockfile: "pnpm-lock.yaml" }, + { name: "yarn", lockfile: "yarn.lock" }, +]; diff --git a/tests/fixtures/npm/node_modules/gtsubpath/dist/detect.js b/tests/fixtures/npm/node_modules/gtsubpath/dist/detect.js new file mode 100644 index 000000000..d129041fe --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtsubpath/dist/detect.js @@ -0,0 +1,11 @@ +// A hand-rolled __toESM the recognizer cannot verify (see gtdrift). +"use strict"; +var __toESM = (mod) => { + var ns = { default: mod }; + for (var key in mod) ns[key] = mod[key]; + return ns; +}; +var import_gtcore = __toESM(require("gtcore")); +exports.detect = function detect(files) { + return import_gtcore.default.label(files.length); +}; diff --git a/tests/fixtures/npm/node_modules/gtsubpath/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtsubpath/dist/index.d.ts new file mode 100644 index 000000000..24b05d86d --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtsubpath/dist/index.d.ts @@ -0,0 +1,6 @@ +export interface Manager { + name: string; + lockfile: string; +} +export declare const managers: Manager[]; +export declare function detect(files: string[]): string; diff --git a/tests/fixtures/npm/node_modules/gtsubpath/dist/index.js b/tests/fixtures/npm/node_modules/gtsubpath/dist/index.js new file mode 100644 index 000000000..a330d28c0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtsubpath/dist/index.js @@ -0,0 +1,5 @@ +"use strict"; +const detect = require("./detect.js"); +const data = require("./data.js"); +exports.detect = detect.detect; +exports.managers = data.managers; diff --git a/tests/fixtures/npm/node_modules/gtsubpath/package.json b/tests/fixtures/npm/node_modules/gtsubpath/package.json new file mode 100644 index 000000000..f785cb154 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtsubpath/package.json @@ -0,0 +1,10 @@ +{ + "name": "gtsubpath", + "version": "1.0.0", + "description": "a package whose main entry reaches a file with bundler interop the recognizer cannot verify, beside a leaf data module the program imports by subpath", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "dependencies": { + "gtcore": "1.0.0" + } +} diff --git a/tests/fixtures/npm/node_modules/tscdefault/build/index.d.ts b/tests/fixtures/npm/node_modules/tscdefault/build/index.d.ts new file mode 100644 index 000000000..7fedd198f --- /dev/null +++ b/tests/fixtures/npm/node_modules/tscdefault/build/index.d.ts @@ -0,0 +1,8 @@ +import { registry } from './registry-api'; +export { registry }; +export declare const VERSION = "1.0.0"; +declare const _default: { + registry: import("./registry").Registry; + version: string; +}; +export default _default; diff --git a/tests/fixtures/npm/node_modules/tscdefault/build/index.js b/tests/fixtures/npm/node_modules/tscdefault/build/index.js new file mode 100644 index 000000000..0dff83d00 --- /dev/null +++ b/tests/fixtures/npm/node_modules/tscdefault/build/index.js @@ -0,0 +1,11 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.VERSION = exports.registry = void 0; +const registry_api_1 = require("./registry-api"); +Object.defineProperty(exports, "registry", { enumerable: true, get: function () { return registry_api_1.registry; } }); +exports.VERSION = "1.0.0"; +// Default export. +exports.default = { + registry: registry_api_1.registry, + version: "1.0.0", +}; diff --git a/tests/fixtures/npm/node_modules/tscdefault/build/registry-api.d.ts b/tests/fixtures/npm/node_modules/tscdefault/build/registry-api.d.ts new file mode 100644 index 000000000..2010f655e --- /dev/null +++ b/tests/fixtures/npm/node_modules/tscdefault/build/registry-api.d.ts @@ -0,0 +1,2 @@ +import { Registry } from './registry'; +export declare const registry: Registry; diff --git a/tests/fixtures/npm/node_modules/tscdefault/build/registry-api.js b/tests/fixtures/npm/node_modules/tscdefault/build/registry-api.js new file mode 100644 index 000000000..209ad4e9a --- /dev/null +++ b/tests/fixtures/npm/node_modules/tscdefault/build/registry-api.js @@ -0,0 +1,5 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.registry = void 0; +const registry_1 = require("./registry"); +exports.registry = registry_1.Registry.getInstance(); diff --git a/tests/fixtures/npm/node_modules/tscdefault/build/registry.d.ts b/tests/fixtures/npm/node_modules/tscdefault/build/registry.d.ts new file mode 100644 index 000000000..e9957d79d --- /dev/null +++ b/tests/fixtures/npm/node_modules/tscdefault/build/registry.d.ts @@ -0,0 +1,5 @@ +export declare class Registry { + private counts; + static getInstance(): Registry; + count(name: string, n: number): number; +} diff --git a/tests/fixtures/npm/node_modules/tscdefault/build/registry.js b/tests/fixtures/npm/node_modules/tscdefault/build/registry.js new file mode 100644 index 000000000..2797e469a --- /dev/null +++ b/tests/fixtures/npm/node_modules/tscdefault/build/registry.js @@ -0,0 +1,20 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Registry = void 0; +class Registry { + constructor() { + this.counts = new Map(); + } + static getInstance() { + if (!this._instance) { + this._instance = new Registry(); + } + return this._instance; + } + count(name, n) { + const next = (this.counts.get(name) ?? 0) + n; + this.counts.set(name, next); + return next; + } +} +exports.Registry = Registry; diff --git a/tests/fixtures/npm/node_modules/tscdefault/package.json b/tests/fixtures/npm/node_modules/tscdefault/package.json new file mode 100644 index 000000000..230541aaf --- /dev/null +++ b/tests/fixtures/npm/node_modules/tscdefault/package.json @@ -0,0 +1,6 @@ +{ + "name": "tscdefault", + "version": "1.0.0", + "main": "build/index.js", + "types": "build/index.d.ts" +} diff --git a/tests/fixtures/npm/store/gtpattern@1.0.0/node_modules/gtpattern/lib/index.d.ts b/tests/fixtures/npm/store/gtpattern@1.0.0/node_modules/gtpattern/lib/index.d.ts new file mode 100644 index 000000000..1ba7851fa --- /dev/null +++ b/tests/fixtures/npm/store/gtpattern@1.0.0/node_modules/gtpattern/lib/index.d.ts @@ -0,0 +1,2 @@ +export declare const version: string; +export declare function compile(path: string): string; diff --git a/tests/fixtures/npm/store/gtpattern@1.0.0/node_modules/gtpattern/lib/index.js b/tests/fixtures/npm/store/gtpattern@1.0.0/node_modules/gtpattern/lib/index.js new file mode 100644 index 000000000..c76f7222f --- /dev/null +++ b/tests/fixtures/npm/store/gtpattern@1.0.0/node_modules/gtpattern/lib/index.js @@ -0,0 +1,5 @@ +"use strict"; +exports.version = "v1"; +exports.compile = function compile(path) { + return path.split("/").filter((part) => part.length > 0).join("."); +}; diff --git a/tests/fixtures/npm/store/gtpattern@1.0.0/node_modules/gtpattern/package.json b/tests/fixtures/npm/store/gtpattern@1.0.0/node_modules/gtpattern/package.json new file mode 100644 index 000000000..fd67b847e --- /dev/null +++ b/tests/fixtures/npm/store/gtpattern@1.0.0/node_modules/gtpattern/package.json @@ -0,0 +1,6 @@ +{ + "name": "gtpattern", + "version": "1.0.0", + "main": "lib/index.js", + "typings": "lib/index.d.ts" +} diff --git a/tests/fixtures/npm/store/gtpattern@2.0.0/node_modules/gtpattern/lib/index.d.ts b/tests/fixtures/npm/store/gtpattern@2.0.0/node_modules/gtpattern/lib/index.d.ts new file mode 100644 index 000000000..1ba7851fa --- /dev/null +++ b/tests/fixtures/npm/store/gtpattern@2.0.0/node_modules/gtpattern/lib/index.d.ts @@ -0,0 +1,2 @@ +export declare const version: string; +export declare function compile(path: string): string; diff --git a/tests/fixtures/npm/store/gtpattern@2.0.0/node_modules/gtpattern/lib/index.js b/tests/fixtures/npm/store/gtpattern@2.0.0/node_modules/gtpattern/lib/index.js new file mode 100644 index 000000000..a192ea3ab --- /dev/null +++ b/tests/fixtures/npm/store/gtpattern@2.0.0/node_modules/gtpattern/lib/index.js @@ -0,0 +1,5 @@ +"use strict"; +exports.version = "v2"; +exports.compile = function compile(path) { + return path.split("/").filter((part) => part.length > 0).map((part) => (part.startsWith(":") ? "<" + part.slice(1) + ">" : part)).join("/"); +}; diff --git a/tests/fixtures/npm/store/gtpattern@2.0.0/node_modules/gtpattern/package.json b/tests/fixtures/npm/store/gtpattern@2.0.0/node_modules/gtpattern/package.json new file mode 100644 index 000000000..6eb6c6d78 --- /dev/null +++ b/tests/fixtures/npm/store/gtpattern@2.0.0/node_modules/gtpattern/package.json @@ -0,0 +1,6 @@ +{ + "name": "gtpattern", + "version": "2.0.0", + "main": "lib/index.js", + "typings": "lib/index.d.ts" +} diff --git a/tests/fixtures/npm/store/gtroute@1.0.0/node_modules/gtpattern b/tests/fixtures/npm/store/gtroute@1.0.0/node_modules/gtpattern new file mode 120000 index 000000000..25c5c39d5 --- /dev/null +++ b/tests/fixtures/npm/store/gtroute@1.0.0/node_modules/gtpattern @@ -0,0 +1 @@ +../../gtpattern@1.0.0/node_modules/gtpattern \ No newline at end of file diff --git a/tests/fixtures/npm/store/gtroute@1.0.0/node_modules/gtpattern-next b/tests/fixtures/npm/store/gtroute@1.0.0/node_modules/gtpattern-next new file mode 120000 index 000000000..916b8c35f --- /dev/null +++ b/tests/fixtures/npm/store/gtroute@1.0.0/node_modules/gtpattern-next @@ -0,0 +1 @@ +../../gtpattern@2.0.0/node_modules/gtpattern \ No newline at end of file diff --git a/tests/fixtures/npm/store/gtroute@1.0.0/node_modules/gtroute/index.d.ts b/tests/fixtures/npm/store/gtroute@1.0.0/node_modules/gtroute/index.d.ts new file mode 100644 index 000000000..2d203bf4e --- /dev/null +++ b/tests/fixtures/npm/store/gtroute@1.0.0/node_modules/gtroute/index.d.ts @@ -0,0 +1 @@ +export declare function compile(path: string): string; diff --git a/tests/fixtures/npm/store/gtroute@1.0.0/node_modules/gtroute/index.js b/tests/fixtures/npm/store/gtroute@1.0.0/node_modules/gtroute/index.js new file mode 100644 index 000000000..da52d5e12 --- /dev/null +++ b/tests/fixtures/npm/store/gtroute@1.0.0/node_modules/gtroute/index.js @@ -0,0 +1,7 @@ +"use strict"; +const legacy = require("gtpattern"); +const next = require("gtpattern-next"); + +exports.compile = function compile(path) { + return legacy.version + ":" + legacy.compile(path) + " " + next.version + ":" + next.compile(path); +}; diff --git a/tests/fixtures/npm/store/gtroute@1.0.0/node_modules/gtroute/package.json b/tests/fixtures/npm/store/gtroute@1.0.0/node_modules/gtroute/package.json new file mode 100644 index 000000000..10bee06e7 --- /dev/null +++ b/tests/fixtures/npm/store/gtroute@1.0.0/node_modules/gtroute/package.json @@ -0,0 +1,11 @@ +{ + "name": "gtroute", + "version": "1.0.0", + "description": "depends on two releases of gtpattern, the newer one through an npm alias (\"gtpattern-next\": \"npm:gtpattern@2.0.0\"), in a pnpm store layout", + "main": "index.js", + "types": "index.d.ts", + "dependencies": { + "gtpattern": "1.0.0", + "gtpattern-next": "npm:gtpattern@2.0.0" + } +} diff --git a/tests/harness/__snapshots__/boot-path-node-surface-fences.ts.txt b/tests/harness/__snapshots__/boot-path-node-surface-fences.ts.txt new file mode 100644 index 000000000..c287a1641 --- /dev/null +++ b/tests/harness/__snapshots__/boot-path-node-surface-fences.ts.txt @@ -0,0 +1,35 @@ +boot-path-node-surface-fences.ts:8:14 - error SC2020: 'process.send' is part of the standard library types but has no scriptc lowering yet + + 7 | + 8 | const send = process.send; + | ^~~~~~~~~~~~ + 9 | console.log(typeof send); + + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + +boot-path-node-surface-fences.ts:11:1 - error SC2020: 'a '_write' method whose parameter 0 is 'number' where Node passes 'Uint8Array'' is part of the standard library types but has no scriptc lowering yet + + 10 | + 11 | class Counter extends Writable { + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 12 | _write(chunk: number, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + + hint: annotate the parameter with the Node signature's type or a supertype of it (chunks are Buffers here) + +boot-path-node-surface-fences.ts:22:31 - error SC2020: 'util.promisify of this target' is part of the standard library types but has no scriptc lowering yet + + 21 | } + 22 | const legacyAsync = promisify(legacy); + | ^~~~~~ + 23 | legacyAsync().then((v) => console.log(v)); + + hint: child_process.exec, child_process.execFile, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions (randomBytes, pbkdf2, scrypt, hkdf, randomFill, randomInt, generateKey), dns.lookup, dns.lookupService, the fs callback functions with an fs/promises counterpart, and typed callback-last functions whose callback takes (error?: Error | null, value?) are the supported targets + +boot-path-node-surface-fences.ts:23:1 - error SC2004: uses of 'legacyAsync' inherit the blocker on its declaration + + 22 | const legacyAsync = promisify(legacy); + 23 | legacyAsync().then((v) => console.log(v)); + | ^~~~~~~~~~~ + 24 | + + hint: the declaration of 'legacyAsync' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file diff --git a/tests/harness/__snapshots__/builtin-base-class-value-arguments.ts.txt b/tests/harness/__snapshots__/builtin-base-class-value-arguments.ts.txt new file mode 100644 index 000000000..5e3af4f45 --- /dev/null +++ b/tests/harness/__snapshots__/builtin-base-class-value-arguments.ts.txt @@ -0,0 +1,17 @@ +builtin-base-class-value-arguments.ts:9:34 - error SC2020: 'new Upper with arguments through an inherited stream constructor' is part of the standard library types but has no scriptc lowering yet + + 8 | const registry = { upper: Upper }; + 9 | const upper = new registry.upper({ objectMode: true }); + | ^~~~~~~~~~~~~~~~~~~~ + 10 | upper.destroy(); + + hint: declare a constructor that passes an inline options object to super(...) + +builtin-base-class-value-arguments.ts:13:24 - error SC2020: 'new Upper with arguments through an inherited stream constructor' is part of the standard library types but has no scriptc lowering yet + + 12 | const Kind = Upper; + 13 | const other = new Kind({ highWaterMark: 4 }); + | ^~~~~~~~~~~~~~~~~~~~ + 14 | other.destroy(); + + hint: declare a constructor that passes an inline options object to super(...) \ No newline at end of file diff --git a/tests/harness/__snapshots__/builtin-constructor-members.ts.txt b/tests/harness/__snapshots__/builtin-constructor-members.ts.txt new file mode 100644 index 000000000..c0e45edea --- /dev/null +++ b/tests/harness/__snapshots__/builtin-constructor-members.ts.txt @@ -0,0 +1,71 @@ +builtin-constructor-members.ts:5:22 - error SC2020: 'Map.prototype' is part of the standard library types but has no scriptc lowering yet + + 4 | // through globalThis or through an alias; it never reads undefined. + 5 | const mapPrototype = Map.prototype; + | ^~~~~~~~~~~~~ + 6 | const datePrototype = Date.prototype; + + hint: prototype objects are not values here (method lookup is static) — call the method on an instance instead + +builtin-constructor-members.ts:6:23 - error SC2020: 'Date.prototype' is part of the standard library types but has no scriptc lowering yet + + 5 | const mapPrototype = Map.prototype; + 6 | const datePrototype = Date.prototype; + | ^~~~~~~~~~~~~~ + 7 | const regExpPrototype = RegExp.prototype; + + hint: prototype objects are not values here (method lookup is static) — call the method on an instance instead + +builtin-constructor-members.ts:7:25 - error SC2020: 'RegExp.prototype' is part of the standard library types but has no scriptc lowering yet + + 6 | const datePrototype = Date.prototype; + 7 | const regExpPrototype = RegExp.prototype; + | ^~~~~~~~~~~~~~~~ + 8 | const promisePrototype = Promise.prototype; + + hint: prototype objects are not values here (method lookup is static) — call the method on an instance instead + +builtin-constructor-members.ts:8:26 - error SC2020: 'Promise.prototype' is part of the standard library types but has no scriptc lowering yet + + 7 | const regExpPrototype = RegExp.prototype; + 8 | const promisePrototype = Promise.prototype; + | ^~~~~~~~~~~~~~~~~ + 9 | const weakSetPrototype = WeakSet.prototype; + + hint: prototype objects are not values here (method lookup is static) — call the method on an instance instead + +builtin-constructor-members.ts:9:26 - error SC2020: 'WeakSet.prototype' is part of the standard library types but has no scriptc lowering yet + + 8 | const promisePrototype = Promise.prototype; + 9 | const weakSetPrototype = WeakSet.prototype; + | ^~~~~~~~~~~~~~~~~ + 10 | const frozen = Object.isFrozen; + + hint: prototype objects are not values here (method lookup is static) — call the method on an instance instead + +builtin-constructor-members.ts:10:16 - error SC2020: 'Object.isFrozen' is part of the standard library types but has no scriptc lowering yet + + 9 | const weakSetPrototype = WeakSet.prototype; + 10 | const frozen = Object.isFrozen; + | ^~~~~~~~~~~~~~~ + 11 | const setPrototype = globalThis.Set.prototype; + + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + +builtin-constructor-members.ts:11:22 - error SC2020: 'SetConstructor.prototype' is part of the standard library types but has no scriptc lowering yet + + 10 | const frozen = Object.isFrozen; + 11 | const setPrototype = globalThis.Set.prototype; + | ^~~~~~~~~~~~~~~~~~~~~~~~ + 12 | const D = Date; + + hint: prototype objects are not values here (method lookup is static) — call the method on an instance instead + +builtin-constructor-members.ts:13:24 - error SC2020: 'DateConstructor.prototype' is part of the standard library types but has no scriptc lowering yet + + 12 | const D = Date; + 13 | const aliasPrototype = D.prototype; + | ^~~~~~~~~~~ + 14 | // End with a nonblank context line so diagnostic snapshots carry no trailing whitespace. + + hint: prototype objects are not values here (method lookup is static) — call the method on an instance instead \ No newline at end of file diff --git a/tests/harness/__snapshots__/class-field-accessor-descriptor.ts.txt b/tests/harness/__snapshots__/class-field-accessor-descriptor.ts.txt new file mode 100644 index 000000000..986e0d023 --- /dev/null +++ b/tests/harness/__snapshots__/class-field-accessor-descriptor.ts.txt @@ -0,0 +1,6 @@ +class-field-accessor-descriptor.ts:9:1 - error SC2020: Object.defineProperty with an accessor descriptor for the declared field 'x' is not supported yet + + 8 | const value = new Value(); + 9 | Object.defineProperty(value, "x", { + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 10 | get() { \ No newline at end of file diff --git a/tests/harness/__snapshots__/coverage-divergences.txt b/tests/harness/__snapshots__/coverage-divergences.txt index 2f811f892..66373655f 100644 --- a/tests/harness/__snapshots__/coverage-divergences.txt +++ b/tests/harness/__snapshots__/coverage-divergences.txt @@ -6,7 +6,7 @@ scriptc coverage tests/coverage-fixtures/divergences.ts fully static — all analyzed statements compile to native code. - differs from Node 8 sites (compiles; the native program can behave differently) + differs from Node 5 sites (compiles; the native program can behave differently) ×1 SC6001 passing a 'Tagged' where 'Labeled' is expected copies the object (converting to a record type with other properties compiles as a field copy): the write to 'target.label' updates only the copy, while… at tests/coverage-fixtures/divergences.ts:16:9 hint: declare the parameter or binding with the value's full type ('Tagged'), or make the shape a class — class instances keep their identity when passed as a base class @@ -21,13 +21,4 @@ scriptc coverage tests/coverage-fixtures/divergences.ts hint: results agree with Node for Latin-script text; for other scripts compare explicitly (for example by code point with < and >) so both runtimes agree ×1 SC6004 this 'Circle' reaches a cast to the unrelated class 'Square' in the callee: natively the checked cast throws a TypeError when it executes, while Node reads the members structurally at tests/coverage-fixtures/divergences.ts:48:20 - hint: narrow with 'instanceof Square' before casting, or cast to a base class or interface both classes share - ×1 SC6005 the date string 'Jan 2, 2024' parses to an invalid date (NaN) natively, while Node accepts it (the native parser takes ISO 8601 date-times with an explicit offset, such as '2024-01-02T03:04:05Z', and… - at tests/coverage-fixtures/divergences.ts:51:24 - hint: write the date in ISO 8601 with an explicit offset ('Z' or '+HH:MM'), or build it from numbers with Date.UTC - ×1 SC6005 the date string '2024/01/02' parses to an invalid date (NaN) natively, while Node accepts it (the native parser takes ISO 8601 date-times with an explicit offset, such as '2024-01-02T03:04:05Z', and … - at tests/coverage-fixtures/divergences.ts:51:49 - hint: write the date in ISO 8601 with an explicit offset ('Z' or '+HH:MM'), or build it from numbers with Date.UTC - ×1 SC6005 the date string 'March 4, 2024' parses to an invalid date (NaN) natively, while Node accepts it (the native parser takes ISO 8601 date-times with an explicit offset, such as '2024-01-02T03:04:05Z', a… - at tests/coverage-fixtures/divergences.ts:52:41 - hint: write the date in ISO 8601 with an explicit offset ('Z' or '+HH:MM'), or build it from numbers with Date.UTC \ No newline at end of file + hint: narrow with 'instanceof Square' before casting, or cast to a base class or interface both classes share \ No newline at end of file diff --git a/tests/harness/__snapshots__/inferred-callback-drops-conditional.ts.txt b/tests/harness/__snapshots__/inferred-callback-drops-conditional.ts.txt new file mode 100644 index 000000000..a14f19317 --- /dev/null +++ b/tests/harness/__snapshots__/inferred-callback-drops-conditional.ts.txt @@ -0,0 +1,6 @@ +inferred-callback-drops-conditional.ts:10:32 - error SC1090: returning '{ extra: boolean; n: number; name: string }[]' from a callback whose result the checker infers as the narrower '{ n: number; name: string }[]' (the conversion would drop 'extra', which the object keeps at run time — annotate the callback's return type or the call's type argument with a type that declares them) is not supported yet + + 9 | const items: Item[] = run(() => + 10 | plain.length === 0 ? plain : plain.map((x) => ({ ...x, extra: true })), + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 11 | ); \ No newline at end of file diff --git a/tests/harness/__snapshots__/inferred-callback-drops-satisfies.ts.txt b/tests/harness/__snapshots__/inferred-callback-drops-satisfies.ts.txt new file mode 100644 index 000000000..ea1e37549 --- /dev/null +++ b/tests/harness/__snapshots__/inferred-callback-drops-satisfies.ts.txt @@ -0,0 +1,6 @@ +inferred-callback-drops-satisfies.ts:11:10 - error SC1090: returning '{ extra: boolean; n: number; name: string }[]' from a callback whose result the checker infers as the narrower '{ n: number; name: string }[]' (the conversion would drop 'extra', which the object keeps at run time — annotate the callback's return type or the call's type argument with a type that declares them) is not supported yet + + 10 | if (plain.length === 0) return plain; + 11 | return plain.map((x) => ({ ...x, extra: true })) satisfies Plain[]; + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 12 | }); \ No newline at end of file diff --git a/tests/harness/__snapshots__/inferred-callback-drops.ts.txt b/tests/harness/__snapshots__/inferred-callback-drops.ts.txt new file mode 100644 index 000000000..daa62f484 --- /dev/null +++ b/tests/harness/__snapshots__/inferred-callback-drops.ts.txt @@ -0,0 +1,6 @@ +inferred-callback-drops.ts:11:10 - error SC1090: returning '{ extra: boolean; n: number; name: string }[]' from a callback whose result the checker infers as the narrower '{ n: number; name: string }[]' (the conversion would drop 'extra', which the object keeps at run time — annotate the callback's return type or the call's type argument with a type that declares them) is not supported yet + + 10 | if (plain.length === 0) return plain; + 11 | return plain.map((x) => ({ ...x, extra: true })); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 12 | }); \ No newline at end of file diff --git a/tests/harness/__snapshots__/maybe-promise-fences.ts.txt b/tests/harness/__snapshots__/maybe-promise-fences.ts.txt index 58fb23517..159e88f76 100644 --- a/tests/harness/__snapshots__/maybe-promise-fences.ts.txt +++ b/tests/harness/__snapshots__/maybe-promise-fences.ts.txt @@ -55,11 +55,4 @@ maybe-promise-fences.ts:32:13 - error SC1090: 'instanceof Promise' on '{ v: numb 31 | const record = { v: 1 }; 32 | console.log(record instanceof Promise); | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 33 | - -maybe-promise-fences.ts:36:26 - error SC1090: '(unknown) => Promise' values where '() => Promise' is expected is not supported yet - - 35 | type HandlerFn = (...args: unknown[]) => Promise; - 36 | const fixed: HandlerFn = async (first: unknown) => first; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 37 | fixed(1, 2).then((v) => console.log(v)); \ No newline at end of file + 33 | \ No newline at end of file diff --git a/tests/harness/__snapshots__/mock-fences.ts.txt b/tests/harness/__snapshots__/mock-fences.ts.txt index 6b4defcbb..d4b8115e8 100644 --- a/tests/harness/__snapshots__/mock-fences.ts.txt +++ b/tests/harness/__snapshots__/mock-fences.ts.txt @@ -1,62 +1,62 @@ -mock-fences.ts:14:15 - error SC2020: 'mock.fn of this function type' is part of the standard library types but has no scriptc lowering yet +mock-fences.ts:18:17 - error SC2020: 'mock.fn() of a function returning 'number'' is part of the standard library types but has no scriptc lowering yet - 13 | - 14 | const empty = mock.fn(); // no original function - | ^~~~~~~~~ - 15 | empty(); + 17 | + 18 | const counted = mock.fn<() => number>(); // no original, returns number + | ^~~~~~~~~~~~~~~~~~~~~~~ + 19 | counted(); - hint: mock functions lower for functions with fixed parameters, or a single rest array parameter, and an implementation of the same type + hint: mock.fn() without an original returns undefined: pass an original, or use a function type whose return type includes undefined -mock-fences.ts:27:13 - error SC2020: 'mock.method on 'Counter'' is part of the standard library types but has no scriptc lowering yet +mock-fences.ts:31:13 - error SC2020: 'mock.method on 'Counter'' is part of the standard library types but has no scriptc lowering yet - 26 | } - 27 | mock.method(new Counter(), "count"); // class instance + 30 | } + 31 | mock.method(new Counter(), "count"); // class instance | ^~~~~~~~~~~~~ - 28 | mock.method(console, "log"); // builtin object + 32 | mock.method(console, "log"); // builtin object hint: mock.method lowers for objects whose type maps to a record (object literals and interface-typed objects) and a function-valued field named by a string literal -mock-fences.ts:28:13 - error SC2020: 'mock.method on this object' is part of the standard library types but has no scriptc lowering yet +mock-fences.ts:32:13 - error SC2020: 'mock.method on this object' is part of the standard library types but has no scriptc lowering yet - 27 | mock.method(new Counter(), "count"); // class instance - 28 | mock.method(console, "log"); // builtin object + 31 | mock.method(new Counter(), "count"); // class instance + 32 | mock.method(console, "log"); // builtin object | ^~~~~~~ - 29 | mock.method(obj, "value", { times: 1 }); // options + 33 | mock.method(obj, "value", { times: 1 }); // options hint: mock.method lowers for objects whose type maps to a record (object literals and interface-typed objects) and a function-valued field named by a string literal -mock-fences.ts:29:27 - error SC2020: 'mock.method with an options object' is part of the standard library types but has no scriptc lowering yet +mock-fences.ts:33:27 - error SC2020: 'mock.method with an options object' is part of the standard library types but has no scriptc lowering yet - 28 | mock.method(console, "log"); // builtin object - 29 | mock.method(obj, "value", { times: 1 }); // options + 32 | mock.method(console, "log"); // builtin object + 33 | mock.method(obj, "value", { times: 1 }); // options | ^~~~~~~~~~~~ - 30 | const other = mock.method(obj, "value", () => "one"); // implementation of another type + 34 | const other = mock.method(obj, "value", () => "one"); // implementation of another type hint: mock options (times, getter and setter) have no lowering -mock-fences.ts:30:41 - error SC2020: 'mock.method with an implementation of another type' is part of the standard library types but has no scriptc lowering yet +mock-fences.ts:34:41 - error SC2020: 'mock.method with an implementation of another type' is part of the standard library types but has no scriptc lowering yet - 29 | mock.method(obj, "value", { times: 1 }); // options - 30 | const other = mock.method(obj, "value", () => "one"); // implementation of another type + 33 | mock.method(obj, "value", { times: 1 }); // options + 34 | const other = mock.method(obj, "value", () => "one"); // implementation of another type | ^~~~~~~~~~~ - 31 | other(); + 35 | other(); hint: give the implementation the method's parameter and return types, or discard the result -mock-fences.ts:35:31 - error SC2020: 't.mock.method with an options object' is part of the standard library types but has no scriptc lowering yet +mock-fences.ts:39:31 - error SC2020: 't.mock.method with an options object' is part of the standard library types but has no scriptc lowering yet - 34 | t.mock.method(obj, "value"); // lowers - 35 | t.mock.method(obj, "value", { times: 2 }); // options + 38 | t.mock.method(obj, "value"); // lowers + 39 | t.mock.method(obj, "value", { times: 2 }); // options | ^~~~~~~~~~~~ - 36 | }); + 40 | }); hint: mock options (times, getter and setter) have no lowering -mock-fences.ts:38:14 - error SC2020: 'mock.fn of a function returning 'number | string'' is part of the standard library types but has no scriptc lowering yet +mock-fences.ts:42:14 - error SC2020: 'mock.fn of a function returning 'number | string'' is part of the standard library types but has no scriptc lowering yet - 37 | - 38 | const pick = mock.fn((flag: boolean): number | string => (flag ? 1 : "one")); // union without undefined + 41 | + 42 | const pick = mock.fn((flag: boolean): number | string => (flag ? 1 : "one")); // union without undefined | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 39 | pick(true); + 43 | pick(true); hint: a union return type needs an undefined arm here (the calls log records undefined for calls that throw) \ No newline at end of file diff --git a/tests/harness/__snapshots__/object-literal-accessor-values.ts.txt b/tests/harness/__snapshots__/object-literal-accessor-values.ts.txt index 89bc092aa..372849388 100644 --- a/tests/harness/__snapshots__/object-literal-accessor-values.ts.txt +++ b/tests/harness/__snapshots__/object-literal-accessor-values.ts.txt @@ -1,24 +1,6 @@ -object-literal-accessor-values.ts:18:28 - error SC1090: JSON.stringify of '{ get doubled(): number; count: number }' values (only number, string, boolean, records, arrays, unions of those, and 'unknown' stringify) are not supported yet +object-literal-accessor-values.ts:13:28 - error SC1090: JSON.stringify of '{ get doubled(): number; count: number }' values (only number, string, boolean, records, arrays, unions of those, and 'unknown' stringify) are not supported yet - 17 | // JSON.stringify of the typed value would omit the getter's property. - 18 | console.log(JSON.stringify(counter)); + 12 | // JSON.stringify of the typed value would omit the getter's property. + 13 | console.log(JSON.stringify(counter)); | ^~~~~~~ - 19 | - -object-literal-accessor-values.ts:21:6 - error SC1101: converting typed values to 'unknown' is not supported yet - - 20 | // Converting the typed value to `unknown` or `any` would lose the getter. - 21 | hold(counter); - | ^~~~~~~ - 22 | const viaAny: any = counter; - - hint: numbers, strings, booleans, JSON-safe records/arrays/unions (a deep copy), functions, program class instances, and supported native handles convert into 'unknown' slots; functions, exact-class round trips, and handles preserve identity, while this value's type has no dynamic representation yet - -object-literal-accessor-values.ts:22:21 - error SC1101: converting typed values to 'unknown' is not supported yet - - 21 | hold(counter); - 22 | const viaAny: any = counter; - | ^~~~~~~ - 23 | console.log(viaAny); - - hint: numbers, strings, booleans, JSON-safe records/arrays/unions (a deep copy), functions, program class instances, and supported native handles convert into 'unknown' slots; functions, exact-class round trips, and handles preserve identity, while this value's type has no dynamic representation yet \ No newline at end of file + 14 | \ No newline at end of file diff --git a/tests/harness/__snapshots__/promise-all-fences.ts.txt b/tests/harness/__snapshots__/promise-all-fences.ts.txt index 8d17f3f57..18018a8b5 100644 --- a/tests/harness/__snapshots__/promise-all-fences.ts.txt +++ b/tests/harness/__snapshots__/promise-all-fences.ts.txt @@ -1,26 +1,17 @@ -promise-all-fences.ts:6:34 - error SC2020: 'Promise.all over this argument shape' is part of the standard library types but has no scriptc lowering yet +promise-all-fences.ts:7:44 - error SC2020: 'Promise.allSettled over this argument shape' is part of the standard library types but has no scriptc lowering yet - 5 | async function hetero(): Promise { - 6 | const pair = await Promise.all([ - | ^ - 7 | new Promise((resolve) => resolve("s")), - - hint: the entries must share ONE promise type — Promise.all([p, q]) lowers when p and q are the same Promise - -promise-all-fences.ts:13:44 - error SC2020: 'Promise.allSettled over this argument shape' is part of the standard library types but has no scriptc lowering yet - - 12 | async function settled(): Promise { - 13 | const results = await Promise.allSettled([ - | ^ - 14 | new Promise((resolve) => resolve("s")), + 6 | async function settled(): Promise { + 7 | const results = await Promise.allSettled([ + | ^ + 8 | new Promise((resolve) => resolve("s")), hint: the entries must share ONE promise type — Promise.allSettled([p, q]) lowers when p and q are the same Promise -promise-all-fences.ts:23:33 - error SC2020: 'Promise.any over this argument shape' is part of the standard library types but has no scriptc lowering yet +promise-all-fences.ts:17:33 - error SC2020: 'Promise.any over this argument shape' is part of the standard library types but has no scriptc lowering yet - 22 | async function anyOfGenerator(): Promise { - 23 | console.log(await Promise.any(pending())); + 16 | async function anyOfGenerator(): Promise { + 17 | console.log(await Promise.any(pending())); | ^~~~~~~~~ - 24 | } + 18 | } hint: an array of promises (Promise[]) is the supported form \ No newline at end of file diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index b813840d8..b0ebed494 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -50,14 +50,14 @@ stdlib-fence.ts:20:13 - error SC2020: 'new Intl.NumberFormat' is part of the sta hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) -stdlib-fence.ts:23:11 - error SC2020: 'new Date with 2 arguments' is part of the standard library types but has no scriptc lowering yet +stdlib-fence.ts:23:11 - error SC2020: 'new Date with spread arguments' is part of the standard library types but has no scriptc lowering yet 22 | // Constructors. - 23 | const d = new Date(2024, 0); // the local-time year/month field constructor stays fenced - | ^~~~~~~~~~~~~~~~~ + 23 | const d = new Date(...([2024, 0] as const)); // spread field arguments stay fenced + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 24 | const mutableDate = new Date(0); - hint: new Date(), new Date(milliseconds), and new Date(dateString) are supported; the local-time year/month field constructor has no lowering + hint: new Date(), new Date(milliseconds), new Date(dateString), and new Date(year, monthIndex, ...) with listed arguments are supported stdlib-fence.ts:25:1 - error SC2020: 'Date.prototype.setUTCFullYear with arguments' is part of the standard library types but has no scriptc lowering yet diff --git a/tests/harness/__snapshots__/stream-completion-callback-error.ts.txt b/tests/harness/__snapshots__/stream-completion-callback-error.ts.txt new file mode 100644 index 000000000..f3b876cc7 --- /dev/null +++ b/tests/harness/__snapshots__/stream-completion-callback-error.ts.txt @@ -0,0 +1,17 @@ +stream-completion-callback-error.ts:10:17 - error SC2020: 'write completion callbacks taking the error argument' is part of the standard library types but has no scriptc lowering yet + + 9 | }); + 10 | sink.write("a", (err) => console.log("written", err)); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 11 | sink.end((err?: Error | null) => console.log("ended", err)); + + hint: () => void callbacks are supported (listen for 'error' to observe failures) + +stream-completion-callback-error.ts:11:10 - error SC2020: 'end callbacks with parameters' is part of the standard library types but has no scriptc lowering yet + + 10 | sink.write("a", (err) => console.log("written", err)); + 11 | sink.end((err?: Error | null) => console.log("ended", err)); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 12 | + + hint: () => void callbacks are supported \ No newline at end of file diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 2cecccc59..85e4ba4aa 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2231,6 +2231,8 @@ "6001-json-stringify-js-class-instances.js": 4668, "6002-inspect-class-added-properties.ts": 6044, "6003-inspect-js-class-properties.js": 2752, + "6004-class-instance-integrity.ts": 10548, + "6005-class-instance-integrity-sloppy.cjs": 7466, "601-closures-loops.ts": 1788, "6010-match-metadata.ts": 3990, "6011-match-metadata-js.js": 3660, @@ -5326,6 +5328,8 @@ "6001-json-stringify-js-class-instances.js": 7469, "6002-inspect-class-added-properties.ts": 9670, "6003-inspect-js-class-properties.js": 4403, + "6004-class-instance-integrity.ts": 16877, + "6005-class-instance-integrity-sloppy.cjs": 11946, "601-closures-loops.ts": 2297, "6010-match-metadata.ts": 5450, "6011-match-metadata-js.js": 4890, diff --git a/tests/harness/corpus-costs/5779-sha512-lengths.ts.json b/tests/harness/corpus-costs/5779-sha512-lengths.ts.json new file mode 100644 index 000000000..9e04a316c --- /dev/null +++ b/tests/harness/corpus-costs/5779-sha512-lengths.ts.json @@ -0,0 +1 @@ +{"plain":2610,"san":3540} diff --git a/tests/harness/corpus-costs/5816-fixed-into-unknown-rest.ts.json b/tests/harness/corpus-costs/5816-fixed-into-unknown-rest.ts.json new file mode 100644 index 000000000..2b149eb6a --- /dev/null +++ b/tests/harness/corpus-costs/5816-fixed-into-unknown-rest.ts.json @@ -0,0 +1 @@ +{ "plain": 3028, "san": 5470 } diff --git a/tests/harness/corpus-costs/5916-dyn-error-stack-property.ts.json b/tests/harness/corpus-costs/5916-dyn-error-stack-property.ts.json new file mode 100644 index 000000000..3ed0acb36 --- /dev/null +++ b/tests/harness/corpus-costs/5916-dyn-error-stack-property.ts.json @@ -0,0 +1 @@ +{ "plain": 3232, "san": 4298 } diff --git a/tests/harness/corpus-costs/6055-class-generator-iterables.ts.json b/tests/harness/corpus-costs/6055-class-generator-iterables.ts.json new file mode 100644 index 000000000..68b6f9db4 --- /dev/null +++ b/tests/harness/corpus-costs/6055-class-generator-iterables.ts.json @@ -0,0 +1 @@ +{ "plain": 4921, "san": 6451 } diff --git a/tests/harness/corpus-costs/6056-class-symbol-method-calls.ts.json b/tests/harness/corpus-costs/6056-class-symbol-method-calls.ts.json new file mode 100644 index 000000000..4207a0ed0 --- /dev/null +++ b/tests/harness/corpus-costs/6056-class-symbol-method-calls.ts.json @@ -0,0 +1 @@ +{ "plain": 3468, "san": 5120 } diff --git a/tests/harness/corpus-costs/6230-date-parse-legacy.ts.json b/tests/harness/corpus-costs/6230-date-parse-legacy.ts.json new file mode 100644 index 000000000..38848e321 --- /dev/null +++ b/tests/harness/corpus-costs/6230-date-parse-legacy.ts.json @@ -0,0 +1 @@ +{ "plain": 2490, "san": 3133 } diff --git a/tests/harness/corpus-costs/6231-date-local-fields.ts.json b/tests/harness/corpus-costs/6231-date-local-fields.ts.json new file mode 100644 index 000000000..900da7a06 --- /dev/null +++ b/tests/harness/corpus-costs/6231-date-local-fields.ts.json @@ -0,0 +1 @@ +{ "plain": 2472, "san": 3365 } diff --git a/tests/harness/corpus-costs/6232-date-fields-js.cjs.json b/tests/harness/corpus-costs/6232-date-fields-js.cjs.json new file mode 100644 index 000000000..dce549ea0 --- /dev/null +++ b/tests/harness/corpus-costs/6232-date-fields-js.cjs.json @@ -0,0 +1 @@ +{ "plain": 2875, "san": 3637 } diff --git a/tests/harness/corpus-costs/6233-object-identity-dynamic.cjs.json b/tests/harness/corpus-costs/6233-object-identity-dynamic.cjs.json new file mode 100644 index 000000000..18fc9038e --- /dev/null +++ b/tests/harness/corpus-costs/6233-object-identity-dynamic.cjs.json @@ -0,0 +1 @@ +{ "plain": 4636, "san": 4061 } diff --git a/tests/harness/corpus-costs/6234-object-prototype-values.cjs.json b/tests/harness/corpus-costs/6234-object-prototype-values.cjs.json new file mode 100644 index 000000000..2681d28e6 --- /dev/null +++ b/tests/harness/corpus-costs/6234-object-prototype-values.cjs.json @@ -0,0 +1 @@ +{ "plain": 3149, "san": 3979 } diff --git a/tests/harness/corpus-costs/6235-object-identity-typed.ts.json b/tests/harness/corpus-costs/6235-object-identity-typed.ts.json new file mode 100644 index 000000000..2929ddfc7 --- /dev/null +++ b/tests/harness/corpus-costs/6235-object-identity-typed.ts.json @@ -0,0 +1 @@ +{ "plain": 2802, "san": 3441 } diff --git a/tests/harness/corpus-costs/6236-builtin-function-values.cjs.json b/tests/harness/corpus-costs/6236-builtin-function-values.cjs.json new file mode 100644 index 000000000..c01634b4a --- /dev/null +++ b/tests/harness/corpus-costs/6236-builtin-function-values.cjs.json @@ -0,0 +1 @@ +{ "plain": 3572, "san": 4025 } diff --git a/tests/harness/corpus-costs/6400-dyn-error-stack-property-js.js.json b/tests/harness/corpus-costs/6400-dyn-error-stack-property-js.js.json new file mode 100644 index 000000000..c51a3475f --- /dev/null +++ b/tests/harness/corpus-costs/6400-dyn-error-stack-property-js.js.json @@ -0,0 +1 @@ +{ "plain": 3172, "san": 4044 } diff --git a/tests/harness/corpus-costs/6401-dyn-error-own-property-names.ts.json b/tests/harness/corpus-costs/6401-dyn-error-own-property-names.ts.json new file mode 100644 index 000000000..f85c4901a --- /dev/null +++ b/tests/harness/corpus-costs/6401-dyn-error-own-property-names.ts.json @@ -0,0 +1 @@ +{ "plain": 3175, "san": 4362 } diff --git a/tests/harness/corpus-costs/6402-dyn-error-own-property-names-js.js.json b/tests/harness/corpus-costs/6402-dyn-error-own-property-names-js.js.json new file mode 100644 index 000000000..55012b442 --- /dev/null +++ b/tests/harness/corpus-costs/6402-dyn-error-own-property-names-js.js.json @@ -0,0 +1 @@ +{ "plain": 3056, "san": 4620 } diff --git a/tests/harness/corpus-costs/6403-error-own-name-order.ts.json b/tests/harness/corpus-costs/6403-error-own-name-order.ts.json new file mode 100644 index 000000000..882c13186 --- /dev/null +++ b/tests/harness/corpus-costs/6403-error-own-name-order.ts.json @@ -0,0 +1 @@ +{ "plain": 3289, "san": 4302 } diff --git a/tests/harness/corpus-costs/6404-error-own-name-order-js.js.json b/tests/harness/corpus-costs/6404-error-own-name-order-js.js.json new file mode 100644 index 000000000..05ac342cb --- /dev/null +++ b/tests/harness/corpus-costs/6404-error-own-name-order-js.js.json @@ -0,0 +1 @@ +{ "plain": 3030, "san": 3963 } diff --git a/tests/harness/corpus-costs/6445-rest-function-fixed-slot.ts.json b/tests/harness/corpus-costs/6445-rest-function-fixed-slot.ts.json new file mode 100644 index 000000000..858c662a8 --- /dev/null +++ b/tests/harness/corpus-costs/6445-rest-function-fixed-slot.ts.json @@ -0,0 +1 @@ +{ "plain": 2526, "san": 3450 } diff --git a/tests/harness/corpus-costs/6446-rest-function-nested-declaration.ts.json b/tests/harness/corpus-costs/6446-rest-function-nested-declaration.ts.json new file mode 100644 index 000000000..39f698b22 --- /dev/null +++ b/tests/harness/corpus-costs/6446-rest-function-nested-declaration.ts.json @@ -0,0 +1 @@ +{ "plain": 2222, "san": 3035 } diff --git a/tests/harness/corpus-costs/comma-null-union-return.cjs.json b/tests/harness/corpus-costs/comma-null-union-return.cjs.json new file mode 100644 index 000000000..843c2ab0b --- /dev/null +++ b/tests/harness/corpus-costs/comma-null-union-return.cjs.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 3000 } diff --git a/tests/harness/corpus-costs/local-class-detached-method.ts.json b/tests/harness/corpus-costs/local-class-detached-method.ts.json new file mode 100644 index 000000000..0de92c93b --- /dev/null +++ b/tests/harness/corpus-costs/local-class-detached-method.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3900 } diff --git a/tests/harness/corpus-costs/object-literal-accessors-dyn.ts.json b/tests/harness/corpus-costs/object-literal-accessors-dyn.ts.json new file mode 100644 index 000000000..9643a761b --- /dev/null +++ b/tests/harness/corpus-costs/object-literal-accessors-dyn.ts.json @@ -0,0 +1 @@ +{ "plain": 2903, "san": 3777 } diff --git a/tests/harness/corpus-costs/required-module-truthiness.cjs.json b/tests/harness/corpus-costs/required-module-truthiness.cjs.json new file mode 100644 index 000000000..efd5011d1 --- /dev/null +++ b/tests/harness/corpus-costs/required-module-truthiness.cjs.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 3700 } diff --git a/tests/harness/corpus-costs/strict-arguments-parameters.cjs.json b/tests/harness/corpus-costs/strict-arguments-parameters.cjs.json new file mode 100644 index 000000000..adc7f5470 --- /dev/null +++ b/tests/harness/corpus-costs/strict-arguments-parameters.cjs.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 3300 } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index b1e25dead..a23d4789d 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -82,9 +82,6 @@ test("divergences report as warnings, never as blockers", async () => { "SC6002", "SC6003", "SC6004", - "SC6005", - "SC6005", - "SC6005", ]); expect(coveragePasses(coverage, "blockers")).toBe(true); expect(coveragePasses(coverage, "divergences")).toBe(false); @@ -256,11 +253,11 @@ test("CLI: build --print=diagnostics reports failures and divergence warnings as }; expect(envelope.success).toBe(true); expect(envelope.artifact).toBe(join(outDir, "out.ir.json")); - expect(envelope.diagnostics.map((d) => d.severity)).toEqual(Array(8).fill("warning")); + expect(envelope.diagnostics.map((d) => d.severity)).toEqual(Array(5).fill("warning")); const human = build(fixture("divergences.ts")); expect(human.status).toBe(0); expect(human.stderr).toContain("warning SC6001"); - expect(human.stderr).toContain("8 warnings: the program compiled"); + expect(human.stderr).toContain("5 warnings: the program compiled"); } finally { rmSync(outDir, { recursive: true, force: true }); } diff --git a/tests/harness/diagnostics.test.ts b/tests/harness/diagnostics.test.ts index 57ac8c6c0..77fd5937d 100644 --- a/tests/harness/diagnostics.test.ts +++ b/tests/harness/diagnostics.test.ts @@ -33,7 +33,17 @@ describe(`diagnostics corpus (${files.length} programs)`, () => { // any backend runs (that is the suite's contract), so the default // lane is irrelevant by construction — and riding it proves the // fallback machinery never turns a diagnostic into a build. - const result = await compile(file, { outPath: join(outDir, "never"), outDir }); + // The checker-panic programs exercise tsgo's own failures (a panic, and a + // +Infinity it cannot send), so they compile with tsgo. + const checker = process.env["SCRIPTC_CHECKER"]; + if (name.startsWith("checker-panic-")) process.env["SCRIPTC_CHECKER"] = "tsgo"; + let result: Awaited>; + try { + result = await compile(file, { outPath: join(outDir, "never"), outDir }); + } finally { + if (checker === undefined) delete process.env["SCRIPTC_CHECKER"]; + else process.env["SCRIPTC_CHECKER"] = checker; + } if (result.ok) { expect.unreachable(`${name} compiled successfully but must produce diagnostics`); } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 1f3d9729b..6d93675ef 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -359,6 +359,37 @@ const arr = tuple.map(() => new Date(0));`, expect(r).toEqual({ stdout: reference.stdout, stderr: reference.stderr, exitCode: 0 }); }); + test.each([ + ["an argument", "make(Upper, { objectMode: true });"], + ["a spread argument", "spread(Upper, [{ objectMode: true }]);"], + ])( + "%s through an untyped factory to an inherited stream constructor is a named deferred fence (JS lane)", + async (_, call) => { + // The constructor a ctor-less stream subclass inherits takes no + // arguments here; typed construction refuses them at compile time, + // and construction from untyped code refuses them at run time + // instead of constructing without them. + const r = await compileAndRun( + `inherited-stream-ctor-${call.startsWith("make") ? "args" : "spread"}`, + `'use strict'; +const { Transform } = require('node:stream'); +class Upper extends Transform {} +function make(F, options) { return new F(options); } +function bare(F) { return new F(); } +function spread(F, args) { return new F(...args); } +console.log('bare', bare(Upper) instanceof Upper, spread(Upper, []) instanceof Upper); +${call} +`, + "cjs", + ); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe("bare true true\n"); + expect(r.stderr).toMatch( + /^Uncaught Error: new Upper with arguments through an inherited stream constructor .* is not supported yet \[SC1090 at .*inherited-stream-ctor-\w+\.cjs:3\]\n$/, + ); + }, + ); + test("extending a property-assigned class ABOVE its assignment is a named deferred fence (JS lane)", async () => { // The property-assigned class-expression family (corpus 2032/2033 // pins what lowers): `exports.O = class extends exports.I {}` with @@ -1470,6 +1501,70 @@ console.log(count(require("./lib.cjs"))); }); }); +describe("builtin constructor values (JS lane)", () => { + test("members without a lowering stay refused instead of reading undefined", async () => { + // Builtin constructors read as interned function values that model only + // name, length and constructor. Every other static member or prototype + // read (direct, bracketed, `in`, through an alias or globalThis), and + // reflection on or redefinition of their members, must stay a located + // SC2020/SC1090 refusal, never an undefined or otherwise wrong value. + // Test262 lost 769 refusals to undefined reads before this was pinned. + const reads = [ + ["Map.prototype", "Map.prototype"], + ["Date.prototype", "Date.prototype"], + ["RegExp.prototype", "RegExp.prototype"], + ["Set.prototype", "Set.prototype"], + ["Promise.prototype", "Promise.prototype"], + ["WeakMap.prototype", "WeakMap.prototype"], + ["WeakSet.prototype", "WeakSet.prototype"], + ["Date.prototype.getYear", "Date.prototype.getYear"], + ["Map.prototype.constructor", "Map.prototype.constructor"], + ["Object.isFrozen", "Object.isFrozen"], + [ + "getOwnPropertyDescriptor(Map.prototype)", + 'Object.getOwnPropertyDescriptor(Map.prototype, "size")', + ], + ["isExtensible(Date.prototype)", "Object.isExtensible(Date.prototype)"], + ["Map['prototype']", 'Map["prototype"]'], + ["'prototype' in Map", '"prototype" in Map'], + ["globalThis.Map.prototype", "globalThis.Map.prototype"], + ["Map alias prototype", "MapAlias.prototype"], + ["Date alias prototype", "DateAlias.prototype"], + ["Object alias isFrozen", "ObjectAlias.isFrozen"], + ["Object.hasOwn(Object)", 'Object.hasOwn(Object, "hasOwn")'], + ["getOwnPropertyNames(Date)", "Object.getOwnPropertyNames(Date)"], + ["defineProperty(Promise)", 'Object.defineProperty(Promise, "resolve", { value: 1 })'], + ["hasOwnProperty.call(Map)", 'Object.prototype.hasOwnProperty.call(Map, "groupBy")'], + ] as const; + const source = `"use strict"; +const MapAlias = Map; +const DateAlias = Date; +const { Object: ObjectAlias } = globalThis; +function report(label, e) { + const m = /\\[(SC\\d{4}) at [^\\]]*:(\\d+)\\]$/.exec(e.message); + console.log(label, m ? "refused " + m[1] + " line " + m[2] : "threw " + e.message); +} +${reads.map(([label, read]) => `try { const value = ${read}; console.log(${JSON.stringify(label)}, "read", typeof value); } catch (e) { report(${JSON.stringify(label)}, e); }`).join("\n")} +console.log(Date.length, RegExp.length, Promise.length, Object.length, Map.length, Set.length); +console.log(Date.name, Map.name, Map.constructor === Function, Object.constructor === Function); +`; + const r = await compileAndRun("builtin-constructor-members", source, "cjs"); + expect(r.stderr).toBe(""); + expect(r.exitCode).toBe(0); + const lines = r.stdout.trimEnd().split("\n"); + expect(lines.slice(0, reads.length)).toEqual( + reads.map(([label], i) => + expect.stringMatching( + new RegExp( + `^${label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} refused SC(2020|1090) line ${i + 9}$`, + ), + ), + ), + ); + expect(lines.slice(reads.length)).toEqual(["7 2 1 1 0 0", "Date Map true true"]); + }); +}); + // WebCrypto's native slice is HMAC importKey('raw'), sign and verify, and // the SHA-1/SHA-2 digests. Algorithms Node recognizes outside that slice // reject with a coded SC2020 Error instead of a Node-shaped success, and a diff --git a/tests/harness/esm-path-globals.test.ts b/tests/harness/esm-path-globals.test.ts new file mode 100644 index 000000000..546c924a9 --- /dev/null +++ b/tests/harness/esm-path-globals.test.ts @@ -0,0 +1,38 @@ +/* __dirname / __filename in TypeScript files that Node's strip-types rule + * loads as ES modules: scriptc compiles them to the module's source path, + * the CommonJS-bundle reading. Node itself has no such globals in an ES + * module, so the oracle runs a twin of each source with the globals spelled + * import.meta.dirname / import.meta.filename, at the same paths. */ +import { execFile } from "node:child_process"; +import { cpSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const fixture = join(import.meta.dirname, "../fixtures/esm-path-globals"); + +test("__dirname and __filename in TypeScript ES modules read the module's source path", async () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-esm-path-globals-")); + cpSync(fixture, join(dir, "src"), { recursive: true }); + const entry = join(dir, "src", "main.ts"); + const result = await compile(entry, { + outPath: join(dir, "main"), + outDir: dir, + sanitize: process.env["SCRIPTC_SAN"] === "1", + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const native = await execFileAsync(result.binaryPath); + for (const file of [entry, join(dir, "src", "lib", "helper.ts")]) { + const text = readFileSync(file, "utf8") + .replaceAll("__dirname", "import.meta.dirname") + .replaceAll("__filename", "import.meta.filename"); + writeFileSync(file, text); + } + const node = await execFileAsync(process.execPath, [entry]); + expect(native.stdout).toBe(node.stdout); + expect(native.stdout).toContain("string string\nsrc main.ts\ntrue true\ntrue helper.ts\n"); +}); diff --git a/tests/harness/inheritance.test.ts b/tests/harness/inheritance.test.ts index c0ce3d7d4..27a176764 100644 --- a/tests/harness/inheritance.test.ts +++ b/tests/harness/inheritance.test.ts @@ -204,14 +204,11 @@ r.on("data", (b: Buffer) => console.log(b.toString())); expect(llvm).toContain("call void @scr_stream_init_readable(ptr "); }); - test("a runtime-fenced nested stream class emits the refusal without class helpers", async () => { - // The phase-1 reachable bug: a class declared inside a block (a - // runtime fence in JS) whose instances are captured emitted capture- - // box RC adapters for the uncollected class — a C compile error. - // run()'s unregistered-class sweep now rewrites such type slots to - // the inert f64 placeholder before emission (no instance can exist; - // every use traps), so the capture box is a PLAIN-kind box and the - // class never reaches the emitter at all. + test("a block-scoped stream class lowers natively with its class helpers", async () => { + // A class declared inside a block is collected like a class expression. + // Its captured instances once needed the unregistered-class sweep (the + // class was fenced); a builtin stream base no longer fences, so the + // class reaches the emitter with its object type and stream init. const llvm = await emittedLlvm( "stream-nested-fence", `const { Readable } = require("stream"); @@ -226,11 +223,10 @@ r.on("data", (b: Buffer) => console.log(b.toString())); `, "js", ); - expect(llvm).not.toContain("sc_retain_R_v"); expect(llvm).not.toContain("uncollected class"); - expect(llvm).not.toContain("%sc_o_R"); - expect(llvm).toMatch(/%sc_l_stream_0 = alloca ptr/); - expect(llvm).toContain("call void @scr_throw_error_msg_code"); + expect(llvm).toContain("%sc_o_R"); + expect(llvm).toContain("call void @scr_stream_init_readable(ptr "); + expect(llvm).not.toContain("call void @scr_throw_error_msg_code"); }); test("a getter-only override of a pair fills the set slot with a thrower", async () => { diff --git a/tests/harness/native-cli-packaging.test.ts b/tests/harness/native-cli-packaging.test.ts index a62a3c0c7..e69f9d9fe 100644 --- a/tests/harness/native-cli-packaging.test.ts +++ b/tests/harness/native-cli-packaging.test.ts @@ -42,6 +42,7 @@ function fixture(windows = true) { compiler_version: identity.version, target, ts7: asset(`../lib/typescript/lib/tsc${windows ? ".exe" : ""}`), + tsc_ts: asset(`../lib/tsc-ts/lib/tsc-ts${windows ? ".exe" : ""}`), llvm_package: asset("../lib/llvm"), runtime_pack: asset("../lib/runtime"), runtime_sources: asset("../lib/runtime-sources"), @@ -54,7 +55,12 @@ function fixture(windows = true) { write(join(distribution, "bin", executable), Buffer.alloc(2048, 1)); chmodSync(join(distribution, "bin", executable), 0o755); write(manifestPath, JSON.stringify(manifest)); - for (const path of [manifest.ts7, manifest.comptime, manifest.wasi_node_runner]) { + for (const path of [ + manifest.ts7, + manifest.tsc_ts, + manifest.comptime, + manifest.wasi_node_runner, + ]) { write(join(distribution, "bin", path.replaceAll("\\", "/")), "fixture"); } write( @@ -112,6 +118,7 @@ test.each([true, false])( readFileSync(join(unpacked, "bin", f.executable + ".json"), "utf8"), ); expect(manifest.ts7).toBe(f.manifest.ts7); + expect(manifest.tsc_ts).toBe(f.manifest.tsc_ts); const runtime = manifest.runtime_packs[0]; expect( JSON.parse( diff --git a/tests/harness/npm-cases.ts b/tests/harness/npm-cases.ts index 4dda0b9ad..01718abf9 100644 --- a/tests/harness/npm-cases.ts +++ b/tests/harness/npm-cases.ts @@ -19,12 +19,14 @@ export function npmCases(fixturesRoot: string): NpmCase[] { return [ ...globSync(join(fixturesRoot, "npm/cases/*/main.ts")) .sort() - // 2465-2469 and 2556-2557 are the --npm-static bundler-emitted-CJS + // 2465-2469, 2556-2567, 2571 and 2580-2584 are the --npm-static bundler-emitted-CJS // cases (npm-static.test.ts drives them with the opt-in): their // consumers exercise the static rewrite's surface (lexer-visible // names the shipped .d.ts never declares, the __toESM interop // family), so they stay out of the flagless island lane by design. - .filter((entry) => !/\/(246[5-9]|255[67])-[^/]+\/main\.ts$/.test(entry)) + .filter( + (entry) => !/\/(246[5-9]|255[6-9]|256[0-7]|2571|258[0-4])-[^/]+\/main\.ts$/.test(entry), + ) .map((entry) => ({ name: entry.split("/").at(-2)!, entry, diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 356d2fa6d..c16567f47 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -76,8 +76,9 @@ async function buildStatic(entry: string, npmStatic: string[] | "auto"): Promise ...globSync( join(fixturesRoot, "npm/node_modules/cryptozoo/**/*.{js,mjs,cjs,json,d.ts}"), ).sort(), - // the bundler-emitted-CJS mini packages (cases 2465-2469, 2556-2557) - ...globSync(join(fixturesRoot, "npm/node_modules/gt*/**/*.{js,json}")).sort(), + // the bundler-emitted-CJS mini packages (cases 2465-2469, 2556-2567, 2571, 2580-2584) + ...globSync(join(fixturesRoot, "npm/node_modules/gt*/**/*.{js,cjs,json}")).sort(), + ...globSync(join(fixturesRoot, "npm/store/gt*/**/*.{js,cjs,json}")).sort(), ]; for (const f of inputs) hash.update(f).update(readFileSync(f)); const key = hash @@ -828,30 +829,41 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { // Types-first resolution answers @types/twinned, but the refusal names the // package that has the code and the flag that compiles it. - test("an @types-typed package is refused under its runtime name with the opt-in remedy", () => { - const { coverage } = analyze(join(pilotRoot, "twinned-cli.ts")); + // The default selection compiles a package typed by a DefinitelyTyped + // twin that describes its installed release, and the untyped + // dependencies of compiled packages. + test("an @types-typed package and its untyped dependency compile by default", async () => { + const entry = join(pilotRoot, "twinned-cli.ts"); + const { coverage } = analyze(entry); expect(coverage.npmStatic).toEqual([ - { - package: "twinned", - status: "refused", - detail: - "its declared types come from the separate @types/twinned package, not the package itself", - hint: "to compile it anyway, name it with --npm-static=twinned (or --npm-static=twinned... to include its dependencies)", - }, + { package: "twinned", status: "static" }, + { package: "ms", status: "static" }, ]); - const diagnostic = coverage.diagnostics.find((d) => d.code === "SC2013"); - expect(diagnostic?.message).toContain("'twinned'"); - expect(diagnostic?.hint).toContain("--npm-static=twinned"); + expect(coverage.preflightFailed).toBe(false); + const binary = await buildStatic(entry, "auto"); + const [nodeRes, nativeRes] = await Promise.all([ + runBinary("node", [entry]), + runBinary(binary, []), + ]); + expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); + expect(nativeRes.exitCode).toBe(nodeRes.exitCode); + }, 180_000); + + // The twin only checks the program; the JavaScript compiles with its + // inferred surface, so a twin naming an export the JavaScript lacks + // (Node's link error) fails the build at the import's use instead of + // compiling against the declaration. + test("an @types twin that disagrees with the shipped JavaScript does not build", () => { + const { coverage } = analyze(join(pilotRoot, "twinstale-cli.ts")); + expect(coverage.npmStatic).toEqual([{ package: "twinstale", status: "static" }]); + expect(coverage.diagnostics).toContainEqual( + expect.objectContaining({ message: expect.stringContaining("'stale'") as string }), + ); }, 120_000); - test("a named package's untyped dependency points at the transitive opt-in", () => { + test("a named package's untyped dependency compiles with it", () => { const { coverage } = analyze(join(pilotRoot, "twinned-cli.ts"), { npmStatic: ["twinned"] }); - expect(coverage.npmStatic).toContainEqual({ - package: "ms", - status: "refused", - detail: "it ships no own .d.ts declaration surface", - hint: "to compile it anyway, name it with --npm-static=ms, or use --npm-static=twinned... to include all of twinned's dependencies", - }); + expect(coverage.npmStatic).toContainEqual({ package: "ms", status: "static" }); }, 120_000); test("--npm-static=pkg... compiles the package's dependency closure", async () => { @@ -1389,6 +1401,224 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { ]); }, 120_000); + /* ── 2558-2561: esbuild's bundled CommonJS chunks and per-file CommonJS + * cycles ─ every __commonJS chunk compiles as its own CommonJS module + * (evaluated once, at its first require); a require cycle between + * esbuild outputs whose live getter tables install before their first + * require initializes exactly like Node, and a cycle that reads a + * binding in its temporal dead zone stays refused. */ + async function expectNodeParity(entry: string, packages: string[]): Promise { + const { coverage } = analyze(entry, { npmStatic: packages }); + expect(coverage.npmStatic).toEqual(packages.map((pkg) => ({ package: pkg, status: "static" }))); + expect(coverage.preflightFailed).toBe(false); + expect(coverage.diagnostics).toHaveLength(0); + const binary = await buildStatic(entry, packages); + const [nodeRes, nativeRes] = await Promise.all([ + runBinary("node", [entry]), + runBinary(binary, []), + ]); + // Node's stderr carries its type-stripping warning; the programs write + // only to stdout. + expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); + expect(comparableStderr(nativeRes.stderr).toString("utf8")).toBe(""); + expect(nativeRes.exitCode).toBe(nodeRes.exitCode); + expect(nativeRes.exitCode).toBe(0); + } + + // 2558: esbuild 0.25 (plain and whitespace/syntax minified) and 0.17 + // CommonJS bundles of one source, plus a .cjs bundle in a type-module + // package with the node-mode interop. + test("bundled CommonJS chunks 2558-bundled-chunks compile statically and byte-match Node", async () => { + await expectNodeParity(join(fixturesRoot, "npm/cases/2558-bundled-chunks/main.ts"), [ + "gtchunk", + "gtchunkws", + "gtchunk17", + "gtcjsdist", + "gtcore", + ]); + }, 180_000); + + // A whitespace/syntax-minified bundle whose helpers keep their names is + // judged by the recognizers, not refused up front as minified. + test("--npm-static=auto opts a minified bundle with named helpers in", () => { + const entry = join(fixturesRoot, "npm/cases/2558-bundled-chunks/main.ts"); + const { coverage } = analyze(entry, { npmStatic: "auto" }); + expect(coverage.npmStatic).toContainEqual({ package: "gtchunkws", status: "static" }); + }, 120_000); + + test.each(["2559-cjs-cycle", "2560-cjs-cycle-partner-first"])( + "the esbuild CommonJS cycle %s compiles statically and byte-matches Node", + async (name) => { + await expectNodeParity(join(fixturesRoot, `npm/cases/${name}/main.ts`), ["gtcycle"]); + }, + 180_000, + ); + + // 2561: Node throws a ReferenceError there; the cycle keeps SC1016. + test("a CommonJS cycle that reads a binding before its declaration stays refused", () => { + const entry = join(fixturesRoot, "npm/cases/2561-cjs-cycle-early-read/main.ts"); + const { coverage } = analyze(entry, { npmStatic: ["gtcyclebad"] }); + expect(coverage.npmStatic).toEqual([ + { + package: "gtcyclebad", + status: "refused", + detail: expect.stringMatching( + /circular imports .*can use its binding 'import_index' of a module of the cycle/, + ) as string, + }, + ]); + }, 120_000); + + // 2563: lazily initialized ES modules (__esm) in esbuild 0.25 plain and + // whitespace/syntax-minified bundles. + test("bundled ES module initializers 2563-bundled-esm-inits compile statically and byte-match Node", async () => { + await expectNodeParity(join(fixturesRoot, "npm/cases/2563-bundled-esm-inits/main.ts"), [ + "gtesm", + "gtesmws", + "gtcore", + ]); + }, 180_000); + + // 2580-2581: a barrel whose re-exports are memoized lazy loaders of + // lazily initialized ES modules (a bundle plugin's shape): ES-module + // importers copy every export when they link (Node's CommonJS import), + // require() reads each export's getter at the read. + test.each([ + ["2580-lazy-barrel", ["gtlazy", "gtlazyws"]], + ["2581-lazy-barrel-require", ["gtlazyuser", "gtlazy"]], + ])( + "the lazy barrel %s compiles statically and byte-matches Node", + async (name, packages) => { + await expectNodeParity(join(fixturesRoot, `npm/cases/${name}/main.ts`), packages); + }, + 180_000, + ); + + // 2582: two releases of one package in a pnpm store, the newer through + // an npm alias directory; the default selection compiles both copies. + test("an npm alias of a selected package compiles from its JavaScript", async () => { + const entry = join(fixturesRoot, "npm/cases/2582-npm-alias-two-releases/main.ts"); + const { coverage } = analyze(entry); + expect(coverage.npmStatic).toEqual( + expect.arrayContaining([ + { package: "gtroute", status: "static" }, + { package: "gtpattern", status: "static" }, + ]), + ); + const binary = await buildStatic(entry, "auto"); + const [nodeRes, nativeRes] = await Promise.all([ + runBinary("node", [entry]), + runBinary(binary, []), + ]); + expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); + expect(nativeRes.exitCode).toBe(nodeRes.exitCode); + }, 180_000); + + // 2583: a module whose export object is one of its functions (semver + // 5/6's `exports = module.exports = Version`), under both values of the + // environment variable the module reads. + test("function export objects 2583-cjs-function-exports compile statically and byte-match Node", async () => { + const entry = join(fixturesRoot, "npm/cases/2583-cjs-function-exports/main.ts"); + await expectNodeParity(entry, ["gtfnexports", "gtfnmembers"]); + const binary = await buildStatic(entry, ["gtfnexports", "gtfnmembers"]); + const env = { ...process.env, GT_LABEL: "x" }; + const [nodeRes, nativeRes] = await Promise.all([ + runBinary("node", [entry], env), + runBinary(binary, [], env), + ]); + expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); + expect(nativeRes.stdout.toString("utf8")).toContain("[x]"); + }, 180_000); + + // 2584: an export object replaced inside an environment-dependent branch + // (readable-stream 2.x) is named precisely. + test("an environment-dependent export object is refused by name", () => { + const entry = join(fixturesRoot, "npm/cases/2584-cjs-conditional-exports/main.ts"); + const { coverage } = analyze(entry, { npmStatic: ["gtcondexports"] }); + expect(JSON.stringify(coverage)).toContain( + "replacing module.exports outside a top-level statement (a conditional or environment-dependent export object)", + ); + }, 120_000); + + // 2567: an export that reads `this`, called through its namespace, sees + // Node's module object; the lowered namespace would differ, so the + // package stays refused. + test("a namespace call to an export that reads `this` stays refused", () => { + const entry = join(fixturesRoot, "npm/cases/2567-esm-namespace-this/main.ts"); + const { coverage } = analyze(entry, { npmStatic: ["gtesmthis", "gtcore"] }); + expect(coverage.npmStatic).toContainEqual({ + package: "gtesmthis", + status: "refused", + detail: expect.stringMatching( + /calls only of exported functions that do not read 'this'/, + ) as string, + }); + }, 120_000); + + // 2562: a file with unverifiable interop that no compiled code can load + // (the program imports a leaf data module and only a type from the + // main entry) leaves its package static. + test("a package is judged by the files its imported subpaths can load", async () => { + await expectNodeParity(join(fixturesRoot, "npm/cases/2562-subpath-leaf/main.ts"), [ + "gtsubpath", + ]); + }, 180_000); + + // 2566: the same early read through esbuild's `(0, binding.fn)()` call + // spelling also keeps SC1016. + test("a CommonJS cycle that calls an export during initialization stays refused", () => { + const entry = join(fixturesRoot, "npm/cases/2566-cjs-cycle-indirect-call/main.ts"); + const { coverage } = analyze(entry, { npmStatic: ["gtcyclecall"] }); + expect(coverage.npmStatic).toEqual([ + { + package: "gtcyclecall", + status: "refused", + detail: expect.stringMatching( + /circular imports .*can use its binding 'import_index' of a module of the cycle/, + ) as string, + }, + ]); + }, 120_000); + + // 2571: the partner stores the export (in a const, or in an object + // literal) and calls it while the cycle initializes; both keep SC1016. + test("a CommonJS cycle that stores an export and calls it stays refused", () => { + const entry = join(fixturesRoot, "npm/cases/2571-cjs-cycle-stored-function/main.ts"); + const { coverage } = analyze(entry, { npmStatic: ["gtcyclealias", "gtcyclebox"] }); + for (const pkg of ["gtcyclealias", "gtcyclebox"]) + expect(coverage.npmStatic).toContainEqual({ + package: pkg, + status: "refused", + detail: expect.stringMatching( + /circular imports .*can use its binding 'import_index' of a module of the cycle/, + ) as string, + }); + }, 120_000); + + // 2564: a chunk reads a top-level function whose name a nested scope of + // the chunk also declares; the function still travels with the chunk. + test("a chunk carries a top-level function its nested scopes shadow", async () => { + await expectNodeParity(join(fixturesRoot, "npm/cases/2564-chunk-helper-shadowing/main.ts"), [ + "gtchunkshadow", + ]); + }, 180_000); + + // 2565: Node shares one function between the chunks that export it; a + // per-chunk copy would not, so the package stays refused. + test("chunks that export a shared top-level function stay refused", () => { + const entry = join(fixturesRoot, "npm/cases/2565-chunk-shared-function/main.ts"); + const { coverage } = analyze(entry, { npmStatic: ["gtchunkshared"] }); + expect(coverage.npmStatic).toEqual([ + { + package: "gtchunkshared", + status: "refused", + detail: expect.stringMatching( + /uses the bundle's top-level function 'tally' as a value, which its chunks cannot share/, + ) as string, + }, + ]); + }, 120_000); + test("published type-only declarations coexist with a static runtime bundle", async () => { const entry = join(fixturesRoot, "npm/cases/2469-bundle-offender/main.ts"); const { coverage } = analyze(entry, { npmStatic: ["gtghost"] }); diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 54207f246..6602bf8e5 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -40,7 +40,6 @@ const KNOWN: Record = { "bytes-exit": { build: "SC2004" }, "cjs-esm-named": { build: "SC2013" }, "cjs-lexer-aligned": { build: "SC2013" }, - "cjs-lexer-invisible": { build: "SC2013" }, "commander-calc": { run: "SC1090", argvs: [["--help"]] }, "commander-calc-async": { run: "SC1090", argvs: [["--help"]] }, "create-require": { run: "SC1090" }, @@ -53,7 +52,6 @@ const KNOWN: Record = { "esm-missing-export": { build: "SC1090" }, "fetch-destructure": { run: "SC1031" }, "fs-shims": { run: "SC1101" }, - "hoisted-workspace": { build: "SC2013" }, "import-refusal": { build: "SC2020" }, "island-registry": { build: "SC2011" }, "island-timers": { @@ -89,7 +87,7 @@ const KNOWN: Record = { "node24-url-factories": { run: "SC2020" }, "node24-url-mutation": { run: "SC1090" }, "node24-util-adapters": { run: "SC2020" }, - "node24-util-types": { run: "SC2009" }, + "node24-util-types": { run: "SC1090" }, "node24-web-stream-encoding": { build: "SC2013" }, "node24-web-stream-errors": { build: "SC2013" }, "node24-web-stream-lifecycle": { build: "SC2013" }, @@ -104,7 +102,7 @@ const KNOWN: Record = { "package-imports": { build: "SC2013" }, "promise-bridge": { build: "SC1090" }, "self-name": { build: "SC2013" }, - "stream-shims": { build: "SC1090" }, + "stream-shims": { run: "SC1090" }, "util-shims": { run: "SC2020" }, "web-streams": { build: "SC2013" }, "workspace-copied": { build: "SC2013" }, diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index e979c772c..b5d47fae1 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -425,6 +425,61 @@ test("node-types: promisified fs callback functions lower as their fs/promises c expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: util.promisify over typed callback-last functions settles like Node", async () => { + const outDir = outDirFor("promisify-callback-functions"); + const entry = join(nodeTypesDir, "promisify-callback-functions.ts"); + const result = await compile(entry, { + outPath: join(outDir, "promisify-callback-functions"), + 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); +}); + +test("node-types: lookup-option unions and narrowed header-record entries under @types/node", async () => { + const outDir = outDirFor("lookup-and-header-entries"); + const entry = join(nodeTypesDir, "lookup-and-header-entries.ts"); + const result = await compile(entry, { + outPath: join(outDir, "lookup-and-header-entries"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const native = await execFileAsync(result.binaryPath); + const node = await execFileAsync(process.execPath, [ + "--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", + entry, + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + +test("node-types: stream subclasses extend the @types/node stream classes", async () => { + const outDir = outDirFor("stream-subclass-hooks"); + const entry = join(nodeTypesDir, "stream-subclass-hooks.ts"); + const result = await compile(entry, { + outPath: join(outDir, "stream-subclass-hooks"), + 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); +}); + test("node-types: promisified setTimeout and setImmediate lower as timers/promises", async () => { const outDir = outDirFor("promisify-timers"); const entry = join(nodeTypesDir, "promisify-timers.ts"); diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index fa71b5a08..defa4ff63 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -121,6 +121,7 @@ for (const backend of ["llvm"] as const) { compiler_version: manifest.version, target: target.name, ts7: "ts7", + tsc_ts: "tsc-ts", llvm_package: "llvm", runtime_pack: "pack", linker: "ld", diff --git a/tests/harness/self-hosting-ts7-process.test.ts b/tests/harness/self-hosting-ts7-process.test.ts index b8d53dff9..d49a5c092 100644 --- a/tests/harness/self-hosting-ts7-process.test.ts +++ b/tests/harness/self-hosting-ts7-process.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { analyze, compile } from "@scriptc/compiler"; import { expect, test } from "vitest"; -import { ts7Executable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js"; +import { tsgoExecutable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js"; import { runClient } from "./ts7-client-harness.js"; const root = join(import.meta.dirname, "../.."); @@ -96,8 +96,9 @@ for (const backend of ["llvm"] as const) { ); const report = join(dir, "native.json"); // Only ordinary stdio is inherited. The native executable starts the - // actual tsgo process; the test runner never creates its RPC pipes. - const run = spawnSync(built.binaryPath, [ts7Executable(), dir, report], { + // actual tsgo process; the test runner never creates its RPC pipes. As in + // ts7-client-harness.ts, the protocol cases run against tsgo. + const run = spawnSync(built.binaryPath, [tsgoExecutable(), dir, report], { encoding: "utf8", timeout: 45_000, }); diff --git a/tests/harness/ts7-client-harness.ts b/tests/harness/ts7-client-harness.ts index e0d0eccd6..b65dc706f 100644 --- a/tests/harness/ts7-client-harness.ts +++ b/tests/harness/ts7-client-harness.ts @@ -2,7 +2,7 @@ import { spawn, type ChildProcess } from "node:child_process"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { expect } from "vitest"; -import { ts7Executable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js"; +import { tsgoExecutable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js"; import { TS7_FILE_SYSTEM_CALLBACKS } from "../../packages/compiler/src/frontend/ts7/rpc-filesystem.js"; const root = fileURLToPath(new URL("../..", import.meta.url)); @@ -40,8 +40,10 @@ export async function runClient( directory: string, report: string, ): Promise { + // The protocol cases echo bytes that are not UTF-8 text, which tsc-ts 0.0.3 + // returns decoded, so the transport is checked against tsgo. const server = spawn( - ts7Executable(), + tsgoExecutable(), ["--api", "--timing", "--cwd", directory, `--callbacks=${TS7_FILE_SYSTEM_CALLBACKS}`], { stdio: ["pipe", "pipe", "pipe"],