diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a9ad9fb0..3eaaef8d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,6 +126,8 @@ jobs: SCRIPTC_TEST_WORKERS: "1" - name: Verify mixed-source generated AST getters run: pnpm generate:ts7-ast --check + - name: Verify the generated IR field readers + run: pnpm generate:ir-fields --check native_macos: needs: [validation_scope, lanes] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 67da1d0f4..e59cf7739 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -435,7 +435,8 @@ jobs: (needs.check-release.outputs.reuse_artifacts == 'true' || (needs.build-native-packages.result == 'success' && needs.build-mobile-runtime-packs.result == 'success')) runs-on: ubuntu-latest - timeout-minutes: 15 + # Two waits for npm availability of at most 25 minutes each. + timeout-minutes: 70 environment: Release permissions: contents: read @@ -588,9 +589,8 @@ jobs: } # pnpm pack resolves workspace dependencies; npm handles OIDC. - # Publish dependencies first. Registry propagation after an accepted - # upload must not block the remaining packages; the npm-available - # job waits for it. + # Publish dependencies first. The npm-available job waits for the + # packages that nothing in this release depends on. publish_dir packages/runtime publish_dir packages/runtime-darwin-arm64 publish_dir packages/llvm-darwin-arm64 @@ -610,12 +610,25 @@ jobs: publish_dir packages/runtime-wasm32-wasi publish_dir packages/runtime-win32-x64-msvc publish_dir packages/llvm-win32-x64-msvc + # A native CLI package depends on its platform's LLVM helper and + # runtime pack packages, and scriptc on the native CLI packages. + # Each group publishes only once the packages it depends on + # resolve, so an install never finds a package whose dependencies + # npm is still processing. A rerun skips accepted uploads and waits + # again. + wait_available() { + awk 'FILENAME == ARGV[1] { accepted[$1] = $2; next } { print $1, accepted[$1] }' \ + "$RUNNER_TEMP/npm-accepted-times.txt" "$RUNNER_TEMP/npm-release.txt" > "$RUNNER_TEMP/npm-wait.txt" + node scripts/wait-npm-available.mjs "$RUNNER_TEMP/npm-wait.txt" --interval 30 --timeout 1500 + } + wait_available for pkg in packages/cli-*; do publish_dir "$pkg"; done publish_dir packages/compiler publish_dir packages/experimental publish_dir packages/node publish_dir packages/threads publish_dir packages/types + wait_available publish_dir packages/cli echo "All package uploads accepted; npm registry propagation may continue after this job." >> "$GITHUB_STEP_SUMMARY" diff --git a/RELEASING.md b/RELEASING.md index 26566f32d..e650554a2 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -14,7 +14,7 @@ To prepare a release: CI (`.github/workflows/release.yml`) compares the version in `packages/cli/package.json` to what `scriptc` has on npm. If it differs, it builds platform packages on matching macOS, Linux, and Windows runners, verifies the version spine, and publishes the runtime, helper, compiler, and CLI packages in dependency order. After the publish succeeds, a separate job creates the git tag `v` and the GitHub release with the marked changelog entry as its body, and attaches `surface-manifest.json` — the machine-readable listing of the surface the static tier compiles at that version (stable per-entry ids, so two releases diff mechanically; see `packages/compiler/src/coverage/surface-manifest.ts` for the schema). The job regenerates the manifest from the tree and fails on any byte difference from the committed file before attaching, so the asset is always the manifest of the code being released. The same file ships inside the `@scriptc/compiler` package as `@scriptc/compiler/surface-manifest.json`. -The release job builds each pinned LLVM helper, precompiled runtime pack, and native CLI on its matching host. The native CLI bundles its TypeScript checker, LLVM helper, runtime pack, declarations, and comptime evaluator. npm postinstall installs the matching native executable; Node is needed for npm installation but not native compilation or execution. The user's toolchain performs the final platform link. Sanitized builds also use it to instrument program LLVM and compile the C runtime. Platform packages publish ahead of `@scriptc/compiler` and `scriptc`. The GitHub release attaches standalone compiler archives, their SHA-256 checksums, and the surface manifest after npm publication succeeds. +The release job builds each pinned LLVM helper, precompiled runtime pack, and native CLI on its matching host. The native CLI package bundles its TypeScript checker, declarations, and comptime evaluator, and depends on its platform's LLVM helper and runtime pack packages at the same version; the compiler resolves them beside itself. The standalone archives bundle the helper and runtime pack too. npm postinstall installs the matching native executable; Node is needed for npm installation but not native compilation or execution. The user's toolchain performs the final platform link. Sanitized builds also use it to instrument program LLVM and compile the C runtime. Platform packages publish ahead of `@scriptc/compiler` and `scriptc`. The GitHub release attaches standalone compiler archives, their SHA-256 checksums, and the surface manifest after npm publication succeeds. Release builds of the native CLI optimize the compiler with a profile collected on the release commit. The `cli-profile` job runs `node scripts/refresh-cli-profile.mjs` on Linux x64, which builds an instrumented compiler, runs it on tsc-ts, a slice of the corpus, and the runtime benchmark workloads, and writes the merged sparse profile. Every platform's CLI build uses that profile (`SCRIPTC_NATIVE_PROFILE_USE`), and the GitHub release attaches it as `scriptc--cli.profdata.gz`. Code the profile does not describe, or describes from older compiler sources, is optimized as without a profile. When the profile job fails, the CLI builds use the committed profile `packages/compiler/native/cli.profdata.gz`, which local and CI builds of the native compiler also use. Refresh the committed profile when compiler code has changed substantially since the last refresh: run `node scripts/refresh-cli-profile.mjs --tsc-ts ` on a quiet Linux x64 host and commit the new profile on its own. diff --git a/package.json b/package.json index f4efc6d8d..b392696a9 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "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", + "generate:ir-fields": "node scripts/generate-ir-fields.mjs", "test:fetch-conformance": "vitest run tests/harness/fetch-conformance.test.ts", "test:test262": "tsx tests/test262/run.ts", "test:test262:fetch": "node tests/test262/fetch.mjs", diff --git a/packages/cli-darwin-arm64/package.json b/packages/cli-darwin-arm64/package.json index 1a8ee5d2c..1d4921f48 100644 --- a/packages/cli-darwin-arm64/package.json +++ b/packages/cli-darwin-arm64/package.json @@ -18,9 +18,13 @@ "files": [ "dist" ], + "dependencies": { + "@scriptc/llvm-darwin-arm64": "workspace:*", + "@scriptc/runtime-darwin-arm64": "workspace:*" + }, "scripts": { "build": "node -e \"\"", - "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist", + "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages", "prepack": "node ../../scripts/verify-native-cli.mjs ." }, "publishConfig": { diff --git a/packages/cli-darwin-x64/package.json b/packages/cli-darwin-x64/package.json index 0cb24b908..dca236b32 100644 --- a/packages/cli-darwin-x64/package.json +++ b/packages/cli-darwin-x64/package.json @@ -18,9 +18,13 @@ "files": [ "dist" ], + "dependencies": { + "@scriptc/llvm-darwin-x64": "workspace:*", + "@scriptc/runtime-darwin-x64": "workspace:*" + }, "scripts": { "build": "node -e \"\"", - "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist", + "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages", "prepack": "node ../../scripts/verify-native-cli.mjs ." }, "publishConfig": { diff --git a/packages/cli-linux-arm64-gnu/package.json b/packages/cli-linux-arm64-gnu/package.json index 3a1c7488d..1459caa1d 100644 --- a/packages/cli-linux-arm64-gnu/package.json +++ b/packages/cli-linux-arm64-gnu/package.json @@ -18,9 +18,13 @@ "files": [ "dist" ], + "dependencies": { + "@scriptc/llvm-linux-arm64-gnu": "workspace:*", + "@scriptc/runtime-linux-arm64-gnu": "workspace:*" + }, "scripts": { "build": "node -e \"\"", - "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist", + "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages", "prepack": "node ../../scripts/verify-native-cli.mjs ." }, "publishConfig": { diff --git a/packages/cli-linux-arm64-musl/package.json b/packages/cli-linux-arm64-musl/package.json index e671e926b..3703c0059 100644 --- a/packages/cli-linux-arm64-musl/package.json +++ b/packages/cli-linux-arm64-musl/package.json @@ -18,9 +18,13 @@ "files": [ "dist" ], + "dependencies": { + "@scriptc/llvm-linux-arm64-musl": "workspace:*", + "@scriptc/runtime-linux-arm64-musl": "workspace:*" + }, "scripts": { "build": "node -e \"\"", - "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist", + "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages", "prepack": "node ../../scripts/verify-native-cli.mjs ." }, "publishConfig": { diff --git a/packages/cli-linux-x64-gnu/package.json b/packages/cli-linux-x64-gnu/package.json index 2b56308fd..6d70361f9 100644 --- a/packages/cli-linux-x64-gnu/package.json +++ b/packages/cli-linux-x64-gnu/package.json @@ -18,9 +18,13 @@ "files": [ "dist" ], + "dependencies": { + "@scriptc/llvm-linux-x64-gnu": "workspace:*", + "@scriptc/runtime-linux-x64-gnu": "workspace:*" + }, "scripts": { "build": "node -e \"\"", - "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist", + "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages", "prepack": "node ../../scripts/verify-native-cli.mjs ." }, "publishConfig": { diff --git a/packages/cli-linux-x64-musl/package.json b/packages/cli-linux-x64-musl/package.json index 4ca4c2837..0c12fc60d 100644 --- a/packages/cli-linux-x64-musl/package.json +++ b/packages/cli-linux-x64-musl/package.json @@ -18,9 +18,13 @@ "files": [ "dist" ], + "dependencies": { + "@scriptc/llvm-linux-x64-musl": "workspace:*", + "@scriptc/runtime-linux-x64-musl": "workspace:*" + }, "scripts": { "build": "node -e \"\"", - "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist", + "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages", "prepack": "node ../../scripts/verify-native-cli.mjs ." }, "publishConfig": { diff --git a/packages/cli-win32-x64-msvc/package.json b/packages/cli-win32-x64-msvc/package.json index d734c0807..8872d2f15 100644 --- a/packages/cli-win32-x64-msvc/package.json +++ b/packages/cli-win32-x64-msvc/package.json @@ -18,9 +18,13 @@ "files": [ "dist" ], + "dependencies": { + "@scriptc/llvm-win32-x64-msvc": "workspace:*", + "@scriptc/runtime-win32-x64-msvc": "workspace:*" + }, "scripts": { "build": "node -e \"\"", - "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist", + "build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages", "prepack": "node ../../scripts/verify-native-cli.mjs ." }, "publishConfig": { diff --git a/packages/cli/scripts/install-native.mjs b/packages/cli/scripts/install-native.mjs index 4db76cc24..e8317d341 100644 --- a/packages/cli/scripts/install-native.mjs +++ b/packages/cli/scripts/install-native.mjs @@ -93,6 +93,34 @@ export function installNativeCli( const bin = join(directory, "bin"); mkdirSync(bin, { recursive: true }); const manifest = relocateToolchain(original, sourceDirectory, bin); + // A platform package depends on its LLVM helper and runtime pack packages + // instead of bundling them. Record where the package manager put them. + if (manifest.llvm_package === undefined || manifest.runtime_pack === undefined) { + const dependencies = Object.keys(identity.dependencies ?? {}); + const installed = (prefix) => { + const name = dependencies.find((dependency) => dependency.startsWith(prefix)); + if (name === undefined) + throw new Error(`${packageName} does not depend on a ${prefix}* package`); + let path, found; + try { + path = createRequire(platformManifest).resolve(`${name}/package.json`); + found = JSON.parse(readFileSync(path, "utf8")).version; + } catch { + throw new Error(`scriptc requires ${name}@${version}; reinstall scriptc`); + } + if (found !== version) + throw new Error(`scriptc requires ${name}@${version}, found ${found}; reinstall scriptc`); + return relative(bin, dirname(path)); + }; + manifest.llvm_package ??= installed("@scriptc/llvm-"); + if (manifest.runtime_pack === undefined) { + manifest.runtime_pack = installed("@scriptc/runtime-"); + manifest.runtime_packs = [ + { target: manifest.target, path: manifest.runtime_pack }, + ...(manifest.runtime_packs ?? []), + ]; + } + } // npm normalizes modes for payloads outside package bin entries. These // tools are launched directly by the compiler after installation. if (process.platform !== "win32") { diff --git a/packages/cli/src/bootstrap.ts b/packages/cli/src/bootstrap.ts index f63b3d784..0a3c5304c 100644 --- a/packages/cli/src/bootstrap.ts +++ b/packages/cli/src/bootstrap.ts @@ -144,6 +144,11 @@ async function tryFastPath(): Promise { ...(values["cpu-dispatch"] === true ? { cpuDispatch: true as const } : {}), ...(checker === undefined ? {} : { checker }), npmStatic, + // Mirrors the full compiler's key: shape inference's overlay is opt-in + // (SCRIPTC_SHAPE_OVERLAY=1) and --no-shape-overlay turns it off. + ...(process.env["SCRIPTC_SHAPE_OVERLAY"] === "1" && values["shape-overlay"] !== false + ? { shapeOverlay: true as const } + : {}), ffiProfile: ffiPath === null ? null : { path: ffiPath, bytes: ffiBytes! }, target: `${process.env["SCRIPTC_TARGET"] ?? "native"}:${buildPlatform}:${process.arch}:${ helperObjectRoute ? "runtime-pack" : "driver-tu" @@ -233,6 +238,7 @@ async function coverageFastPath( values["provenance-sources"] || values["emit-ir"] || (values["npm-static"] ?? []).length > 0 || + values["shape-overlay"] === false || (values["external-types"] ?? []).length > 0 || // The replay covers the default Node island policy only. hasNodeIslandFlags(values) diff --git a/packages/cli/test/native-install.test.ts b/packages/cli/test/native-install.test.ts index 5e51a2447..7a6d3ef16 100644 --- a/packages/cli/test/native-install.test.ts +++ b/packages/cli/test/native-install.test.ts @@ -7,6 +7,7 @@ import { renameSync, rmSync, statSync, + symlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; @@ -104,6 +105,71 @@ test("installs a direct executable and relocatable references to platform assets expect(JSON.stringify(manifest)).not.toContain(f.directory); }); +test.each(["hoisted", "pnpm"])( + "resolves the LLVM helper and runtime pack a platform package depends on (%s)", + (layout) => { + const f = fixture(); + const helper = "@scriptc/llvm-darwin-arm64"; + const runtime = "@scriptc/runtime-darwin-arm64"; + const toolchain: Record = { ...f.toolchain }; + delete toolchain.llvm_package; + delete toolchain.runtime_pack; + delete toolchain.runtime_packs; + writeFileSync(f.binary + ".json", JSON.stringify(toolchain)); + // pnpm links a package's dependencies beside it in its virtual store, + // where the installation's node_modules does not see them. + const platform = + layout === "pnpm" + ? join(f.directory, "node_modules/.pnpm/cli@1.2.3/node_modules", f.packageName) + : f.platform; + if (layout === "pnpm") { + mkdirSync(dirname(platform), { recursive: true }); + renameSync(f.platform, platform); + symlinkSync(platform, f.platform, "junction"); + } + const modules = + layout === "pnpm" ? dirname(dirname(platform)) : join(f.directory, "node_modules"); + writeFileSync( + join(platform, "package.json"), + JSON.stringify({ + name: f.packageName, + version: "1.2.3", + dependencies: { [helper]: "1.2.3", [runtime]: "1.2.3" }, + }), + ); + for (const name of [helper, runtime]) { + mkdirSync(join(modules, name), { recursive: true }); + writeFileSync( + join(modules, name, "package.json"), + JSON.stringify({ name, version: "1.2.3" }), + ); + } + mkdirSync(join(modules, helper, "bin")); + writeFileSync(join(modules, helper, "bin/scriptc-llvm-codegen"), "helper", { mode: 0o644 }); + const command = installNativeCli(f.directory, f.packageName); + const manifest = JSON.parse(readFileSync(command + ".json", "utf8")); + const bin = dirname(command); + expect(resolve(bin, manifest.llvm_package)).toBe(join(modules, helper)); + expect(resolve(bin, manifest.runtime_pack)).toBe(join(modules, runtime)); + expect(manifest.runtime_packs).toEqual([ + { target: "macos-arm64", path: manifest.runtime_pack }, + ]); + if (process.platform !== "win32") + expect(statSync(join(modules, helper, "bin/scriptc-llvm-codegen")).mode & 0o111).toBe(0o111); + writeFileSync( + join(modules, runtime, "package.json"), + JSON.stringify({ name: runtime, version: "1.2.4" }), + ); + expect(() => installNativeCli(f.directory, f.packageName)).toThrow( + `scriptc requires ${runtime}@1.2.3, found 1.2.4`, + ); + rmSync(join(modules, runtime), { recursive: true }); + expect(() => installNativeCli(f.directory, f.packageName)).toThrow( + `scriptc requires ${runtime}@1.2.3; reinstall scriptc`, + ); + }, +); + test("missing and mismatched packages fail without replacing an installed command", () => { const f = fixture(); const command = installNativeCli(f.directory, f.packageName); diff --git a/packages/compiler/ambient/node-fallback/net.d.ts b/packages/compiler/ambient/node-fallback/net.d.ts index 744fbb0b9..806039fb5 100644 --- a/packages/compiler/ambient/node-fallback/net.d.ts +++ b/packages/compiler/ambient/node-fallback/net.d.ts @@ -356,6 +356,12 @@ declare module "net" { }, connectListener?: () => void, ): Socket; + /* A Unix domain socket client (Node's IpcNetConnectOpts and path + * overloads). */ + export function connect(path: string, connectListener?: () => void): Socket; + export function connect(options: { path: string }, connectListener?: () => void): Socket; + export function createConnection(path: string, connectListener?: () => void): Socket; + export function createConnection(options: { path: string }, connectListener?: () => void): Socket; /* The happy-eyeballs attempt budget (Node's process-wide knob; the * autoSelectFamily dial consults it): one runtime int, default 250ms * like Node's. */ diff --git a/packages/compiler/src/backend/llvm/blocks.ts b/packages/compiler/src/backend/llvm/blocks.ts index eee4d00b9..3ae65cb0f 100644 --- a/packages/compiler/src/backend/llvm/blocks.ts +++ b/packages/compiler/src/backend/llvm/blocks.ts @@ -103,8 +103,8 @@ export class BlockBuilder { } } - br(label: string): void { - this.terminate(`br label %${label}`); + br(label: string, attachment = ""): void { + this.terminate(`br label %${label}${attachment}`); } condBr(v: string, t: string, f: string, attachment = ""): void { diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index f472eec60..e7ba72784 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -87,9 +87,13 @@ test("teardowns of types with children of their own type free their blocks as on classes: [ { name: "Linked", + // The array field keeps the recursive teardown: a type whose every + // reference field holds its own type tears down in one loop + // (iterative-teardown.test.ts). fields: [ { name: "next", type: linked }, { name: "other", type: linked }, + { name: "tags", type: { kind: "array", elem: { kind: "string" } } }, ], methods: [], loc, diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 5ff7477f3..5941f5d5c 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -45,6 +45,7 @@ import { directDestroyTarget, segmentPrologueLines, segmentTeardownLines, + iterativeTeardownDefinitions, segmentedTeardown, usedDirectTargets, type DirectDestroy, @@ -1334,6 +1335,15 @@ export function emitClassShapes( } else { const freeBody: string[] = []; teardown(freeBody); + // A segmented class whose every reference field holds its own type and + // that has no collector header tears down in one loop (shapes.ts + // iterativeTeardownDefinitions). + const iterative = + segmented && + !traced && + refFields.every( + (f) => directDestroyTarget(host, f.type, directTargets)?.destroy === destroy, + ); defs.push( ...releaseBody( host, @@ -1345,6 +1355,18 @@ export function emitClassShapes( lifetime, chained, segmented, + false, + iterative + ? iterativeTeardownDefinitions( + host, + destroy, + struct, + refFields.map((f) => f.index), + `@${mangleClassRelease(cls.name)}`, + "weak", + count, + ) + : null, ), ``, ); diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 274370277..368d10ad3 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -1,4 +1,5 @@ /* LlEmitter's module-level output: module parts, helper definitions, library declarations and FFI callback definitions. */ +import { loopUnrollMetadata } from "./periodic-guards.js"; import { relieveHeapPressure } from "../../heap-pressure.js"; import { emitWasmModules } from "./wasm-modules.js"; import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js"; @@ -1118,6 +1119,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { out.push(...functionAttributes(emitter, hasNoInlineRecordClone)); out.push(``); out.push(...emitter.fieldAliasMetadata()); + out.push(...loopUnrollMetadata(out)); if (emitter.debug !== null) out.push(emitter.debug.render()); return out; } @@ -1299,6 +1301,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { ); } out.push(...emitter.fieldAliasMetadata()); + out.push(...loopUnrollMetadata(out)); if (emitter.debug !== null) out.push(emitter.debug.render()); return out; } diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index c1f3cf18f..45d341853 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -95,6 +95,7 @@ import { } from "./string-construction.js"; import { emitStringInputs } from "./string-lifetimes.js"; import { LOOPS } from "./loop-state.js"; +import { loopUnrollAttachment, periodicGuardPeriod } from "./periodic-guards.js"; import { loopAccessesTypedArrays } from "../loop-polls.js"; import { emitConstantChoice, matchConstantChoice } from "./constant-choices.js"; @@ -1315,6 +1316,13 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const lb = B.newLabel("loop.b"); const lu = s.update || capturedLets.length > 0 || workerBudget !== null ? B.newLabel("loop.u") : lc; + // A periodic guard on the counter (periodic-guards.ts): the back + // edge asks LLVM to unroll the loop by the guard's period. + const period = + countedLoop && integerSlot + ? periodicGuardPeriod(s, countedLoop.localId, countedLoop.start, countedLoop.step) + : null; + const backEdge = period === null ? "" : loopUnrollAttachment(period); B.br(lc); B.startBlock(lc); enterLoopHeader(emitter, lc); @@ -1375,7 +1383,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { }); emitter.emitBlock(s.body); emitter.jumpTargets.pop(); - B.br(lu); + B.br(lu, lu === lc ? backEdge : ""); if (lu !== lc) { B.startBlock(lu); emitter.emitWorkerLoopCheck(workerBudget); @@ -1408,7 +1416,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.line(`store i64 ${next}, ptr ${pairedSlots[i]}`); }); } else if (s.update) emitter.emitStmt(s.update); - B.br(lc); + B.br(lc, backEdge); } }); B.startBlock(le); diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index 037179d8e..335946d9d 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -409,12 +409,24 @@ export function emitSerializationExpr( } const dynV = emitBorrowedInput(host, e.value); const helper = host.dyn.dynCheckHelper(e.type, e.preserveRefs); - if (isDynTypedRefType(e.type)) return emitClassCheck(host, e.type, dynV.name, helper, note); + // A check of an analysis-proven fact (shape inference's overlay) + // names its site, so a failure reports a compiler bug. + const site = e.inferredSite !== undefined ? host.cstr(e.inferredSite) : null; + if (site !== null) { + host.declare(`declare void @scr_dyn_inferred_site(ptr)`); + B.line(`call void @scr_dyn_inferred_site(ptr ${site})`); + } + if (isDynTypedRefType(e.type)) { + const checked = emitClassCheck(host, e.type, dynV.name, helper, note); + if (site !== null) B.line(`call void @scr_dyn_inferred_site(ptr null)`); + return checked; + } const ty = host.llType(e.type); const t = B.tmp(); B.line( `${t} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr ${dynV.name}, ptr null)`, ); + if (site !== null) B.line(`call void @scr_dyn_inferred_site(ptr null)`); const out = host.own({ name: t, type: e.type }); host.emitPendingCheck(undefined, false, undefined, note); return out; diff --git a/packages/compiler/src/backend/llvm/expr-records.ts b/packages/compiler/src/backend/llvm/expr-records.ts index af4bb092f..58f1297b3 100644 --- a/packages/compiler/src/backend/llvm/expr-records.ts +++ b/packages/compiler/src/backend/llvm/expr-records.ts @@ -195,9 +195,31 @@ function emitReplaceWith( return host.own({ name: t, type: e.type }); } +/** Substitutions whose result owns its text: once the runtime call returns, + * no operand is read again. */ +function releasesInputsOnReturn(method: string): boolean { + return method === "replace" || method === "replaceAll"; +} + export function emitRegexIntrinsic( host: LlvmEmitterContext, e: IrExpr & { kind: "regexIntrinsic" }, +): LlValue { + if (!releasesInputsOnReturn(e.method)) return emitRegexOperation(host, e); + // The operands' temps (the subject of a chained substitution, a computed + // replacement) are released when the call returns rather than at the + // end of the statement, so a chain of substitutions holds at most its + // current subject and result. + host.frames.push([]); + const out = emitRegexOperation(host, e); + host.toFrame(out); + host.releaseTemps(host.frames.pop()!); + return host.own(out); +} + +function emitRegexOperation( + host: LlvmEmitterContext, + e: IrExpr & { kind: "regexIntrinsic" }, ): LlValue { const B = host.B; const method = e.method; diff --git a/packages/compiler/src/backend/llvm/iterative-teardown.test.ts b/packages/compiler/src/backend/llvm/iterative-teardown.test.ts new file mode 100644 index 000000000..befa258e8 --- /dev/null +++ b/packages/compiler/src/backend/llvm/iterative-teardown.test.ts @@ -0,0 +1,68 @@ +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 "../../index.js"; +import { emitLlvmModule } from "./emitter.js"; + +async function moduleFor(source: string) { + const dir = await mkdtemp(join(tmpdir(), "scriptc-iterative-teardown-")); + try { + const entry = join(dir, "main.ts"), + output = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n")); + return deserializeModule(await readFile(output, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const SOURCE = ` +interface TreeNode { left: TreeNode | null; right: TreeNode | null } +class ListNode { + next: ListNode | null; + value: number; + constructor(next: ListNode | null, value: number) { this.next = next; this.value = value; } +} +interface Labeled { child: Labeled | null; label: string[] } +function tree(d: number): TreeNode { return d > 0 ? { left: tree(d - 1), right: tree(d - 1) } : { left: null, right: null }; } +function size(n: TreeNode | null): number { return n === null ? 0 : 1 + size(n.left) + size(n.right); } +let list: ListNode | null = null; +for (let i = 0; i < 10; i++) list = new ListNode(list, i); +let labeled: Labeled | null = { child: { child: null, label: ["b"] }, label: ["a"] }; +console.log(size(tree(4)), list!.value, labeled!.label.length); +list = null; +labeled = null; +`; + +/** The definitions whose symbols end in `suffix`. */ +function definitions(ir: string, suffix: string): string[] { + return [ + ...ir.matchAll(new RegExp(`^define internal [^\\n]*@([\\w.$%-]+${suffix})\\(`, "gm")), + ].map((m) => m[1]!); +} + +test("self-recursive records and classes tear down in one loop", async () => { + const ir = emitLlvmModule(await moduleFor(SOURCE), { targetTriple: "x86_64-unknown-linux-gnu" }); + const loops = definitions(ir, "_destroy_it"); + // The tree record and the list class; the labeled record also holds an + // array, so it keeps the recursive teardown. + expect(loops.length).toBe(2); + expect(loops.some((name) => name.includes("ListNode"))).toBe(true); + for (const name of loops) { + const body = new RegExp( + `^define internal void @${name.replace(/[$.]/g, "\\$&")}\\(ptr %root\\)[^]*?^}`, + "m", + ).exec(ir)![0]; + // No call to a teardown: dead children go onto the pending list. + expect(body).not.toMatch(/call [^\n]*_destroy/); + expect(body).toContain("inttoptr"); + const base = name.slice(0, -"_it".length); + expect(ir).toContain(`define internal void @${base}(ptr %o)`); + expect(ir).not.toContain(`@${base}_s(`); + } + expect(definitions(ir, "_destroy_s").length).toBe(1); +}); diff --git a/packages/compiler/src/backend/llvm/lib-network.ts b/packages/compiler/src/backend/llvm/lib-network.ts index 63661ab4c..5136d4aa0 100644 --- a/packages/compiler/src/backend/llvm/lib-network.ts +++ b/packages/compiler/src/backend/llvm/lib-network.ts @@ -401,6 +401,22 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) host.emitPendingCheck(); return out; } + if (e.fn === "net.connectPath" || e.fn === "net.connectPathCb") { + const args = e.args.map((a) => host.emitExpr(a)); + let cb = "null"; + if (e.fn === "net.connectPathCb") { + host.toHeap(args[2]!); + cb = args[2]!.name; + } + host.declare(`declare ptr @scr_net_connect_path_checked(ptr, i1 zeroext, ptr)`); + const t = B.tmp(); + B.line( + `${t} = call ptr @scr_net_connect_path_checked(ptr ${args[0]!.name}, i1 ${args[1]!.name}, ptr ${cb})`, + ); + const out = host.own({ name: t, type: e.type }); + host.emitPendingCheck(); + return out; + } if (e.fn === "net.sockOnData" || e.fn === "http.reqOnData") { const cbT = e.args[1]!.type; if (cbT.kind !== "func") diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index b2a0b676d..2fd4e49a9 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1982,6 +1982,8 @@ export const USES_TIMERS_LIB_FNS = new Set([ "net.listenOptsReusePortCb", "net.connect", "net.connectCb", + "net.connectPath", + "net.connectPathCb", "net.connectLookup", "net.connectLookupDyn", "net.connectAttempt", diff --git a/packages/compiler/src/backend/llvm/periodic-guards.test.ts b/packages/compiler/src/backend/llvm/periodic-guards.test.ts new file mode 100644 index 000000000..d1df4e7ab --- /dev/null +++ b/packages/compiler/src/backend/llvm/periodic-guards.test.ts @@ -0,0 +1,80 @@ +import { expect, test } from "vitest"; +import { countedFor } from "../../ir/build.js"; +import { BOOL, F64, type IrExpr, type IrStmt } from "../../ir/ir.js"; +import { + loopUnrollAttachment, + loopUnrollMetadata, + periodicGuardPeriod, +} from "./periodic-guards.js"; + +const loc = { file: "guards.ts", start: 0, end: 0 }; +const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); +const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +const bin = (op: string, left: IrExpr, right: IrExpr, type = F64): IrExpr => + ({ kind: "bin", op, left, right, type, loc }) as IrExpr; +const and = (left: IrExpr, right: IrExpr): IrExpr => ({ + kind: "logical", + op: "&&", + left, + right, + type: BOOL, + loc, +}); +const guard = (cond: IrExpr): IrStmt => ({ + kind: "if", + cond, + then: [{ kind: "break", loc } as IrStmt], + else_: null, + loc, +}); +/** `for (i = 0; i < 50; i++) { ...body }` */ +const loop = (body: IrStmt[]) => countedFor(loc, num(50), () => body) as IrStmt & { kind: "for" }; +const period = (body: IrStmt[], start = 0, step = 1) => + periodicGuardPeriod(loop(body), "i.0", num(start), step); + +test("a guard on the counter's low bits gives the loop its period", () => { + // if ((i & 3) === 3 && x > 4) break; + const masked = bin("===", bin("&", ref("i.0"), num(3)), num(3), BOOL); + expect(period([guard(and(masked, bin(">", ref("x"), num(4), BOOL)))])).toBe(4); + // if (0 == (7 & i)) ... + expect(period([guard(bin("==", num(0), bin("&", num(7), ref("i.0")), BOOL))])).toBe(8); + expect(period([guard(bin("!==", bin("&", ref("i.0"), num(1)), num(0), BOOL))])).toBe(2); +}); + +test("other guards and loops get no period", () => { + const masked = (mask: number, value: number, counter = "i.0") => + guard(bin("===", bin("&", ref(counter), num(mask)), num(value), BOOL)); + // Masks that are not 1, 3 or 7, values outside the mask, another variable. + expect(period([masked(5, 1)])).toBeNull(); + expect(period([masked(15, 15)])).toBeNull(); + expect(period([masked(3, 4)])).toBeNull(); + expect(period([masked(3, 3, "j")])).toBeNull(); + // The masked test is not the condition's first conjunct. + const later = and( + bin(">", ref("x"), num(4), BOOL), + bin("===", bin("&", ref("i.0"), num(3)), num(3), BOOL), + ); + expect(period([guard(later)])).toBeNull(); + // A start that is not a multiple of the period, a step other than 1. + expect(period([masked(3, 3)], 1)).toBeNull(); + expect(period([masked(3, 3)], 0, 2)).toBeNull(); + // A nested loop. + expect(period([masked(3, 3), loop([])])).toBeNull(); +}); + +test("a large body is not unrolled", () => { + const work: IrStmt[] = []; + for (let k = 0; k < 400; k++) + work.push({ kind: "assign", localId: "x", value: bin("+", ref("x"), num(k)), loc } as IrStmt); + expect( + period([guard(bin("===", bin("&", ref("i.0"), num(3)), num(3), BOOL)), ...work]), + ).toBeNull(); +}); + +test("the module carries the loop ID nodes its back edges refer to", () => { + const lines = [` br label %loop.c1${loopUnrollAttachment(4)}`, " ret void"]; + expect(loopUnrollMetadata(lines)).toEqual([ + `!1000004 = distinct !{!1000004, !{!"llvm.loop.unroll.count", i32 4}}`, + ]); + expect(loopUnrollMetadata([" ret void"])).toEqual([]); +}); diff --git a/packages/compiler/src/backend/llvm/periodic-guards.ts b/packages/compiler/src/backend/llvm/periodic-guards.ts new file mode 100644 index 000000000..3a07126db --- /dev/null +++ b/packages/compiler/src/backend/llvm/periodic-guards.ts @@ -0,0 +1,97 @@ +/* Counted loops with a periodic guard: a branch on the counter's low bits, + * such as `if ((j & 3) === 3 && ...)` (a check every fourth iteration). + * LLVM checks such a guard on every iteration, and when the guarded + * condition is cheap it speculates the condition into the loop. The loop's + * back edge asks LLVM to unroll it by the guard's period + * (`llvm.loop.unroll.count`): in each unrolled copy the counter's low bits + * are known, so the guard folds to a constant, and only one copy in each + * period keeps the check. The metadata changes how LLVM compiles the loop, + * not what it computes. */ +import type { IrExpr, IrStmt } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; + +/** The most IR nodes a loop body may have, times the period, for the loop + * to be unrolled: a larger body gains less than its unrolled copies cost. */ +const UNROLLED_BODY_NODES = 4096; + +/** The loop IDs, one per period (2, 4 and 8). A loop ID is a distinct + * node whose first operand is itself; loops with the same period share + * one, as the copies of an inlined loop do. */ +const loopIds = new Map([ + [2, 1_000_002], + [4, 1_000_004], + [8, 1_000_008], +]); + +/** The guard period of `loop`, a counted loop on `counter` that starts at + * `start` and steps by `step`, or null. The period is M + 1 where the body + * has an `if` whose condition is, or starts with an `&&` of, + * `(counter & M) === C` (or `==`, `!==`, `!=`, either operand order) with + * M one of 1, 3 and 7 and C an integer from 0 to M. The loop steps by 1 + * from a multiple of the period, so that the counter's low bits are known + * in each unrolled copy, and its body has no nested loop. */ +export function periodicGuardPeriod( + loop: IrStmt & { kind: "for" }, + counter: string, + start: IrExpr, + step: number, +): number | null { + if (step !== 1 || start.kind !== "numLit") return null; + let period: number | null = null; + let nodes = 0; + let plain = true; + const expr = (e: IrExpr): boolean => { + nodes++; + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + nodes++; + if (s.kind === "for" || s.kind === "while" || s.kind === "doWhile" || s.kind === "forOf") { + plain = false; + return false; + } + if (s.kind === "if" && period === null) period = guardPeriod(s.cond, counter); + return everyStmtChild(s, expr, stmt); + }; + for (const s of loop.body) if (!stmt(s)) break; + if (!plain || period === null) return null; + if (start.value % period !== 0 || nodes * period > UNROLLED_BODY_NODES) return null; + return period; +} + +function guardPeriod(cond: IrExpr, counter: string): number | null { + let test = cond; + while (test.kind === "logical" && test.op === "&&") test = test.left; + if (test.kind !== "bin" || !["===", "==", "!==", "!="].includes(test.op)) return null; + const masked = (e: IrExpr, other: IrExpr): number | null => { + if (e.kind !== "bin" || e.op !== "&" || other.kind !== "numLit") return null; + const [ref, mask] = e.left.kind === "varRef" ? [e.left, e.right] : [e.right, e.left]; + if (ref.kind !== "varRef" || ref.localId !== counter || mask.kind !== "numLit") return null; + const m = mask.value; + if (m !== 1 && m !== 3 && m !== 7) return null; + const c = other.value; + return Number.isInteger(c) && c >= 0 && c <= m ? m + 1 : null; + }; + return masked(test.left, test.right) ?? masked(test.right, test.left); +} + +/** The attachment that asks LLVM to unroll a loop by `period`, for its + * back edge. */ +export function loopUnrollAttachment(period: number): string { + return `, !llvm.loop !${loopIds.get(period)!}`; +} + +/** The loop ID nodes that the module's text `lines` refer to. */ +export function loopUnrollMetadata(lines: readonly string[]): string[] { + const used = new Set(); + for (const line of lines) { + if (!line.includes("!llvm.loop !")) continue; + for (const [period, id] of loopIds) if (line.includes(`!llvm.loop !${id}`)) used.add(period); + } + return [...used] + .sort((a, b) => a - b) + .map((period) => { + const id = loopIds.get(period)!; + return `!${id} = distinct !{!${id}, !{!"llvm.loop.unroll.count", i32 ${period}}}`; + }); +} diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 5b76886b2..ea429d71a 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -35,6 +35,7 @@ import { emitObjectAlloc, emitObjectFree, emitSegmentFree, + type FreeObservers, CYC_APPEND, cycleAppendDecls, cycleAppendHelper, @@ -1820,6 +1821,115 @@ function sizeLiteral(sizeType: RcCount, value: bigint): string { return BigInt.asIntN(sizeType === "i64" ? 64 : 32, value).toString(); } +/** The teardown of a type whose reference fields all hold its own type + * (`fields`: their struct indices; `release`: the type's inlinable release; + * no collector header): one loop instead of a call per node. A child whose + * last reference the dead node held joins a pending list linked through its + * own count word, which it no longer needs; the loop takes the list's first + * node next, so nodes are visited parent first, fields in order. Each node + * releases its block as emitObjectFree does, keeping a block of the calling + * thread's arena in a segment linked through its first word, and the segment + * goes onto the free list in one step at the end (emitSegmentFree): the next + * structure built parent first takes the blocks in the order it allocates + * them, as segmentTeardownLines arranges for the recursive form. The loop + * needs no depth budget. `_d` (the direct-chain entry other teardowns call) + * and the plain entry both run it. A class that keeps its id in the count + * word counts in the low half (`count`); a dead node's whole word is free. */ +export function iterativeTeardownDefinitions( + host: ShapeHost, + destroy: string, + struct: string, + fields: readonly number[], + release: string, + observers: FreeObservers, + count: RcCount = host.sizeType, +): string[] { + const S = host.sizeType; + const lines = [ + `define internal void @${destroy}_it(ptr %root) ${FN_ATTRS} {`, + `entry:`, + ` %sg.own = alloca ptr`, + ` br label %it.node`, + `it.node:`, + ` %o = phi ptr [ %root, %entry ], [ %it.top, %it.next ]`, + ` %it.pend = phi ptr [ null, %entry ], [ %it.rest, %it.next ]`, + ` %it.first = phi ptr [ null, %entry ], [ %it.fa, %it.next ]`, + ` %it.last = phi ptr [ null, %entry ], [ %it.la, %it.next ]`, + ...fields.flatMap((index, i) => [ + ` %it.f${i} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${index}`, + ` %it.v${i} = load ptr, ptr %it.f${i}`, + ]), + ` store ptr null, ptr %sg.own`, + ...emitObjectFree(host, false, observers, "%sg.own"), + ` br label %it.freed`, + `it.freed:`, + ` %it.k = load ptr, ptr %sg.own`, + ` %it.kept = icmp ne ptr %it.k, null`, + ` br i1 %it.kept, label %it.app, label %it.kids`, + `it.app:`, + ` %it.none = icmp eq ptr %it.first, null`, + ` br i1 %it.none, label %it.appd, label %it.link`, + `it.link:`, + ` store ptr %it.k, ptr %it.last`, + ` br label %it.appd`, + `it.appd:`, + ` %it.nf = select i1 %it.none, ptr %it.k, ptr %it.first`, + ` br label %it.kids`, + `it.kids:`, + ` %it.fa = phi ptr [ %it.nf, %it.appd ], [ %it.first, %it.freed ]`, + ` %it.la = phi ptr [ %it.k, %it.appd ], [ %it.last, %it.freed ]`, + ]; + // The fields go onto the list last first, so the first field is next. + let pend = "%it.pend"; + let from = "it.kids"; + for (let i = fields.length - 1; i >= 0; i--) { + const v = `%it.v${i}`; + lines.push( + ` %it.nn${i} = icmp ne ptr ${v}, null`, + ` br i1 %it.nn${i}, label %it.chk${i}, label %it.j${i}`, + `it.chk${i}:`, + ` %it.rc${i} = load ${count}, ptr ${v}`, + ` %it.last${i} = icmp eq ${count} %it.rc${i}, 1`, + ` br i1 %it.last${i}, label %it.push${i}, label %it.sh${i}`, + `it.push${i}:`, + ` %it.pl${i} = ptrtoint ptr ${pend} to ${S}`, + ` store ${S} %it.pl${i}, ptr ${v}`, + ` br label %it.j${i}`, + `it.sh${i}:`, + ` call void ${release}(ptr ${v}) ; a shared or immortal child`, + ` br label %it.j${i}`, + `it.j${i}:`, + ` %it.p${i} = phi ptr [ ${pend}, %${from} ], [ ${v}, %it.push${i} ], [ ${pend}, %it.sh${i} ]`, + ); + pend = `%it.p${i}`; + from = `it.j${i}`; + } + lines.push( + ` %it.empty = icmp eq ptr ${pend}, null`, + ` br i1 %it.empty, label %it.done, label %it.next`, + `it.next:`, + ` %it.top = getelementptr i8, ptr ${pend}, i64 0`, + ` %it.lw = load ${S}, ptr %it.top`, + ` %it.rest = inttoptr ${S} %it.lw to ptr`, + ` br label %it.node`, + `it.done:`, + ...emitSegmentFree(host, "sgf", "%it.fa", "%it.la"), + ` ret void`, + `}`, + `define internal void @${destroy}_d(ptr %o, i32 %d) ${FN_ATTRS} {`, + `entry:`, + ` call void @${destroy}_it(ptr %o)`, + ` ret void`, + `}`, + `define internal void @${destroy}(ptr %o) ${FN_ATTRS} {`, + `entry:`, + ` call void @${destroy}_it(ptr %o)`, + ` ret void`, + `}`, + ); + return lines; +} + /** Common NULL/immortal/decrement skeleton for ordinary object releases. * `freeBody` owns the zero-ref teardown and must leave the current block at * its end; traced objects also get the possible-cycle-root branch. @@ -1838,6 +1948,7 @@ export function releaseBody( chained = false, segmented = false, frozen = false, + destroyDefs: readonly string[] | null = null, ): string[] { const S = host.sizeType; if (bounded) host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); @@ -1845,7 +1956,9 @@ export function releaseBody( // A freezable record's count is the word without its frozen bit. const notFrozen = sizeLiteral(S, ~recordFrozenBit(S)); const lines = [ - ...(bounded ? destroyDefinitions(destroy, freeBody, chained, segmented ? host : null) : []), + ...(bounded + ? (destroyDefs ?? destroyDefinitions(destroy, freeBody, chained, segmented ? host : null)) + : []), ...releaseFastPath(host, fnName, `${fnName}_slow`, traced, comment, immortal, S, frozen), `define internal void @${fnName}_slow(ptr %o) noinline ${FN_ATTRS} {${comment ? ` ; ${comment}` : ""}`, `entry:`, @@ -2141,6 +2254,11 @@ export function emitRecordShapes( ), ); else freeBody.push(...emitObjectFree(host, traced, observers)); + // A segmented shape whose every reference field holds its own type, with + // no collector header and no frozen bit in its count, tears down in one + // loop (iterativeTeardownDefinitions). + const iterative = + segmented && !traced && shape.freezable !== true && ownType.length === refMembers.length; defs.push( ...releaseBody( host, @@ -2153,6 +2271,16 @@ export function emitRecordShapes( chained, segmented, shape.freezable === true, + iterative + ? iterativeTeardownDefinitions( + host, + destroy, + struct, + refMembers.map((m) => m.index), + `@${mangleRecordRelease(shape.id)}`, + observers, + ) + : null, ), ``, ); diff --git a/packages/compiler/src/backend/llvm/unwind-abi.ts b/packages/compiler/src/backend/llvm/unwind-abi.ts index 91722fb1d..821c5351b 100644 --- a/packages/compiler/src/backend/llvm/unwind-abi.ts +++ b/packages/compiler/src/backend/llvm/unwind-abi.ts @@ -2,6 +2,7 @@ * pending flag (see unwindAbiFunctions). */ import type { IrExpr, IrModule } from "../../ir/ir.js"; import { everyStmtList } from "../../ir/traverse.js"; +import { eachStringField } from "../../ir/fields.generated.js"; /** How many calls above a `throw` functions keep the pending flag. */ const THROW_LEVELS = 3; @@ -129,13 +130,14 @@ export function unwindAbiFunctions( } // Any function a node names other than as a call's callee may be reached // through a pointer. + // Every string field counts (eachStringField), as a key walk of the node + // would find them; a call's callee does not. const referenced = new Set(); - const noteNames = (node: object, skip: string | undefined): void => { - for (const key in node) { - if (key === skip) continue; - const value = (node as Record)[key]; - if (typeof value === "string" && names.has(value)) referenced.add(value); - } + const noteName = (value: string): void => { + if (names.has(value)) referenced.add(value); + }; + const noteCallName = (value: string, field: string): void => { + if (field !== "callee") noteName(value); }; // The call graph, for the functions near a throw and the recursive ones. // Calls through values (closures, class values, dynamic calls, callbacks @@ -171,10 +173,10 @@ export function unwindAbiFunctions( // flag they keep landing pads out of the top-level code inlined // into the entry. if (coroutine || fn.name === mod.entry) blocked.add(e.callee); - noteNames(e, "callee"); + eachStringField(e, noteCallName); return true; } - noteNames(e, undefined); + eachStringField(e, noteName); if (e.kind === "virtualCall") for (const target of e.targets ?? methodsNamed.get(e.method) ?? []) edge(fn.name, target); else if (e.kind === "new") { @@ -193,7 +195,7 @@ export function unwindAbiFunctions( stmt: (s) => { const kind = s.kind as string; if (kind === "throw" || kind === "rethrow") throws.add(fn.name); - noteNames(s, undefined); + eachStringField(s, noteName); return true; }, }); diff --git a/packages/compiler/src/cli/command.ts b/packages/compiler/src/cli/command.ts index 8ddfa7585..020d3eb4b 100644 --- a/packages/compiler/src/cli/command.ts +++ b/packages/compiler/src/cli/command.ts @@ -649,6 +649,7 @@ async function main(args: string[], host: CliHost): Promise { ? {} : { profileUse: resolve(values["profile-use"]) }), ...(npmStatic !== undefined ? { npmStatic } : {}), + ...(values["shape-overlay"] === false ? { shapeOverlay: false } : {}), ...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}), ...(printNativeLinkInfo ? { nativeLinkInfo: true } : {}), ...(tier !== undefined ? { tier } : {}), diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts index 727e0ed4f..84cd744b3 100644 --- a/packages/compiler/src/cli/usage.ts +++ b/packages/compiler/src/cli/usage.ts @@ -109,6 +109,10 @@ Options: "pkg..." also names every package pkg depends on). Packages that fail preflight are refused with a diagnostic. Eligible dependencies compile automatically. + --no-shape-overlay + compile npm packages' JavaScript without the types shape + inference proves (per package: "scriptc": {"packages": + {"": {"shapes": false}}} in package.json) --provenance-sources EXPERIMENTAL: compile npm dependencies from their provenance-attested SOURCE (fetched at the attested @@ -186,6 +190,7 @@ export const CLI_OPTIONS = { sanitize: { type: "boolean", default: false }, ffi: { type: "string" }, "npm-static": { type: "string", multiple: true }, + "shape-overlay": { type: "boolean" }, "provenance-sources": { type: "boolean", default: false }, "external-types": { type: "string", multiple: true }, tier: { type: "string" }, diff --git a/packages/compiler/src/compile-types.ts b/packages/compiler/src/compile-types.ts index 72a6fa3cf..b5a5ac265 100644 --- a/packages/compiler/src/compile-types.ts +++ b/packages/compiler/src/compile-types.ts @@ -73,6 +73,10 @@ export interface CompileBaseOptions { /** Eligible packages compile automatically. Explicit names override automatic selection heuristics. */ npmStatic?: readonly string[] | "auto"; + /** Shape inference's typed overlay for opted-in packages' JavaScript. + * False disables it (`--no-shape-overlay`); a package opts out with + * `"scriptc": { "packages": { "": { "shapes": false } } }`. */ + shapeOverlay?: boolean; /** Outbound native FFI manifest. Its signature-only TypeScript bindings * lower to direct C ABI calls. Source outputs retain those declarations; * archive/system-library inputs join only an executable link. */ diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 291edd08b..51a08dc5d 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -64,7 +64,10 @@ * parses date strings with V8's grammar. SC61xx are performance * notes, also warnings (coverage --fail-on=dynamic opts in): * likely-hot functions that use untyped values (SC6101, - * diagnostics/dynamic-tier.ts) + * diagnostics/dynamic-tier.ts), package files whose projected + * declaration types were dropped (SC6102), and shape inference's + * overlay report: what it typed and how many inferred facts the + * build checks (SC6103, frontend/shape-inference) * SC8xxx Node islands: packages and modules outside the native subset * that run (or, in report-only mode, would run) in a Node * island — unknown project configuration (SC8004), a package diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 64d19e5fe..c5dca4297 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -175,6 +175,9 @@ export interface EarlyExecutableCacheOptions { /** Omitted unless SCRIPTC_YOUNG_NURSERY=1 (backend/young-nursery.ts). */ youngNursery?: true; npmStatic: readonly string[] | "auto" | null; + /** Omitted unless shape inference's overlay compiles the packages + * (frontend/shape-inference). */ + shapeOverlay?: true; /** Raw manifest identity: path and bytes. Native archives remain under the * stricter native cache's independent dependency validation. */ ffiProfile: { path: string; bytes: Uint8Array } | null; diff --git a/packages/compiler/src/frontend/canon-map.ts b/packages/compiler/src/frontend/canon-map.ts new file mode 100644 index 000000000..2dadc31cb --- /dev/null +++ b/packages/compiler/src/frontend/canon-map.ts @@ -0,0 +1,171 @@ +/* Canonical mapping (localized-failure design §4.3), stage N1: the IR type + * of a canonical node, built from about ten structural rules plus the + * nominal table. Nominal objects (classes, standard-library interfaces, + * handles), callables, index-signature shapes, tuples, bigint and symbol + * keep their per-type recognitions: they map through mapType on the + * checker type the node came from. Everything structural (primitives, + * literals, units, arrays, flat unions, plain records) maps here, from the + * canonical form alone. + * + * Shadow mode (SCRIPTC_CANON_SHADOW=1) compares this mapping with mapType + * for every successful top-level mapping and appends one JSON line per + * comparison to SCRIPTC_CANON_SHADOW_OUT. It maps into throwaway + * registries, so the program's own IR is untouched. */ +import { appendFileSync } from "node:fs"; +import { BOOL, DYN, F64, NULL_T, STRING, UNDEFINED_T, typeKey, type IrType } from "../ir/ir.js"; +import { canonKey, Normalizer, type Canon, type CanonProp } from "./canon.js"; +import * as ts from "./ts7/adapter.js"; +import { + mapType, + ShapeRegistry, + UnionRegistry, + withUndefinedArm, + type TypeMapperCtx, +} from "./type-mapper.js"; + +const MAX_DEPTH = 24; + +/** The IR type of a canonical node, or null when no rule applies. */ +export function mapCanonical(c: Canon, ctx: TypeMapperCtx, depth = 0): IrType | null { + if (depth > MAX_DEPTH) return null; + const viaChecker = (): IrType | null => (c.src === undefined ? null : mapType(c.src, ctx)); + switch (c.k) { + case "prim": + return c.p === "number" + ? F64 + : c.p === "string" + ? STRING + : c.p === "boolean" + ? BOOL + : viaChecker(); + case "lit": + return typeof c.value === "number" ? F64 : typeof c.value === "string" ? STRING : BOOL; + case "unit": + return c.u === "undefined" ? UNDEFINED_T : c.u === "null" ? NULL_T : viaChecker(); + case "top": + return DYN; + case "param": + // A class's type parameters bind at IR level only (a generic class + // instance's bindings); the lowerer's resolver answers them. + // `this` in a class body is the class itself: the nominal table. + if (c.src === undefined) return null; + if ((c.src as { isThisType?: boolean }).isThisType === true) return viaChecker(); + return ctx.resolveTypeParam?.(c.src) ?? null; + case "opaque": + case "tuple": + return viaChecker(); + case "array": { + const elem = mapCanonical(c.elem, ctx, depth + 1); + return elem === null ? null : { kind: "array", elem }; + } + case "union": { + const byKey = new Map(); + for (const arm of c.arms) { + const mapped = mapCanonical(arm, ctx, depth + 1); + if (mapped === null || mapped.kind === "void") return null; + if (mapped.kind === "union") { + const def = ctx.unions.get(mapped.unionId); + if (def === undefined) return null; + for (const a of def.arms) byKey.set(typeKey(a), a); + } else byKey.set(typeKey(mapped), mapped); + } + const arms = [...byKey.values()]; + if (arms.length === 1) return arms[0]!; + if (arms.some((a) => a.kind === "dyn")) return DYN; + arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + return { kind: "union", unionId: ctx.unions.intern(arms) }; + } + case "obj": { + if (c.nominal !== undefined || c.call().length > 0 || c.construct().length > 0) + return viaChecker(); + if (c.index().length > 0) return viaChecker(); + const props = c.props(); + // `{}` holds any non-nullish value: the checked dynamic, as mapType. + if (props.length === 0) return DYN; + const structural = mapCanonicalRecord(props, ctx, depth); + // An object whose members do not map structurally (an interface over + // Map or Set, methods) keeps its per-type recognition. + return structural ?? viaChecker(); + } + } +} + +/** A plain record from an object's canonical members, or null when a + * member does not map. */ +function mapCanonicalRecord( + props: readonly CanonProp[], + ctx: TypeMapperCtx, + depth: number, +): IrType | null { + const fields: { name: string; type: IrType }[] = []; + for (const p of props) { + let t = mapCanonical(p.type(), ctx, depth + 1); + if (t === null) return null; + if (p.optional && t.kind !== "dyn") { + const armed = withUndefinedArm(t, ctx.unions); + if (armed === null) return null; + t = armed; + } + fields.push({ name: p.name, type: t }); + } + const declaredOrder = fields.map((f) => f.name); + fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); + return { kind: "record", shapeId: ctx.shapes.intern(fields, false, undefined, declaredOrder) }; +} + +export type ShadowStatus = "equal" | "diverge" | "canon-fail"; + +/** Installs nothing unless SCRIPTC_CANON_SHADOW=1: the hook mapType calls + * after each successful top-level mapping. */ +export function canonShadowHook( + checker: ts.TypeChecker, +): ((type: ts.Type, mapped: IrType, ctx: TypeMapperCtx) => void) | undefined { + if (process.env["SCRIPTC_CANON_SHADOW"] !== "1") return undefined; + const out = process.env["SCRIPTC_CANON_SHADOW_OUT"]; + let busy = false; + return (type, mapped, ctx) => { + if (busy) return; + busy = true; + // The shadow records and must never break a compile. + try { + const resolve = ctx.resolveTypeParamTs; + const normalizer = new Normalizer(checker); + const c = normalizer.canon(type, resolve ? (t) => resolve(t) : new Map()); + const { typeMemo: _memo, canonShadow: _hook, ...rest } = ctx; + const shadowCtx: TypeMapperCtx = { + ...rest, + shapes: new ShapeRegistry(), + unions: new UnionRegistry(), + canMemoizeType: () => false, + }; + let canonical: IrType | null = null; + try { + canonical = mapCanonical(c, shadowCtx); + } catch { + canonical = null; + } + const status: ShadowStatus = + canonical === null + ? "canon-fail" + : typeKey(canonical) === typeKey(mapped) + ? "equal" + : "diverge"; + if (out !== undefined) + appendFileSync( + out, + `${JSON.stringify({ + status, + kind: c.k, + type: checker.typeToString(type).slice(0, 160), + canon: canonKey(c).slice(0, 160), + strict: typeKey(mapped), + ...(canonical !== null ? { canonical: typeKey(canonical) } : {}), + })}\n`, + ); + } catch { + // A census line lost, nothing else. + } finally { + busy = false; + } + }; +} diff --git a/packages/compiler/src/frontend/canon.ts b/packages/compiler/src/frontend/canon.ts index 5d75ae806..3ec18a46b 100644 --- a/packages/compiler/src/frontend/canon.ts +++ b/packages/compiler/src/frontend/canon.ts @@ -44,7 +44,13 @@ export interface AliasRef { tsType: ts.Type; } -export type Canon = +export type Canon = CanonNode & { + /** The checker type the node was normalized from (a type parameter's + * binding for substituted parameters). */ + src?: ts.Type; +}; + +type CanonNode = | { k: "prim"; p: CanonPrim } | { k: "lit"; value: string | number | boolean } | { k: "unit"; u: "undefined" | "null" | "void" | "never" } @@ -67,8 +73,10 @@ export type Canon = | { k: "param"; name: string } | { k: "opaque"; ts: ts.Type; reason: string }; -/** Type-parameter bindings: the checker type a parameter is bound to. */ -export type CanonEnv = ReadonlyMap; +/** Type-parameter bindings: the checker type a parameter is bound to, by + * the parameter's symbol, or a resolver over the parameter type (the + * lowerer's resolveTypeParamTs). */ +export type CanonEnv = ReadonlyMap | ((param: ts.Type) => ts.Type | null); const PRIM_FLAGS: readonly [number, CanonPrim][] = [ [ts.TypeFlags.Number, "number"], @@ -78,9 +86,23 @@ const PRIM_FLAGS: readonly [number, CanonPrim][] = [ [ts.TypeFlags.ESSymbol | ts.TypeFlags.UniqueESSymbol, "symbol"], ]; -/** A stable key for a canonical node, for deduplication and tests. */ -export function canonKey(c: Canon, depth = 0): string { - if (depth > 6) return "…"; +/** A stable key for a canonical node, for deduplication and tests. Deep + * and wide types are cut off (depth 6, about 4,000 characters), so equal + * keys mean equal up to that horizon. */ +export function canonKey(c: Canon): string { + const budget = { left: 4000 }; + return canonKeyAt(c, 0, budget); +} + +function canonKeyAt(c: Canon, depth: number, budget: { left: number }): string { + const key = canonKeyInner(c, depth, budget); + budget.left -= key.length; + return key; +} + +function canonKeyInner(c: Canon, depth: number, budget: { left: number }): string { + if (depth > 6 || budget.left <= 0) return "…"; + const canonKey = (n: Canon, d: number) => canonKeyAt(n, d, budget); switch (c.k) { case "prim": return c.p; @@ -259,11 +281,15 @@ export class Normalizer { if (known !== undefined) return known; } const node = this.build(type, env, key); + if (node.src === undefined) node.src = type; if (key !== null && !this.interned.has(key)) this.interned.set(key, node); return node; } private envKey(env: CanonEnv): string { + // A resolver's bindings are the caller's current instantiation: a + // normalizer is used for one instantiation at a time. + if (typeof env === "function") return "fn"; if (env.size === 0) return ""; const parts: string[] = []; for (const [param, bound] of env) { @@ -294,7 +320,12 @@ export class Normalizer { for (const [flag, p] of PRIM_FLAGS) if (flags & flag) return { k: "prim", p }; if (flags & ts.TypeFlags.TypeParameter) { const symbol = type.getSymbol(); - const bound = symbol === undefined ? undefined : env.get(symbol); + const bound = + typeof env === "function" + ? (env(type) ?? undefined) + : symbol === undefined + ? undefined + : env.get(symbol); if (bound !== undefined && bound !== type) return this.canon(bound, env); return { k: "param", name: symbol === undefined ? "?" : ts.symbolName(symbol) }; } @@ -361,9 +392,23 @@ export class Normalizer { return arms.length === 1 ? arms[0]! : { k: "union", arms }; } const prims = parts.filter((p) => p.k === "prim" || p.k === "lit"); - if (prims.length === 1 && parts.every((p) => p === prims[0] || p.k === "obj")) return prims[0]!; - if (parts.every((p) => p.k === "obj" || (p.k === "top" && p.t === "object"))) - return this.object(type, env, key); + if (prims.length === 1 && parts.every((p) => p.k === "prim" || p.k === "lit" || p.k === "obj")) + return prims[0]!; + if (parts.every((p) => p.k === "obj" || (p.k === "top" && p.t === "object"))) { + const merged = this.object(type, env, key); + // A class (or mixin instance) refined by other object types is that + // class, with its type arguments: the nominal table answers it, not a + // merged record. + let refined: { nominal: string; typeArgs: () => readonly Canon[] } | undefined; + for (const p of parts) + if (p.k === "obj" && p.nominal !== undefined) + refined ??= { nominal: p.nominal, typeArgs: p.typeArgs }; + if (refined !== undefined && merged.k === "obj" && merged.nominal === undefined) { + merged.nominal = refined.nominal; + merged.typeArgs = refined.typeArgs; + } + return merged; + } return { k: "opaque", ts: type, reason: "intersection" }; } @@ -406,6 +451,14 @@ export class Normalizer { }); return node; } + // A mapped type the checker could not resolve (over an unbound or + // symbolically bound parameter) shows no members: irreducible here. + if ( + ((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Mapped) !== 0 && + checker.getPropertiesOfType(type).length === 0 && + checker.getIndexInfosOfType(type).length === 0 + ) + return { k: "opaque", ts: type, reason: "unresolved mapped type" }; const symbol = type.getSymbol(); const decl = symbol?.declarations[0]; const nominal = diff --git a/packages/compiler/src/frontend/dts-paths.ts b/packages/compiler/src/frontend/dts-paths.ts index 638e58641..7e53a2a9b 100644 --- a/packages/compiler/src/frontend/dts-paths.ts +++ b/packages/compiler/src/frontend/dts-paths.ts @@ -7,13 +7,27 @@ export { tsgoPath } from "./ts7/session-path.js"; const require = createRequire(import.meta.url); let installedDeclarationRoot: string | null = null; +/** The paths `declarationPath` answered, by name, for the current root. + * Lowering asks for them once per mapped type (isFallbackDtsPath), and each + * answer without an installed root costs a package resolution. */ +const declarationPaths = new Map(); /** A native distribution supplies its relocatable declaration directory. */ export function setDeclarationRoot(root: string): void { installedDeclarationRoot = root; + declarationPaths.clear(); } function declarationPath(name: string): string { + let path = declarationPaths.get(name); + if (path === undefined) { + path = resolveDeclarationPath(name); + declarationPaths.set(name, path); + } + return path; +} + +function resolveDeclarationPath(name: string): string { return tsgoPath( installedDeclarationRoot === null ? name === "scriptc.d.ts" diff --git a/packages/compiler/src/frontend/input-tracker.test.ts b/packages/compiler/src/frontend/input-tracker.test.ts index d6f9a91b7..a320eb2bd 100644 --- a/packages/compiler/src/frontend/input-tracker.test.ts +++ b/packages/compiler/src/frontend/input-tracker.test.ts @@ -316,3 +316,41 @@ test("a file changed shortly before the read is hashed on every check", async () expect(frontendInputsStillMatch(snapshot)).toBe(true); expect(reads.mock.calls.filter(([path]) => path === source)).toHaveLength(1); }); + +test("a read before and a read after a file settles record one probe", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-")); + scratch.push(dir); + const source = join(dir, "entry.ts"); + await writeFile(source, "export const answer = 1;\n"); + const tracker = new FrontendInputTracker(); + // The first read comes right after the write, the second once the file + // has been unchanged long enough for its identity to stand for it. + tracker.run(() => trackedReadFile(source)); + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(Date.now() + 5000); + try { + tracker.run(() => trackedReadFile(source)); + } finally { + vi.useRealTimers(); + } + const snapshot = tracker.snapshot(); + expect(snapshot.stable).toBe(true); + const probe = snapshot.probes.find((entry) => entry.op === "file"); + expect(probe?.op === "file" && probe.identity !== undefined).toBe(true); + expect(frontendInputsStillMatch(snapshot)).toBe(true); + // The identity still invalidates the snapshot on any write. + await writeFile(source, "export const answer = 2;\n"); + expect(frontendInputsStillMatch(snapshot)).toBe(false); +}); + +test("two reads of a file that saw different content prevent cache publication", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-")); + scratch.push(dir); + const source = join(dir, "entry.ts"); + await writeFile(source, "export const answer = 1;\n"); + const tracker = new FrontendInputTracker(); + tracker.run(() => trackedReadFile(source)); + await writeFile(source, "export const answer = 2;\n"); + tracker.run(() => trackedReadFile(source)); + expect(tracker.snapshot().stable).toBe(false); +}); diff --git a/packages/compiler/src/frontend/input-tracker.ts b/packages/compiler/src/frontend/input-tracker.ts index 8b9f35fe3..5dd80dde5 100644 --- a/packages/compiler/src/frontend/input-tracker.ts +++ b/packages/compiler/src/frontend/input-tracker.ts @@ -86,6 +86,22 @@ function readWithIdentity(path: string): { text: string; identity: string | null return { text, identity: settled && after === identity ? identity : null }; } +/** Two reads of one file that saw the same content, as one probe: a read + * before the file had been unchanged for IDENTITY_SETTLE_MS records no + * identity, and a later read of the same bytes does. The probe keeps the + * identity, which the settled read proved for that content. Null when the + * reads differ in anything else. */ +function sameFileContent( + previous: FrontendInputProbe, + probe: FrontendInputProbe, +): FrontendInputProbe | null { + if (previous.op !== "file" || probe.op !== "file" || previous.digest !== probe.digest) + return null; + if (previous.identity === undefined) return probe; + if (probe.identity === undefined || probe.identity === previous.identity) return previous; + return null; +} + function pathKind(path: string): Extract["kind"] { try { const info = statSync(path); @@ -144,12 +160,17 @@ export class FrontendInputTracker { const key = `${probe.op}\0${probe.path}`; if (probe.op === "text" && this.probes.has(key)) return; const previous = this.probes.get(key); - if (previous !== undefined && JSON.stringify(previous) !== JSON.stringify(probe)) { + const merged = previous === undefined ? null : sameFileContent(previous, probe); + if ( + merged === null && + previous !== undefined && + JSON.stringify(previous) !== JSON.stringify(probe) + ) { // An input changed while this frontend was running. The build may // still finish normally, but it is not safe to publish as a cache key. this.stable = false; } - this.probes.set(key, probe); + this.probes.set(key, merged ?? probe); if (probe.op === "file") { // A successful content read supersedes earlier existence probes for the // same path only when they also observed a file. A missing/non-file diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index c7fb8d36e..cefe518dd 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -1,3 +1,4 @@ +import { shapeOverlayAnnotated } from "../shape-inference/index.js"; import { typedArgumentsCandidate, typedArgumentsShapes, @@ -181,6 +182,12 @@ export function checkedJsDocumentedParameter( lowerer: Lowerer, param: ts.ParameterDeclaration, ): boolean { + if ( + param.parent !== undefined && + shapeOverlayAnnotated(param.parent) && + documentedParameter(param) + ) + return true; return lowerer.checkedJsFile(ts.getSourceFileOfNode(param)) && documentedParameter(param); } diff --git a/packages/compiler/src/frontend/lowering/completion-callbacks.ts b/packages/compiler/src/frontend/lowering/completion-callbacks.ts index 3296b3979..a8ae99ec1 100644 --- a/packages/compiler/src/frontend/lowering/completion-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/completion-callbacks.ts @@ -45,6 +45,7 @@ const COMPLETION_CALLBACKS: ReadonlyMap = new Map< ["http.requestDynCb", "last"], ["http.requestUrlCb", "last"], ["net.connectCb", 2], + ["net.connectPathCb", 2], ["net.listenCb", 2], ["net.listenOptsCb", 4], ["net.listenOptsReusePortCb", 5], diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 72a4b2ef3..d7068ecb5 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -56,6 +56,7 @@ import { varRef } from "../../../ir/build.js"; import { ambientUndefReadType, ambientUndefinedFnSymbolOf, + ambientUndefinedVarSymbolOf, contextualUndefReadType, fenceEarlyAliasUse, isTemporalGlobalRoot, @@ -843,14 +844,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // Stdlib/@types/node ambients never reach here (their chokepoint // is above); only user-file declares do. { - const sym = lowerer.resolveValueSymbol(expr); - const decl = sym ? sym.declarations[0] : undefined; - if ( - decl !== undefined && - ts.isVariableDeclaration(decl) && - decl.initializer === undefined && - ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Ambient - ) { + if (ambientUndefinedVarSymbolOf(lowerer, expr)) { const declared = lowerer.mapTypeOf(lowerer.typeOf(expr)); if (declared && declared.kind !== "void") { return { diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 72d56d9d2..51d48b127 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -436,6 +436,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc loc, }; } + // Plain data (records, arrays, strings, numbers, booleans, and unions + // of them) is never a promise: false after the operand's evaluation. + const neverPromise = (t: IrType): boolean => + NEVER_PROMISE_KINDS.has(t.kind) || + (t.kind === "union" && (lowerer.unions.get(t.unionId)?.arms ?? [t]).every(neverPromise)); + if (neverPromise(left.type)) { + const no: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; + if (isSafeToDiscard(left)) return no; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: left, loc }], + result: no, + type: BOOL, + loc, + }; + } lowerer.unsupported( "SC1090", expr, @@ -2275,3 +2291,20 @@ function nodeDeclaredMember(lowerer: Lowerer, t: ts.Type, key: string): boolean ); }); } + +const NEVER_PROMISE_KINDS: ReadonlySet = new Set([ + "record", + "array", + "string", + "f64", + "bool", + "bigint", + "date", + "map", + "set", + "regex", + "bytes", + "symbol", + "nullT", + "undefinedT", +]); diff --git a/packages/compiler/src/frontend/lowering/expressions/typeof.ts b/packages/compiler/src/frontend/lowering/expressions/typeof.ts index f6743768a..5f1e4a830 100644 --- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts +++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts @@ -34,7 +34,11 @@ import { varRef } from "../../../ir/build.js"; import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors.js"; import { InternalCompilerError } from "../../../errors.js"; import { TEMPORAL_GLOBAL } from "../../builtin-modules.js"; -import { isTemporalGlobalRoot } from "../lower-namespaces.js"; +import { + ambientUndefinedFnSymbolOf, + ambientUndefinedVarSymbolOf, + isTemporalGlobalRoot, +} from "../lower-namespaces.js"; import { isF64HandleTyped } from "../f64-handles.js"; export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc: SrcLoc): IrExpr { @@ -220,6 +224,16 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc if (isTemporalGlobalRoot(lowerer, tested, sym)) { return { kind: "strLit", value: "undefined", type: STRING, loc }; } + // A name the program declares ambiently but nothing defines + // (`declare const Bun`, `declare function f(): void`): Node erases the + // declaration, so typeof answers "undefined" without reading it, where + // a read throws ReferenceError. + if ( + ambientUndefinedVarSymbolOf(lowerer, tested) || + ambientUndefinedFnSymbolOf(lowerer, tested) + ) { + return { kind: "strLit", value: "undefined", type: STRING, loc }; + } // `const { inspect } = require("util")` / `import { inspect } from // "node:util"`: a binding of a builtin module's exported function, // class or object. JavaScript sources represent such a value without diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 1769e8562..647ca52a1 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -1,3 +1,5 @@ +import { shapeOverlayAnnotated } from "../shape-inference/index.js"; +import { documentedParameter } from "./call-signatures.js"; import { staticNameBindings } from "./static-function-names.js"; import { positionTag } from "./structural-positions.js"; import { keyedHelperName } from "./stable-names.js"; @@ -1863,10 +1865,14 @@ export function implicitAnyParamSymbolsOf( // The variadic-`arguments` form keeps its dynRest story whole. if (bodyReadsArguments(decl)) return null; let hasImplicitParams = false; + const overlayTyped = shapeOverlayAnnotated(decl); const out = decl.parameters.nodes.map((param): ts.Symbol | null => { if (!ts.isIdentifier(param.name)) return null; if (param.dotDotDotToken || param.questionToken || param.initializer) return null; if (param.name.text === "this") return null; + // A parameter typed by shape inference's overlay is a proven type, + // not an implicit type parameter. + if (overlayTyped && documentedParameter(param)) return null; const t = lowerer.typeOf(param.name); const broadFunction = lowerer.checker.typeToString(t) === "Function"; const broadArray = lowerer.checkerAnyArrayType(t); diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index c4d1aa6fb..80919a251 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,4 +1,5 @@ import { lowerAsyncLocalStorageOptions } from "./builtins/async-context.js"; +import { shapeOverlayAnnotated } from "../shape-inference/index.js"; import { DATE_TIME_FIELD, dateRootOf, @@ -1181,7 +1182,14 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E ) return DYN; } - if (type.kind === "array" || type.kind === "record") return DYN; + // Shape inference's overlay types a field as a record or array only when + // the analysis proves its values never reach code that could observe a + // copy, so those fields keep their native layout. + if ( + (type.kind === "array" || type.kind === "record") && + !shapeOverlayAnnotated(expression.parent?.parent ?? expression) + ) + return DYN; // JavaScript documentation cannot guarantee that a call or an indexed // read returns a present number. Numeric fields receiving those values // must retain undefined, so later arithmetic performs JS coercion (NaN). @@ -3374,6 +3382,7 @@ export function collectClassShapeInner( !functionLocalClass(decl) && ts.isIdentifier(member.name) && !lowerer.virtualJsMethods.has(member) && + !lowerer.checkedJsOverrideMethods.has(member) && !lowerer.prototypeMethodAccesses.has(member.name.text) && inst === undefined && decl.typeParameters === undefined && @@ -7692,6 +7701,16 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou if (!ts.isIdentifier(member.name)) continue; const property = lowerer.checker.getPropertyOfType(base, member.name.text); for (const declaration of property ? property.declarations : []) { + if ( + ts.isMethodDeclaration(declaration) && + isJsSourceFile(ts.getSourceFileOfNode(declaration)) && + checkedJsOverride(lowerer, declaration, member) + ) { + // A checked override pair dispatches through the vtable, + // so neither side may become an implicit generic. + lowerer.checkedJsOverrideMethods.add(declaration); + lowerer.checkedJsOverrideMethods.add(member); + } if ( ts.isMethodDeclaration(declaration) && isJsSourceFile(ts.getSourceFileOfNode(declaration)) && @@ -7778,7 +7797,7 @@ function checkedJsOverride( ): boolean { return [base, member].every( (method) => - lowerer.checkedJsFile(ts.getSourceFileOfNode(method)) && + (lowerer.checkedJsFile(ts.getSourceFileOfNode(method)) || shapeOverlayAnnotated(method)) && method.parameters.nodes.every(documentedParameter), ); } diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 13d1c29fc..4ea102769 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -676,6 +676,30 @@ export function ambientUndefReadType(lowerer: Lowerer, node: ts.Node): IrType | return null; } +/** The symbol of a variable a program file declares ambiently without + * an initializer (`declare const Bun: ...`), which nothing defines: Node + * erases the declaration, so a read throws the catchable ReferenceError + * " is not defined" and typeof answers "undefined". Null for other + * identifiers, including the standard library's declarations, which + * describe real runtime values. */ +export function ambientUndefinedVarSymbolOf( + lowerer: Lowerer, + ident: ts.Identifier, +): ts.Symbol | null { + const sym = lowerer.resolveValueSymbol(ident); + const decl = sym ? sym.declarations[0] : undefined; + if ( + sym === null || + decl === undefined || + lowerer.isStdlibSymbol(sym) || + !ts.isVariableDeclaration(decl) || + decl.initializer !== undefined || + !(ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Ambient) + ) + return null; + return sym; +} + /** The symbol behind `ident` when it names an ambient `declare function` * NOTHING defines: every function declaration on the symbol is body-less * and ambient in a USER program file (merge partners — the fundule's diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 614d6a206..d3132d02f 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -804,6 +804,19 @@ export function lowerNetModuleCall( `the supported form is ${bi.member}(port, host?, connectListener?)`, ); } + // connect(path[, connectListener]): a string first argument names a + // Unix socket, or a localhost port when it reads as a non-negative + // number (Node's isPipeName, decided at run time). + if (lowerer.mapTypeOf(lowerer.typeOf(args[0]!))?.kind === "string") { + return lowerNetConnectPath( + lowerer, + expr, + bi.member, + lowerer.lowerExpr(args[0]!), + true, + args[1], + ); + } const port = lowerer.lowerExprExpecting(args[0]!, F64); // The optional middle host: a 2-arg call's second argument is the // host when string-typed, the connect listener when func-typed. @@ -841,6 +854,47 @@ export function lowerNetModuleCall( ); } +/** net.connect / net.createConnection of a Unix socket path (a string + * argument or the `path` option) with an optional connect listener. Node + * ignores any other argument between them for a path, so only a listener + * may follow. */ +function lowerNetConnectPath( + lowerer: Lowerer, + expr: ts.CallExpression, + member: string, + path: IrExpr, + pipeNameRule: boolean, + listener: ts.Expression | undefined, +): IrExpr { + const loc = locOf(expr); + const rule: IrExpr = { kind: "boolLit", value: pipeNameRule, type: BOOL, loc }; + if (expr.arguments.nodes.length > 2) { + lowerer.noLowering( + `${member} of a path with ${expr.arguments.nodes.length} arguments`, + expr, + `the supported form is ${member}(path[, connectListener])`, + ); + } + if (listener === undefined) + return { kind: "libCall", fn: "net.connectPath", args: [path, rule], type: NETSOCKET_T, loc }; + const { cb } = lowerCallbackArg( + lowerer, + listener, + "connect listeners", + 0, + () => false, + "use ()", + [], + ); + return { + kind: "libCall", + fn: "net.connectPathCb", + args: [path, rule, cb], + type: NETSOCKET_T, + loc, + }; +} + /** True when `t` is the lookup-function shape the connect lowering * accepts: (hostname: string, options: unknown, callback) => void where * callback is (err: , addresses: { address: @@ -929,6 +983,29 @@ function lowerNetConnectOptions( } } } + // connect({ path }[, connectListener]): the Unix-socket form. + { + const keys = optsNode.properties.nodes.map((p) => + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) + ? p.name.text + : null, + ); + if (keys.length === 1 && keys[0] === "path") { + const prop = optsNode.properties.nodes[0]!; + const path = ts.isPropertyAssignment(prop) + ? lowerer.lowerExpr(prop.initializer) + : lowerer.lowerShorthandValue(prop as ts.ShorthandPropertyAssignment); + if (path.type.kind !== "string") { + lowerer.noLowering( + `a ${member} 'path' option of '${lowerer.fmt(path.type)}' values`, + prop, + "the path is a string here", + ); + } + return lowerNetConnectPath(lowerer, expr, member, path, false, args[1]); + } + } let port: IrExpr | null = null; let host: IrExpr | null = null; let lookup: IrExpr | null = null; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index f1abf8935..46631a27e 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,3 +1,5 @@ +import { markInferredFactChecks, shapeOverlayFileNames } from "../shape-inference/index.js"; +import { npmStaticPackageOfPath } from "../npm-static.js"; import { globalMemberValue } from "./global-values.js"; import { unionNarrowFailureMessage } from "./union-narrow.js"; import { @@ -7,6 +9,7 @@ import { arrayRootOf, lowerWithArraySpecies, } from "./builtins/array-classes.js"; +import { canonShadowHook } from "../canon-map.js"; import { compactDeclOrigins, inPackageFile, @@ -3301,6 +3304,8 @@ export class Lowerer { /** Inferred JS methods participating in an override chain keep a vtable * ABI instead of call-site specialization. Filled before class collection. */ readonly virtualJsMethods = new Set(); + /** Methods of checked-JavaScript override pairs (vtable dispatch). */ + readonly checkedJsOverrideMethods = new Set(); /** Descendant writes must widen the original JS field before collecting * any class layout; base and derived instances share its storage ABI. */ readonly inheritedJsFieldWrites = new Map< @@ -4102,6 +4107,7 @@ export class Lowerer { this.externalTypeSpecifiersByFile = mode.externalTypeSpecifiersByFile ?? directExternalTypeSpecifiersByFile(this.externalTypes); this.checker = program.getTypeChecker(); + const shadow = canonShadowHook(this.checker); this.typeCtx = { checker: this.checker, positionOf: (node) => this.positions.of(node), @@ -4115,6 +4121,7 @@ export class Lowerer { const id = (type as { id?: number }).id; return id === undefined ? null : (this.degradedTypes.get(id)?.lenient ?? null); }, + ...(shadow !== undefined ? { canonShadow: shadow } : {}), isErasing: () => this.erasingDepth > 0, genericClassInstance: (decl, ref, mapArgument) => this.genericClassInstanceType(decl, ref, mapArgument), @@ -8719,6 +8726,7 @@ export class Lowerer { // reference are untouched — byte-stability holds. checkRequireBindingAccesses(this, functions); refuseX509DynValues(this, functions); + markInferredFactChecks(functions, this.program); const module: IrModule | null = this.diags.length > 0 ? null @@ -8735,6 +8743,13 @@ export class Lowerer { ...(this.usesLifetimeRegions ? { lifetimeRegions: true as const } : {}), ...(this.usesNodeIsland ? { nodeIsland: this.nodeIslandRecord() } : {}), ...(this.declBoundary ? { declBoundary: true as const } : {}), + ...(shapeOverlayFileNames().length > 0 + ? { + inferredFactFiles: shapeOverlayFileNames() + .map((file) => ({ file, pkg: npmStaticPackageOfPath(file) ?? "" })) + .sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0)), + } + : {}), ...(this.foreignElementKeys.size > 0 ? { foreignElementKeys: [...this.foreignElementKeys].sort() } : {}), diff --git a/packages/compiler/src/frontend/lowering/static-typeof.ts b/packages/compiler/src/frontend/lowering/static-typeof.ts index 852cc3117..cef2d21aa 100644 --- a/packages/compiler/src/frontend/lowering/static-typeof.ts +++ b/packages/compiler/src/frontend/lowering/static-typeof.ts @@ -1,12 +1,14 @@ import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; +import { ambientUndefinedFnSymbolOf, ambientUndefinedVarSymbolOf } from "./lower-namespaces.js"; /** The answer `typeof ` gives without evaluating anything, for * the global-detection idioms (`typeof window < "u"`, `typeof global !== - * "undefined"`): a name nothing declares answers "undefined" (Node's - * answer for an undeclared global such as window or self, and typeof never - * throws for one), and the Node global object and process answer - * "object". Null for every other operand. */ + * "undefined"`): a name nothing declares, or one the program declares + * ambiently and nothing defines, answers "undefined" (Node's answer for an + * undeclared global such as window or self, and typeof never throws for + * one), and the Node global object and process answer "object". Null for + * every other operand. */ export function staticTypeofAnswer(lowerer: Lowerer, operand: ts.Expression): string | null { let e = operand; while (ts.isParenthesizedExpression(e)) e = e.expression; @@ -14,6 +16,10 @@ export function staticTypeofAnswer(lowerer: Lowerer, operand: ts.Expression): st if (lowerer.checker.getSymbolAtLocation(e) === undefined) return "undefined"; for (const name of ["global", "globalThis", "process"]) if (lowerer.isStdlibGlobal(e, name)) return "object"; + // A name the program declares ambiently but nothing defines (`declare + // const Bun`): undeclared at run time. + if (ambientUndefinedVarSymbolOf(lowerer, e) || ambientUndefinedFnSymbolOf(lowerer, e)) + return "undefined"; return null; } diff --git a/packages/compiler/src/frontend/package-shims.ts b/packages/compiler/src/frontend/package-shims.ts index 5493b108d..34d8461dc 100644 --- a/packages/compiler/src/frontend/package-shims.ts +++ b/packages/compiler/src/frontend/package-shims.ts @@ -205,14 +205,26 @@ export function packageShimEntries(): string[] { let unloadedPackages: ReadonlySet = new Set(); /** Reads the project's package-shim configuration: the `scriptc.packages` - * entries marked "unloaded" in the entry's nearest package.json - * (project-config.ts validates the rest). Called once per program load. */ + * entries marked "unloaded", and those with `"shapes": false`, in the + * entry's nearest package.json (project-config.ts validates the rest). + * Called once per program load. */ export function configurePackageShims(entryPath: string): void { const packages = new Set(); + const noShapes = new Set(); for (const [name, setting] of readProjectConfig(entryPath).packages) { if (setting.unloaded) packages.add(name); + if (!setting.shapes) noShapes.add(name); } unloadedPackages = packages; + shapelessPackages = noShapes; +} + +let shapelessPackages: ReadonlySet = new Set(); + +/** Packages the project compiles without shape inference's overlay + * (`"scriptc": { "packages": { "": { "shapes": false } } }`). */ +export function isShapelessPackage(name: string): boolean { + return shapelessPackages.has(name); } /** True when `--npm-static` leaves the package `name` out of its package diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 75faaa781..13407f499 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -1,3 +1,4 @@ +import { shapeOverlayReport } from "./shape-inference/index.js"; /** Shared frontend orchestration. Hosts supply a program loader so Node * and native callers use the same npm selection, preflight and lowering. */ import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; @@ -807,8 +808,11 @@ function loadFrontend( }); // The declaration-typed boundary's notes (SC6102) join the // performance warnings; they never fail a build. - const notes = finalLoad.declBoundaryNotes; - if (notes === undefined) return lowered; + const notes = [ + ...(finalLoad.declBoundaryNotes ?? []), + ...shapeOverlayReport(finalLoad.entry.fileName), + ]; + if (notes.length === 0) return lowered; return { ...lowered, performance: [...notes, ...(lowered.performance ?? [])] }; }, npmStatic: statuses, diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index a4ab89b43..58cdf219a 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -148,7 +148,12 @@ import { } from "./decl-projection.js"; import { configureTypePatches, packageSourceText } from "./type-patches/layer.js"; import { resetDeclOriginVersions } from "./lowering/decl-origin.js"; -import { reportShapes } from "./shape-inference/index.js"; +import { + reportShapes, + resetShapeOverlay, + shapeOverlayEnabled, + shapeOverlayer, +} from "./shape-inference/index.js"; import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js"; import { containsJsx, @@ -884,6 +889,9 @@ function loadProgram7( /** The declaration-typed boundary's projection (decl-projection.ts): * answers rewritten user files for the lowering world, or null. */ projector?: (program: ts.Program) => DeclProjection | null, + /** Shape inference's typed overlay (shape-inference/overlay.ts): + * rewritten package JavaScript for the lowering world, or null. */ + overlayer?: (program: ts.Program) => ReadonlyMap | null, ): LoadResult & { disposeAll: () => void } { const config = adoptProjectConfig7(host, entryPath); const configuredPaths = config.options["paths"]; @@ -1036,6 +1044,26 @@ function loadProgram7( } } } + // Shape inference's overlay: package files typed from proven facts. The + // tsc gate keeps reading the files as written. + // A load without the overlay must not see an earlier load's overlay + // files (the registry is per process). + resetShapeOverlay(); + if (overlayer !== undefined && jsx.refusal === null) { + const overlay = overlayer(program); + if (overlay !== null && overlay.size > 0) { + const before = program; + for (const [file, text] of overlay) host.addVirtualFile(file, text); + program = host.createProgram([...programRoots, overridesDtsPath()], options); + if (tscWorld === undefined) + tscWorld = { + program: before, + diagnostics: ts.getPreEmitDiagnostics(before), + projectWorld: null, + }; + else if (tscWorld.program !== before) before.dispose(); + } + } const entry = program.getSourceFile(entryPath); if (!entry) throw new Error(`could not load ${entryPath}`); const externalTypeSpecifiersByFile = externalTypeFileClosure7(program, externalTypes); @@ -1303,6 +1331,7 @@ export function loadProgram( externalTypes, npmStaticPackages.length > 0, projector, + npmStaticPackages.length > 0 && shapeOverlayEnabled() ? shapeOverlayer : undefined, ); // Shape inference, stage S1: a report only (SCRIPTC_DEBUG_SHAPES). const shapes = process.env["SCRIPTC_DEBUG_SHAPES"]; diff --git a/packages/compiler/src/frontend/shape-inference/analyze.ts b/packages/compiler/src/frontend/shape-inference/analyze.ts index c403c6ba8..2a0ee09a1 100644 --- a/packages/compiler/src/frontend/shape-inference/analyze.ts +++ b/packages/compiler/src/frontend/shape-inference/analyze.ts @@ -70,6 +70,8 @@ export interface LiteralSite { readonly kind: "object" | "array"; /** Static keys in creation order; null when a spread or computed key makes them unknown. */ readonly keys: string[] | null; + /** The literal's own static keys (without spread contributions). */ + readonly ownKeys: string[]; readonly hasAccessor: boolean; readonly hasProto: boolean; readonly hasComputedKey: boolean; @@ -129,6 +131,17 @@ export interface ShapeAnalysis { readonly escapedFunctions: ReadonlySet; /** ES5 constructors: each function's default prototype object. */ readonly protoOf: ReadonlyMap; + /** ES5 constructor instances: the function atom whose `new` made each. */ + readonly fnOfInstance: ReadonlyMap; + /** Every value a declaration (parameter, variable, binding element) holds, + * merged over the analysis contexts it was analyzed in. */ + declValues(decl: Node): Set; + /** The classes a class declaration's chain contains, base last. */ + classChain(decl: Node): Node[]; + /** `if` statements in walked code whose condition is known: the branch + * whose condition values never allowed it is absent from `walkedBranches`. */ + readonly branchIfs: ReadonlySet; + readonly walkedBranches: ReadonlySet; } /** Array.prototype members: other names on an array are own properties. */ @@ -238,6 +251,47 @@ function isLoop(node: Node): boolean { ); } +/** Whether control can reach the end of `stmts`. False only when every + * path ends in a return or throw, or in a `while (true)`, `do … while + * (true)` or `for (;;)` loop with no break inside; anything else is + * assumed to complete. */ +function canComplete(stmts: readonly Node[]): boolean { + return stmts.every(canCompleteStatement); +} + +function canCompleteStatement(s: Node): boolean { + if (ts.isReturnStatement(s) || ts.isThrowStatement(s)) return false; + if (ts.isBlock(s)) return canComplete(list(s.statements)); + if (ts.isIfStatement(s)) + return ( + s.elseStatement === undefined || + canCompleteStatement(s.thenStatement) || + canCompleteStatement(s.elseStatement) + ); + if (ts.isTryStatement(s)) { + if (s.finallyBlock !== undefined && !canCompleteStatement(s.finallyBlock)) return false; + return ( + canCompleteStatement(s.tryBlock) || + (s.catchClause !== undefined && canCompleteStatement(s.catchClause.block)) + ); + } + const endless = + ((ts.isWhileStatement(s) || ts.isDoStatement(s)) && + s.expression.kind === ts.SyntaxKind.TrueKeyword) || + (ts.isForStatement(s) && s.condition === undefined); + return !endless || containsBreak(s); +} + +/** Whether a break statement occurs in `node`'s body, outside nested + * functions. */ +function containsBreak(node: Node): boolean { + return ( + ts.forEachChild(node, (c) => + ts.isBreakStatement(c) || (!ts.isFunctionLike(c) && containsBreak(c)) ? true : undefined, + ) === true + ); +} + const LOOP_CALLBACK_METHODS: ReadonlySet = new Set([ "map", "forEach", @@ -357,6 +411,11 @@ class Analyzer { nodeLabel: this.nodeLabel, escapedFunctions: this.escapedFunctionCalls, protoOf: this.protoOf, + fnOfInstance: this.fnOfInstance, + declValues: (decl) => this.declValues(decl), + classChain: (decl) => this.classChain(decl), + branchIfs: this.branchIfs, + walkedBranches: this.walkedBranches, }; } @@ -449,6 +508,78 @@ class Analyzer { return p; } + /** A boolean literal: labeled `boolean` like any boolean, but its + * truthiness is known, so `if` statements on it walk one branch. */ + boolLiteral(value: boolean): Atom { + return this.atoms.intern(`prim:boolean:${value}`, () => ({ + kind: "prim", + label: "boolean", + literal: value, + })); + } + + /** The analysis environment a deferred walk restores. */ + private env(): WalkEnv { + return { + ctx: this.ctx, + ctxRoot: this.ctxRoot, + currentFn: this.currentFn, + loopDepth: this.loopDepth, + }; + } + + private inEnv(env: WalkEnv, walk: () => void): void { + const saved = this.env(); + this.ctx = env.ctx; + this.ctxRoot = env.ctxRoot; + this.currentFn = env.currentFn; + this.loopDepth = env.loopDepth; + try { + walk(); + } finally { + this.ctx = saved.ctx; + this.ctxRoot = saved.ctxRoot; + this.currentFn = saved.currentFn; + this.loopDepth = saved.loopDepth; + } + } + + readonly branchIfs = new Set(); + readonly walkedBranches = new Set(); + /** Ifs whose branches wait for condition values; at the fixpoint, an + * `if` whose condition node stayed empty walks every waiting branch. */ + private readonly pendingIfs: PendingIf[] = []; + + /** An `if` walks a branch once a condition value allows it: the then + * branch on a value that may be truthy, the else branch on one that may + * be falsy. A branch no value allows is never walked, so its effects + * (returns, calls, writes) stay out of the facts; the overlay turns it + * into a checked internal error. */ + private ifStatement(s: ts.IfStatement): void { + const cond = this.expr(s.expression); + this.branchIfs.add(s); + const pending: PendingIf = { + s, + cond, + env: this.env(), + thenDone: false, + elseDone: s.elseStatement === undefined, + }; + this.pendingIfs.push(pending); + this.on(cond, (a) => { + if (!pending.thenDone && mayBeTruthy(a)) this.walkBranch(pending, "then"); + if (!pending.elseDone && mayBeFalsy(a)) this.walkBranch(pending, "else"); + }); + } + + private walkBranch(pending: PendingIf, which: "then" | "else"): void { + const branch = which === "then" ? pending.s.thenStatement : pending.s.elseStatement!; + if (which === "then") pending.thenDone = true; + else pending.elseDone = true; + this.walkedBranches.add(branch); + this.inEnv(pending.env, () => this.stmt(branch)); + } + builtin(path: string, receiver?: Atom): Atom { const key = receiver === undefined ? `builtin:${path}` : `builtin:${path}@${receiver.id}`; return this.atoms.intern(key, () => ({ kind: "builtin", label: path, path, receiver })); @@ -717,6 +848,24 @@ class Analyzer { return `${inside ? this.ctx : ""}|${this.nodeKey(node)}`; } + private declIndex: Map | null = null; + declValues(decl: Node): Set { + if (this.declIndex === null) { + this.declIndex = new Map(); + for (const [key, id] of this.bindings) { + const k = Number(key.slice(key.lastIndexOf("|") + 1)); + let l = this.declIndex.get(k); + if (l === undefined) this.declIndex.set(k, (l = [])); + l.push(id); + } + } + const out = new Set(); + const k = this.nodeKeys.get(decl); + if (k === undefined) return out; + for (const id of this.declIndex.get(k) ?? []) for (const a of this.nodes[id]!.atoms) out.add(a); + return out; + } + receiverCtx(atom: Atom): string { return atom.kind === "instance" || atom.kind === "class" ? `C${atom.id}` : ""; } @@ -812,8 +961,11 @@ class Analyzer { } const body = fn.body; if (body !== undefined) { - if (ts.isBlock(body)) this.stmts(list(body.statements)); - else this.edge(this.expr(body as ts.Expression), info.ret); + if (ts.isBlock(body)) { + this.stmts(list(body.statements)); + // A call that runs off the end of the body returns undefined. + if (canComplete(list(body.statements))) this.add(info.ret, this.prim("undefined")); + } else this.edge(this.expr(body as ts.Expression), info.ret); } this.currentFn = saved; this.loopDepth = savedLoop; @@ -1147,8 +1299,9 @@ class Analyzer { return; } if (ts.isReturnStatement(s)) { - if (s.expression !== undefined && this.currentFn !== null) - this.edge(this.expr(s.expression), this.currentFn.ret); + if (this.currentFn === null) return; + if (s.expression !== undefined) this.edge(this.expr(s.expression), this.currentFn.ret); + else this.add(this.currentFn.ret, this.prim("undefined")); return; } if (ts.isFunctionDeclaration(s)) { @@ -1222,6 +1375,10 @@ class Analyzer { this.expr(s.expression); return; } + if (ts.isIfStatement(s) && this.currentFn !== null) { + this.ifStatement(s); + return; + } // Blocks, ifs, switches, labels: walk children, evaluating expressions. ts.forEachChild(s, (child) => { if (ts.isExpression(child)) this.expr(child); @@ -1316,8 +1473,9 @@ class Analyzer { case ts.SyntaxKind.BigIntLiteral: return this.constNode(this.prim("bigint")); case ts.SyntaxKind.TrueKeyword: + return this.constNode(this.boolLiteral(true)); case ts.SyntaxKind.FalseKeyword: - return this.constNode(this.prim("boolean")); + return this.constNode(this.boolLiteral(false)); case ts.SyntaxKind.NullKeyword: return this.constNode(this.prim("null")); case ts.SyntaxKind.RegularExpressionLiteral: @@ -1749,6 +1907,7 @@ class Analyzer { node: e, kind: "object", keys: keysKnown ? keys : null, + ownKeys: keys, hasAccessor, hasProto, hasComputedKey, @@ -1915,6 +2074,7 @@ class Analyzer { node: e, kind: "array", keys: null, + ownKeys: [], hasAccessor: false, hasProto: hasHole, hasComputedKey: false, @@ -2558,7 +2718,11 @@ class Analyzer { this.activate(info); this.add(info.thisNode, inst); this.bindArgs(info, args, spread); - this.edge(info.ret, dst); + // `new` ignores a primitive result (and the undefined of a + // body that runs off its end) and yields the instance. + this.on(info.ret, (r) => { + if (r.kind !== "prim") this.add(dst, r); + }); this.edgeFor(e, info); } return; @@ -3205,6 +3369,21 @@ class Analyzer { const before = this.steps; for (const c of this.lateCopies) c(); this.solve(); + // A condition with no values at the fixpoint is one the analysis + // could not evaluate: walk both of its branches. + let walked = false; + for (const p of this.pendingIfs) { + if (this.nodes[p.cond]!.atoms.size > 0) continue; + if (!p.thenDone) { + this.walkBranch(p, "then"); + walked = true; + } + if (!p.elseDone) { + this.walkBranch(p, "else"); + walked = true; + } + } + if (walked) continue; if (this.steps === before || this.budgetExceeded) return; } } @@ -3227,3 +3406,34 @@ function cjsExportName(decl: Node): string | null { return decl.left.name.text; return null; } + +interface WalkEnv { + readonly ctx: string; + readonly ctxRoot: Node | null; + readonly currentFn: FunctionInfo | null; + readonly loopDepth: number; +} + +interface PendingIf { + readonly s: ts.IfStatement; + readonly cond: number; + readonly env: WalkEnv; + thenDone: boolean; + elseDone: boolean; +} + +/** Whether a value of the atom may be truthy (only `null`, `undefined` and + * the literal `false` never are; ⊤ may be anything). */ +function mayBeTruthy(a: Atom): boolean { + if (a.kind !== "prim") return true; + if (a.label === "null" || a.label === "undefined") return false; + return a.literal !== false; +} + +/** Whether a value of the atom may be falsy (objects, arrays, functions, + * classes, promises and collections never are). */ +function mayBeFalsy(a: Atom): boolean { + if (a.kind === "top") return true; + if (a.kind !== "prim") return false; + return a.literal !== true; +} diff --git a/packages/compiler/src/frontend/shape-inference/domain.ts b/packages/compiler/src/frontend/shape-inference/domain.ts index d90a83cf2..21e52346c 100644 --- a/packages/compiler/src/frontend/shape-inference/domain.ts +++ b/packages/compiler/src/frontend/shape-inference/domain.ts @@ -38,6 +38,8 @@ export interface Atom { /** function atoms: the analysis context of the function's clone (a * method's receiver class), "" for the context-insensitive default. */ readonly ctx?: string | undefined; + /** prim `boolean` atoms of a literal: its value. */ + readonly literal?: boolean | undefined; } export class AtomTable { diff --git a/packages/compiler/src/frontend/shape-inference/index.ts b/packages/compiler/src/frontend/shape-inference/index.ts index 402a788b1..e2f7add04 100644 --- a/packages/compiler/src/frontend/shape-inference/index.ts +++ b/packages/compiler/src/frontend/shape-inference/index.ts @@ -1,7 +1,13 @@ /* Shape inference for untyped npm JavaScript - * (results/design-shape-inference/DESIGN.md). Stage S1: report only. */ + * (results/design-shape-inference/DESIGN.md): the report (S1) and the typed + * overlay (overlay.ts). */ import type * as ts from "../ts7/adapter.js"; -import { analyzeShapes } from "./analyze.js"; +import type { IrFunction } from "../../ir/ir.js"; +import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; +import { npmStaticPackageOfPath } from "../npm-static.js"; +import { isShapelessPackage } from "../package-shims.js"; +import { analyzeShapes, isPackageJsFile } from "./analyze.js"; +import { OVERLAY_MARK, planShapeOverlay } from "./overlay.js"; import { buildShapeReport } from "./report.js"; import { computeConstantGraphs, constantGraphReport } from "./constant-graphs.js"; @@ -10,7 +16,8 @@ export { buildShapeReport } from "./report.js"; /** SCRIPTC_DEBUG_SHAPES: `1` reports package code in hot functions, * `verbose` every package site, `all` user files too, any other value only - * files whose path contains it. Writes to stderr; changes nothing else. */ + * files whose path contains it. Writes to stderr; changes nothing else. The + * build cache does not key on the variable: use SCRIPTC_NO_CACHE=1. */ export function reportShapes(program: ts.Program, filter: string): void { const start = performance.now(); const analysis = analyzeShapes(program, program.getTypeChecker()); @@ -22,3 +29,134 @@ export function reportShapes(program: ts.Program, filter: string): void { const ms = (performance.now() - start).toFixed(0); process.stderr.write(`[shapes] ${ms} ms\n${lines.map((l) => `[shapes] ${l}`).join("\n")}\n`); } + +/* The overlay's files for the current load: the lowering treats them as + * checked JavaScript (their JSDoc types are the analysis's proven facts). */ +let overlayFiles: ReadonlySet = new Set(); +let overlayAnnotations = 0; +let overlayDeadBranches = 0; + +export function isShapeOverlayFile(fileName: string): boolean { + return overlayFiles.has(fileName); +} + +/** Whether a statement or declaration carries an overlay annotation. */ +export function shapeOverlayAnnotated(node: ts.Node): boolean { + const sf = node.getSourceFile(); + if (!overlayFiles.has(sf.fileName)) return false; + return sf.text.slice(node.pos, node.getStart(sf)).includes(OVERLAY_MARK); +} + +export function shapeOverlayFileNames(): readonly string[] { + return [...overlayFiles]; +} + +export function shapeOverlayAnnotationCount(): number { + return overlayAnnotations; +} + +export function resetShapeOverlay(): void { + overlayFiles = new Set(); + overlayAnnotations = 0; + overlayDeadBranches = 0; + inferredFactChecks = 0; +} + +/** Whether the overlay runs. While it is new it is opt-in + * (SCRIPTC_SHAPE_OVERLAY=1); `--no-shape-overlay` (the `shapeOverlay: + * false` build option) sets the variable to 0 for the build. */ +export function shapeOverlayEnabled(): boolean { + return process.env["SCRIPTC_SHAPE_OVERLAY"] === "1"; +} + +/** loadProgram7's overlayer: analyze, plan, and register the overlay files. */ +export function shapeOverlayer(program: ts.Program): ReadonlyMap | null { + resetShapeOverlay(); + const analysis = analyzeShapes(program, program.getTypeChecker()); + if (analysis.budgetExceeded) return null; + const report = buildShapeReport(analysis, "1"); + const packageFiles = analysis.files.filter((sf) => { + if (!isPackageJsFile(sf)) return false; + const pkg = npmStaticPackageOfPath(sf.fileName); + return pkg === null || !isShapelessPackage(pkg); + }); + const overlay = planShapeOverlay(analysis, report.sites, packageFiles); + overlayFiles = new Set(overlay.files.keys()); + overlayAnnotations = overlay.annotations; + overlayDeadBranches = overlay.deadBranches; + if (process.env["SCRIPTC_DEBUG_SHAPE_OVERLAY"] !== undefined) { + for (const [file, text] of overlay.files) + process.stderr.write(`[shape-overlay] ${file}\n${text}\n`); + } + return overlay.files; +} + +/** Marks the checked conversions lowered in overlay files: each checks an + * analysis-proven fact, so its failure names the site as an analysis bug. */ +export function markInferredFactChecks( + functions: readonly IrFunction[], + program: ts.Program, +): number { + if (overlayFiles.size === 0) return 0; + let count = 0; + const lines = new Map number>(); + const lineOf = (file: string, pos: number): number => { + let f = lines.get(file); + if (f === undefined) { + const sf = program.getSourceFile(file); + f = sf === undefined ? () => 0 : (p: number) => sf.getLineAndCharacterOfPosition(p).line + 1; + lines.set(file, f); + } + return f(pos); + }; + const visit = (x: unknown): void => { + if (Array.isArray(x)) { + for (const v of x) visit(v); + return; + } + if (x === null || typeof x !== "object") return; + const node = x as { + kind?: unknown; + loc?: { file: string; start: number }; + inferredSite?: string; + }; + if (node.kind === "dynCheck" && node.loc !== undefined && overlayFiles.has(node.loc.file)) { + const pkg = npmStaticPackageOfPath(node.loc.file) ?? ""; + const rel = node.loc.file.split("/node_modules/").pop() ?? node.loc.file; + node.inferredSite = `${pkg} ${rel}:${lineOf(node.loc.file, node.loc.start)}`; + count++; + } + for (const v of Object.values(x)) if (v !== null && typeof v === "object") visit(v); + }; + for (const fn of functions) visit(fn.body); + inferredFactChecks = count; + return count; +} + +let inferredFactChecks = 0; + +/** Inferred-fact checks inserted by the last build (the build report). */ +export function shapeInferredFactCheckCount(): number { + return inferredFactChecks; +} + +/** SC6103, the overlay's build report: what the overlay typed and how many + * checks of inferred facts the build inserted. A note, never a failure. */ +export function shapeOverlayReport(entryFile: string): ScrDiagnostic[] { + if (overlayFiles.size === 0) return []; + const packages = new Set([...overlayFiles].map((f) => npmStaticPackageOfPath(f) ?? f)); + return [ + { + code: "SC6103", + message: + `shape inference typed ${overlayAnnotations} declarations in ${overlayFiles.size} files of ${packages.size} ` + + `package${packages.size === 1 ? "" : "s"} (${[...packages].sort().join(", ")}); the build checks ` + + `${inferredFactChecks} inferred fact${inferredFactChecks === 1 ? "" : "s"} at run time` + + (overlayDeadBranches > 0 + ? ` and replaces ${overlayDeadBranches} branch${overlayDeadBranches === 1 ? "" : "es"} it proved unreachable with checks` + : ""), + loc: { file: entryFile, start: 0, end: 0 }, + hint: "a failing check reports a scriptc analysis bug; --no-shape-overlay compiles without the overlay", + }, + ]; +} diff --git a/packages/compiler/src/frontend/shape-inference/overlay.test.ts b/packages/compiler/src/frontend/shape-inference/overlay.test.ts new file mode 100644 index 000000000..f0effae77 --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/overlay.test.ts @@ -0,0 +1,32 @@ +import { afterAll, expect, test } from "vitest"; +import { closeSourceParser, parseSourceFile } from "../ts7/source-parser-node.js"; +import { neutralizeDocTags, OVERLAY_MARK } from "./overlay.js"; + +afterAll(closeSourceParser); + +test("neutralizeDocTags rewrites JSDoc comments and keeps literals", () => { + const source = [ + "/** @param {number} a */", + "function f(a) {", + ' const s = "/** @type {number} */";', + " const t = `/** @returns {string} ${a} */`;", + " const r = /\\/\\*\\* @param \\*\\//;", + " // /** @type {string} */", + ` /** @type {string} ${OVERLAY_MARK} */ const u = s + t;`, + " return a / 2 /** @returns {number} */;", + "}", + ].join("\n"); + const out = neutralizeDocTags(parseSourceFile("overlay.js", source, "js")); + expect(out.length).toBe(source.length); + expect(out.split("\n")).toEqual([ + "/** -param {number} a */", + "function f(a) {", + ' const s = "/** @type {number} */";', + " const t = `/** @returns {string} ${a} */`;", + " const r = /\\/\\*\\* @param \\*\\//;", + " // /** @type {string} */", + ` /** @type {string} ${OVERLAY_MARK} */ const u = s + t;`, + " return a / 2 /** -returns {number} */;", + "}", + ]); +}); diff --git a/packages/compiler/src/frontend/shape-inference/overlay.ts b/packages/compiler/src/frontend/shape-inference/overlay.ts new file mode 100644 index 000000000..edaad7b80 --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/overlay.ts @@ -0,0 +1,774 @@ +/* Shape inference: the typed overlay (results/design-shape-inference, + * maintainer decision 2026-10-23). The analysis's proven facts become JSDoc + * types in a rewritten copy of each package JavaScript file, and the + * lowering treats the rewritten file like a checked JavaScript file, so the + * static tier compiles the package's hot code from those types. + * + * Only proven facts are written: a position whose value set contains ⊤ + * (anything from islands, JSON.parse, the network or the file system, + * reflection, globals, `process`, caught exceptions, or code the analysis + * cannot see) stays untyped. A check the lowering inserts where a value + * meets an inferred type therefore fails only when the analysis is wrong; + * such checks carry the inferred-fact failure message. + * + * The package's own JSDoc is a hint, not a declaration: its tags are + * neutralized in the rewritten file, so only the analysis's types remain. + * + * Every edit is inline: no line breaks are added or removed, so positions + * keep their lines. */ +import * as ts from "../ts7/adapter.js"; +import type { Atom } from "./domain.js"; +import type { ShapeAnalysis } from "./analyze.js"; +import type { SiteVerdict } from "./report.js"; + +export interface ShapeOverlay { + /** Rewritten text per package file. */ + readonly files: Map; + /** Annotations written, for the build report. */ + readonly annotations: number; + /** Branches the analysis proved unreachable, replaced by checks. */ + readonly deadBranches: number; +} + +interface Edit { + readonly pos: number; + readonly text: string; + /** Replaces pos..end instead of inserting at pos. */ + readonly end?: number; +} + +/** Marks the overlay's own comments (an unknown JSDoc tag, ignored). */ +export const OVERLAY_MARK = "@scriptcShape"; +const MARK = OVERLAY_MARK; + +/** Above 1, values of several record key sets type as one merged record + * with optional fields (SCRIPTC_SHAPE_RECORD_ARMS; experimental). */ +const MAX_RECORD_ARMS = Number(process.env.SCRIPTC_SHAPE_RECORD_ARMS ?? 1); + +/** The most class or record arms an inferred union may have. */ +const MAX_ARMS = 6; + +function list(arr: { readonly nodes: readonly T[] } | undefined): readonly T[] { + return arr?.nodes ?? []; +} + +let debugIndex = 0; + +export function planShapeOverlay( + analysis: ShapeAnalysis, + verdicts: readonly SiteVerdict[], + packageFiles: readonly ts.SourceFile[], +): ShapeOverlay { + const pSites = new Map(); + const siteVerdicts = new Set(verdicts.map((v) => v.site.atom)); + // Records and arrays print only for sites whose objects never escape: a + // native record that reached untyped code would be seen as a copy. + const siteKeys = new Map }>(); + for (const v of verdicts) { + if (v.verdict !== "P-local") continue; + if (v.site.kind === "array") { + pSites.set(v.site.atom, v); + continue; + } + const keys = keysOfSite(analysis, v); + if (keys === null) continue; + siteKeys.set(v.site.atom, keys); + pSites.set(v.site.atom, v); + } + const files = new Map(); + let annotations = 0; + let deadBranches = 0; + debugIndex = 0; + // `if` branches no condition value allowed, per file: the overlay + // replaces each with a throwing check, so the checker and the lowering + // see the code the analysis saw. + const deadByFile = new Map(); + if (process.env["SCRIPTC_SHAPE_DEAD_BRANCHES"] !== "0") + for (const s of analysis.branchIfs) { + for (const branch of [s.thenStatement, s.elseStatement]) { + if (branch === undefined || analysis.walkedBranches.has(branch)) continue; + const sf = s.getSourceFile(); + let list = deadByFile.get(sf); + if (list === undefined) deadByFile.set(sf, (list = [])); + list.push(branch); + } + } + for (const sf of packageFiles) { + const typedefs = new Map(); + const typedefText: string[] = []; + const edits: Edit[] = []; + + const className = (decl: ts.Node): string | null => { + if (decl.getSourceFile() !== sf) return null; + const own = (decl as ts.ClassLikeDeclaration).name; + if (own !== undefined && ts.isClassDeclaration(decl) && decl.parent === sf) return own.text; + const p = decl.parent; + if ( + p !== undefined && + ts.isVariableDeclaration(p) && + ts.isIdentifier(p.name) && + p.parent?.parent?.parent === sf + ) + return p.name.text; + return null; + }; + + // `inner`: inside a record field, where ⊤ and unprintable values are `any`. + const typeOf = (atoms: ReadonlySet, inner: boolean, depth: number): string | null => { + if (atoms.size === 0) return inner ? "any" : null; + const arms = new Set(); + let objects = 0; + // Every array arm becomes one Array<…> of the joined element values. + const elements = new Set(); + let hasArray = false; + const promised = new Set(); + let hasPromise = false; + const recordSites: Atom[] = []; + for (const a of atoms) { + let t: string | null = null; + switch (a.kind) { + case "prim": + t = + a.label === "null" || + a.label === "undefined" || + a.label === "string" || + a.label === "number" || + a.label === "boolean" || + a.label === "bigint" + ? a.label + : null; + break; + case "instance": + t = a.node !== undefined ? className(a.node) : null; + objects++; + break; + case "object": { + objects++; + const v = pSites.get(a); + if (v === undefined || depth >= 4 || !siteKeys.has(a)) return inner ? "any" : null; + recordSites.push(a); + continue; + } + case "array": { + objects++; + if (analysis.escaped.has(a) || (siteVerdicts.has(a) && !pSites.has(a))) + return inner ? "any" : null; + const el = analysis.elements.get(a); + if (el !== undefined) for (const x of analysis.nodes[el]!.atoms) elements.add(x); + hasArray = true; + continue; + } + case "promise": { + // One promise arm: the values it resolves to, joined. + const value = analysis.fields.get(a)?.get("value"); + if (value !== undefined) for (const x of analysis.nodes[value]!.atoms) promised.add(x); + hasPromise = true; + continue; + } + default: + t = null; + } + if (t === null) return inner ? "any" : null; + arms.add(t); + } + // Records of one key set print as their typedef; records of several + // key sets merge into one record whose fields missing from some sites + // are optional (unions of overlapping records would need the lowering + // to pick an arm per value). + if (recordSites.length > 0) { + const keySets = new Set(recordSites.map(keySetOf)); + if (keySets.size === 1) arms.add(recordType(recordSites[0]!, depth)); + else if (MAX_RECORD_ARMS > 1) arms.add(mergedRecordType(recordSites, depth)); + else return inner ? "any" : null; + } + if (hasArray) { + const inside = depth < 4 ? typeOf(elements, true, depth + 1) : "any"; + arms.add(`Array<${inside ?? "any"}>`); + } + // A promise beside other values cannot cross into untyped code + // (maybe-promise unions stay typed-only), so it stays untyped here. + if (hasPromise) return inner ? "any" : null; + if (objects > MAX_ARMS) return inner ? "any" : null; + if (arms.has("any")) return inner ? "any" : null; + return [...arms].join(" | "); + }; + + // One typedef per key set: sites with the same keys intern to the same + // structural record, so their field types are joined. + const keySetOf = (site: Atom): string => { + const k = siteKeys.get(site)!; + return k.keys + .map((key) => (k.optional.has(key) ? `${key}?` : key)) + .sort() + .join(","); + }; + const sitesByKeys = new Map(); + for (const site of pSites.keys()) { + if (pSites.get(site)!.site.kind !== "object") continue; + const k = keySetOf(site); + let l = sitesByKeys.get(k); + if (l === undefined) sitesByKeys.set(k, (l = [])); + l.push(site); + } + const recordType = (site: Atom, depth: number): string => { + const keySet = keySetOf(site); + const existing = typedefs.get(keySet); + if (existing !== undefined) return existing; + const name = `__shape_${typedefs.size}`; + typedefs.set(keySet, name); + const { keys, optional } = siteKeys.get(site)!; + const parts = keys.map((k) => { + const atoms = new Set(); + for (const other of sitesByKeys.get(keySet) ?? [site]) { + const id = analysis.fields.get(other)?.get(k); + if (id !== undefined) for (const a of analysis.nodes[id]!.atoms) atoms.add(a); + } + const t = typeOf(atoms, true, depth + 1); + return `${/^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k)}${optional.has(k) ? "?" : ""}: ${t ?? "any"}`; + }); + typedefText.push(`/** @typedef {{${parts.join(", ")}}} ${name} ${MARK} */`); + return name; + }; + + const mergedRecordType = (sites: readonly Atom[], depth: number): string => { + const all = new Set(); + for (const site of sites) + for (const other of sitesByKeys.get(keySetOf(site)) ?? [site]) all.add(other); + const keys: string[] = []; + const optional = new Set(); + for (const site of all) { + const k = siteKeys.get(site)!; + for (const key of k.keys) { + if (!keys.includes(key)) keys.push(key); + if (k.optional.has(key)) optional.add(key); + } + } + for (const site of all) { + const own = siteKeys.get(site)!.keys; + for (const key of keys) if (!own.includes(key)) optional.add(key); + } + const keySet = `merged:${keys + .map((k) => (optional.has(k) ? `${k}?` : k)) + .sort() + .join(",")}`; + const existing = typedefs.get(keySet); + if (existing !== undefined) return existing; + const name = `__shape_${typedefs.size}`; + typedefs.set(keySet, name); + const parts = keys.map((k) => { + const atoms = new Set(); + for (const site of all) { + const id = analysis.fields.get(site)?.get(k); + if (id !== undefined) for (const a of analysis.nodes[id]!.atoms) atoms.add(a); + } + const t = typeOf(atoms, true, depth + 1); + return `${/^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k)}${optional.has(k) ? "?" : ""}: ${t ?? "any"}`; + }); + typedefText.push(`/** @typedef {{${parts.join(", ")}}} ${name} ${MARK} */`); + return name; + }; + + // Functions called with `new` yield their instance, whatever they return. + const constructed = new Set( + [...analysis.fnOfInstance.values()].map((a) => a.node), + ); + // Return values merged over every analysis context of each function. + const returns = new Map>(); + for (const info of analysis.functions.values()) { + if (info.node.getSourceFile() !== sf || info.isAsync || info.isGenerator) continue; + let set = returns.get(info.node); + if (set === undefined) returns.set(info.node, (set = new Set())); + for (const a of analysis.nodes[info.ret]!.atoms) set.add(a); + } + + // Receivers each method runs on, merged over its analysis contexts. + const thisAtoms = new Map>(); + for (const info of analysis.functions.values()) { + if (info.node.getSourceFile() !== sf || !ts.isMethodDeclaration(info.node)) continue; + let set = thisAtoms.get(info.node); + if (set === undefined) thisAtoms.set(info.node, (set = new Set())); + for (const a of analysis.nodes[info.thisNode]!.atoms) set.add(a); + } + const declaresMember = (cls: ts.Node, name: string): boolean => + analysis.classChain(cls).some((c) => + (c as ts.ClassLikeDeclaration).members.nodes.some((m) => { + const n = (m as { name?: ts.Node }).name; + return ( + n !== undefined && (ts.isIdentifier(n) || ts.isPrivateIdentifier(n)) && n.text === name + ); + }), + ); + // `this.m(…)` in an inherited method whose class does not declare `m` + // (zod's ZodType._parseSync calls the subclasses' _parse): the receiver + // is cast to the classes the method runs on, so the call dispatches over + // them instead of through an untyped call. + const receiverCasts = (method: ts.MethodDeclaration): void => { + const cls = method.parent; + const receivers = thisAtoms.get(method); + if (cls === undefined || receivers === undefined || receivers.size === 0) return; + if (![...receivers].every((a) => a.kind === "instance")) return; + const t = typeOf(receivers, false, 0); + if (t === null) return; + const scan = (node: ts.Node): void => { + if (ts.isFunctionLike(node) && !ts.isArrowFunction(node)) return; + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + node.expression.expression.kind === ts.SyntaxKind.ThisKeyword && + ts.isIdentifier(node.expression.name) && + !declaresMember(cls, node.expression.name.text) + ) { + const self = node.expression.expression; + edits.push({ pos: ts.tokenStartOfNode(self), text: `/** @type {${t}} ${MARK} */ (` }); + edits.push({ pos: self.end, text: ")" }); + annotations++; + } + ts.forEachChild(node, (c) => { + scan(c); + return undefined; + }); + }; + if (method.body !== undefined) for (const st of method.body.statements.nodes) scan(st); + }; + + // Same-named instance methods of the file's classes form a family: a + // call on a union of those classes needs one signature, so each member's + // parameters and result join the whole family's values. + const families = new Map(); + const familyVisit = (node: ts.Node): void => { + if ( + ts.isMethodDeclaration(node) && + ts.isIdentifier(node.name) && + !list((node as { modifiers?: { nodes: readonly ts.Node[] } }).modifiers).some( + (m) => m.kind === ts.SyntaxKind.StaticKeyword, + ) + ) { + let l = families.get(node.name.text); + if (l === undefined) families.set(node.name.text, (l = [])); + l.push(node); + } + ts.forEachChild(node, (c) => { + familyVisit(c); + return undefined; + }); + }; + familyVisit(sf); + // Members the program runs (they have analysis contexts); members it + // never reaches would only widen the family's types. + const reached = new Set(); + for (const info of analysis.functions.values()) + if (info.node.getSourceFile() === sf) reached.add(info.node); + const familyOf = (fn: ts.FunctionLikeDeclaration): readonly ts.MethodDeclaration[] => { + if (!ts.isMethodDeclaration(fn) || !ts.isIdentifier(fn.name)) + return [fn as ts.MethodDeclaration]; + const members = (families.get(fn.name.text) ?? [fn]).filter((m) => reached.has(m)); + return members.length > 0 ? members : [fn]; + }; + const paramValues = (fn: ts.FunctionLikeDeclaration, index: number): Set => { + const out = new Set(); + for (const member of familyOf(fn)) { + const p = list(member.parameters)[index]; + if (p === undefined) continue; + for (const a of analysis.declValues(p)) out.add(a); + } + return out; + }; + const returnValues = (fn: ts.FunctionLikeDeclaration): Set | undefined => { + let out: Set | undefined; + for (const member of familyOf(fn)) { + const r = returns.get(member); + if (r === undefined) continue; + out ??= new Set(); + for (const a of r) out.add(a); + } + return out; + }; + + const paramTags = (fn: ts.FunctionLikeDeclaration): string | null => { + const tags: string[] = []; + if (!ts.isConstructorDeclaration(fn) && !constructed.has(fn)) { + const ret = returnValues(fn); + const t = ret !== undefined && ret.size > 0 ? typeOf(ret, false, 0) : null; + const why = process.env["SCRIPTC_DEBUG_SHAPE_OVERLAY_WHY"]; + if ( + why !== undefined && + t === null && + ret !== undefined && + ts.isMethodDeclaration(fn) && + ts.isIdentifier(fn.name) && + fn.name.text === why + ) + process.stderr.write( + `[shape-overlay-why] ${fn.name.text}: ${[...ret].map((a) => `${a.kind}:${a.label}${analysis.escaped.has(a) ? "(escaped)" : ""}`).join(" | ")}\n`, + ); + if (t !== null) tags.push(`@returns {${t}}`); + } + list(fn.parameters).forEach((p, index) => { + if ( + !ts.isIdentifier(p.name) || + p.dotDotDotToken !== undefined || + p.initializer !== undefined || + p.questionToken !== undefined + ) + return; + const t = typeOf(paramValues(fn, index), false, 0); + if (t !== null) tags.push(`@param {${t}} ${p.name.text}`); + }); + return tags.length > 0 ? `${tags.join(" ")} ${MARK}` : null; + }; + + const visit = (node: ts.Node): void => { + if (ts.isMethodDeclaration(node)) receiverCasts(node); + if ( + ts.isMethodDeclaration(node) || + ts.isConstructorDeclaration(node) || + ts.isFunctionDeclaration(node) + ) { + if (node.body !== undefined) { + const tags = paramTags(node); + if (tags !== null) { + edits.push({ pos: ts.tokenStartOfNode(node), text: `/** ${tags} */ ` }); + annotations++; + } + } + // `this.f = …` fields of a constructor's top-level statements. + if (ts.isConstructorDeclaration(node) && node.body !== undefined) { + const cls = node.parent; + const seen = new Set(); + for (const st of list(node.body.statements)) { + if (!ts.isExpressionStatement(st) || !ts.isBinaryExpression(st.expression)) continue; + const b = st.expression; + if ( + b.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !ts.isPropertyAccessExpression(b.left) + ) + continue; + if ( + b.left.expression.kind !== ts.SyntaxKind.ThisKeyword || + !ts.isIdentifier(b.left.name) + ) + continue; + const name = b.left.name.text; + if (seen.has(name)) continue; + seen.add(name); + const t = cls !== undefined ? fieldType(cls, name) : null; + if (t !== null) { + edits.push({ pos: ts.tokenStartOfNode(st), text: `/** @type {${t}} ${MARK} */ ` }); + annotations++; + } + } + } + } + // Locals only: module-level bindings are initialized once (and a + // record-typed module constant can turn a return type into a union of + // overlapping records, which the lowering width-coerces). + if ( + ts.isVariableStatement(node) && + (node.parent !== sf || process.env["SCRIPTC_SHAPE_OVERLAY_MODULE_VARS"] === "1") && + node.declarationList.declarations.nodes.length === 1 + ) { + const d = node.declarationList.declarations.nodes[0]!; + const init = d.initializer; + if (ts.isIdentifier(d.name) && init !== undefined && !literalInitializer(init)) { + const t = typeOf(analysis.declValues(d), false, 0); + if (t !== null) { + edits.push({ pos: ts.tokenStartOfNode(node), text: `/** @type {${t}} ${MARK} */ ` }); + annotations++; + } + } + } + ts.forEachChild(node, (c) => { + visit(c); + return undefined; + }); + }; + + const fieldType = (cls: ts.Node, name: string): string | null => { + const atoms = new Set(); + for (const a of analysis.atoms.atoms) { + if ( + a.kind !== "instance" || + a.node === undefined || + !analysis.classChain(a.node).includes(cls) + ) + continue; + const id = analysis.fields.get(a)?.get(name); + if (id !== undefined) for (const x of analysis.nodes[id]!.atoms) atoms.add(x); + } + return typeOf(atoms, false, 0); + }; + + visit(sf); + // Dead branches: outermost first; edits inside a replaced range drop. + const dead = (deadByFile.get(sf) ?? []).sort((a, b) => a.pos - b.pos); + const ranges: [number, number][] = []; + const removed = new Set(); + for (const branch of dead) { + const start = ts.tokenStartOfNode(branch); + if (ranges.some(([lo, hi]) => start >= lo && start < hi)) continue; + if (hoistedNames(branch, removed) === null) continue; + removed.add(branch); + ranges.push([start, branch.end]); + } + // A var that only removed branches declare stays declared in each. + const hoisted = new Map(); + for (const branch of removed) + hoisted.set(ts.tokenStartOfNode(branch), hoistedNames(branch, removed)!); + if (ranges.length > 0) { + const kept = edits.filter((e) => !ranges.some(([lo, hi]) => e.pos > lo && e.pos < hi)); + edits.length = 0; + edits.push(...kept); + const pkg = packageNameOf(sf.fileName); + const rel = sf.fileName.split("/node_modules/").pop() ?? sf.fileName; + for (const [lo, hi] of ranges) { + const line = sf.getLineAndCharacterOfPosition(lo).line + 1; + const message = + `scriptc internal error: shape inference proved the branch at ${rel}:${line} unreachable, but it ran. ` + + `This is a bug in scriptc's shape analysis, not in the program. Rebuild with --no-shape-overlay, ` + + `or set "scriptc": {"packages": {"${pkg}": {"shapes": false}}} in package.json.`; + // The branch's own var names stay declared in its scope, undefined. + const names = hoisted.get(lo)!; + const vars = names.length > 0 ? `var ${names.join(", ")}; ` : ""; + edits.push({ + pos: lo, + end: hi, + text: `{ ${vars}throw new Error(${JSON.stringify(message)}); }`, + }); + deadBranches++; + } + } + // Debugging: SCRIPTC_SHAPE_OVERLAY_ONLY=a-b keeps the annotations + // numbered a..b (program order across files), to bisect a miscompile. + const only = process.env["SCRIPTC_SHAPE_OVERLAY_ONLY"]; + if (only !== undefined) { + const [lo, hi] = only.split("-").map(Number) as [number, number]; + edits.sort((a, b) => a.pos - b.pos); + const kept = edits.filter(() => { + const n = debugIndex++; + return n >= lo && n <= hi; + }); + edits.length = 0; + edits.push(...kept); + } + if (edits.length === 0) continue; + files.set(sf.fileName, applyEdits(neutralizeDocTags(sf), edits) + typedefText.join("")); + } + return { files, annotations, deadBranches }; +} + +function packageNameOf(fileName: string): string { + const parts = (fileName.split("/node_modules/").pop() ?? "").split("/"); + return parts[0]!.startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0]!; +} + +function literalInitializer(e: ts.Expression): boolean { + return ( + ts.isObjectLiteralExpression(e) || + (ts.isArrayLiteralExpression(e) && e.elements.nodes.length > 0) || + ts.isStringLiteral(e) || + ts.isNumericLiteral(e) || + ts.isNoSubstitutionTemplateLiteral(e) || + ts.isArrowFunction(e) || + ts.isFunctionExpression(e) || + e.kind === ts.SyntaxKind.TrueKeyword || + e.kind === ts.SyntaxKind.FalseKeyword + ); +} + +function applyEdits(text: string, edits: readonly Edit[]): string { + const sorted = [...edits].sort((a, b) => a.pos - b.pos); + let out = ""; + let at = 0; + for (const e of sorted) { + out += text.slice(at, e.pos) + e.text; + at = e.end ?? e.pos; + } + return out + text.slice(at); +} + +/** The file's text with the JSDoc type tags of its comments made plain + * text: package JSDoc is a hint, not a declaration. String, template and + * regular expression literals and JSX text are copied unchanged, so + * comment-like text in them keeps its runtime value. Lengths are unchanged, + * so edit positions in the source text stay valid. */ +export function neutralizeDocTags(sf: ts.SourceFile): string { + const text = sf.text; + // Literal token ranges, in source order. + const literals: [number, number][] = []; + const visit = (node: ts.Node): void => { + if (ts.isJsxText(node)) { + literals.push([node.pos, node.end]); + return; + } + if ( + ts.isStringLiteral(node) || + ts.isNoSubstitutionTemplateLiteral(node) || + ts.isTemplateHead(node) || + ts.isTemplateMiddle(node) || + ts.isTemplateTail(node) || + ts.isRegularExpressionLiteral(node) + ) { + literals.push([ts.tokenStartOfNode(node), node.end]); + return; + } + ts.forEachChild(node, (c) => { + visit(c); + return undefined; + }); + }; + visit(sf); + literals.sort((a, b) => a[0] - b[0]); + let out = ""; + let copied = 0; + let next = 0; + let pos = 0; + // Outside literals, `//` and `/*` only start comments. + while (pos < text.length) { + while (next < literals.length && literals[next]![1] <= pos) next++; + const literal = literals[next]; + if (literal !== undefined && literal[0] <= pos) { + pos = literal[1]; + continue; + } + if (text.charCodeAt(pos) !== 47) { + pos++; + continue; + } + const second = text.charCodeAt(pos + 1); + if (second === 47) { + while (pos < text.length && !lineBreak(text.charCodeAt(pos))) pos++; + continue; + } + if (second !== 42) { + pos++; + continue; + } + const close = text.indexOf("*/", pos + 2); + const end = close < 0 ? text.length : close + 2; + const comment = text.slice(pos, end); + if (comment.startsWith("/**") && !comment.includes(MARK)) { + out += text.slice(copied, pos) + neutralizeComment(comment); + copied = end; + } + pos = end; + } + return out + text.slice(copied); +} + +function lineBreak(code: number): boolean { + return code === 10 || code === 13 || code === 0x2028 || code === 0x2029; +} + +function neutralizeComment(comment: string): string { + return comment.replace( + /@(param|type|typedef|returns?|template|this|callback|property|prop|arg|argument|enum|implements|extends|augments|satisfies|overload|import)\b/g, + "-$1", + ); +} + +/** The `var` names a dead branch declares that its function (or file) + * declares nowhere else outside the `removed` branches: replacing the + * branch must keep them bound, as undefined. Null when the branch declares + * a function in a block, whose binding outside the block depends on the + * file's strictness; such a branch is kept. */ +function hoistedNames(branch: ts.Node, removed: ReadonlySet): readonly string[] | null { + let scope: ts.Node | undefined = branch.parent; + while (scope !== undefined && !ts.isSourceFile(scope) && !functionScope(scope)) + scope = scope.parent; + if (scope === undefined) return null; + const own = new Set(); + let blockFunction = false; + ts.walkPreorder(branch, (n) => { + if (n !== branch && ts.isFunctionDeclaration(n)) { + blockFunction = true; + return "skip"; + } + if (n !== branch && (functionScope(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, own); + return undefined; + }); + if (blockFunction) return null; + if (own.size === 0) return []; + // Names the scope binds outside the branch: parameters, a function + // expression's name, other var declarations and function declarations. + const elsewhere = new Set(); + if (!ts.isSourceFile(scope)) { + for (const p of list( + (scope as { parameters?: { readonly nodes: readonly ts.ParameterDeclaration[] } }).parameters, + )) + addBindingNames(p.name, elsewhere); + const name = (scope as { name?: ts.Node }).name; + if (name !== undefined && ts.isIdentifier(name)) elsewhere.add(name.text); + } + const body = ts.isSourceFile(scope) ? scope : (scope as { body?: ts.Node }).body; + if (body !== undefined) + ts.walkPreorder(body, (n) => { + if (n === branch || removed.has(n)) return "skip"; + if (n !== body && ts.isFunctionDeclaration(n) && n.name !== undefined) + elsewhere.add(n.name.text); + if (n !== body && (functionScope(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, elsewhere); + return undefined; + }); + return [...own].filter((name) => !elsewhere.has(name)); +} + +function functionScope(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) || + ts.isClassStaticBlockDeclaration(n) + ); +} + +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); +} + +/** A record site's keys: its static keys, or for a literal with spreads its + * own keys plus every key a spread source contributes (optional unless + * every value of that source has it). Null when a computed key or an + * untracked spread source makes the key set unknown. */ +function keysOfSite( + analysis: ShapeAnalysis, + v: SiteVerdict, +): { keys: string[]; optional: Set } | null { + const site = v.site; + if (site.keys !== null) return { keys: site.keys, optional: new Set() }; + if (site.hasComputedKey) return null; + const fields = analysis.fields.get(site.atom); + const any = fields?.get("*"); + if (any !== undefined && analysis.nodes[any]!.atoms.size > 0) return null; + const required = new Set(site.ownKeys); + for (const spread of site.spreads) { + const sets: Set[] = []; + for (const a of analysis.nodes[spread]!.atoms) { + if (a.kind === "prim" && (a.label === "undefined" || a.label === "null")) continue; + if (a.kind !== "object") return null; + const own = new Set(); + for (const k of analysis.fields.get(a)?.keys() ?? []) if (k !== "*") own.add(k); + sets.push(own); + } + const first = sets[0]; + if (first === undefined) continue; + for (const k of first) if (sets.every((set) => set.has(k))) required.add(k); + } + const all = [...site.ownKeys]; + for (const k of fields?.keys() ?? []) if (k !== "*" && !all.includes(k)) all.push(k); + return { keys: all, optional: new Set(all.filter((k) => !required.has(k))) }; +} diff --git a/packages/compiler/src/frontend/ts7/ast-node.ts b/packages/compiler/src/frontend/ts7/ast-node.ts index c49dceffb..7120b90d4 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.ts @@ -194,7 +194,7 @@ export class AstFile { if (parsed.path !== this.root.path) throw new AstDecodeError("node handle belongs to another source file"); const node = this.node(parsed.index); - if (node.kind !== parsed.kind) + if (node.kind !== this.wire.kindFromWire(parsed.kind)) throw new AstDecodeError("node handle kind does not match the response"); return node; } @@ -350,7 +350,7 @@ export class AstNode { } get id(): string { - return `${this.index}.${this.kind}.${this.file.root.path}`; + return `${this.index}.${this.file.wire.kindOnWire(this.kind)}.${this.file.root.path}`; } get parent(): AstNode | undefined { if (!this.parentResolved) { diff --git a/packages/compiler/src/frontend/ts7/ast-wire-protocols.test.ts b/packages/compiler/src/frontend/ts7/ast-wire-protocols.test.ts new file mode 100644 index 000000000..36aca02dd --- /dev/null +++ b/packages/compiler/src/frontend/ts7/ast-wire-protocols.test.ts @@ -0,0 +1,132 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { expect, test } from "vitest"; +import { AstFile } from "./ast-node.js"; +import { AstWireFile, PROTOCOL_VERSION_7_1 } from "./ast-wire.js"; +import { + AstKind, + HEADER_OFFSET_NODES, + KIND_NODE_LIST, + NODE_LEN, + NODE_OFFSET_DATA, + NODE_OFFSET_KIND, + PROTOCOL_VERSION, +} from "./ast-schema.generated.js"; + +/** The same sources' getSourceFile responses from tsc 7.0.2 (protocol 5) + * and tsc 7.1.0-beta (protocol 9). */ +const fixtures = JSON.parse( + readFileSync( + fileURLToPath(new URL("../../../test/fixtures/ts7-protocols.json", import.meta.url)), + "utf8", + ), +) as { + sources: Record; + protocol5: Record; + protocol9: Record; +}; +const names = Object.keys(fixtures.sources); +const bytes = (protocol: "protocol5" | "protocol9", name: string): Uint8Array => + new Uint8Array(Buffer.from(fixtures[protocol][name]!, "base64")); + +/** Every node's fields as TypeScript 7.0 sees them. A node list's flags word + * is left out: protocol 9 stores whether the list has a trailing comma there, + * which readers take from the source text. */ +function nodes(wire: AstWireFile): string[] { + const result: string[] = []; + for (let index = 1; index < wire.nodeCount; index++) { + const kind = wire.kind(index); + const list = kind === KIND_NODE_LIST; + result.push( + [ + kind, + wire.pos(index), + wire.end(index), + list ? 0 : wire.flags(index), + list || wire.data(index) >>> 30 !== 2 ? wire.data(index) : wire.data(index) >>> 24, + wire.parent(index), + wire.next(index), + list ? "" : (wire.text(index) ?? ""), + list ? "" : (wire.rawText(index) ?? ""), + ].join(" "), + ); + } + return result; +} + +test("protocol 5 and protocol 9 responses decode to the same trees", () => { + for (const name of names) { + const old = new AstWireFile(bytes("protocol5", name)); + const next = new AstWireFile(bytes("protocol9", name)); + expect(old.protocol).toBe(PROTOCOL_VERSION); + expect(next.protocol).toBe(PROTOCOL_VERSION_7_1); + expect(nodes(next), name).toEqual(nodes(old)); + expect(next.preorder(), name).toEqual(old.preorder()); + const oldFile = new AstFile(bytes("protocol5", name)).sourceFile; + const nextFile = new AstFile(bytes("protocol9", name)).sourceFile; + expect(nextFile.fileName).toBe(oldFile.fileName); + expect(nextFile.text).toBe(fixtures.sources[name]); + expect(nextFile.languageVariant).toBe(oldFile.languageVariant); + expect(nextFile.statements.nodes.length).toBe(oldFile.statements.nodes.length); + } +}); + +test("protocol 9 node handles name TypeScript 7.1's kinds", () => { + const file = new AstFile(bytes("protocol9", "main.ts")); + const wire = file.wire; + let index = 1; + while (wire.kind(index) !== AstKind.FunctionDeclaration) index++; + const node = file.node(index); + expect(node.kind).toBe(AstKind.FunctionDeclaration); + expect(node.id).toBe(`${index}.${AstKind.FunctionDeclaration + 1}.${file.root.path}`); + expect(file.resolve(node.id)).toBe(node); + expect(() => file.resolve(`${index}.${AstKind.FunctionDeclaration}.${file.root.path}`)).toThrow( + "node handle kind does not match the response", + ); + // Tokens below SourceKeyword keep their numbers. + expect(wire.kindOnWire(AstKind.Identifier)).toBe(AstKind.Identifier); + expect(wire.kindFromWire(167)).toBe(-1); + const old = new AstFile(bytes("protocol5", "main.ts")); + expect(old.node(index).id).toBe(`${index}.${AstKind.FunctionDeclaration}.${old.root.path}`); +}); + +/** A copy of `name`'s protocol 9 response with the first node of `kind` + * (TypeScript 7.0's) changed by `change`. */ +function changed( + name: string, + kind: number, + change: (view: DataView, at: number) => void, +): Uint8Array { + const data = bytes("protocol9", name); + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + const nodes = view.getUint32(HEADER_OFFSET_NODES, true); + const onWire = kind >= 167 ? kind + 1 : kind; + for (let at = nodes + NODE_LEN; at < data.length; at += NODE_LEN) { + if (view.getUint32(at + NODE_OFFSET_KIND, true) !== onWire) continue; + change(view, at); + return data; + } + throw new Error(`no node of kind ${kind}`); +} + +test("protocol 9 nodes that TypeScript 7.0 has no form for are rejected", () => { + const sourceKeyword = changed("main.ts", AstKind.Identifier, (view, at) => + view.setUint32(at + NODE_OFFSET_KIND, 167, true), + ); + expect(() => new AstWireFile(sourceKeyword)).toThrow("SourceKeyword"); + const attributes = changed("main.ts", AstKind.ModuleDeclaration, (view, at) => + view.setUint32(at + NODE_OFFSET_DATA, view.getUint32(at + NODE_OFFSET_DATA, true) | 4, true), + ); + expect(() => new AstWireFile(attributes)).toThrow("import attributes on a module declaration"); + const source = changed("main.ts", AstKind.ImportClause, (view, at) => + view.setUint32( + at + NODE_OFFSET_DATA, + (view.getUint32(at + NODE_OFFSET_DATA, true) | (3 << 24)) >>> 0, + true, + ), + ); + expect(() => new AstWireFile(source)).toThrow("`import source`"); + const version = bytes("protocol9", "main.ts"); + version[3] = 6; + expect(() => new AstWireFile(version)).toThrow("unsupported protocol version 6"); +}); diff --git a/packages/compiler/src/frontend/ts7/ast-wire.ts b/packages/compiler/src/frontend/ts7/ast-wire.ts index 6b2222eae..4cba5923a 100644 --- a/packages/compiler/src/frontend/ts7/ast-wire.ts +++ b/packages/compiler/src/frontend/ts7/ast-wire.ts @@ -123,6 +123,44 @@ function astWords(bytes: Uint8Array, offset: number, count: number): Uint32Array return words; } +/** TypeScript 7.1's encoding, which this reader accepts beside + * PROTOCOL_VERSION (TypeScript 7.0's). Its header is 64 bytes: after the + * section offsets come the source file ID and lease ID (u64 each) and the + * binder data offset. Its node kinds count 7.1's SourceKeyword (167), so each + * kind above it is one more than 7.0's; the reader returns 7.0's kinds. A + * module declaration's children gain `attributes` in slot 2, which moves + * `body` to slot 3, and an import clause's phase gains `source` (3). */ +export const PROTOCOL_VERSION_7_1 = 9; +const HEADER_SIZE_7_1 = 64; +/** SourceKeyword in TypeScript 7.1's kinds. */ +const SOURCE_KEYWORD_7_1 = 167; + +/** The node table of a protocol 7.1 response in TypeScript 7.0's terms: each + * kind above SourceKeyword one lower, and each module declaration's child + * mask without the attributes slot. Throws for the nodes 7.0 has no form + * for: a SourceKeyword node, a module declaration with import attributes, + * and an `import source` clause. */ +function wordsAs70(words: Uint32Array, count: number): Uint32Array { + const result = new Uint32Array(words); + for (let index = 1; index < count; index++) { + const word = index * NODE_WORDS; + let kind = result[word + WORD_KIND]!; + if (kind === KIND_NODE_LIST) continue; + if (kind === SOURCE_KEYWORD_7_1) + throw new AstDecodeError("TypeScript 7.1's SourceKeyword node is not supported"); + if (kind > SOURCE_KEYWORD_7_1) result[word + WORD_KIND] = --kind; + const data = result[word + WORD_DATA]!; + if (kind === AstKind.ModuleDeclaration && data >>> 30 === 0) { + if ((data & 4) !== 0) + throw new AstDecodeError("import attributes on a module declaration are not supported"); + result[word + WORD_DATA] = ((data & ~0xff) | (data & 3) | ((data & 8) >>> 1)) >>> 0; + } else if (kind === AstKind.ImportClause && ((data >>> 24) & 3) === 3) { + throw new AstDecodeError("`import source` is not supported"); + } + } + return result; +} + function hexWord(value: number): string { const digits = "0123456789abcdef"; let result = ""; @@ -139,6 +177,8 @@ export class AstWireFile { readonly stringCount: number; readonly contentHash: string; readonly parseOptionsKey: string; + /** The response's protocol: PROTOCOL_VERSION or PROTOCOL_VERSION_7_1. */ + readonly protocol: number; private readonly strings: number; private readonly extended: number; private readonly structured: number; @@ -155,15 +195,19 @@ export class AstWireFile { constructor(private readonly bytes: Uint8Array) { astBounds(bytes, 0, HEADER_SIZE); const metadata = astU32(bytes, HEADER_OFFSET_METADATA); - if (metadata >>> 24 !== PROTOCOL_VERSION) - throw new AstDecodeError(`unsupported protocol version ${metadata >>> 24}`); + const protocol = metadata >>> 24; + if (protocol !== PROTOCOL_VERSION && protocol !== PROTOCOL_VERSION_7_1) + throw new AstDecodeError(`unsupported protocol version ${protocol}`); + this.protocol = protocol; + const headerSize = protocol === PROTOCOL_VERSION_7_1 ? HEADER_SIZE_7_1 : HEADER_SIZE; + astBounds(bytes, 0, headerSize); const stringOffsets = astU32(bytes, HEADER_OFFSET_STRING_TABLE_OFFSETS); this.strings = astU32(bytes, HEADER_OFFSET_STRING_TABLE); this.extended = astU32(bytes, HEADER_OFFSET_EXTENDED_DATA); this.structured = astU32(bytes, HEADER_OFFSET_STRUCTURED_DATA); this.nodes = astU32(bytes, HEADER_OFFSET_NODES); if ( - stringOffsets < HEADER_SIZE || + stringOffsets < headerSize || this.strings < stringOffsets || this.extended < this.strings || this.structured < this.extended || @@ -177,7 +221,8 @@ export class AstWireFile { } this.nodeCount = (bytes.length - this.nodes) / NODE_LEN; this.stringCount = (this.strings - stringOffsets) / 8; - this.words = astWords(bytes, this.nodes, this.nodeCount * NODE_WORDS); + const words = astWords(bytes, this.nodes, this.nodeCount * NODE_WORDS); + this.words = protocol === PROTOCOL_VERSION_7_1 ? wordsAs70(words, this.nodeCount) : words; this.stringWords = astWords(bytes, stringOffsets, this.stringCount * 2); this.stringCache = new Array(this.stringCount).fill(undefined); if (this.nodeCount < 2) throw new AstDecodeError("missing root"); @@ -200,6 +245,19 @@ export class AstWireFile { } } + /** `kind` (TypeScript 7.0's) as this response's protocol numbers it, the + * kind a node handle names. */ + kindOnWire(kind: number): number { + return this.protocol === PROTOCOL_VERSION_7_1 && kind >= SOURCE_KEYWORD_7_1 ? kind + 1 : kind; + } + + /** A kind as this response's protocol numbers it (a node handle's) in + * TypeScript 7.0's numbering; -1 for 7.1's SourceKeyword. */ + kindFromWire(kind: number): number { + if (this.protocol !== PROTOCOL_VERSION_7_1 || kind < SOURCE_KEYWORD_7_1) return kind; + return kind === SOURCE_KEYWORD_7_1 ? -1 : kind - 1; + } + /** The first word of node `index`. `index >>> 0 === index` accepts exactly * the integers in [0, 2^32), so with the bound it accepts the same indices * as an integer and range check. */ diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 86ff00129..b2fc774f2 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1287,6 +1287,10 @@ export interface TypeMapperCtx { * only after the strict mapping (and its bound-alias retry) returns null, * at every depth, so every occurrence of a degraded type maps alike. */ degradedType?: (type: ts.Type) => IrType | null; + /** Canonical normalization's shadow mode (canon-map.ts): called after + * each successful top-level mapping; installed only under + * SCRIPTC_CANON_SHADOW=1. It records, and never changes a result. */ + canonShadow?: (type: ts.Type, mapped: IrType, ctx: TypeMapperCtx) => void; /** GENERIC program classes (monomorphization by flow): the mapping of a * concrete instantiation reference (`Box`) — the Lowerer * registers/reuses the instantiation (`Box%0`) and answers its object @@ -1841,6 +1845,17 @@ function arrayClassBaseType( } export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { + const shadow = ctx.canonShadow; + if (shadow === undefined || mapTypeDepth !== 0) return mapTypeEntry(type, ctx); + // Shadow mode observes every successful top-level mapping, including the + // intrinsic primitives, local class values and memo hits that return + // before the mapping proper. + const mapped = mapTypeEntry(type, ctx); + if (mapped !== null) shadow(type, mapped, ctx); + return mapped; +} + +function mapTypeEntry(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (mapTypeDepth >= MAP_TYPE_MAX_DEPTH) return null; // These intrinsic domains cannot depend on generic bindings, declaration // provenance, or recursive shape state. Enum literals retain the ordinary diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index d6314cd75..73f99598e 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -1,3 +1,4 @@ +import { shapeOverlayEnabled } from "./frontend/shape-inference/index.js"; import { nodeFrontendServices } from "./frontend/services-node.js"; import { runTypePatchCommand, @@ -485,6 +486,10 @@ export async function compile( opts: CompileRequestOptions, ): Promise { clearCompileSessionCaches(); + // `shapeOverlay: false` reaches the frontend (and its child process) + // through the environment the overlay reads. + const overlayEnv = process.env["SCRIPTC_SHAPE_OVERLAY"]; + if (opts.shapeOverlay === false) process.env["SCRIPTC_SHAPE_OVERLAY"] = "0"; const frontendInputs = new FrontendInputTracker(); const warnings: CompileWarningSink = { warnings: [], sourceTexts: null }; // compileTracked starts it for a build with no earlier build of the same invocation. @@ -497,6 +502,10 @@ export async function compile( } finally { setLibDiagnosticsStore(null); frontendProcess?.release(); + if (opts.shapeOverlay === false) { + if (overlayEnv === undefined) delete process.env["SCRIPTC_SHAPE_OVERLAY"]; + else process.env["SCRIPTC_SHAPE_OVERLAY"] = overlayEnv; + } } } @@ -1348,6 +1357,8 @@ function configureLibDiagnostics(root: string | null): void { export async function runFrontendProcess(requestPath: string, resultPath: string): Promise { const request = JSON.parse(await readFile(requestPath, "utf8")) as FrontendProcessRequest; + // The process started before the build's options were known. + if (request.opts.shapeOverlay === false) process.env["SCRIPTC_SHAPE_OVERLAY"] = "0"; clearCompileSessionCaches(); setProvenanceSources(request.provenance); // The compile that started this process prepared the cache root. @@ -1698,6 +1709,7 @@ async function compileTracked( ...checkerField(), ...(youngNurseryRequested() ? { youngNursery: true as const } : {}), npmStatic: opts.npmStatic ?? null, + ...(shapeOverlayEnabled() ? { shapeOverlay: true as const } : {}), ffiProfile: opts.ffiProfilePath === undefined || ffiProfileBytes === null ? null diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 9cc8bb030..deae5fb58 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -946,6 +946,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dns.setDefaultResultOrder", "net.connect", "net.connectCb", + "net.connectPath", + "net.connectPathCb", // resetAndDestroy on a TLS socket throws ERR_INVALID_HANDLE_TYPE. "net.sockResetAndDestroy", // SocketAddress's option checks and parse()'s input check. diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 3e0e1be99..b9c30779e 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1563,6 +1563,15 @@ function buildLibFnSignatures() { argTypes: [F64, STRING, { kind: "func", params: [], ret: VOID }], result: NETSOCKET_T, }, + /** net.connect/createConnection(path[, connectListener]) and the + * `path` option: a client of the Unix socket at `path`. With the flag + * (a positional string), Node's isPipeName applies first: a string that + * reads as a non-negative number is a localhost port. */ + "net.connectPath": { argTypes: [STRING, BOOL], result: NETSOCKET_T }, + "net.connectPathCb": { + argTypes: [STRING, BOOL, { kind: "func", params: [], ret: VOID }], + result: NETSOCKET_T, + }, // The lookup's exact func shape is program data (its answer callback's // union/record types) — checked specially in the libCall case. /** connect({ port, host, autoSelectFamily: true, lookup }) — the diff --git a/packages/compiler/src/ir/char-unit-locals.ts b/packages/compiler/src/ir/char-unit-locals.ts index fc1d5873b..be1eae2c3 100644 --- a/packages/compiler/src/ir/char-unit-locals.ts +++ b/packages/compiler/src/ir/char-unit-locals.ts @@ -8,6 +8,7 @@ import { type IrStmt, } from "./ir.js"; import { mapExprChildren, mapStmtChildren } from "./traverse.js"; +import { type IrFieldWalker, walkStmtFields } from "./fields.generated.js"; type CharAt = IrExpr & { kind: "strIntrinsic" }; type UnitUse = IrExpr & { kind: "strEq" | "strCmp" }; @@ -302,26 +303,24 @@ function rewriteFunction(fn: IrFunction): IrFunction { const copyOf = new Map(); // Whether a candidate string is declared from a charAt. let charAtDeclared = false; - const collect = (value: unknown): void => { - if (Array.isArray(value)) for (const item of value) collect(item); - else if (value !== null && typeof value === "object") { - const node = value as Record; - if (node.kind === "varDecl" && candidates.has(node.localId as string) && node.init) { - const id = node.localId as string; - const init = node.init as IrExpr; - if (locals.get(id)?.type.kind === "string" && isCharAt(init)) charAtDeclared = true; - const read = optionalCharRead(init); - if (read) reads.set(id, read); - else if (init.kind === "varRef" && reads.has(init.localId)) { - reads.set(id, reads.get(init.localId)!); - copyOf.set(id, init.localId); - } + // The declarations, in the order a walk of every field finds them. + const collector: IrFieldWalker = { + node: (node) => { + if (node.kind !== "varDecl" || !candidates.has(node.localId) || !node.init) return; + const id = node.localId; + const init = node.init; + if (locals.get(id)?.type.kind === "string" && isCharAt(init)) charAtDeclared = true; + const read = optionalCharRead(init); + if (read) reads.set(id, read); + else if (init.kind === "varRef" && reads.has(init.localId)) { + reads.set(id, reads.get(init.localId)!); + copyOf.set(id, init.localId); } - for (const key in node) if (key !== "loc") collect(node[key]); - } + }, + string: () => {}, }; // Both walks only record candidates: a function without one holds none. - if (candidates.size > 0) collect(fn.body); + if (candidates.size > 0) for (const stmt of fn.body) walkStmtFields(stmt, collector, false); const localTags = (operand: IrExpr): OptionalCharRead | null => operand.kind === "varRef" ? (reads.get(operand.localId) ?? null) : null; @@ -333,24 +332,22 @@ function rewriteFunction(fn: IrFunction): IrFunction { const note = (map: Map, id: string): void => { map.set(id, (map.get(id) ?? 0) + 1); }; - const count = (value: unknown): void => { - if (typeof value === "string") { - if (candidates.has(value)) note(mentions, value); - } else if (Array.isArray(value)) { - for (const item of value) count(item); - } else if (value !== null && typeof value === "object") { - const node = value as Record; + const counter: IrFieldWalker = { + node: (node) => { const id = understoodMention(node, locals, reads, localTags); if (id !== null && candidates.has(id)) { note(understood, id); if (node.kind === "varDecl") note(declarations, id); } - for (const key in node) if (key !== "loc") count(node[key]); - } + }, + string: (value) => { + if (candidates.has(value)) note(mentions, value); + }, }; // Only candidates declared from a charAt or an optional read can be held - // (understoodMention), and count() only decides which are. - if (reads.size > 0 || charAtDeclared) count(fn.body); + // (understoodMention), and the count only decides which are. + if (reads.size > 0 || charAtDeclared) + for (const stmt of fn.body) walkStmtFields(stmt, counter, false); // A copy is held only with its source, and the source's mention in the // copy's initializer is understood only while the copy is held. const held = new Set(); @@ -492,14 +489,12 @@ function unitLocal(local: IrLocal): IrLocal { * over one-unit cases; a union local's declaration from an optional read, * or its test against a one-unit literal or for undefined. */ function understoodMention( - node: Record, + n: IrExpr | IrStmt, locals: ReadonlyMap, reads: ReadonlyMap, localTags: (operand: IrExpr) => OptionalCharRead | null, ): string | null { - // count() visits every object in the body (types, locations, cases), so - // only the node kinds handled here are read as IR nodes - const kind = node.kind; + const kind = n.kind; if ( kind !== "varDecl" && kind !== "strEq" && @@ -510,7 +505,6 @@ function understoodMention( kind !== "unionIsTag" ) return null; - const n = node as unknown as IrExpr | IrStmt; if (n.kind === "varDecl") { if (!n.init) return null; const local = locals.get(n.localId); diff --git a/packages/compiler/src/ir/fields.generated.ts b/packages/compiler/src/ir/fields.generated.ts new file mode 100644 index 000000000..e9f5efaa3 --- /dev/null +++ b/packages/compiler/src/ir/fields.generated.ts @@ -0,0 +1,2071 @@ +// Generated from packages/compiler/src/ir/ir.ts by scripts/generate-ir-fields.mjs. +// Run `pnpm generate:ir-fields` after changing the fields of an IR node. +import type { IrExpr, IrStmt, IrType } from "./ir.js"; + +/** Calls `visit` with each string field of `node` (including `kind`) and its + * name: the string values `for (const key in node)` finds on a node, read + * by name in the order ir.ts declares them. */ +export function eachStringField( + node: IrExpr | IrStmt, + visit: (value: string, field: string) => void, +): void { + switch (node.kind) { + case "arrIntrinsic": { + visit(node.kind, "kind"); + visit(node.method, "method"); + return; + } + case "arrayDelete": { + visit(node.kind, "kind"); + return; + } + case "arrayGet": { + visit(node.kind, "kind"); + return; + } + case "arrayHas": { + visit(node.kind, "kind"); + return; + } + case "arrayLit": { + visit(node.kind, "kind"); + return; + } + case "arrayNewLen": { + visit(node.kind, "kind"); + return; + } + case "arraySet": { + visit(node.kind, "kind"); + return; + } + case "arraySetLength": { + visit(node.kind, "kind"); + return; + } + case "arraySetUndefined": { + visit(node.kind, "kind"); + return; + } + case "arrayState": { + visit(node.kind, "kind"); + return; + } + case "assign": { + visit(node.kind, "kind"); + visit(node.localId, "localId"); + return; + } + case "assignExpr": { + visit(node.kind, "kind"); + visit(node.localId, "localId"); + return; + } + case "awaitExpr": { + visit(node.kind, "kind"); + return; + } + case "awaitUnionExpr": { + visit(node.kind, "kind"); + return; + } + case "bin": { + visit(node.kind, "kind"); + visit(node.op, "op"); + return; + } + case "block": { + visit(node.kind, "kind"); + return; + } + case "boolLit": { + visit(node.kind, "kind"); + return; + } + case "break": { + visit(node.kind, "kind"); + if (typeof node.label === "string") visit(node.label, "label"); + return; + } + case "bytesIntrinsic": { + visit(node.kind, "kind"); + visit(node.method, "method"); + return; + } + case "bytesNew": { + visit(node.kind, "kind"); + return; + } + case "bytesSet": { + visit(node.kind, "kind"); + return; + } + case "call": { + visit(node.kind, "kind"); + visit(node.callee, "callee"); + return; + } + case "callValue": { + visit(node.kind, "kind"); + return; + } + case "caughtCheck": { + visit(node.kind, "kind"); + visit(node.className, "className"); + return; + } + case "caughtNarrow": { + visit(node.kind, "kind"); + return; + } + case "caughtTest": { + visit(node.kind, "kind"); + visit(node.test, "test"); + if (typeof node.className === "string") visit(node.className, "className"); + return; + } + case "caughtToDyn": { + visit(node.kind, "kind"); + return; + } + case "chainRecv": { + visit(node.kind, "kind"); + visit(node.id, "id"); + return; + } + case "classRef": { + visit(node.kind, "kind"); + visit(node.className, "className"); + return; + } + case "closure": { + visit(node.kind, "kind"); + visit(node.fnName, "fnName"); + return; + } + case "continue": { + visit(node.kind, "kind"); + if (typeof node.label === "string") visit(node.label, "label"); + return; + } + case "doWhile": { + visit(node.kind, "kind"); + return; + } + case "downcast": { + visit(node.kind, "kind"); + return; + } + case "dynArrLit": { + visit(node.kind, "kind"); + return; + } + case "dynCall": { + visit(node.kind, "kind"); + visit(node.calleeName, "calleeName"); + return; + } + case "dynCheck": { + visit(node.kind, "kind"); + if (typeof node.inferredSite === "string") visit(node.inferredSite, "inferredSite"); + return; + } + case "dynDestrCheck": { + visit(node.kind, "kind"); + visit(node.spelling, "spelling"); + if (typeof node.firstProp === "string") visit(node.firstProp, "firstProp"); + return; + } + case "dynFrom": { + visit(node.kind, "kind"); + if (typeof node.fnName === "string") visit(node.fnName, "fnName"); + return; + } + case "dynHasKey": { + visit(node.kind, "kind"); + visit(node.key, "key"); + return; + } + case "dynInvoke": { + visit(node.kind, "kind"); + visit(node.method, "method"); + visit(node.calleeName, "calleeName"); + return; + } + case "dynIterN": { + visit(node.kind, "kind"); + if (typeof node.notIterableMessage === "string") visit(node.notIterableMessage, "notIterableMessage"); + return; + } + case "dynKeyGet": { + visit(node.kind, "kind"); + return; + } + case "dynObjLit": { + visit(node.kind, "kind"); + return; + } + case "dynScalarEq": { + visit(node.kind, "kind"); + return; + } + case "dynTest": { + visit(node.kind, "kind"); + visit(node.test, "test"); + if (typeof node.bytesElem === "string") visit(node.bytesElem, "bytesElem"); + return; + } + case "exprStmt": { + visit(node.kind, "kind"); + return; + } + case "ffiCall": { + visit(node.kind, "kind"); + visit(node.import, "import"); + return; + } + case "fieldAbsent": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + return; + } + case "fieldGet": { + visit(node.kind, "kind"); + visit(node.className, "className"); + visit(node.field, "field"); + return; + } + case "fieldIncDec": { + visit(node.kind, "kind"); + visit(node.op, "op"); + visit(node.className, "className"); + visit(node.field, "field"); + return; + } + case "fieldSet": { + visit(node.kind, "kind"); + visit(node.className, "className"); + visit(node.field, "field"); + return; + } + case "for": { + visit(node.kind, "kind"); + return; + } + case "forOf": { + visit(node.kind, "kind"); + visit(node.localId, "localId"); + return; + } + case "genResume": { + visit(node.kind, "kind"); + visit(node.mode, "mode"); + return; + } + case "if": { + visit(node.kind, "kind"); + return; + } + case "incDec": { + visit(node.kind, "kind"); + visit(node.op, "op"); + visit(node.localId, "localId"); + return; + } + case "instanceOf": { + visit(node.kind, "kind"); + visit(node.className, "className"); + return; + } + case "instanceOfValue": { + visit(node.kind, "kind"); + return; + } + case "intrinsic": { + visit(node.kind, "kind"); + visit(node.name, "name"); + return; + } + case "jsonStringify": { + visit(node.kind, "kind"); + if (typeof node.indent === "string") visit(node.indent, "indent"); + return; + } + case "libCall": { + visit(node.kind, "kind"); + visit(node.fn, "fn"); + return; + } + case "logical": { + visit(node.kind, "kind"); + visit(node.op, "op"); + return; + } + case "mapIntrinsic": { + visit(node.kind, "kind"); + visit(node.method, "method"); + return; + } + case "mapNew": { + visit(node.kind, "kind"); + return; + } + case "moduleNsRef": { + visit(node.kind, "kind"); + visit(node.moduleId, "moduleId"); + return; + } + case "new": { + visit(node.kind, "kind"); + visit(node.className, "className"); + return; + } + case "newPromise": { + visit(node.kind, "kind"); + return; + } + case "newValue": { + visit(node.kind, "kind"); + return; + } + case "nullish": { + visit(node.kind, "kind"); + return; + } + case "numLit": { + visit(node.kind, "kind"); + if (typeof node.spelling === "string") visit(node.spelling, "spelling"); + return; + } + case "optChain": { + visit(node.kind, "kind"); + visit(node.id, "id"); + return; + } + case "orDefault": { + visit(node.kind, "kind"); + if (typeof node.retag === "string") visit(node.retag, "retag"); + return; + } + case "promiseVoidWiden": { + visit(node.kind, "kind"); + return; + } + case "promiseWithResolvers": { + visit(node.kind, "kind"); + return; + } + case "recordClone": { + visit(node.kind, "kind"); + return; + } + case "recordGet": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + visit(node.field, "field"); + return; + } + case "recordHas": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + visit(node.field, "field"); + return; + } + case "recordKeyDelete": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + return; + } + case "recordKeyGet": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + return; + } + case "recordKeySet": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + return; + } + case "recordLit": { + visit(node.kind, "kind"); + return; + } + case "recordNullProto": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + return; + } + case "recordOvfHas": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + return; + } + case "recordOvfKeys": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + return; + } + case "recordSet": { + visit(node.kind, "kind"); + visit(node.shapeId, "shapeId"); + visit(node.field, "field"); + return; + } + case "regexIntrinsic": { + visit(node.kind, "kind"); + visit(node.method, "method"); + return; + } + case "regexLit": { + visit(node.kind, "kind"); + visit(node.pattern, "pattern"); + visit(node.flags, "flags"); + return; + } + case "rethrow": { + visit(node.kind, "kind"); + visit(node.localId, "localId"); + return; + } + case "return": { + visit(node.kind, "kind"); + return; + } + case "runtimeFence": { + visit(node.kind, "kind"); + visit(node.code, "code"); + visit(node.message, "message"); + return; + } + case "selfRef": { + visit(node.kind, "kind"); + return; + } + case "seqExpr": { + visit(node.kind, "kind"); + return; + } + case "setIntrinsic": { + visit(node.kind, "kind"); + visit(node.method, "method"); + return; + } + case "setNew": { + visit(node.kind, "kind"); + return; + } + case "strCmp": { + visit(node.kind, "kind"); + visit(node.op, "op"); + return; + } + case "strConcat": { + visit(node.kind, "kind"); + return; + } + case "strEq": { + visit(node.kind, "kind"); + return; + } + case "strIntrinsic": { + visit(node.kind, "kind"); + visit(node.method, "method"); + return; + } + case "strLit": { + visit(node.kind, "kind"); + visit(node.value, "value"); + return; + } + case "switch": { + visit(node.kind, "kind"); + return; + } + case "templateStrings": { + visit(node.kind, "kind"); + visit(node.key, "key"); + return; + } + case "ternary": { + visit(node.kind, "kind"); + return; + } + case "throw": { + visit(node.kind, "kind"); + return; + } + case "toBool": { + visit(node.kind, "kind"); + return; + } + case "toString": { + visit(node.kind, "kind"); + return; + } + case "tryCatch": { + visit(node.kind, "kind"); + if (typeof node.catchLocalId === "string") visit(node.catchLocalId, "catchLocalId"); + return; + } + case "unary": { + visit(node.kind, "kind"); + visit(node.op, "op"); + return; + } + case "unionDisc": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + visit(node.field, "field"); + return; + } + case "unionEq": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + return; + } + case "unionFuncEq": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + return; + } + case "unionIsTag": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + return; + } + case "unionKeyGet": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + return; + } + case "unionNarrow": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + return; + } + case "unionRetype": { + visit(node.kind, "kind"); + visit(node.fromId, "fromId"); + visit(node.unionId, "unionId"); + return; + } + case "unionWrap": { + visit(node.kind, "kind"); + visit(node.unionId, "unionId"); + return; + } + case "unitLit": { + visit(node.kind, "kind"); + visit(node.unit, "unit"); + return; + } + case "upcast": { + visit(node.kind, "kind"); + return; + } + case "varDecl": { + visit(node.kind, "kind"); + visit(node.localId, "localId"); + return; + } + case "varRef": { + visit(node.kind, "kind"); + visit(node.localId, "localId"); + return; + } + case "virtualCall": { + visit(node.kind, "kind"); + visit(node.className, "className"); + visit(node.method, "method"); + return; + } + case "while": { + visit(node.kind, "kind"); + return; + } + case "yieldExpr": { + visit(node.kind, "kind"); + return; + } + } +} + +/** What a field walk reports (walkExprFields): each expression and + * statement node before its fields, and each string a field holds. */ +export interface IrFieldWalker { + node(node: IrExpr | IrStmt): void; + string(value: string): void; +} + +/** Walks every field of `node` but `loc` (and `type` with `skipType`), and + * every field of each object under it: the strings and nodes a recursive + * `for (const key in value)` walk with the same skips finds, read by name in + * the order ir.ts declares the fields. */ +export function walkExprFields(node: IrExpr, walker: IrFieldWalker, skipType: boolean): void { + walker.node(node); + switch (node.kind) { + case "arrIntrinsic": { + walker.string(node.kind); + walker.string(node.method); + walkExprFields(node.receiver, walker, skipType); + for (const item0 of node.args) { + walkExprFields(item0, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "arrayGet": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.index, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "arrayHas": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.index, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "arrayLit": { + walker.string(node.kind); + for (const item1 of node.elems) { + walkExprFields(item1, walker, skipType); + } + if (node.spreads !== undefined && node.spreads !== null) { + for (const item2 of node.spreads) { + } + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "arrayNewLen": { + walker.string(node.kind); + walkExprFields(node.length, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "arrayState": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.index, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "assignExpr": { + walker.string(node.kind); + walker.string(node.localId); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "awaitExpr": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "awaitUnionExpr": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "bin": { + walker.string(node.kind); + walker.string(node.op); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "boolLit": { + walker.string(node.kind); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "bytesIntrinsic": { + walker.string(node.kind); + walker.string(node.method); + walkExprFields(node.receiver, walker, skipType); + for (const item3 of node.args) { + walkExprFields(item3, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "bytesNew": { + walker.string(node.kind); + if (node.source !== undefined && node.source !== null) { + walkExprFields(node.source, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "call": { + walker.string(node.kind); + walker.string(node.callee); + for (const item4 of node.args) { + walkExprFields(item4, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "callValue": { + walker.string(node.kind); + walkExprFields(node.callee, walker, skipType); + if (node.receiver !== undefined && node.receiver !== null) { + walkExprFields(node.receiver, walker, skipType); + } + for (const item5 of node.args) { + walkExprFields(item5, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "caughtCheck": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + walker.string(node.className); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "caughtNarrow": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "caughtTest": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + walker.string(node.test); + if (node.className !== undefined && node.className !== null) { + walker.string(node.className); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "caughtToDyn": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "chainRecv": { + walker.string(node.kind); + walker.string(node.id); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "classRef": { + walker.string(node.kind); + walker.string(node.className); + if (node.captures !== undefined && node.captures !== null) { + for (const item6 of node.captures) { + walker.string(item6); + } + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "closure": { + walker.string(node.kind); + walker.string(node.fnName); + for (const item7 of node.captures) { + walker.string(item7); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "downcast": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynArrLit": { + walker.string(node.kind); + for (const item8 of node.elems) { + walkExprFields(item8, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynCall": { + walker.string(node.kind); + walkExprFields(node.callee, walker, skipType); + if (node.receiver !== undefined && node.receiver !== null) { + walkExprFields(node.receiver, walker, skipType); + } + walker.string(node.calleeName); + if (node.calleeNameValue !== undefined && node.calleeNameValue !== null) { + walkExprFields(node.calleeNameValue, walker, skipType); + } + for (const item9 of node.args) { + walkExprFields(item9, walker, skipType); + } + if (node.spreads !== undefined && node.spreads !== null) { + for (const item10 of node.spreads) { + walker.string(item10.what); + } + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynCheck": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (node.inferredSite !== undefined && node.inferredSite !== null) { + walker.string(node.inferredSite); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynDestrCheck": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + walker.string(node.spelling); + if (node.firstProp !== undefined && node.firstProp !== null) { + walker.string(node.firstProp); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynFrom": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (node.fnName !== undefined && node.fnName !== null) { + walker.string(node.fnName); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynHasKey": { + walker.string(node.kind); + walker.string(node.key); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynInvoke": { + walker.string(node.kind); + walkExprFields(node.recv, walker, skipType); + walker.string(node.method); + walker.string(node.calleeName); + if (node.calleeNameValue !== undefined && node.calleeNameValue !== null) { + walkExprFields(node.calleeNameValue, walker, skipType); + } + for (const item11 of node.args) { + walkExprFields(item11, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynIterN": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (node.notIterableMessage !== undefined && node.notIterableMessage !== null) { + walker.string(node.notIterableMessage); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynKeyGet": { + walker.string(node.kind); + walkExprFields(node.key, walker, skipType); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynObjLit": { + walker.string(node.kind); + if (node.fields !== undefined && node.fields !== null) { + for (const item12 of node.fields) { + walkExprFields(item12.key, walker, skipType); + walkExprFields(item12.value, walker, skipType); + } + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynScalarEq": { + walker.string(node.kind); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "dynTest": { + walker.string(node.kind); + walker.string(node.test); + if (node.bytesElem !== undefined && node.bytesElem !== null) { + walker.string(node.bytesElem); + } + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "ffiCall": { + walker.string(node.kind); + walker.string(node.import); + for (const item13 of node.args) { + walkExprFields(item13, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "fieldAbsent": { + walker.string(node.kind); + walker.string(node.unionId); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "fieldGet": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.className); + walker.string(node.field); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "fieldIncDec": { + walker.string(node.kind); + walker.string(node.op); + walkExprFields(node.obj, walker, skipType); + walker.string(node.className); + walker.string(node.field); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "genResume": { + walker.string(node.kind); + walker.string(node.mode); + walkExprFields(node.gen, walker, skipType); + if (node.arg !== undefined && node.arg !== null) { + walkExprFields(node.arg, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "incDec": { + walker.string(node.kind); + walker.string(node.op); + walker.string(node.localId); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "instanceOf": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + walker.string(node.className); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "instanceOfValue": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + walkExprFields(node.classValue, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "intrinsic": { + walker.string(node.kind); + walker.string(node.name); + for (const item14 of node.args) { + walkExprFields(item14, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "jsonStringify": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (node.indent !== undefined && node.indent !== null) { + walker.string(node.indent); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "libCall": { + walker.string(node.kind); + walker.string(node.fn); + for (const item15 of node.args) { + walkExprFields(item15, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + if (node.assertOptions !== undefined && node.assertOptions !== null) { + if (node.assertOptions.diff !== undefined && node.assertOptions.diff !== null) { + walker.string(node.assertOptions.diff); + } + } + return; + } + case "logical": { + walker.string(node.kind); + walker.string(node.op); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "mapIntrinsic": { + walker.string(node.kind); + walker.string(node.method); + walkExprFields(node.receiver, walker, skipType); + for (const item16 of node.args) { + walkExprFields(item16, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "mapNew": { + walker.string(node.kind); + if (node.seed !== undefined && node.seed !== null) { + for (const item17 of node.seed) { + walkExprFields(item17.key, walker, skipType); + walkExprFields(item17.value, walker, skipType); + } + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "moduleNsRef": { + walker.string(node.kind); + walker.string(node.moduleId); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "new": { + walker.string(node.kind); + walker.string(node.className); + for (const item18 of node.args) { + walkExprFields(item18, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "newPromise": { + walker.string(node.kind); + walkExprFields(node.executor, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "newValue": { + walker.string(node.kind); + walkExprFields(node.callee, walker, skipType); + for (const item19 of node.args) { + walkExprFields(item19, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "nullish": { + walker.string(node.kind); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "numLit": { + walker.string(node.kind); + if (node.spelling !== undefined && node.spelling !== null) { + walker.string(node.spelling); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "optChain": { + walker.string(node.kind); + walker.string(node.id); + walkExprFields(node.receiver, walker, skipType); + walkExprFields(node.body, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "orDefault": { + walker.string(node.kind); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (node.retag !== undefined && node.retag !== null) { + walker.string(node.retag); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "promiseVoidWiden": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "promiseWithResolvers": { + walker.string(node.kind); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordClone": { + walker.string(node.kind); + walkExprFields(node.source, walker, skipType); + for (const item20 of node.overrides) { + walker.string(item20.name); + walkExprFields(item20.value, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordGet": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + walker.string(node.field); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordHas": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + walker.string(node.field); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordKeyGet": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + walkExprFields(node.key, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordLit": { + walker.string(node.kind); + for (const item21 of node.fields) { + walker.string(item21.name); + walkExprFields(item21.value, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordNullProto": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordOvfHas": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + walkExprFields(node.key, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "recordOvfKeys": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "regexIntrinsic": { + walker.string(node.kind); + walker.string(node.method); + walkExprFields(node.receiver, walker, skipType); + for (const item22 of node.args) { + walkExprFields(item22, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "regexLit": { + walker.string(node.kind); + walker.string(node.pattern); + walker.string(node.flags); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "selfRef": { + walker.string(node.kind); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "seqExpr": { + walker.string(node.kind); + for (const item23 of node.stmts) { + walkStmtFields(item23, walker, skipType); + } + walkExprFields(node.result, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "setIntrinsic": { + walker.string(node.kind); + walker.string(node.method); + walkExprFields(node.receiver, walker, skipType); + for (const item24 of node.args) { + walkExprFields(item24, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "setNew": { + walker.string(node.kind); + if (node.seed !== undefined && node.seed !== null) { + walkExprFields(node.seed, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "strCmp": { + walker.string(node.kind); + walker.string(node.op); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "strConcat": { + walker.string(node.kind); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "strEq": { + walker.string(node.kind); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "strIntrinsic": { + walker.string(node.kind); + walker.string(node.method); + walkExprFields(node.receiver, walker, skipType); + for (const item25 of node.args) { + walkExprFields(item25, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "strLit": { + walker.string(node.kind); + walker.string(node.value); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "templateStrings": { + walker.string(node.kind); + walker.string(node.key); + for (const item26 of node.cooked) { + walker.string(item26); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "ternary": { + walker.string(node.kind); + walkExprFields(node.cond, walker, skipType); + walkExprFields(node.then, walker, skipType); + walkExprFields(node.else_, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "toBool": { + walker.string(node.kind); + walkExprFields(node.operand, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "toString": { + walker.string(node.kind); + walkExprFields(node.operand, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unary": { + walker.string(node.kind); + walker.string(node.op); + walkExprFields(node.operand, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionDisc": { + walker.string(node.kind); + walker.string(node.unionId); + walker.string(node.field); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionEq": { + walker.string(node.kind); + walker.string(node.unionId); + walkExprFields(node.left, walker, skipType); + walkExprFields(node.right, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionFuncEq": { + walker.string(node.kind); + walker.string(node.unionId); + walkExprFields(node.union, walker, skipType); + walkExprFields(node.func, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionIsTag": { + walker.string(node.kind); + walker.string(node.unionId); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionKeyGet": { + walker.string(node.kind); + walker.string(node.unionId); + walkExprFields(node.key, walker, skipType); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionNarrow": { + walker.string(node.kind); + walker.string(node.unionId); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionRetype": { + walker.string(node.kind); + walker.string(node.fromId); + walker.string(node.unionId); + for (const item27 of node.tags) { + } + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unionWrap": { + walker.string(node.kind); + walker.string(node.unionId); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "unitLit": { + walker.string(node.kind); + walker.string(node.unit); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "upcast": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "varRef": { + walker.string(node.kind); + walker.string(node.localId); + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + case "virtualCall": { + walker.string(node.kind); + walker.string(node.className); + walker.string(node.method); + for (const item28 of node.args) { + walkExprFields(item28, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + if (node.targets !== undefined && node.targets !== null) { + for (const item29 of node.targets) { + walker.string(item29); + } + } + return; + } + case "yieldExpr": { + walker.string(node.kind); + if (node.value !== undefined && node.value !== null) { + walkExprFields(node.value, walker, skipType); + } + if (node.captureCompletion !== undefined && node.captureCompletion !== null) { + walkTypeFields(node.captureCompletion.returnType, walker, skipType); + } + if (!skipType) { + walkTypeFields(node.type, walker, skipType); + } + return; + } + } +} + +/** Walks every field of `node` but `loc` (and `type` with `skipType`), and + * every field of each object under it: the strings and nodes a recursive + * `for (const key in value)` walk with the same skips finds, read by name in + * the order ir.ts declares the fields. */ +export function walkStmtFields(node: IrStmt, walker: IrFieldWalker, skipType: boolean): void { + walker.node(node); + switch (node.kind) { + case "arrayDelete": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.index, walker, skipType); + return; + } + case "arraySet": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.index, walker, skipType); + walkExprFields(node.value, walker, skipType); + return; + } + case "arraySetLength": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.length, walker, skipType); + return; + } + case "arraySetUndefined": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.index, walker, skipType); + return; + } + case "assign": { + walker.string(node.kind); + walker.string(node.localId); + walkExprFields(node.value, walker, skipType); + return; + } + case "block": { + walker.string(node.kind); + for (const item30 of node.body) { + walkStmtFields(item30, walker, skipType); + } + if (node.labels !== undefined && node.labels !== null) { + for (const item31 of node.labels) { + walker.string(item31); + } + } + return; + } + case "break": { + walker.string(node.kind); + if (node.label !== undefined && node.label !== null) { + walker.string(node.label); + } + return; + } + case "bytesSet": { + walker.string(node.kind); + walkExprFields(node.arr, walker, skipType); + walkExprFields(node.index, walker, skipType); + walkExprFields(node.value, walker, skipType); + return; + } + case "continue": { + walker.string(node.kind); + if (node.label !== undefined && node.label !== null) { + walker.string(node.label); + } + return; + } + case "doWhile": { + walker.string(node.kind); + for (const item32 of node.body) { + walkStmtFields(item32, walker, skipType); + } + walkExprFields(node.cond, walker, skipType); + if (node.labels !== undefined && node.labels !== null) { + for (const item33 of node.labels) { + walker.string(item33); + } + } + return; + } + case "exprStmt": { + walker.string(node.kind); + walkExprFields(node.expr, walker, skipType); + return; + } + case "fieldSet": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.className); + walker.string(node.field); + walkExprFields(node.value, walker, skipType); + return; + } + case "for": { + walker.string(node.kind); + if (node.init !== undefined && node.init !== null) { + walkStmtFields(node.init, walker, skipType); + } + if (node.cond !== undefined && node.cond !== null) { + walkExprFields(node.cond, walker, skipType); + } + if (node.update !== undefined && node.update !== null) { + walkStmtFields(node.update, walker, skipType); + } + for (const item34 of node.body) { + walkStmtFields(item34, walker, skipType); + } + if (node.labels !== undefined && node.labels !== null) { + for (const item35 of node.labels) { + walker.string(item35); + } + } + return; + } + case "forOf": { + walker.string(node.kind); + walker.string(node.localId); + walkExprFields(node.iterable, walker, skipType); + for (const item36 of node.body) { + walkStmtFields(item36, walker, skipType); + } + if (node.labels !== undefined && node.labels !== null) { + for (const item37 of node.labels) { + walker.string(item37); + } + } + return; + } + case "if": { + walker.string(node.kind); + walkExprFields(node.cond, walker, skipType); + for (const item38 of node.then) { + walkStmtFields(item38, walker, skipType); + } + if (node.else_ !== undefined && node.else_ !== null) { + for (const item39 of node.else_) { + walkStmtFields(item39, walker, skipType); + } + } + return; + } + case "recordKeyDelete": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + walkExprFields(node.key, walker, skipType); + return; + } + case "recordKeySet": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + walkExprFields(node.key, walker, skipType); + walkExprFields(node.value, walker, skipType); + return; + } + case "recordSet": { + walker.string(node.kind); + walkExprFields(node.obj, walker, skipType); + walker.string(node.shapeId); + walker.string(node.field); + walkExprFields(node.value, walker, skipType); + return; + } + case "rethrow": { + walker.string(node.kind); + walker.string(node.localId); + return; + } + case "return": { + walker.string(node.kind); + if (node.value !== undefined && node.value !== null) { + walkExprFields(node.value, walker, skipType); + } + return; + } + case "runtimeFence": { + walker.string(node.kind); + walker.string(node.code); + walker.string(node.message); + return; + } + case "switch": { + walker.string(node.kind); + walkExprFields(node.disc, walker, skipType); + for (const item40 of node.cases) { + if (item40.test !== undefined && item40.test !== null) { + walkExprFields(item40.test, walker, skipType); + } + for (const item41 of item40.body) { + walkStmtFields(item41, walker, skipType); + } + } + if (node.labels !== undefined && node.labels !== null) { + for (const item42 of node.labels) { + walker.string(item42); + } + } + return; + } + case "throw": { + walker.string(node.kind); + walkExprFields(node.value, walker, skipType); + return; + } + case "tryCatch": { + walker.string(node.kind); + for (const item43 of node.tryBody) { + walkStmtFields(item43, walker, skipType); + } + if (node.catchBody !== undefined && node.catchBody !== null) { + for (const item44 of node.catchBody) { + walkStmtFields(item44, walker, skipType); + } + } + if (node.catchLocalId !== undefined && node.catchLocalId !== null) { + walker.string(node.catchLocalId); + } + if (node.finallyBody !== undefined && node.finallyBody !== null) { + for (const item45 of node.finallyBody) { + walkStmtFields(item45, walker, skipType); + } + } + return; + } + case "varDecl": { + walker.string(node.kind); + walker.string(node.localId); + if (node.init !== undefined && node.init !== null) { + walkExprFields(node.init, walker, skipType); + } + return; + } + case "while": { + walker.string(node.kind); + walkExprFields(node.cond, walker, skipType); + for (const item46 of node.body) { + walkStmtFields(item46, walker, skipType); + } + if (node.labels !== undefined && node.labels !== null) { + for (const item47 of node.labels) { + walker.string(item47); + } + } + return; + } + } +} + +/** Walks every field of `node` but `loc` (and `type` with `skipType`), and + * every field of each object under it: the strings and nodes a recursive + * `for (const key in value)` walk with the same skips finds, read by name in + * the order ir.ts declares the fields. */ +export function walkTypeFields(node: IrType, walker: IrFieldWalker, skipType: boolean): void { + switch (node.kind) { + case "array": { + walker.string(node.kind); + walkTypeFields(node.elem, walker, skipType); + return; + } + case "bigint": { + walker.string(node.kind); + return; + } + case "bool": { + walker.string(node.kind); + return; + } + case "bytes": { + walker.string(node.kind); + walker.string(node.elem); + return; + } + case "caught": { + walker.string(node.kind); + return; + } + case "child": { + walker.string(node.kind); + return; + } + case "childStream": { + walker.string(node.kind); + return; + } + case "childWriter": { + walker.string(node.kind); + return; + } + case "classval": { + walker.string(node.kind); + walker.string(node.className); + return; + } + case "cryptoHash": { + walker.string(node.kind); + return; + } + case "cryptoHmac": { + walker.string(node.kind); + return; + } + case "date": { + walker.string(node.kind); + return; + } + case "dgramSocket": { + walker.string(node.kind); + return; + } + case "dir": { + walker.string(node.kind); + return; + } + case "dyn": { + walker.string(node.kind); + return; + } + case "f64": { + walker.string(node.kind); + return; + } + case "fileHandle": { + walker.string(node.kind); + return; + } + case "fsWatcher": { + walker.string(node.kind); + return; + } + case "func": { + walker.string(node.kind); + for (const item48 of node.params) { + walkTypeFields(item48, walker, skipType); + } + walkTypeFields(node.ret, walker, skipType); + if (node.restAbi !== undefined && node.restAbi !== null) { + walker.string(node.restAbi); + } + return; + } + case "generator": { + walker.string(node.kind); + walkTypeFields(node.yieldT, walker, skipType); + walkTypeFields(node.retT, walker, skipType); + walkTypeFields(node.nextT, walker, skipType); + return; + } + case "http2Session": { + walker.string(node.kind); + return; + } + case "http2Stream": { + walker.string(node.kind); + return; + } + case "httpClientReq": { + walker.string(node.kind); + return; + } + case "httpReq": { + walker.string(node.kind); + return; + } + case "httpRes": { + walker.string(node.kind); + return; + } + case "jsval": { + walker.string(node.kind); + return; + } + case "map": { + walker.string(node.kind); + walkTypeFields(node.key, walker, skipType); + walkTypeFields(node.value, walker, skipType); + return; + } + case "moduleNs": { + walker.string(node.kind); + walker.string(node.moduleId); + return; + } + case "netServer": { + walker.string(node.kind); + return; + } + case "netSocket": { + walker.string(node.kind); + return; + } + case "nullT": { + walker.string(node.kind); + return; + } + case "object": { + walker.string(node.kind); + walker.string(node.className); + return; + } + case "procStream": { + walker.string(node.kind); + return; + } + case "promise": { + walker.string(node.kind); + walkTypeFields(node.inner, walker, skipType); + return; + } + case "record": { + walker.string(node.kind); + walker.string(node.shapeId); + return; + } + case "regex": { + walker.string(node.kind); + return; + } + case "searchParams": { + walker.string(node.kind); + return; + } + case "secureCtx": { + walker.string(node.kind); + return; + } + case "set": { + walker.string(node.kind); + walkTypeFields(node.elem, walker, skipType); + return; + } + case "spawnRes": { + walker.string(node.kind); + return; + } + case "stats": { + walker.string(node.kind); + return; + } + case "string": { + walker.string(node.kind); + return; + } + case "symbol": { + walker.string(node.kind); + return; + } + case "testCtx": { + walker.string(node.kind); + return; + } + case "undefinedT": { + walker.string(node.kind); + return; + } + case "union": { + walker.string(node.kind); + walker.string(node.unionId); + return; + } + case "url": { + walker.string(node.kind); + return; + } + case "void": { + walker.string(node.kind); + return; + } + } +} diff --git a/packages/compiler/src/ir/fields.test.ts b/packages/compiler/src/ir/fields.test.ts new file mode 100644 index 000000000..8d1aaa433 --- /dev/null +++ b/packages/compiler/src/ir/fields.test.ts @@ -0,0 +1,111 @@ +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../index.js"; +import type { IrExpr, IrStmt } from "./ir.js"; +import { everyStmtList } from "./traverse.js"; +import { eachStringField, walkStmtFields } from "./fields.generated.js"; + +// Programs with many node kinds: classes, closures, async, generators, +// dynamic values, streams and records. +const PROGRAMS = [ + "1003-json-parse-unions.ts", + "114-live-record-delete.ts", + "1200-regex-test-basics.ts", + "1631-inspect-arrays.ts", + "4900-console-instances.ts", + "5612-readable-from-iterables.cjs", + "7110-als-completion-callbacks.ts", + "7321-speculative-loop-bail-sites.ts", +]; + +/** The string fields a key walk of `node` finds, as `field=value`. */ +function keyWalk(node: IrExpr | IrStmt): string[] { + const found: string[] = []; + for (const key in node) { + const value = (node as unknown as Record)[key]; + if (typeof value === "string") found.push(`${key}=${value}`); + } + return found.sort(); +} + +/** The events of a recursive key walk of `value` that skips `loc` (and + * `type` with `skipType`): `n:` for an IR node (an object with a kind + * and a location; `n:varDecl:` for a declaration), `s:` for + * each string. */ +function keyWalkDeep(value: unknown, events: string[], skipType: boolean): void { + if (typeof value === "string") events.push(`s:${value}`); + else if (Array.isArray(value)) for (const item of value) keyWalkDeep(item, events, skipType); + else if (value !== null && typeof value === "object") { + const record = value as Record; + if ("kind" in record && "loc" in record) events.push(nodeEvent(record as unknown as IrExpr)); + for (const key in record) + if (key !== "loc" && !(skipType && key === "type")) + keyWalkDeep(record[key], events, skipType); + } +} + +function nodeEvent(node: IrExpr | IrStmt): string { + return node.kind === "varDecl" ? `n:varDecl:${node.localId}` : `n:${node.kind}`; +} + +async function lowered(program: string, dir: string) { + const outPath = join(dir, "main.ir.json"); + const entry = join(import.meta.dirname, "../../../../tests/corpus", program); + 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")); +} + +// A key walk visits a node's fields in the order the lowering created them, +// which can differ from ir.ts's order between sibling fields (a dynKeyGet is +// built key first). The walk's callers count what they find, except that +// declarations must come in the same order. +test("walkStmtFields reports what a recursive key walk finds", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-ir-walk-")); + try { + for (const program of PROGRAMS) { + const mod = await lowered(program, dir); + for (const skipType of [false, true]) + for (const fn of mod.functions) { + const expected: string[] = []; + keyWalkDeep(fn.body, expected, skipType); + const events: string[] = []; + const walker = { + node: (node: IrExpr | IrStmt) => events.push(nodeEvent(node)), + string: (value: string) => events.push(`s:${value}`), + }; + for (const stmt of fn.body) walkStmtFields(stmt, walker, skipType); + const where = `${program}: ${fn.name}, skipType ${skipType}`; + expect([...events].sort(), where).toEqual([...expected].sort()); + const declarations = (list: string[]) => list.filter((e) => e.startsWith("n:varDecl:")); + expect(declarations(events), where).toEqual(declarations(expected)); + } + } + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 300_000); + +test("eachStringField reads every string field a key walk of a node finds", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-ir-fields-")); + try { + for (const program of PROGRAMS) { + const mod = await lowered(program, dir); + let nodes = 0; + const check = (node: IrExpr | IrStmt): boolean => { + const read: string[] = []; + eachStringField(node, (value, field) => read.push(`${field}=${value}`)); + expect(read.sort(), `${program}: ${node.kind}`).toEqual(keyWalk(node)); + nodes++; + return true; + }; + for (const fn of mod.functions) everyStmtList(fn.body, { expr: check, stmt: check }); + expect(nodes, program).toBeGreaterThan(0); + } + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 300_000); diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index 375fefc94..48fa8a841 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -100,6 +100,7 @@ const EXPR_FIELDS: Classified = { unionIsTag: { unionId: "other" }, unionEq: { unionId: "other" }, jsonStringify: { indent: "other" }, + dynCheck: { inferredSite: "other" }, }; const STMT_FIELDS: Classified = { @@ -233,6 +234,8 @@ const MODULE_FIELDS: Record>, Role> = { "nodeIsland.modules.files[].sha256": "other", "nodeIsland.modules.links[].path": "other", "nodeIsland.modules.links[].target": "other", + "inferredFactFiles[].file": "other", + "inferredFactFiles[].pkg": "other", "declOrigins[]": "other", }; diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 70cc151a0..4663e2736 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1075,6 +1075,11 @@ export interface IrModule { * closures boxed for package code return records and arrays as live * capsules, so a value that comes back keeps its identity. */ declBoundary?: true; + /** Package files compiled from shape inference's typed overlay + * (frontend/shape-inference/overlay.ts), each with its package name: a + * checked conversion in them checks an analysis-proven fact, so its + * failure reports a compiler analysis bug instead of a type error. */ + inferredFactFiles?: { file: string; pkg: string }[]; /** Origins of package-declared types checked by `dynCheck.origin` * (frontend/lowering/decl-origin.ts): fields separated by \x1f — * package, version, suggested range, selector, where it is declared, @@ -4090,6 +4095,10 @@ export type IrExpr = preserveRefs?: true; adopt?: true; exact?: true; + /** Shape inference's overlay: this conversion checks an + * analysis-proven fact (" :"); a failure is a + * compiler analysis bug and reports itself as one. */ + inferredSite?: string; origin?: number; type: IrType; loc: SrcLoc; diff --git a/packages/compiler/src/ir/paired-while-loops.ts b/packages/compiler/src/ir/paired-while-loops.ts index c096c34fe..030b255f2 100644 --- a/packages/compiler/src/ir/paired-while-loops.ts +++ b/packages/compiler/src/ir/paired-while-loops.ts @@ -6,8 +6,10 @@ import { type IrFunction, type IrLocal, type IrModule, + type IrParam, type IrStmt, } from "./ir.js"; +import { type IrFieldWalker, walkStmtFields } from "./fields.generated.js"; import { everyExprChild, everyStmtChild, mapExprChildren, mapStmtChildren } from "./traverse.js"; type WhileLoop = IrStmt & { kind: "while" }; @@ -99,22 +101,20 @@ function continuesLoop(loop: WhileLoop): boolean { return found; } -/** Mentions of each id in `node`, in any field (a string literal equal to - * an id counts too, which only refuses more loops). */ +/** Mentions of each id in `stmts`, in any field but types (a string + * literal equal to an id counts too, which only refuses more loops). */ function countMentions( - node: unknown, + stmts: readonly IrStmt[], ids: ReadonlySet, mentions: Map, ): void { - if (typeof node === "string") { - if (ids.has(node)) mentions.set(node, (mentions.get(node) ?? 0) + 1); - } else if (Array.isArray(node)) { - for (const item of node) countMentions(item, ids, mentions); - } else if (node !== null && typeof node === "object") { - const record = node as Record; - for (const key in record) - if (key !== "loc" && key !== "type") countMentions(record[key], ids, mentions); - } + const walker: IrFieldWalker = { + node: () => {}, + string: (value) => { + if (ids.has(value)) mentions.set(value, (mentions.get(value) ?? 0) + 1); + }, + }; + for (const stmt of stmts) walkStmtFields(stmt, walker, true); } /** A loop with the qualifying shape, the statements before it in its @@ -184,7 +184,12 @@ function rewriteFunction(fn: IrFunction): IrFunction { const params = new Set(fn.params.map((p) => p.localId)); // A counter this function reaches through its environment. const outer = new Map(); - countMentions([fn.captures ?? [], fn.classCaptures ?? []], ids, outer); + const noteOuter = (param: IrParam): void => { + for (const value of [param.localId, param.name]) + if (ids.has(value)) outer.set(value, (outer.get(value) ?? 0) + 1); + }; + for (const param of fn.captures ?? []) noteOuter(param); + for (const param of fn.classCaptures ?? []) noteOuter(param); let ranges: ReadonlyMap | null = null; const used = new Set(fn.locals.map((l) => l.id)); @@ -192,7 +197,7 @@ function rewriteFunction(fn: IrFunction): IrFunction { const rewritten = new Map>(); for (const { loop, counters, before, nested } of candidates) { const inside = new Map(); - countMentions(loop, new Set(counters), inside); + countMentions([loop], new Set(counters), inside); // Each counter's start: its declaration's initializer, or null for a // parameter. const starts = new Map(); diff --git a/packages/compiler/src/native/toolchain.test.ts b/packages/compiler/src/native/toolchain.test.ts index c64bce76b..0acd4a4f5 100644 --- a/packages/compiler/src/native/toolchain.test.ts +++ b/packages/compiler/src/native/toolchain.test.ts @@ -276,3 +276,66 @@ test("a manifest without tsc_ts takes the host's tsc-ts executable from the inst rmSync(root, { recursive: true, force: true }); } }); + +test.each(["nested", "hoisted", "pnpm"])( + "an npm platform package resolves its LLVM helper and runtime pack dependencies (%s)", + (layout) => { + const root = realpathSync( + mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-dependencies-")), + ); + const modules = + layout === "pnpm" + ? join(root, "node_modules/.pnpm/@scriptc+cli-linux-x64-gnu@1.2.3/node_modules") + : join(root, "node_modules"); + const platform = join(modules, "@scriptc/cli-linux-x64-gnu"); + const dependencies = layout === "nested" ? join(platform, "node_modules") : modules; + const bin = join(platform, "dist/bin"); + const path = join(bin, "scriptc.json"); + const manifest: NativeToolchainManifest = { + schema: "scriptc.native-toolchain.v1", + compiler_version: "1.2.3", + target: "linux-x64-gnu", + ts7: "../lib/typescript/tsc", + tsc_ts: "../lib/tsc-ts/lib/tsc-ts", + linker: "clang", + linker_args: [], + dsymutil: "dsymutil", + }; + const install = (name: string, version = "1.2.3") => { + mkdirSync(join(dependencies, name), { recursive: true }); + writeFileSync(join(dependencies, name, "package.json"), JSON.stringify({ name, version })); + }; + try { + mkdirSync(bin, { recursive: true }); + writeFileSync(path, JSON.stringify(manifest)); + expect(() => loadNativeToolchain(path)).toThrow( + "scriptc requires @scriptc/llvm-linux-x64-gnu@1.2.3, which is not installed beside the native compiler", + ); + install("@scriptc/llvm-linux-x64-gnu"); + install("@scriptc/runtime-linux-x64-gnu", "1.2.2"); + expect(() => loadNativeToolchain(path)).toThrow( + "scriptc requires @scriptc/runtime-linux-x64-gnu@1.2.3, found 1.2.2", + ); + install("@scriptc/runtime-linux-x64-gnu"); + const toolchain = loadNativeToolchain(path); + expect(toolchain.helperPackageRoot).toBe(join(dependencies, "@scriptc/llvm-linux-x64-gnu")); + expect(toolchain.helperExecutable).toBe( + join(dependencies, "@scriptc/llvm-linux-x64-gnu/bin/scriptc-llvm-codegen"), + ); + expect(toolchain.runtimePackRoot).toBe(join(dependencies, "@scriptc/runtime-linux-x64-gnu")); + // Other targets' packs still come from the project. + const project = join(root, "project"); + const wasi = join(project, "node_modules/@scriptc/runtime-wasm32-wasi"); + mkdirSync(wasi, { recursive: true }); + writeFileSync(join(wasi, "package.json"), "{}"); + expect( + loadNativeToolchain(path, { SCRIPTC_TARGET: "wasm32-wasi" }, project).runtimePackRoot, + ).toBe(wasi); + expect( + loadNativeToolchain(path, { SCRIPTC_RUNTIME_PACK: "../explicit" }).runtimePackRoot, + ).toBe(join(platform, "dist/explicit")); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }, +); diff --git a/packages/compiler/src/native/toolchain.ts b/packages/compiler/src/native/toolchain.ts index c9167ba14..d93cce1af 100644 --- a/packages/compiler/src/native/toolchain.ts +++ b/packages/compiler/src/native/toolchain.ts @@ -44,8 +44,12 @@ export interface NativeToolchainManifest { 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; + /** The LLVM helper package; without it, the host's helper package + * installed beside the compiler (an npm platform package's dependency). */ + llvm_package?: string; + /** The host runtime pack; without it, the host's runtime pack package + * installed beside the compiler. */ + runtime_pack?: string; runtime_sources?: string; linker: string; linker_args: string[]; @@ -69,9 +73,31 @@ function commandFrom(root: string, value: string): string { return value.includes("/") || value.includes("\\") ? pathFrom(root, value) : value; } +/** A package the npm platform package depends on, as Node resolves it from + * the compiler's directory: nested in the platform package, beside it + * (hoisted, or pnpm's virtual store), or higher up. The version must be the + * compiler's. */ +function compilerDependency(root: string, packageName: string, version: string): string { + const directory = installedPackage(root, packageName); + if (directory === undefined) + throw new Error( + `scriptc requires ${packageName}@${version}, which is not installed beside the native compiler; reinstall scriptc`, + ); + const found = ( + JSON.parse(readFileSync(join(directory, "package.json"), "utf8")) as { + version?: unknown; + } + ).version; + if (found !== version) + throw new Error( + `scriptc requires ${packageName}@${version}, found ${String(found)}; reinstall scriptc`, + ); + return directory; +} + /** Additional targets are ordinary project dependencies. Resolve them when * requested so adding a pack never requires reinstalling the compiler. */ -function installedRuntimePack(start: string, packageName: string): string | undefined { +function installedPackage(start: string, packageName: string): string | undefined { let directory = resolve(start); for (;;) { const candidate = join(directory, "node_modules", packageName); @@ -96,15 +122,7 @@ export function loadNativeToolchain( const value = raw as Record; if (value.schema !== "scriptc.native-toolchain.v1") throw new Error("unsupported native toolchain manifest schema"); - for (const name of [ - "compiler_version", - "target", - "ts7", - "llvm_package", - "runtime_pack", - "linker", - "dsymutil", - ]) { + for (const name of ["compiler_version", "target", "ts7", "linker", "dsymutil"]) { if (typeof value[name] !== "string" || value[name] === "") throw new Error(`native toolchain requires ${name}`); } @@ -119,6 +137,8 @@ export function loadNativeToolchain( const manifest = raw as NativeToolchainManifest; for (const name of [ "tsc_ts", + "llvm_package", + "runtime_pack", "runtime_sources", "declarations", "archiver", @@ -157,21 +177,35 @@ export function loadNativeToolchain( if (target === null) throw new Error(`unsupported native compiler target: ${env["SCRIPTC_TARGET"]}`); const root = dirname(manifestPath); - const helperPackageRoot = pathFrom(root, env["SCRIPTC_LLVM_PACKAGE"] ?? manifest.llvm_package); + const llvmPackage = + env["SCRIPTC_LLVM_PACKAGE"] ?? + manifest.llvm_package ?? + compilerDependency(root, host.helper.packageName, manifest.compiler_version); + const helperPackageRoot = pathFrom(root, llvmPackage); + // The host pack of an npm platform package is its dependency; a + // distribution archive bundles it beside the compiler. + const hostRuntimePack = + manifest.runtime_pack ?? + (target.name === host.name && env["SCRIPTC_RUNTIME_PACK"] === undefined + ? compilerDependency(root, host.runtimePackPackage, manifest.compiler_version) + : undefined); let runtimePath = manifest.runtime_packs?.find((pack) => pack.target === target.name)?.path ?? (target.name === host.name - ? manifest.runtime_pack - : join(dirname(manifest.runtime_pack), target.runtimePackPackage.replace("@scriptc/", ""))); + ? hostRuntimePack + : hostRuntimePack === undefined + ? undefined + : join(dirname(hostRuntimePack), target.runtimePackPackage.replace("@scriptc/", ""))); if ( target.name !== host.name && env["SCRIPTC_RUNTIME_PACK"] === undefined && - !existsSync(pathFrom(root, runtimePath)) + (runtimePath === undefined || !existsSync(pathFrom(root, runtimePath))) ) { runtimePath = - installedRuntimePack(cwd, target.runtimePackPackage) ?? - installedRuntimePack(root, target.runtimePackPackage) ?? - runtimePath; + installedPackage(cwd, target.runtimePackPackage) ?? + installedPackage(root, target.runtimePackPackage) ?? + runtimePath ?? + join(root, "node_modules", target.runtimePackPackage); } const hostLinker = target.name === host.name && target.defaultLinker === host.defaultLinker; const linker = env["SCRIPTC_LINKER"] ?? (hostLinker ? manifest.linker : target.defaultLinker); @@ -206,7 +240,7 @@ export function loadNativeToolchain( "bin", host.platform === "win32" ? "scriptc-llvm-codegen.exe" : "scriptc-llvm-codegen", ), - runtimePackRoot: pathFrom(root, env["SCRIPTC_RUNTIME_PACK"] ?? runtimePath), + runtimePackRoot: pathFrom(root, env["SCRIPTC_RUNTIME_PACK"] ?? runtimePath!), ...(manifest.runtime_sources === undefined ? {} : { runtimeSourceRoot: pathFrom(root, manifest.runtime_sources) }), diff --git a/packages/compiler/src/project-config.test.ts b/packages/compiler/src/project-config.test.ts index f12a8ea89..4a479b597 100644 --- a/packages/compiler/src/project-config.test.ts +++ b/packages/compiler/src/project-config.test.ts @@ -70,6 +70,7 @@ test("package settings: strings and objects", () => { expect(c.packages.get("semver")?.run).toBe("auto"); expect(c.packages.get("proper-lockfile")).toEqual({ unloaded: false, + shapes: true, run: "node", evaluate: "lazy", share: "duplicate", @@ -78,6 +79,24 @@ test("package settings: strings and objects", () => { }); }); +test("shapes: false turns off the shape overlay for one package", () => { + const c = config({ + packages: { + zod: { shapes: false }, + effect: { shapes: true, run: "native" }, + ms: { shapes: "no" }, + }, + }); + expect(c.packages.get("zod")?.shapes).toBe(false); + expect(c.packages.get("zod")?.run).toBe("auto"); + expect(c.packages.get("effect")?.shapes).toBe(true); + expect(c.packages.get("effect")?.run).toBe("native"); + expect(c.packages.has("ms")).toBe(false); + expect(c.diagnostics.map((d) => d.message)).toEqual([ + `package.json 'scriptc.packages.ms.shapes' must be true or false, not "no"`, + ]); +}); + test("nodeIslands settings", () => { const c = config({ nodeIslands: { diff --git a/packages/compiler/src/project-config.ts b/packages/compiler/src/project-config.ts index b87b67838..b4a7ac6fa 100644 --- a/packages/compiler/src/project-config.ts +++ b/packages/compiler/src/project-config.ts @@ -10,7 +10,8 @@ * "encoding": "unloaded", // never loaded (package-shims.ts) * "dd-trace": "node", // run in a Node island * "hot-shots": "native", // must compile natively (SC8003) - * "proper-lockfile": { "run": "node", "evaluate": "lazy" } + * "proper-lockfile": { "run": "node", "evaluate": "lazy" }, + * "zod": { "shapes": false } // no shape-inference overlay * }, * "nodeIslands": { "auto": true, "modules": ["src/plugins/*.ts"] }, * "wasm": ["build/module.wasm"], @@ -40,9 +41,12 @@ export type PackageMarshal = "copy" | "handles"; export type PackageSchedule = "lockstep" | "free"; /** One `scriptc.packages.` entry. `unloaded` packages never load; - * the other settings choose where the package runs. */ + * `shapes: false` compiles the package without shape inference's typed + * overlay (shape-inference/overlay.ts); the other settings choose where the + * package runs. */ export interface PackageSetting { unloaded: boolean; + shapes: boolean; run: PackageRun; evaluate: PackageEvaluate; share: PackageShare; @@ -117,6 +121,9 @@ const PACKAGE_OBJECT_KEYS: Record = { marshal: ["copy", "handles"], schedule: ["lockstep", "free"], }; +/** Every key of a package setting object: the string-valued ones above and + * the boolean `shapes`. */ +const PACKAGE_SETTING_KEYS = [...Object.keys(PACKAGE_OBJECT_KEYS), "shapes"]; const NODE_ISLANDS_KEYS = [ "auto", "modules", @@ -129,6 +136,7 @@ const NODE_ISLANDS_KEYS = [ function defaultPackageSetting(): PackageSetting { return { unloaded: false, + shapes: true, run: "auto", evaluate: "auto", share: "auto", @@ -223,7 +231,7 @@ export function parseProjectConfig(manifest: string, text: string): ProjectConfi problem( ["packages", name], `has the unknown value ${shown(value)}`, - `use one of ${quoted(PACKAGE_STRINGS)}, or an object with ${Object.keys(PACKAGE_OBJECT_KEYS).join(", ")}`, + `use one of ${quoted(PACKAGE_STRINGS)}, or an object with ${PACKAGE_SETTING_KEYS.join(", ")}`, ); continue; } @@ -236,12 +244,24 @@ export function parseProjectConfig(manifest: string, text: string): ProjectConfi problem( ["packages", name], `must be a string or an object, not ${shown(value)}`, - `use one of ${quoted(PACKAGE_STRINGS)}, or an object with ${Object.keys(PACKAGE_OBJECT_KEYS).join(", ")}`, + `use one of ${quoted(PACKAGE_STRINGS)}, or an object with ${PACKAGE_SETTING_KEYS.join(", ")}`, ); continue; } let valid = true; for (const key of Object.keys(value)) { + if (key === "shapes") { + const v = value[key]; + if (typeof v !== "boolean") { + problem( + ["packages", name, key], + `must be true or false, not ${shown(v)}`, + "use a boolean", + ); + valid = false; + } else setting.shapes = v; + continue; + } const allowed = Object.hasOwn(PACKAGE_OBJECT_KEYS, key) ? PACKAGE_OBJECT_KEYS[key] : undefined; @@ -249,7 +269,7 @@ export function parseProjectConfig(manifest: string, text: string): ProjectConfi problem( ["packages", name, key], "is not a known package setting", - `the settings are ${Object.keys(PACKAGE_OBJECT_KEYS).join(", ")}`, + `the settings are ${PACKAGE_SETTING_KEYS.join(", ")}`, ); valid = false; continue; diff --git a/packages/compiler/test/canon-normalize.test.ts b/packages/compiler/test/canon-normalize.test.ts index ac25924f3..365818a5a 100644 --- a/packages/compiler/test/canon-normalize.test.ts +++ b/packages/compiler/test/canon-normalize.test.ts @@ -3,6 +3,14 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { canonEqual, canonKey, Normalizer, type Canon } from "../src/frontend/canon.js"; +import { mapCanonical } from "../src/frontend/canon-map.js"; +import { + mapType, + ShapeRegistry, + UnionRegistry, + type TypeMapperCtx, +} from "../src/frontend/type-mapper.js"; +import { typeKey } from "../src/ir/ir.js"; import { loadProgram } from "../src/frontend/program-node.js"; import * as ts from "../src/frontend/ts7/adapter.js"; @@ -65,6 +73,10 @@ beforeAll(() => { export type Loose = [string, number?, ...boolean[]]; // Arrays of two instantiations of one generic class. export type Maps = Map[] | Map[]; + // The same instantiations, each refined by an object type. + export type RefinedMaps = + | (Map & { tag: number })[] + | (Map & { tag: number })[]; `, ); load = loadProgram(entry); @@ -259,6 +271,12 @@ describe("canon", () => { expect(key(maps)).toBe("Map[] | Map[]"); }); + test("a refined instantiation keeps its type arguments", () => { + const maps = new Normalizer(checker).canon(aliases.get("RefinedMaps")!); + expect(maps.k === "union" && maps.arms.length).toBe(2); + expect(key(maps)).toBe("Map[] | Map[]"); + }); + test("optional and rest tuple elements keep their flags", () => { expect(key(new Normalizer(checker).canon(aliases.get("Loose")!))).toBe( "[string, number | undefined?, ...boolean]", @@ -271,3 +289,54 @@ describe("canon", () => { expect(key(n.canon(aliases.get("Pair")!))).toBe("[string, number]"); }); }); + +describe("mapCanonical", () => { + const ctx = (): TypeMapperCtx => ({ + checker, + shapes: new ShapeRegistry(), + unions: new UnionRegistry(), + classNamer: (decl) => decl.name?.text ?? "anonymous", + isStdlibFile: (sf) => load.program.isSourceFileDefaultLibrary(sf), + isNpmFile: () => false, + isExternalTypeFile: () => false, + isProgramFile: (sf) => load.moduleOrder.includes(sf), + }); + test.for(["Normalized", "Same", "Flat", "Id", "Pair"])("%s maps like mapType", (name) => { + const strict = mapType(aliases.get(name)!, ctx()); + const canonical = mapCanonical(new Normalizer(checker).canon(aliases.get(name)!), ctx()); + expect(strict).not.toBeNull(); + expect(canonical && typeKey(canonical)).toBe(typeKey(strict!)); + }); + test("shadow mode observes every successful top-level mapping", () => { + const seen: string[] = []; + const point = { kind: "object", className: "Point" } as const; + const observed: TypeMapperCtx = { + ...ctx(), + typeMemo: new Map(), + localClassValue: (type) => (type === aliases.get("Points") ? point : null), + canonShadow: (type, mapped) => seen.push(`${checker.typeToString(type)} ${typeKey(mapped)}`), + }; + // An intrinsic primitive, a mapping and its memo hit, and a local class + // value: one observation each; the nested mappings of `Same` are not + // observed. + const number = mapType(valueTypes.get("sampleNumber")!, observed); + const same = mapType(aliases.get("Same")!, observed); + expect(mapType(aliases.get("Same")!, observed)).toBe(same); + expect(mapType(aliases.get("Points")!, observed)).toBe(point); + expect(seen).toEqual([ + `number ${typeKey(number!)}`, + `Same ${typeKey(same!)}`, + `Same ${typeKey(same!)}`, + `Points ${typeKey(point)}`, + ]); + }); + test("a generic alias maps under bindings, where mapType needs the instance table", () => { + const { type, params } = asTypes.get("Catalog")!; + const env = new Map([ + [params[0]!, valueTypes.get("sampleNumber")!], + [params[1]!, valueTypes.get("sampleText")!], + ]); + const canonical = mapCanonical(new Normalizer(checker).canon(type, env), ctx()); + expect(canonical?.kind).toBe("record"); + }); +}); diff --git a/packages/compiler/test/fixtures/ts7-protocols.json b/packages/compiler/test/fixtures/ts7-protocols.json new file mode 100644 index 000000000..e60dd5b8d --- /dev/null +++ b/packages/compiler/test/fixtures/ts7-protocols.json @@ -0,0 +1,18 @@ +{ + "//": "getSourceFile responses for the same sources from tsc 7.0.2 (protocol 5) and tsc 7.1.0-beta (protocol 9), base64, project at /project", + "sources": { + "main.ts": "/// \nimport type { Shape } from \"./other.js\";\nimport defer * as lazy from \"./other.js\";\nimport * as other from \"./other.js\";\nexport { other, lazy };\n/** A box.\n * @template T\n */\nexport abstract class Box extends other.Base {\n #secret = \"\\ud800\ufeff\";\n abstract method(...values: U[],): U;\n get value() { return this.#secret; }\n static { console.log(\"ready\"); }\n}\nexport namespace Space.Inner { export const n = 1; }\ndeclare module \"ambient\" { export const a: number; }\nexport type Values = readonly [name: string, count?: number, ...rest: boolean[]];\nexport type Keys = { readonly [K in keyof T as `p${K & string}`]?: T[K] };\nexport enum Color { Red, Green = 3, }\nexport function call(f: (a: number, b: string,) => void, ...args: [number, string]): void {\n f(...args,);\n const tagged = String.raw`a${args[0]}b\\n`;\n label: for (const x of [1, 2, 3,]) { if (x > 1) break label; }\n void (async () => { for await (const y of [] as AsyncIterable) y satisfies number; })();\n const o = { a: 1, b, ...{ c: 2 }, [Symbol.iterator]() { return [][Symbol.iterator](); }, };\n try { throw new Error(`x${o.a}`); } catch { } finally { }\n switch (o.a) { case 1: break; default: }\n let b = 0n ** 2n; b ??= 1n; b++;\n}\n", + "other.ts": "export interface Shape { kind: \"circle\" }\nexport class Base { }\n", + "view.tsx": "export const view = (items: string[]) =>
    {items.map((item) =>
  • {item}
  • )}<>
;\n" + }, + "protocol5": { + "main.ts": "AAAABY6wdLJEh2ZzyMQ0XA6mzLQAAAAALAAAAIwDAACpCAAAuQkAANAJAAAAAAAA6QQAAOkEAAD5BAAA+QQAAAkFAAAyAAAANwAAAEAAAABKAAAAXwAAAGMAAABqAAAAdAAAAIMAAACIAAAAjwAAAJkAAAClAAAAqgAAAKwAAACwAAAA6AAAAOsAAADsAAAA7QAAAPcAAAD8AAAA/QAAAAEBAAAGAQAADQEAAAkFAAAPBQAAKAEAAC4BAAAvAQAAMAEAADUBAAA7AQAAPQEAAD4BAABEAQAARQEAAE0BAABSAQAAYwEAAGoBAAB5AQAAgAEAAIEBAACEAQAAhgEAAIsBAAAPBQAAFQUAAMMAAADLAAAAzAAAAM0AAACkAQAAqQEAAKoBAACvAQAAvwEAAMABAADDAQAAxAEAANgBAADfAQAA8AEAAPEBAAAJAgAADwIAABwCAAAgAgAAKgIAAC8CAAA9AgAAQQIAAFsCAABfAgAAYAIAAGECAABxAgAAcgIAAHwCAAB9AgAAFQUAABYFAAAWBQAAFwUAAIUCAACGAgAAkAIAAJACAACQAgAAkAIAAJUCAACWAgAAlwIAAJgCAACpAgAArgIAALECAAC0AgAAtgIAALsCAAC+AgAAvwIAANMCAADXAgAA2AIAANkCAADcAgAA3QIAAOcCAADoAgAA/wIAAAMDAAAhAwAAIgMAACYDAAAqAwAANgMAADwDAAA/AwAARQMAAEYDAABJAwAAFwUAABgFAAAYBQAAGQUAAE0DAABRAwAAUgMAAFMDAAAZBQAAGwUAAFUDAABYAwAAXQMAAGIDAABvAwAAcAMAAHUDAAB2AwAAeAMAAHkDAAB7AwAAfAMAAIYDAACHAwAAigMAAIsDAACTAwAAmAMAAMMDAADEAwAAzgMAANsDAADlAwAA5gMAAAcEAAAIBAAADQQAAA4EAAAQBAAAEQQAABMEAAAUBAAAGwQAABwEAAAeBAAAHwQAACQEAAAqBAAAKwQAADMEAABDBAAASQQAAEoEAABSBAAAbwQAAHQEAAAbBQAAHAUAABwFAAAdBQAAeQQAAHoEAAB7BAAAfAQAAH0EAAB9BAAAfQQAAH0EAACjBAAApAQAAKUEAACmBAAArwQAALAEAADKBAAAywQAAM4EAADQBAAA1AQAANYEAADYBAAA2QQAAN4EAADgBAAA4gQAAOMEAAAvLy8gPHJlZmVyZW5jZSBwYXRoPSIuL290aGVyLnRzIiAvPgppbXBvcnQgdHlwZSB7IFNoYXBlIH0gZnJvbSAiLi9vdGhlci5qcyI7CmltcG9ydCBkZWZlciAqIGFzIGxhenkgZnJvbSAiLi9vdGhlci5qcyI7CmltcG9ydCAqIGFzIG90aGVyIGZyb20gIi4vb3RoZXIuanMiOwpleHBvcnQgeyBvdGhlciwgbGF6eSB9OwovKiogQSBib3guCiAqIEB0ZW1wbGF0ZSBUCiAqLwpleHBvcnQgYWJzdHJhY3QgY2xhc3MgQm94PFQ+IGV4dGVuZHMgb3RoZXIuQmFzZSB7CiAgI3NlY3JldCA9ICJcdWQ4MDDvu78iOwogIGFic3RyYWN0IG1ldGhvZDxVPiguLi52YWx1ZXM6IFVbXSwpOiBVOwogIGdldCB2YWx1ZSgpIHsgcmV0dXJuIHRoaXMuI3NlY3JldDsgfQogIHN0YXRpYyB7IGNvbnNvbGUubG9nKCJyZWFkeSIpOyB9Cn0KZXhwb3J0IG5hbWVzcGFjZSBTcGFjZS5Jbm5lciB7IGV4cG9ydCBjb25zdCBuID0gMTsgfQpkZWNsYXJlIG1vZHVsZSAiYW1iaWVudCIgeyBleHBvcnQgY29uc3QgYTogbnVtYmVyOyB9CmV4cG9ydCB0eXBlIFZhbHVlcyA9IHJlYWRvbmx5IFtuYW1lOiBzdHJpbmcsIGNvdW50PzogbnVtYmVyLCAuLi5yZXN0OiBib29sZWFuW11dOwpleHBvcnQgdHlwZSBLZXlzPFQ+ID0geyByZWFkb25seSBbSyBpbiBrZXlvZiBUIGFzIGBwJHtLICYgc3RyaW5nfWBdPzogVFtLXSB9OwpleHBvcnQgZW51bSBDb2xvciB7IFJlZCwgR3JlZW4gPSAzLCB9CmV4cG9ydCBmdW5jdGlvbiBjYWxsKGY6IChhOiBudW1iZXIsIGI6IHN0cmluZywpID0+IHZvaWQsIC4uLmFyZ3M6IFtudW1iZXIsIHN0cmluZ10pOiB2b2lkIHsKICBmKC4uLmFyZ3MsKTsKICBjb25zdCB0YWdnZWQgPSBTdHJpbmcucmF3YGEke2FyZ3NbMF19YlxuYDsKICBsYWJlbDogZm9yIChjb25zdCB4IG9mIFsxLCAyLCAz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", + "other.ts": "AAAABQnH2k8H5bbOVZ0OdlANqCcAAAAALAAAAGQAAADGAAAA/gAAAP4AAAAAAAAAQAAAAEAAAABRAAAAUQAAAGIAAAARAAAAFgAAABkAAAAdAAAAIAAAACYAAAA3AAAAOwAAAGV4cG9ydCBpbnRlcmZhY2UgU2hhcGUgeyBraW5kOiAiY2lyY2xlIiB9CmV4cG9ydCBjbGFzcyBCYXNlIHsgfQovcHJvamVjdC9vdGhlci50cy9wcm9qZWN0L290aGVyLnRzAAAAAAIAAAAEAAAAAAAAAAMAAAD///////////////////////////////8DAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwEAAAAAAABAAAAAAAAAAAAAAAAAAACAAAAAAP////8AAAAAPwAAABEAAAABAAAAAgAAAAAAAAAJAQAAAAAAACkAAAAMAAAAAgAAABMAAAAAAAAA/////wAAAAAGAAAABgAAAAMAAAABAAAAAAAAAF4AAAAAAAAABgAAAAAAAAAEAAAAAAAAAAAAAABPAAAAEAAAABYAAAAHAAAAAwAAAAYAAEAAAAAA/////xgAAAAnAAAAAAAAAAMAAAABAAAAAAAAAKwAAAAYAAAAJwAAAAAAAAAHAAAACgAAAAAAAABPAAAAGAAAAB0AAAAKAAAACAAAAAgAAEAAAAAAygAAAB4AAAAnAAAAAAAAAAgAAAABAAAAAAAAAAoAAAAeAAAAJwAAAAAAAAAKAAAAMAAAgAAAAAAIAQAAKQAAAD8AAAAAAAAAAgAAABMAAAAAAAAA/////ykAAAAwAAAADwAAAAwAAAABAAAAAAAAAF4AAAApAAAAMAAAAAAAAAANAAAAAAAAAAAAAABPAAAANgAAADsAAAAQAAAADAAAAAwAAEAAAAAA/////z0AAAA9AAAAAAAAAAwAAAAAAAAAAAAAAAEAAAA/AAAAQAAAAAAAAAABAAAAAAAAAAAAAAA=", + "view.tsx": "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" + }, + "protocol9": { + "main.ts": "AAAACY6wdLJEh2ZzyMQ0XA6mzLQAAAAAQAAAAKADAAC9CAAA6QkAAAAKAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADpBAAA6QQAAPkEAAD5BAAACQUAADIAAAA3AAAAQAAAAEoAAABfAAAAYwAAAGoAAAB0AAAAgwAAAIgAAACPAAAAmQAAAKUAAACqAAAArAAAALAAAADoAAAA6wAAAOwAAADtAAAA9wAAAPwAAAD9AAAAAQEAAAYBAAANAQAACQUAAA8FAAAoAQAALgEAAC8BAAAwAQAANQEAADsBAAA9AQAAPgEAAEQBAABFAQAATQEAAFIBAABjAQAAagEAAHkBAACAAQAAgQEAAIQBAACGAQAAiwEAAA8FAAAVBQAAwwAAAMsAAADMAAAAzQAAAKQBAACpAQAAqgEAAK8BAAC/AQAAwAEAAMMBAADEAQAA2AEAAN8BAADwAQAA8QEAAAkCAAAPAgAAHAIAACACAAAqAgAALwIAAD0CAABBAgAAWwIAAF8CAABgAgAAYQIAAHECAAByAgAAfAIAAH0CAAAVBQAAFgUAABYFAAAXBQAAhQIAAIYCAACQAgAAkAIAAJACAACQAgAAlQIAAJYCAACXAgAAmAIAAKkCAACuAgAAsQIAALQCAAC2AgAAuwIAAL4CAAC/AgAA0wIAANcCAADYAgAA2QIAANwCAADdAgAA5wIAAOgCAAD/AgAAAwMAACEDAAAiAwAAJgMAACoDAAA2AwAAPAMAAD8DAABFAwAARgMAAEkDAAAXBQAAGAUAABgFAAAZBQAATQMAAFEDAABSAwAAUwMAABkFAAAbBQAAVQMAAFgDAABdAwAAYgMAAG8DAABwAwAAdQMAAHYDAAB4AwAAeQMAAHsDAAB8AwAAhgMAAIcDAACKAwAAiwMAAJMDAACYAwAAwwMAAMQDAADOAwAA2wMAAOUDAADmAwAABwQAAAgEAAANBAAADgQAABAEAAARBAAAEwQAABQEAAAbBAAAHAQAAB4EAAAfBAAAJAQAACoEAAArBAAAMwQAAEMEAABJBAAASgQAAFIEAABvBAAAdAQAABsFAAAcBQAAHAUAAB0FAAB5BAAAegQAAHsEAAB8BAAAfQQAAH0EAAB9BAAAfQQAAKMEAACkBAAApQQAAKYEAACvBAAAsAQAAMoEAADLBAAAzgQAANAEAADUBAAA1gQAANgEAADZBAAA3gQAAOAEAADiBAAA4wQAAC8vLyA8cmVmZXJlbmNlIHBhdGg9Ii4vb3RoZXIudHMiIC8+CmltcG9ydCB0eXBlIHsgU2hhcGUgfSBmcm9tICIuL290aGVyLmpzIjsKaW1wb3J0IGRlZmVyICogYXMgbGF6eSBmcm9tICIuL290aGVyLmpzIjsKaW1wb3J0ICogYXMgb3RoZXIgZnJvbSAiLi9vdGhlci5qcyI7CmV4cG9ydCB7IG90aGVyLCBsYXp5IH07Ci8qKiBBIGJveC4KICogQHRlbXBsYXRlIFQKICovCmV4cG9ydCBhYnN0cmFjdCBjbGFzcyBCb3g8VD4gZXh0ZW5kcyBvdGhlci5CYXNlIHsKICAjc2VjcmV0ID0gIlx1ZDgwMO+7vyI7CiAgYWJzdHJhY3QgbWV0aG9kPFU+KC4uLnZhbHVlczogVVtdLCk6IFU7CiAgZ2V0IHZhbHVlKCkgeyByZXR1cm4gdGhpcy4jc2VjcmV0OyB9CiAgc3RhdGljIHsgY29uc29sZS5sb2coInJlYWR5Iik7IH0KfQpleHBvcnQgbmFtZXNwYWNlIFNwYWNlLklubmVyIHsgZXhwb3J0IGNvbnN0IG4gPSAxOyB9CmRlY2xhcmUgbW9kdWxlICJhbWJpZW50IiB7IGV4cG9ydCBjb25zdCBhOiBudW1iZXI7IH0KZXhwb3J0IHR5cGUgVmFsdWVzID0gcmVhZG9ubHkgW25hbWU6IHN0cmluZywgY291bnQ/OiBudW1iZXIsIC4uLnJlc3Q6IGJvb2xlYW5bXV07CmV4cG9ydCB0eXBlIEtleXM8VD4gPSB7IHJlYWRvbmx5IFtLIGluIGtleW9mIFQgYXMgYHAke0sgJiBzdHJpbmd9YF0/OiBUW0tdIH07CmV4cG9ydCBlbnVtIENvbG9yIHsgUmVkLCBHcmVlbiA9IDMsIH0KZXhwb3J0IGZ1bmN0aW9uIGNhbGwoZjogKGE6IG51bWJlciwgYjogc3RyaW5nLCkgPT4gdm9pZCwgLi4uYXJnczogW251bWJlciwgc3RyaW5nXSk6IHZvaWQgewogIGYoLi4uYXJncywpOwogIGNvbnN0IHRhZ2dlZCA9IFN0cmluZy5yYXdgYSR7YXJnc1swXX1iXG5gOwogIGxhYmVsOiBmb3Ig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", + "other.ts": "AAAACQnH2k8H5bbOVZ0OdlANqCcAAAAAQAAAAHgAAADaAAAALgEAAC4BAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAFEAAABRAAAAYgAAABEAAAAWAAAAGQAAAB0AAAAgAAAAJgAAADcAAAA7AAAAZXhwb3J0IGludGVyZmFjZSBTaGFwZSB7IGtpbmQ6ICJjaXJjbGUiIH0KZXhwb3J0IGNsYXNzIEJhc2UgeyB9Ci9wcm9qZWN0L290aGVyLnRzL3Byb2plY3Qvb3RoZXIudHMAAAAAAgAAAAQAAAAAAAAAAwAAAP///////////////////////////////wMAAAAAAAAA////////////////////////////////CgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAEAAAAAAABAAAAAAAAAAAAAAAAAAACAAAAAAP////8AAAAAPwAAABEAAAABAAAAAgAAAAAAAAAKAQAAAAAAACkAAAAMAAAAAgAAABMAAAAAAAAA/////wAAAAAGAAAABgAAAAMAAAABAAAAAAAAAF4AAAAAAAAABgAAAAAAAAAEAAAAAAAAAAAAAABPAAAAEAAAABYAAAAHAAAAAwAAAAYAAEAAAAAA/////xgAAAAnAAAAAAAAAAMAAAABAAAAAAAAAK0AAAAYAAAAJwAAAAAAAAAHAAAACgAAAAAAAABPAAAAGAAAAB0AAAAKAAAACAAAAAgAAEAAAAAAywAAAB4AAAAnAAAAAAAAAAgAAAABAAAAAAAAAAoAAAAeAAAAJwAAAAAAAAAKAAAATAAAgAAAAAAJAQAAKQAAAD8AAAAAAAAAAgAAABMAAAAAAAAA/////ykAAAAwAAAADwAAAAwAAAABAAAAAAAAAF4AAAApAAAAMAAAAAAAAAANAAAAAAAAAAAAAABPAAAANgAAADsAAAAQAAAADAAAAAwAAEAAAAAA/////z0AAAA9AAAAAAAAAAwAAAAAAAAAAAAAAAEAAAA/AAAAQAAAAAAAAAABAAAAAAAAAAAAAAA=", + "view.tsx": "AAAACRPTtHLHVfDM+81x6tFeUTsAAAAAQAAAAMgAAABhAQAAtQEAALUBAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB3AAAAdwAAAIgAAACIAAAAmQAAAA0AAAARAAAAFQAAABoAAAAqAAAALAAAAC0AAAAyAAAANAAAADgAAAA7AAAAQAAAAEEAAABEAAAARgAAAEoAAABQAAAAUgAAAFMAAABWAAAAWAAAAFwAAABfAAAAYwAAAGYAAABoAAAAcgAAAHQAAABleHBvcnQgY29uc3QgdmlldyA9IChpdGVtczogc3RyaW5nW10pID0+IDx1bCBjbGFzcz0ibGlzdCI+e2l0ZW1zLm1hcCgoaXRlbSkgPT4gPGxpIGtleT17aXRlbX0+e2l0ZW19PC9saT4pfTw+PC8+PC91bD47Ci9wcm9qZWN0L3ZpZXcudHN4L3Byb2plY3Qvdmlldy50c3gAAAAAAgAAAAQAAAABAAAABAAAAP///////////////////////////////wMAAAAAAAAA////////////////////////////////DgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAEAAAAAAAB3AAAAAAAAAAAAAAAAAACAAAAAAP////8AAAAAdgAAADkAAAABAAAAAQAAAAAAAAD1AAAAAAAAAHYAAAAAAAAAAgAAAAMAAAAAAAAA/////wAAAAAGAAAABgAAAAMAAAABAAAAAAAAAF4AAAAAAAAABgAAAAAAAAAEAAAAAAAAAAAAAAAHAQAABgAAAHUAAAAAAAAAAwAAAAEAAAACAAAA/////wwAAAB1AAAAAAAAAAYAAAABAAAAAAAAAAYBAAAMAAAAdQAAAAAAAAAHAAAACQAAAAAAAABPAAAADAAAABEAAAAKAAAACAAAAAYAAEAAAAAA3QAAABMAAAB1AAAAAAAAAAgAAAA0AAAAAAEAAP////8VAAAAJAAAABAAAAAKAAAAAQAAAAAAAACrAAAAFQAAACQAAAAAAAAACwAAABQAAAAAAAAATwAAABUAAAAaAAAADgAAAAwAAAAIAABAAAAAAL4AAAAbAAAAJAAAAAAAAAAMAAAAAQAAAAAAAACaAAAAGwAAACIAAAAAAAAADgAAAAAAAAAAAAAAJgAAACUAAAAoAAAAEQAAAAoAAAAAAAAAAAAAAB4BAAAoAAAAdQAAAAAAAAAKAAAABwAAAAAAAAAgAQAAKAAAADoAAAAZAAAAEQAAAAUAAAAAAAAATwAAACoAAAAsAAAAFAAAABIAAAAKAABAAAAAACYBAAAsAAAAOQAAAAAAAAASAAAAAQAAAAAAAAD/////LAAAADkAAAAAAAAAFAAAAAEAAAAAAAAAJQEAACwAAAA5AAAAAAAAABUAAAADAAAAAAAAAE8AAAAsAAAAMgAAABgAAAAWAAAADAAAQAAAAAAKAAAAMwAAADkAAAAAAAAAFgAAAEwAAIAAAAAA/////zoAAABwAAAANwAAABEAAAACAAAAAAAAACgBAAA6AAAAawAAADMAAAAZAAAAAgAAAAAAAADXAAAAOwAAAGoAAAAAAAAAGgAAAAkAAAAAAAAA1QAAADsAAABEAAAAHwAAABsAAAAFAAAAAAAAAE8AAAA7AAAAQAAAAB4AAAAcAAAAEAAAQAAAAABPAAAAQQAAAEQAAAAAAAAAHAAAABIAAEAAAAAA/////0UAAABpAAAAAAAAABsAAAABAAAAAAAAAN0AAABFAAAAaQAAAAAAAAAfAAAANAAAAAABAAD/////RgAAAEoAAAAkAAAAIAAAAAEAAAAAAAAAqwAAAEYAAABKAAAAAAAAACEAAAAEAAAAAAAAAE8AAABGAAAASgAAAAAAAAAiAAAAFAAAQAAAAAAmAAAASwAAAE4AAAAlAAAAIAAAAAAAAAAAAAAAHgEAAE4AAABpAAAAAAAAACAAAAAHAAAAAAAAACABAABOAAAAXgAAAC4AAAAlAAAABQAAAAAAAABPAAAAUAAAAFIAAAAoAAAAJgAAABYAAEAAAAAAJgEAAFIAAABdAAAAAAAAACYAAAABAAAAAAAAAP////9SAAAAXQAAAAAAAAAoAAAAAQAAAAAAAAAlAQAAUgAAAF0AAAAAAAAAKQAAAAMAAAAAAAAATwAAAFIAAABWAAAALAAAACoAAAAYAABAAAAAACgBAABXAAAAXQAAAAAAAAAqAAAAAgAAAAAAAABPAAAAWAAAAFwAAAAAAAAALAAAABoAAEAAAAAA/////14AAABkAAAAMQAAACUAAAABAAAAAAAAACgBAABeAAAAZAAAAAAAAAAuAAAAAgAAAAAAAABPAAAAXwAAAGMAAAAAAAAALwAAABwAAEAAAAAAIQEAAGQAAABpAAAAAAAAACUAAAABAAAAAAAAAE8AAABmAAAAaAAAAAAAAAAxAAAAHgAAQAAAAAAiAQAAawAAAHAAAAAAAAAAGQAAAAcAAAAAAAAAIwEAAGsAAABtAAAANQAAADMAAAAAAAAAAAAAAP////9tAAAAbQAAADYAAAAzAAAAAAAAAAAAAAAkAQAAbQAAAHAAAAAAAAAAMwAAAAAAAAAAAAAAIQEAAHAAAAB1AAAAAAAAABEAAAABAAAAAAAAAE8AAAByAAAAdAAAAAAAAAA3AAAAIAAAQAAAAAABAAAAdgAAAHcAAAAAAAAAAQAAAAAAAAAAAAAA" + } +} diff --git a/packages/compiler/test/periodic-guard-emission.test.ts b/packages/compiler/test/periodic-guard-emission.test.ts new file mode 100644 index 000000000..dfb8c60ae --- /dev/null +++ b/packages/compiler/test/periodic-guard-emission.test.ts @@ -0,0 +1,53 @@ +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 } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-periodic-guards-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +test("a counted loop with a check every fourth iteration asks LLVM to unroll it by 4", async () => { + const llvm = await emitLlvm(` +function escape(c: number): number { + let z = 0; + let m = 0; + for (let j = 0; j < 50; j++) { + z = z * z + c; + m = z * z; + if ((j & 3) === 3 && !(m <= 4)) break; + } + return m; +} +console.log(escape(Number(process.argv[2] ?? "0.25"))); +`); + expect(llvm).toMatch(/br label %loop\.c\d+, !llvm\.loop !1000004/); + expect(llvm).toContain(`!1000004 = distinct !{!1000004, !{!"llvm.loop.unroll.count", i32 4}}`); +}); + +test("a loop without a periodic guard has no loop metadata", async () => { + const llvm = await emitLlvm(` +function sum(n: number): number { + let s = 0; + for (let j = 0; j < n; j++) { + s += j; + if (s > 1000) break; + } + return s; +} +console.log(sum(Number(process.argv[2] ?? "10"))); +`); + expect(llvm).not.toContain("!llvm.loop"); +}); diff --git a/packages/compiler/test/regex-replace-lifetimes.test.ts b/packages/compiler/test/regex-replace-lifetimes.test.ts new file mode 100644 index 000000000..0c60a9e5e --- /dev/null +++ b/packages/compiler/test/regex-replace-lifetimes.test.ts @@ -0,0 +1,57 @@ +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 } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-regex-replace-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function functionBody(llvm: string, name: string): string[] { + const start = llvm.split("\n").findIndex((l) => l.startsWith("define") && l.includes(name)); + expect(start).toBeGreaterThanOrEqual(0); + const lines = llvm.split("\n").slice(start); + return lines.slice(0, lines.indexOf("}")); +} + +test("a chain of substitutions releases each intermediate result when the next one returns", async () => { + const llvm = await emitLlvm(` +function chain(s: string): number { + return s + .replace(/a[bc]/g, "<1>") + .replace(/b[cd]/g, "<2>") + .replace(/c[de]/g, "<3>") + .replaceAll(/<[^>]*>/g, "|").length; +} +console.log(chain(process.argv[2] ?? "abcabc")); +`); + const body = functionBody(llvm, "chain"); + const calls = body + .map((line, index) => ({ line, index })) + .filter(({ line }) => /call ptr @scr_regex_replace(_all|_with)?\(/.test(line)); + expect(calls).toHaveLength(4); + const results = calls.map(({ line }) => /^\s*(%[\w.]+) = call/.exec(line)![1]!); + // Each intermediate result is released after the call that consumes it + // and before the next substitution starts. + for (let k = 0; k < 3; k++) { + const release = body.findIndex( + (line, index) => + index > calls[k + 1]!.index && + line.includes("release") && + line.includes(`(ptr ${results[k]})`), + ); + expect(release, `release of ${results[k]}`).toBeGreaterThan(calls[k + 1]!.index); + if (k + 2 < calls.length) expect(release).toBeLessThan(calls[k + 2]!.index); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8326-never-promise-instanceof.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8326-never-promise-instanceof.ts.json new file mode 100644 index 000000000..d10bc2c18 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8326-never-promise-instanceof.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8326-never-promise-instanceof.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8400-iterative-teardown.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8400-iterative-teardown.ts.json new file mode 100644 index 000000000..80ad38bdf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8400-iterative-teardown.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8400-iterative-teardown.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/array-prototype-call-native-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/array-prototype-call-native-arrays.ts.json new file mode 100644 index 000000000..2904322c5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/array-prototype-call-native-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/array-prototype-call-native-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-unix-socket-connect-default-import.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-unix-socket-connect-default-import.mjs.json new file mode 100644 index 000000000..a1bb1df19 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-unix-socket-connect-default-import.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/net-unix-socket-connect-default-import.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-unix-socket-connect.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-unix-socket-connect.ts.json new file mode 100644 index 000000000..7bf0a928d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-unix-socket-connect.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/net-unix-socket-connect.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typeof-ambient-undefined-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typeof-ambient-undefined-names.ts.json new file mode 100644 index 000000000..a2bd8dc1c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typeof-ambient-undefined-names.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/typeof-ambient-undefined-names.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/shape-overlay-facts/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/shape-overlay-facts/main.ts.json new file mode 100644 index 000000000..f01de4ae5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/shape-overlay-facts/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/shape-overlay-facts/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index a71aaa244..5bb56f082 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1621,6 +1621,10 @@ void scr_dyn_check_fail(const ScrDynPath *path, const char *want, const ScrDyn * /* The Date object a checked-dynamic Date handle holds, +1, or NULL with * the check failure pending. */ ScrDate *scr_dyn_native_date_check(const ScrDyn *value, const ScrDynPath *path); +/* Names the analysis-proven fact the next checked conversion checks + * (shape inference's overlay), or NULL: its failure then reports a compiler + * analysis bug (" :"). */ +void scr_dyn_inferred_site(const char *site); /* Matchers and builders of recursive shapes (records and arrays whose type * reaches itself) track the dyn values they visit, so cyclic values match diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 778d17eaf..7a9b1bc35 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -916,6 +916,117 @@ void scr_dyn_invoke_discard_id(ScrDyn *recv, ScrDyn *callee, const char *method, scr_dyn_release(scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what)); } +/* The callback methods of a dense dyn array that take a thisArg: + * forEach, map, filter, some, every, find, findIndex, findLast, + * findLastIndex and flatMap. The caller binds `this` for the callbacks. + * `len` is the length before the first callback. */ +static ScrDyn *dyn_arr_callback_method(ScrDyn *recv, int mid, ScrDyn *const *args, size_t argc, + size_t len, ScrDyn *callback_recv) { + if ((mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex)) { + if (!dyn_cb_check(args, argc)) return NULL; + ScrDyn *visible_recv = callback_recv ? callback_recv : recv; + for (size_t i = len; i > 0; i--) { + size_t index = i - 1; + ScrDyn *item = index < recv->v.arr.len + ? scr_dyn_retain(recv->v.arr.items[index]) + : scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *result = dyn_call_cb(args[0], item, index, visible_recv); + if (!result) { scr_dyn_release(item); return NULL; } + bool matched = scr_dyn_truthy(result); + scr_dyn_release(result); + if (matched) { + if ((mid == DYN_M_findLast)) return item; + scr_dyn_release(item); + return scr_dyn_new_num((double)index); + } + scr_dyn_release(item); + } + return (mid == DYN_M_findLast) + ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(-1); + } + if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || + (mid == DYN_M_filter) || (mid == DYN_M_some) || + (mid == DYN_M_every) || (mid == DYN_M_find) || + (mid == DYN_M_findIndex)) { + if (!dyn_cb_check(args, argc)) return NULL; + ScrDyn *cb = args[0]; + ScrDyn *out = ((mid == DYN_M_map) || (mid == DYN_M_filter)) ? scr_dyn_new_arr() : NULL; + /* Array iteration methods snapshot length before the first callback. + * A shrinking callback can remove a later dense entry; an expanding + * callback must not make the new tail observable to this pass. Live + * Web-boundary capsules are the externally visible receiver, so pass + * that capsule as callback arg 3: mutations through the callback's + * array argument then commit directly to the original static array. */ + size_t n = len; + ScrDyn *visible_recv = callback_recv ? callback_recv : recv; + for (size_t step = 0; step < n; step++) { + size_t i = step; + if (i >= recv->v.arr.len) continue; + ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]); + ScrDyn *r = dyn_call_cb(cb, item, i, visible_recv); + if (!r) { scr_dyn_release(item); scr_dyn_release(out); return NULL; } + if ((mid == DYN_M_map)) { + scr_dyn_arr_push(out, r); /* ownership moves in */ + r = NULL; + } else { + bool truthy = scr_dyn_truthy(r); + scr_dyn_release(r); + if ((mid == DYN_M_filter) && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); + if ((mid == DYN_M_some) && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } + if ((mid == DYN_M_every) && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } + if ((mid == DYN_M_find) && truthy) return item; /* +1 moves out */ + if ((mid == DYN_M_findIndex) && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } + } + scr_dyn_release(item); + } + if (out) return out; + if ((mid == DYN_M_some)) return scr_dyn_new_bool(false); + if ((mid == DYN_M_every)) return scr_dyn_new_bool(true); + if ((mid == DYN_M_find)) return scr_dyn_retain(scr_dyn_undefined()); + if ((mid == DYN_M_findIndex)) return scr_dyn_new_num(-1); + return scr_dyn_retain(scr_dyn_undefined()); /* forEach */ + } + if ((mid == DYN_M_flatMap)) { + /* JS Array.prototype.flatMap over a dyn array: map + a depth-1 + * flatten. Native dyn-array results flatten element-by-element; a + * WRAPPED engine array flattens through the routed keyed reads + * (elements wrap back scalar-normalized); everything else pushes + * as a single element (JS keeps non-array callback results whole). + * JS's spec snapshots the length up front (elements appended by + * the callback are not visited). */ + if (!dyn_cb_check(args, argc)) return NULL; + ScrDyn *cb = args[0]; + ScrDyn *out = scr_dyn_new_arr(); + size_t n = recv->v.arr.len; + for (size_t i = 0; i < n && i < recv->v.arr.len; i++) { + if (!dyn_borrow_present(recv, i)) { if (scr_exc_pending()) { scr_dyn_release(out); return NULL; } continue; } + ScrDyn *item = dyn_borrow_item(recv, i); + if (!item) { scr_dyn_release(out); return NULL; } + ScrDyn *r = dyn_call_cb( + cb, item, i, callback_recv ? callback_recv : recv); + scr_dyn_release(item); + if (!r) { scr_dyn_release(out); return NULL; } + if (r->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(r)) { + ScrDyn *view = scr_dyn_typed_ref_materialize(r); + scr_dyn_release(r); + r = view; + if (!r) { scr_dyn_release(out); return NULL; } + } + if (r->kind == SCR_DYN_ARR) { + for (size_t j = 0; j < r->v.arr.len; j++) { + if (dyn_borrow_present(r, j)) scr_dyn_arr_push(out, dyn_borrow_item(r, j)); + if (scr_exc_pending()) { scr_dyn_release(r); scr_dyn_release(out); return NULL; } + } + scr_dyn_release(r); + } else { + scr_dyn_arr_push(out, r); /* ownership moves in */ + } + } + return out; + } + return NULL; +} + static ScrDyn *scr_dyn_invoke_impl( ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv) { @@ -1367,27 +1478,16 @@ static ScrDyn *scr_dyn_invoke_impl( } return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); } - if ((mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex)) { - if (!dyn_cb_check(args, argc)) return NULL; - ScrDyn *visible_recv = callback_recv ? callback_recv : recv; - for (size_t i = len; i > 0; i--) { - size_t index = i - 1; - ScrDyn *item = index < recv->v.arr.len - ? scr_dyn_retain(recv->v.arr.items[index]) - : scr_dyn_retain(scr_dyn_undefined()); - ScrDyn *result = dyn_call_cb(args[0], item, index, visible_recv); - if (!result) { scr_dyn_release(item); return NULL; } - bool matched = scr_dyn_truthy(result); - scr_dyn_release(result); - if (matched) { - if ((mid == DYN_M_findLast)) return item; - scr_dyn_release(item); - return scr_dyn_new_num((double)index); - } - scr_dyn_release(item); - } - return (mid == DYN_M_findLast) - ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(-1); + if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || (mid == DYN_M_filter) || + (mid == DYN_M_some) || (mid == DYN_M_every) || (mid == DYN_M_find) || + (mid == DYN_M_findIndex) || (mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex) || + (mid == DYN_M_flatMap)) { + /* Every callback runs with `this` bound to the thisArg, or undefined + * when there is none. */ + scr_dyn_this_push_dyn(argc > 1 ? args[1] : scr_dyn_undefined()); + ScrDyn *result = dyn_arr_callback_method(recv, mid, args, argc, len, callback_recv); + scr_dyn_this_pop(); + return result; } if ((mid == DYN_M_join)) { ScrJsonBuf b; @@ -1550,48 +1650,6 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || - (mid == DYN_M_filter) || (mid == DYN_M_some) || - (mid == DYN_M_every) || (mid == DYN_M_find) || - (mid == DYN_M_findIndex)) { - if (!dyn_cb_check(args, argc)) return NULL; - ScrDyn *cb = args[0]; - ScrDyn *out = ((mid == DYN_M_map) || (mid == DYN_M_filter)) ? scr_dyn_new_arr() : NULL; - /* Array iteration methods snapshot length before the first callback. - * A shrinking callback can remove a later dense entry; an expanding - * callback must not make the new tail observable to this pass. Live - * Web-boundary capsules are the externally visible receiver, so pass - * that capsule as callback arg 3: mutations through the callback's - * array argument then commit directly to the original static array. */ - size_t n = len; - ScrDyn *visible_recv = callback_recv ? callback_recv : recv; - for (size_t step = 0; step < n; step++) { - size_t i = step; - if (i >= recv->v.arr.len) continue; - ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]); - ScrDyn *r = dyn_call_cb(cb, item, i, visible_recv); - if (!r) { scr_dyn_release(item); scr_dyn_release(out); return NULL; } - if ((mid == DYN_M_map)) { - scr_dyn_arr_push(out, r); /* ownership moves in */ - r = NULL; - } else { - bool truthy = scr_dyn_truthy(r); - scr_dyn_release(r); - if ((mid == DYN_M_filter) && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); - if ((mid == DYN_M_some) && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } - if ((mid == DYN_M_every) && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } - if ((mid == DYN_M_find) && truthy) return item; /* +1 moves out */ - if ((mid == DYN_M_findIndex) && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } - } - scr_dyn_release(item); - } - if (out) return out; - if ((mid == DYN_M_some)) return scr_dyn_new_bool(false); - if ((mid == DYN_M_every)) return scr_dyn_new_bool(true); - if ((mid == DYN_M_find)) return scr_dyn_retain(scr_dyn_undefined()); - if ((mid == DYN_M_findIndex)) return scr_dyn_new_num(-1); - return scr_dyn_retain(scr_dyn_undefined()); /* forEach */ - } if ((mid == DYN_M_reduce) || (mid == DYN_M_reduceRight)) { if (!dyn_cb_check(args, argc)) return NULL; bool backward = (mid == DYN_M_reduceRight); @@ -1616,44 +1674,6 @@ static ScrDyn *scr_dyn_invoke_impl( scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if ((mid == DYN_M_flatMap)) { - /* JS Array.prototype.flatMap over a dyn array: map + a depth-1 - * flatten. Native dyn-array results flatten element-by-element; a - * WRAPPED engine array flattens through the routed keyed reads - * (elements wrap back scalar-normalized); everything else pushes - * as a single element (JS keeps non-array callback results whole). - * JS's spec snapshots the length up front (elements appended by - * the callback are not visited). */ - if (!dyn_cb_check(args, argc)) return NULL; - ScrDyn *cb = args[0]; - ScrDyn *out = scr_dyn_new_arr(); - size_t n = recv->v.arr.len; - for (size_t i = 0; i < n && i < recv->v.arr.len; i++) { - if (!dyn_borrow_present(recv, i)) { if (scr_exc_pending()) { scr_dyn_release(out); return NULL; } continue; } - ScrDyn *item = dyn_borrow_item(recv, i); - if (!item) { scr_dyn_release(out); return NULL; } - ScrDyn *r = dyn_call_cb( - cb, item, i, callback_recv ? callback_recv : recv); - scr_dyn_release(item); - if (!r) { scr_dyn_release(out); return NULL; } - if (r->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(r)) { - ScrDyn *view = scr_dyn_typed_ref_materialize(r); - scr_dyn_release(r); - r = view; - if (!r) { scr_dyn_release(out); return NULL; } - } - if (r->kind == SCR_DYN_ARR) { - for (size_t j = 0; j < r->v.arr.len; j++) { - if (dyn_borrow_present(r, j)) scr_dyn_arr_push(out, dyn_borrow_item(r, j)); - if (scr_exc_pending()) { scr_dyn_release(r); scr_dyn_release(out); return NULL; } - } - scr_dyn_release(r); - } else { - scr_dyn_arr_push(out, r); /* ownership moves in */ - } - } - return out; - } if ((mid == DYN_M_sort)) { ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined(); if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) { @@ -1801,7 +1821,10 @@ static ScrDyn *dyn_array_method_value_call(ScrClosure *closure, ScrDyn *const *a ScrStr *method = scr_box_get_ref(closure->caps[0]); ScrDyn *receiver = scr_dyn_this_get(); ScrDyn *result; - if (receiver->kind == SCR_DYN_ARR) { + /* An array, or a live view of a native array, runs the method its own + * dispatch has; other receivers take the generic array-like algorithms. */ + if (receiver->kind == SCR_DYN_ARR || + (receiver->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(receiver))) { result = scr_dyn_invoke(receiver, method->data, args, argc, method->data); } else { ScrDyn *pack = scr_dyn_new_arr(); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index eb6f931e0..1893ec193 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -11694,7 +11694,53 @@ static const char *scr_dyn_kind_name(const ScrDyn *d) { return "unknown"; } +/* Shape inference's overlay (compiler frontend/shape-inference): a checked + * conversion of an analysis-proven fact names its site (" + * :") while it runs. A failure there is a compiler analysis + * bug, so it reports itself as one instead of as a type error. */ +static SCR_TL const char *scr_dyn_inferred_site_name; + +void scr_dyn_inferred_site(const char *site) { scr_dyn_inferred_site_name = site; } + +static void scr_dyn_inferred_fact_fail(const char *site, const ScrDynPath *path, const char *want, + const ScrDyn *got) { + const char *space = strchr(site, ' '); + size_t pkg_len = space ? (size_t)(space - site) : 0; + const char *where = space ? space + 1 : site; + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "scriptc internal error: an inferred type was contradicted at "); + scr_jb_puts(&b, where); + scr_jb_puts(&b, " (expected "); + scr_jb_puts(&b, want); + scr_jb_puts(&b, " at "); + scr_dyn_path_render(&b, path); + scr_jb_puts(&b, ", got "); + scr_jb_puts(&b, scr_dyn_kind_name(got)); + scr_jb_puts(&b, "). This is a bug in scriptc's shape analysis, not in the program's data. " + "Rebuild with --no-shape-overlay, or set \"scriptc\": {\"packages\": {\""); + for (size_t i = 0; i < pkg_len; i++) scr_jb_putc(&b, site[i]); + scr_jb_puts(&b, "\": {\"shapes\": false}}} in package.json, to compile the package without it."); + /* Validation mode: every hit is logged, also when the program catches it. */ + const char *log = getenv("SCRIPTC_SHAPE_CHECK_LOG"); + if (log && *log) { + FILE *f = fopen(log, "a"); + if (f) { + fwrite(b.data, 1, b.len, f); + fputc('\n', f); + fclose(f); + } + } + scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b)); +} + void scr_dyn_check_fail(const ScrDynPath *path, const char *want, const ScrDyn *got) { + const char *site = scr_dyn_inferred_site_name; + if (site) { + scr_dyn_inferred_site_name = NULL; + scr_dyn_inferred_fact_fail(site, path, want, got); + return; + } ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "expected "); diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index a940fb7f1..776829e8c 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -2741,6 +2741,24 @@ ScrNetSocket *scr_net_connect_checked(double port, ScrStr *host /*borrowed, null return scr_net_connect(port, host, cb); } +/* net.connect/createConnection(path[, connectListener]) and the `path` + * option from compiled code: a client of the Unix socket at `path` + * (borrowed). For a positional string (`pipe_name_rule`), Node's isPipeName + * applies first: a string that reads as a non-negative number is a port on + * localhost. `cb` (moves, nullable) is the connect listener. */ +ScrNetSocket *scr_net_connect_path_checked(ScrStr *path, bool pipe_name_rule, ScrClosure *cb) { + double port = pipe_name_rule ? scr_string_to_number(path) : -1; + if (port >= 0) { + ScrStr *host = scr_str_new("localhost", 9); + ScrNetSocket *s = scr_net_connect_checked(port, host, cb); + scr_str_release(host); + return s; + } + ScrNetSocket *s = scr_net_connect_path(path, false); + if (cb) scr_net_ls_add(&s->conn_ls, cb, NULL, true); + return s; +} + static bool scr_net_dyn_truthy(const ScrDyn *v) { switch (v->kind) { case SCR_DYN_UNDEF: diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index 6a41e26d8..af4b8e1b8 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -265,6 +265,10 @@ ScrNetSocket *scr_net_connect_lookup_dyn(double port, ScrStr *host /*borrowed*/, /* A client socket for the Unix socket at `path`, dialed now or (deferred) * by scr_net_sock_dial_start. A failure is the deferred 'error'. */ ScrNetSocket *scr_net_connect_path(ScrStr *path /*borrowed*/, bool deferred); /* +1 */ +/* net.connect(path[, cb]) and connect({ path }): with `pipe_name_rule`, a + * numeric string is a localhost port, as in Node. */ +ScrNetSocket *scr_net_connect_path_checked(ScrStr *path /*borrowed*/, bool pipe_name_rule, + ScrClosure *cb /*moves, nullable*/); /* +1 */ /* server.listen(path[, cb]): a Unix-socket server, its file removed at close. */ void scr_net_listen_path(ScrNetServer *s, ScrStr *path /*borrowed*/, ScrClosure *cb /*moves, nullable*/); void scr_net_sock_dial_start(ScrNetSocket *s); diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 2f4ad185e..fdd7179e5 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -119,6 +119,10 @@ struct ScrStreamState { bool reading, in_read_sync, ended, end_emitted, end_scheduled; bool in_read_call; /* inside the user's _read (in_read_sync also starts true) */ bool need_readable, emitted_readable, readable_listening; + /* the stream has 'data' listeners: kept by scr_stream_listener_observe + * from the registry's empty/nonempty transitions, so the per-chunk + * paths read it instead of looking the name up */ + bool data_listeners; bool resume_scheduled, maybe_more_scheduled; /* The buffer's representation: encoded means every entry is a ScrStr, * otherwise every entry is a ScrBytes (scr_stream_rbuf_push checks @@ -881,10 +885,14 @@ static void scr_stream_emit_data(ScrStream *s, void *chunk, ScrEntryKind kind) { } else { b = chunk; } - ScrStr *n = (ScrStr *)&scr_stream_lit_data; - scr_emitter_emit((ScrEmitter *)s, n, b, str, value); - scr_str_release(n); - if (scr_exc_pending()) return; + /* emit('data') with no listeners does nothing; a piped stream delivers + * through st->pipes below */ + if (st->r.data_listeners) { + ScrStr *n = (ScrStr *)&scr_stream_lit_data; + scr_emitter_emit((ScrEmitter *)s, n, b, str, value); + scr_str_release(n); + if (scr_exc_pending()) return; + } /* pipe delivery, with backpressure: a full destination pauses us until * its 'drain'. Encoded sources write the string (utf8 bytes at the * destination — Node writes the decoded string through). */ @@ -1033,7 +1041,7 @@ static bool scr_stream_add_chunk(ScrStream *s, ScrBytes *chunk, bool front) { } if (entry_len > 0 && st->r.flowing == 1 && st->r.length == 0 && !st->r.in_read_sync && !front && - (scr_emitter_has((ScrEmitter *)s, "data") || st->pipes.n > 0)) { + (st->r.data_listeners || st->pipes.n > 0)) { /* the direct-emit fast path: flowing, empty buffer, async push */ if (st->r.emitted_readable) st->r.emitted_readable = false; ScrEntryKind kind = scr_stream_rkind(st); @@ -2002,7 +2010,7 @@ static int scr_stream_add_entry(ScrStream *s, void *entry /*moves*/, ScrEntryKin } st->r.reading = false; if (st->r.flowing == 1 && st->r.length == 0 && !st->r.in_read_sync && - (scr_emitter_has((ScrEmitter *)s, "data") || st->pipes.n > 0)) { + (st->r.data_listeners || st->pipes.n > 0)) { if (st->r.emitted_readable) st->r.emitted_readable = false; scr_stream_emit_data(s, entry, kind); scr_stream_entry_release_kind(entry, kind); @@ -2501,6 +2509,20 @@ static ScrStreamState *scr_stream_state_new(bool has_r, bool has_w, return st; } +/* The registry's empty/nonempty transitions for the names the per-chunk + * paths test (scr_emitter_observe): no script runs here. */ +static void scr_stream_listener_observe(void *context, const ScrStr *name, bool present) { + ScrStream *s = context; + if (s->st != NULL && name->len == 4 && memcmp(name->data, "data", 4) == 0) + s->st->r.data_listeners = present; +} + +/* Starts tracking the stream's 'data' listeners, once its state exists. */ +static void scr_stream_watch_listeners(ScrStream *s) { + s->st->r.data_listeners = scr_emitter_has((ScrEmitter *)s, "data"); + scr_emitter_observe((ScrEmitter *)s, &scr_stream_listener_observe, s); +} + static ScrStream *scr_stream_alloc(const ScrVt *vt, const char *cls, bool has_r, bool has_w, double rhwm, double whwm, bool auto_destroy, bool emit_close, bool allow_half_open) { @@ -2529,6 +2551,7 @@ static ScrStream *scr_stream_alloc(const ScrVt *vt, const char *cls, bool has_r, scr_emitter_reserve(em, "end"); scr_emitter_reserve(em, "readable"); } + scr_stream_watch_listeners(s); scr_obj_alloc_note(); return s; } @@ -2651,6 +2674,7 @@ void scr_stream_init_readable(ScrStream *s, double hwm, bool auto_destroy, bool ScrClosure *destroy, ScrStreamErrInv destroy_inv) { ScrStreamState *st = scr_stream_state_new(true, false, hwm, -1, auto_destroy, emit_close, true); s->st = st; + scr_stream_watch_listeners(s); st->r.read_cb = read; st->r.read_inv = read_inv; st->destroy_cb = destroy; @@ -2663,6 +2687,7 @@ void scr_stream_init_writable(ScrStream *s, double hwm, bool auto_destroy, bool ScrClosure *destroy, ScrStreamErrInv destroy_inv) { ScrStreamState *st = scr_stream_state_new(false, true, -1, hwm, auto_destroy, emit_close, true); s->st = st; + scr_stream_watch_listeners(s); st->w.write_cb = write; st->w.write_inv = write_inv; st->w.final_cb = final_cb; @@ -2680,6 +2705,7 @@ void scr_stream_init_duplex(ScrStream *s, double rhwm, double whwm, bool auto_de ScrStreamState *st = scr_stream_state_new(readable_side, writable_side, rhwm, whwm, auto_destroy, emit_close, allow_half_open); s->st = st; + scr_stream_watch_listeners(s); if (!readable_side) { st->r.ended = true; st->r.end_emitted = true; @@ -2706,6 +2732,7 @@ static void scr_stream_init_transformish(ScrStream *s, bool passthrough, ScrStreamState *st = scr_stream_state_new(true, true, rhwm, whwm, auto_destroy, emit_close, allow_half_open); s->st = st; + scr_stream_watch_listeners(s); st->is_transform = !passthrough; st->passthrough = passthrough; /* the Transform constructor's readable-half stance (see new_transformish) */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index baf180930..047aeaafd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,19 +93,68 @@ importers: specifier: workspace:* version: link:../cli-win32-x64-msvc - packages/cli-darwin-arm64: {} + packages/cli-darwin-arm64: + dependencies: + '@scriptc/llvm-darwin-arm64': + specifier: workspace:* + version: link:../llvm-darwin-arm64 + '@scriptc/runtime-darwin-arm64': + specifier: workspace:* + version: link:../runtime-darwin-arm64 - packages/cli-darwin-x64: {} + packages/cli-darwin-x64: + dependencies: + '@scriptc/llvm-darwin-x64': + specifier: workspace:* + version: link:../llvm-darwin-x64 + '@scriptc/runtime-darwin-x64': + specifier: workspace:* + version: link:../runtime-darwin-x64 - packages/cli-linux-arm64-gnu: {} + packages/cli-linux-arm64-gnu: + dependencies: + '@scriptc/llvm-linux-arm64-gnu': + specifier: workspace:* + version: link:../llvm-linux-arm64-gnu + '@scriptc/runtime-linux-arm64-gnu': + specifier: workspace:* + version: link:../runtime-linux-arm64-gnu - packages/cli-linux-arm64-musl: {} + packages/cli-linux-arm64-musl: + dependencies: + '@scriptc/llvm-linux-arm64-musl': + specifier: workspace:* + version: link:../llvm-linux-arm64-musl + '@scriptc/runtime-linux-arm64-musl': + specifier: workspace:* + version: link:../runtime-linux-arm64-musl - packages/cli-linux-x64-gnu: {} + packages/cli-linux-x64-gnu: + dependencies: + '@scriptc/llvm-linux-x64-gnu': + specifier: workspace:* + version: link:../llvm-linux-x64-gnu + '@scriptc/runtime-linux-x64-gnu': + specifier: workspace:* + version: link:../runtime-linux-x64-gnu - packages/cli-linux-x64-musl: {} + packages/cli-linux-x64-musl: + dependencies: + '@scriptc/llvm-linux-x64-musl': + specifier: workspace:* + version: link:../llvm-linux-x64-musl + '@scriptc/runtime-linux-x64-musl': + specifier: workspace:* + version: link:../runtime-linux-x64-musl - packages/cli-win32-x64-msvc: {} + packages/cli-win32-x64-msvc: + dependencies: + '@scriptc/llvm-win32-x64-msvc': + specifier: workspace:* + version: link:../llvm-win32-x64-msvc + '@scriptc/runtime-win32-x64-msvc': + specifier: workspace:* + version: link:../runtime-win32-x64-msvc packages/compiler: dependencies: diff --git a/scripts/build-native-cli.mts b/scripts/build-native-cli.mts index 331503890..3dbe22bf3 100644 --- a/scripts/build-native-cli.mts +++ b/scripts/build-native-cli.mts @@ -18,6 +18,9 @@ import type { NativeToolchainManifest } from "../packages/compiler/src/native/to const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const output = resolve(process.argv[2] ?? join(root, ".scriptc/native-cli")); +// --platform-packages builds an npm platform package's distribution, which +// depends on the helper and runtime pack packages instead of bundling them. +const platformPackages = process.argv.includes("--platform-packages"); const optimization = process.env["SCRIPTC_NATIVE_OPTIMIZATION"] === "dev" ? "dev" : "release"; const sanitize = process.env["SCRIPTC_SAN"] === "1"; // SCRIPTC_NATIVE_PROFILE_GENERATE= builds a compiler that writes a @@ -187,6 +190,7 @@ const assets = stageNativeCliAssets({ ts7: tsgoExecutable(), compilerVersion, strip, + platformPackages, }); // A cross-libc seed may use Zig on the build host. The installed compiler // selects the destination platform's ordinary linker and SDK. diff --git a/scripts/generate-ir-fields.mjs b/scripts/generate-ir-fields.mjs new file mode 100644 index 000000000..5b5ad011a --- /dev/null +++ b/scripts/generate-ir-fields.mjs @@ -0,0 +1,215 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +// Generates packages/compiler/src/ir/fields.generated.ts from the node types +// in ir.ts: for each IrExpr and IrStmt kind, the fields that can hold a +// string, read by name. Code that would enumerate a node's keys reads them +// through this switch instead: the native compiler enumerates and reads an +// untyped object's keys through its dynamic representation, one conversion +// per node. `--check` fails when the file is stale. +const root = fileURLToPath(new URL("..", import.meta.url)); +const require = createRequire(join(root, "package.json")); +const ts = require("typescript"); +const irPath = join(root, "packages/compiler/src/ir/ir.ts"); +const target = join(root, "packages/compiler/src/ir/fields.generated.ts"); + +const program = ts.createProgram([irPath], { strict: true, target: ts.ScriptTarget.ES2022 }); +const checker = program.getTypeChecker(); +const source = program.getSourceFile(irPath); +const aliases = new Map(); +ts.forEachChild(source, (node) => { + if (ts.isTypeAliasDeclaration(node)) aliases.set(node.name.text, node); +}); + +/** Whether a value of `type` can be a string. */ +function holdsString(type) { + if (type.isUnion()) return type.types.some(holdsString); + return (type.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; +} + +/** Whether every value of `type` is a string. */ +function onlyString(type) { + if (type.isUnion()) return type.types.every(onlyString); + return (type.flags & ts.TypeFlags.StringLike) !== 0; +} + +/** Each kind's string fields, in declaration order, merged over the unions: + * a kind both unions declare lists the fields of both. */ +const kinds = new Map(); +for (const name of ["IrExpr", "IrStmt"]) { + const alias = aliases.get(name); + const union = checker.getTypeAtLocation(alias.name); + for (const member of union.isUnion() ? union.types : [union]) { + const kindSymbol = member.getProperty("kind"); + const kindType = checker.getTypeOfSymbolAtLocation(kindSymbol, alias); + if (!kindType.isStringLiteral()) throw new Error(`${name} member without a literal kind`); + const fields = kinds.get(kindType.value) ?? []; + for (const property of member.getProperties()) { + const type = checker.getTypeOfSymbolAtLocation(property, alias); + if (!holdsString(type)) continue; + const known = fields.find((field) => field.name === property.name); + const always = onlyString(type) && (property.flags & ts.SymbolFlags.Optional) === 0; + if (known === undefined) fields.push({ name: property.name, always }); + else known.always &&= always; + } + kinds.set(kindType.value, fields); + } +} + +const lines = [ + "// Generated from packages/compiler/src/ir/ir.ts by scripts/generate-ir-fields.mjs.", + "// Run `pnpm generate:ir-fields` after changing the fields of an IR node.", + 'import type { IrExpr, IrStmt, IrType } from "./ir.js";', + "", + "/** Calls `visit` with each string field of `node` (including `kind`) and its", + " * name: the string values `for (const key in node)` finds on a node, read", + " * by name in the order ir.ts declares them. */", + "export function eachStringField(", + " node: IrExpr | IrStmt,", + " visit: (value: string, field: string) => void,", + "): void {", + " switch (node.kind) {", +]; +for (const [kind, fields] of [...kinds].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) { + lines.push(` case ${JSON.stringify(kind)}: {`); + for (const { name, always } of fields) { + const read = `node.${name}`; + if (always) lines.push(` visit(${read}, ${JSON.stringify(name)});`); + else + lines.push(` if (typeof ${read} === "string") visit(${read}, ${JSON.stringify(name)});`); + } + lines.push(" return;", " }"); +} +lines.push(" }", "}", ""); + +// The deep walkers: every field of every object under an IR node, as a +// recursive key walk finds them, read by name. +const members = new Map(); +for (const name of ["IrExpr", "IrStmt", "IrType"]) { + const union = checker.getTypeAtLocation(aliases.get(name).name); + members.set(name, new Set(union.isUnion() ? union.types : [union])); +} +const nullish = ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void; +const scalar = + ts.TypeFlags.NumberLike | + ts.TypeFlags.BooleanLike | + ts.TypeFlags.BigIntLike | + ts.TypeFlags.ESSymbolLike; + +/** The walker an IR union member belongs to, or null. */ +function irKind(type) { + for (const [name, set] of members) if (set.has(type)) return name; + return null; +} + +let temp = 0; +/** Lines that walk `expr` of `type`: strings to `walker.string`, IR nodes to + * their walkers, arrays by element and other objects by field. */ +function walkLines(expr, type, indent, depth) { + if (depth > 12) throw new Error(`field type nests too deep at ${expr}`); + const parts = type.isUnion() ? type.types : [type]; + const present = parts.filter((part) => (part.flags & nullish) === 0); + const optional = present.length !== parts.length; + const strings = present.filter((part) => (part.flags & ts.TypeFlags.StringLike) !== 0); + const objects = present.filter((part) => (part.flags & (ts.TypeFlags.StringLike | scalar)) === 0); + for (const part of present) + if ((part.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) + throw new Error(`untyped field ${expr}`); + const body = []; + if (objects.length > 0) { + const kinds = new Set(objects.map(irKind)); + if (kinds.size === 1 && !kinds.has(null)) { + const [kind] = kinds; + body.push(`walk${kind.slice(2)}Fields(${expr}, walker, skipType);`); + } else if (objects.every((part) => checker.isArrayType(part))) { + if (objects.length !== 1) throw new Error(`several array types at ${expr}`); + const item = `item${temp++}`; + body.push(`for (const ${item} of ${expr}) {`); + body.push(...walkLines(item, checker.getTypeArguments(objects[0])[0], " ", depth + 1)); + body.push("}"); + } else if (kinds.has(null) && objects.length === 1) { + for (const property of checker.getPropertiesOfType(objects[0])) { + if (property.name === "loc") continue; + const fieldType = checker.getTypeOfSymbol(property); + const field = `${expr}.${property.name}`; + const lines = walkLines(field, fieldType, "", depth + 1); + if (lines.length === 0) continue; + if (property.name === "type") + body.push("if (!skipType) {", ...lines.map((l) => ` ${l}`), "}"); + else body.push(...lines); + } + } else throw new Error(`mixed object types at ${expr}: ${checker.typeToString(type)}`); + } + let lines = body; + if (strings.length > 0 && objects.length > 0) + lines = [ + `if (typeof ${expr} === "string") walker.string(${expr});`, + ...(body.length > 0 ? [`else {`, ...body.map((l) => ` ${l}`), "}"] : []), + ]; + else if (strings.length > 0) lines = [`walker.string(${expr});`]; + if (lines.length > 0 && optional && !(strings.length > 0 && objects.length > 0)) + lines = [ + `if (${expr} !== undefined && ${expr} !== null) {`, + ...lines.map((l) => ` ${l}`), + "}", + ]; + else if (lines.length > 0 && optional) + lines = [ + `if (${expr} !== undefined && ${expr} !== null) {`, + ...lines.map((l) => ` ${l}`), + "}", + ]; + return lines.map((line) => indent + line); +} + +lines.push( + "/** What a field walk reports (walkExprFields): each expression and", + " * statement node before its fields, and each string a field holds. */", + "export interface IrFieldWalker {", + " node(node: IrExpr | IrStmt): void;", + " string(value: string): void;", + "}", + "", +); +for (const name of ["IrExpr", "IrStmt", "IrType"]) { + const short = name.slice(2); + const nodeCall = name === "IrType" ? [] : [" walker.node(node);"]; + lines.push( + `/** Walks every field of \`node\` but \`loc\` (and \`type\` with \`skipType\`), and`, + " * every field of each object under it: the strings and nodes a recursive", + " * `for (const key in value)` walk with the same skips finds, read by name in", + " * the order ir.ts declares the fields. */", + `export function walk${short}Fields(node: ${name}, walker: IrFieldWalker, skipType: boolean): void {`, + ...nodeCall, + " switch (node.kind) {", + ); + const byKind = new Map(); + for (const member of members.get(name)) { + const kind = checker.getTypeOfSymbol(member.getProperty("kind")).value; + if (byKind.has(kind)) throw new Error(`${name} kind ${kind} declared twice`); + byKind.set(kind, member); + } + for (const [kind, member] of [...byKind].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) { + lines.push(` case ${JSON.stringify(kind)}: {`); + for (const property of member.getProperties()) { + if (property.name === "loc") continue; + const fieldType = checker.getTypeOfSymbol(property); + const field = `node.${property.name}`; + const walk = walkLines(field, fieldType, "", 0); + if (walk.length === 0) continue; + if (property.name === "type") + lines.push(" if (!skipType) {", ...walk.map((l) => ` ${l}`), " }"); + else lines.push(...walk.map((l) => ` ${l}`)); + } + lines.push(" return;", " }"); + } + lines.push(" }", "}", ""); +} +const text = lines.join("\n"); + +if (process.argv.includes("--check")) { + if (readFileSync(target, "utf8") !== text) + throw new Error("fields.generated.ts is stale; run node scripts/generate-ir-fields.mjs"); +} else writeFileSync(target, text); diff --git a/scripts/native-cli-assets.mts b/scripts/native-cli-assets.mts index 377dd1957..21734e938 100644 --- a/scripts/native-cli-assets.mts +++ b/scripts/native-cli-assets.mts @@ -2,28 +2,13 @@ import { spawnSync } from "node:child_process"; import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; import { basename, dirname, join, relative } from "node:path"; import type { NativeTargetSpec } from "../packages/compiler/src/backend/targets.js"; +import { copyPackage, llvmNotice } from "./native-cli-bundle.mjs"; 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 { - mkdirSync(destination, { recursive: true }); - for (const member of ["package.json", ...members]) { - cpSync(join(source, member), join(destination, member), { - recursive: true, - filter: (path) => ![".cache", "node_modules", ".DS_Store"].includes(basename(path)), - }); - } - for (const name of readdirSync(source)) { - if (/^(?:LICENSE|COPYING|NOTICE|SCRIPTC_LICENSE|THIRD_PARTY_NOTICES)(?:\..*)?$/.test(name)) { - cpSync(join(source, name), join(destination, name)); - } - } -} - /** Removes the symbol table and debug sections of a copied executable. * llvm-strip rewrites the linker-signed ad hoc code signature of a Mach-O * executable, which arm64 macOS requires to run it. Windows executables keep @@ -67,6 +52,10 @@ export function stageNativeCliAssets(options: { compilerVersion: string; /** Strip the copied tsc-ts executable. */ strip: boolean; + /** Leave the LLVM helper and host runtime pack out: an npm platform + * package depends on their packages, and the compiler resolves them + * beside itself. */ + platformPackages: boolean; }): Pick< NativeToolchainManifest, | "ts7" @@ -85,16 +74,18 @@ export function stageNativeCliAssets(options: { cpSync(join(repository, "LICENSE"), join(output, "LICENSE")); const helper = join(lib, "llvm"); const runtime = join(lib, target.runtimePackPackage.replace("@scriptc/", "")); - copyPackage( - join(repository, "packages", target.helper.packageName.replace("@scriptc/", "")), - helper, - ["bin"], - ); - copyPackage( - join(repository, "packages", target.runtimePackPackage.replace("@scriptc/", "")), - runtime, - ["artifacts", "runtime-pack.json"], - ); + if (!options.platformPackages) { + copyPackage( + join(repository, "packages", target.helper.packageName.replace("@scriptc/", "")), + helper, + ["bin"], + ); + copyPackage( + join(repository, "packages", target.runtimePackPackage.replace("@scriptc/", "")), + runtime, + ["artifacts", "runtime-pack.json"], + ); + } 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. @@ -146,7 +137,7 @@ export function stageNativeCliAssets(options: { `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.", + llvmNotice(options.platformPackages ? target.helper.packageName : undefined), "Runtime dependency licenses: lib/runtime-sources/vendor/*/LICENSE*.", "", ].join("\n"), @@ -154,9 +145,13 @@ 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) }], + ...(options.platformPackages + ? {} + : { + llvm_package: relative(bin, helper), + runtime_pack: relative(bin, runtime), + runtime_packs: [{ target: target.name, path: relative(bin, runtime) }], + }), runtime_sources: relative(bin, sources), declarations: relative(bin, declarations), wasi_node_runner: relative(bin, join(wasi, "cli/wasi-runner.js")), diff --git a/scripts/native-cli-bundle.mjs b/scripts/native-cli-bundle.mjs new file mode 100644 index 000000000..62762aadd --- /dev/null +++ b/scripts/native-cli-bundle.mjs @@ -0,0 +1,72 @@ +import { cpSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { basename, dirname, join, resolve } from "node:path"; + +/** Copy release payloads explicitly; development caches and build trees never ship. */ +export function copyPackage(source, destination, members) { + mkdirSync(destination, { recursive: true }); + for (const member of ["package.json", ...members]) { + cpSync(join(source, member), join(destination, member), { + recursive: true, + filter: (path) => ![".cache", "node_modules", ".DS_Store"].includes(basename(path)), + }); + } + for (const name of readdirSync(source)) { + if (/^(?:LICENSE|COPYING|NOTICE|SCRIPTC_LICENSE|THIRD_PARTY_NOTICES)(?:\..*)?$/.test(name)) { + cpSync(join(source, name), join(destination, name)); + } + } +} + +/** The LLVM helper and host runtime pack packages a native CLI package + * depends on. */ +export function platformPackagesOf(identity) { + const names = Object.keys(identity.dependencies ?? {}); + const helper = names.find((name) => name.startsWith("@scriptc/llvm-")); + const runtime = names.find((name) => name.startsWith("@scriptc/runtime-")); + if (helper === undefined || runtime === undefined) + throw new Error(`${identity.name} must depend on its LLVM helper and runtime pack packages`); + return { helper, runtime }; +} + +/** Copies the helper and runtime pack packages from `packages` (the + * workspace's packages directory) into a distribution's lib directory, as a + * standalone archive needs them, and returns the manifest paths, relative to + * the distribution's bin directory in the target's path style. */ +export function bundlePlatformPackages({ packages, distribution, helper, runtime, windows }) { + const short = (name) => name.replace("@scriptc/", ""); + copyPackage(join(packages, short(helper)), join(distribution, "lib", "llvm"), ["bin"]); + copyPackage(join(packages, short(runtime)), join(distribution, "lib", short(runtime)), [ + "artifacts", + "runtime-pack.json", + ]); + const path = (name) => ["..", "lib", name].join(windows ? "\\" : "/"); + return { llvm_package: path("llvm"), runtime_pack: path(short(runtime)) }; +} + +/** The THIRD_PARTY_NOTICES line for the LLVM helper, bundled or as a dependency. */ +export function llvmNotice(helper) { + return helper === undefined + ? "LLVM licensing and notices: lib/llvm/LICENSE and lib/llvm/THIRD_PARTY_NOTICES." + : `LLVM licensing and notices: LICENSE and THIRD_PARTY_NOTICES in the ${helper} package.`; +} + +/** Turns a distribution that bundles its LLVM helper and runtime pack into + * an npm platform package's: removes both from lib and from the manifest at + * `manifestPath`, as `build-native-cli.mts --platform-packages` builds it. */ +export function unbundlePlatformPackages(distribution, manifestPath, helper) { + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + for (const path of [manifest.llvm_package, manifest.runtime_pack]) + rmSync(resolve(dirname(manifestPath), path.replaceAll("\\", "/")), { + recursive: true, + force: true, + }); + delete manifest.llvm_package; + delete manifest.runtime_pack; + delete manifest.runtime_packs; + writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n"); + const notices = join(distribution, "THIRD_PARTY_NOTICES"); + writeFileSync( + notices, + readFileSync(notices, "utf8").replace(llvmNotice(undefined), llvmNotice(helper)), + ); +} diff --git a/scripts/package-native-cli.mjs b/scripts/package-native-cli.mjs index 6d1c05991..9b8ea5880 100644 --- a/scripts/package-native-cli.mjs +++ b/scripts/package-native-cli.mjs @@ -13,6 +13,7 @@ import { import { tmpdir } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { bundlePlatformPackages, llvmNotice, platformPackagesOf } from "./native-cli-bundle.mjs"; const repository = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const packages = resolve(process.argv[2] ?? join(repository, "packages")); @@ -39,6 +40,27 @@ for (const name of readdirSync(packages) const binary = join(bin, name.includes("win32-") ? "scriptc.exe" : "scriptc"); const manifest = JSON.parse(readFileSync(binary + ".json", "utf8")); const hostPath = (value) => (name.includes("win32-") ? value.replaceAll("\\", "/") : value); + // The standalone archive bundles the LLVM helper and runtime pack that + // the npm platform package depends on. + if (manifest.llvm_package === undefined) { + const { helper, runtime } = platformPackagesOf(identity); + const paths = bundlePlatformPackages({ + packages, + distribution, + helper, + runtime, + windows: name.includes("win32-"), + }); + manifest.llvm_package = paths.llvm_package; + manifest.runtime_pack = paths.runtime_pack; + manifest.runtime_packs = [{ target: manifest.target, path: paths.runtime_pack }]; + writeFileSync(binary + ".json", JSON.stringify(manifest, null, 2) + "\n"); + const notices = join(distribution, "THIRD_PARTY_NOTICES"); + writeFileSync( + notices, + readFileSync(notices, "utf8").replace(llvmNotice(helper), llvmNotice(undefined)), + ); + } // GitHub artifact transport drops executable modes. Restore the native // tools before validating and creating the standalone archive. for (const path of [ diff --git a/scripts/smoke-native-install.mjs b/scripts/smoke-native-install.mjs index 2325f658b..06ff9e3f0 100644 --- a/scripts/smoke-native-install.mjs +++ b/scripts/smoke-native-install.mjs @@ -54,7 +54,27 @@ function bytes(directory) { return size; } try { - const platformTarball = pack(platform); + // A platform package depends on its LLVM helper and runtime pack packages. + // Pack them from this checkout, and pack the platform package with the + // exact versions pnpm would publish for its workspace dependencies. + const dependencies = Object.keys(identity.dependencies ?? {}); + let packed = platform; + if (dependencies.length > 0) { + packed = join(root, "platform"); + mkdirSync(packed); + cpSync(join(platform, "dist"), join(packed, "dist"), { recursive: true }); + writeFileSync( + join(packed, "package.json"), + JSON.stringify({ + ...identity, + dependencies: Object.fromEntries(dependencies.map((name) => [name, identity.version])), + }), + ); + } + const platformTarball = pack(packed); + const dependencyTarballs = dependencies.map((name) => + pack(join(repository, "packages", name.replace("@scriptc/", ""))), + ); const wrapper = join(root, "wrapper"); mkdirSync(wrapper); cpSync(join(repository, "packages/cli/scripts"), join(wrapper, "scripts"), { recursive: true }); @@ -77,7 +97,11 @@ try { const tarball = pack(wrapper); const project = join(root, "project"); mkdirSync(project); - command(npm, ["install", "--offline", "--no-audit", "--no-fund", tarball], project); + command( + npm, + ["install", "--offline", "--no-audit", "--no-fund", tarball, ...dependencyTarballs], + project, + ); const executable = join(project, "node_modules/scriptc/bin/scriptc.exe"); assert.equal( command(executable, ["--version"], project, { ...env, PATH: "" }).trim(), @@ -99,8 +123,11 @@ try { ); command(executable, ["build", entry, "-o", binary], project, compileEnv); assert.equal(command(binary, [], project, { PATH: "" }), "true true true\n"); - const installed = readdirSync(join(project, "node_modules/@scriptc")); - assert.deepEqual(installed, [identity.name.slice("@scriptc/".length)]); + const installed = readdirSync(join(project, "node_modules/@scriptc")).sort(); + assert.deepEqual( + installed, + [identity.name, ...dependencies].map((name) => name.slice("@scriptc/".length)).sort(), + ); console.log( JSON.stringify({ package: identity.name, diff --git a/scripts/verify-native-cli.mjs b/scripts/verify-native-cli.mjs index 6a18f9e70..fc94927be 100644 --- a/scripts/verify-native-cli.mjs +++ b/scripts/verify-native-cli.mjs @@ -1,5 +1,6 @@ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs"; +import { createRequire } from "node:module"; import { dirname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path"; import { verifyLinuxAbi } from "./verify-linux-abi.mjs"; @@ -26,11 +27,30 @@ const within = (path) => { if (child === ".." || child.startsWith(".." + sep) || isAbsolute(child)) throw new Error(`distribution references external asset: ${path}`); }; +// An npm platform package depends on its LLVM helper and runtime pack +// packages at its own version instead of bundling them. +const platformPackages = toolchain.llvm_package === undefined; +const dependencies = identity.dependencies ?? {}; +const dependency = (prefix) => { + const name = Object.keys(dependencies).find((key) => key.startsWith(prefix)); + if (name === undefined || ![identity.version, "workspace:*"].includes(dependencies[name])) + throw new Error( + `${identity.name} must depend on its ${prefix}* package at ${identity.version}`, + ); + return dirname(createRequire(join(root, "package.json")).resolve(`${name}/package.json`)); +}; +if (platformPackages && (toolchain.runtime_pack !== undefined || toolchain.runtime_packs)) + throw new Error("a platform package's runtime pack is its dependency"); +const helperRoot = platformPackages + ? dependency("@scriptc/llvm-") + : resolve(dirname(binary), hostPath(toolchain.llvm_package)); +const runtimeRoot = platformPackages + ? dependency("@scriptc/runtime-") + : resolve(dirname(binary), hostPath(toolchain.runtime_pack)); for (const name of [ "ts7", "tsc_ts", - "llvm_package", - "runtime_pack", + ...(platformPackages ? [] : ["llvm_package", "runtime_pack"]), "runtime_sources", "declarations", "comptime", @@ -48,14 +68,11 @@ if (toolchain.lib_diagnostics !== undefined) { throw new Error("native toolchain needs a relative lib_diagnostics"); within(resolve(dirname(binary), hostPath(value))); } -const pack = JSON.parse( - readFileSync( - resolve(dirname(binary), hostPath(toolchain.runtime_pack), "runtime-pack.json"), - "utf8", - ), -); +const pack = JSON.parse(readFileSync(join(runtimeRoot, "runtime-pack.json"), "utf8")); if (pack.version !== identity.version || pack.target.name !== toolchain.target) throw new Error("native compiler runtime identity mismatch"); +if (JSON.parse(readFileSync(join(helperRoot, "package.json"), "utf8")).version !== identity.version) + throw new Error("native compiler LLVM helper version mismatch"); const seen = new Set(); for (const runtime of toolchain.runtime_packs ?? []) { if (seen.has(runtime.target)) throw new Error(`duplicate runtime target: ${runtime.target}`); @@ -91,9 +108,7 @@ if (process.argv.includes("--run")) { ]) { verifyLinuxAbi(executable, { staticOnly: target.endsWith("-musl") }); } - verifyLinuxAbi(resolve(dirname(binary), toolchain.llvm_package, "bin/scriptc-llvm-codegen"), { - staticOnly: true, - }); + verifyLinuxAbi(join(helperRoot, "bin/scriptc-llvm-codegen"), { staticOnly: true }); } const version = execFileSync(binary, ["--version"], { encoding: "utf8", diff --git a/scripts/zero-cost-gate.mjs b/scripts/zero-cost-gate.mjs index 891c8a021..053c2f0d0 100644 --- a/scripts/zero-cost-gate.mjs +++ b/scripts/zero-cost-gate.mjs @@ -80,7 +80,8 @@ runtime pack). Both compile the same program files, taken from --programs --set picks the good-code set: bench (benchmarks/runtime workloads and the benchmarks/builds applications), self (packages/compiler/src/native/cli.ts, -LLVM only), corpus (tests/corpus TypeScript programs; --corpus=all adds +LLVM only; the candidate's own compiler source must also build with the +candidate), corpus (tests/corpus TypeScript programs; --corpus=all adds JavaScript entries). --quick is --set=bench --timing-runs=0, for CI. --shard=i/n keeps every n-th corpus program; --only filters program ids. @@ -634,6 +635,30 @@ if (timingRuns > 0) { } } +// The self program is the baseline's compiler source (the programs come +// from one checkout), so a change that adds compiler code the self-hosted +// frontend cannot lower would pass unnoticed. The candidate's own compiler +// source must still build with the candidate (LLVM only, not compared). +if (sets.has("self") && programsRoot !== trees.candidate) { + const entry = join(trees.candidate, "packages/compiler/src/native/cli.ts"); + const result = await buildProgram( + { id: "self/candidate-source", entry, kind: "source" }, + "candidate", + ); + report.candidateSelf = result.ok + ? { ok: true, buildMs: result.ms } + : { ok: false, error: result.error }; + console.log( + result.ok + ? `self candidate compiler source builds with the candidate (${fmtMs(result.ms)})` + : `self candidate compiler source FAILS to build with the candidate`, + ); + if (!result.ok) { + failed = true; + report.failures.push(`self/candidate-source: ${result.error.split("\n")[0]}`); + } +} + const counts = {}; for (const p of report.programs) counts[p.verdict] = (counts[p.verdict] ?? 0) + 1; if (!report.programs.some((p) => ["identical", "allowed", "differs"].includes(p.verdict))) { diff --git a/tests/corpus/8326-never-promise-instanceof.ts b/tests/corpus/8326-never-promise-instanceof.ts new file mode 100644 index 000000000..6e01e3510 --- /dev/null +++ b/tests/corpus/8326-never-promise-instanceof.ts @@ -0,0 +1,26 @@ +// `instanceof Promise` on values that are never promises (records, arrays, +// dates, collections, regular expressions, byte arrays, null, and unions of +// them) is false, after the operand is evaluated. +let evaluated = 0; +function next(): { v: number } { + evaluated++; + return { v: evaluated }; +} + +const record = { v: 1 }; +console.log("record", record instanceof Promise); +console.log("call", next() instanceof Promise, evaluated); +console.log("array", [1, 2] instanceof Promise); +console.log("date", new Date(0) instanceof Promise); +console.log("map", new Map() instanceof Promise); +console.log("set", new Set() instanceof Promise); +console.log("regex", /a/ instanceof Promise); +console.log("bytes", new Uint8Array(2) instanceof Promise); + +function pick(i: number): { v: number } | number[] | null { + return i === 0 ? { v: 0 } : i === 1 ? [i] : null; +} +for (let i = 0; i < 3; i++) console.log("union", i, pick(i) instanceof Promise); + +const promised: unknown = Promise.resolve(1); +console.log("promise", promised instanceof Promise); diff --git a/tests/corpus/8400-iterative-teardown.ts b/tests/corpus/8400-iterative-teardown.ts new file mode 100644 index 000000000..69ec13824 --- /dev/null +++ b/tests/corpus/8400-iterative-teardown.ts @@ -0,0 +1,96 @@ +// Teardown of self-recursive records and classes: shared and immortal +// children, deep chains, weak-collection keys, and trees rebuilt from freed +// blocks. + +interface TreeNode { + left: TreeNode | null; + right: TreeNode | null; +} + +class ListNode { + next: ListNode | null; + value: number; + constructor(next: ListNode | null, value: number) { + this.next = next; + this.value = value; + } +} + +class Pair { + a: Pair | null; + b: Pair | null; + constructor(a: Pair | null, b: Pair | null) { + this.a = a; + this.b = b; + } +} + +function build(depth: number): TreeNode { + return depth > 0 ? { left: build(depth - 1), right: build(depth - 1) } : { left: null, right: null }; +} + +function count(node: TreeNode | null): number { + if (node === null) return 0; + return 1 + count(node.left) + count(node.right); +} + +// Trees torn down and rebuilt from their freed blocks. +let total = 0; +for (let depth = 0; depth <= 12; depth++) { + for (let i = 0; i < 4; i++) total += count(build(depth)); +} +console.log("trees", total); + +// A child shared by two parents survives the first parent's teardown. +const shared = build(3); +let left: TreeNode | null = { left: shared, right: build(2) }; +let right: TreeNode | null = { left: build(1), right: shared }; +left = null; +console.log("shared after first parent", count(shared), count(right)); +right = null; +console.log("shared after second parent", count(shared)); + +// A node held from outside keeps its subtree when the tree around it dies. +let outer: TreeNode | null = build(6); +const kept = outer.left!.right!; +outer = null; +console.log("kept subtree", count(kept)); + +// A chain far deeper than any call stack. +let head: ListNode | null = null; +for (let i = 0; i < 2000000; i++) head = new ListNode(head, i); +let sum = 0; +for (let n = head; n !== null; n = n.next) sum += n.value % 7; +head = null; +console.log("chain", sum); + +// A list whose middle node is still referenced keeps the tail from it. +let list: ListNode | null = null; +let middle: ListNode | null = null; +for (let i = 0; i < 1000; i++) { + list = new ListNode(list, i); + if (i === 500) middle = list; +} +list = null; +let tail = 0; +for (let n = middle; n !== null; n = n.next) tail++; +console.log("tail from middle", tail); +middle = null; + +// Nodes used as WeakMap keys: their entries go away when the tree dies. +const weak = new WeakMap(); +let root: Pair | null = new Pair(new Pair(null, null), new Pair(new Pair(null, null), null)); +const probe = root.b!; +weak.set(root, 1); +weak.set(root.a!, 2); +weak.set(probe, 3); +root = null; +console.log("weak probe", weak.get(probe), weak.has(probe)); + +// Immortal leaves shared by many nodes. +const leaf: TreeNode = { left: null, right: null }; +let wide: TreeNode | null = { left: leaf, right: leaf }; +for (let i = 0; i < 100; i++) wide = { left: wide, right: leaf }; +console.log("wide", count(wide)); +wide = null; +console.log("leaf", count(leaf)); diff --git a/tests/corpus/array-prototype-call-native-arrays.ts b/tests/corpus/array-prototype-call-native-arrays.ts new file mode 100644 index 000000000..d5bfec507 --- /dev/null +++ b/tests/corpus/array-prototype-call-native-arrays.ts @@ -0,0 +1,65 @@ +// Array.prototype methods called with .call on native arrays run the arrays' own +// methods, which mutate the array in place: copyWithin, fill, reverse, +// sort, push, pop, splice, and the read-only join, indexOf and includes. +const optional: (number | undefined)[] = [1, 2, 3, 4]; +Array.prototype.copyWithin.call(optional, 0, 2, 4); +console.log(optional.join(",")); +const nums: number[] = [5, 1, 4, 2, 3]; +Array.prototype.fill.call(nums, 0, 0, 1); +console.log(nums.join(",")); +console.log(Array.prototype.reverse.call(nums) === nums, nums.join(",")); +Array.prototype.sort.call(nums, (a: number, b: number) => a - b); +console.log(nums.join(",")); +console.log(Array.prototype.push.call(nums, 9, 10), nums.join(",")); +console.log(Array.prototype.pop.call(nums), nums.length); +console.log(Array.prototype.splice.call(nums, 1, 2).join(","), nums.join(",")); +console.log(Array.prototype.join.call(nums, "-"), Array.prototype.indexOf.call(nums, 4)); +const words: string[] = ["c", "a", "b"]; +Array.prototype.sort.call(words); +console.log(words.join(","), Array.prototype.includes.call(words, "a")); +const records: { id: number }[] = [{ id: 1 }, { id: 2 }, { id: 3 }]; +Array.prototype.copyWithin.call(records, 1, 0, 1); +console.log(records.map((r) => r.id).join(","), records[0] === records[1]); + +// The callback methods bind `this` to their thisArg, for native arrays +// through .call and for dynamic arrays called directly. +type Scale = { k: number; seen: number[] }; +const scale: Scale = { k: 10, seen: [] }; +const values: number[] = [1, 2, 3, 4]; +Array.prototype.forEach.call(values, function (this: Scale, v: number) { + this.seen.push(v); +}, scale); +console.log(scale.seen.join(",")); +console.log(Array.prototype.map.call(values, function (this: Scale, v: number) { + return v * this.k; +}, scale).join(",")); +console.log(Array.prototype.filter.call(values, function (this: Scale, v: number) { + return v * this.k > 15; +}, scale).join(",")); +console.log( + Array.prototype.some.call(values, function (this: Scale, v: number) { return v === this.k / 5; }, scale), + Array.prototype.every.call(values, function (this: Scale, v: number) { return v < this.k; }, scale), + Array.prototype.find.call(values, function (this: Scale, v: number) { return v * this.k === 30; }, scale), + Array.prototype.findIndex.call(values, function (this: Scale, v: number) { return v * this.k === 30; }, scale), + Array.prototype.findLast.call(values, function (this: Scale, v: number) { return v * this.k < 30; }, scale), + Array.prototype.findLastIndex.call(values, function (this: Scale, v: number) { return v * this.k < 30; }, scale), +); +console.log(Array.prototype.flatMap.call(values, function (this: unknown, v: number) { + return [v, v * (this as Scale).k]; +}, scale).join(",")); +const parsed: any = JSON.parse("[1, 2, 3]"); +const sum = { total: 0 }; +parsed.forEach(function (this: { total: number }, v: number) { + this.total += v; +}, sum); +console.log(sum.total, parsed.map(function (this: Scale, v: number) { return v * this.k; }, scale).join(",")); +class Counter { + total = 0; + addAll(items: number[]): number { + Array.prototype.forEach.call(items, (v: number) => { + this.total += v; + }); + return this.total; + } +} +console.log(new Counter().addAll(values)); diff --git a/tests/corpus/net-unix-socket-connect-default-import.mjs b/tests/corpus/net-unix-socket-connect-default-import.mjs new file mode 100644 index 000000000..7e58c18e2 --- /dev/null +++ b/tests/corpus/net-unix-socket-connect-default-import.mjs @@ -0,0 +1,22 @@ +// net.createConnection(path) through the default import of node:net, as a +// JavaScript module reads it: a client of a Unix domain socket server. +import net from "node:net"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-unix-default-")); +const socketPath = path.join(dir, "server.sock"); +const server = net.createServer((socket) => socket.end("hello")); +server.listen(socketPath, () => { + const sock = net.createConnection(socketPath); + sock.once("connect", () => console.log("connected")); + sock.on("data", (chunk) => console.log("data", String(chunk))); + sock.once("error", (e) => console.log("error", String(e))); + sock.on("close", () => { + server.close(() => { + fs.rmSync(dir, { recursive: true, force: true }); + console.log("closed"); + }); + }); +}); diff --git a/tests/corpus/net-unix-socket-connect.ts b/tests/corpus/net-unix-socket-connect.ts new file mode 100644 index 000000000..77832d60d --- /dev/null +++ b/tests/corpus/net-unix-socket-connect.ts @@ -0,0 +1,69 @@ +// net.createConnection and net.connect to a Unix domain socket: a path +// argument, the `path` option, a connect listener, data both ways, the +// error for a path nothing listens on, and a numeric string, which Node +// treats as a localhost port. +import * as net from "node:net"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-unix-connect-")); +const socketPath = path.join(dir, "server.sock"); +const missingPath = path.join(dir, "missing.sock"); + +const server = net.createServer((socket) => { + socket.on("data", (chunk) => { + socket.write(`echo:${chunk.toString()}`); + }); +}); + +function exchange(label: string, socket: net.Socket, message: string): Promise { + return new Promise((resolve) => { + socket.on("data", (chunk) => { + console.log(label, chunk.toString()); + socket.end(); + }); + socket.on("close", () => resolve()); + socket.write(message); + }); +} + +server.listen(socketPath, async () => { + console.log("listening"); + const first = net.createConnection(socketPath); + first.once("connect", () => console.log("first connected")); + await exchange("first", first, "one"); + + let listenerCalls = 0; + const second = net.connect(socketPath, () => { + listenerCalls++; + }); + await exchange("second", second, "two"); + console.log("second listener calls", listenerCalls); + + const third = net.createConnection({ path: socketPath }); + await exchange("third", third, "three"); + + await new Promise((resolve) => { + const missing = net.createConnection(missingPath); + missing.once("connect", () => console.log("missing connected")); + missing.once("error", (error: NodeJS.ErrnoException) => { + console.log("missing", error.code, error.message === `connect ENOENT ${missingPath}`); + resolve(); + }); + }); + + server.close(() => { + const tcp = net.createServer((socket) => socket.end("tcp")); + tcp.listen(0, "localhost", () => { + const { port } = tcp.address() as net.AddressInfo; + const client = net.createConnection(String(port)); + client.on("data", (chunk) => console.log("numeric string", chunk.toString())); + client.on("close", () => { + tcp.close(); + fs.rmSync(dir, { recursive: true, force: true }); + console.log("done"); + }); + }); + }); +}); diff --git a/tests/corpus/typeof-ambient-undefined-names.ts b/tests/corpus/typeof-ambient-undefined-names.ts new file mode 100644 index 000000000..94713319e --- /dev/null +++ b/tests/corpus/typeof-ambient-undefined-names.ts @@ -0,0 +1,21 @@ +// typeof on names the program declares (`declare const`, `declare function`) +// but nothing defines at run time answers "undefined"; reading them throws +// ReferenceError. +declare const Bun: { randomUUIDv7: () => string } | undefined; +declare const Deno: { version: { deno: string } }; +declare function webkitRequestAnimationFrame(callback: () => void): number; +console.log("start"); +console.log(typeof Bun, typeof Deno, typeof webkitRequestAnimationFrame); +console.log(typeof Bun !== "undefined" ? Bun?.randomUUIDv7() : "no Bun"); +const runtime = typeof Deno === "object" ? "deno" : typeof Bun === "object" ? "bun" : "node"; +console.log(runtime); +try { + console.log(Bun); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} +try { + console.log(typeof Deno.version); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} diff --git a/tests/diagnostics/maybe-promise-fences.ts b/tests/diagnostics/maybe-promise-fences.ts index 1dcccaf08..e6fdf98fa 100644 --- a/tests/diagnostics/maybe-promise-fences.ts +++ b/tests/diagnostics/maybe-promise-fences.ts @@ -26,7 +26,3 @@ console.log(maybe(true)); // must be that arm's own promise type. const widened = Promise.resolve(maybe(false)); console.log(typeof widened); - -// `instanceof Promise` on a value that is never a promise. -const record = { v: 1 }; -console.log(record instanceof Promise); diff --git a/tests/fixtures/npm/cases/shape-overlay-facts/main.ts b/tests/fixtures/npm/cases/shape-overlay-facts/main.ts new file mode 100644 index 000000000..2c83bcb85 --- /dev/null +++ b/tests/fixtures/npm/cases/shape-overlay-facts/main.ts @@ -0,0 +1,12 @@ +// The shape overlay's facts: results that include undefined, literals that +// hold comment-like text, a var declared in a branch no call takes, and +// a var declared in two such branches. +import { banner, hoisted, lookup, lookupOrReturn, shared } from "shape-facts"; + +console.log(lookup("b")); +console.log(lookup("z")); +console.log(lookupOrReturn("a")); +console.log(lookupOrReturn("y")); +console.log(banner(7)); +console.log(hoisted("note:")); +console.log(shared("level:")); diff --git a/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/index.d.ts b/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/index.d.ts new file mode 100644 index 000000000..12ff3f2d8 --- /dev/null +++ b/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/index.d.ts @@ -0,0 +1,5 @@ +export declare function lookup(key: string): string; +export declare function lookupOrReturn(key: string): string; +export declare function banner(n: number): string; +export declare function hoisted(label: string): string; +export declare function shared(label: string): string; diff --git a/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/index.js b/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/index.js new file mode 100644 index 000000000..8cfa73620 --- /dev/null +++ b/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/index.js @@ -0,0 +1,70 @@ +// Functions whose results include the undefined of a fall-through or a +// bare return, comment-like text in literals, a branch no call takes +// that declares a var read outside it, and two such branches that declare +// the same var. + +const entries = [ + { key: "a", value: 1 }, + { key: "b", value: 2 }, +]; + +function find(key) { + for (const entry of entries) { + if (entry.key === key) return entry; + } +} + +function findOrReturn(key) { + for (const entry of entries) if (entry.key === key) return entry; + return; +} + +export function lookup(key) { + const hit = find(key); + if (hit) { + return "found " + hit.key + "=" + hit.value; + } else { + return "missing " + key; + } +} + +export function lookupOrReturn(key) { + const hit = findOrReturn(key); + if (hit) { + return "found " + hit.key + "=" + hit.value; + } else { + return "missing " + key; + } +} + +export function banner(n) { + const quoted = "/** @type {number} */"; + const template = `/** @returns {string} ${n} */`; + const pattern = /\/\*\* @param \*\//; + return quoted + " " + template + " " + pattern.source; +} + +function noteOf(extra) { + if (extra) { + var note = "set"; + } + return typeof note + " " + String(note); +} + +export function hoisted(label) { + return label + " " + noteOf(undefined); +} + +function levelOf(warn, fail) { + if (warn) { + var level = "warn"; + } + if (fail) { + var level = "fail"; + } + return typeof level + " " + String(level); +} + +export function shared(label) { + return label + " " + levelOf(undefined, undefined); +} diff --git a/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/package.json b/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/package.json new file mode 100644 index 000000000..317269e91 --- /dev/null +++ b/tests/fixtures/npm/cases/shape-overlay-facts/node_modules/shape-facts/package.json @@ -0,0 +1 @@ +{ "name": "shape-facts", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/shape-overlay-facts/package.json b/tests/fixtures/npm/cases/shape-overlay-facts/package.json new file mode 100644 index 000000000..d15818a37 --- /dev/null +++ b/tests/fixtures/npm/cases/shape-overlay-facts/package.json @@ -0,0 +1 @@ +{ "name": "shape-overlay-facts-case", "private": true, "type": "module" } diff --git a/tests/harness/__snapshots__/maybe-promise-fences.ts.txt b/tests/harness/__snapshots__/maybe-promise-fences.ts.txt index 159e88f76..ab325aaaa 100644 --- a/tests/harness/__snapshots__/maybe-promise-fences.ts.txt +++ b/tests/harness/__snapshots__/maybe-promise-fences.ts.txt @@ -48,11 +48,4 @@ maybe-promise-fences.ts:27:50 - error SC2020: 'Promise.resolve over 'Promise { + const f = fixture(windows); + const helper = `@scriptc/llvm-${f.platform}`; + const runtime = `@scriptc/runtime-${f.platform}`; + const manifest: Record = { ...f.manifest }; + delete manifest.llvm_package; + delete manifest.runtime_pack; + delete manifest.runtime_packs; + writeFileSync(f.manifestPath, JSON.stringify(manifest)); + const notices = join(f.cli, "dist/THIRD_PARTY_NOTICES"); + writeFileSync( + notices, + `LLVM licensing and notices: LICENSE and THIRD_PARTY_NOTICES in the ${helper} package.\n`, + ); + rmSync(join(f.cli, "dist/lib/llvm"), { recursive: true }); + rmSync(join(f.cli, "dist/lib/runtime"), { recursive: true }); + const helperPackage = join(f.packages, `llvm-${f.platform}`); + const codegen = windows ? "scriptc-llvm-codegen.exe" : "scriptc-llvm-codegen"; + mkdirSync(join(helperPackage, "bin"), { recursive: true }); + for (const [name, directory] of [ + [helper, helperPackage], + [runtime, join(f.packages, `runtime-${f.platform}`)], + ] as const) { + writeFileSync(join(directory, "package.json"), JSON.stringify({ name, version: "1.2.3" })); + // pnpm links workspace dependencies into the package's node_modules. + mkdirSync(join(f.cli, "node_modules/@scriptc"), { recursive: true }); + symlinkSync(directory, join(f.cli, "node_modules", name), "junction"); + } + writeFileSync(join(helperPackage, "bin", codegen), "helper"); + const identity = { name: `@scriptc/cli-${f.platform}`, version: "1.2.3" }; + writeFileSync(join(f.cli, "package.json"), JSON.stringify(identity)); + const missing = run("verify-native-cli.mjs", f.cli); + expect(missing.status).toBe(1); + expect(missing.stderr).toContain(`must depend on its @scriptc/llvm-* package at 1.2.3`); + writeFileSync( + join(f.cli, "package.json"), + JSON.stringify({ + ...identity, + dependencies: { [helper]: "1.2.3", [runtime]: "workspace:*" }, + }), + ); + const verified = run("verify-native-cli.mjs", f.cli); + expect(verified.status, verified.stderr).toBe(0); + const output = join(f.directory, "archives"); + const packed = run("package-native-cli.mjs", f.packages, output); + expect(packed.status, packed.stderr).toBe(0); + const unpacked = join(f.directory, "unpacked"); + mkdirSync(unpacked); + const archive = join(output, `scriptc-1.2.3-${f.platform}.tar.gz`); + const extracted = spawnSync("tar", ["-xzf", archive, "-C", unpacked], { encoding: "utf8" }); + expect(extracted.status, extracted.stderr).toBe(0); + const bundled = JSON.parse(readFileSync(join(unpacked, "bin", f.executable + ".json"), "utf8")); + const slash = windows ? "\\" : "/"; + expect(bundled.llvm_package).toBe(["..", "lib", "llvm"].join(slash)); + expect(bundled.runtime_pack).toBe(["..", "lib", `runtime-${f.platform}`].join(slash)); + expect(bundled.runtime_packs).toEqual([ + { target: f.manifest.target, path: bundled.runtime_pack }, + ]); + expect(readFileSync(join(unpacked, "lib/llvm/bin", codegen), "utf8")).toBe("helper"); + expect( + JSON.parse( + readFileSync(join(unpacked, `lib/runtime-${f.platform}/runtime-pack.json`), "utf8"), + ).target.name, + ).toBe(f.manifest.target); + expect(readFileSync(join(unpacked, "THIRD_PARTY_NOTICES"), "utf8")).toBe( + "LLVM licensing and notices: lib/llvm/LICENSE and lib/llvm/THIRD_PARTY_NOTICES.\n", + ); + // The npm package itself is unchanged. + expect(JSON.parse(readFileSync(f.manifestPath, "utf8"))).toEqual(manifest); + }, +); + test.each([ "C:\\outside\\tsc.exe", "C:tsc.exe", diff --git a/tests/harness/self-hosting-native-driver.test.ts b/tests/harness/self-hosting-native-driver.test.ts index 8cfd16eea..60969d0b2 100644 --- a/tests/harness/self-hosting-native-driver.test.ts +++ b/tests/harness/self-hosting-native-driver.test.ts @@ -26,6 +26,7 @@ import { } from "../../scripts/ci-native-bootstrap.mjs"; import { withoutFirstLine } from "./large-artifacts.js"; import { bootstrapStep } from "./self-hosting-timing.js"; +import { unbundlePlatformPackages } from "../../scripts/native-cli-bundle.mjs"; const root = join(import.meta.dirname, "../.."); const exec = promisify(execFile); @@ -105,6 +106,21 @@ test( join(packageDirectory, "package.json"), ); cpSync(distribution, join(packageDirectory, "dist"), { recursive: true }); + // The npm package depends on its LLVM helper and runtime pack. Link + // them into its node_modules, as pnpm links a workspace package's + // dependencies, so the package checks resolve them. + unbundlePlatformPackages( + join(packageDirectory, "dist"), + join(packageDirectory, "dist/bin/scriptc.json"), + "@scriptc/llvm-linux-x64-gnu", + ); + mkdirSync(join(packageDirectory, "node_modules/@scriptc"), { recursive: true }); + for (const name of ["llvm-linux-x64-gnu", "runtime-linux-x64-gnu"]) + symlinkSync( + join(root, "packages", name), + join(packageDirectory, "node_modules/@scriptc", name), + "dir", + ); writeFileSync(packageDirectory + ".ready", "ready\n"); } // All compiler assets must survive moving the complete distribution. diff --git a/tests/harness/shape-overlay.test.ts b/tests/harness/shape-overlay.test.ts new file mode 100644 index 000000000..6c843c7cb --- /dev/null +++ b/tests/harness/shape-overlay.test.ts @@ -0,0 +1,126 @@ +/* The shape overlay (shape-inference/overlay.ts): with SCRIPTC_SHAPE_OVERLAY=1 + * the zod3-boundary npm case compiles zod's JavaScript with analysis-proven JSDoc + * types, reports what it typed and checks (SC6103), and prints what Node + * prints; the shapeOverlay: false option (the CLI's --no-shape-overlay) + * turns it off. */ +import { execFile } from "node:child_process"; +import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const entry = join(repoRoot, "tests/fixtures/npm/cases/zod3-boundary/main.ts"); +const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/shape-overlay"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +async function withOverlay(run: () => Promise): Promise { + const saved = process.env["SCRIPTC_SHAPE_OVERLAY"]; + process.env["SCRIPTC_SHAPE_OVERLAY"] = "1"; + try { + return await run(); + } finally { + if (saved === undefined) delete process.env["SCRIPTC_SHAPE_OVERLAY"]; + else process.env["SCRIPTC_SHAPE_OVERLAY"] = saved; + } +} + +test( + "the overlay types zod's JavaScript and keeps Node's output", + { timeout: 900_000 }, + async () => { + mkdirSync(outDir, { recursive: true }); + const result = await withOverlay(() => + compile(entry, { outPath: join(outDir, "on"), outDir, sanitize }), + ); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const report = (result.warnings ?? []).find((w) => w.code === "SC6103"); + expect(report?.message).toMatch( + /shape inference typed \d+ declarations in \d+ files? of 1 package \(zod\)/, + ); + const run = execFileAsync(result.binaryPath, [], { cwd: dirname(entry), encoding: "utf8" }); + run.child.stdin?.end(); + const node = await execFileAsync(process.execPath, [entry], { + cwd: dirname(entry), + encoding: "utf8", + }); + expect((await run).stdout).toBe(node.stdout); + }, +); + +test("shapeOverlay: false compiles without the overlay", { timeout: 900_000 }, async () => { + mkdirSync(outDir, { recursive: true }); + const result = await withOverlay(() => + compile(entry, { outPath: join(outDir, "off"), outDir, sanitize, shapeOverlay: false }), + ); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + expect((result.warnings ?? []).some((w) => w.code === "SC6103")).toBe(false); +}); + +// shape-facts: results that include the undefined of a fall-through or a bare +// return keep the caller's falsy branch, comment-like text in literals keeps +// its value, and a replaced branch keeps its var declared, also when two +// replaced branches declare the same var. +const factsEntry = join(repoRoot, "tests/fixtures/npm/cases/shape-overlay-facts/main.ts"); + +test( + "the overlay keeps undefined results, literal text and hoisted vars", + { timeout: 900_000 }, + async () => { + mkdirSync(outDir, { recursive: true }); + const result = await withOverlay(() => + compile(factsEntry, { outPath: join(outDir, "facts"), outDir, sanitize }), + ); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const report = (result.warnings ?? []).find((w) => w.code === "SC6103"); + expect(report?.message).toMatch(/of 1 package \(shape-facts\)/); + expect(report?.message).toMatch(/replaces 3 branches it proved unreachable/); + const native = await execFileAsync(result.binaryPath, [], { + cwd: dirname(factsEntry), + encoding: "utf8", + }); + const node = await execFileAsync(process.execPath, [factsEntry], { + cwd: dirname(factsEntry), + encoding: "utf8", + }); + expect(native.stdout).toBe(node.stdout); + }, +); + +// `"scriptc": { "packages": { "": { "shapes": false } } }` in the +// project's package.json compiles that package without the overlay. +test( + "a package with shapes: false compiles without the overlay", + { timeout: 900_000 }, + async () => { + const project = join(outDir, "facts-shapeless"); + rmSync(project, { recursive: true, force: true }); + cpSync(dirname(factsEntry), project, { recursive: true }); + writeFileSync( + join(project, "package.json"), + JSON.stringify({ + name: "shape-overlay-facts-case", + private: true, + type: "module", + scriptc: { packages: { "shape-facts": { shapes: false } } }, + }), + ); + const result = await withOverlay(() => + compile(join(project, "main.ts"), { + outPath: join(outDir, "facts-shapeless-bin"), + outDir, + sanitize, + }), + ); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + expect((result.warnings ?? []).some((w) => w.code === "SC6103" || w.code === "SC8004")).toBe( + false, + ); + }, +);