mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-30 07:14:39 +08:00
Merge remote-tracking branch 'origin/ni/1-libnode-loader' into mac/ni1
This commit is contained in:
@@ -68,6 +68,16 @@ Test location follows scope:
|
||||
Keep existing tests in place unless a change already touches their organization;
|
||||
new tests should follow this convention.
|
||||
|
||||
## Numbered registries
|
||||
|
||||
Parallel changes must never pick the same number by hand. Each registry has one source of truth and a fast check:
|
||||
|
||||
- Diagnostic codes: claim a code by adding its row, in numeric order, to `DIAGNOSTIC_CODES` in `packages/compiler/src/diagnostics/codes.ts` before any site uses it. A shipped construct's code moves to `RETIRED_DIAGNOSTIC_CODES` and is never reused; codes that open work already uses are held in `RESERVED_DIAGNOSTIC_CODES`. `codes.test.ts` fails on a code without a row, a retired code in use, an unused row, or rows out of order, and two claims of one code are a merge conflict or a duplicate key.
|
||||
- Dyn handle tags: add a new tag by name only to `SCR_DYNH_NEW_TAGS` in `packages/runtime/src/scr_dyn.h`, in name order; the enum numbers it. Pin a tag to an explicit value in `SCR_DYNH_TAGS` only when the LLVM backend needs its number. The header does not compile when two tags share a value, and `dyn-handle-tags.test.ts` names the problem.
|
||||
- Runtime enums and flag bits the backend mirrors (`ScrDynKind`, `ScrMessageKind`, `ScrBytesElem`, `SCR_CLOSURE_KIND_*`, the `%ScrArr` spare flags) are checked against their C declarations by `packages/compiler/src/backend/llvm/runtime-registries.test.ts`. A new mirrored enum or flag set gets a check there, and C code names flag bits instead of writing their values.
|
||||
|
||||
Before taking a number in a pinned registry, run `node scripts/registry-claims.mjs`: it lists the diagnostic codes and pinned handle tags that open pull requests claim and reports collisions; `--check` reports only collisions with the current branch and exits 1 when there is one.
|
||||
|
||||
## Where things live
|
||||
|
||||
- `packages/compiler` — the frontend (tsc API to IR), typed IR, validator, serializer, and LLVM backend.
|
||||
|
||||
@@ -139,8 +139,8 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
|
||||
<dt><code>--print <native-link-info|diagnostics></code></dt>
|
||||
<dd><code>native-link-info</code>: build an object (equivalent to <code>--emit=obj</code>) and print its machine-readable external link recipe as JSON instead of printing the artifact path. The document names the exact installed precompiled runtime pack and all link inputs, but does not invoke a linker. <code>diagnostics</code>: for <code>build</code> and <code>coverage</code>, print the versioned JSON diagnostics document described in <a href="/docs/coverage#machine-readable-diagnostics">Coverage Reports</a> instead of the human output.</dd>
|
||||
|
||||
<dt><code>--fail-on <blockers|divergences|dynamic></code></dt>
|
||||
<dd>Coverage only. Exit with status 1 when the entry has blockers that would fail a build, with <code>divergences</code>, also when it has Node divergence warnings, or with <code>dynamic</code>, also when it has the <code>SC6101</code> warning for untyped code that likely runs often. See <a href="/docs/coverage#gating-builds-and-ci">Gating builds and CI</a>.</dd>
|
||||
<dt><code>--fail-on <blockers|divergences|dynamic|dynamic-slots></code></dt>
|
||||
<dd>Coverage only. Exit with status 1 when the entry has blockers that would fail a build, with <code>divergences</code>, also when it has Node divergence warnings, with <code>dynamic</code>, also when it has the <code>SC6101</code> warning for untyped code that likely runs often, or with <code>dynamic-slots</code>, also when it has <a href="/docs/coverage#dynamic-slots">dynamic slots</a> (<code>SC6104</code>). See <a href="/docs/coverage#gating-builds-and-ci">Gating builds and CI</a>.</dd>
|
||||
|
||||
<dt><code>--ffi <file></code></dt>
|
||||
<dd>Bind signature-only TypeScript declarations to native C ABI symbols and link the manifest's archive, object, and system-library inputs. See <a href="/docs/ffi">Native FFI</a>.</dd>
|
||||
|
||||
@@ -83,6 +83,39 @@ Without a profile, a dynamic-tier function counts as likely to run often when a
|
||||
|
||||
The warning names at most five functions, most likely to run often first, and gives the total. A function expression is named by its own name, else by the variable, property or class field it is assigned to, else as `anonymous function`. Dynamic-tier code in npm packages is summarized on one line with each package's function count. A build that comes from the cache repeats the warning. `--no-dynamic-tier-warning` leaves it out, and `--fail-on=dynamic` fails a coverage run on it.
|
||||
|
||||
## Dynamic slots
|
||||
|
||||
A declared type that has no native representation does not fail the build. The field, parameter, return value, variable or expression that holds it becomes a checked dynamic slot: the parts of the type that compile keep their native layout, and the failing parts hold checked-dynamic values. Coverage lists the slots under the statement counts, and `scriptc build` prints one `SC6104` warning per slot.
|
||||
|
||||
```console
|
||||
$ cat plugins.ts
|
||||
type Hook = (() => void) | AbortSignal | Map<string, (x: number) => string>;
|
||||
|
||||
interface Plugin {
|
||||
name: string;
|
||||
hook: Hook;
|
||||
}
|
||||
|
||||
const plugins: Plugin[] = [{ name: "log", hook: () => console.log("hook ran") }];
|
||||
for (const p of plugins) {
|
||||
const hook = p.hook;
|
||||
if (typeof hook === "function") hook();
|
||||
console.log(p.name);
|
||||
}
|
||||
|
||||
$ scriptc coverage plugins.ts
|
||||
|
||||
statements analyzed 6
|
||||
compile statically 6 (100%)
|
||||
in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them)
|
||||
dynamic slots 1 (declared types with no static lowering, held as checked-dynamic values; SC6104)
|
||||
module global 'plugins' (declared 'Plugin[]') (dynamic pieces) plugins.ts:8:7
|
||||
|
||||
fully static — all analyzed statements compile to native code.
|
||||
```
|
||||
|
||||
Here `Plugin` keeps its native `name` field, and only `hook` is checked-dynamic. Reading a dynamic value into typed code checks it against the type that reads it, so data that contradicts the declaration still throws a `TypeError`. Slots with the most dynamic operations are listed first. `--fail-on=dynamic-slots` fails a coverage run when any slot exists, and `--tier=static-only` reports the original errors instead.
|
||||
|
||||
## Diagnostic codes
|
||||
|
||||
Coverage and `scriptc build` report the same diagnostic codes. Common ones include:
|
||||
@@ -109,7 +142,7 @@ By default, `scriptc coverage` exits with status 0 whenever the program type-che
|
||||
$ scriptc coverage src/main.ts --fail-on=blockers
|
||||
```
|
||||
|
||||
`--fail-on=blockers` exits with status 1 when the program has blockers that would fail `scriptc build`. Blockers in unreached code and deferred operations don't count. `--fail-on=divergences` also fails on Node divergences, and `--fail-on=dynamic` also fails on the `SC6101` warning for [untyped code that likely runs often](#untyped-code-that-likely-runs-often). A program that doesn't type-check always exits with status 1.
|
||||
`--fail-on=blockers` exits with status 1 when the program has blockers that would fail `scriptc build`. Blockers in unreached code and deferred operations don't count. `--fail-on=divergences` also fails on Node divergences, `--fail-on=dynamic` also fails on the `SC6101` warning for [untyped code that likely runs often](#untyped-code-that-likely-runs-often), and `--fail-on=dynamic-slots` also fails on [dynamic slots](#dynamic-slots). A program that doesn't type-check always exits with status 1.
|
||||
|
||||
Coverage caches its result. Running it again on an unchanged program, or after edits that only touch comments, reuses the previous verdict. Set `SCRIPTC_NO_CACHE=1` to turn the cache off.
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/types-and-class-definitions/unions.mdx</include>
|
||||
|
||||
<include>../limitations/types-and-class-definitions/dynamic-slots.mdx</include>
|
||||
|
||||
<include>../limitations/types-and-class-definitions/null-and-optional-values.mdx</include>
|
||||
|
||||
<include>../limitations/types-and-class-definitions/static-field-updates.mdx</include>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
- **Dynamic slots:** A field, parameter, return value, variable or expression whose declared type has no native representation compiles as a checked dynamic slot instead of failing the build.
|
||||
The parts of the type that compile keep their native layout, and only the failing parts hold checked-dynamic values.
|
||||
Operations on those values run in the dynamic tier, and reading one into typed code checks it against the type that reads it, so a value that contradicts its declaration throws a `TypeError` when typed code first uses it rather than when it is stored.
|
||||
Each slot is an `SC6104` warning; `scriptc coverage --fail-on=dynamic-slots` fails on them, and `--tier=static-only` turns them back into build errors.
|
||||
Values that scriptc cannot produce at all, such as `WeakRef` instances, still fail where they are created.
|
||||
@@ -44,6 +44,9 @@
|
||||
"typescript": "7.0.2",
|
||||
"typescript5": "npm:typescript@5.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript-7.1": "npm:typescript@7.1.0-beta"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@scriptc/llvm-darwin-arm64": "workspace:*",
|
||||
"@scriptc/llvm-darwin-x64": "workspace:*",
|
||||
|
||||
@@ -32,6 +32,16 @@ export const SCR_ARR_STATES = 16;
|
||||
/** The `spare` flag of a frozen %ScrArr (SCR_ARR_FROZEN in scr_array.h). */
|
||||
export const SCR_ARR_FROZEN = 32;
|
||||
|
||||
/** The bits of ScrClosure.function_kind (SCR_CLOSURE_KIND_* in
|
||||
* scr_closure.h; runtime-registries.test.ts checks they agree). */
|
||||
export const CLOSURE_KIND = {
|
||||
GENERATOR: 1,
|
||||
ASYNC: 2,
|
||||
OWN_PROTOTYPE: 4,
|
||||
CHECKED_ADAPTER: 8,
|
||||
RECEIVER_FREE: 16,
|
||||
} as const;
|
||||
|
||||
/** Loads the i32 `len` or `cap` of a %ScrArr at `ptr` into `dest` as a
|
||||
* size-type count. */
|
||||
export function loadArrCount(
|
||||
@@ -277,10 +287,6 @@ export function emitFieldAbsentTest(B: BlockBuilder, value: string, undefinedTag
|
||||
return absent;
|
||||
}
|
||||
|
||||
/** ScrClosure.function_kind's SCR_FN_RECEIVER_FREE bit (scr_closure.h):
|
||||
* the body never observes the ambient receiver. */
|
||||
export const CLOSURE_RECEIVER_FREE = 16;
|
||||
|
||||
/** The right-hand side of a JS function-identity comparison: two closures
|
||||
* are one function when their identity roots match (a signature adapter
|
||||
* stands for the function it adapts). Both operands must be closures. */
|
||||
|
||||
@@ -3,27 +3,52 @@ import { expect, test } from "vitest";
|
||||
import { DYN_HANDLE_KINDS } from "../../ir/ir.js";
|
||||
import { DYN_HANDLE_TAG_NUM, TEXT_CODEC_TAG_NUM } from "./dyn.js";
|
||||
|
||||
// SCR_DYNH_TAGS in scr_dyn.h lists the ScrDynHandleTag values by name, each
|
||||
// with an explicit value, so changes that add tags edit different lines. The
|
||||
// scr_dyn.h declares the ScrDynHandleTag values in two lists: SCR_DYNH_TAGS
|
||||
// pins tags to explicit values, and SCR_DYNH_NEW_TAGS names tags added since
|
||||
// the last pin, which the enum numbers after the largest pinned value. Both
|
||||
// are sorted by name, so changes that add tags edit different lines. The
|
||||
// header itself fails to compile on a duplicate or out-of-range value; these
|
||||
// checks report the same problems by name and keep the backend's numbers in
|
||||
// step.
|
||||
const header = readFileSync(new URL("../../../../runtime/src/scr_dyn.h", import.meta.url), "utf8");
|
||||
const body = /#define SCR_DYNH_TAGS\(X\) \\\n([\s\S]*?)\n\n/.exec(header)![1]!;
|
||||
const tags = [...body.matchAll(/^\s*X\(([A-Z0-9_]+), (\d+)\)/gm)].map(([, name, value]) => ({
|
||||
const macro = (name: string, param: string) =>
|
||||
new RegExp(`#define ${name}\\(${param}\\)(?: \\\\\\n([\\s\\S]*?))?\\n\\n`).exec(header)![1] ?? "";
|
||||
const pinnedBody = macro("SCR_DYNH_TAGS", "X");
|
||||
const newBody = macro("SCR_DYNH_NEW_TAGS", "Y");
|
||||
const unassignedBody = macro("SCR_DYNH_UNASSIGNED", "Z");
|
||||
const pinned = [...pinnedBody.matchAll(/^\s*X\(([A-Z0-9_]+), (\d+)\)/gm)].map(
|
||||
([, name, value]) => ({
|
||||
name: `SCR_DYNH_${name!}`,
|
||||
value: Number(value),
|
||||
}),
|
||||
);
|
||||
const lastPinned = Math.max(...pinned.map((tag) => tag.value));
|
||||
const added = [...newBody.matchAll(/^\s*Y\(([A-Z0-9_]+)\)/gm)].map(([, name], i) => ({
|
||||
name: `SCR_DYNH_${name!}`,
|
||||
value: Number(value),
|
||||
value: lastPinned + 1 + i,
|
||||
}));
|
||||
const tags = [...pinned, ...added];
|
||||
const unassigned = [...unassignedBody.matchAll(/^\s*Z\((\d+)\)/gm)].map(([, value]) =>
|
||||
Number(value),
|
||||
);
|
||||
const capacity = Number(/#define SCR_DYNH_CAPACITY (\d+)/.exec(header)![1]);
|
||||
const rows = (body: string, open: string) =>
|
||||
body.split("\n").filter((line) => line.trim().startsWith(open)).length;
|
||||
|
||||
test("every ScrDynHandleTag has an explicit value, in name order", () => {
|
||||
const lines = body.split("\n").filter((line) => line.trim().startsWith("X("));
|
||||
expect(tags.length).toBe(lines.length);
|
||||
expect(tags.map((tag) => tag.name)).toEqual(tags.map((tag) => tag.name).toSorted());
|
||||
test("every pinned ScrDynHandleTag has an explicit value, in name order", () => {
|
||||
expect(pinned.length).toBeGreaterThan(0);
|
||||
expect(pinned.length).toBe(rows(pinnedBody, "X("));
|
||||
expect(pinned.map((tag) => tag.name)).toEqual(pinned.map((tag) => tag.name).toSorted());
|
||||
});
|
||||
|
||||
test("the new tags are listed by name only, in name order", () => {
|
||||
expect(added.length).toBe(rows(newBody, "Y("));
|
||||
expect(added.map((tag) => tag.name)).toEqual(added.map((tag) => tag.name).toSorted());
|
||||
});
|
||||
|
||||
test("ScrDynHandleTag values are unique and below SCR_DYNH_CAPACITY", () => {
|
||||
const owners = new Map<number, string>();
|
||||
const owners = new Map<number, string>(unassigned.map((value) => [value, "SCR_DYNH_UNASSIGNED"]));
|
||||
expect(owners.size).toBe(unassigned.length);
|
||||
for (const { name, value } of tags) {
|
||||
expect(owners.get(value), `${name} = ${value}`).toBeUndefined();
|
||||
owners.set(value, name);
|
||||
@@ -31,17 +56,27 @@ test("ScrDynHandleTag values are unique and below SCR_DYNH_CAPACITY", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("the backend's handle tag numbers match scr_dyn.h", () => {
|
||||
const values = new Map(tags.map((tag) => [tag.name, tag.value]));
|
||||
test("every value up to the largest pinned one is a tag or listed as unassigned", () => {
|
||||
const used = new Set([...pinned.map((tag) => tag.value), ...unassigned]);
|
||||
const gaps = Array.from({ length: lastPinned + 1 }, (_, value) => value).filter(
|
||||
(value) => !used.has(value),
|
||||
);
|
||||
expect(gaps, "add each value to SCR_DYNH_UNASSIGNED with the reason").toEqual([]);
|
||||
for (const value of unassigned) expect(value).toBeLessThan(lastPinned);
|
||||
});
|
||||
|
||||
test("the backend's handle tag numbers match pinned tags in scr_dyn.h", () => {
|
||||
const values = new Map(pinned.map((tag) => [tag.name, tag.value]));
|
||||
for (const [kind, num] of Object.entries(DYN_HANDLE_TAG_NUM)) {
|
||||
const tag = DYN_HANDLE_KINDS.get(kind)?.tag;
|
||||
expect(tag, kind).toBeDefined();
|
||||
expect(values.get(tag!), `${kind} (${tag})`).toBe(num);
|
||||
expect(values.get(tag!), `${kind} (${tag}) must be pinned with this value`).toBe(num);
|
||||
}
|
||||
});
|
||||
|
||||
test("the backend's codec handle tag numbers match scr_dyn.h", () => {
|
||||
const values = new Map(tags.map((tag) => [tag.name, tag.value]));
|
||||
test("the backend's codec handle tag numbers match pinned tags in scr_dyn.h", () => {
|
||||
const values = new Map(pinned.map((tag) => [tag.name, tag.value]));
|
||||
expect(values.get("SCR_DYNH_TEXT_DECODER")).toBe(TEXT_CODEC_TAG_NUM.TextDecoder);
|
||||
expect(values.get("SCR_DYNH_TEXT_ENCODER")).toBe(TEXT_CODEC_TAG_NUM.TextEncoder);
|
||||
expect(values.get("SCR_DYNH_STRING_DECODER")).toBe(TEXT_CODEC_TAG_NUM.StringDecoder);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { emitPerKeyOrder } from "./key-orders.js";
|
||||
import { typedRefConstructor } from "./shapes.js";
|
||||
import { bindLiveArrayOps } from "./expr-stream-bridges.js";
|
||||
import { BYTES_ELEM_NUM, SCR_ARR_FIELD, SCR_ARR_STATES, f64Lit, sizeToDouble } from "./common.js";
|
||||
import {
|
||||
BYTES_ELEM_NUM,
|
||||
CLOSURE_KIND,
|
||||
SCR_ARR_FIELD,
|
||||
SCR_ARR_STATES,
|
||||
f64Lit,
|
||||
sizeToDouble,
|
||||
} from "./common.js";
|
||||
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
||||
import {
|
||||
INLINE_UNION_FIELD,
|
||||
@@ -145,8 +152,8 @@ const DYN_HANDLE_UNBOX_FN: Record<string, string> = {
|
||||
dgramSocket: "scr_dgram_from_dyn",
|
||||
};
|
||||
|
||||
/** ScrMessageKind (scr_message.h): the node kinds message readers test. */
|
||||
const MESSAGE_KIND = {
|
||||
/** ScrMessageKind (scr_dyn.h): the node kinds message readers test. */
|
||||
export const MESSAGE_KIND = {
|
||||
UNDEFINED: 0,
|
||||
NULL: 1,
|
||||
BOOL: 2,
|
||||
@@ -3174,7 +3181,7 @@ export class LlDyn {
|
||||
B.line(`${targetKind} = getelementptr inbounds %ScrClosure, ptr ${a}, i64 0, i32 4`);
|
||||
const adapterKind = B.tmp();
|
||||
B.line(
|
||||
`${adapterKind} = or i32 ${functionKind}, 8 ; preserves the checked callable in caps[0]`,
|
||||
`${adapterKind} = or i32 ${functionKind}, ${CLOSURE_KIND.CHECKED_ADAPTER} ; preserves the checked callable in caps[0]`,
|
||||
);
|
||||
B.line(`store i32 ${adapterKind}, ptr ${targetKind}`);
|
||||
const box = B.tmp();
|
||||
|
||||
@@ -217,7 +217,7 @@ import {
|
||||
import { LazyCaptures } from "./lazy-captures.js";
|
||||
import { StackCaptures } from "./stack-captures.js";
|
||||
import {
|
||||
CLOSURE_RECEIVER_FREE,
|
||||
CLOSURE_KIND,
|
||||
emitFieldAbsentTest,
|
||||
f64Lit,
|
||||
SCR_ARR_FIELD,
|
||||
@@ -7506,15 +7506,15 @@ export class LlEmitter {
|
||||
return wrapEmitterListener(this, target, adapterFn);
|
||||
}
|
||||
|
||||
/** ScrClosure.function_kind for a closure over `fn`: generator 1, async
|
||||
* 2, own prototype 4, and SCR_FN_RECEIVER_FREE (16) when its body never
|
||||
/** ScrClosure.function_kind for a closure over `fn`: its generator,
|
||||
* async and own prototype bits, and RECEIVER_FREE when its body never
|
||||
* observes the ambient receiver, so indirect callers skip the bracket. */
|
||||
closureFunctionKind(fn: IrFunction): number {
|
||||
return (
|
||||
(fn.generator ? 1 : 0) +
|
||||
(fn.async ? 2 : 0) +
|
||||
(fn.ownsPrototype ? 4 : 0) +
|
||||
(this.receiverReaders.ignores(fn) ? CLOSURE_RECEIVER_FREE : 0)
|
||||
(fn.generator ? CLOSURE_KIND.GENERATOR : 0) +
|
||||
(fn.async ? CLOSURE_KIND.ASYNC : 0) +
|
||||
(fn.ownsPrototype ? CLOSURE_KIND.OWN_PROTOTYPE : 0) +
|
||||
(this.receiverReaders.ignores(fn) ? CLOSURE_KIND.RECEIVER_FREE : 0)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import {
|
||||
CLOSURE_RECEIVER_FREE,
|
||||
CLOSURE_KIND,
|
||||
f64Lit,
|
||||
ffiNativeTypeLl,
|
||||
ffiNativeParamLl,
|
||||
@@ -819,7 +819,7 @@ export function emitCallExpr(
|
||||
`${fromPtr} = getelementptr inbounds %ScrClosure, ptr ${original}, i64 0, i32 4`,
|
||||
);
|
||||
B.line(`${from} = load i32, ptr ${fromPtr}`);
|
||||
B.line(`${bit} = and i32 ${from}, ${CLOSURE_RECEIVER_FREE}`);
|
||||
B.line(`${bit} = and i32 ${from}, ${CLOSURE_KIND.RECEIVER_FREE}`);
|
||||
B.line(`${toPtr} = getelementptr inbounds %ScrClosure, ptr ${c}, i64 0, i32 4`);
|
||||
B.line(`${to} = load i32, ptr ${toPtr}`);
|
||||
B.line(`${kind} = or i32 ${to}, ${bit}`);
|
||||
@@ -938,7 +938,7 @@ export function emitCallExpr(
|
||||
)
|
||||
: args;
|
||||
// An entry through the closure's own pointer brackets the call only
|
||||
// when the closure may observe the receiver (SCR_FN_RECEIVER_FREE).
|
||||
// when the closure may observe the receiver (SCR_CLOSURE_KIND_RECEIVER_FREE).
|
||||
const invoke = (
|
||||
target: string,
|
||||
bracket: boolean,
|
||||
@@ -958,7 +958,7 @@ export function emitCallExpr(
|
||||
`${kindPtr} = getelementptr inbounds %ScrClosure, ptr ${callee.name}, i64 0, i32 4`,
|
||||
);
|
||||
B.line(`${kind} = load i32, ptr ${kindPtr}`);
|
||||
B.line(`${free} = and i32 ${kind}, ${CLOSURE_RECEIVER_FREE}`);
|
||||
B.line(`${free} = and i32 ${kind}, ${CLOSURE_KIND.RECEIVER_FREE}`);
|
||||
B.line(`${observes} = icmp eq i32 ${free}, 0`);
|
||||
const push = B.newLabel("receiver.push"),
|
||||
pushed = B.newLabel("receiver.call");
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { expect, test } from "vitest";
|
||||
import { BYTES_ELEM_NUM, CLOSURE_KIND, SCR_ARR_FROZEN, SCR_ARR_STATES } from "./common.js";
|
||||
import { DYN_KIND, MESSAGE_KIND } from "./dyn.js";
|
||||
|
||||
// The backend writes the numbers of several runtime enums and flag sets
|
||||
// into the IR it emits. Each check here reads the C declaration and fails
|
||||
// when the backend's copy disagrees, or when two names share a value.
|
||||
const source = (file: string) =>
|
||||
readFileSync(new URL(`../../../../runtime/src/${file}`, import.meta.url), "utf8")
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
.replace(/\/\/.*$/gm, "");
|
||||
|
||||
/** The members of `typedef enum ... { ... } <name>;` with their values. */
|
||||
function cEnum(file: string, name: string): Map<string, number> {
|
||||
const match = new RegExp(`typedef enum(?: \\w+)? \\{([^}]*)\\} ${name};`).exec(source(file));
|
||||
expect(match, `${name} in ${file}`).not.toBeNull();
|
||||
const body = match![1]!;
|
||||
const members = new Map<string, number>();
|
||||
let next = 0;
|
||||
for (const entry of body
|
||||
.split(",")
|
||||
.map((e) => e.trim())
|
||||
.filter(Boolean)) {
|
||||
const [, member, value] = /^(\w+)(?:\s*=\s*(\d+))?$/.exec(entry)!;
|
||||
const v = value === undefined ? next : Number(value);
|
||||
members.set(member!, v);
|
||||
next = v + 1;
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
/** The `#define <prefix>NAME <n>u` flags of a header. */
|
||||
function cFlags(file: string, prefix: string): Map<string, number> {
|
||||
const re = new RegExp(`^#define ${prefix}(\\w+) (\\d+)u\\b`, "gm");
|
||||
return new Map([...source(file).matchAll(re)].map(([, name, value]) => [name!, Number(value)]));
|
||||
}
|
||||
|
||||
function expectDistinct(values: Map<string, number>, what: string): void {
|
||||
const owners = new Map<number, string>();
|
||||
for (const [name, value] of values) {
|
||||
expect(owners.get(value), `${what}: ${name} = ${value}`).toBeUndefined();
|
||||
owners.set(value, name);
|
||||
}
|
||||
}
|
||||
|
||||
function expectSingleBits(values: Map<string, number>, what: string): void {
|
||||
for (const [name, value] of values) {
|
||||
expect(value > 0 && (value & (value - 1)) === 0, `${what}: ${name} = ${value}`).toBe(true);
|
||||
}
|
||||
expectDistinct(values, what);
|
||||
}
|
||||
|
||||
test("DYN_KIND matches ScrDynKind", () => {
|
||||
const kinds = cEnum("scr_dyn.h", "ScrDynKind");
|
||||
expectDistinct(kinds, "ScrDynKind");
|
||||
for (const [name, value] of Object.entries(DYN_KIND)) {
|
||||
expect(kinds.get(`SCR_DYN_${name}`), name).toBe(value);
|
||||
}
|
||||
});
|
||||
|
||||
test("MESSAGE_KIND matches ScrMessageKind", () => {
|
||||
const kinds = cEnum("scr_dyn.h", "ScrMessageKind");
|
||||
expectDistinct(kinds, "ScrMessageKind");
|
||||
for (const [name, value] of Object.entries(MESSAGE_KIND)) {
|
||||
expect(kinds.get(`SCR_MESSAGE_${name}`), name).toBe(value);
|
||||
}
|
||||
});
|
||||
|
||||
test("BYTES_ELEM_NUM matches ScrBytesElem", () => {
|
||||
const elems = cEnum("scr_bytes.h", "ScrBytesElem");
|
||||
expectDistinct(elems, "ScrBytesElem");
|
||||
expect(Object.keys(BYTES_ELEM_NUM).length).toBe(elems.size);
|
||||
for (const [name, value] of Object.entries(BYTES_ELEM_NUM)) {
|
||||
expect(elems.get(`SCR_BYTES_${name.toUpperCase()}`), name).toBe(value);
|
||||
}
|
||||
});
|
||||
|
||||
test("CLOSURE_KIND matches the SCR_CLOSURE_KIND_* bits", () => {
|
||||
const flags = cFlags("scr_closure.h", "SCR_CLOSURE_KIND_");
|
||||
expect(flags.get("CALL_MASK")).toBe(CLOSURE_KIND.GENERATOR | CLOSURE_KIND.ASYNC);
|
||||
flags.delete("CALL_MASK");
|
||||
expect(flags.get("MASK")).toBe(CLOSURE_KIND.RECEIVER_FREE - 1);
|
||||
flags.delete("MASK");
|
||||
expectSingleBits(flags, "SCR_CLOSURE_KIND");
|
||||
expect(Object.fromEntries(flags)).toEqual(CLOSURE_KIND);
|
||||
});
|
||||
|
||||
test("the %ScrArr spare flags the backend tests match scr_array.h", () => {
|
||||
const flags = cFlags("scr_array.h", "SCR_ARR_");
|
||||
const spare = new Map(
|
||||
["INLINE_SLOTS", "HEAD_ROOM", "UNSHIFTED", "HAS_EXT", "STATES", "FROZEN"].map((name) => [
|
||||
name,
|
||||
flags.get(name)!,
|
||||
]),
|
||||
);
|
||||
expectSingleBits(spare, "%ScrArr spare");
|
||||
expect(spare.get("STATES")).toBe(SCR_ARR_STATES);
|
||||
expect(spare.get("FROZEN")).toBe(SCR_ARR_FROZEN);
|
||||
});
|
||||
@@ -42,9 +42,10 @@ Options:
|
||||
coverage only: exit 1 when the entry path has blockers
|
||||
(a build would fail), or also when it has Node
|
||||
divergences, likely-hot functions that use untyped
|
||||
values (SC6101), any Node island (eager-node-islands is
|
||||
the same until deferred evaluation lands), or a gap
|
||||
island (a package scriptc should compile but cannot yet)
|
||||
values (SC6101), dynamic slots (SC6104), any Node
|
||||
island (eager-node-islands is the same until deferred
|
||||
evaluation lands), or a gap island (a package scriptc
|
||||
should compile but cannot yet)
|
||||
--backend <b> code generator (llvm)
|
||||
--optimization <release|dev|speed>
|
||||
native optimization posture (default: release/-O2). dev
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
* counts (classes.ts programLifetimeClass).
|
||||
*
|
||||
* Verified facts land on the IR as `contracts` (IrContracts). */
|
||||
import type { DiagnosticCode } from "../diagnostics/codes.js";
|
||||
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type {
|
||||
IrClassDef,
|
||||
@@ -158,7 +159,7 @@ class ContractChecker {
|
||||
return raw.length > max ? `${raw.slice(0, max - 3)}...` : raw;
|
||||
}
|
||||
|
||||
report(code: `SC${number}`, loc: SrcLoc, message: string, hint: string): void {
|
||||
report(code: DiagnosticCode, loc: SrcLoc, message: string, hint: string): void {
|
||||
const key = `${code}\0${contractLocKey(loc)}\0${message}`;
|
||||
if (this.reported.has(key)) return;
|
||||
this.reported.add(key);
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
* Holes, present undefined, out-of-range reads and `length` changes keep
|
||||
* their JavaScript behavior: the contract constrains the values stored, not
|
||||
* the array's shape. */
|
||||
import type { DiagnosticCode } from "../diagnostics/codes.js";
|
||||
import type { ContractNote, ContractAnnotations } from "../frontend/contract-types.js";
|
||||
import type {
|
||||
IrClassDef,
|
||||
@@ -61,7 +62,7 @@ export interface ElementContractHost {
|
||||
readonly notes: ContractAnnotations;
|
||||
readonly functions: ReadonlyMap<string, IrFunction>;
|
||||
readonly classes: ReadonlyMap<string, IrClassDef>;
|
||||
report(code: `SC${number}`, loc: SrcLoc, message: string, hint: string): void;
|
||||
report(code: DiagnosticCode, loc: SrcLoc, message: string, hint: string): void;
|
||||
text(loc: SrcLoc, max?: number): string;
|
||||
where(loc: SrcLoc | null): string;
|
||||
displayName(fnName: string): string;
|
||||
|
||||
@@ -650,6 +650,8 @@ export function coverageEnvelope(
|
||||
declaredType: slot.declaredType,
|
||||
code: slot.code,
|
||||
reason: slot.reason,
|
||||
...(slot.partial === true ? { partial: true } : {}),
|
||||
dynamicOps: slot.operations ?? 0,
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
@@ -762,8 +764,15 @@ function renderDynamicSlots(
|
||||
out.push(
|
||||
` dynamic slots ${slots.length} ${c(DIM, "(declared types with no static lowering, held as checked-dynamic values; SC6104)")}`,
|
||||
);
|
||||
for (const slot of slots.slice(0, DYNAMIC_SLOTS_SHOWN))
|
||||
out.push(` ${dynamicSlotLabel(slot)} ${c(DIM, where(slot.loc))}`);
|
||||
// The costly slots first: most dynamic operations, then source order.
|
||||
const ranked = slots
|
||||
.map((slot, index) => ({ slot, index }))
|
||||
.sort((a, b) => (b.slot.operations ?? 0) - (a.slot.operations ?? 0) || a.index - b.index)
|
||||
.map(({ slot }) => slot);
|
||||
for (const slot of ranked.slice(0, DYNAMIC_SLOTS_SHOWN))
|
||||
out.push(
|
||||
` ${dynamicSlotLabel(slot)}${slot.partial === true ? " (dynamic pieces)" : ""} ${c(DIM, `${where(slot.loc)}${(slot.operations ?? 0) > 0 ? `, ${slot.operations} dynamic operation${slot.operations === 1 ? "" : "s"}` : ""}`)}`,
|
||||
);
|
||||
if (slots.length > DYNAMIC_SLOTS_SHOWN)
|
||||
out.push(` ${c(DIM, `and ${slots.length - DYNAMIC_SLOTS_SHOWN} more`)}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { DIAGNOSTIC_CODES, RESERVED_DIAGNOSTIC_CODES, RETIRED_DIAGNOSTIC_CODES } from "./codes.js";
|
||||
import { FENCE_CODES, UNSUPPORTED } from "./diagnostic.js";
|
||||
|
||||
const here = import.meta.dirname;
|
||||
const packagesDir = join(here, "../../..");
|
||||
const codesSource = readFileSync(join(here, "codes.ts"), "utf8");
|
||||
|
||||
function sourceFiles(dir: string): string[] {
|
||||
return readdirSync(dir, { recursive: true, withFileTypes: true })
|
||||
.filter((e) => e.isFile() && e.name.endsWith(".ts") && !e.name.endsWith(".test.ts"))
|
||||
.map((e) => join(e.parentPath, e.name));
|
||||
}
|
||||
|
||||
// Every non-test source file that can name a code.
|
||||
const sources = [
|
||||
...sourceFiles(join(packagesDir, "compiler/src")),
|
||||
...sourceFiles(join(packagesDir, "cli/src")),
|
||||
]
|
||||
.filter((file) => file !== join(here, "codes.ts"))
|
||||
.map((file) => ({ file: file.slice(packagesDir.length), text: readFileSync(file, "utf8") }));
|
||||
|
||||
const active = new Set(Object.keys(DIAGNOSTIC_CODES));
|
||||
const retired = new Set(Object.keys(RETIRED_DIAGNOSTIC_CODES));
|
||||
const reserved = new Set(Object.keys(RESERVED_DIAGNOSTIC_CODES));
|
||||
|
||||
/** The keys of one table in codes.ts, as written (duplicates included). */
|
||||
function writtenKeys(table: string): string[] {
|
||||
const start = codesSource.indexOf(`export const ${table} = {`);
|
||||
const end = codesSource.indexOf("\n}", start);
|
||||
return [...codesSource.slice(start, end).matchAll(/^ {2}(SC\d{4}):/gm)].map((m) => m[1]!);
|
||||
}
|
||||
|
||||
test.each(["DIAGNOSTIC_CODES", "RETIRED_DIAGNOSTIC_CODES", "RESERVED_DIAGNOSTIC_CODES"])(
|
||||
"%s is written once per code, in numeric order",
|
||||
(table) => {
|
||||
const keys = writtenKeys(table);
|
||||
expect(keys.length).toBeGreaterThan(0);
|
||||
expect(keys).toEqual([...new Set(keys)].toSorted());
|
||||
},
|
||||
);
|
||||
|
||||
test("a code is active, retired or reserved, never two of them", () => {
|
||||
for (const code of retired) expect(active.has(code) || reserved.has(code), code).toBe(false);
|
||||
for (const code of reserved) expect(active.has(code), code).toBe(false);
|
||||
});
|
||||
|
||||
test("every code the sources name has a row in codes.ts", () => {
|
||||
const missing: string[] = [];
|
||||
for (const { file, text } of sources) {
|
||||
for (const [code] of text.matchAll(/\bSC\d{4}\b/g)) {
|
||||
if (!active.has(code) && !retired.has(code) && !reserved.has(code)) {
|
||||
missing.push(`${code} (${file})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(missing, "claim each code in packages/compiler/src/diagnostics/codes.ts").toEqual([]);
|
||||
});
|
||||
|
||||
test("no source produces a retired code", () => {
|
||||
const produced: string[] = [];
|
||||
for (const { file, text } of sources) {
|
||||
for (const [, code] of text.matchAll(/["'`](SC\d{4})["'`]/g)) {
|
||||
if (retired.has(code!)) produced.push(`${code} (${file})`);
|
||||
}
|
||||
}
|
||||
expect(produced).toEqual([]);
|
||||
});
|
||||
|
||||
test("every active code is used by some source", () => {
|
||||
const seen = new Set(sources.flatMap(({ text }) => text.match(/\bSC\d{4}\b/g) ?? []));
|
||||
const unused = [...active].filter((code) => !seen.has(code));
|
||||
expect(unused, "retire codes nothing produces any more").toEqual([]);
|
||||
});
|
||||
|
||||
test("the construct tables only name active codes", () => {
|
||||
for (const code of [...Object.keys(UNSUPPORTED), ...Object.keys(FENCE_CODES)]) {
|
||||
expect(active.has(code), code).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("every band in use is documented in diagnostic.ts", () => {
|
||||
const header = readFileSync(join(here, "diagnostic.ts"), "utf8");
|
||||
for (const band of new Set([...active].map((code) => code[2]))) {
|
||||
expect(header, `SC${band}xxx`).toContain(`SC${band}xxx`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
/* Every SC diagnostic code, in one table.
|
||||
*
|
||||
* A code is claimed by adding its row here, in numeric order, before any
|
||||
* site uses it. Two changes that claim the same code then either edit the
|
||||
* same line (a merge conflict) or add the same key twice (TypeScript error
|
||||
* TS1117), and codes.test.ts fails when a source file uses a code that has
|
||||
* no row, uses a retired code, or when a row is out of order. The band of a
|
||||
* code (its first digit) says what kind of diagnostic it is; diagnostic.ts
|
||||
* documents the bands.
|
||||
*
|
||||
* Codes are part of scriptc's user-facing output and never change meaning:
|
||||
* when a construct ships, its code moves to RETIRED_DIAGNOSTIC_CODES instead
|
||||
* of being reused. RESERVED_DIAGNOSTIC_CODES holds codes that open work
|
||||
* already uses, so other changes do not take them. */
|
||||
|
||||
/** The active codes, with a short summary of what each reports. */
|
||||
export const DIAGNOSTIC_CODES = {
|
||||
SC0001: "TypeScript error passed through",
|
||||
SC0002: "project configuration incompatible with scriptc",
|
||||
SC0003: "malformed imported JSON module",
|
||||
SC0004: "TypeScript checker panic",
|
||||
SC1010: "package imports outside the supported set",
|
||||
SC1012: "default exports/imports (residual edges)",
|
||||
SC1013: "namespace imports (residual edges)",
|
||||
SC1014: "re-exports and export lists",
|
||||
SC1015: "dynamic import()",
|
||||
SC1016: "circular imports",
|
||||
SC1020: "class expressions",
|
||||
SC1030: "var declarations (residual forms)",
|
||||
SC1031: "destructuring",
|
||||
SC1040: "loose equality (== and !=)",
|
||||
SC1042: "logical operators on mixed operand types",
|
||||
SC1043: "comparing non-number, non-string values",
|
||||
SC1045: "increment/decrement in expression position",
|
||||
SC1050: "labeled break/continue",
|
||||
SC1052: "for-in loops",
|
||||
SC1062: "destructuring catch clause bindings",
|
||||
SC1063: "this use of a catch binding",
|
||||
SC1070: "async/await",
|
||||
SC1071: "generators",
|
||||
SC1080: "'this' outside a class method",
|
||||
SC1090: "this syntax (generic construct fence)",
|
||||
SC1100: "operations on 'unknown' values",
|
||||
SC1101: "converting typed values to 'unknown'",
|
||||
SC1110: "comptime evaluation failure",
|
||||
SC1120: "regex feature outside the native engine",
|
||||
SC2001: "type outside the compilable set",
|
||||
SC2002: "record shape flow outside the width-copy rules",
|
||||
SC2003: "union-to-union conversion outside the re-tagging rule",
|
||||
SC2004: "use of a binding whose declaration did not compile",
|
||||
SC2005: "generic call signature in a value's type",
|
||||
SC2006: "index-signature object type outside the supported shape",
|
||||
SC2007: "overloaded function type as a value",
|
||||
SC2008: "intersection type with no lowering",
|
||||
SC2009: "component type outside its container or function slot",
|
||||
SC2010: "runtime JavaScript execution",
|
||||
SC2011: "'any'-typed values and their operations",
|
||||
SC2012: "standard-library form without native lowering",
|
||||
SC2013: "npm package import or value without native lowering",
|
||||
SC2020: "standard-library or @types/node surface with no lowering",
|
||||
SC3001: "LLVM IR tier refusal",
|
||||
SC3002: "target or capability refusal",
|
||||
SC3003: "native helper installation or version failure",
|
||||
SC3004: "native helper code-generation failure",
|
||||
SC4001: "library profile malformed",
|
||||
SC4002: "library export unresolved",
|
||||
SC4003: "library export signature not mappable",
|
||||
SC4004: "async or generator library export",
|
||||
SC4005: "library graph reaches the event loop or ambient process state",
|
||||
SC4007: "generic library export signature",
|
||||
SC4008: "profile-fenced surface reached by a library",
|
||||
SC4009: "contract sidecar projection failure",
|
||||
SC4010: "tabled type declared at several sites",
|
||||
SC4011: "conditional or mapped type producing a tabled type",
|
||||
SC4012: "library inbound-bytes trap (runtime)",
|
||||
SC4013: "library escaped exception trap (runtime)",
|
||||
SC4014: "library range trap (runtime)",
|
||||
SC4015: "library type trap (runtime)",
|
||||
SC4016: "library syntax trap (runtime)",
|
||||
SC4017: "library out-of-memory trap (runtime)",
|
||||
SC4018: "library internal invariant trap (runtime)",
|
||||
SC4019: "library other detected trap (runtime)",
|
||||
SC4020: "npm package imported by a library is not statically eligible",
|
||||
SC4021: "integer boundary: literal not representable",
|
||||
SC4022: "integer boundary: value may not be whole",
|
||||
SC4023: "integer boundary: value out of range",
|
||||
SC4024: "host callback reference the profile cannot serve",
|
||||
SC4025: "host callback invoked before registration (runtime)",
|
||||
SC4026: "library re-entered from a host callback (runtime)",
|
||||
SC5001: "FFI manifest malformed",
|
||||
SC5002: "FFI binding is not an ambient function declaration",
|
||||
SC5003: "FFI signature does not match its native ABI",
|
||||
SC5004: "FFI native build failure",
|
||||
SC6001: "divergence: width copy written or compared through",
|
||||
SC6002: "divergence: observed object key order",
|
||||
SC6003: "divergence: localeCompare outside the built-in collation",
|
||||
SC6004: "divergence: cast to an unrelated class",
|
||||
SC6101: "performance: likely-hot function with untyped values",
|
||||
SC6102: "performance: declaration-typed boundary note",
|
||||
SC6103: "performance: shape inference overlay report",
|
||||
SC6104: "localized failure: slot held as a checked-dynamic value",
|
||||
SC7001: "NoThrow contract on an async function or generator",
|
||||
SC7002: "Int32 contract value not provably int32",
|
||||
SC7003: "Int32 contract cannot be checked",
|
||||
SC7004: "Immutable contract value may be referenced elsewhere",
|
||||
SC7005: "Immutable contract cannot be checked",
|
||||
SC7006: "Acyclic contract in an unchecked position",
|
||||
SC7007: "Acyclic type can lie on a reference cycle",
|
||||
SC7008: "ProgramLifetime class extends another class",
|
||||
SC8002: "Node islands off for a package or module outside the native subset",
|
||||
SC8003: "package marked native is outside the native subset",
|
||||
SC8004: "unknown scriptc.packages or scriptc.nodeIslands configuration",
|
||||
SC8005: "no usable libnode pack for a program that calls into Node.js",
|
||||
SC8006: "Node island modules the build cannot collect",
|
||||
SC8030: "type patch: no range covers the installed copy",
|
||||
SC8031: "type patch: selector matches nothing",
|
||||
SC8032: "type patch: declaration already has the type",
|
||||
SC8033: "type patch: selector ambiguous or not patchable",
|
||||
SC8101: "user JavaScript: constructor function",
|
||||
SC8102: "user JavaScript: prototype mutation",
|
||||
SC8103: "user JavaScript: shape mutation outside the checker's view",
|
||||
SC8104: "user JavaScript: sloppy-mode semantics",
|
||||
SC8105: "user JavaScript: builtin and global patching",
|
||||
SC9001: "internal compiler error",
|
||||
SC9002: "internal error trap in generated code",
|
||||
} as const satisfies Record<`SC${number}`, string>;
|
||||
|
||||
/** Codes whose construct shipped or whose diagnostic was removed. Never
|
||||
* reuse one: old output and documentation still name it. */
|
||||
export const RETIRED_DIAGNOSTIC_CODES = {
|
||||
SC1011: "exports (shipped)",
|
||||
SC1021: "arrow functions and closures (shipped)",
|
||||
SC1022: "nested function declarations (shipped)",
|
||||
SC1023: "object literals (shipped as records)",
|
||||
SC1024: "arrays (shipped)",
|
||||
SC1032: "multiple declarations in one statement (shipped)",
|
||||
SC1033: "let without an initializer (shipped)",
|
||||
SC1041: "bitwise operators (shipped)",
|
||||
SC1051: "do-while loops (shipped)",
|
||||
SC1060: "switch statements (shipped)",
|
||||
SC1061: "exceptions (shipped)",
|
||||
SC1121: "stateful g/y regex tests (shipped)",
|
||||
SC2030: "npm embedding failure",
|
||||
SC4006: "library export refusal (removed)",
|
||||
SC6005: "Date strings outside the native parser's grammar",
|
||||
} as const satisfies Record<`SC${number}`, string>;
|
||||
|
||||
/** Codes that open work already uses, so no other change claims them. When
|
||||
* that work lands it moves its codes into DIAGNOSTIC_CODES; when it is
|
||||
* abandoned, delete them here. */
|
||||
export const RESERVED_DIAGNOSTIC_CODES = {
|
||||
SC8001: "Node islands (ni/* branches)",
|
||||
SC8010: "Node islands (ni/* branches)",
|
||||
SC8011: "Node islands (ni/* branches)",
|
||||
SC8012: "Node islands (ni/* branches)",
|
||||
SC8020: "Node islands (ni/* branches)",
|
||||
SC8021: "Node islands (ni/* branches)",
|
||||
SC8022: "Node islands (ni/* branches)",
|
||||
SC8023: "Node islands (ni/* branches)",
|
||||
} as const satisfies Record<`SC${number}`, string>;
|
||||
|
||||
export type DiagnosticCode = keyof typeof DIAGNOSTIC_CODES;
|
||||
@@ -1,4 +1,5 @@
|
||||
/* Diagnostic codes live in ONE registry. Message strings are never inlined
|
||||
/* Diagnostics live in ONE registry, and every code is claimed by its row in
|
||||
* codes.ts (DIAGNOSTIC_CODES). Message strings are never inlined
|
||||
* at rejection sites — every "not supported yet" the compiler can produce is
|
||||
* enumerable here, which is the seed of the future coverage report
|
||||
* ("N constructs prevented static compilation, grouped by code").
|
||||
@@ -68,6 +69,9 @@
|
||||
* 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)
|
||||
* SC7xxx contract types (@scriptc/types): a NoThrow, Int32, Immutable,
|
||||
* Acyclic or ProgramLifetime contract that does not hold or
|
||||
* cannot be checked (SC7001–SC7008, contracts/check.ts)
|
||||
* 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
|
||||
@@ -82,6 +86,7 @@
|
||||
* SC9xxx internal compiler errors (still source-anchored)
|
||||
*/
|
||||
import type { SrcLoc } from "../ir/ir.js";
|
||||
import type { DiagnosticCode } from "./codes.js";
|
||||
|
||||
/* Internal prioritization buckets. NEVER rendered to users — user-facing
|
||||
* output says only what is and isn't supported (plus hints); scheduling is
|
||||
@@ -89,7 +94,7 @@ import type { SrcLoc } from "../ir/ir.js";
|
||||
export type Milestone = "M1" | "M2" | "M3" | "M4" | "M5" | "later";
|
||||
|
||||
export interface ScrDiagnostic {
|
||||
code: `SC${number}`;
|
||||
code: DiagnosticCode;
|
||||
message: string;
|
||||
loc: SrcLoc;
|
||||
milestone?: Milestone;
|
||||
@@ -669,7 +674,7 @@ export const FENCE_CODES: Record<string, { name: string; status: "unsupported" }
|
||||
};
|
||||
|
||||
export function unsupportedDiag(
|
||||
code: keyof typeof UNSUPPORTED & `SC${number}`,
|
||||
code: keyof typeof UNSUPPORTED & DiagnosticCode,
|
||||
loc: SrcLoc,
|
||||
featureOverride?: string,
|
||||
hintOverride?: string,
|
||||
|
||||
@@ -17,6 +17,12 @@ export interface DynamicSlotEntry {
|
||||
/** The diagnostic the slot's declaration would have failed with. */
|
||||
code: string;
|
||||
reason: string;
|
||||
/** Push-down: the slot keeps its type's compilable shape, and only the
|
||||
* components that do not compile are checked-dynamic. */
|
||||
partial?: boolean;
|
||||
/** Dynamic operations the program performs on the slot's values (the
|
||||
* dynamic-tier sites whose cause names it). */
|
||||
operations?: number;
|
||||
}
|
||||
|
||||
const SLOT_WORD: Record<TypeFailureSlot, string> = {
|
||||
@@ -38,7 +44,10 @@ export function dynamicSlotLabel(entry: DynamicSlotEntry): string {
|
||||
export function dynamicSlotDiag(entry: DynamicSlotEntry): ScrDiagnostic {
|
||||
return {
|
||||
code: DYNAMIC_SLOT_CODE,
|
||||
message: `${dynamicSlotLabel(entry)} is held as a checked-dynamic value: its type has no static lowering (${entry.reason}); reads into typed code are checked`,
|
||||
message:
|
||||
entry.partial === true
|
||||
? `${dynamicSlotLabel(entry)} keeps its compilable shape with checked-dynamic pieces where its type has no static lowering (${entry.reason}); reads of those pieces into typed code are checked`
|
||||
: `${dynamicSlotLabel(entry)} is held as a checked-dynamic value: its type has no static lowering (${entry.reason}); reads into typed code are checked`,
|
||||
loc: entry.loc,
|
||||
hint: "restate the declared type inside the compilable set to keep this slot static; scriptc coverage lists dynamic slots, and --fail-on=dynamic-slots fails on them",
|
||||
};
|
||||
|
||||
@@ -26,7 +26,14 @@ export type UntypedCause =
|
||||
| { kind: "any"; name: string }
|
||||
| { kind: "json-binding"; name: string }
|
||||
| { kind: "json" }
|
||||
| { kind: "javascript" };
|
||||
| { kind: "javascript" }
|
||||
| {
|
||||
kind: "slot";
|
||||
name: string;
|
||||
declaredType: string;
|
||||
/** The declared type this is a dynamic piece of (push-down). */
|
||||
owner?: string;
|
||||
};
|
||||
|
||||
export function causePhrase(cause: UntypedCause): string {
|
||||
switch (cause.kind) {
|
||||
@@ -38,6 +45,10 @@ export function causePhrase(cause: UntypedCause): string {
|
||||
return "untyped `JSON.parse` result";
|
||||
case "javascript":
|
||||
return "untyped JavaScript file";
|
||||
case "slot":
|
||||
return cause.owner === undefined
|
||||
? `\`${cause.name}\` is a dynamic slot (declared \`${cause.declaredType}\`, which does not compile)`
|
||||
: `\`${cause.name}\` is a dynamic piece of \`${cause.owner}\` (declared \`${cause.declaredType}\`, which does not compile)`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,5 +106,8 @@ export function renderPerformanceWarnings(diags: readonly ScrDiagnostic[]): stri
|
||||
const blocks = diags.map(
|
||||
(d) => `warning ${d.code}: ${d.message}${d.hint === undefined ? "" : `\n hint: ${d.hint}`}`,
|
||||
);
|
||||
return `${blocks.join("\n\n")}\n\nThe program compiled. Adding types to these functions lets scriptc compile them to faster code.`;
|
||||
const closing = diags.every((d) => d.code === "SC6104")
|
||||
? "The program compiled. Declared types that compile keep these slots static."
|
||||
: "The program compiled. Adding types to these functions lets scriptc compile them to faster code.";
|
||||
return `${blocks.join("\n\n")}\n\n${closing}`;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { DiagnosticCode } from "./codes.js";
|
||||
import { expect, test } from "vitest";
|
||||
import type { ScrDiagnostic } from "./diagnostic.js";
|
||||
import { SourceLocator, buildEnvelope, categoryOf, groupDiagnostics, hintOf } from "./envelope.js";
|
||||
|
||||
const diag = (
|
||||
code: `SC${number}`,
|
||||
code: DiagnosticCode,
|
||||
message: string,
|
||||
start: number,
|
||||
extra: Partial<ScrDiagnostic> = {},
|
||||
|
||||
@@ -172,6 +172,8 @@ export interface DiagnosticsEnvelope {
|
||||
declaredType: string;
|
||||
code: string;
|
||||
reason: string;
|
||||
partial?: boolean;
|
||||
dynamicOps: number;
|
||||
})[];
|
||||
/** coverage --explain-dynamic only: the report-only tier assignment. */
|
||||
tiers?: TiersEnvelope;
|
||||
@@ -239,7 +241,7 @@ export function isDivergenceCode(code: string): boolean {
|
||||
}
|
||||
|
||||
export function categoryOf(diag: ScrDiagnostic): DiagnosticCategory {
|
||||
const code = diag.code;
|
||||
const code: string = diag.code;
|
||||
if (isDivergenceCode(code)) return "divergence";
|
||||
if (isPerformanceCode(code)) return "performance";
|
||||
if (code === "SC0001") return diag.typeWorld === true ? "environment" : "invalid-source";
|
||||
@@ -257,7 +259,7 @@ export function categoryOf(diag: ScrDiagnostic): DiagnosticCategory {
|
||||
* carries one. */
|
||||
export function hintOf(diag: ScrDiagnostic): string {
|
||||
if (diag.hint !== undefined && diag.hint !== "") return diag.hint;
|
||||
const code = diag.code;
|
||||
const code: string = diag.code;
|
||||
if (code === "SC0001")
|
||||
return "fix the TypeScript error; scriptc only compiles programs that typecheck under its type world (strict null checks, the ES2025 library, Node types)";
|
||||
if (code === "SC1090")
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* variable and class field types, and the class markers (Acyclic,
|
||||
* ProgramLifetime) in heritage clauses. The IR-side checks live in
|
||||
* ir/contract-check.ts. */
|
||||
import type { DiagnosticCode } from "../diagnostics/codes.js";
|
||||
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type { SrcLoc } from "../ir/ir.js";
|
||||
import { locOf } from "./program.js";
|
||||
@@ -134,7 +135,7 @@ function nameText(node: ts.Node | undefined): string {
|
||||
}
|
||||
|
||||
function contractDiag(
|
||||
code: `SC${number}`,
|
||||
code: DiagnosticCode,
|
||||
loc: SrcLoc,
|
||||
message: string,
|
||||
hint: string,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { shapeOverlayAnnotated } from "../shape-inference/index.js";
|
||||
import { layoutReturnType } from "./structural-instances.js";
|
||||
import {
|
||||
typedArgumentsCandidate,
|
||||
typedArgumentsShapes,
|
||||
@@ -897,6 +898,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara
|
||||
lowerer.badType(nameBlame, lowerer.typeOf(nameBlame));
|
||||
}
|
||||
|
||||
if (!isAsync && !isGenerator) returnType = layoutReturnType(lowerer, decl, returnType);
|
||||
const symbol = declSymbolOf(lowerer, decl);
|
||||
if (!symbol) lowerer.unsupported("SC1090", decl, "this function form");
|
||||
lowerer.fnSigsBySymbol.set(symbol, {
|
||||
|
||||
@@ -177,6 +177,8 @@ export function scanDivergences(
|
||||
if (use === null || use === undefined || param === undefined || param.type === undefined)
|
||||
return;
|
||||
if (!isExistingObjectExpression(arg)) return;
|
||||
// The call kept the argument's layout: the callee has the value itself.
|
||||
if (lowerer.layoutKeptNodes.has(arg)) return;
|
||||
const copy = safely(() => widthCopyBetween(lowerer, arg, param.type!));
|
||||
if (copy === undefined || copy === null) return;
|
||||
out.push(
|
||||
@@ -185,6 +187,7 @@ export function scanDivergences(
|
||||
});
|
||||
}
|
||||
for (const decl of typedDecls) {
|
||||
if (lowerer.layoutKeptNodes.has(decl.initializer!)) continue;
|
||||
const name = (decl.name as ts.Identifier).text;
|
||||
const scope = enclosingScope(decl);
|
||||
if (scope === undefined) continue;
|
||||
|
||||
@@ -30,7 +30,7 @@ import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { isCheckedJsSourceFile, isJsSourceFile } from "../program.js";
|
||||
import { isNodeModulesPath } from "../resolve.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { DynamicSlot, Lowerer } from "./lowerer.js";
|
||||
|
||||
export interface HotDynamicFunction {
|
||||
/** The IR function name (`%Point.norm`, `parse`, ...). */
|
||||
@@ -208,9 +208,41 @@ function identifierCause(lowerer: Lowerer, id: ts.Identifier, depth = 0): Untype
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A dynamic slot (localized failure): the node's declared type degraded
|
||||
* to the checked dynamic, wholly or in pieces. Null while nothing degraded. */
|
||||
function slotCause(lowerer: Lowerer, n: ts.Node): UntypedCause | null {
|
||||
if (lowerer.degradedTypes.size === 0) return null;
|
||||
if (!ts.isIdentifier(n) && !ts.isPropertyAccessExpression(n)) return null;
|
||||
// The declared type of the binding or property, not its narrowed flow
|
||||
// type (narrowing makes new checker types).
|
||||
let symbol = lowerer.checker.getSymbolAtLocation(ts.isIdentifier(n) ? n : n.name);
|
||||
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0)
|
||||
symbol = lowerer.checker.getAliasedSymbol(symbol);
|
||||
const type =
|
||||
symbol !== undefined && (symbol.flags & VALUE_BINDING) !== 0
|
||||
? lowerer.checker.getTypeOfSymbol(symbol)
|
||||
: lowerer.checker.getTypeAtLocation(n);
|
||||
const id = (type as { id?: number }).id ?? -1;
|
||||
// A whole degraded type, or a dynamic piece of a pushed-down one. A type
|
||||
// that is also a slot of its own belongs to that slot, not to a container
|
||||
// it is a piece of.
|
||||
const owner = lowerer.degradedTypes.has(id) ? undefined : lowerer.lenientPieces.get(id);
|
||||
if (!lowerer.degradedTypes.has(id) && owner === undefined) return null;
|
||||
const text = ts.getTextOfNode(n);
|
||||
const declaredType = lowerer.checker.typeToString(type);
|
||||
return {
|
||||
kind: "slot",
|
||||
name: text.length > 40 ? `${text.slice(0, 37)}...` : text,
|
||||
declaredType,
|
||||
...(owner !== undefined && owner !== declaredType ? { owner } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Why a node's value has no static type, or null. */
|
||||
function nodeCause(lowerer: Lowerer, n: ts.Node): UntypedCause | null {
|
||||
if (isJsonParse(n)) return { kind: "json" };
|
||||
const slot = slotCause(lowerer, n);
|
||||
if (slot !== null) return slot;
|
||||
const id = rootIdentifier(n);
|
||||
return id === null ? null : identifierCause(lowerer, id);
|
||||
}
|
||||
@@ -219,15 +251,28 @@ function nodeCause(lowerer: Lowerer, n: ts.Node): UntypedCause | null {
|
||||
* the source does not say. The site's own subexpressions are asked first
|
||||
* (the operand of a dynamic `for...of`), then its ancestors. */
|
||||
function causeAt(lowerer: Lowerer, loc: SrcLoc): UntypedCause | null {
|
||||
return causeSiteAt(lowerer, loc)?.cause ?? null;
|
||||
}
|
||||
|
||||
/** causeAt, with the node that names the cause (none for an untyped
|
||||
* JavaScript file). */
|
||||
function causeSiteAt(
|
||||
lowerer: Lowerer,
|
||||
loc: SrcLoc,
|
||||
): { cause: UntypedCause; node?: ts.Node } | null {
|
||||
const sf = lowerer.program.getSourceFile(loc.file);
|
||||
if (sf === undefined) return null;
|
||||
if (isJsSourceFile(sf) && !isCheckedJsSourceFile(sf, lowerer.projectCheckJs))
|
||||
return { kind: "javascript" };
|
||||
return { cause: { kind: "javascript" } };
|
||||
const node = nodeAt(sf, loc.start, loc.end);
|
||||
let found: UntypedCause | null = null;
|
||||
let found: { cause: UntypedCause; node: ts.Node } | null = null;
|
||||
const ask = (n: ts.Node): void => {
|
||||
const cause = nodeCause(lowerer, n);
|
||||
if (cause !== null) found = { cause, node: n };
|
||||
};
|
||||
const descend = (n: ts.Node, depth: number): void => {
|
||||
if (found !== null || depth > 3) return;
|
||||
found = nodeCause(lowerer, n);
|
||||
ask(n);
|
||||
if (found === null)
|
||||
ts.forEachChild(n, (child) => (descend(child, depth + 1), found !== null || undefined));
|
||||
};
|
||||
@@ -237,7 +282,7 @@ function causeAt(lowerer: Lowerer, loc: SrcLoc): UntypedCause | null {
|
||||
found === null && n !== undefined && n !== sf;
|
||||
n = n.parent
|
||||
)
|
||||
found = nodeCause(lowerer, n);
|
||||
ask(n);
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -254,6 +299,10 @@ function causeOf(lowerer: Lowerer, sites: readonly { loc: SrcLoc }[]): UntypedCa
|
||||
/** Generated functions (see scanDynamicTier), after any module prefix. */
|
||||
const GENERATED = /^%*(?:m\d+\.)?%*(?:dyn|caught|init)\./;
|
||||
|
||||
/** Generated helpers that repeat their callers' locations: `%dyn.*` and
|
||||
* `%caught.*`, but not a module's top level (`%init.*`). */
|
||||
const HELPERS = /^%*(?:m\d+\.)?%*(?:dyn|caught)\./;
|
||||
|
||||
/** The likely-hot dynamic-tier functions of `module` (see the header). */
|
||||
export function scanDynamicTier(lowerer: Lowerer, module: IrModule): DynamicTierReport {
|
||||
const facts = new Map<string, FunctionFacts>();
|
||||
@@ -382,3 +431,69 @@ export function dynamicTierWarning(
|
||||
first.loc,
|
||||
);
|
||||
}
|
||||
|
||||
/** Localized failure: dynamic operations attributed to each dynamic slot
|
||||
* of `slots` (by index), over the dynamic-tier functions of the program. A
|
||||
* site counts for the slot the SC6101 cause search names at it: the slot
|
||||
* declared by the binding the site reads (for a dynamic piece, the binding
|
||||
* its access starts from), or else the one slot of the declared type. */
|
||||
export function dynamicSlotOperationCounts(
|
||||
lowerer: Lowerer,
|
||||
bodies: readonly IrFunction[],
|
||||
slots: readonly DynamicSlot[],
|
||||
): number[] {
|
||||
const counts = slots.map(() => 0);
|
||||
if (lowerer.degradedTypes.size === 0) return counts;
|
||||
for (const fn of bodies) {
|
||||
// Generated helpers repeat their callers' locations; top-level code
|
||||
// (`%init.*`) counts like any function.
|
||||
if (HELPERS.test(fn.name)) continue;
|
||||
for (const site of dynamicOperationSites(fn)) {
|
||||
const found = causeSiteAt(lowerer, site.loc);
|
||||
if (found?.cause.kind !== "slot" || found.node === undefined) continue;
|
||||
const declaredType = found.cause.owner ?? found.cause.declaredType;
|
||||
const index = slotOfSite(lowerer, slots, found.node, declaredType, found.cause.owner);
|
||||
if (index >= 0) counts[index]!++;
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
/** The index in `slots` of the slot a dynamic operation on `node` (an
|
||||
* identifier or property access the cause search named) belongs to, or -1
|
||||
* when the declared type is not one slot's and no declaration says. */
|
||||
function slotOfSite(
|
||||
lowerer: Lowerer,
|
||||
slots: readonly DynamicSlot[],
|
||||
node: ts.Node,
|
||||
declaredType: string,
|
||||
owner: string | undefined,
|
||||
): number {
|
||||
const candidates: number[] = [];
|
||||
slots.forEach((slot, i) => {
|
||||
if (slot.failure.declaredType === declaredType) candidates.push(i);
|
||||
});
|
||||
if (candidates.length <= 1) return candidates[0] ?? -1;
|
||||
// The binding: the identifier or property itself for a whole slot, the
|
||||
// root of the access for a piece of a container.
|
||||
const named =
|
||||
owner !== undefined
|
||||
? rootIdentifier(node)
|
||||
: ts.isPropertyAccessExpression(node)
|
||||
? node.name
|
||||
: node;
|
||||
if (named === null) return -1;
|
||||
let symbol = lowerer.checker.getSymbolAtLocation(named);
|
||||
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0)
|
||||
symbol = lowerer.checker.getAliasedSymbol(symbol);
|
||||
for (const decl of symbol?.declarations ?? []) {
|
||||
const file = ts.getSourceFileOfNode(decl).fileName;
|
||||
const start = ts.tokenStartOfNode(decl);
|
||||
const index = candidates.find((i) => {
|
||||
const loc = slots[i]!.loc;
|
||||
return loc.file === file && loc.start >= start && loc.start < decl.end;
|
||||
});
|
||||
if (index !== undefined) return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1652,7 +1652,7 @@ export function implicitMonoFile(sf: ts.SourceFile): boolean {
|
||||
* parameter symbol — assignment, compound assignment, ++/--, a
|
||||
* destructuring-assignment target, or a for-in/of cursor. A written
|
||||
* param's binding could lie after the write, so it stays dyn. */
|
||||
function paramWrittenInBody(
|
||||
export function paramWrittenInBody(
|
||||
lowerer: Lowerer,
|
||||
body: ts.Node,
|
||||
sym: ts.Symbol,
|
||||
@@ -2376,18 +2376,46 @@ export function implicitDefaultInstance(
|
||||
return internImplicitInstance(lowerer, blame, info, shapes, argTypes);
|
||||
}
|
||||
|
||||
function internImplicitInstance(
|
||||
export function internImplicitInstance(
|
||||
lowerer: Lowerer,
|
||||
blame: ts.Node,
|
||||
info: GenericFnInfo,
|
||||
shapes: ParamShape[],
|
||||
argTypes: Map<ts.Symbol, ts.Type>,
|
||||
patternKey = "",
|
||||
declaredReturn?: IrType,
|
||||
): GenericInstance {
|
||||
return internImplicitInstanceOr(
|
||||
lowerer,
|
||||
blame,
|
||||
info,
|
||||
shapes,
|
||||
argTypes,
|
||||
patternKey,
|
||||
declaredReturn,
|
||||
false,
|
||||
)!;
|
||||
}
|
||||
|
||||
/** internImplicitInstance; with `fallible`, a body that does not lower
|
||||
* leaves no instance and no diagnostics, and the answer is null (the caller
|
||||
* keeps its own lowering). */
|
||||
export function internImplicitInstanceOr(
|
||||
lowerer: Lowerer,
|
||||
blame: ts.Node,
|
||||
info: GenericFnInfo,
|
||||
shapes: ParamShape[],
|
||||
argTypes: Map<ts.Symbol, ts.Type>,
|
||||
patternKey: string,
|
||||
declaredReturn: IrType | undefined,
|
||||
fallible: boolean,
|
||||
): GenericInstance | null {
|
||||
const key = shapes.map((s) => typeKey(s.type)).join(",") + (patternKey ? `|${patternKey}` : "");
|
||||
let inst = info.instances.get(key);
|
||||
if (inst?.implicitState === "failed")
|
||||
if (inst?.implicitState === "failed") {
|
||||
if (fallible) return null;
|
||||
lowerer.unsupported("SC1090", blame, `the failed specialization of '${info.baseName}'`);
|
||||
}
|
||||
if (inst) {
|
||||
// A recursive caller has observed this ABI before inference settles.
|
||||
// Keep the promised return slot and coerce the completed body's returns.
|
||||
@@ -2411,7 +2439,8 @@ function internImplicitInstance(
|
||||
shapes[index]?.type.kind === "dyn" &&
|
||||
broadPromiseParam(lowerer, lowerer.typeOf(param.name)),
|
||||
);
|
||||
const declared = dynamicBroadPromise ? null : implicitDeclaredReturn(lowerer, info);
|
||||
const declared =
|
||||
declaredReturn ?? (dynamicBroadPromise ? null : implicitDeclaredReturn(lowerer, info));
|
||||
inst = {
|
||||
path,
|
||||
name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""),
|
||||
@@ -2432,6 +2461,25 @@ function internImplicitInstance(
|
||||
// JS body must still have a throwing implementation: later references
|
||||
// (including recursive ones) can already hold its cached signature.
|
||||
const diagsBefore = lowerer.diags.length;
|
||||
const fencesBefore = lowerer.runtimeFences.length;
|
||||
const totalBefore = lowerer.stats.statementsTotal;
|
||||
const failedBefore = lowerer.stats.statementsFailed;
|
||||
const skippedBefore = lowerer.stats.functionsSkipped;
|
||||
const blockedBefore = fallible ? new Set(lowerer.blockedBindings) : null;
|
||||
const deferredBefore = fallible ? new Set(lowerer.deferredDiags.keys()) : null;
|
||||
/** Undoes what a failed fallible body recorded: the instance's
|
||||
* statements share their symbols with the declaration's own body. */
|
||||
const rollback = (): void => {
|
||||
lowerer.diags.splice(diagsBefore);
|
||||
lowerer.runtimeFences.splice(fencesBefore);
|
||||
lowerer.stats.statementsTotal = totalBefore;
|
||||
lowerer.stats.statementsFailed = failedBefore;
|
||||
lowerer.stats.functionsSkipped = skippedBefore;
|
||||
for (const symbol of [...lowerer.blockedBindings])
|
||||
if (!blockedBefore!.has(symbol)) lowerer.blockedBindings.delete(symbol);
|
||||
for (const symbol of [...lowerer.deferredDiags.keys()])
|
||||
if (!deferredBefore!.has(symbol)) lowerer.deferredDiags.delete(symbol);
|
||||
};
|
||||
// The instance is cached even when a receiver probe requested it.
|
||||
// Its body must retain statement fences instead of dropping failures
|
||||
// into that probe's temporary diagnostic sink and caching empty code.
|
||||
@@ -2451,10 +2499,22 @@ function internImplicitInstance(
|
||||
...(info.decl.body ? [info.decl.body] : []),
|
||||
]);
|
||||
const fn = lowerer.lowerGenericInstance(info, inst);
|
||||
if (
|
||||
fallible &&
|
||||
(lowerer.diags.length > diagsBefore || lowerer.runtimeFences.length > fencesBefore)
|
||||
) {
|
||||
inst.implicitState = "failed";
|
||||
rollback();
|
||||
return null;
|
||||
}
|
||||
lowerer.implicitFns.push(fn);
|
||||
} catch (e) {
|
||||
if (!(e instanceof PoisonError)) throw e;
|
||||
inst.implicitState = "failed";
|
||||
if (fallible) {
|
||||
rollback();
|
||||
return null;
|
||||
}
|
||||
const types: IrType[] = shapes.map((shape) => shape.type);
|
||||
if (info.member?.kind === "method")
|
||||
types.unshift({ kind: "object", className: info.member.cls.def.name });
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
arrayClassReceiver,
|
||||
lowerArrayClassStaticCall,
|
||||
} from "./builtins/array-classes.js";
|
||||
import { structuralCallInstance } from "./structural-instances.js";
|
||||
import { checkedPrivateMethodReceiver, privateNameClass } from "./private-brands.js";
|
||||
import { assertFunctionAliasOf } from "./lower-assert.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
@@ -1368,6 +1369,16 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
sig &&
|
||||
!(jsSpreadArgs && spreadNeedsRuntimeArity(lowerer, sig.params, expr.arguments.nodes))
|
||||
) {
|
||||
const layout = structuralCallInstance(lowerer, expr, sig);
|
||||
if (layout) {
|
||||
return reconcileOverloadReturn(lowerer, expr, {
|
||||
kind: "call",
|
||||
callee: layout.name,
|
||||
args: lowerer.completeArgs(expr.arguments.nodes, layout.params, loc, expr),
|
||||
type: layout.returnType,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
lowerer.noteEdge(sig.name);
|
||||
const args = lowerer.completeArgs(expr.arguments.nodes, sig.params, loc, expr);
|
||||
const symbol = lowerer.resolveValueSymbol(expr.expression);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { DiagnosticCode } from "../../diagnostics/codes.js";
|
||||
import {
|
||||
arrayClassMemberOwner,
|
||||
arrayOfClassInstance,
|
||||
@@ -810,7 +811,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
const entry = UNSUPPORTED_EXPR[expr.kind];
|
||||
if (entry)
|
||||
lowerer.unsupported(
|
||||
entry.code as `SC${number}` & keyof typeof UNSUPPORTED,
|
||||
entry.code as DiagnosticCode & keyof typeof UNSUPPORTED,
|
||||
expr,
|
||||
entry.feature,
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { DiagnosticCode } from "../../diagnostics/codes.js";
|
||||
import { keepsInitializerLayout, notePreservedLayout } from "./structural-instances.js";
|
||||
import { jsGlobalValueByName } from "./global-values.js";
|
||||
import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js";
|
||||
import {
|
||||
@@ -758,10 +760,18 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
|
||||
// out of tdzPredeclared; a retry after a later declarator degrades
|
||||
// must initialize the same box again.
|
||||
const tdzBefore = new Map(lowerer.ctx.tdzPredeclared);
|
||||
// The first attempt's diagnostics: a statement that still fails
|
||||
// after degrading reports them, since a retry does not repeat what
|
||||
// lowered once (a poisoned generic instance) and its own diagnostics
|
||||
// can miss the failure's cause.
|
||||
let firstAttempt: ScrDiagnostic[] | null = null;
|
||||
try {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
const hits = new Set<number>();
|
||||
try {
|
||||
lowerOneStatement(lowerer, stmt, out, usingMarkers, diagsBefore, deferFences);
|
||||
lowerer.withPartialHits(hits, () =>
|
||||
lowerOneStatement(lowerer, stmt, out, usingMarkers, diagsBefore, deferFences),
|
||||
);
|
||||
break;
|
||||
} catch (e) {
|
||||
// Localized failure's statement commit point (design §3.3): a
|
||||
@@ -769,9 +779,21 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
|
||||
// global degrades that type and the statement lowers again.
|
||||
// A declaration this statement flushed inside a probe (where
|
||||
// no retry runs) can retry here too.
|
||||
if (attempt >= MAX_DEGRADE_RETRIES || deferFences) throw e;
|
||||
firstAttempt ??= lowerer.diags.slice(diagsBefore);
|
||||
const giveUp = (): unknown => {
|
||||
if (attempt > 0) lowerer.restoreFirstAttemptDiags(diagsBefore, firstAttempt!);
|
||||
return e;
|
||||
};
|
||||
if (attempt >= MAX_DEGRADE_RETRIES || deferFences) throw giveUp();
|
||||
const slot = lowerer.degradeAtCommit(e, STATEMENT_SLOTS);
|
||||
if (slot === null && !lowerer.reopenFlushedAtCommit(diagsBefore)) throw e;
|
||||
// Pushed-down types the failed attempt mapped widen to the
|
||||
// whole slot before anything else is given up.
|
||||
if (
|
||||
slot === null &&
|
||||
!lowerer.reopenFlushedAtCommit(diagsBefore) &&
|
||||
!lowerer.widenPartials(hits)
|
||||
)
|
||||
throw giveUp();
|
||||
// A failed attempt adds nothing to `out` but the TDZ boxes
|
||||
// forward captures declared (they stay declared), so the
|
||||
// retry appends after them.
|
||||
@@ -863,7 +885,8 @@ const MAX_DEGRADE_RETRIES = 16;
|
||||
/** The slot kinds lowerStmts' commit point degrades: locals, loop
|
||||
* variables and module globals (stage L1), and the parameters and returns
|
||||
* of the function expressions, arrows and methods a statement holds
|
||||
* (stage L2). */
|
||||
* (stage L2), and the types of the statement's own expressions: casts,
|
||||
* literals with a contextual type, spread clones, temporaries (stage L4). */
|
||||
const STATEMENT_SLOTS: ReadonlySet<TypeFailureSlot> = new Set<TypeFailureSlot>([
|
||||
"local",
|
||||
"global",
|
||||
@@ -871,6 +894,8 @@ const STATEMENT_SLOTS: ReadonlySet<TypeFailureSlot> = new Set<TypeFailureSlot>([
|
||||
"param",
|
||||
"return",
|
||||
"field",
|
||||
"member",
|
||||
"expression",
|
||||
]);
|
||||
|
||||
/** One attempt at lowering a statement of lowerStmts into `out`. */
|
||||
@@ -1766,8 +1791,8 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(stmt.name);
|
||||
const pre = symbol ? lowerer.ctx.tdzPredeclared.get(symbol) : undefined;
|
||||
if (pre) {
|
||||
lowerer.ctx.tdzPredeclared.delete(symbol!);
|
||||
const value = lowerer.coerceToExpected(lowerer.lowerClassExpression(stmt), DYN);
|
||||
lowerer.ctx.tdzPredeclared.delete(symbol!);
|
||||
return { kind: "assign", localId: pre.id, value, initializes: true, loc: locOf(stmt) };
|
||||
}
|
||||
const type: IrType = { kind: "classval", className: info.def.name };
|
||||
@@ -2041,7 +2066,7 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
|
||||
const entry = UNSUPPORTED_STMT[stmt.kind];
|
||||
if (entry)
|
||||
lowerer.unsupported(
|
||||
entry.code as `SC${number}` & keyof typeof UNSUPPORTED,
|
||||
entry.code as DiagnosticCode & keyof typeof UNSUPPORTED,
|
||||
stmt,
|
||||
entry.feature,
|
||||
);
|
||||
@@ -5787,8 +5812,8 @@ export function lowerVarDecl(
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
|
||||
const pre = symbol ? lowerer.ctx.tdzPredeclared.get(symbol) : undefined;
|
||||
if (pre) {
|
||||
lowerer.ctx.tdzPredeclared.delete(symbol!);
|
||||
const value = genericBindingStoredValue(lowerer, decl, info);
|
||||
lowerer.ctx.tdzPredeclared.delete(symbol!);
|
||||
return { kind: "assign", localId: pre.id, value, initializes: true, loc: locOf(decl) };
|
||||
}
|
||||
return genericBindingPresenceStore(info, decl);
|
||||
@@ -5924,6 +5949,16 @@ export function lowerVarDecl(
|
||||
)
|
||||
g.type = DYN;
|
||||
g.type = inferredOptionalRecordType(lowerer, decl, g.type, raw.type);
|
||||
// A module const keeps its initializer's record or array layout, as a
|
||||
// function's const does (keepsInitializerLayout).
|
||||
if (
|
||||
!isVarDeclared(decl) &&
|
||||
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 &&
|
||||
keepsInitializerLayout(lowerer, decl.initializer, raw.type, g.type)
|
||||
) {
|
||||
g.type = raw.type;
|
||||
notePreservedLayout(lowerer, decl);
|
||||
}
|
||||
const arithmetic =
|
||||
raw.type.kind === "union" ? lowerer.unions.get(raw.type.unionId)?.arms : undefined;
|
||||
if (
|
||||
@@ -5951,11 +5986,13 @@ export function lowerVarDecl(
|
||||
// A forward-captured binding pre-declared as a TDZ box (an earlier
|
||||
// function in this scope captured it — predeclareForwardCapture): the
|
||||
// binding and its scope-entry varDecl already exist; the source
|
||||
// declaration is the one initializing `assign` into the shared box.
|
||||
// declaration is the one initializing `assign` into the shared box. The
|
||||
// box stays pre-declared until the initializer lowers: a statement the
|
||||
// commit point lowers again (localized failure) initializes the same box.
|
||||
const pre = declSymbol ? lowerer.ctx.tdzPredeclared.get(declSymbol) : undefined;
|
||||
if (pre && decl.initializer) {
|
||||
lowerer.ctx.tdzPredeclared.delete(declSymbol!);
|
||||
const init = lowerer.lowerExprExpecting(decl.initializer, pre.type);
|
||||
lowerer.ctx.tdzPredeclared.delete(declSymbol!);
|
||||
return { kind: "assign", localId: pre.id, value: init, initializes: true, loc: locOf(decl) };
|
||||
}
|
||||
|
||||
@@ -6071,6 +6108,17 @@ export function lowerVarDecl(
|
||||
// Promise.prototype is an ordinary object the checker types Promise<any>.
|
||||
if (!decl.type && !hasJsTypeAnnotation(decl) && isPromisePrototypeRead(lowerer, decl.initializer))
|
||||
type = DYN;
|
||||
// A const whose initializer has another record or array layout than the
|
||||
// binding's type keeps the initializer's layout: converting would copy the
|
||||
// value, so the binding would not be the value its initializer names.
|
||||
if (
|
||||
!isLet &&
|
||||
type !== null &&
|
||||
keepsInitializerLayout(lowerer, decl.initializer, init.type, type)
|
||||
) {
|
||||
type = init.type;
|
||||
notePreservedLayout(lowerer, decl);
|
||||
}
|
||||
// A typed-arguments function expression (typed-arguments.ts): the
|
||||
// checker types the binding variadic, but its value has the fixed
|
||||
// signature every use asserts.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { DiagnosticCode } from "../../diagnostics/codes.js";
|
||||
import { markInferredFactChecks, shapeOverlayFileNames } from "../shape-inference/index.js";
|
||||
import { npmStaticPackageOfPath } from "../npm-static.js";
|
||||
import { globalMemberValue } from "./global-values.js";
|
||||
@@ -187,7 +188,7 @@ import { ClassProtocols } from "./class-protocols.js";
|
||||
import { earlyReadVarSymbols } from "./var-hoisting.js";
|
||||
import { recordTextCodecClass } from "../../ir/ir.js";
|
||||
import { assignLegacyTiers, type DynamicFunction } from "../../ir/tiers.js";
|
||||
import { dynamicTierWarning } from "./dynamic-tier.js";
|
||||
import { dynamicSlotOperationCounts, dynamicTierWarning } from "./dynamic-tier.js";
|
||||
import { promoteJsvals } from "../../ir/jsval.js";
|
||||
import {
|
||||
analyzeTiers,
|
||||
@@ -750,6 +751,9 @@ export interface DynamicSlot {
|
||||
failure: TypeFailure;
|
||||
code: string;
|
||||
reason: string;
|
||||
/** The slot keeps its type's compilable shape; only the failing
|
||||
* components are checked-dynamic (push-down, stage L3). */
|
||||
partial?: true;
|
||||
}
|
||||
|
||||
/** Localized failure: rounds of the declaration flush retry. */
|
||||
@@ -964,7 +968,7 @@ export interface LowerOptions {
|
||||
}
|
||||
|
||||
interface RuntimeFenceFallback {
|
||||
code: `SC${number}`;
|
||||
code: DiagnosticCode;
|
||||
message: string;
|
||||
}
|
||||
|
||||
@@ -1001,6 +1005,14 @@ type RuntimeFenceTarget =
|
||||
| RuntimeFenceFunctionTarget
|
||||
| RuntimeFenceClosureTarget;
|
||||
|
||||
/** A degraded checker type (Lowerer.degradedTypes): its lenient mapping and
|
||||
* the failure that degraded it. */
|
||||
export interface DegradedType {
|
||||
lenient: IrType | null;
|
||||
code: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/** The Lowerer's pass configuration (see lowerToIr). */
|
||||
export interface LowererMode {
|
||||
/** Symbols whose deferred diagnostics reachable emit already flushed —
|
||||
@@ -1009,10 +1021,14 @@ export interface LowererMode {
|
||||
/** Localized failure: the types reachable emit degraded (the coverage
|
||||
* remainder maps them alike, so the declarations reachable emit
|
||||
* registered with dynamic slots collect there too). */
|
||||
degradedTypes?: ReadonlyMap<number, { lenient: IrType; code: string; reason: string }>;
|
||||
degradedTypes?: ReadonlyMap<number, DegradedType>;
|
||||
/** Localized failure: the declarations reachable emit registered with
|
||||
* dynamic slots (the coverage remainder registers them too). */
|
||||
retriedSymbols?: ReadonlySet<ts.Symbol>;
|
||||
/** Localized failure: degrade every type to the whole-slot checked
|
||||
* dynamic, never to its pushed-down shape (lowerToIr's comparison
|
||||
* pass). */
|
||||
wholeSlotDegradation?: boolean;
|
||||
/** LowerOptions.externalTypes, threaded through every lowering pass. */
|
||||
externalTypes?: ReadonlyMap<string, string>;
|
||||
/** LowerOptions.externalTypeSpecifiersByFile, shared by every pass. */
|
||||
@@ -1230,6 +1246,39 @@ export function lowerToIr(
|
||||
let result = emitted.result;
|
||||
let resultLowerer = reachableEmit;
|
||||
timing("reachable-emit", { reachable: reachable.size });
|
||||
// Localized failure (stage L3): push-down must never cost a failing
|
||||
// program more errors than the whole-slot checked dynamic. A pushed-down
|
||||
// type is baked into the globals, fields and signatures that hold it, so
|
||||
// when the program still fails, the emit runs once more with every
|
||||
// degraded type held whole, and the pass with fewer errors wins (ties
|
||||
// keep push-down). A program that compiles never takes this path.
|
||||
if (result.diagnostics.length > 0 && reachableEmit.hasPartialDegradedTypes()) {
|
||||
const whole = new Lowerer(program, entry, moduleOrder, {
|
||||
frontendServices: options.frontendServices,
|
||||
projectCheckJs: options.projectCheckJs ?? false,
|
||||
targetPlatform,
|
||||
startupCrash,
|
||||
inlineRequireCycleCalls,
|
||||
ffiImports,
|
||||
libraryCallbacks,
|
||||
forkTargets,
|
||||
workerTargets,
|
||||
externalTypes,
|
||||
externalTypeSpecifiersByFile,
|
||||
tierMode,
|
||||
ffiBindingSymbols: ffiValidation.symbolsByName,
|
||||
wholeSlotDegradation: true,
|
||||
});
|
||||
for (const d of extraModuleCycleDiags) whole.pushDiag(d);
|
||||
for (const d of ffiValidation.diagnostics) whole.pushDiag(d);
|
||||
const wholeEmitted = whole.emitReachable(options.libRoots);
|
||||
if (wholeEmitted.result.diagnostics.length < result.diagnostics.length) {
|
||||
reachable = wholeEmitted.reachable;
|
||||
result = wholeEmitted.result;
|
||||
resultLowerer = whole;
|
||||
}
|
||||
timing("whole-slot-relower", { reachable: wholeEmitted.reachable.size });
|
||||
}
|
||||
// The general stream representation (TypeMapperCtx.generalNodeStreams)
|
||||
// is a per-program decision: only a program whose reachable code moved
|
||||
// a general stream into a stdio-represented stream slot lowers again
|
||||
@@ -1256,6 +1305,7 @@ export function lowerToIr(
|
||||
ffiBindingSymbols: ffiValidation.symbolsByName,
|
||||
generalNodeStreams: true,
|
||||
...(dyn ? { dynNodeStreams: true } : {}),
|
||||
...(resultLowerer.wholeSlotDegradation ? { wholeSlotDegradation: true } : {}),
|
||||
});
|
||||
for (const d of extraModuleCycleDiags) general.pushDiag(d);
|
||||
for (const d of ffiValidation.diagnostics) general.pushDiag(d);
|
||||
@@ -1298,6 +1348,7 @@ export function lowerToIr(
|
||||
externalTypes,
|
||||
externalTypeSpecifiersByFile,
|
||||
tierMode,
|
||||
...(resultLowerer.wholeSlotDegradation ? { wholeSlotDegradation: true } : {}),
|
||||
});
|
||||
for (const d of extraModuleCycleDiags) emit.pushDiag(d);
|
||||
for (const d of ffiValidation.diagnostics) emit.pushDiag(d);
|
||||
@@ -1319,13 +1370,20 @@ export function lowerToIr(
|
||||
}
|
||||
// Localized failure (SC6104): one performance warning per dynamic slot.
|
||||
if (resultLowerer.dynamicSlots.length > 0) {
|
||||
const dynamicSlots: DynamicSlotEntry[] = resultLowerer.dynamicSlots.map((slot) => ({
|
||||
const operations = dynamicSlotOperationCounts(
|
||||
resultLowerer,
|
||||
resultLowerer.dynamicBodies,
|
||||
resultLowerer.dynamicSlots,
|
||||
);
|
||||
const dynamicSlots: DynamicSlotEntry[] = resultLowerer.dynamicSlots.map((slot, i) => ({
|
||||
operations: operations[i]!,
|
||||
loc: slot.loc,
|
||||
name: slot.failure.name,
|
||||
slot: slot.failure.slot,
|
||||
declaredType: slot.failure.declaredType,
|
||||
code: slot.code,
|
||||
reason: slot.reason,
|
||||
...(slot.partial ? { partial: true } : {}),
|
||||
}));
|
||||
result = {
|
||||
...result,
|
||||
@@ -2033,6 +2091,15 @@ export class Lowerer {
|
||||
* exact binding discipline as globals/functions. */
|
||||
readonly staticCallables = new Map<ts.Symbol, StaticCallableProjection>();
|
||||
readonly genericFnsBySymbol = new Map<ts.Symbol, GenericFnInfo>();
|
||||
/** Typed declarations' instances for arguments that keep their own
|
||||
* record or array layout (structural-instances.ts). */
|
||||
readonly structuralFnInfos = new Map<ts.Symbol, GenericFnInfo>();
|
||||
/** Consts that keep their initializer's record or array layout, with the
|
||||
* initializer's checker type (structural-instances.ts). */
|
||||
readonly preservedLayoutTypes = new Map<ts.Symbol, ts.Type>();
|
||||
/** Arguments and const initializers whose array or record layout the
|
||||
* lowering kept instead of copying (divergences.ts reports no copy there). */
|
||||
readonly layoutKeptNodes = new Set<ts.Node>();
|
||||
/** Object-literal GENERIC methods (`{ m<T>(x: T) {...} }`), interned by
|
||||
* their function-like node — instances ride the same monomorphization
|
||||
* queue (objLitGenericFnInfoOf). */
|
||||
@@ -3892,13 +3959,24 @@ export class Lowerer {
|
||||
* at a committed slot, with their lenient mapping. Filled only at the
|
||||
* commit points (degradeAtCommit), so a program that compiles today
|
||||
* never fills it. */
|
||||
readonly degradedTypes = new Map<number, { lenient: IrType; code: string; reason: string }>();
|
||||
readonly degradedTypes = new Map<number, DegradedType>();
|
||||
/** A pushed-down (partial) degraded type was mapped (hasPartialDegradedTypes). */
|
||||
private partialDegradedTypesMapped = false;
|
||||
/** The components lenient mappings turned into the checked dynamic, by
|
||||
* checker type id, with the declared type they are pieces of (SC6101
|
||||
* causes and per-slot operation counts). */
|
||||
readonly lenientPieces = new Map<number, string>();
|
||||
/** One frame per unit a commit point may retry: the pushed-down
|
||||
* (partial) degraded types its lowering mapped (withPartialHits). */
|
||||
readonly partialHits: Set<number>[] = [];
|
||||
/** The dynamic slots the commit points created: one SC6104 each. */
|
||||
readonly dynamicSlots: DynamicSlot[] = [];
|
||||
/** Declarations retryDeferred registered. */
|
||||
readonly retriedSymbols = new Set<ts.Symbol>();
|
||||
/** LowererMode.retriedSymbols. */
|
||||
readonly priorRetriedSymbols: ReadonlySet<ts.Symbol>;
|
||||
/** LowererMode.wholeSlotDegradation. */
|
||||
readonly wholeSlotDegradation: boolean;
|
||||
/** Bodies run() skipped because their declaration had not registered. */
|
||||
readonly skippedBodies = new Set<ts.Node>();
|
||||
/** Declarations retryDeferred registered, in order: run() lowers the
|
||||
@@ -4078,9 +4156,16 @@ export class Lowerer {
|
||||
) {
|
||||
// The mode's options, sorted by name; the values derived from them follow.
|
||||
this.alreadyFlushed = mode.alreadyFlushed ?? new Set();
|
||||
// Lenient forms are IR types of the other pass's shape tables: only the
|
||||
// whole-slot dynamic carries over as is.
|
||||
if (mode.degradedTypes)
|
||||
for (const [id, degraded] of mode.degradedTypes) this.degradedTypes.set(id, degraded);
|
||||
for (const [id, degraded] of mode.degradedTypes)
|
||||
this.degradedTypes.set(id, {
|
||||
...degraded,
|
||||
lenient: degraded.lenient?.kind === "dyn" ? DYN : null,
|
||||
});
|
||||
this.priorRetriedSymbols = mode.retriedSymbols ?? new Set();
|
||||
this.wholeSlotDegradation = mode.wholeSlotDegradation ?? false;
|
||||
this.externalTypes = mode.externalTypes ?? new Map();
|
||||
this.ffiBindingSymbols = mode.ffiBindingSymbols ?? null;
|
||||
this.ffiImports = mode.ffiImports ?? [];
|
||||
@@ -4119,7 +4204,21 @@ export class Lowerer {
|
||||
resolveBoundAliasTs: this.boundAliasTsResolver,
|
||||
degradedType: (type) => {
|
||||
const id = (type as { id?: number }).id;
|
||||
return id === undefined ? null : (this.degradedTypes.get(id)?.lenient ?? null);
|
||||
if (id === undefined) return null;
|
||||
const degraded = this.degradedTypes.get(id);
|
||||
if (degraded === undefined) return null;
|
||||
// An entry handed over from another pass (the coverage remainder)
|
||||
// re-derives its lenient form in this lowerer's shape tables; the
|
||||
// checked dynamic cuts the knot while it does.
|
||||
if (degraded.lenient === null) {
|
||||
degraded.lenient = DYN;
|
||||
degraded.lenient = this.lenientType(type);
|
||||
}
|
||||
if (degraded.lenient.kind !== "dyn") {
|
||||
this.partialHits[this.partialHits.length - 1]?.add(id);
|
||||
this.partialDegradedTypesMapped = true;
|
||||
}
|
||||
return degraded.lenient;
|
||||
},
|
||||
...(shadow !== undefined ? { canonShadow: shadow } : {}),
|
||||
isErasing: () => this.erasingDepth > 0,
|
||||
@@ -9714,7 +9813,7 @@ export class Lowerer {
|
||||
}
|
||||
|
||||
unsupported(
|
||||
code: keyof typeof UNSUPPORTED & `SC${number}`,
|
||||
code: keyof typeof UNSUPPORTED & DiagnosticCode,
|
||||
node: ts.Node,
|
||||
featureOverride?: string,
|
||||
hintOverride?: string,
|
||||
@@ -9781,8 +9880,9 @@ export class Lowerer {
|
||||
if (!failure.degradable || failure.family === "dyn-field-policy") return null;
|
||||
if (!slots.has(failure.slot)) return null;
|
||||
const id = (type as { id?: number }).id;
|
||||
// Already degraded and still failing: no progress, commit as today.
|
||||
if (id === undefined || this.degradedTypes.has(id)) return null;
|
||||
// Already degraded to the whole-slot dynamic and still failing: no
|
||||
// progress, commit as today.
|
||||
if (id === undefined || this.degradedTypes.get(id)?.lenient?.kind === "dyn") return null;
|
||||
let committed: ScrDiagnostic | undefined;
|
||||
for (let i = this.diags.length - 1; i >= 0; i--) {
|
||||
if (this.diags[i]!.typeFailure !== failure) continue;
|
||||
@@ -9790,8 +9890,15 @@ export class Lowerer {
|
||||
this.diags.splice(i, 1);
|
||||
}
|
||||
if (committed === undefined) return null;
|
||||
this.degradedTypes.set(id, { lenient: DYN, code: committed.code, reason: committed.message });
|
||||
return { loc: committed.loc, failure, code: committed.code, reason: committed.message };
|
||||
this.degradeType(type, committed.code, committed.message);
|
||||
const partial = this.degradedTypes.get(id)?.lenient?.kind !== "dyn";
|
||||
return {
|
||||
...(partial ? { partial: true as const } : {}),
|
||||
loc: committed.loc,
|
||||
failure,
|
||||
code: committed.code,
|
||||
reason: committed.message,
|
||||
};
|
||||
}
|
||||
|
||||
/** A declaration slot whose type maps through degradedTypes is a
|
||||
@@ -9800,7 +9907,9 @@ export class Lowerer {
|
||||
noteDegradedSlot(node: ts.Node, type: ts.Type): void {
|
||||
if (this.diagSink !== null) return;
|
||||
const degraded = this.degradedTypes.get((type as { id?: number }).id ?? -1);
|
||||
if (degraded === undefined) return;
|
||||
// A pushed-down type is one slot, reported where it degraded; only
|
||||
// whole-slot dynamic declarations are reported one by one.
|
||||
if (degraded === undefined || degraded.lenient?.kind !== "dyn") return;
|
||||
const failure = typeFailureOf(node, this.checker.typeToString(type), "unsupported");
|
||||
if (failure.slot === "expression" || failure.slot === "member") return;
|
||||
this.noteDynamicSlot({
|
||||
@@ -9858,6 +9967,19 @@ export class Lowerer {
|
||||
this.diags.push(...kept);
|
||||
}
|
||||
|
||||
/** A unit that still fails after its retries commits its first
|
||||
* attempt's own diagnostics (`first`, taken before the first
|
||||
* degradation) in place of the last attempt's. The diagnostics the
|
||||
* retries carry (dropAttemptDiags) stay as they are now: a declaration
|
||||
* reopened since may have registered. */
|
||||
restoreFirstAttemptDiags(diagsBefore: number, first: readonly ScrDiagnostic[]): void {
|
||||
const carried = (d: ScrDiagnostic): boolean =>
|
||||
d.rejects !== undefined || this.loweredOnceDiags.has(d);
|
||||
const kept = this.diags.slice(diagsBefore).filter(carried);
|
||||
this.diags.length = diagsBefore;
|
||||
this.diags.push(...first.filter((d) => !carried(d)), ...kept);
|
||||
}
|
||||
|
||||
/** Lowers one unit whose body-level poison skips it (a generic
|
||||
* instance), with localized failure's commit point: a degradable type
|
||||
* failure at one of its declaration slots degrades the type and the unit
|
||||
@@ -9867,21 +9989,100 @@ export class Lowerer {
|
||||
const slots: DynamicSlot[] = [];
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
const mark = this.diags.length;
|
||||
const hits = new Set<number>();
|
||||
try {
|
||||
const result = lower();
|
||||
const result = this.withPartialHits(hits, lower);
|
||||
for (const slot of slots) this.noteDynamicSlot(slot);
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (!(e instanceof PoisonError)) throw e;
|
||||
if (attempt >= MAX_DECLARATION_RETRIES) return null;
|
||||
const slot = this.degradeAtCommit(e, DECLARATION_SLOTS);
|
||||
if (slot === null) return null;
|
||||
slots.push(slot);
|
||||
if (slot === null && !this.widenPartials(hits)) return null;
|
||||
if (slot !== null) slots.push(slot);
|
||||
this.dropAttemptDiags(mark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Localized failure's lenient mapping (stage L3, push-down): the type
|
||||
* mapped with each failing component as the checked dynamic, so a record
|
||||
* keeps its static members, an array of records its records, and a
|
||||
* function its arity. The whole slot is the checked dynamic when even
|
||||
* that shape does not map. Throwaway context: nothing memoizes. */
|
||||
lenientType(type: ts.Type): IrType {
|
||||
const { typeMemo: _strictMemo, ...strict } = this.typeCtx;
|
||||
const owner = this.checker.typeToString(type);
|
||||
const mapped = mapType(type, {
|
||||
...strict,
|
||||
lenient: true,
|
||||
canMemoizeType: () => false,
|
||||
onLenientPiece: (piece) => {
|
||||
const id = (piece as { id?: number }).id;
|
||||
if (id !== undefined && !this.lenientPieces.has(id)) this.lenientPieces.set(id, owner);
|
||||
},
|
||||
});
|
||||
return mapped === null || mapped.kind === "void" ? DYN : mapped;
|
||||
}
|
||||
|
||||
/** Degrades a type for a commit point: its lenient (pushed-down) form
|
||||
* first; a type already degraded that still fails falls back to the
|
||||
* whole-slot checked dynamic. False when nothing is left to degrade. */
|
||||
private degradeType(type: ts.Type, code: string, reason: string): boolean {
|
||||
const id = (type as { id?: number }).id;
|
||||
if (id === undefined) return false;
|
||||
const known = this.degradedTypes.get(id);
|
||||
if (known !== undefined) {
|
||||
if (known.lenient?.kind === "dyn") return false;
|
||||
this.degradedTypes.set(id, { lenient: DYN, code, reason });
|
||||
return true;
|
||||
}
|
||||
this.degradedTypes.set(id, {
|
||||
lenient: this.wholeSlotDegradation ? DYN : this.lenientType(type),
|
||||
code,
|
||||
reason,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Some degraded type kept a pushed-down (partial) shape, or one was
|
||||
* mapped before a retry widened it: the IR lowered before the retry keeps
|
||||
* that shape. */
|
||||
hasPartialDegradedTypes(): boolean {
|
||||
if (this.partialDegradedTypesMapped) return true;
|
||||
for (const degraded of this.degradedTypes.values())
|
||||
if (degraded.lenient !== null && degraded.lenient.kind !== "dyn") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Runs a unit a commit point may retry, recording the pushed-down
|
||||
* types it maps; the frame's hits also count for the enclosing unit. */
|
||||
withPartialHits<T>(hits: Set<number>, run: () => T): T {
|
||||
this.partialHits.push(hits);
|
||||
try {
|
||||
return run();
|
||||
} finally {
|
||||
this.partialHits.pop();
|
||||
const outer = this.partialHits[this.partialHits.length - 1];
|
||||
if (outer !== undefined) for (const id of hits) outer.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
/** Push-down is a refinement, never a new failure: a unit that still
|
||||
* fails after mapping pushed-down types falls back to the whole-slot
|
||||
* checked dynamic for them. True when some type widened (retry). */
|
||||
widenPartials(hits: ReadonlySet<number>): boolean {
|
||||
if (this.tierMode !== "auto" || this.diagSink !== null) return false;
|
||||
let widened = false;
|
||||
for (const id of hits) {
|
||||
const degraded = this.degradedTypes.get(id);
|
||||
if (degraded === undefined || degraded.lenient?.kind === "dyn") continue;
|
||||
degraded.lenient = DYN;
|
||||
widened = true;
|
||||
}
|
||||
return widened;
|
||||
}
|
||||
|
||||
/** Records a dynamic slot whose unit lowered with it (one per site). */
|
||||
noteDynamicSlot(slot: DynamicSlot): void {
|
||||
const seen = this.dynamicSlots.some(
|
||||
@@ -12985,8 +13186,9 @@ export class Lowerer {
|
||||
if (collector === undefined) return false;
|
||||
const slots: DynamicSlot[] = [];
|
||||
// The types this retry degraded (not those of retries its collections
|
||||
// ran), for retryDeferredAtDeclaration to restore when it fails.
|
||||
const degraded: number[] = [];
|
||||
// ran) and their entries before it, for retryDeferredAtDeclaration to
|
||||
// restore when it fails.
|
||||
const degraded: { id: number; prior: DegradedType | undefined }[] = [];
|
||||
try {
|
||||
return this.retryDeferredRounds(symbol, collector, diags, slots, degraded);
|
||||
} finally {
|
||||
@@ -12999,11 +13201,11 @@ export class Lowerer {
|
||||
collector: { symbolOf: () => ts.Symbol | undefined; collect: () => void },
|
||||
diags: readonly ScrDiagnostic[],
|
||||
slots: DynamicSlot[],
|
||||
degraded: number[],
|
||||
degraded: { id: number; prior: DegradedType | undefined }[],
|
||||
): boolean {
|
||||
let current = diags;
|
||||
for (let round = 0; round < MAX_DECLARATION_RETRIES; round++) {
|
||||
const failing: { diag: ScrDiagnostic; failure: TypeFailure; id: number }[] = [];
|
||||
const failing: { diag: ScrDiagnostic; failure: TypeFailure; type: ts.Type }[] = [];
|
||||
let retriable = false;
|
||||
for (const diag of current) {
|
||||
if (this.retriableDiags.has(diag)) {
|
||||
@@ -13021,28 +13223,54 @@ export class Lowerer {
|
||||
const id = (type as { id?: number } | undefined)?.id;
|
||||
if (
|
||||
failure === undefined ||
|
||||
type === undefined ||
|
||||
id === undefined ||
|
||||
!failure.degradable ||
|
||||
!DECLARATION_SLOTS.has(failure.slot) ||
|
||||
this.degradedTypes.has(id)
|
||||
this.degradedTypes.get(id)?.lenient?.kind === "dyn"
|
||||
)
|
||||
return false;
|
||||
failing.push({ diag, failure, id });
|
||||
failing.push({ diag, failure, type });
|
||||
}
|
||||
// A round makes progress by degrading a type, or (once) by running
|
||||
// a collection that only waited for another declaration.
|
||||
if (failing.length === 0 && !(retriable && round === 0)) return false;
|
||||
for (const { diag, failure, id } of failing) {
|
||||
this.degradedTypes.set(id, { lenient: DYN, code: diag.code, reason: diag.message });
|
||||
degraded.push(id);
|
||||
slots.push({ loc: diag.loc, failure, code: diag.code, reason: diag.message });
|
||||
for (const { diag, failure, type } of failing) {
|
||||
const id = (type as { id?: number }).id ?? -1;
|
||||
const prior = this.degradedTypes.get(id);
|
||||
degraded.push({
|
||||
id,
|
||||
prior:
|
||||
prior === undefined
|
||||
? undefined
|
||||
: { lenient: prior.lenient, code: prior.code, reason: prior.reason },
|
||||
});
|
||||
this.degradeType(type, diag.code, diag.message);
|
||||
const partial = this.degradedTypes.get(id)?.lenient?.kind !== "dyn";
|
||||
slots.push({
|
||||
loc: diag.loc,
|
||||
failure,
|
||||
code: diag.code,
|
||||
reason: diag.message,
|
||||
...(partial ? { partial: true as const } : {}),
|
||||
});
|
||||
}
|
||||
this.deferredDiags.delete(symbol);
|
||||
const lifted = this.liftDynFieldPolicy;
|
||||
this.liftDynFieldPolicy = true;
|
||||
let registered: boolean;
|
||||
// Pushed-down types that did not help widen to the whole slot, and the
|
||||
// collection runs again (a few times at most).
|
||||
let registered = false;
|
||||
try {
|
||||
registered = this.collectDeferring(collector.symbolOf, collector.collect) === null;
|
||||
for (let widen = 0; widen < 4; widen++) {
|
||||
const hits = new Set<number>();
|
||||
registered =
|
||||
this.withPartialHits(hits, () =>
|
||||
this.collectDeferring(collector.symbolOf, collector.collect),
|
||||
) === null;
|
||||
if (registered || !this.widenPartials(hits)) break;
|
||||
this.deferredDiags.delete(symbol);
|
||||
}
|
||||
} finally {
|
||||
this.liftDynFieldPolicy = lifted;
|
||||
}
|
||||
@@ -13064,7 +13292,7 @@ export class Lowerer {
|
||||
return false;
|
||||
}
|
||||
|
||||
private retryDegraded: number[] = [];
|
||||
private retryDegraded: { id: number; prior: DegradedType | undefined }[] = [];
|
||||
|
||||
/** A deferred class with static field initializers is reached where its
|
||||
* statement evaluates: Node runs the initializers there, referenced or
|
||||
@@ -13080,7 +13308,11 @@ export class Lowerer {
|
||||
this.retryDegraded = [];
|
||||
registered = this.retryDeferred(symbol, diags);
|
||||
if (!registered) {
|
||||
for (const id of this.retryDegraded) this.degradedTypes.delete(id);
|
||||
for (let i = this.retryDegraded.length - 1; i >= 0; i--) {
|
||||
const { id, prior } = this.retryDegraded[i]!;
|
||||
if (prior === undefined) this.degradedTypes.delete(id);
|
||||
else this.degradedTypes.set(id, prior);
|
||||
}
|
||||
this.deferredDiags.set(symbol, [...diags]);
|
||||
}
|
||||
this.retryDegraded = [];
|
||||
@@ -13546,8 +13778,7 @@ export class Lowerer {
|
||||
/* ── scoping and captures ─────────────────────────────────────────── */
|
||||
|
||||
declareLocal(nameNode: ts.Node, name: string, type: IrType, mutable: boolean): IrLocal {
|
||||
if (type.kind === "dyn" && this.degradedTypes.size > 0)
|
||||
this.noteDegradedSlot(nameNode, this.typeOf(nameNode));
|
||||
if (this.degradedTypes.size > 0) this.noteDegradedSlot(nameNode, this.typeOf(nameNode));
|
||||
const symbol = this.checker.getSymbolAtLocation(nameNode);
|
||||
const local = declareContextLocal(
|
||||
this.ctx,
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { IrType } from "../../ir/ir.js";
|
||||
import { typeEquals, UNDEFINED_T } from "../../ir/ir.js";
|
||||
import type { FnSig, ParamShape } from "./call-signatures.js";
|
||||
import { isThisParameter } from "./call-signatures.js";
|
||||
import {
|
||||
internImplicitInstanceOr,
|
||||
paramWrittenInBody,
|
||||
type GenericFnInfo,
|
||||
type GenericInstance,
|
||||
} from "./generic-functions.js";
|
||||
import { nsPathPrefix } from "./lower-namespaces.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** Instances of a typed function for arguments that keep their own layout.
|
||||
*
|
||||
* A record or array whose layout differs from a parameter's declared
|
||||
* structural type converts by copying it (`{ id; name; owned?: true }[]`
|
||||
* into `{ id; name }[]`). JavaScript passes the same object, so the
|
||||
* callee's writes (`push`, index stores, element-field writes) and identity
|
||||
* comparisons see the caller's value. When an argument names an existing
|
||||
* value, the call instead targets an instance of the function lowered with
|
||||
* that parameter bound to the argument's own type: the body reads and
|
||||
* writes the caller's storage. Instances are keyed by the parameter
|
||||
* layouts, so each layout combination compiles once. */
|
||||
|
||||
/** The argument expressions that can name a value another reference sees:
|
||||
* bindings, member reads and `this`. Fresh values (literals, calls) have no
|
||||
* other reference, so their copy is not observable. */
|
||||
function namesExistingValue(arg: ts.Expression): boolean {
|
||||
let e = arg;
|
||||
for (;;) {
|
||||
if (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e)) e = e.expression;
|
||||
else if (ts.isAsExpression(e) || ts.isSatisfiesExpression(e)) e = e.expression;
|
||||
else break;
|
||||
}
|
||||
return (
|
||||
ts.isIdentifier(e) ||
|
||||
ts.isPropertyAccessExpression(e) ||
|
||||
ts.isElementAccessExpression(e) ||
|
||||
e.kind === ts.SyntaxKind.ThisKeyword
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether a value of layout `from` can stand for a value of layout `to`
|
||||
* in code written against `to`: the same layout, or a record that has every
|
||||
* field of `to` at such a layout plus fields `to` lacks that are optional
|
||||
* (`{ id; name; owned?: true }` for `{ id; name }`), or an array of such
|
||||
* elements. Every value that code stores converts into `from` by completing
|
||||
* the optional fields as absent, so reads and writes through either type see
|
||||
* one value. */
|
||||
function standsFor(
|
||||
lowerer: Lowerer,
|
||||
from: IrType,
|
||||
to: IrType,
|
||||
assumed: Set<string> = new Set(),
|
||||
): boolean {
|
||||
if (typeEquals(from, to)) return true;
|
||||
if (from.kind === "array" && to.kind === "array")
|
||||
return standsFor(lowerer, from.elem, to.elem, assumed);
|
||||
if (from.kind !== "record" || to.kind !== "record") return false;
|
||||
// Recursive layouts (`{ kids: Tree[] }` against `{ kids: Plain[] }`) meet
|
||||
// the same pair again inside their fields; the pair holds there if it
|
||||
// holds for the other fields.
|
||||
const pair = `${from.shapeId}>${to.shapeId}`;
|
||||
if (assumed.has(pair)) return true;
|
||||
assumed.add(pair);
|
||||
const source = lowerer.shapes.get(from.shapeId);
|
||||
const target = lowerer.shapes.get(to.shapeId);
|
||||
if (!source || !target || source.tuple || target.tuple) return false;
|
||||
if (source.indexValue || target.indexValue) return false;
|
||||
for (const field of target.fields) {
|
||||
const kept = source.fields.find((f) => f.name === field.name);
|
||||
if (!kept || !standsFor(lowerer, kept.type, field.type, assumed)) return false;
|
||||
}
|
||||
return source.fields.every(
|
||||
(field) =>
|
||||
target.fields.some((f) => f.name === field.name) ||
|
||||
(field.type.kind === "union" && lowerer.armTag(field.type.unionId, UNDEFINED_T) >= 0),
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether converting `from` into `to` copies a value that could keep its
|
||||
* own layout instead. */
|
||||
function copies(lowerer: Lowerer, from: IrType, to: IrType): boolean {
|
||||
if (typeEquals(from, to) || !standsFor(lowerer, from, to)) return false;
|
||||
const plan = lowerer.widthLiftPlan(from, to);
|
||||
return plan !== null && (plan.how === "arr" || plan.how === "width");
|
||||
}
|
||||
|
||||
/** The structural-instance info of a typed function declaration, created on
|
||||
* first use and kept on the Lowerer. */
|
||||
function structuralInfoOf(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.FunctionDeclaration,
|
||||
symbol: ts.Symbol,
|
||||
): GenericFnInfo {
|
||||
const existing = lowerer.structuralFnInfos.get(symbol);
|
||||
if (existing) return existing;
|
||||
const nameText = decl.name?.text ?? "%default";
|
||||
const body = decl.body!;
|
||||
const info: GenericFnInfo = {
|
||||
decl,
|
||||
baseName: nameText,
|
||||
qualifiedName: lowerer.qualify(
|
||||
ts.getSourceFileOfNode(decl),
|
||||
nsPathPrefix(lowerer, decl) + nameText + "%layout",
|
||||
),
|
||||
typeParams: [],
|
||||
instances: new Map(),
|
||||
implicitParams: decl.parameters.nodes.map((param) => {
|
||||
if (!ts.isIdentifier(param.name) || param.dotDotDotToken || param.initializer) return null;
|
||||
if (param.questionToken) return null;
|
||||
const sym = lowerer.checker.getSymbolAtLocation(param.name);
|
||||
if (!sym || paramWrittenInBody(lowerer, body, sym, param.name.text)) return null;
|
||||
return sym;
|
||||
}),
|
||||
};
|
||||
lowerer.structuralFnInfos.set(symbol, info);
|
||||
return info;
|
||||
}
|
||||
|
||||
/** The instance a direct call of the declared function `sig` targets when
|
||||
* one of its arguments names a record or array whose layout the parameter
|
||||
* would copy; null when every argument converts as declared. */
|
||||
export function structuralCallInstance(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
sig: FnSig,
|
||||
): GenericInstance | null {
|
||||
const symbol = lowerer.resolveValueSymbol(expr.expression as ts.Identifier);
|
||||
const decl = symbol?.valueDeclaration;
|
||||
if (!symbol || !decl || !ts.isFunctionDeclaration(decl) || !decl.body) return null;
|
||||
if (
|
||||
decl.asteriskToken ||
|
||||
decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)
|
||||
)
|
||||
return null;
|
||||
if (decl.parameters.nodes.some(isThisParameter)) return null;
|
||||
if (symbol.declarations.filter((d) => ts.isFunctionDeclaration(d)).length !== 1) return null;
|
||||
const args = expr.arguments.nodes;
|
||||
if (args.some((a) => ts.isSpreadElement(a))) return null;
|
||||
const params = decl.parameters.nodes;
|
||||
if (sig.params.length !== params.length) return null;
|
||||
const bound: { index: number; type: IrType; checker: ts.Type }[] = [];
|
||||
params.forEach((_, index) => {
|
||||
const arg = args[index];
|
||||
const declared = sig.params[index]!;
|
||||
if (!arg || declared.mode !== "required" || !namesExistingValue(arg)) return;
|
||||
// A signature that widens the declared type's layout (record parameters
|
||||
// whose fields also take undefined) keeps its own boundary conversion.
|
||||
const own = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(params[index]!.name));
|
||||
if (!own || !typeEquals(own, declared.type)) return;
|
||||
const checker = preservedLayoutOf(lowerer, arg) ?? lowerer.typeOf(arg);
|
||||
const type = lowerer.mapTypeOf(checker);
|
||||
if (type && copies(lowerer, type, declared.type)) bound.push({ index, type, checker });
|
||||
});
|
||||
if (bound.length === 0) return null;
|
||||
const info = structuralInfoOf(lowerer, decl, symbol);
|
||||
const shapes: ParamShape[] = [...sig.params];
|
||||
const argTypes = new Map<ts.Symbol, ts.Type>();
|
||||
for (const { index, type, checker } of bound) {
|
||||
const sym = info.implicitParams![index];
|
||||
if (!sym) continue;
|
||||
shapes[index] = { type, mode: "required" };
|
||||
argTypes.set(sym, checker);
|
||||
}
|
||||
if (argTypes.size === 0) return null;
|
||||
// A body that returns a bound parameter returns the argument's layout.
|
||||
const previous = lowerer.implicitParamTypes;
|
||||
lowerer.implicitParamTypes = argTypes;
|
||||
let returnType: IrType;
|
||||
try {
|
||||
returnType = layoutReturnType(lowerer, decl, sig.returnType);
|
||||
} finally {
|
||||
lowerer.implicitParamTypes = previous;
|
||||
}
|
||||
const instance = internImplicitInstanceOr(
|
||||
lowerer,
|
||||
expr,
|
||||
info,
|
||||
shapes,
|
||||
argTypes,
|
||||
"",
|
||||
returnType,
|
||||
true,
|
||||
);
|
||||
if (instance) for (const { index } of bound) lowerer.layoutKeptNodes.add(args[index]!);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/** The layout of the value an existing-value expression names: a binding's
|
||||
* own type, or the element or field of such a value read through `[i]` or
|
||||
* `.name`. Reads of a parameter bound to an argument's layout follow that
|
||||
* layout rather than the parameter's declared type. */
|
||||
function layoutOf(lowerer: Lowerer, expression: ts.Expression): IrType | null {
|
||||
let e = expression;
|
||||
while (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e)) e = e.expression;
|
||||
if (ts.isElementAccessExpression(e)) {
|
||||
const base = layoutOf(lowerer, e.expression);
|
||||
if (
|
||||
base?.kind === "array" &&
|
||||
lowerer.mapTypeOf(lowerer.typeOf(e.argumentExpression))?.kind === "f64"
|
||||
)
|
||||
return base.elem;
|
||||
} else if (ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.name)) {
|
||||
const base = layoutOf(lowerer, e.expression);
|
||||
if (base?.kind === "record") {
|
||||
const field = lowerer.shapes.get(base.shapeId)?.fields.find((f) => f.name === e.name.text);
|
||||
if (field) return field.type;
|
||||
}
|
||||
}
|
||||
return lowerer.mapTypeOf(lowerer.typeOf(e));
|
||||
}
|
||||
|
||||
/** The return type of a declared function whose every `return` names an
|
||||
* existing value of one record or array layout that the declared return type
|
||||
* would copy (`function items(values: Refined[]): Value[] { return values; }`):
|
||||
* that layout, so the caller receives the value itself. `declared` otherwise. */
|
||||
export function layoutReturnType(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.FunctionDeclaration,
|
||||
declared: IrType,
|
||||
): IrType {
|
||||
if (!decl.body || (declared.kind !== "array" && declared.kind !== "record")) return declared;
|
||||
// An exported function keeps its declared result: a library build calls it
|
||||
// from outside the program, through the declared ABI.
|
||||
if (decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) return declared;
|
||||
const returned: ts.Expression[] = [];
|
||||
let open = false;
|
||||
ts.walkPreorder(decl.body, (node) => {
|
||||
if (node !== decl.body && ts.isFunctionLike(node)) return "skip";
|
||||
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) return "skip";
|
||||
if (!ts.isReturnStatement(node)) return undefined;
|
||||
if (!node.expression || !namesExistingValue(node.expression)) open = true;
|
||||
else returned.push(node.expression);
|
||||
return undefined;
|
||||
});
|
||||
if (open || returned.length === 0) return declared;
|
||||
let layout: IrType | null = null;
|
||||
for (const expression of returned) {
|
||||
const type = layoutOf(lowerer, expression);
|
||||
if (!type || !copies(lowerer, type, declared)) return declared;
|
||||
if (layout && !typeEquals(layout, type)) return declared;
|
||||
layout = type;
|
||||
}
|
||||
return layout ?? declared;
|
||||
}
|
||||
|
||||
/** The initializer type of a const the argument names when the const kept
|
||||
* that initializer's layout (keepsInitializerLayout): the checker types the
|
||||
* binding by its annotation, but its value has the initializer's layout. A
|
||||
* declaration lowers once per function instance, and only some instances
|
||||
* keep a layout, so the recorded type applies only while it maps to the
|
||||
* layout of the binding the current lowering resolves. */
|
||||
function preservedLayoutOf(lowerer: Lowerer, arg: ts.Expression): ts.Type | undefined {
|
||||
let e = arg;
|
||||
while (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e)) e = e.expression;
|
||||
if (!ts.isIdentifier(e)) return undefined;
|
||||
const symbol = lowerer.resolveValueSymbol(e);
|
||||
const recorded = symbol ? lowerer.preservedLayoutTypes.get(symbol) : undefined;
|
||||
if (recorded === undefined) return undefined;
|
||||
const binding = lowerer.peekLocal(e)?.type ?? lowerer.globalOf(e)?.type;
|
||||
const layout = lowerer.mapTypeOf(recorded);
|
||||
return binding && layout && typeEquals(binding, layout) ? recorded : undefined;
|
||||
}
|
||||
|
||||
/** Whether a `const` binding typed `declared` keeps the record or array
|
||||
* layout `actual` of an initializer that names an existing value or returns
|
||||
* one, instead of converting it by a copy. Fresh literals convert as
|
||||
* declared: their copy is not observable. */
|
||||
export function keepsInitializerLayout(
|
||||
lowerer: Lowerer,
|
||||
initializer: ts.Expression,
|
||||
actual: IrType,
|
||||
declared: IrType,
|
||||
): boolean {
|
||||
let e = initializer;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!namesExistingValue(e) && !ts.isCallExpression(e)) return false;
|
||||
if (lowerer.isRuntimeOptionalRecordVariant(actual, declared)) return false;
|
||||
return copies(lowerer, actual, declared);
|
||||
}
|
||||
|
||||
/** Records that the const `decl` keeps its initializer's layout, so calls
|
||||
* that pass it bind the initializer's type (preservedLayoutOf). */
|
||||
export function notePreservedLayout(lowerer: Lowerer, decl: ts.VariableDeclaration): void {
|
||||
if (!ts.isIdentifier(decl.name) || !decl.initializer) return;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
|
||||
if (!symbol) return;
|
||||
const initializer =
|
||||
preservedLayoutOf(lowerer, decl.initializer) ?? lowerer.typeOf(decl.initializer);
|
||||
lowerer.preservedLayoutTypes.set(symbol, initializer);
|
||||
lowerer.layoutKeptNodes.add(decl.initializer);
|
||||
}
|
||||
@@ -138,7 +138,11 @@ export function typeFailureSlot(node: ts.Node): { slot: TypeFailureSlot; name: s
|
||||
if (!declared && !ts.isTypeNode(current) && !ts.isTypeNode(parent)) break;
|
||||
current = parent;
|
||||
}
|
||||
const text = ts.getTextOfNode(node);
|
||||
// A type node names its expression (`raw as Plugin`, not `Plugin`).
|
||||
let named: ts.Node = node;
|
||||
while (ts.isTypeNode(named) && named.parent !== undefined && !ts.isSourceFile(named.parent))
|
||||
named = named.parent;
|
||||
const text = ts.getTextOfNode(named).replace(/\s+/g, " ");
|
||||
return { slot: "expression", name: text.length > 60 ? `${text.slice(0, 57)}...` : text };
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,10 @@ export class Ts7Api {
|
||||
|
||||
constructor(rpc: Ts7RpcClient, fs: Ts7FileSystem, collectTiming = false) {
|
||||
try {
|
||||
registerTs7FileSystem(rpc, fs);
|
||||
this.session = new Ts7Session(rpc, collectTiming);
|
||||
const session = new Ts7Session(rpc, collectTiming);
|
||||
// Callbacks run inside requests, after initialize chose the protocol.
|
||||
registerTs7FileSystem(rpc, fs, () => session.speaksTs71());
|
||||
this.session = session;
|
||||
} catch (error) {
|
||||
rpc.close();
|
||||
throw error;
|
||||
|
||||
@@ -135,6 +135,11 @@ const HEADER_SIZE_7_1 = 64;
|
||||
/** SourceKeyword in TypeScript 7.1's kinds. */
|
||||
const SOURCE_KEYWORD_7_1 = 167;
|
||||
|
||||
/** A TypeScript 7.0 SyntaxKind as TypeScript 7.1 numbers it. */
|
||||
export function syntaxKind71(kind: number): number {
|
||||
return kind >= SOURCE_KEYWORD_7_1 ? kind + 1 : kind;
|
||||
}
|
||||
|
||||
/** 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
|
||||
@@ -248,7 +253,7 @@ 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;
|
||||
return this.protocol === PROTOCOL_VERSION_7_1 ? syntaxKind71(kind) : kind;
|
||||
}
|
||||
|
||||
/** A kind as this response's protocol numbers it (a node handle's) in
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Ts7RpcClient } from "./rpc-client.js";
|
||||
import { registerTs7FileSystem, TS7_FILE_SYSTEM_CALLBACKS } from "./rpc-filesystem.js";
|
||||
import { spawnTs7Wire } from "./rpc-process.js";
|
||||
import { ts7Executable } from "./rpc-api.js";
|
||||
import { ts7TestServers, type Ts7TestServer } from "./test-servers.js";
|
||||
import { Ts7Session } from "./session.js";
|
||||
import { SemanticChecker } from "./semantic-checker.js";
|
||||
import { SemanticSnapshot } from "./semantic-model.js";
|
||||
@@ -18,26 +19,36 @@ import type { Identifier, SourceFile } from "./ast-types.js";
|
||||
const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp";
|
||||
const protocolPath = (path: string) => path.replaceAll("\\", "/");
|
||||
|
||||
function connect(files: Record<string, string>) {
|
||||
/** The default checker (tsc-ts) and tsc 7.1. */
|
||||
const servers: Ts7TestServer[] = [
|
||||
{ name: "tsc 7.0", executable: ts7Executable(), ts71: false },
|
||||
...ts7TestServers().filter((server) => server.ts71),
|
||||
];
|
||||
|
||||
function connect(server: Ts7TestServer, files: Record<string, string>) {
|
||||
const dir = mkdtempSync(join(tempRoot, "scriptc-checker-lifetime-"));
|
||||
const paths = new Map(
|
||||
Object.entries(files).map(([name, text]) => [protocolPath(join(dir, name)), text]),
|
||||
);
|
||||
const rpc = new Ts7RpcClient(
|
||||
spawnTs7Wire(ts7Executable(), [
|
||||
spawnTs7Wire(server.executable, [
|
||||
"--api",
|
||||
"--cwd",
|
||||
dir,
|
||||
`--callbacks=${TS7_FILE_SYSTEM_CALLBACKS}`,
|
||||
]),
|
||||
);
|
||||
registerTs7FileSystem(rpc, {
|
||||
readFile: (path) => paths.get(path),
|
||||
fileExists: (path) => (paths.has(path) ? true : undefined),
|
||||
directoryExists: () => undefined,
|
||||
realpath: () => undefined,
|
||||
getAccessibleEntries: () => undefined,
|
||||
});
|
||||
registerTs7FileSystem(
|
||||
rpc,
|
||||
{
|
||||
readFile: (path) => paths.get(path),
|
||||
fileExists: (path) => (paths.has(path) ? true : undefined),
|
||||
directoryExists: () => undefined,
|
||||
realpath: () => undefined,
|
||||
getAccessibleEntries: () => undefined,
|
||||
},
|
||||
() => server.ts71,
|
||||
);
|
||||
const session = new Ts7Session(rpc);
|
||||
return {
|
||||
session,
|
||||
@@ -65,184 +76,188 @@ const config = JSON.stringify({
|
||||
files: ["main.ts"],
|
||||
});
|
||||
|
||||
test("unchanged AST identities receive independent semantic answers after a dependency update", () => {
|
||||
const h = connect({
|
||||
"tsconfig.json": config,
|
||||
"main.ts": 'import { value } from "./dependency.js"; export const result = value;',
|
||||
"dependency.ts": "export const value: number = 1;",
|
||||
});
|
||||
try {
|
||||
const first = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] });
|
||||
const oldProject = first.getProjects()[0]!;
|
||||
const root = oldProject.program.getSourceFile(h.path("main.ts"))!;
|
||||
const name = declaration(root, "result").name!;
|
||||
const old = new CheckerFacade(oldProject.checker, { project: oldProject.checker.project });
|
||||
const oldType = old.getTypeAtLocation(name);
|
||||
const oldSymbol = old.getSymbolAtLocation(name)!;
|
||||
expect(old.typeToString(oldType)).toBe("number");
|
||||
expect(oldSymbol.valueDeclaration).toBe(declaration(root, "result"));
|
||||
|
||||
h.paths.set(h.path("dependency.ts"), 'export const value: string = "changed";');
|
||||
const second = h.session.updateSnapshot({
|
||||
fileChanges: { changed: [h.path("dependency.ts")] },
|
||||
for (const server of servers)
|
||||
test(`unchanged AST identities receive independent semantic answers after a dependency update (${server.name})`, () => {
|
||||
const h = connect(server, {
|
||||
"tsconfig.json": config,
|
||||
"main.ts": 'import { value } from "./dependency.js"; export const result = value;',
|
||||
"dependency.ts": "export const value: number = 1;",
|
||||
});
|
||||
const newProject = second.getProjects()[0]!;
|
||||
const nextRoot = newProject.program.getSourceFile(h.path("main.ts"))!;
|
||||
expect(nextRoot).toBe(root);
|
||||
expect(declaration(nextRoot, "result").name).toBe(name);
|
||||
const next = new CheckerFacade(newProject.checker, { project: newProject.checker.project });
|
||||
const newType = next.getTypeAtLocation(name);
|
||||
expect(next.typeToString(newType)).toBe("string");
|
||||
expect(newType).not.toBe(oldType);
|
||||
expect(next.getSymbolAtLocation(name)).not.toBe(oldSymbol);
|
||||
expect(old.typeToString(old.getTypeAtLocation(name))).toBe("number");
|
||||
first.dispose();
|
||||
expect(() => old.getTypeAtLocation(name)).toThrow("disposed");
|
||||
expect(next.getTypeAtLocation(name)).toBe(newType);
|
||||
expect(next.typeToString(newType)).toBe("string");
|
||||
second.dispose();
|
||||
expect(() => next.getTypeAtLocation(name)).toThrow("disposed");
|
||||
} finally {
|
||||
h.close();
|
||||
}
|
||||
});
|
||||
try {
|
||||
const first = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] });
|
||||
const oldProject = first.getProjects()[0]!;
|
||||
const root = oldProject.program.getSourceFile(h.path("main.ts"))!;
|
||||
const name = declaration(root, "result").name!;
|
||||
const old = new CheckerFacade(oldProject.checker, { project: oldProject.checker.project });
|
||||
const oldType = old.getTypeAtLocation(name);
|
||||
const oldSymbol = old.getSymbolAtLocation(name)!;
|
||||
expect(old.typeToString(oldType)).toBe("number");
|
||||
expect(oldSymbol.valueDeclaration).toBe(declaration(root, "result"));
|
||||
|
||||
test("project-local type handles never alias across simultaneous checker caches", () => {
|
||||
const h = connect({
|
||||
"first.json": config,
|
||||
"second.json": config,
|
||||
"main.ts": "export type Choice = string | number; export const value = 42;",
|
||||
h.paths.set(h.path("dependency.ts"), 'export const value: string = "changed";');
|
||||
const second = h.session.updateSnapshot({
|
||||
fileChanges: { changed: [h.path("dependency.ts")] },
|
||||
});
|
||||
const newProject = second.getProjects()[0]!;
|
||||
const nextRoot = newProject.program.getSourceFile(h.path("main.ts"))!;
|
||||
expect(nextRoot).toBe(root);
|
||||
expect(declaration(nextRoot, "result").name).toBe(name);
|
||||
const next = new CheckerFacade(newProject.checker, { project: newProject.checker.project });
|
||||
const newType = next.getTypeAtLocation(name);
|
||||
expect(next.typeToString(newType)).toBe("string");
|
||||
expect(newType).not.toBe(oldType);
|
||||
expect(next.getSymbolAtLocation(name)).not.toBe(oldSymbol);
|
||||
expect(old.typeToString(old.getTypeAtLocation(name))).toBe("number");
|
||||
first.dispose();
|
||||
expect(() => old.getTypeAtLocation(name)).toThrow("disposed");
|
||||
expect(next.getTypeAtLocation(name)).toBe(newType);
|
||||
expect(next.typeToString(newType)).toBe("string");
|
||||
second.dispose();
|
||||
expect(() => next.getTypeAtLocation(name)).toThrow("disposed");
|
||||
} finally {
|
||||
h.close();
|
||||
}
|
||||
});
|
||||
try {
|
||||
const snapshot = h.session.updateSnapshot({
|
||||
openProjects: [h.path("first.json"), h.path("second.json")],
|
||||
|
||||
for (const server of servers)
|
||||
test(`project-local type handles never alias across simultaneous checker caches (${server.name})`, () => {
|
||||
const h = connect(server, {
|
||||
"first.json": config,
|
||||
"second.json": config,
|
||||
"main.ts": "export type Choice = string | number; export const value = 42;",
|
||||
});
|
||||
const first = snapshot.getProject(h.path("first.json"))!;
|
||||
const second = snapshot.getProject(h.path("second.json"))!;
|
||||
const root = first.program.getSourceFile(h.path("main.ts"))!;
|
||||
expect(second.program.getSourceFile(h.path("main.ts"))).toBe(root);
|
||||
const a = new CheckerFacade(first.checker, { project: first.checker.project });
|
||||
const b = new CheckerFacade(second.checker, { project: second.checker.project });
|
||||
const name = declaration(root, "value").name!;
|
||||
const firstType = a.getTypeAtLocation(name);
|
||||
const secondType = b.getTypeAtLocation(name);
|
||||
expect(firstType.project).not.toBe(secondType.project);
|
||||
expect(firstType).not.toBe(secondType);
|
||||
expect(a.typeToString(firstType)).toBe(b.typeToString(secondType));
|
||||
const alias = declaration(root, "Choice").type!;
|
||||
const firstArms = constituentTypes(a.getTypeFromTypeNode(alias));
|
||||
const secondArms = constituentTypes(b.getTypeFromTypeNode(alias));
|
||||
expect(firstArms).not.toBe(secondArms);
|
||||
expect(firstArms.map((arm) => a.typeToString(arm))).toEqual(
|
||||
secondArms.map((arm) => b.typeToString(arm)),
|
||||
);
|
||||
first.checker.project.dispose();
|
||||
expect(() => a.getTypeAtLocation(name)).toThrow("disposed");
|
||||
expect(b.getTypeAtLocation(name)).toBe(secondType);
|
||||
expect(b.getTypeFromTypeNode(alias).getTypes()).toBe(secondArms);
|
||||
} finally {
|
||||
h.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("all public query and prefetch entry points reject a disposed facade before returning cached or local answers", () => {
|
||||
const h = connect({
|
||||
"tsconfig.json": config,
|
||||
"main.ts": "export const value = 42; export function fn(v: number): number { return v; }",
|
||||
try {
|
||||
const snapshot = h.session.updateSnapshot({
|
||||
openProjects: [h.path("first.json"), h.path("second.json")],
|
||||
});
|
||||
const first = snapshot.getProject(h.path("first.json"))!;
|
||||
const second = snapshot.getProject(h.path("second.json"))!;
|
||||
const root = first.program.getSourceFile(h.path("main.ts"))!;
|
||||
expect(second.program.getSourceFile(h.path("main.ts"))).toBe(root);
|
||||
const a = new CheckerFacade(first.checker, { project: first.checker.project });
|
||||
const b = new CheckerFacade(second.checker, { project: second.checker.project });
|
||||
const name = declaration(root, "value").name!;
|
||||
const firstType = a.getTypeAtLocation(name);
|
||||
const secondType = b.getTypeAtLocation(name);
|
||||
expect(firstType.project).not.toBe(secondType.project);
|
||||
expect(firstType).not.toBe(secondType);
|
||||
expect(a.typeToString(firstType)).toBe(b.typeToString(secondType));
|
||||
const alias = declaration(root, "Choice").type!;
|
||||
const firstArms = constituentTypes(a.getTypeFromTypeNode(alias));
|
||||
const secondArms = constituentTypes(b.getTypeFromTypeNode(alias));
|
||||
expect(firstArms).not.toBe(secondArms);
|
||||
expect(firstArms.map((arm) => a.typeToString(arm))).toEqual(
|
||||
secondArms.map((arm) => b.typeToString(arm)),
|
||||
);
|
||||
first.checker.project.dispose();
|
||||
expect(() => a.getTypeAtLocation(name)).toThrow("disposed");
|
||||
expect(b.getTypeAtLocation(name)).toBe(secondType);
|
||||
expect(b.getTypeFromTypeNode(alias).getTypes()).toBe(secondArms);
|
||||
} finally {
|
||||
h.close();
|
||||
}
|
||||
});
|
||||
try {
|
||||
const snapshot = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] });
|
||||
const project = snapshot.getProjects()[0]!;
|
||||
const root = project.program.getSourceFile(h.path("main.ts"))!;
|
||||
const facade = new CheckerFacade(project.checker, { project: project.checker.project });
|
||||
const name = declaration(root, "value").name!;
|
||||
const fn = declaration(root, "fn");
|
||||
const type = facade.getTypeAtLocation(name);
|
||||
const symbol = facade.getSymbolAtLocation(name)!;
|
||||
const signature = facade.getSignatureFromDeclaration(fn)!;
|
||||
const never = project.checker.getNeverType();
|
||||
facade.getUnknownType();
|
||||
facade.typeToString(type);
|
||||
facade.prefetchSourceFile(root);
|
||||
const calls: Record<string, () => unknown> = {
|
||||
getCallSignatures: () => facade.getCallSignatures(type),
|
||||
getConstructSignatures: () => facade.getConstructSignatures(type),
|
||||
getSignaturesOfType: () => facade.getSignaturesOfType(type, SignatureKind.Call),
|
||||
resolveName: () => facade.resolveName("value", SymbolFlags.Value, name),
|
||||
getResolvedSymbol: () => facade.getResolvedSymbol(name as Identifier),
|
||||
getAnyType: () => facade.getAnyType(),
|
||||
getNeverType: () => facade.getNeverType(),
|
||||
getBigIntType: () => facade.getBigIntType(),
|
||||
getPropertyOfType: () => facade.getPropertyOfType(type, "missing"),
|
||||
prefetchSourceFile: () => facade.prefetchSourceFile(root),
|
||||
prefetchSourceFileStructures: () => facade.prefetchSourceFileStructures([]),
|
||||
prefetchRoots: () => facade.prefetchRoots([]),
|
||||
prefetchSymbolRoots: () => facade.prefetchSymbolRoots([]),
|
||||
prefetchSymbolNodesExact: () => facade.prefetchSymbolNodesExact([]),
|
||||
prefetchCollectionTypes: () => facade.prefetchCollectionTypes([]),
|
||||
prefetchClassCollection: () => facade.prefetchClassCollection([], []),
|
||||
getTypeAtLocation: () => facade.getTypeAtLocation(name),
|
||||
getSymbolAtLocation: () => facade.getSymbolAtLocation(name),
|
||||
getTypeOfSymbol: () => facade.getTypeOfSymbol(symbol),
|
||||
prefetchMemberTypes: () => facade.prefetchMemberTypes([symbol], 0),
|
||||
getAliasedSymbol: () => facade.getAliasedSymbol(symbol),
|
||||
getDeclaredTypeOfSymbol: () => facade.getDeclaredTypeOfSymbol(symbol),
|
||||
getContextualType: () => facade.getContextualType(name),
|
||||
getTypeFromTypeNode: () => facade.getTypeFromTypeNode(fn.type!),
|
||||
getShorthandAssignmentValueSymbol: () => facade.getShorthandAssignmentValueSymbol(name),
|
||||
getResolvedSignature: () => facade.getResolvedSignature(name),
|
||||
getSignatureFromDeclaration: () => facade.getSignatureFromDeclaration(fn),
|
||||
getReturnTypeOfSignature: () => facade.getReturnTypeOfSignature(signature),
|
||||
getTypePredicateOfSignature: () => facade.getTypePredicateOfSignature(signature),
|
||||
getBaseTypeOfLiteralType: () => facade.getBaseTypeOfLiteralType(type),
|
||||
getConstantValue: () => facade.getConstantValue(name),
|
||||
getNonNullableType: () => facade.getNonNullableType(type),
|
||||
getPropertiesOfType: () => facade.getPropertiesOfType(type),
|
||||
getBaseTypes: () => facade.getBaseTypes(type),
|
||||
isNeverType: () => facade.isNeverType(never),
|
||||
isTypeAssignableTo: () => facade.isTypeAssignableTo(type, type),
|
||||
getIndexInfosOfType: () => facade.getIndexInfosOfType(type),
|
||||
getTypeArguments: () => facade.getTypeArguments(type),
|
||||
isArrayType: () => facade.isArrayType(type),
|
||||
isTupleType: () => facade.isTupleType(type),
|
||||
isArrayLikeType: () => facade.isArrayLikeType(type),
|
||||
typeToString: () => facade.typeToString(type),
|
||||
getTypeOfSymbolAtLocation: () => facade.getTypeOfSymbolAtLocation(symbol, name),
|
||||
getUnknownType: () => facade.getUnknownType(),
|
||||
getStringType: () => facade.getStringType(),
|
||||
getNumberType: () => facade.getNumberType(),
|
||||
getBooleanType: () => facade.getBooleanType(),
|
||||
getAwaitedType: () => facade.getAwaitedType(type),
|
||||
};
|
||||
const before = h.rpc.timing().requests;
|
||||
facade.dispose();
|
||||
facade.dispose();
|
||||
for (const [name, call] of Object.entries(calls))
|
||||
expect(call, name).toThrow("checker facade is disposed");
|
||||
expect(h.rpc.timing().requests).toBe(before);
|
||||
expect(project.checker.typeToString(type)).toBe("42");
|
||||
} finally {
|
||||
h.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("session shutdown invalidates every retained facade without explicit program disposal", () => {
|
||||
const h = connect({ "tsconfig.json": config, "main.ts": "export const value = 1;" });
|
||||
try {
|
||||
const snapshot = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] });
|
||||
const project = snapshot.getProjects()[0]!;
|
||||
const facade = new CheckerFacade(project.checker);
|
||||
const name = declaration(project.program.getSourceFile(h.path("main.ts"))!, "value").name!;
|
||||
facade.getTypeAtLocation(name);
|
||||
facade.getNumberType();
|
||||
h.session.close();
|
||||
expect(() => facade.getTypeAtLocation(name)).toThrow("disposed");
|
||||
expect(() => facade.getNumberType()).toThrow("disposed");
|
||||
expect(() => new CheckerFacade(project.checker)).toThrow("disposed");
|
||||
} finally {
|
||||
h.close();
|
||||
}
|
||||
});
|
||||
for (const server of servers)
|
||||
test(`all public query and prefetch entry points reject a disposed facade before returning cached or local answers (${server.name})`, () => {
|
||||
const h = connect(server, {
|
||||
"tsconfig.json": config,
|
||||
"main.ts": "export const value = 42; export function fn(v: number): number { return v; }",
|
||||
});
|
||||
try {
|
||||
const snapshot = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] });
|
||||
const project = snapshot.getProjects()[0]!;
|
||||
const root = project.program.getSourceFile(h.path("main.ts"))!;
|
||||
const facade = new CheckerFacade(project.checker, { project: project.checker.project });
|
||||
const name = declaration(root, "value").name!;
|
||||
const fn = declaration(root, "fn");
|
||||
const type = facade.getTypeAtLocation(name);
|
||||
const symbol = facade.getSymbolAtLocation(name)!;
|
||||
const signature = facade.getSignatureFromDeclaration(fn)!;
|
||||
const never = project.checker.getNeverType();
|
||||
facade.getUnknownType();
|
||||
facade.typeToString(type);
|
||||
facade.prefetchSourceFile(root);
|
||||
const calls: Record<string, () => unknown> = {
|
||||
getCallSignatures: () => facade.getCallSignatures(type),
|
||||
getConstructSignatures: () => facade.getConstructSignatures(type),
|
||||
getSignaturesOfType: () => facade.getSignaturesOfType(type, SignatureKind.Call),
|
||||
resolveName: () => facade.resolveName("value", SymbolFlags.Value, name),
|
||||
getResolvedSymbol: () => facade.getResolvedSymbol(name as Identifier),
|
||||
getAnyType: () => facade.getAnyType(),
|
||||
getNeverType: () => facade.getNeverType(),
|
||||
getBigIntType: () => facade.getBigIntType(),
|
||||
getPropertyOfType: () => facade.getPropertyOfType(type, "missing"),
|
||||
prefetchSourceFile: () => facade.prefetchSourceFile(root),
|
||||
prefetchSourceFileStructures: () => facade.prefetchSourceFileStructures([]),
|
||||
prefetchRoots: () => facade.prefetchRoots([]),
|
||||
prefetchSymbolRoots: () => facade.prefetchSymbolRoots([]),
|
||||
prefetchSymbolNodesExact: () => facade.prefetchSymbolNodesExact([]),
|
||||
prefetchCollectionTypes: () => facade.prefetchCollectionTypes([]),
|
||||
prefetchClassCollection: () => facade.prefetchClassCollection([], []),
|
||||
getTypeAtLocation: () => facade.getTypeAtLocation(name),
|
||||
getSymbolAtLocation: () => facade.getSymbolAtLocation(name),
|
||||
getTypeOfSymbol: () => facade.getTypeOfSymbol(symbol),
|
||||
prefetchMemberTypes: () => facade.prefetchMemberTypes([symbol], 0),
|
||||
getAliasedSymbol: () => facade.getAliasedSymbol(symbol),
|
||||
getDeclaredTypeOfSymbol: () => facade.getDeclaredTypeOfSymbol(symbol),
|
||||
getContextualType: () => facade.getContextualType(name),
|
||||
getTypeFromTypeNode: () => facade.getTypeFromTypeNode(fn.type!),
|
||||
getShorthandAssignmentValueSymbol: () => facade.getShorthandAssignmentValueSymbol(name),
|
||||
getResolvedSignature: () => facade.getResolvedSignature(name),
|
||||
getSignatureFromDeclaration: () => facade.getSignatureFromDeclaration(fn),
|
||||
getReturnTypeOfSignature: () => facade.getReturnTypeOfSignature(signature),
|
||||
getTypePredicateOfSignature: () => facade.getTypePredicateOfSignature(signature),
|
||||
getBaseTypeOfLiteralType: () => facade.getBaseTypeOfLiteralType(type),
|
||||
getConstantValue: () => facade.getConstantValue(name),
|
||||
getNonNullableType: () => facade.getNonNullableType(type),
|
||||
getPropertiesOfType: () => facade.getPropertiesOfType(type),
|
||||
getBaseTypes: () => facade.getBaseTypes(type),
|
||||
isNeverType: () => facade.isNeverType(never),
|
||||
isTypeAssignableTo: () => facade.isTypeAssignableTo(type, type),
|
||||
getIndexInfosOfType: () => facade.getIndexInfosOfType(type),
|
||||
getTypeArguments: () => facade.getTypeArguments(type),
|
||||
isArrayType: () => facade.isArrayType(type),
|
||||
isTupleType: () => facade.isTupleType(type),
|
||||
isArrayLikeType: () => facade.isArrayLikeType(type),
|
||||
typeToString: () => facade.typeToString(type),
|
||||
getTypeOfSymbolAtLocation: () => facade.getTypeOfSymbolAtLocation(symbol, name),
|
||||
getUnknownType: () => facade.getUnknownType(),
|
||||
getStringType: () => facade.getStringType(),
|
||||
getNumberType: () => facade.getNumberType(),
|
||||
getBooleanType: () => facade.getBooleanType(),
|
||||
getAwaitedType: () => facade.getAwaitedType(type),
|
||||
};
|
||||
const before = h.rpc.timing().requests;
|
||||
facade.dispose();
|
||||
facade.dispose();
|
||||
for (const [name, call] of Object.entries(calls))
|
||||
expect(call, name).toThrow("checker facade is disposed");
|
||||
expect(h.rpc.timing().requests).toBe(before);
|
||||
expect(project.checker.typeToString(type)).toBe("42");
|
||||
} finally {
|
||||
h.close();
|
||||
}
|
||||
});
|
||||
|
||||
for (const server of servers)
|
||||
test(`session shutdown invalidates every retained facade without explicit program disposal (${server.name})`, () => {
|
||||
const h = connect(server, { "tsconfig.json": config, "main.ts": "export const value = 1;" });
|
||||
try {
|
||||
const snapshot = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] });
|
||||
const project = snapshot.getProjects()[0]!;
|
||||
const facade = new CheckerFacade(project.checker);
|
||||
const name = declaration(project.program.getSourceFile(h.path("main.ts"))!, "value").name!;
|
||||
facade.getTypeAtLocation(name);
|
||||
facade.getNumberType();
|
||||
h.session.close();
|
||||
expect(() => facade.getTypeAtLocation(name)).toThrow("disposed");
|
||||
expect(() => facade.getNumberType()).toThrow("disposed");
|
||||
expect(() => new CheckerFacade(project.checker)).toThrow("disposed");
|
||||
} finally {
|
||||
h.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("program disposal and host close seal cached source and checker access", () => {
|
||||
const dir = mkdtempSync(join(tempRoot, "scriptc-program-lifetime-"));
|
||||
@@ -356,37 +371,38 @@ test("property lookup caches hits and misses by type and name within each projec
|
||||
snapshot.dispose();
|
||||
});
|
||||
|
||||
test("dependency edits invalidate positive and negative property answers", () => {
|
||||
const h = connect({
|
||||
"tsconfig.json": config,
|
||||
"main.ts": 'import { value } from "./dependency.js"; export const result = value;',
|
||||
"dependency.ts": "export const value = { before: 1 };",
|
||||
});
|
||||
try {
|
||||
const first = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] });
|
||||
const project = first.getProjects()[0]!;
|
||||
const root = project.program.getSourceFile(h.path("main.ts"))!;
|
||||
const old = new CheckerFacade(project.checker);
|
||||
const type = old.getTypeAtLocation(declaration(root, "result").name!);
|
||||
expect(old.getPropertyOfType(type, "before")).toBeDefined();
|
||||
expect(old.getPropertyOfType(type, "after")).toBeUndefined();
|
||||
h.paths.set(h.path("dependency.ts"), "export const value = { after: 2 };");
|
||||
const second = h.session.updateSnapshot({
|
||||
fileChanges: { changed: [h.path("dependency.ts")] },
|
||||
for (const server of servers)
|
||||
test(`dependency edits invalidate positive and negative property answers (${server.name})`, () => {
|
||||
const h = connect(server, {
|
||||
"tsconfig.json": config,
|
||||
"main.ts": 'import { value } from "./dependency.js"; export const result = value;',
|
||||
"dependency.ts": "export const value = { before: 1 };",
|
||||
});
|
||||
const nextProject = second.getProjects()[0]!;
|
||||
const nextRoot = nextProject.program.getSourceFile(h.path("main.ts"))!;
|
||||
expect(nextRoot).toBe(root);
|
||||
const next = new CheckerFacade(nextProject.checker);
|
||||
const nextType = next.getTypeAtLocation(declaration(nextRoot, "result").name!);
|
||||
expect(next.getPropertyOfType(nextType, "before")).toBeUndefined();
|
||||
expect(next.getPropertyOfType(nextType, "after")).toBeDefined();
|
||||
expect(old.getPropertyOfType(type, "before")).toBeDefined();
|
||||
expect(old.getPropertyOfType(type, "after")).toBeUndefined();
|
||||
} finally {
|
||||
h.close();
|
||||
}
|
||||
});
|
||||
try {
|
||||
const first = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] });
|
||||
const project = first.getProjects()[0]!;
|
||||
const root = project.program.getSourceFile(h.path("main.ts"))!;
|
||||
const old = new CheckerFacade(project.checker);
|
||||
const type = old.getTypeAtLocation(declaration(root, "result").name!);
|
||||
expect(old.getPropertyOfType(type, "before")).toBeDefined();
|
||||
expect(old.getPropertyOfType(type, "after")).toBeUndefined();
|
||||
h.paths.set(h.path("dependency.ts"), "export const value = { after: 2 };");
|
||||
const second = h.session.updateSnapshot({
|
||||
fileChanges: { changed: [h.path("dependency.ts")] },
|
||||
});
|
||||
const nextProject = second.getProjects()[0]!;
|
||||
const nextRoot = nextProject.program.getSourceFile(h.path("main.ts"))!;
|
||||
expect(nextRoot).toBe(root);
|
||||
const next = new CheckerFacade(nextProject.checker);
|
||||
const nextType = next.getTypeAtLocation(declaration(nextRoot, "result").name!);
|
||||
expect(next.getPropertyOfType(nextType, "before")).toBeUndefined();
|
||||
expect(next.getPropertyOfType(nextType, "after")).toBeDefined();
|
||||
expect(old.getPropertyOfType(type, "before")).toBeDefined();
|
||||
expect(old.getPropertyOfType(type, "after")).toBeUndefined();
|
||||
} finally {
|
||||
h.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("member type batches are bounded, reuse warm entries, and isolate absent or panicking fields", () => {
|
||||
const calls: number[][] = [];
|
||||
|
||||
@@ -15,7 +15,12 @@ const fallback: Ts7FileSystem = {
|
||||
getAccessibleEntries: () => undefined,
|
||||
};
|
||||
|
||||
function callback(name: string, fs: Ts7FileSystem, payload = JSON.stringify("/file.ts")) {
|
||||
function callback(
|
||||
name: string,
|
||||
fs: Ts7FileSystem,
|
||||
payload = JSON.stringify("/file.ts"),
|
||||
ts71 = false,
|
||||
) {
|
||||
const incoming: number[] = [];
|
||||
const encoded = new Ts7Wire({
|
||||
read: () => 0,
|
||||
@@ -47,7 +52,7 @@ function callback(name: string, fs: Ts7FileSystem, payload = JSON.stringify("/fi
|
||||
},
|
||||
}),
|
||||
);
|
||||
registerTs7FileSystem(client, fs);
|
||||
registerTs7FileSystem(client, fs, () => ts71);
|
||||
let error: unknown;
|
||||
try {
|
||||
client.requestText("query", "null");
|
||||
@@ -88,6 +93,36 @@ test.each(TS7_FILE_SYSTEM_CALLBACKS.split(","))("%s delegates undefined to the s
|
||||
expect(callback(name, fallback)).toEqual({ kind: 2, text: "", error: undefined, closed: false });
|
||||
});
|
||||
|
||||
test("TypeScript 7.1 replies carry the same answers as value, missing and useOS", () => {
|
||||
const reply = (name: string, fs: Ts7FileSystem) =>
|
||||
JSON.parse(callback(name, fs, JSON.stringify("/file.ts"), true).text) as unknown;
|
||||
expect(reply("readFile", { ...fallback, readFile: () => "text" })).toEqual({
|
||||
kind: "value",
|
||||
value: "text",
|
||||
});
|
||||
expect(reply("readFile", { ...fallback, readFile: () => "" })).toEqual({
|
||||
kind: "value",
|
||||
value: "",
|
||||
});
|
||||
expect(reply("readFile", { ...fallback, readFile: () => null })).toEqual({ kind: "missing" });
|
||||
for (const name of TS7_FILE_SYSTEM_CALLBACKS.split(","))
|
||||
expect(reply(name, fallback), name).toEqual({ kind: "useOS" });
|
||||
expect(reply("fileExists", { ...fallback, fileExists: () => false })).toEqual({
|
||||
kind: "value",
|
||||
value: false,
|
||||
});
|
||||
expect(reply("realpath", { ...fallback, realpath: () => "/real.ts" })).toEqual({
|
||||
kind: "value",
|
||||
value: "/real.ts",
|
||||
});
|
||||
expect(
|
||||
reply("getAccessibleEntries", {
|
||||
...fallback,
|
||||
getAccessibleEntries: () => ({ files: ["a.ts"], directories: [] }),
|
||||
}),
|
||||
).toEqual({ kind: "value", value: { files: ["a.ts"], directories: [] } });
|
||||
});
|
||||
|
||||
test("boolean false and empty directory entries do not fall through", () => {
|
||||
const fs = {
|
||||
...fallback,
|
||||
|
||||
@@ -18,31 +18,50 @@ function pathArgument(payload: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
/** A TypeScript 7.1 callback reply: the value, `missing` for a path the
|
||||
* client hides, or `useOS` to delegate to the server's file system. */
|
||||
function reply71(result: unknown): string {
|
||||
if (result === undefined) return '{"kind":"useOS"}';
|
||||
if (result === null) return '{"kind":"missing"}';
|
||||
return JSON.stringify({ kind: "value", value: result });
|
||||
}
|
||||
|
||||
/** Preserve all three readFile answers. An empty reply delegates to the
|
||||
* native server's filesystem; {content:null} hides a path; {content:""}
|
||||
* supplies a present empty file. Conflating these changes module resolution. */
|
||||
export function registerTs7FileSystem(client: Ts7RpcClient, fs: Ts7FileSystem): void {
|
||||
* supplies a present empty file. Conflating these changes module resolution.
|
||||
* `ts71` tells whether the server speaks TypeScript 7.1's protocol, whose
|
||||
* replies carry the same three answers as reply71 encodes them. */
|
||||
export function registerTs7FileSystem(
|
||||
client: Ts7RpcClient,
|
||||
fs: Ts7FileSystem,
|
||||
ts71: () => boolean = () => false,
|
||||
): void {
|
||||
client.registerCallback("readFile", (payload) => {
|
||||
const content = fs.readFile(pathArgument(payload));
|
||||
if (ts71()) return reply71(content);
|
||||
return content === undefined ? "" : JSON.stringify({ content });
|
||||
});
|
||||
client.registerCallback("fileExists", (payload) => {
|
||||
const result = fs.fileExists(pathArgument(payload));
|
||||
if (ts71()) return reply71(result);
|
||||
if (result === undefined) return "";
|
||||
return result ? "true" : "false";
|
||||
});
|
||||
client.registerCallback("directoryExists", (payload) => {
|
||||
const result = fs.directoryExists(pathArgument(payload));
|
||||
if (ts71()) return reply71(result);
|
||||
if (result === undefined) return "";
|
||||
return result ? "true" : "false";
|
||||
});
|
||||
client.registerCallback("realpath", (payload) => {
|
||||
const result = fs.realpath(pathArgument(payload));
|
||||
if (ts71()) return reply71(result);
|
||||
if (result === undefined) return "";
|
||||
return JSON.stringify(result);
|
||||
});
|
||||
client.registerCallback("getAccessibleEntries", (payload) => {
|
||||
const result = fs.getAccessibleEntries(pathArgument(payload));
|
||||
if (ts71()) return reply71(result);
|
||||
if (result === undefined) return "";
|
||||
return JSON.stringify(result!);
|
||||
});
|
||||
|
||||
@@ -6,9 +6,9 @@ import { API } from "typescript/unstable/sync";
|
||||
import type { Node as SdkNode } from "typescript/unstable/ast";
|
||||
import { AstNode } from "./ast-node.js";
|
||||
import { Ts7RpcClient } from "./rpc-client.js";
|
||||
import { tsgoExecutable } from "./rpc-api.js";
|
||||
import { spawnTs7Wire } from "./rpc-process.js";
|
||||
import { Ts7Session } from "./session.js";
|
||||
import { ts7TestServers } from "./test-servers.js";
|
||||
import { SyntaxKind } from "./enums.js";
|
||||
import { getSourceFileOfNode } from "./node-functions.js";
|
||||
import { forEachChild } from "./ast.js";
|
||||
@@ -25,12 +25,14 @@ const source = [
|
||||
"export function predicate(value: unknown): value is Box { return value instanceof Box; }",
|
||||
"export function rest(...values: number[]): number { return values.length; }",
|
||||
"export function receiver(this: Box, value: number): number { return value; }",
|
||||
'export enum Kind { A = 1, B = "b", C = A + 2, D }',
|
||||
].join("\n");
|
||||
|
||||
let directory: string;
|
||||
let file: string;
|
||||
let config: string;
|
||||
let session: Ts7Session;
|
||||
const servers = ts7TestServers();
|
||||
const sessions = new Map<string, Ts7Session>();
|
||||
let oracle: API;
|
||||
|
||||
beforeAll(() => {
|
||||
@@ -47,15 +49,19 @@ beforeAll(() => {
|
||||
files: [file],
|
||||
}),
|
||||
);
|
||||
// Compared with the pinned SDK, which runs tsgo.
|
||||
session = new Ts7Session(
|
||||
new Ts7RpcClient(spawnTs7Wire(tsgoExecutable(), ["--api", "--cwd", directory])),
|
||||
);
|
||||
// Compared with the pinned SDK, which runs tsgo 7.0; tsc 7.1 answers these cases the same.
|
||||
for (const server of servers)
|
||||
sessions.set(
|
||||
server.name,
|
||||
new Ts7Session(
|
||||
new Ts7RpcClient(spawnTs7Wire(server.executable, ["--api", "--cwd", directory])),
|
||||
),
|
||||
);
|
||||
oracle = new API({ cwd: directory });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
session?.close();
|
||||
for (const session of sessions.values()) session.close();
|
||||
oracle?.close();
|
||||
if (directory) rmSync(directory, { recursive: true, force: true });
|
||||
});
|
||||
@@ -75,82 +81,115 @@ function shape(node: AstNode | SdkNode): unknown {
|
||||
};
|
||||
}
|
||||
|
||||
test("synthetic type roots and descendants match the pinned SDK without claiming SourceFile fields", () => {
|
||||
const snapshot = session.updateSnapshot({ openProjects: [config] });
|
||||
const sdkSnapshot = oracle.updateSnapshot({ openProjects: [config] });
|
||||
try {
|
||||
const project = snapshot.getProject(config)!;
|
||||
const sdk = sdkSnapshot.getProject(config)!;
|
||||
const root = project.program.getSourceFile(file)!;
|
||||
const expectedRoot = sdk.program.getSourceFile(file)!;
|
||||
let checked = 0;
|
||||
for (let index = 1; index < 7; index++) {
|
||||
const statement = root.statements.nodes[index]!;
|
||||
const expectedStatement = expectedRoot.statements[index]!;
|
||||
const node = statement.declarationList!.declarations!.nodes[0]!.name!;
|
||||
const expectedNode = Reflect.get(expectedStatement, "declarationList").declarations[0]
|
||||
.name as SdkNode;
|
||||
const type = project.checker.getTypeAtLocation(node)!;
|
||||
const expectedType = sdk.checker.getTypeAtLocation(expectedNode)!;
|
||||
const actual = project.checker.typeToTypeNode(type, node)!;
|
||||
const expected = sdk.checker.typeToTypeNode(expectedType, expectedNode)!;
|
||||
expect(shape(actual)).toEqual(shape(expected));
|
||||
expect(actual.kind).not.toBe(SyntaxKind.SourceFile);
|
||||
expect(actual.file.root).toBe(actual);
|
||||
expect(actual.file.node(1)).toBe(actual);
|
||||
expect(actual.parent).toBeUndefined();
|
||||
expect(() => actual.file.sourceFile).toThrow("expected a source file root");
|
||||
expect(() => getSourceFileOfNode(actual)).toThrow("expected a source file root");
|
||||
forEachChild(actual, (child) => {
|
||||
expect(child.parent).toBe(actual);
|
||||
});
|
||||
checked++;
|
||||
}
|
||||
expect(checked).toBe(6);
|
||||
expect(root.file.sourceFile).toBe(root);
|
||||
expect(getSourceFileOfNode(root)).toBe(root);
|
||||
} finally {
|
||||
snapshot.dispose();
|
||||
sdkSnapshot.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test("generic, predicate, rest and this-parameter signature fragments match the pinned SDK", () => {
|
||||
const snapshot = session.updateSnapshot({ openProjects: [config] });
|
||||
const sdkSnapshot = oracle.updateSnapshot({ openProjects: [config] });
|
||||
try {
|
||||
const project = snapshot.getProject(config)!;
|
||||
const sdk = sdkSnapshot.getProject(config)!;
|
||||
const root = project.program.getSourceFile(file)!;
|
||||
const expectedRoot = sdk.program.getSourceFile(file)!;
|
||||
for (let index = 7; index < 11; index++) {
|
||||
const node = root.statements.nodes[index]!;
|
||||
const expectedNode = expectedRoot.statements[index]!;
|
||||
const signature = project.checker.getSignatureFromDeclaration(node)!;
|
||||
const expectedSignature = sdk.checker.getSignatureFromDeclaration(expectedNode)!;
|
||||
const actual = project.checker.signatureToSignatureDeclaration(
|
||||
signature,
|
||||
SyntaxKind.FunctionType,
|
||||
node,
|
||||
)!;
|
||||
const expected = sdk.checker.signatureToSignatureDeclaration(
|
||||
expectedSignature,
|
||||
SyntaxKind.FunctionType,
|
||||
expectedNode,
|
||||
)!;
|
||||
expect(actual.kind).toBe(SyntaxKind.FunctionType);
|
||||
expect(shape(actual)).toEqual(shape(expected));
|
||||
expect(actual.file.root).toBe(actual);
|
||||
const parameters = actual.parameters!.nodes;
|
||||
expect(parameters.length).toBeGreaterThan(0);
|
||||
for (const parameter of parameters) {
|
||||
expect(parameter.parent).toBe(actual);
|
||||
expect(actual.file.node(parameter.index)).toBe(parameter);
|
||||
for (const server of servers)
|
||||
test(`synthetic type roots and descendants match the pinned SDK without claiming SourceFile fields (${server.name})`, () => {
|
||||
const session = sessions.get(server.name)!;
|
||||
const snapshot = session.updateSnapshot({ openProjects: [config] });
|
||||
const sdkSnapshot = oracle.updateSnapshot({ openProjects: [config] });
|
||||
try {
|
||||
const project = snapshot.getProject(config)!;
|
||||
const sdk = sdkSnapshot.getProject(config)!;
|
||||
const root = project.program.getSourceFile(file)!;
|
||||
const expectedRoot = sdk.program.getSourceFile(file)!;
|
||||
let checked = 0;
|
||||
for (let index = 1; index < 7; index++) {
|
||||
const statement = root.statements.nodes[index]!;
|
||||
const expectedStatement = expectedRoot.statements[index]!;
|
||||
const node = statement.declarationList!.declarations!.nodes[0]!.name!;
|
||||
const expectedNode = Reflect.get(expectedStatement, "declarationList").declarations[0]
|
||||
.name as SdkNode;
|
||||
const type = project.checker.getTypeAtLocation(node)!;
|
||||
const expectedType = sdk.checker.getTypeAtLocation(expectedNode)!;
|
||||
const actual = project.checker.typeToTypeNode(type, node)!;
|
||||
const expected = sdk.checker.typeToTypeNode(expectedType, expectedNode)!;
|
||||
expect(shape(actual)).toEqual(shape(expected));
|
||||
expect(actual.kind).not.toBe(SyntaxKind.SourceFile);
|
||||
expect(actual.file.root).toBe(actual);
|
||||
expect(actual.file.node(1)).toBe(actual);
|
||||
expect(actual.parent).toBeUndefined();
|
||||
expect(() => actual.file.sourceFile).toThrow("expected a source file root");
|
||||
expect(() => getSourceFileOfNode(actual)).toThrow("expected a source file root");
|
||||
forEachChild(actual, (child) => {
|
||||
expect(child.parent).toBe(actual);
|
||||
});
|
||||
checked++;
|
||||
}
|
||||
expect(() => actual.file.sourceFile).toThrow("expected a source file root");
|
||||
expect(checked).toBe(6);
|
||||
expect(root.file.sourceFile).toBe(root);
|
||||
expect(getSourceFileOfNode(root)).toBe(root);
|
||||
} finally {
|
||||
snapshot.dispose();
|
||||
sdkSnapshot.dispose();
|
||||
}
|
||||
} finally {
|
||||
snapshot.dispose();
|
||||
sdkSnapshot.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
for (const server of servers)
|
||||
test(`generic, predicate, rest and this-parameter signature fragments match the pinned SDK (${server.name})`, () => {
|
||||
const session = sessions.get(server.name)!;
|
||||
const snapshot = session.updateSnapshot({ openProjects: [config] });
|
||||
const sdkSnapshot = oracle.updateSnapshot({ openProjects: [config] });
|
||||
try {
|
||||
const project = snapshot.getProject(config)!;
|
||||
const sdk = sdkSnapshot.getProject(config)!;
|
||||
const root = project.program.getSourceFile(file)!;
|
||||
const expectedRoot = sdk.program.getSourceFile(file)!;
|
||||
for (let index = 7; index < 11; index++) {
|
||||
const node = root.statements.nodes[index]!;
|
||||
const expectedNode = expectedRoot.statements[index]!;
|
||||
const signature = project.checker.getSignatureFromDeclaration(node)!;
|
||||
const expectedSignature = sdk.checker.getSignatureFromDeclaration(expectedNode)!;
|
||||
const actual = project.checker.signatureToSignatureDeclaration(
|
||||
signature,
|
||||
SyntaxKind.FunctionType,
|
||||
node,
|
||||
)!;
|
||||
const expected = sdk.checker.signatureToSignatureDeclaration(
|
||||
expectedSignature,
|
||||
SyntaxKind.FunctionType,
|
||||
expectedNode,
|
||||
)!;
|
||||
expect(actual.kind).toBe(SyntaxKind.FunctionType);
|
||||
expect(shape(actual)).toEqual(shape(expected));
|
||||
expect(actual.file.root).toBe(actual);
|
||||
const parameters = actual.parameters!.nodes;
|
||||
expect(parameters.length).toBeGreaterThan(0);
|
||||
for (const parameter of parameters) {
|
||||
expect(parameter.parent).toBe(actual);
|
||||
expect(actual.file.node(parameter.index)).toBe(parameter);
|
||||
}
|
||||
expect(() => actual.file.sourceFile).toThrow("expected a source file root");
|
||||
}
|
||||
} finally {
|
||||
snapshot.dispose();
|
||||
sdkSnapshot.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
for (const server of servers)
|
||||
test(`enum member constant values match the pinned SDK (${server.name})`, () => {
|
||||
const session = sessions.get(server.name)!;
|
||||
const snapshot = session.updateSnapshot({ openProjects: [config] });
|
||||
const sdkSnapshot = oracle.updateSnapshot({ openProjects: [config] });
|
||||
try {
|
||||
const project = snapshot.getProject(config)!;
|
||||
const sdk = sdkSnapshot.getProject(config)!;
|
||||
const statement = project.program.getSourceFile(file)!.statements.nodes[11]!;
|
||||
const expected = Reflect.get(
|
||||
sdk.program.getSourceFile(file)!.statements[11]!,
|
||||
"members",
|
||||
) as SdkNode[];
|
||||
const members = statement.members!.nodes;
|
||||
expect(members.map((member) => project.checker.getConstantValue(member))).toEqual(
|
||||
expected.map((member) => sdk.checker.getConstantValue(member)),
|
||||
);
|
||||
expect(members.map((member) => project.checker.getConstantValue(member))).toEqual([
|
||||
1,
|
||||
"b",
|
||||
3,
|
||||
4,
|
||||
]);
|
||||
} finally {
|
||||
snapshot.dispose();
|
||||
sdkSnapshot.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
SemanticType,
|
||||
type SemanticDocument,
|
||||
type SemanticQuery,
|
||||
type Ts71SymbolData,
|
||||
type Ts71TypeData,
|
||||
present,
|
||||
} from "./semantic-model.js";
|
||||
import { SemanticSymbolFlags } from "./semantic-schema.generated.js";
|
||||
@@ -72,7 +74,8 @@ export interface SemanticCompletionOptions {
|
||||
triggerCharacter?: string;
|
||||
includeSymbol?: boolean;
|
||||
}
|
||||
interface CompletionResponse {
|
||||
/** `S` is TypeScript 7.0's SymbolResponse or 7.1's Ts71SymbolData. */
|
||||
interface CompletionResponse<S> {
|
||||
isIncomplete: boolean;
|
||||
entries: {
|
||||
name: string;
|
||||
@@ -82,9 +85,36 @@ interface CompletionResponse {
|
||||
filterText?: string;
|
||||
detail?: string;
|
||||
labelDetails?: { detail?: string; description?: string };
|
||||
symbol?: SymbolResponse;
|
||||
symbol?: S;
|
||||
}[];
|
||||
}
|
||||
|
||||
/** TypeScript 7.1's type predicate and index info replies. */
|
||||
interface TypePredicateResponse71 {
|
||||
kind: number;
|
||||
parameterIndex: number;
|
||||
parameterName?: string;
|
||||
type?: Ts71TypeData;
|
||||
}
|
||||
interface IndexInfoResponse71 {
|
||||
keyType: Ts71TypeData;
|
||||
valueType: Ts71TypeData;
|
||||
isReadonly?: boolean;
|
||||
declaration?: string;
|
||||
}
|
||||
|
||||
/** TypeScript 7.1's getConstantValue answer: the value, with a number's
|
||||
* non-finite values written as "+Infinity", "-Infinity" and "NaN". */
|
||||
interface ConstantValueResponse71 {
|
||||
isNumber: boolean;
|
||||
value?: string | number | boolean | null;
|
||||
}
|
||||
|
||||
interface ReferencedSymbolResponse<S> {
|
||||
definition: string;
|
||||
symbol?: S;
|
||||
references?: string[];
|
||||
}
|
||||
export interface SemanticCompletion {
|
||||
name: string;
|
||||
kind: number | undefined;
|
||||
@@ -369,6 +399,7 @@ export class SemanticChecker {
|
||||
return this.project.request<boolean>("isArrayLikeType", { type: type.id });
|
||||
}
|
||||
isTupleType(type: SemanticType): boolean {
|
||||
if (this.project.ts71()) return this.project.isTupleType71(type.id);
|
||||
return this.project.request<boolean>("isTupleType", { type: type.id });
|
||||
}
|
||||
isTypeAssignableTo(source: SemanticType, target: SemanticType): boolean {
|
||||
@@ -378,6 +409,16 @@ export class SemanticChecker {
|
||||
});
|
||||
}
|
||||
getConstantValue(node: AstNode): string | number | undefined {
|
||||
if (this.project.ts71()) {
|
||||
const data = this.project.request<ConstantValueResponse71 | null>("getConstantValue", {
|
||||
location: node.id,
|
||||
});
|
||||
const value = data?.value;
|
||||
if (typeof value === "number") return value;
|
||||
if (typeof value !== "string") return undefined;
|
||||
if (!data!.isNumber) return value;
|
||||
return value === "+Infinity" ? Infinity : value === "-Infinity" ? -Infinity : NaN;
|
||||
}
|
||||
return (
|
||||
this.project.request<string | number | null>("getConstantValue", { location: node.id }) ??
|
||||
undefined
|
||||
@@ -390,6 +431,20 @@ export class SemanticChecker {
|
||||
);
|
||||
}
|
||||
getTypePredicateOfSignature(signature: SemanticSignature): SemanticTypePredicate | undefined {
|
||||
if (this.project.ts71()) {
|
||||
const data = this.project.request<TypePredicateResponse71 | null>(
|
||||
"getTypePredicateOfSignature",
|
||||
{ signature: signature.id },
|
||||
);
|
||||
return data === null
|
||||
? undefined
|
||||
: {
|
||||
kind: data.kind,
|
||||
parameterIndex: data.parameterIndex,
|
||||
parameterName: data.parameterName,
|
||||
type: data.type === undefined ? undefined : this.project.type71(data.type),
|
||||
};
|
||||
}
|
||||
const data = this.project.request<TypePredicateResponse | null>("getTypePredicateOfSignature", {
|
||||
signature: signature.id,
|
||||
});
|
||||
@@ -403,6 +458,22 @@ export class SemanticChecker {
|
||||
};
|
||||
}
|
||||
getIndexInfosOfType(type: SemanticType): SemanticIndexInfo[] {
|
||||
if (this.project.ts71()) {
|
||||
const data = this.project.request<IndexInfoResponse71[] | null>("getIndexInfosOfType", {
|
||||
type: type.id,
|
||||
});
|
||||
return (data ?? []).map(
|
||||
(item) =>
|
||||
new SemanticIndexInfo(
|
||||
this.project.type71(item.keyType),
|
||||
this.project.type71(item.valueType),
|
||||
item.isReadonly ?? false,
|
||||
item.declaration === undefined
|
||||
? undefined
|
||||
: new SemanticNodeHandle(item.declaration, this.project),
|
||||
),
|
||||
);
|
||||
}
|
||||
const data = this.project.request<IndexInfoResponse[] | null>("getIndexInfosOfType", {
|
||||
type: type.id,
|
||||
});
|
||||
@@ -420,8 +491,14 @@ export class SemanticChecker {
|
||||
}
|
||||
private getWellKnownSymbols(): WellKnownSymbols {
|
||||
this.project.ensureActive();
|
||||
if (this.wellKnown === undefined)
|
||||
this.wellKnown = this.project.request<WellKnownSymbols>("getWellKnownSymbols");
|
||||
if (this.wellKnown === undefined) {
|
||||
const ids = this.project.request<WellKnownSymbols>("getWellKnownSymbols");
|
||||
this.wellKnown = {
|
||||
unknown: this.project.snapshotSymbolId(ids.unknown),
|
||||
undefined: this.project.snapshotSymbolId(ids.undefined),
|
||||
arguments: this.project.snapshotSymbolId(ids.arguments),
|
||||
};
|
||||
}
|
||||
return this.wellKnown;
|
||||
}
|
||||
isUnknownSymbol(symbol: SemanticSymbol): boolean {
|
||||
@@ -445,9 +522,24 @@ export class SemanticChecker {
|
||||
return (data ?? []).map((handle) => new SemanticNodeHandle(handle, this.project));
|
||||
}
|
||||
getReferencedSymbolsForNode(node: AstNode, position: number): SemanticReferencedSymbol[] {
|
||||
const data = this.project.request<
|
||||
{ definition: string; symbol?: SymbolResponse; references?: string[] }[] | null
|
||||
>("getReferencedSymbolsForNode", { node: node.id, position });
|
||||
const query = { node: node.id, position };
|
||||
if (this.project.ts71()) {
|
||||
const data = this.project.request<ReferencedSymbolResponse<Ts71SymbolData>[] | null>(
|
||||
"getReferencedSymbolsForNode",
|
||||
query,
|
||||
);
|
||||
return (data ?? []).map((item) => ({
|
||||
definition: new SemanticNodeHandle(item.definition, this.project),
|
||||
symbol: item.symbol === undefined ? undefined : this.project.symbol71(item.symbol),
|
||||
references: (item.references ?? []).map(
|
||||
(handle) => new SemanticNodeHandle(handle, this.project),
|
||||
),
|
||||
}));
|
||||
}
|
||||
const data = this.project.request<ReferencedSymbolResponse<SymbolResponse>[] | null>(
|
||||
"getReferencedSymbolsForNode",
|
||||
query,
|
||||
);
|
||||
return (data ?? []).map((item) => ({
|
||||
definition: new SemanticNodeHandle(item.definition, this.project),
|
||||
symbol: item.symbol === undefined ? undefined : this.project.symbol(item.symbol),
|
||||
@@ -479,12 +571,36 @@ export class SemanticChecker {
|
||||
position: number,
|
||||
options?: SemanticCompletionOptions,
|
||||
): { isIncomplete: boolean; entries: SemanticCompletion[] } | undefined {
|
||||
const data = this.project.request<CompletionResponse | null>("getCompletionsAtPosition", {
|
||||
const query = {
|
||||
file,
|
||||
position,
|
||||
triggerCharacter: options?.triggerCharacter,
|
||||
includeSymbol: options?.includeSymbol,
|
||||
});
|
||||
};
|
||||
if (this.project.ts71()) {
|
||||
const data = this.project.request<CompletionResponse<Ts71SymbolData> | null>(
|
||||
"getCompletionsAtPosition",
|
||||
query,
|
||||
);
|
||||
if (data === null) return undefined;
|
||||
return {
|
||||
isIncomplete: data.isIncomplete,
|
||||
entries: data.entries.map((item) => ({
|
||||
name: item.name,
|
||||
kind: item.kind,
|
||||
sortText: item.sortText,
|
||||
insertText: item.insertText,
|
||||
filterText: item.filterText,
|
||||
detail: item.detail,
|
||||
labelDetails: item.labelDetails,
|
||||
symbol: item.symbol === undefined ? undefined : this.project.symbol71(item.symbol),
|
||||
})),
|
||||
};
|
||||
}
|
||||
const data = this.project.request<CompletionResponse<SymbolResponse> | null>(
|
||||
"getCompletionsAtPosition",
|
||||
query,
|
||||
);
|
||||
if (data === null) return undefined;
|
||||
return {
|
||||
isIncomplete: data.isIncomplete,
|
||||
|
||||
@@ -149,7 +149,7 @@ function parseJsonSlice<T>(bytes: Uint8Array, start: number, end: number): T {
|
||||
|
||||
/** One element of a reply: null, or the object with leading id `id`. Objects
|
||||
* the caller already holds under that id are not decoded. */
|
||||
function replyElement<T extends { id: number }, R>(
|
||||
function replyElement<T extends object, R>(
|
||||
bytes: Uint8Array,
|
||||
start: number,
|
||||
end: number,
|
||||
@@ -172,7 +172,7 @@ function replyElement<T extends { id: number }, R>(
|
||||
* reply is null. Objects the caller already holds under their id, which a
|
||||
* reply repeats whenever it names them, are returned from `cached` without
|
||||
* decoding their text; the rest are decoded and handed to `build`. */
|
||||
export function decodeSemanticObject<T extends { id: number }, R>(
|
||||
export function decodeSemanticObject<T extends object, R>(
|
||||
bytes: Uint8Array,
|
||||
cached: (id: number) => R | undefined,
|
||||
build: (data: T) => R,
|
||||
@@ -187,7 +187,7 @@ export function decodeSemanticObject<T extends { id: number }, R>(
|
||||
/** A checker reply that is an array of objects (each with a numeric `id`) and
|
||||
* nulls, or null. An empty reply is null. Elements are resolved as in
|
||||
* decodeSemanticObject. */
|
||||
export function decodeSemanticObjects<T extends { id: number }, R>(
|
||||
export function decodeSemanticObjects<T extends object, R>(
|
||||
bytes: Uint8Array,
|
||||
cached: (id: number) => R | undefined,
|
||||
build: (data: T) => R,
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { SemanticChecker } from "./semantic-checker.js";
|
||||
import { SemanticSnapshot } from "./semantic-model.js";
|
||||
import { SyntaxKind, TypeFlags } from "./enums.js";
|
||||
|
||||
/** A snapshot on a fake TypeScript 7.1 server: `answer` gets each request's
|
||||
* method and parsed payload and returns the reply's JSON. */
|
||||
function server(answer: (method: string, query: Record<string, unknown>) => unknown) {
|
||||
const requests: { method: string; query: Record<string, unknown> }[] = [];
|
||||
const snapshot = new SemanticSnapshot(
|
||||
1,
|
||||
{
|
||||
text: (method, payload) => {
|
||||
const query = JSON.parse(payload) as Record<string, unknown>;
|
||||
requests.push({ method, query });
|
||||
return JSON.stringify(answer(method, query) ?? null);
|
||||
},
|
||||
binary(method, payload) {
|
||||
const text = this.text(method, payload);
|
||||
// AST replies (typeToTypeNode, signatureToSignatureDeclaration) are empty for none.
|
||||
return text === "null" ? new Uint8Array() : Buffer.from(text, "utf8");
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
const project = snapshot.addProject("project", () => undefined);
|
||||
return { snapshot, project, checker: new SemanticChecker(project), requests };
|
||||
}
|
||||
|
||||
const file = {
|
||||
fileName: "/main.ts",
|
||||
path: "/main.ts",
|
||||
contentHash: "0",
|
||||
parseOptionsKey: "0",
|
||||
scriptKind: 3,
|
||||
nodeId: "77",
|
||||
};
|
||||
|
||||
test("TypeScript 7.1 symbol references round-trip through local ids", () => {
|
||||
const box = { kind: 0, file, id: 1 };
|
||||
const parent = { kind: 0, file, id: 2 };
|
||||
const { project, checker, requests } = server((method, query) => {
|
||||
if (method === "getTypeAtPosition")
|
||||
return { id: 10, flags: TypeFlags.Object, objectFlags: 1, symbol: { id: 1, file: "77" } };
|
||||
if (method === "getSymbolOfType")
|
||||
return {
|
||||
reference: box,
|
||||
name: "Box",
|
||||
flags: 32,
|
||||
checkFlags: 0,
|
||||
parent: { id: 2, file: "77" },
|
||||
};
|
||||
if (method === "getParentOfSymbol") {
|
||||
expect(query["symbol"]).toEqual(box);
|
||||
expect(query["objectId"]).toBeUndefined();
|
||||
return { reference: parent, name: '"/main"', flags: 512, checkFlags: 0 };
|
||||
}
|
||||
if (method === "getDeclaredTypeOfSymbol") {
|
||||
expect(query["symbol"]).toEqual(box);
|
||||
return { id: 11, flags: TypeFlags.Object, isTupleType: true };
|
||||
}
|
||||
if (method === "getExportsOfModule") {
|
||||
expect(query["symbol"]).toEqual(parent);
|
||||
return [{ reference: box, name: "Box", flags: 32, checkFlags: 0 }];
|
||||
}
|
||||
throw new Error(`unexpected ${method}`);
|
||||
});
|
||||
const type = checker.getTypeAtPosition("/main.ts", 0)!;
|
||||
const symbol = type.getSymbol()!;
|
||||
expect(symbol.escapedName).toBe("Box");
|
||||
// The compact reference in the type and the full one in the symbol name one symbol.
|
||||
expect(project.snapshot.cachedSymbol(symbol.id)).toBe(symbol);
|
||||
const owner = symbol.getParent()!;
|
||||
expect(owner.escapedName).toBe('"/main"');
|
||||
expect(checker.getExportsOfModule(owner)).toEqual([symbol]);
|
||||
const declared = checker.getDeclaredTypeOfSymbol(symbol);
|
||||
expect(checker.isTupleType(declared)).toBe(true);
|
||||
expect(checker.isTupleType(type)).toBe(false);
|
||||
expect(requests.map((request) => request.method)).not.toContain("isTupleType");
|
||||
});
|
||||
|
||||
test("TypeScript 7.1 requests name types and signatures as 7.1 does", () => {
|
||||
const { project, checker, requests } = server((method) => {
|
||||
if (method === "getApparentType" || method === "getReturnTypeOfSignature")
|
||||
return { id: 12, flags: TypeFlags.Number };
|
||||
if (method === "signatureToSignatureDeclaration") return null;
|
||||
if (method === "getWellKnownSymbols") return { unknown: 5, undefined: 6, arguments: 7 };
|
||||
if (method === "getSymbolAtPosition")
|
||||
return {
|
||||
reference: { kind: 1, snapshot: 1, project: "project", id: 6 },
|
||||
name: "undefined",
|
||||
flags: 3,
|
||||
checkFlags: 0,
|
||||
};
|
||||
throw new Error(`unexpected ${method}`);
|
||||
});
|
||||
const type = project.type({ id: 1, flags: TypeFlags.Object });
|
||||
const signature = project.signature({ id: 3, flags: 0 });
|
||||
checker.getApparentType(type);
|
||||
checker.getReturnTypeOfSignature(signature);
|
||||
checker.signatureToSignatureDeclaration(signature, SyntaxKind.FunctionType);
|
||||
const queries = new Map(requests.map((request) => [request.method, request.query]));
|
||||
expect(queries.get("getApparentType")).toMatchObject({ objectId: 1 });
|
||||
expect(queries.get("getApparentType")!["type"]).toBeUndefined();
|
||||
expect(queries.get("getReturnTypeOfSignature")).toMatchObject({ objectId: 3 });
|
||||
// 7.1 numbers every kind from SourceKeyword up one higher.
|
||||
expect(queries.get("signatureToSignatureDeclaration")).toMatchObject({
|
||||
kind: SyntaxKind.FunctionType + 1,
|
||||
});
|
||||
// A well-known symbol id names a symbol of the snapshot.
|
||||
const undefinedSymbol = checker.getSymbolAtPosition("/main.ts", 0)!;
|
||||
expect(checker.isUndefinedSymbol(undefinedSymbol)).toBe(true);
|
||||
expect(checker.isUnknownSymbol(undefinedSymbol)).toBe(false);
|
||||
});
|
||||
|
||||
test("TypeScript 7.1 constant values are read from their answer object", () => {
|
||||
const answers = [
|
||||
{ isNumber: true, value: 1 },
|
||||
{ isNumber: false, value: "a" },
|
||||
{ isNumber: true, value: "+Infinity" },
|
||||
{ isNumber: true, value: "-Infinity" },
|
||||
{ isNumber: true, value: "NaN" },
|
||||
null,
|
||||
];
|
||||
let next = 0;
|
||||
const { checker } = server((method) => {
|
||||
expect(method).toBe("getConstantValue");
|
||||
return answers[next++];
|
||||
});
|
||||
const node = { id: "2.306./main.ts" } as Parameters<typeof checker.getConstantValue>[0];
|
||||
expect(checker.getConstantValue(node)).toBe(1);
|
||||
expect(checker.getConstantValue(node)).toBe("a");
|
||||
expect(checker.getConstantValue(node)).toBe(Infinity);
|
||||
expect(checker.getConstantValue(node)).toBe(-Infinity);
|
||||
expect(checker.getConstantValue(node)).toBeNaN();
|
||||
expect(checker.getConstantValue(node)).toBeUndefined();
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
import { AstNode } from "./ast-node.js";
|
||||
import type { __String } from "./ast-types.js";
|
||||
import type * as enums from "./enums.generated.js";
|
||||
import { parseAstNodeHandle } from "./ast-wire.js";
|
||||
import { parseAstNodeHandle, syntaxKind71 } from "./ast-wire.js";
|
||||
import type { SemanticChecker, SemanticJsDocTag } from "./semantic-checker.js";
|
||||
import { decodeSemanticObject, decodeSemanticObjects, parseSemanticJson } from "./semantic-json.js";
|
||||
import {
|
||||
@@ -44,8 +44,9 @@ export interface SemanticQuery {
|
||||
position?: number | undefined;
|
||||
positions?: readonly number[] | undefined;
|
||||
type?: number | undefined;
|
||||
symbol?: number | undefined;
|
||||
symbols?: number[] | undefined;
|
||||
/** A symbol's id; on a TypeScript 7.1 server, its reference (SemanticProject.encode). */
|
||||
symbol?: number | Ts71SymbolReference | undefined;
|
||||
symbols?: (number | Ts71SymbolReference)[] | undefined;
|
||||
signature?: number | undefined;
|
||||
source?: number | undefined;
|
||||
target?: number | undefined;
|
||||
@@ -61,6 +62,214 @@ export interface SemanticQuery {
|
||||
includeSymbol?: boolean | undefined;
|
||||
}
|
||||
|
||||
/** A symbol in TypeScript 7.1's protocol: its owner, a source file (kind 0)
|
||||
* or the snapshot and project (kind 1), and its id within that owner. */
|
||||
export interface Ts71SymbolReference {
|
||||
kind: number;
|
||||
file?: Ts71SourceFileDescriptor;
|
||||
snapshot?: number;
|
||||
project?: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface Ts71SourceFileDescriptor {
|
||||
fileName: string;
|
||||
path: string;
|
||||
contentHash: string;
|
||||
parseOptionsKey: string;
|
||||
scriptKind: number;
|
||||
nodeId: string;
|
||||
}
|
||||
|
||||
/** A symbol another TypeScript 7.1 response names: its id and, for a symbol
|
||||
* a source file owns, the file's node ID. Without a file, the response's
|
||||
* snapshot and project own it. */
|
||||
export interface Ts71CompactReference {
|
||||
id: number;
|
||||
file?: string;
|
||||
}
|
||||
|
||||
/** A symbol as TypeScript 7.1 reports it. */
|
||||
export interface Ts71SymbolData {
|
||||
reference: Ts71SymbolReference;
|
||||
name: string;
|
||||
flags: number;
|
||||
checkFlags: number;
|
||||
declarations?: string[];
|
||||
valueDeclaration?: string;
|
||||
parent?: Ts71CompactReference;
|
||||
exportSymbol?: Ts71CompactReference;
|
||||
}
|
||||
|
||||
/** A type as TypeScript 7.1 reports it: TypeResponse with compact symbol
|
||||
* references. */
|
||||
export interface Ts71TypeData {
|
||||
id: number;
|
||||
flags: number;
|
||||
objectFlags?: number;
|
||||
value?: string | number | boolean | null;
|
||||
freshType?: number;
|
||||
regularType?: number;
|
||||
target?: number;
|
||||
typeParameters?: number[];
|
||||
outerTypeParameters?: number[];
|
||||
localTypeParameters?: number[];
|
||||
elementFlags?: number[];
|
||||
fixedLength?: number;
|
||||
readonly?: boolean;
|
||||
objectType?: number;
|
||||
indexType?: number;
|
||||
checkType?: number;
|
||||
extendsType?: number;
|
||||
baseType?: number;
|
||||
substConstraint?: number;
|
||||
texts?: string[];
|
||||
intrinsicName?: string;
|
||||
isThisType?: boolean;
|
||||
/** Whether the type is a tuple type, which 7.0 answers with isTupleType. */
|
||||
isTupleType?: boolean;
|
||||
aliasTypeArguments?: number[];
|
||||
aliasSymbol?: Ts71CompactReference;
|
||||
symbol?: Ts71CompactReference;
|
||||
}
|
||||
|
||||
/** A signature as TypeScript 7.1 reports it. */
|
||||
export interface Ts71SignatureData {
|
||||
id: number;
|
||||
flags: number;
|
||||
declaration?: string;
|
||||
typeParameters?: number[];
|
||||
parameters?: Ts71CompactReference[];
|
||||
thisParameter?: Ts71CompactReference;
|
||||
target?: number;
|
||||
}
|
||||
|
||||
/** The methods whose source is a symbol: TypeScript 7.0 names it by
|
||||
* `objectId`, 7.1 by its reference in `symbol`. */
|
||||
const SYMBOL_PROPERTY_METHODS = new Set([
|
||||
"getParentOfSymbol",
|
||||
"getExportSymbolOfSymbol",
|
||||
"getMembersOfSymbol",
|
||||
"getExportsOfSymbol",
|
||||
]);
|
||||
|
||||
/** The methods TypeScript 7.1 gives their type or signature in `objectId`,
|
||||
* where 7.0 takes it in `type` or `signature`. */
|
||||
const OBJECT_ID_METHODS_7_1 = new Map<string, "type" | "signature">([
|
||||
["getApparentType", "type"],
|
||||
["getConstraintOfTypeParameter", "type"],
|
||||
["getNonNullableType", "type"],
|
||||
["getReturnTypeOfSignature", "signature"],
|
||||
]);
|
||||
|
||||
/** TypeScript 7.1's symbol references by local id. The semantic model keeps
|
||||
* TypeScript 7.0's numeric symbol ids; on a 7.1 server, each symbol owner
|
||||
* and id gets a local id, and requests send the reference back. A compact
|
||||
* reference gets the local id of the symbol it names, so a later response
|
||||
* with that symbol's full reference finds the same id. */
|
||||
export class Ts71SymbolTable {
|
||||
private readonly locals = new Map<string, number>();
|
||||
private readonly references: (Ts71SymbolReference | undefined)[] = [undefined];
|
||||
|
||||
private allocate(key: string): number {
|
||||
let local = this.locals.get(key);
|
||||
if (local === undefined) {
|
||||
local = this.references.length;
|
||||
this.references.push(undefined);
|
||||
this.locals.set(key, local);
|
||||
}
|
||||
return local;
|
||||
}
|
||||
|
||||
/** The local id of `reference`, which requests then name it by. */
|
||||
fromReference(reference: Ts71SymbolReference): number {
|
||||
const key =
|
||||
reference.kind === 0
|
||||
? `f${reference.file?.nodeId ?? ""}:${reference.id}`
|
||||
: `s${reference.project ?? ""}:${reference.id}`;
|
||||
const local = this.allocate(key);
|
||||
this.references[local] = reference;
|
||||
return local;
|
||||
}
|
||||
|
||||
/** The local id of a compact reference in a response for `project`. */
|
||||
fromCompact(reference: Ts71CompactReference, project: string): number {
|
||||
return this.allocate(
|
||||
reference.file !== undefined
|
||||
? `f${reference.file}:${reference.id}`
|
||||
: `s${project}:${reference.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** The reference of local id `local`. Throws for a symbol no response has
|
||||
* described yet. */
|
||||
reference(local: number): Ts71SymbolReference {
|
||||
const reference = this.references[local];
|
||||
if (reference === undefined) throw new Error(`Unknown TypeScript 7.1 symbol ${local}`);
|
||||
return reference;
|
||||
}
|
||||
|
||||
/** `data` in TypeScript 7.0's shape, with local ids. */
|
||||
symbolData(data: Ts71SymbolData, project: string): SymbolResponse {
|
||||
const result: SymbolResponse = {
|
||||
id: this.fromReference(data.reference),
|
||||
project,
|
||||
name: data.name,
|
||||
flags: data.flags,
|
||||
checkFlags: data.checkFlags,
|
||||
};
|
||||
if (data.declarations !== undefined) result.declarations = data.declarations;
|
||||
if (data.valueDeclaration !== undefined) result.valueDeclaration = data.valueDeclaration;
|
||||
if (data.parent !== undefined) result.parent = this.fromCompact(data.parent, project);
|
||||
if (data.exportSymbol !== undefined)
|
||||
result.exportSymbol = this.fromCompact(data.exportSymbol, project);
|
||||
return result;
|
||||
}
|
||||
|
||||
typeData(data: Ts71TypeData, project: string): TypeResponse {
|
||||
const result: TypeResponse = { id: data.id, flags: data.flags };
|
||||
if (data.objectFlags !== undefined) result.objectFlags = data.objectFlags;
|
||||
if (data.value !== undefined) result.value = data.value;
|
||||
if (data.freshType !== undefined) result.freshType = data.freshType;
|
||||
if (data.regularType !== undefined) result.regularType = data.regularType;
|
||||
if (data.target !== undefined) result.target = data.target;
|
||||
if (data.typeParameters !== undefined) result.typeParameters = data.typeParameters;
|
||||
if (data.outerTypeParameters !== undefined)
|
||||
result.outerTypeParameters = data.outerTypeParameters;
|
||||
if (data.localTypeParameters !== undefined)
|
||||
result.localTypeParameters = data.localTypeParameters;
|
||||
if (data.elementFlags !== undefined) result.elementFlags = data.elementFlags;
|
||||
if (data.fixedLength !== undefined) result.fixedLength = data.fixedLength;
|
||||
if (data.readonly !== undefined) result.readonly = data.readonly;
|
||||
if (data.objectType !== undefined) result.objectType = data.objectType;
|
||||
if (data.indexType !== undefined) result.indexType = data.indexType;
|
||||
if (data.checkType !== undefined) result.checkType = data.checkType;
|
||||
if (data.extendsType !== undefined) result.extendsType = data.extendsType;
|
||||
if (data.baseType !== undefined) result.baseType = data.baseType;
|
||||
if (data.substConstraint !== undefined) result.substConstraint = data.substConstraint;
|
||||
if (data.texts !== undefined) result.texts = data.texts;
|
||||
if (data.intrinsicName !== undefined) result.intrinsicName = data.intrinsicName;
|
||||
if (data.isThisType !== undefined) result.isThisType = data.isThisType;
|
||||
if (data.aliasTypeArguments !== undefined) result.aliasTypeArguments = data.aliasTypeArguments;
|
||||
if (data.aliasSymbol !== undefined)
|
||||
result.aliasSymbol = this.fromCompact(data.aliasSymbol, project);
|
||||
if (data.symbol !== undefined) result.symbol = this.fromCompact(data.symbol, project);
|
||||
return result;
|
||||
}
|
||||
|
||||
signatureData(data: Ts71SignatureData, project: string): SignatureResponse {
|
||||
const result: SignatureResponse = { id: data.id, flags: data.flags };
|
||||
if (data.declaration !== undefined) result.declaration = data.declaration;
|
||||
if (data.typeParameters !== undefined) result.typeParameters = data.typeParameters;
|
||||
if (data.parameters !== undefined)
|
||||
result.parameters = data.parameters.map((parameter) => this.fromCompact(parameter, project));
|
||||
if (data.thisParameter !== undefined)
|
||||
result.thisParameter = this.fromCompact(data.thisParameter, project);
|
||||
if (data.target !== undefined) result.target = data.target;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class SemanticProgram {
|
||||
constructor(private readonly sourceFile: (path: string) => AstNode | undefined) {}
|
||||
getSourceFile(path: string): AstNode | undefined {
|
||||
@@ -77,10 +286,16 @@ export class SemanticSnapshot {
|
||||
private readonly symbols = new Map<number, SemanticSymbol>();
|
||||
private disposed = false;
|
||||
|
||||
/** TypeScript 7.1's symbol references, on a server that speaks 7.1. */
|
||||
readonly references: Ts71SymbolTable | undefined;
|
||||
|
||||
constructor(
|
||||
readonly id: number,
|
||||
readonly transport: SemanticTransport,
|
||||
) {}
|
||||
ts71 = false,
|
||||
) {
|
||||
this.references = ts71 ? new Ts71SymbolTable() : undefined;
|
||||
}
|
||||
|
||||
addProject(id: string, sourceFile: (path: string) => AstNode | undefined): SemanticProject {
|
||||
this.ensureActive();
|
||||
@@ -128,6 +343,9 @@ export class SemanticSnapshot {
|
||||
|
||||
export class SemanticProject {
|
||||
private readonly types = new Map<number, SemanticType>();
|
||||
/** TypeScript 7.1's isTupleType answers, by type id: 7.1 reports them on
|
||||
* the type instead of answering an isTupleType request. */
|
||||
private readonly tupleTypes = new Map<number, boolean>();
|
||||
private readonly signatures = new Map<number, SemanticSignature>();
|
||||
private readonly cleanups: (() => void)[] = [];
|
||||
private disposed = false;
|
||||
@@ -150,24 +368,90 @@ export class SemanticProject {
|
||||
this.cleanups.push(cleanup);
|
||||
}
|
||||
|
||||
request<T>(method: string, query: SemanticQuery = {}): T {
|
||||
/** `query` for `method` in this project's snapshot. On a TypeScript 7.1
|
||||
* server, symbol ids become their references, and a symbol property
|
||||
* method takes its source symbol's reference in `symbol`. */
|
||||
private encode(method: string, query: SemanticQuery): string {
|
||||
this.ensureActive();
|
||||
query.snapshot = this.snapshot.id;
|
||||
query.project = this.id;
|
||||
return parseSemanticJson<T>(this.snapshot.transport.text(method, JSON.stringify(query)));
|
||||
const references = this.snapshot.references;
|
||||
if (references !== undefined) {
|
||||
if (typeof query.symbol === "number") query.symbol = references.reference(query.symbol);
|
||||
if (query.symbols !== undefined)
|
||||
query.symbols = query.symbols.map((symbol) =>
|
||||
typeof symbol === "number" ? references.reference(symbol) : symbol,
|
||||
);
|
||||
if (SYMBOL_PROPERTY_METHODS.has(method) && query.objectId !== undefined) {
|
||||
query.symbol = references.reference(query.objectId);
|
||||
query.objectId = undefined;
|
||||
}
|
||||
// signatureToSignatureDeclaration's kind is a SyntaxKind.
|
||||
if (method === "signatureToSignatureDeclaration" && query.kind !== undefined)
|
||||
query.kind = syntaxKind71(query.kind);
|
||||
const field = OBJECT_ID_METHODS_7_1.get(method);
|
||||
if (field === "type") {
|
||||
query.objectId = query.type;
|
||||
query.type = undefined;
|
||||
} else if (field === "signature") {
|
||||
query.objectId = query.signature;
|
||||
query.signature = undefined;
|
||||
}
|
||||
}
|
||||
return JSON.stringify(query);
|
||||
}
|
||||
|
||||
request<T>(method: string, query: SemanticQuery = {}): T {
|
||||
return parseSemanticJson<T>(this.snapshot.transport.text(method, this.encode(method, query)));
|
||||
}
|
||||
|
||||
requestBinary(method: string, query: SemanticQuery): Uint8Array {
|
||||
this.ensureActive();
|
||||
query.snapshot = this.snapshot.id;
|
||||
query.project = this.id;
|
||||
return this.snapshot.transport.binary(method, JSON.stringify(query));
|
||||
return this.snapshot.transport.binary(method, this.encode(method, query));
|
||||
}
|
||||
|
||||
/** A type a TypeScript 7.1 reply holds. */
|
||||
type71(data: Ts71TypeData): SemanticType {
|
||||
const references = this.snapshot.references;
|
||||
if (references === undefined) throw new Error("TypeScript 7.1 type from a 7.0 server");
|
||||
this.tupleTypes.set(data.id, data.isTupleType === true);
|
||||
return this.type(references.typeData(data, this.id));
|
||||
}
|
||||
|
||||
/** Whether a type a TypeScript 7.1 server reported is a tuple type. */
|
||||
isTupleType71(type: number): boolean {
|
||||
const result = this.tupleTypes.get(type);
|
||||
if (result === undefined) throw new Error(`Unknown TypeScript 7.1 type ${type}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Whether the server speaks TypeScript 7.1's protocol. */
|
||||
ts71(): boolean {
|
||||
return this.snapshot.references !== undefined;
|
||||
}
|
||||
|
||||
/** A symbol a TypeScript 7.1 reply holds. */
|
||||
symbol71(data: Ts71SymbolData): SemanticSymbol {
|
||||
const references = this.snapshot.references;
|
||||
if (references === undefined) throw new Error("TypeScript 7.1 symbol from a 7.0 server");
|
||||
return this.symbol(references.symbolData(data, this.id));
|
||||
}
|
||||
|
||||
/** The local id of a symbol id a TypeScript 7.1 server reports as a plain
|
||||
* number (getWellKnownSymbols): a symbol of this project's snapshot. */
|
||||
snapshotSymbolId(id: number): number {
|
||||
const references = this.snapshot.references;
|
||||
return references === undefined ? id : references.fromCompact({ id }, this.id);
|
||||
}
|
||||
|
||||
/** A reply of one type, or undefined for null. */
|
||||
requestType(method: string, query: SemanticQuery): SemanticType | undefined {
|
||||
const bytes = this.requestBinary(method, query);
|
||||
const cached = (id: number): SemanticType | undefined => this.types.get(id);
|
||||
const references = this.snapshot.references;
|
||||
if (references !== undefined) {
|
||||
const build71 = (data: Ts71TypeData): SemanticType => this.type71(data);
|
||||
return decodeSemanticObject(bytes, cached, build71) ?? undefined;
|
||||
}
|
||||
const build = (data: TypeResponse): SemanticType => this.type(data);
|
||||
return decodeSemanticObject(bytes, cached, build) ?? undefined;
|
||||
}
|
||||
@@ -176,6 +460,11 @@ export class SemanticProject {
|
||||
requestTypes(method: string, query: SemanticQuery): (SemanticType | undefined)[] {
|
||||
const bytes = this.requestBinary(method, query);
|
||||
const cached = (id: number): SemanticType | undefined => this.types.get(id);
|
||||
const references = this.snapshot.references;
|
||||
if (references !== undefined) {
|
||||
const build71 = (data: Ts71TypeData): SemanticType => this.type71(data);
|
||||
return (decodeSemanticObjects(bytes, cached, build71) ?? []).map((item) => item ?? undefined);
|
||||
}
|
||||
const build = (data: TypeResponse): SemanticType => this.type(data);
|
||||
return (decodeSemanticObjects(bytes, cached, build) ?? []).map((item) => item ?? undefined);
|
||||
}
|
||||
@@ -183,6 +472,14 @@ export class SemanticProject {
|
||||
/** A reply of one symbol, or undefined for null. */
|
||||
requestSymbol(method: string, query: SemanticQuery): SemanticSymbol | undefined {
|
||||
const bytes = this.requestBinary(method, query);
|
||||
const references = this.snapshot.references;
|
||||
if (references !== undefined) {
|
||||
// TypeScript 7.1's symbols start with their reference, not an id.
|
||||
const none = (): SemanticSymbol | undefined => undefined;
|
||||
const build71 = (data: Ts71SymbolData): SemanticSymbol =>
|
||||
this.symbol(references.symbolData(data, this.id));
|
||||
return decodeSemanticObject(bytes, none, build71) ?? undefined;
|
||||
}
|
||||
const cached = (id: number): SemanticSymbol | undefined => this.snapshot.cachedSymbol(id);
|
||||
const build = (data: SymbolResponse): SemanticSymbol => this.symbol(data);
|
||||
return decodeSemanticObject(bytes, cached, build) ?? undefined;
|
||||
@@ -191,6 +488,13 @@ export class SemanticProject {
|
||||
/** A reply of symbols and nulls (undefined here), or [] for null. */
|
||||
requestSymbols(method: string, query: SemanticQuery): (SemanticSymbol | undefined)[] {
|
||||
const bytes = this.requestBinary(method, query);
|
||||
const references = this.snapshot.references;
|
||||
if (references !== undefined) {
|
||||
const none = (): SemanticSymbol | undefined => undefined;
|
||||
const build71 = (data: Ts71SymbolData): SemanticSymbol =>
|
||||
this.symbol(references.symbolData(data, this.id));
|
||||
return (decodeSemanticObjects(bytes, none, build71) ?? []).map((item) => item ?? undefined);
|
||||
}
|
||||
const cached = (id: number): SemanticSymbol | undefined => this.snapshot.cachedSymbol(id);
|
||||
const build = (data: SymbolResponse): SemanticSymbol => this.symbol(data);
|
||||
return (decodeSemanticObjects(bytes, cached, build) ?? []).map((item) => item ?? undefined);
|
||||
@@ -200,6 +504,12 @@ export class SemanticProject {
|
||||
requestSignature(method: string, query: SemanticQuery): SemanticSignature | undefined {
|
||||
const bytes = this.requestBinary(method, query);
|
||||
const cached = (id: number): SemanticSignature | undefined => this.signatures.get(id);
|
||||
const references = this.snapshot.references;
|
||||
if (references !== undefined) {
|
||||
const build71 = (data: Ts71SignatureData): SemanticSignature =>
|
||||
this.signature(references.signatureData(data, this.id));
|
||||
return decodeSemanticObject(bytes, cached, build71) ?? undefined;
|
||||
}
|
||||
const build = (data: SignatureResponse): SemanticSignature => this.signature(data);
|
||||
return decodeSemanticObject(bytes, cached, build) ?? undefined;
|
||||
}
|
||||
@@ -208,6 +518,12 @@ export class SemanticProject {
|
||||
requestSignatures(method: string, query: SemanticQuery): (SemanticSignature | undefined)[] {
|
||||
const bytes = this.requestBinary(method, query);
|
||||
const cached = (id: number): SemanticSignature | undefined => this.signatures.get(id);
|
||||
const references = this.snapshot.references;
|
||||
if (references !== undefined) {
|
||||
const build71 = (data: Ts71SignatureData): SemanticSignature =>
|
||||
this.signature(references.signatureData(data, this.id));
|
||||
return (decodeSemanticObjects(bytes, cached, build71) ?? []).map((item) => item ?? undefined);
|
||||
}
|
||||
const build = (data: SignatureResponse): SemanticSignature => this.signature(data);
|
||||
return (decodeSemanticObjects(bytes, cached, build) ?? []).map((item) => item ?? undefined);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type Ts7FileSystem,
|
||||
} from "./rpc-filesystem.js";
|
||||
import { spawnTs7Wire } from "./rpc-process.js";
|
||||
import { tsgoExecutable } from "./rpc-api.js";
|
||||
import { ts7TestServers, type Ts7TestServer } from "./test-servers.js";
|
||||
import { Ts7Session, Ts7SessionProject, Ts7SessionSnapshot } from "./session.js";
|
||||
import { AstNode } from "./ast-node.js";
|
||||
import { ts7Diagnostic } from "./diagnostic.js";
|
||||
@@ -27,10 +27,16 @@ const fallback: Ts7FileSystem = {
|
||||
getAccessibleEntries: () => undefined,
|
||||
};
|
||||
|
||||
// These tests compare the session with the pinned SDK, which runs tsgo, so the session runs tsgo too.
|
||||
function transport(cwd: string, fs: Ts7FileSystem = fallback, timing = false) {
|
||||
// These tests compare the session with the pinned SDK, which runs tsgo 7.0. The session runs
|
||||
// tsc 7.0 and tsc 7.1, whose answers to these cases are the same.
|
||||
function transport(
|
||||
server: Ts7TestServer,
|
||||
cwd: string,
|
||||
fs: Ts7FileSystem = fallback,
|
||||
timing = false,
|
||||
) {
|
||||
const rpc = new Ts7RpcClient(
|
||||
spawnTs7Wire(tsgoExecutable(), [
|
||||
spawnTs7Wire(server.executable, [
|
||||
"--api",
|
||||
"--cwd",
|
||||
cwd,
|
||||
@@ -38,265 +44,291 @@ function transport(cwd: string, fs: Ts7FileSystem = fallback, timing = false) {
|
||||
...(timing ? ["--timing"] : []),
|
||||
]),
|
||||
);
|
||||
registerTs7FileSystem(rpc, fs);
|
||||
registerTs7FileSystem(rpc, fs, () => server.ts71);
|
||||
return rpc;
|
||||
}
|
||||
|
||||
function connect(cwd: string, fs: Ts7FileSystem = fallback, timing = false) {
|
||||
return new Ts7Session(transport(cwd, fs, timing), timing);
|
||||
function connect(server: Ts7TestServer, cwd: string, fs: Ts7FileSystem = fallback, timing = false) {
|
||||
return new Ts7Session(transport(server, cwd, fs, timing), timing);
|
||||
}
|
||||
|
||||
test("owned snapshots preserve SDK options, diagnostics, metadata and default-project lookup", () => {
|
||||
const dir = mkdtempSync(join(tempRoot, "scriptc-session-parity-"));
|
||||
const file = protocolPath(join(dir, "main.ts"));
|
||||
const config = protocolPath(join(dir, "tsconfig.json"));
|
||||
writeFileSync(file, 'export const answer: number = "wrong";\n');
|
||||
writeFileSync(
|
||||
config,
|
||||
JSON.stringify({
|
||||
compilerOptions: {
|
||||
strict: true,
|
||||
target: "esnext",
|
||||
types: [],
|
||||
declaration: true,
|
||||
sourceMap: true,
|
||||
paths: { "named/*": ["./*", "../*"] },
|
||||
},
|
||||
files: [file],
|
||||
}),
|
||||
);
|
||||
const session = connect(dir, fallback, true);
|
||||
const sdk = new API({ cwd: dir });
|
||||
try {
|
||||
expect(session.parseConfigFile(config)).toEqual(sdk.parseConfigFile(config));
|
||||
const snapshot = session.updateSnapshot({ openProjects: [config], openFiles: [file] });
|
||||
const oracle = sdk.updateSnapshot({ openProjects: [config], openFiles: [file] });
|
||||
expect(snapshot).toBeInstanceOf(Ts7SessionSnapshot);
|
||||
const project = snapshot.getProject(config)!;
|
||||
const expected = oracle.getProject(config)!;
|
||||
expect(project).toBeInstanceOf(Ts7SessionProject);
|
||||
expect(project.program.getCompilerOptions()).toEqual(expected.program.getCompilerOptions());
|
||||
expect(project.rootFiles).toEqual(expected.rootFiles);
|
||||
expect(snapshot.getDefaultProjectForFile(file)).toBe(project);
|
||||
expect(snapshot.getProject(protocolPath(join(dir, "./tsconfig.json")))).toBe(project);
|
||||
expect(project.program.getSourceFileNames()).toEqual(expected.program.getSourceFileNames());
|
||||
const source = project.program.getSourceFile(file)!;
|
||||
expect(source).toBeInstanceOf(AstNode);
|
||||
expect(source.text).toBe(expected.program.getSourceFile(file)!.text);
|
||||
const metadata = project.program.getSourceFileMetadata(file)!;
|
||||
expect(metadata).toEqual(expected.program.getSourceFileMetadata(file));
|
||||
expect(project.program.getSourceFileMetadata(file)).toBe(metadata);
|
||||
const missing = protocolPath(join(dir, "missing.ts"));
|
||||
expect(project.program.getSourceFile(missing)).toBeUndefined();
|
||||
const sourceRequests = session.getTimingInfo().totals.requestCount;
|
||||
expect(project.program.getSourceFile(file)).toBe(source);
|
||||
expect(project.program.getSourceFile({ uri: pathToFileURL(join(dir, "main.ts")).href })).toBe(
|
||||
source,
|
||||
for (const server of ts7TestServers())
|
||||
test(`owned snapshots preserve SDK options, diagnostics, metadata and default-project lookup (${server.name})`, () => {
|
||||
const dir = mkdtempSync(join(tempRoot, "scriptc-session-parity-"));
|
||||
const file = protocolPath(join(dir, "main.ts"));
|
||||
const config = protocolPath(join(dir, "tsconfig.json"));
|
||||
writeFileSync(file, 'export const answer: number = "wrong";\n');
|
||||
writeFileSync(
|
||||
config,
|
||||
JSON.stringify({
|
||||
compilerOptions: {
|
||||
strict: true,
|
||||
target: "esnext",
|
||||
types: [],
|
||||
declaration: true,
|
||||
sourceMap: true,
|
||||
paths: { "named/*": ["./*", "../*"] },
|
||||
},
|
||||
files: [file],
|
||||
}),
|
||||
);
|
||||
expect(project.program.getSourceFile(missing)).toBeUndefined();
|
||||
expect(session.getTimingInfo().totals.requestCount).toBe(sourceRequests);
|
||||
expect(project.program.getSourceFileMetadata(missing)).toBeUndefined();
|
||||
const before = session.getTimingInfo().totals.requestCount;
|
||||
expect(project.program.getSourceFileMetadata(missing)).toBeUndefined();
|
||||
expect(session.getTimingInfo().totals.requestCount).toBe(before);
|
||||
for (const name of [
|
||||
"getSyntacticDiagnostics",
|
||||
"getBindDiagnostics",
|
||||
"getSemanticDiagnostics",
|
||||
"getSuggestionDiagnostics",
|
||||
"getDeclarationDiagnostics",
|
||||
] as const) {
|
||||
// scriptc's diagnostics have every member set (ts7Diagnostic); the SDK's leave absent ones out.
|
||||
expect(project.program[name](file), name).toEqual(
|
||||
expected.program[name](file).map(ts7Diagnostic),
|
||||
const session = connect(server, dir, fallback, true);
|
||||
const sdk = new API({ cwd: dir });
|
||||
try {
|
||||
expect(session.parseConfigFile(config)).toEqual(sdk.parseConfigFile(config));
|
||||
const snapshot = session.updateSnapshot({ openProjects: [config], openFiles: [file] });
|
||||
const oracle = sdk.updateSnapshot({ openProjects: [config], openFiles: [file] });
|
||||
expect(snapshot).toBeInstanceOf(Ts7SessionSnapshot);
|
||||
const project = snapshot.getProject(config)!;
|
||||
const expected = oracle.getProject(config)!;
|
||||
expect(project).toBeInstanceOf(Ts7SessionProject);
|
||||
expect(project.program.getCompilerOptions()).toEqual(expected.program.getCompilerOptions());
|
||||
expect(project.rootFiles).toEqual(expected.rootFiles);
|
||||
expect(snapshot.getDefaultProjectForFile(file)).toBe(project);
|
||||
expect(snapshot.getProject(protocolPath(join(dir, "./tsconfig.json")))).toBe(project);
|
||||
const names = project.program.getSourceFileNames();
|
||||
const expectedNames = expected.program.getSourceFileNames();
|
||||
if (server.ts71) {
|
||||
// tsc 7.1 reads its own lib files, which split some esnext libs into es2026 ones.
|
||||
const isLib = (name: string) => /\/lib\.[^/]*\.d\.ts$/.test(name);
|
||||
expect(names.filter((name) => !isLib(name))).toEqual(
|
||||
expectedNames.filter((name) => !isLib(name)),
|
||||
);
|
||||
for (const lib of ["lib.es5.d.ts", "lib.esnext.d.ts"])
|
||||
expect(
|
||||
names.some((name) => name.endsWith(`/${lib}`)),
|
||||
lib,
|
||||
).toBe(true);
|
||||
} else {
|
||||
expect(names).toEqual(expectedNames);
|
||||
}
|
||||
const source = project.program.getSourceFile(file)!;
|
||||
expect(source).toBeInstanceOf(AstNode);
|
||||
expect(source.text).toBe(expected.program.getSourceFile(file)!.text);
|
||||
const metadata = project.program.getSourceFileMetadata(file)!;
|
||||
expect(metadata).toEqual(expected.program.getSourceFileMetadata(file));
|
||||
expect(project.program.getSourceFileMetadata(file)).toBe(metadata);
|
||||
const missing = protocolPath(join(dir, "missing.ts"));
|
||||
expect(project.program.getSourceFile(missing)).toBeUndefined();
|
||||
const sourceRequests = session.getTimingInfo().totals.requestCount;
|
||||
expect(project.program.getSourceFile(file)).toBe(source);
|
||||
expect(project.program.getSourceFile({ uri: pathToFileURL(join(dir, "main.ts")).href })).toBe(
|
||||
source,
|
||||
);
|
||||
expect(project.program[name](), name).toEqual(expected.program[name]().map(ts7Diagnostic));
|
||||
}
|
||||
for (const name of [
|
||||
"getProgramDiagnostics",
|
||||
"getGlobalDiagnostics",
|
||||
"getConfigFileParsingDiagnostics",
|
||||
] as const)
|
||||
expect(project.program[name](), name).toEqual(expected.program[name]());
|
||||
const library = project.program
|
||||
.getSourceFileNames()
|
||||
.find((name) => name.endsWith("lib.esnext.d.ts"))!;
|
||||
expect(library).toBeDefined();
|
||||
expect(
|
||||
project.program.isSourceFileDefaultLibrary(project.program.getSourceFile(library)!),
|
||||
).toBe(true);
|
||||
expect(project.program.isSourceFileFromExternalLibrary(source)).toBe(false);
|
||||
snapshot.dispose();
|
||||
expect(snapshot.isDisposed()).toBe(true);
|
||||
expect(() => snapshot.getProjects()).toThrow("disposed");
|
||||
expect(() => project.program.getSourceFileNames()).toThrow("disposed");
|
||||
} finally {
|
||||
session.close();
|
||||
sdk.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("virtual updates share active ASTs and refetch after releasing their baseline", () => {
|
||||
const dir = mkdtempSync(join(tempRoot, "scriptc-session-updates-"));
|
||||
const a = protocolPath(join(dir, "a.json")),
|
||||
b = protocolPath(join(dir, "b.json"));
|
||||
const changed = protocolPath(join(dir, "changed.ts")),
|
||||
stable = protocolPath(join(dir, "stable.ts"));
|
||||
let content = "export const value = 1;";
|
||||
let option = "esnext";
|
||||
const overlay: Ts7FileSystem = {
|
||||
...fallback,
|
||||
readFile: (file) => {
|
||||
if (file === a || file === b)
|
||||
return JSON.stringify({
|
||||
compilerOptions: { strict: true, target: option, types: [] },
|
||||
files: [changed, stable],
|
||||
});
|
||||
if (file === changed) return content;
|
||||
if (file === stable) return "export const stable = true;";
|
||||
return undefined;
|
||||
},
|
||||
fileExists: (file) => ([a, b, changed, stable].includes(file) ? true : undefined),
|
||||
};
|
||||
const session = connect(dir, overlay, true);
|
||||
const sdk = new API({ cwd: dir, fs: overlay });
|
||||
try {
|
||||
const first = session.updateSnapshot({ openProjects: [a, b] });
|
||||
const oracleFirst = sdk.updateSnapshot({ openProjects: [a, b] });
|
||||
const oracleStable = oracleFirst.getProject(a)!.program.getSourceFile(stable)!;
|
||||
const old = first.getProject(a)!.program.getSourceFile(changed)!;
|
||||
const retained = first.getProject(a)!.program.getSourceFile(stable)!;
|
||||
expect(first.getProject(b)!.program.getSourceFile(changed)).toBe(old);
|
||||
content = 'export const value: number = "wrong";';
|
||||
const second = session.updateSnapshot({ fileChanges: { changed: [changed] } });
|
||||
const oracleSecond = sdk.updateSnapshot({ fileChanges: { changed: [changed] } });
|
||||
const next = second.getProject(a)!.program.getSourceFile(changed)!;
|
||||
expect(next).not.toBe(old);
|
||||
expect(old.text).toContain("= 1");
|
||||
expect(next.text).toBe(content);
|
||||
expect(
|
||||
second
|
||||
.getProject(a)!
|
||||
.program.getSemanticDiagnostics(changed)
|
||||
.map((d) => d.code),
|
||||
).toEqual([2322]);
|
||||
expect(first.getProject(a)!.program.getSemanticDiagnostics(changed)).toEqual([]);
|
||||
expect(second.getProject(a)!.program.getSourceFile(stable)).toBe(retained);
|
||||
first.dispose();
|
||||
second.dispose();
|
||||
oracleFirst.dispose();
|
||||
oracleSecond.dispose();
|
||||
const before = session.getTimingInfo().totals.sourceFilesFetched;
|
||||
const third = session.updateSnapshot();
|
||||
const oracleThird = sdk.updateSnapshot();
|
||||
const refreshed = third.getProject(a)!.program.getSourceFile(stable)!;
|
||||
expect(refreshed.text).toBe(retained.text);
|
||||
expect(session.getTimingInfo().totals.sourceFilesFetched).toBe(before + 1);
|
||||
option = "es2020";
|
||||
const fourth = session.updateSnapshot({ fileChanges: { changed: [a, b] } });
|
||||
const oracleFourth = sdk.updateSnapshot({ fileChanges: { changed: [a, b] } });
|
||||
const reparsed = fourth.getProject(a)!.program.getSourceFile(stable)!;
|
||||
expect(reparsed === refreshed).toBe(
|
||||
oracleFourth.getProject(a)!.program.getSourceFile(stable) === oracleStable,
|
||||
);
|
||||
expect(fourth.getProject(a)!.compilerOptions.target).toBe(
|
||||
oracleFourth.getProject(a)!.compilerOptions.target,
|
||||
);
|
||||
expect(fourth.getProject(a)!.compilerOptions.target).not.toBe(
|
||||
third.getProject(a)!.compilerOptions.target,
|
||||
);
|
||||
expect(oracleThird.getProject(a)!.program.getSourceFile(stable)).toBe(oracleStable);
|
||||
const closed = session.updateSnapshot({ closeProjects: [b] });
|
||||
expect(closed.getProject(b)).toBeUndefined();
|
||||
expect(closed.getProject(a)).toBeDefined();
|
||||
session.close();
|
||||
expect(third.isDisposed() && fourth.isDisposed() && closed.isDisposed()).toBe(true);
|
||||
expect(session.cache.size).toBe(0);
|
||||
expect(() => session.updateSnapshot()).toThrow("closed");
|
||||
expect(() => session.parseConfigFile(a)).toThrow("closed");
|
||||
expect(() => session.getTimingInfo()).toThrow("closed");
|
||||
} finally {
|
||||
session.close();
|
||||
sdk.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("transport failure during close seals every snapshot and clears retained ASTs", () => {
|
||||
const dir = mkdtempSync(join(tempRoot, "scriptc-session-close-"));
|
||||
const file = protocolPath(join(dir, "main.ts"));
|
||||
const config = protocolPath(join(dir, "tsconfig.json"));
|
||||
writeFileSync(file, "export const answer = 42;");
|
||||
writeFileSync(
|
||||
config,
|
||||
JSON.stringify({ compilerOptions: { strict: true, types: [] }, files: [file] }),
|
||||
);
|
||||
const rpc = transport(dir);
|
||||
const session = new Ts7Session(rpc);
|
||||
try {
|
||||
const first = session.updateSnapshot({ openProjects: [config] });
|
||||
const project = first.getProject(config)!;
|
||||
const source = project.program.getSourceFile(file)!;
|
||||
const type = project.checker.getTypeAtPosition(file, source.text!.indexOf("answer"))!;
|
||||
const second = session.updateSnapshot();
|
||||
const secondProject = second.getProject(config)!;
|
||||
expect(secondProject.program.getSourceFile(file)).toBe(source);
|
||||
const third = session.updateSnapshot();
|
||||
expect(session.cache.size).toBe(1);
|
||||
rpc.close();
|
||||
expect(() => session.close()).toThrow("closed");
|
||||
for (const snapshot of [first, second, third]) {
|
||||
expect(project.program.getSourceFile(missing)).toBeUndefined();
|
||||
expect(session.getTimingInfo().totals.requestCount).toBe(sourceRequests);
|
||||
expect(project.program.getSourceFileMetadata(missing)).toBeUndefined();
|
||||
const before = session.getTimingInfo().totals.requestCount;
|
||||
expect(project.program.getSourceFileMetadata(missing)).toBeUndefined();
|
||||
expect(session.getTimingInfo().totals.requestCount).toBe(before);
|
||||
for (const name of [
|
||||
"getSyntacticDiagnostics",
|
||||
"getBindDiagnostics",
|
||||
"getSemanticDiagnostics",
|
||||
"getSuggestionDiagnostics",
|
||||
"getDeclarationDiagnostics",
|
||||
] as const) {
|
||||
// scriptc's diagnostics have every member set (ts7Diagnostic); the SDK's leave absent ones out.
|
||||
expect(project.program[name](file), name).toEqual(
|
||||
expected.program[name](file).map(ts7Diagnostic),
|
||||
);
|
||||
// tsc 7.1's lib files differ from 7.0's, and so do their diagnostics.
|
||||
const own = <T extends { fileName?: string | undefined }>(list: T[]) =>
|
||||
server.ts71
|
||||
? list.filter((item) => !/\/lib\.[^/]*\.d\.ts$/.test(item.fileName ?? ""))
|
||||
: list;
|
||||
expect(own(project.program[name]()), name).toEqual(
|
||||
own(expected.program[name]().map(ts7Diagnostic)),
|
||||
);
|
||||
}
|
||||
for (const name of [
|
||||
"getProgramDiagnostics",
|
||||
"getGlobalDiagnostics",
|
||||
"getConfigFileParsingDiagnostics",
|
||||
] as const)
|
||||
expect(project.program[name](), name).toEqual(expected.program[name]());
|
||||
const library = project.program
|
||||
.getSourceFileNames()
|
||||
.find((name) => name.endsWith("lib.esnext.d.ts"))!;
|
||||
expect(library).toBeDefined();
|
||||
expect(
|
||||
project.program.isSourceFileDefaultLibrary(project.program.getSourceFile(library)!),
|
||||
).toBe(true);
|
||||
expect(project.program.isSourceFileFromExternalLibrary(source)).toBe(false);
|
||||
snapshot.dispose();
|
||||
expect(snapshot.isDisposed()).toBe(true);
|
||||
expect(() => snapshot.getProjects()).toThrow("closed");
|
||||
expect(() => snapshot.dispose()).not.toThrow();
|
||||
expect(() => snapshot.getProjects()).toThrow("disposed");
|
||||
expect(() => project.program.getSourceFileNames()).toThrow("disposed");
|
||||
} finally {
|
||||
session.close();
|
||||
sdk.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
expect(() => secondProject.checker.getNumberType()).toThrow("disposed");
|
||||
expect(() => secondProject.program.getSourceFile(file)).toThrow("closed");
|
||||
expect(() => type.getRegularType()).toThrow("disposed");
|
||||
expect(session.cache.size).toBe(0);
|
||||
expect(() => session.close()).not.toThrow();
|
||||
} finally {
|
||||
session.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("a released latest snapshot cannot lend stale ASTs to the same project", () => {
|
||||
const dir = mkdtempSync(join(tempRoot, "scriptc-session-released-"));
|
||||
const file = protocolPath(join(dir, "main.ts"));
|
||||
const config = protocolPath(join(dir, "tsconfig.json"));
|
||||
let source = "export const value = 1;";
|
||||
const overlay: Ts7FileSystem = {
|
||||
...fallback,
|
||||
readFile: (path) =>
|
||||
path === file
|
||||
? source
|
||||
: path === config
|
||||
? JSON.stringify({ compilerOptions: { noLib: true, types: [] }, files: [file] })
|
||||
: null,
|
||||
fileExists: (path) => path === file || path === config,
|
||||
};
|
||||
const session = connect(dir, overlay);
|
||||
try {
|
||||
const first = session.updateSnapshot({ openProjects: [config] });
|
||||
const old = first.getProject(config)!.program.getSourceFile(file)!;
|
||||
first.dispose();
|
||||
source = 'export const replacement = "new";';
|
||||
const second = session.updateSnapshot({ fileChanges: { changed: [file] } });
|
||||
const current = second.getProject(config)!.program.getSourceFile(file)!;
|
||||
expect(current.text).toBe(source);
|
||||
expect(current).not.toBe(old);
|
||||
expect(old.text).toBe("export const value = 1;");
|
||||
second.dispose();
|
||||
const deleted = session.updateSnapshot({ fileChanges: { deleted: [file] } });
|
||||
deleted.dispose();
|
||||
source = "export const third = true;";
|
||||
const third = session.updateSnapshot({ fileChanges: { created: [file] } });
|
||||
expect(third.getProject(config)!.program.getSourceFile(file)!.text).toBe(source);
|
||||
expect(getTextOfNode(old.statements.nodes[0]!)).toBe("export const value = 1;");
|
||||
} finally {
|
||||
session.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
for (const server of ts7TestServers())
|
||||
test(`virtual updates share active ASTs and refetch after releasing their baseline (${server.name})`, () => {
|
||||
const dir = mkdtempSync(join(tempRoot, "scriptc-session-updates-"));
|
||||
const a = protocolPath(join(dir, "a.json")),
|
||||
b = protocolPath(join(dir, "b.json"));
|
||||
const changed = protocolPath(join(dir, "changed.ts")),
|
||||
stable = protocolPath(join(dir, "stable.ts"));
|
||||
let content = "export const value = 1;";
|
||||
let option = "esnext";
|
||||
const overlay: Ts7FileSystem = {
|
||||
...fallback,
|
||||
readFile: (file) => {
|
||||
if (file === a || file === b)
|
||||
return JSON.stringify({
|
||||
compilerOptions: { strict: true, target: option, types: [] },
|
||||
files: [changed, stable],
|
||||
});
|
||||
if (file === changed) return content;
|
||||
if (file === stable) return "export const stable = true;";
|
||||
return undefined;
|
||||
},
|
||||
fileExists: (file) => ([a, b, changed, stable].includes(file) ? true : undefined),
|
||||
};
|
||||
const session = connect(server, dir, overlay, true);
|
||||
const sdk = new API({ cwd: dir, fs: overlay });
|
||||
try {
|
||||
const first = session.updateSnapshot({ openProjects: [a, b] });
|
||||
const oracleFirst = sdk.updateSnapshot({ openProjects: [a, b] });
|
||||
const oracleStable = oracleFirst.getProject(a)!.program.getSourceFile(stable)!;
|
||||
const old = first.getProject(a)!.program.getSourceFile(changed)!;
|
||||
const retained = first.getProject(a)!.program.getSourceFile(stable)!;
|
||||
expect(first.getProject(b)!.program.getSourceFile(changed)).toBe(old);
|
||||
content = 'export const value: number = "wrong";';
|
||||
const second = session.updateSnapshot({ fileChanges: { changed: [changed] } });
|
||||
const oracleSecond = sdk.updateSnapshot({ fileChanges: { changed: [changed] } });
|
||||
const next = second.getProject(a)!.program.getSourceFile(changed)!;
|
||||
expect(next).not.toBe(old);
|
||||
expect(old.text).toContain("= 1");
|
||||
expect(next.text).toBe(content);
|
||||
expect(
|
||||
second
|
||||
.getProject(a)!
|
||||
.program.getSemanticDiagnostics(changed)
|
||||
.map((d) => d.code),
|
||||
).toEqual([2322]);
|
||||
expect(first.getProject(a)!.program.getSemanticDiagnostics(changed)).toEqual([]);
|
||||
expect(second.getProject(a)!.program.getSourceFile(stable)).toBe(retained);
|
||||
first.dispose();
|
||||
second.dispose();
|
||||
oracleFirst.dispose();
|
||||
oracleSecond.dispose();
|
||||
const before = session.getTimingInfo().totals.sourceFilesFetched;
|
||||
const third = session.updateSnapshot();
|
||||
const oracleThird = sdk.updateSnapshot();
|
||||
const refreshed = third.getProject(a)!.program.getSourceFile(stable)!;
|
||||
expect(refreshed.text).toBe(retained.text);
|
||||
expect(session.getTimingInfo().totals.sourceFilesFetched).toBe(before + 1);
|
||||
option = "es2020";
|
||||
const fourth = session.updateSnapshot({ fileChanges: { changed: [a, b] } });
|
||||
const oracleFourth = sdk.updateSnapshot({ fileChanges: { changed: [a, b] } });
|
||||
const reparsed = fourth.getProject(a)!.program.getSourceFile(stable)!;
|
||||
expect(reparsed === refreshed).toBe(
|
||||
oracleFourth.getProject(a)!.program.getSourceFile(stable) === oracleStable,
|
||||
);
|
||||
expect(fourth.getProject(a)!.compilerOptions.target).toBe(
|
||||
oracleFourth.getProject(a)!.compilerOptions.target,
|
||||
);
|
||||
expect(fourth.getProject(a)!.compilerOptions.target).not.toBe(
|
||||
third.getProject(a)!.compilerOptions.target,
|
||||
);
|
||||
expect(oracleThird.getProject(a)!.program.getSourceFile(stable)).toBe(oracleStable);
|
||||
const closed = session.updateSnapshot({ closeProjects: [b] });
|
||||
expect(closed.getProject(b)).toBeUndefined();
|
||||
expect(closed.getProject(a)).toBeDefined();
|
||||
session.close();
|
||||
expect(third.isDisposed() && fourth.isDisposed() && closed.isDisposed()).toBe(true);
|
||||
expect(session.cache.size).toBe(0);
|
||||
expect(() => session.updateSnapshot()).toThrow("closed");
|
||||
expect(() => session.parseConfigFile(a)).toThrow("closed");
|
||||
expect(() => session.getTimingInfo()).toThrow("closed");
|
||||
} finally {
|
||||
session.close();
|
||||
sdk.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
for (const server of ts7TestServers())
|
||||
test(`transport failure during close seals every snapshot and clears retained ASTs (${server.name})`, () => {
|
||||
const dir = mkdtempSync(join(tempRoot, "scriptc-session-close-"));
|
||||
const file = protocolPath(join(dir, "main.ts"));
|
||||
const config = protocolPath(join(dir, "tsconfig.json"));
|
||||
writeFileSync(file, "export const answer = 42;");
|
||||
writeFileSync(
|
||||
config,
|
||||
JSON.stringify({ compilerOptions: { strict: true, types: [] }, files: [file] }),
|
||||
);
|
||||
const rpc = transport(server, dir);
|
||||
const session = new Ts7Session(rpc);
|
||||
try {
|
||||
const first = session.updateSnapshot({ openProjects: [config] });
|
||||
const project = first.getProject(config)!;
|
||||
const source = project.program.getSourceFile(file)!;
|
||||
const type = project.checker.getTypeAtPosition(file, source.text!.indexOf("answer"))!;
|
||||
const second = session.updateSnapshot();
|
||||
const secondProject = second.getProject(config)!;
|
||||
expect(secondProject.program.getSourceFile(file)).toBe(source);
|
||||
const third = session.updateSnapshot();
|
||||
expect(session.cache.size).toBe(1);
|
||||
rpc.close();
|
||||
expect(() => session.close()).toThrow("closed");
|
||||
for (const snapshot of [first, second, third]) {
|
||||
expect(snapshot.isDisposed()).toBe(true);
|
||||
expect(() => snapshot.getProjects()).toThrow("closed");
|
||||
expect(() => snapshot.dispose()).not.toThrow();
|
||||
}
|
||||
expect(() => secondProject.checker.getNumberType()).toThrow("disposed");
|
||||
expect(() => secondProject.program.getSourceFile(file)).toThrow("closed");
|
||||
expect(() => type.getRegularType()).toThrow("disposed");
|
||||
expect(session.cache.size).toBe(0);
|
||||
expect(() => session.close()).not.toThrow();
|
||||
} finally {
|
||||
session.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
for (const server of ts7TestServers())
|
||||
test(`a released latest snapshot cannot lend stale ASTs to the same project (${server.name})`, () => {
|
||||
const dir = mkdtempSync(join(tempRoot, "scriptc-session-released-"));
|
||||
const file = protocolPath(join(dir, "main.ts"));
|
||||
const config = protocolPath(join(dir, "tsconfig.json"));
|
||||
let source = "export const value = 1;";
|
||||
const overlay: Ts7FileSystem = {
|
||||
...fallback,
|
||||
readFile: (path) =>
|
||||
path === file
|
||||
? source
|
||||
: path === config
|
||||
? JSON.stringify({ compilerOptions: { noLib: true, types: [] }, files: [file] })
|
||||
: null,
|
||||
fileExists: (path) => path === file || path === config,
|
||||
};
|
||||
const session = connect(server, dir, overlay);
|
||||
try {
|
||||
const first = session.updateSnapshot({ openProjects: [config] });
|
||||
const old = first.getProject(config)!.program.getSourceFile(file)!;
|
||||
first.dispose();
|
||||
source = 'export const replacement = "new";';
|
||||
const second = session.updateSnapshot({ fileChanges: { changed: [file] } });
|
||||
const current = second.getProject(config)!.program.getSourceFile(file)!;
|
||||
expect(current.text).toBe(source);
|
||||
expect(current).not.toBe(old);
|
||||
expect(old.text).toBe("export const value = 1;");
|
||||
second.dispose();
|
||||
const deleted = session.updateSnapshot({ fileChanges: { deleted: [file] } });
|
||||
deleted.dispose();
|
||||
source = "export const third = true;";
|
||||
const third = session.updateSnapshot({ fileChanges: { created: [file] } });
|
||||
expect(third.getProject(config)!.program.getSourceFile(file)!.text).toBe(source);
|
||||
expect(getTextOfNode(old.statements.nodes[0]!)).toBe("export const value = 1;");
|
||||
} finally {
|
||||
session.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
Ts7CompilerOptionsData,
|
||||
Ts7ConfigData,
|
||||
Ts7DiagnosticData,
|
||||
Ts7InitializeData,
|
||||
Ts7ProjectChangeData,
|
||||
Ts7ProjectData,
|
||||
Ts7SnapshotData,
|
||||
Ts7SourceMetadata,
|
||||
@@ -36,6 +36,52 @@ interface Ts7ReportedSnapshotData extends Ts7SnapshotData {
|
||||
projects: Ts7ReportedProjectData[];
|
||||
}
|
||||
|
||||
/** initialize's answer. TypeScript 7.0 reports useCaseSensitiveFileNames;
|
||||
* 7.1 reports caseSensitivity (1 when case-sensitive) instead, which is how
|
||||
* a session tells the two protocols apart. */
|
||||
interface Ts7InitializeReply {
|
||||
currentDirectory: string;
|
||||
useCaseSensitiveFileNames?: boolean;
|
||||
caseSensitivity?: number;
|
||||
}
|
||||
|
||||
/** A project as TypeScript 7.1 reports it: the parsed command line carries
|
||||
* the root files and options, and a project need not have a config file. */
|
||||
interface Ts71ProjectData {
|
||||
id: string;
|
||||
configFileName?: string;
|
||||
parsedCommandLine: { fileNames: string[]; options: Ts7ProjectCompilerOptions };
|
||||
}
|
||||
|
||||
/** createSnapshot's and updateSnapshot's answer in TypeScript 7.1. An update
|
||||
* lists only the projects added or replaced since its base snapshot, and a
|
||||
* changed project's entry is null when its files are not listed. */
|
||||
interface Ts71SnapshotData {
|
||||
snapshot: number;
|
||||
projects: Ts71ProjectData[];
|
||||
changes?: {
|
||||
changedProjects?: Record<string, Ts7ProjectChangeData | null>;
|
||||
removedProjects?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/** TypeScript 7.1's compiler options in 7.0's values: 7.1 swaps JsxEmit's
|
||||
* React (2) and ReactNative (3). */
|
||||
function options71<T extends { jsx?: number | undefined }>(options: T): T {
|
||||
if (options.jsx === 2) options.jsx = 3;
|
||||
else if (options.jsx === 3) options.jsx = 2;
|
||||
return options;
|
||||
}
|
||||
|
||||
function project71(data: Ts71ProjectData): Ts7ReportedProjectData {
|
||||
return {
|
||||
id: data.id,
|
||||
configFileName: data.configFileName ?? data.id,
|
||||
compilerOptions: options71(data.parsedCommandLine.options),
|
||||
rootFiles: data.parsedCommandLine.fileNames,
|
||||
};
|
||||
}
|
||||
|
||||
export interface Ts7FileChanges {
|
||||
changed?: SemanticDocument[];
|
||||
created?: SemanticDocument[];
|
||||
@@ -55,7 +101,7 @@ interface ProgramQuery {
|
||||
snapshot?: number;
|
||||
project?: string;
|
||||
file?: SemanticDocument;
|
||||
files?: string[];
|
||||
files?: SemanticDocument[];
|
||||
}
|
||||
|
||||
/** The answer of getDiagnosticsOfCheckedFiles, as the server sends it. */
|
||||
@@ -73,6 +119,8 @@ export class Ts7Session {
|
||||
private readonly snapshots = new Set<Ts7SessionSnapshot>();
|
||||
private latest: Ts7SessionSnapshot | undefined;
|
||||
private paths: Ts7Paths | undefined;
|
||||
/** Whether the server speaks TypeScript 7.1's protocol (initialize). */
|
||||
private ts71 = false;
|
||||
private closed = false;
|
||||
|
||||
constructor(
|
||||
@@ -116,22 +164,41 @@ export class Ts7Session {
|
||||
initialize(): Ts7Paths {
|
||||
this.ensureOpen();
|
||||
if (this.paths === undefined) {
|
||||
const data = parseSemanticJson<Ts7InitializeData>(this.text("initialize", "null"));
|
||||
this.paths = new Ts7Paths(data.currentDirectory, data.useCaseSensitiveFileNames);
|
||||
const data = parseSemanticJson<Ts7InitializeReply>(this.text("initialize", "null"));
|
||||
this.ts71 = data.caseSensitivity !== undefined;
|
||||
this.paths = new Ts7Paths(
|
||||
data.currentDirectory,
|
||||
this.ts71 ? data.caseSensitivity === 1 : data.useCaseSensitiveFileNames === true,
|
||||
);
|
||||
}
|
||||
return this.paths;
|
||||
}
|
||||
|
||||
parseConfigFile(file: SemanticDocument): Ts7ConfigData {
|
||||
this.initialize();
|
||||
return this.request<Ts7ConfigData>("parseConfigFile", { file });
|
||||
const data = this.request<Ts7ConfigData>("parseConfigFile", { file });
|
||||
// TypeScript 7.1 also reports the raw config, errors and compileOnSave.
|
||||
return this.ts71 ? { options: options71(data.options), fileNames: data.fileNames } : data;
|
||||
}
|
||||
|
||||
/** Whether initialize found a TypeScript 7.1 server; false before it. */
|
||||
speaksTs71(): boolean {
|
||||
return this.ts71;
|
||||
}
|
||||
|
||||
/** Whether the server speaks TypeScript 7.1's protocol. */
|
||||
usesTs71Protocol(): boolean {
|
||||
this.initialize();
|
||||
return this.ts71;
|
||||
}
|
||||
|
||||
updateSnapshot(params: Ts7Update = {}): Ts7SessionSnapshot {
|
||||
const paths = this.initialize();
|
||||
const data = parseSemanticJson<Ts7ReportedSnapshotData>(
|
||||
this.text("updateSnapshot", JSON.stringify(params)),
|
||||
);
|
||||
const data = this.ts71
|
||||
? this.snapshot71(params)
|
||||
: parseSemanticJson<Ts7ReportedSnapshotData>(
|
||||
this.text("updateSnapshot", JSON.stringify(params)),
|
||||
);
|
||||
const snapshot = new Ts7SessionSnapshot(data, this, paths);
|
||||
const previous = this.latest;
|
||||
if (previous !== undefined) {
|
||||
@@ -147,9 +214,56 @@ export class Ts7Session {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/** TypeScript 7.1 derives a snapshot from a base snapshot the client
|
||||
* names (updateSnapshot), or creates the first one (createSnapshot). The
|
||||
* base is the latest snapshot: the server keeps it until the next update,
|
||||
* even after the client disposes it (release), so open projects and files
|
||||
* carry over as they do on a 7.0 server. An update lists only the projects
|
||||
* that changed; the others carry over from the base. */
|
||||
private snapshot71(params: Ts7Update): Ts7ReportedSnapshotData {
|
||||
const changes: Record<string, unknown> = {};
|
||||
if (params.openProjects !== undefined) changes["openProjects"] = params.openProjects;
|
||||
if (params.closeProjects !== undefined) changes["closeProjects"] = params.closeProjects;
|
||||
if (params.openFiles !== undefined) changes["openFiles"] = params.openFiles;
|
||||
if (params.closeFiles !== undefined) changes["closeFiles"] = params.closeFiles;
|
||||
if (params.fileChanges !== undefined) changes["fileNotifications"] = params.fileChanges;
|
||||
// A 7.1 snapshot leaves a project whose files changed dirty until asked
|
||||
// to update its program; a 7.0 snapshot's programs are always current.
|
||||
changes["ensurePrograms"] = true;
|
||||
const base = this.latest;
|
||||
const data = parseSemanticJson<Ts71SnapshotData>(
|
||||
base === undefined
|
||||
? this.text("createSnapshot", JSON.stringify(changes))
|
||||
: this.text("updateSnapshot", JSON.stringify({ snapshot: base.id, changes })),
|
||||
);
|
||||
if (base !== undefined && base.isDisposed())
|
||||
this.binary("release", JSON.stringify({ snapshot: base.id }));
|
||||
const projects = new Map<string, Ts7ReportedProjectData>();
|
||||
const removed = new Set(data.changes?.removedProjects ?? []);
|
||||
if (base !== undefined)
|
||||
for (const project of base.projectData)
|
||||
if (!removed.has(project.id)) projects.set(project.id, project);
|
||||
for (const project of data.projects) projects.set(project.id, project71(project));
|
||||
// A changed project without a file list keeps none of its cached files.
|
||||
const changedProjects: Record<string, Ts7ProjectChangeData> = {};
|
||||
const removedProjects = [...removed];
|
||||
for (const [id, change] of Object.entries(data.changes?.changedProjects ?? {})) {
|
||||
if (change === null) removedProjects.push(id);
|
||||
else changedProjects[id] = change;
|
||||
}
|
||||
return {
|
||||
snapshot: data.snapshot,
|
||||
projects: [...projects.values()],
|
||||
changes: { changedProjects, removedProjects },
|
||||
};
|
||||
}
|
||||
|
||||
release(snapshot: Ts7SessionSnapshot): void {
|
||||
this.snapshots.delete(snapshot);
|
||||
if (snapshot !== this.latest) this.cache.release(snapshot.id);
|
||||
// A 7.1 server keeps the latest snapshot as the next update's base
|
||||
// (snapshot71), which releases it after deriving from it.
|
||||
if (this.ts71 && snapshot === this.latest) return;
|
||||
this.binary("release", JSON.stringify({ snapshot: snapshot.id }));
|
||||
}
|
||||
|
||||
@@ -180,6 +294,8 @@ export class Ts7Session {
|
||||
|
||||
export class Ts7SessionSnapshot {
|
||||
readonly id: number;
|
||||
/** The projects as the server reported them (TypeScript 7.0's shape). */
|
||||
readonly projectData: readonly Ts7ReportedProjectData[];
|
||||
readonly semantic: SemanticSnapshot;
|
||||
private readonly projects = new Map<string, Ts7SessionProject>();
|
||||
private disposed = false;
|
||||
@@ -190,10 +306,15 @@ export class Ts7SessionSnapshot {
|
||||
readonly paths: Ts7Paths,
|
||||
) {
|
||||
this.id = data.snapshot;
|
||||
this.semantic = new SemanticSnapshot(this.id, {
|
||||
text: (method, payload) => this.session.text(method, payload),
|
||||
binary: (method, payload) => this.session.binary(method, payload),
|
||||
});
|
||||
this.projectData = data.projects;
|
||||
this.semantic = new SemanticSnapshot(
|
||||
this.id,
|
||||
{
|
||||
text: (method, payload) => this.session.text(method, payload),
|
||||
binary: (method, payload) => this.session.binary(method, payload),
|
||||
},
|
||||
session.speaksTs71(),
|
||||
);
|
||||
for (const item of data.projects) {
|
||||
const project = new Ts7SessionProject(item, this, session);
|
||||
this.projects.set(paths.canonical(item.configFileName), project);
|
||||
@@ -214,10 +335,15 @@ export class Ts7SessionSnapshot {
|
||||
}
|
||||
getDefaultProjectForFile(file: SemanticDocument): Ts7SessionProject | undefined {
|
||||
this.ensureActive();
|
||||
const data = this.session.request<Ts7ReportedProjectData | null>("getDefaultProjectForFile", {
|
||||
snapshot: this.id,
|
||||
file,
|
||||
});
|
||||
const query = { snapshot: this.id, file };
|
||||
if (this.session.usesTs71Protocol()) {
|
||||
const data = this.session.request<Ts71ProjectData | null>("getDefaultProjectForFile", query);
|
||||
return data === null ? undefined : this.getProject(project71(data).configFileName);
|
||||
}
|
||||
const data = this.session.request<Ts7ReportedProjectData | null>(
|
||||
"getDefaultProjectForFile",
|
||||
query,
|
||||
);
|
||||
return data === null ? undefined : this.getProject(data.configFileName);
|
||||
}
|
||||
isDisposed(): boolean {
|
||||
@@ -344,17 +470,29 @@ export class Ts7SessionProgram {
|
||||
isSourceFileDefaultLibrary(file: AstNode): boolean {
|
||||
return this.getSourceFileMetadataByPath(file.path)?.isDefaultLibrary ?? false;
|
||||
}
|
||||
/** A diagnostics request for `file`, or for the whole program. TypeScript
|
||||
* 7.1 takes the file in a list (`files`). */
|
||||
private diagnosticsQuery(file?: SemanticDocument): ProgramQuery {
|
||||
if (file === undefined || !this.session.usesTs71Protocol()) return this.query(file);
|
||||
const query = this.query();
|
||||
query.files = [file];
|
||||
return query;
|
||||
}
|
||||
private diagnostics(method: string, file?: SemanticDocument): Ts7Diagnostic[] {
|
||||
const result: Ts7Diagnostic[] = [];
|
||||
for (const data of this.session.request<Ts7DiagnosticData[] | null>(method, this.query(file)) ??
|
||||
[])
|
||||
for (const data of this.session.request<Ts7DiagnosticData[] | null>(
|
||||
method,
|
||||
this.diagnosticsQuery(file),
|
||||
) ?? [])
|
||||
result.push(ts7Diagnostic(data));
|
||||
return result;
|
||||
}
|
||||
/** The diagnostics of `method` for `file`, or for the whole program, as
|
||||
* the server sends them. */
|
||||
diagnosticData(method: string, file?: SemanticDocument): Ts7DiagnosticData[] {
|
||||
return this.session.request<Ts7DiagnosticData[] | null>(method, this.query(file)) ?? [];
|
||||
return (
|
||||
this.session.request<Ts7DiagnosticData[] | null>(method, this.diagnosticsQuery(file)) ?? []
|
||||
);
|
||||
}
|
||||
/** The semantic diagnostics of each of `files` and the global diagnostics,
|
||||
* after the server checks only those files (getDiagnosticsOfCheckedFiles),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
import { tsgoExecutable } from "./rpc-api.js";
|
||||
|
||||
/** An API server the TypeScript client tests run against. */
|
||||
export interface Ts7TestServer {
|
||||
/** The label tests append to their names. */
|
||||
name: string;
|
||||
executable: string;
|
||||
/** Whether the server speaks TypeScript 7.1's protocol. */
|
||||
ts71: boolean;
|
||||
}
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
/** tsc 7.0.2 from the pinned `typescript` package and tsc 7.1.0-beta from
|
||||
* the `typescript-7.1` test dependency, so the client's protocol 5 and
|
||||
* protocol 9 paths answer the same cases. For tests only. */
|
||||
export function ts7TestServers(): Ts7TestServer[] {
|
||||
const root = dirname(require.resolve("typescript-7.1/package.json"));
|
||||
const { default: getExePath } = require(join(root, "lib/getExePath.js")) as {
|
||||
default: () => string;
|
||||
};
|
||||
return [
|
||||
{ name: "tsc 7.0", executable: tsgoExecutable(), ts71: false },
|
||||
{ name: "tsc 7.1", executable: getExePath(), ts71: true },
|
||||
];
|
||||
}
|
||||
@@ -1287,6 +1287,14 @@ 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;
|
||||
/** Localized failure's lenient mapping (Lowerer.lenientType): a nested
|
||||
* component whose mapping fails maps to the checked dynamic instead, so a
|
||||
* failing type keeps its outer shape with dynamic pieces. Only ever set
|
||||
* on a throwaway context without memoization. */
|
||||
lenient?: boolean;
|
||||
/** With `lenient`: told each component the lenient mapping turned into
|
||||
* the checked dynamic (Lowerer.lenientPieces). */
|
||||
onLenientPiece?: (type: ts.Type) => void;
|
||||
/** 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. */
|
||||
@@ -1916,6 +1924,13 @@ function mapTypeEntry(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
contextResolutions++;
|
||||
}
|
||||
}
|
||||
// Push-down (stage L3): in a lenient mapping, the smallest failing
|
||||
// component is the checked dynamic.
|
||||
if (mapped === null && ctx.lenient === true && !topLevel) {
|
||||
mapped = DYN;
|
||||
contextResolutions++;
|
||||
ctx.onLenientPiece?.(type);
|
||||
}
|
||||
if (
|
||||
mapped !== null &&
|
||||
memoKey !== null &&
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/localized-failure-degraded-initializer.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/localized-failure-expressions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/localized-failure-push-down.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/width-layout-identity.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/width-layout-parameter-writes.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -82,6 +82,24 @@ void scr_box_set_f64(ScrBox *b, double v);
|
||||
void scr_box_set_bool(ScrBox *b, bool v);
|
||||
void scr_box_set_ref(ScrBox *b, void *v); /* releases old, owns new */
|
||||
|
||||
/* The bits of ScrClosure.function_kind. The LLVM backend stores them too:
|
||||
* CLOSURE_KIND in packages/compiler/src/backend/llvm/common.ts mirrors this
|
||||
* list, and runtime-registries.test.ts checks that the two agree and that
|
||||
* the bits are distinct. The low two bits (SCR_CLOSURE_KIND_CALL_MASK) are
|
||||
* the function's call kind: 0 ordinary, 1 generator, 2 async, 3 async
|
||||
* generator. */
|
||||
#define SCR_CLOSURE_KIND_GENERATOR 1u
|
||||
#define SCR_CLOSURE_KIND_ASYNC 2u
|
||||
#define SCR_CLOSURE_KIND_CALL_MASK 3u
|
||||
#define SCR_CLOSURE_KIND_OWN_PROTOTYPE 4u /* an ordinary function with its own prototype */
|
||||
#define SCR_CLOSURE_KIND_CHECKED_ADAPTER 8u /* a checked callable adapter: the callable is in caps[0] */
|
||||
/* The body never observes the ambient receiver (the `this` an unbound call
|
||||
* binds), so compiled callers skip the receiver bracket around an indirect
|
||||
* call. Set only by the compiler; the kind proper is
|
||||
* function_kind & SCR_CLOSURE_KIND_MASK. */
|
||||
#define SCR_CLOSURE_KIND_RECEIVER_FREE 16u
|
||||
#define SCR_CLOSURE_KIND_MASK 15u
|
||||
|
||||
typedef struct ScrClosure {
|
||||
size_t rc; /* SIZE_MAX = immortal (interned top-level function value) */
|
||||
void *fn;
|
||||
@@ -98,7 +116,7 @@ typedef struct ScrClosure {
|
||||
* (like the dyn→closure edge): a cycle through it is merely never
|
||||
* collected. */
|
||||
ScrBox *props;
|
||||
uint32_t function_kind; /* low bits: 0 ordinary, 1 generator, 2 async, 3 async generator; bit 2: ordinary own prototype; bit 3: checked callable adapter in caps[0]; bit 4: SCR_FN_RECEIVER_FREE */
|
||||
uint32_t function_kind; /* SCR_CLOSURE_KIND_* bits */
|
||||
/* The root's `length` as boxed values report it; meaningful only when
|
||||
* identity is set (the root's own boxes carry their arity). */
|
||||
uint32_t identity_length;
|
||||
@@ -117,13 +135,6 @@ typedef struct ScrClosure {
|
||||
ScrBox *caps[];
|
||||
} ScrClosure;
|
||||
|
||||
/* function_kind bit 4: the body never observes the ambient receiver (the
|
||||
* `this` an unbound call binds), so compiled callers skip the receiver
|
||||
* bracket around an indirect call. Set only by the compiler; the kind
|
||||
* proper is function_kind & SCR_FN_KIND_MASK. */
|
||||
#define SCR_FN_RECEIVER_FREE 16u
|
||||
#define SCR_FN_KIND_MASK 15u
|
||||
|
||||
ScrClosure *scr_closure_new(void *fn, size_t ncaps); /* +1; caller fills caps with +1 box refs */
|
||||
|
||||
static inline ScrClosure *scr_closure_retain(ScrClosure *c) {
|
||||
|
||||
@@ -129,17 +129,30 @@ typedef enum {
|
||||
* lowerings (the http/net receiver surface); new kinds join by adding a
|
||||
* tag + an ops registration in their owning unit.
|
||||
*
|
||||
* SCR_DYNH_TAGS is the one list of tags: X(NAME, value) declares
|
||||
* SCR_DYNH_NAME. Rows are sorted by name and every value is explicit and
|
||||
* permanent: the LLVM backend hardcodes some of them (DYN_HANDLE_TAG_NUM in
|
||||
* packages/compiler/src/backend/llvm/dyn.ts). A new tag goes in its sorted
|
||||
* place with the next value after the largest one in use, below
|
||||
* SCR_DYNH_CAPACITY. Two changes that add tags therefore edit different
|
||||
* lines and merge without a textual conflict; when they picked the same
|
||||
* value, scr_dynh_tags_distinct below has a duplicate case label and every
|
||||
* runtime unit fails to compile, and a value at or above the capacity
|
||||
* fails its _Static_assert. packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts
|
||||
* checks the same properties and that the backend's numbers match. */
|
||||
* Two lists declare the tags. SCR_DYNH_TAGS holds the PINNED tags: X(NAME,
|
||||
* value) declares SCR_DYNH_NAME with an explicit, permanent value, rows
|
||||
* sorted by name. SCR_DYNH_NEW_TAGS holds tags added since the last pin, by
|
||||
* NAME ONLY: Y(NAME), rows sorted by name. A new tag goes in its sorted place
|
||||
* in SCR_DYNH_NEW_TAGS, so a branch never picks a number and two branches
|
||||
* that add tags never collide; the enum numbers the new tags after the
|
||||
* largest pinned value, in list order. Values are an ABI only between the
|
||||
* compiler and the runtime pack of one build, so a new tag added before
|
||||
* another one in name order may renumber it; nothing outside the build
|
||||
* depends on it.
|
||||
*
|
||||
* Pin a new tag (move it to SCR_DYNH_TAGS with the value it has now: the
|
||||
* largest pinned value + 1 for the first row of SCR_DYNH_NEW_TAGS, + 2 for
|
||||
* the second, and so on) only when the LLVM backend needs its number: DYN_HANDLE_TAG_NUM and
|
||||
* TEXT_CODEC_TAG_NUM in packages/compiler/src/backend/llvm/dyn.ts may only
|
||||
* name pinned tags. Pin the new tags in name order, oldest first, in a change
|
||||
* of its own, so their values do not move.
|
||||
*
|
||||
* Checks: a pinned value at or above SCR_DYNH_CAPACITY fails its
|
||||
* _Static_assert, two tags with one value are a duplicate case label in
|
||||
* scr_dynh_tags_distinct below, and the header does not compile.
|
||||
* dyn-handle-tags.test.ts checks the same properties by name, that both lists
|
||||
* are sorted, that every value below the largest pinned one is a tag or
|
||||
* listed in SCR_DYNH_UNASSIGNED, and that the backend's numbers match. */
|
||||
#define SCR_DYNH_TAGS(X) \
|
||||
X(ABORT_CONTROLLER, 15) /* native static-fetch AbortController */ \
|
||||
X(ABORT_SIGNAL, 8) /* native static-fetch AbortSignal */ \
|
||||
@@ -225,24 +238,55 @@ typedef enum {
|
||||
X(WEB_WRITER, 75) /* WritableStreamDefaultWriter (scr_fetch.c) */ \
|
||||
X(WORKER, 35)
|
||||
|
||||
/* Tags added since the last pin, by name only (see above). */
|
||||
#define SCR_DYNH_NEW_TAGS(Y)
|
||||
|
||||
/* Values below the largest pinned one that no tag holds, each with the
|
||||
* reason. Never reuse one without removing it here. */
|
||||
#define SCR_DYNH_UNASSIGNED(Z) \
|
||||
Z(63) /* skipped: claimed by a branch that took another number */
|
||||
|
||||
/* One member per pinned tag, sized value + 1, so its size is the largest
|
||||
* pinned value + 1: the new tags are numbered from there. Never instantiated. */
|
||||
union ScrDynhPinnedSpan {
|
||||
#define SCR_DYNH_SPAN_ROW(name, value) char name[(value) + 1];
|
||||
SCR_DYNH_TAGS(SCR_DYNH_SPAN_ROW)
|
||||
#undef SCR_DYNH_SPAN_ROW
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
#define SCR_DYNH_ENUM_ROW(name, value) SCR_DYNH_##name = value,
|
||||
SCR_DYNH_TAGS(SCR_DYNH_ENUM_ROW)
|
||||
#undef SCR_DYNH_ENUM_ROW
|
||||
SCR_DYNH__LAST_PINNED = (int)sizeof(union ScrDynhPinnedSpan) - 1,
|
||||
#define SCR_DYNH_NEW_ROW(name) SCR_DYNH_##name,
|
||||
SCR_DYNH_NEW_TAGS(SCR_DYNH_NEW_ROW)
|
||||
#undef SCR_DYNH_NEW_ROW
|
||||
} ScrDynHandleTag;
|
||||
|
||||
#define SCR_DYNH_RANGE_ROW(name, value) \
|
||||
_Static_assert((value) < SCR_DYNH_CAPACITY, "SCR_DYNH_" #name " is not below SCR_DYNH_CAPACITY");
|
||||
SCR_DYNH_TAGS(SCR_DYNH_RANGE_ROW)
|
||||
#undef SCR_DYNH_RANGE_ROW
|
||||
#define SCR_DYNH_NEW_RANGE_ROW(name) \
|
||||
_Static_assert(SCR_DYNH_##name < SCR_DYNH_CAPACITY, "SCR_DYNH_" #name " is not below SCR_DYNH_CAPACITY");
|
||||
SCR_DYNH_NEW_TAGS(SCR_DYNH_NEW_RANGE_ROW)
|
||||
#undef SCR_DYNH_NEW_RANGE_ROW
|
||||
|
||||
/* Never called: a case label per tag, so two tags with one value are a
|
||||
* duplicate case value and the header does not compile. */
|
||||
/* Never called: a case label per tag and per unassigned value, so two tags
|
||||
* with one value, or a tag on an unassigned value, are a duplicate case
|
||||
* value and the header does not compile. */
|
||||
static inline void scr_dynh_tags_distinct(ScrDynHandleTag tag) {
|
||||
switch (tag) {
|
||||
switch ((int)tag) {
|
||||
#define SCR_DYNH_CASE_ROW(name, value) case SCR_DYNH_##name:
|
||||
SCR_DYNH_TAGS(SCR_DYNH_CASE_ROW)
|
||||
#undef SCR_DYNH_CASE_ROW
|
||||
#define SCR_DYNH_NEW_CASE_ROW(name) case SCR_DYNH_##name:
|
||||
SCR_DYNH_NEW_TAGS(SCR_DYNH_NEW_CASE_ROW)
|
||||
#undef SCR_DYNH_NEW_CASE_ROW
|
||||
#define SCR_DYNH_UNASSIGNED_CASE_ROW(value) case value:
|
||||
SCR_DYNH_UNASSIGNED(SCR_DYNH_UNASSIGNED_CASE_ROW)
|
||||
#undef SCR_DYNH_UNASSIGNED_CASE_ROW
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1441,7 +1441,7 @@ static void bc_teardown(void) {
|
||||
|
||||
static ScrDyn *bc_new_class(int kind, ScrDyn *parent) {
|
||||
ScrClosure *closure = scr_closure_new(NULL, 1);
|
||||
closure->function_kind = 4; /* an ordinary function with its own prototype */
|
||||
closure->function_kind = SCR_CLOSURE_KIND_OWN_PROTOTYPE;
|
||||
closure->caps[0] = scr_box_new(SCR_BOX_F64);
|
||||
scr_box_set_f64(closure->caps[0], kind);
|
||||
ScrDyn *ctor = scr_dyn_new_func(closure, bc_construct, 1, "native:events", bc_names[kind]);
|
||||
|
||||
@@ -4572,8 +4572,8 @@ bool scr_dyn_util_type_is(const ScrDyn *value, const ScrStr *probe) {
|
||||
if (PROBE("isRegExp")) return value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_REGEXP;
|
||||
if (PROBE("isPromise")) return value->kind == SCR_DYN_PROMISE;
|
||||
if (PROBE("isProxy")) return value->kind == SCR_DYN_PROXY;
|
||||
if (PROBE("isAsyncFunction")) return value->kind == SCR_DYN_FUNC && (scr_closure_identity(value->v.fn.clo)->function_kind & 2) != 0;
|
||||
if (PROBE("isGeneratorFunction")) return value->kind == SCR_DYN_FUNC && (scr_closure_identity(value->v.fn.clo)->function_kind & 1) != 0;
|
||||
if (PROBE("isAsyncFunction")) return value->kind == SCR_DYN_FUNC && (scr_closure_identity(value->v.fn.clo)->function_kind & SCR_CLOSURE_KIND_ASYNC) != 0;
|
||||
if (PROBE("isGeneratorFunction")) return value->kind == SCR_DYN_FUNC && (scr_closure_identity(value->v.fn.clo)->function_kind & SCR_CLOSURE_KIND_GENERATOR) != 0;
|
||||
if (PROBE("isGeneratorObject")) return scr_dyn_generator(value);
|
||||
if (PROBE("isDate")) return value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_DATE;
|
||||
if (PROBE("isMapIterator")) return scr_native_iterator_over(value, 2);
|
||||
@@ -5850,7 +5850,7 @@ void scr_dyn_fence_member(ScrDyn *object, const ScrStr *key) {
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, const char *sig, const char *name) {
|
||||
if (clo->function_kind & 8) {
|
||||
if (clo->function_kind & SCR_CLOSURE_KIND_CHECKED_ADAPTER) {
|
||||
ScrDyn *original = scr_box_get_ref(clo->caps[0]);
|
||||
scr_closure_release(clo);
|
||||
return original;
|
||||
@@ -6009,7 +6009,7 @@ ScrDyn *scr_dyn_bind(ScrDyn *target, ScrDyn *const *args, size_t argc) {
|
||||
scr_str_release(prefix);
|
||||
scr_str_release(suffix);
|
||||
ScrClosure *closure = scr_closure_new(NULL, 4);
|
||||
closure->function_kind = scr_closure_identity(target->v.fn.clo)->function_kind & 3;
|
||||
closure->function_kind = scr_closure_identity(target->v.fn.clo)->function_kind & SCR_CLOSURE_KIND_CALL_MASK;
|
||||
for (size_t i = 0; i < 3; i++) closure->caps[i] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
|
||||
closure->caps[3] = scr_box_new(SCR_BOX_STR);
|
||||
scr_box_set_ref(closure->caps[0], scr_dyn_retain(target));
|
||||
@@ -6829,7 +6829,7 @@ static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function) {
|
||||
scr_box_set_ref(box, table);
|
||||
owner->props = box;
|
||||
if (function->v.fn.class_obj) function->v.fn.class_obj->static_data = scr_box_retain(box);
|
||||
if (owner->function_kind & 4) {
|
||||
if (owner->function_kind & SCR_CLOSURE_KIND_OWN_PROTOTYPE) {
|
||||
ScrDyn *prototype = scr_dyn_new_obj();
|
||||
scr_dyn_obj_set(prototype, "constructor", 11, scr_dyn_retain((ScrDyn *)function));
|
||||
prototype->v.obj.entries[0].enumerable = false;
|
||||
@@ -6908,7 +6908,7 @@ ScrDyn *scr_dyn_class_base_prototype(ScrDyn *constructor) {
|
||||
default: break;
|
||||
}
|
||||
if (constructor->kind != SCR_DYN_FUNC || constructor->v.fn.class_obj ||
|
||||
(scr_closure_identity(constructor->v.fn.clo)->function_kind & SCR_FN_KIND_MASK) != 4) {
|
||||
(scr_closure_identity(constructor->v.fn.clo)->function_kind & SCR_CLOSURE_KIND_MASK) != SCR_CLOSURE_KIND_OWN_PROTOTYPE) {
|
||||
static const char message[] = "Class extends value is not a native ordinary constructor";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
return NULL;
|
||||
@@ -7057,7 +7057,7 @@ void scr_dyn_name_class_prototype(ScrDyn *prototype, const ScrStr *name) {
|
||||
* A constructor may replace the fresh receiver with another object. */
|
||||
ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) {
|
||||
if (callee->kind != SCR_DYN_FUNC || callee->v.fn.class_obj ||
|
||||
(scr_closure_identity(callee->v.fn.clo)->function_kind & SCR_FN_KIND_MASK) != 4)
|
||||
(scr_closure_identity(callee->v.fn.clo)->function_kind & SCR_CLOSURE_KIND_MASK) != SCR_CLOSURE_KIND_OWN_PROTOTYPE)
|
||||
return scr_bytes_construct(callee, args, what);
|
||||
ScrDyn *prototype = scr_dyn_fn_get(callee, "prototype", 9);
|
||||
if (!prototype) return NULL;
|
||||
@@ -9193,7 +9193,7 @@ bool scr_dyn_is_callable(const ScrDyn *value) {
|
||||
int scr_dyn_function_kind(const ScrDyn *value) {
|
||||
while (value->kind == SCR_DYN_PROXY) value = value->v.proxy.target;
|
||||
return value->kind == SCR_DYN_FUNC
|
||||
? (int)(scr_closure_identity(value->v.fn.clo)->function_kind & SCR_FN_KIND_MASK)
|
||||
? (int)(scr_closure_identity(value->v.fn.clo)->function_kind & SCR_CLOSURE_KIND_MASK)
|
||||
: 0;
|
||||
}
|
||||
|
||||
@@ -14602,7 +14602,7 @@ static ScrDyn *scr_function_constructor_call(ScrClosure *closure, ScrDyn *const
|
||||
}
|
||||
|
||||
static ScrDyn *scr_function_constructor(uint32_t kind) {
|
||||
kind &= 3;
|
||||
kind &= SCR_CLOSURE_KIND_CALL_MASK;
|
||||
static const char *const names[] = {"Function", "GeneratorFunction", "AsyncFunction", "AsyncGeneratorFunction"};
|
||||
if (!scr_function_constructors_registered) {
|
||||
scr_atexit(scr_function_constructors_cleanup);
|
||||
@@ -14632,7 +14632,7 @@ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) {
|
||||
return NULL;
|
||||
}
|
||||
if (!d->v.fn.class_obj && key_len == 9 && !memcmp(key, "prototype", 9) && !owner->props) {
|
||||
if (owner->function_kind & 1) {
|
||||
if (owner->function_kind & SCR_CLOSURE_KIND_GENERATOR) {
|
||||
static const char message[] = "Native generator function prototypes have no lowering";
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
|
||||
return NULL;
|
||||
|
||||
@@ -311,7 +311,7 @@ ScrDyn *scr_util_deprecate(ScrDyn *fn, ScrDyn *msg, ScrDyn *code, ScrDyn *option
|
||||
return NULL;
|
||||
}
|
||||
ScrClosure *closure = scr_closure_new(NULL, UC_DEP_CAPS);
|
||||
closure->function_kind = 4; /* an ordinary function with its own prototype */
|
||||
closure->function_kind = SCR_CLOSURE_KIND_OWN_PROTOTYPE;
|
||||
closure->caps[UC_DEP_FN] = uc_box(scr_dyn_retain(fn));
|
||||
closure->caps[UC_DEP_MSG] = uc_box(scr_dyn_retain(msg));
|
||||
closure->caps[UC_DEP_CODE] = uc_box(scr_dyn_retain(code));
|
||||
@@ -557,7 +557,7 @@ ScrDyn *scr_util_callbackify(ScrDyn *original) {
|
||||
return NULL;
|
||||
}
|
||||
ScrClosure *closure = scr_closure_new(NULL, 1);
|
||||
closure->function_kind = 4; /* an ordinary function with its own prototype */
|
||||
closure->function_kind = SCR_CLOSURE_KIND_OWN_PROTOTYPE;
|
||||
closure->caps[0] = uc_box(scr_dyn_retain(original));
|
||||
ScrDyn *callbackified =
|
||||
scr_dyn_new_func(closure, uc_callbackified_call, 0, "native:util.callbackify", "callbackified");
|
||||
@@ -708,7 +708,7 @@ static ScrDyn *uc_debug_impl(bool enabled, ScrDyn *set) {
|
||||
scr_str_release(text);
|
||||
}
|
||||
ScrClosure *closure = scr_closure_new(NULL, 1);
|
||||
closure->function_kind = 4;
|
||||
closure->function_kind = SCR_CLOSURE_KIND_OWN_PROTOTYPE;
|
||||
closure->caps[0] = uc_box(scr_dyn_retain(set));
|
||||
impl = scr_dyn_new_func(closure, uc_debug_write, 0, "native:util.debuglog", "debug");
|
||||
} else {
|
||||
|
||||
@@ -951,7 +951,7 @@ static UtState *ut_state_new(bool throttle, ScrDyn *signal) {
|
||||
* refers to the returned function) stays collectable. */
|
||||
static ScrDyn *ut_build(UtState *s, ScrDyn *wrapped) {
|
||||
ScrClosure *closure = scr_closure_new(NULL, 2);
|
||||
closure->function_kind = 4; /* an ordinary function with its own prototype */
|
||||
closure->function_kind = SCR_CLOSURE_KIND_OWN_PROTOTYPE;
|
||||
UtOwner *owner = malloc(sizeof *owner);
|
||||
if (!owner) ut_oom();
|
||||
owner->rc = 1;
|
||||
|
||||
Generated
+222
@@ -170,6 +170,10 @@ importers:
|
||||
typescript5:
|
||||
specifier: npm:typescript@5.9.3
|
||||
version: typescript@5.9.3
|
||||
devDependencies:
|
||||
typescript-7.1:
|
||||
specifier: npm:typescript@7.1.0-beta
|
||||
version: typescript@7.1.0-beta
|
||||
optionalDependencies:
|
||||
'@scriptc/llvm-darwin-arm64':
|
||||
specifier: workspace:*
|
||||
@@ -1500,120 +1504,246 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@typescript/typescript-aix-ppc64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-DXDG+jIWxBHGdoO70tcHON33rCnSzsEZj9P13oyf/r/KEGIo6awsdSNVNjAw0uMwEpqRu75IrgCcW58YcVkZHg==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@typescript/typescript-android-arm64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-UZx6PxoQavuvaVTrSf3aw0Oy/J/uJpRylipA0kzc038jMvZXh0UtE8vnQp20FqtdgD5bhXZ0aJrRJyoTSUO58Q==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@typescript/typescript-darwin-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@typescript/typescript-darwin-arm64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-q7mxblcyX0E7hXsY4k+Q6KWKK5mGmfVq+BDesdTgRVgjodk27aIxER77Zyj9+TgSTlCje8F7+JM6f6hl2BuNdw==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@typescript/typescript-darwin-x64@7.0.2':
|
||||
resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@typescript/typescript-darwin-x64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-D5EYqG5Wm/guw9+0j8+Mn1Jbr/gfS/VtFTuoH8PJtFbcqeDaQGMulc7SW8q51QG6Y3wGOGX3nbsTOw6fIKWWAA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@typescript/typescript-freebsd-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@typescript/typescript-freebsd-arm64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-9A0XX0tcnH9qPdolSKKzSjTb93dlB0UEcAJbHYEsrFX2fkFNSum9+dvkQQJ9vLc+7vxHetDPujq9Qe6wx+Xhow==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@typescript/typescript-freebsd-x64@7.0.2':
|
||||
resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@typescript/typescript-freebsd-x64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-EDf/dBHfY0TxX3NmUTek0hbffeyZlh3+ls6WQW3ICB8vYxwYLFnzg7wkgEUvl9eRlpiJ/nmc2S8arPMzINJ7rg==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@typescript/typescript-linux-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-arm64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-hNqQBgQ5b5oGqN76QqbAU6CwYhJZ6iOcWc2kPTUs2OentgpxZktETziH6KIyRFJPaiBncognwn2ZLu3DNhHiWw==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-arm@7.0.2':
|
||||
resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-arm@7.1.0-beta':
|
||||
resolution: {integrity: sha512-AvN9HmzOQh46G4312ooSgMHhbB7DtnLwg8D9mo324X49B7weMwtJrBG1MxT9IyI8kzDd7YRtW+1hWGun7WG7Qw==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-loong64@7.0.2':
|
||||
resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-loong64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-xbUustlBrFflt2pNEyG5ye+d4WGPc/rfr2xK29MXeobPSw297JqwuKRrvbKM5o3fE/eUM/c+urVm6jQ3IaMVTw==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-mips64el@7.0.2':
|
||||
resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-mips64el@7.1.0-beta':
|
||||
resolution: {integrity: sha512-fS4/G7/0bXZ3NSq3nBMAXyg65I3tO4S7j08VNQ2g1aNC/qq79gE7Qne1BNjVsjMFfHBVBFjQYoTQCjmVU2iFlg==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-ppc64@7.0.2':
|
||||
resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-ppc64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-0upCCxYPX8X6gOwkvfv+Ee7/ufyy7k97mTrGE0e5BxLlnUEa+w+rtgnv2dsKcxFdwdeZYVlZKCqgN/xCBV08+A==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-riscv64@7.0.2':
|
||||
resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-riscv64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-0jKU9YRgOPRgiy5V/UWv37kB53PaTpIbwGCbbOMUPPAkrjVRNOIAJaz7Wa7s3H7mdDebX1LopUEPEJR+7Xa7zg==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-s390x@7.0.2':
|
||||
resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-s390x@7.1.0-beta':
|
||||
resolution: {integrity: sha512-j+d04s2kxaPQh6DLPb01sknAQfzsrdSZy15jBRHyLEZoaaQ19dVjbFRlkNVvYwUG7ZdP8+y5b/7RmPxdtHUKdQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-x64@7.0.2':
|
||||
resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-x64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-kcD51ihf+pnI5KT4uQQYG04OpwznaZYBlIQhDhl8fflFd27NktxXsUeguxZyvUMyWfum7IF2bBS6Jv7ox0AgFQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-netbsd-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@typescript/typescript-netbsd-arm64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-8W85YyoGK+rgGoSU9jJQUpELbrKcZcbdJk2jPNQ6UKDKI7ryPlPFTrV2jCE5RQuv4Tt96hMDTc33s4YfRrLFBA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@typescript/typescript-netbsd-x64@7.0.2':
|
||||
resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@typescript/typescript-netbsd-x64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-iGsTE0ve2PG0AcyZinwAR0LnGEZhu/y4dceS0hZsYCaKdYETvQtvtlNWlmYKempsBr6C7Il7dMXHVk+VFm9u0g==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@typescript/typescript-openbsd-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@typescript/typescript-openbsd-arm64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-Z2FERdHB9fAKI+KGqdWn04O7uNlnB+VYiEm6INE1aZuzzLNd3KBqQ6IpKUJF2ny4GigGU6nVu75XD9tAUODPng==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@typescript/typescript-openbsd-x64@7.0.2':
|
||||
resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@typescript/typescript-openbsd-x64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-NVG3qeyCq/rJEOhaRjR3LsWgjrjo5vJW25omVLeHAVnpKIG4pE4cenwwKGR06DiJ1s0QTOymcJeKXMEpsiNBVg==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@typescript/typescript-sunos-x64@7.0.2':
|
||||
resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@typescript/typescript-sunos-x64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-ovAdoWXEn2B5/fyWG69pLU3FWmQwPWOjQV5TFbcxtpnOv+R3g8ySGHl1NYRq7Lb/GYdFDvaGcZ+SiU7iKuiMsA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@typescript/typescript-win32-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@typescript/typescript-win32-arm64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-lJbo71IEOfL7ZZIZnArRaOLqjn9BAfoSzyUQn0aSjfSpDZL4ru0afUsdBfo6PQEjD6ssdmi0n/RwaPADX6bsvQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@typescript/typescript-win32-x64@7.0.2':
|
||||
resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@typescript/typescript-win32-x64@7.1.0-beta':
|
||||
resolution: {integrity: sha512-UKcHvbybY2tDTvFW2b1vwiAvDlIRooo69lh8CXWVhOQejcvpM5+IgbiFW9EIwR5Wi5X/v+BdN7gdWbigBuJ3rw==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@vercel/backends@0.8.27':
|
||||
resolution: {integrity: sha512-WW9hfF0mWZDQr23RcjLgVUIoo3V07aq25t6f3RSbq0yIQ2zKZ41x8+0nhNKjzAP7GB7yFmwX4UOv4wV2fWqDDQ==}
|
||||
|
||||
@@ -2659,6 +2789,11 @@ packages:
|
||||
engines: {node: '>=16.20.0'}
|
||||
hasBin: true
|
||||
|
||||
typescript@7.1.0-beta:
|
||||
resolution: {integrity: sha512-cjWjZAJDsmZzsNC5GwHNlW0YPM65jZa5oR0KyQcYX3KSVIfTvtBq//vewISyatKk4OLi5grZjHhIVE9fdrw1UQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
hasBin: true
|
||||
|
||||
uid-promise@1.0.0:
|
||||
resolution: {integrity: sha512-R8375j0qwXyIu/7R0tjdF06/sElHqbmdmWC9M2qQHpEVbvE4I5+38KJI7LUUmQMp7NVq4tKHiBMkT0NFM453Ig==}
|
||||
|
||||
@@ -3560,63 +3695,126 @@ snapshots:
|
||||
'@typescript/typescript-aix-ppc64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-aix-ppc64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-android-arm64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-darwin-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-darwin-arm64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-darwin-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-darwin-x64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-freebsd-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-freebsd-arm64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-freebsd-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-freebsd-x64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-arm64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-arm@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-arm@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-loong64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-loong64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-mips64el@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-mips64el@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-ppc64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-ppc64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-riscv64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-riscv64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-s390x@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-s390x@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-x64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-netbsd-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-netbsd-arm64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-netbsd-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-netbsd-x64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-openbsd-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-openbsd-arm64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-openbsd-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-openbsd-x64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-sunos-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-sunos-x64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-win32-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-win32-arm64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-win32-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-win32-x64@7.1.0-beta':
|
||||
optional: true
|
||||
|
||||
'@vercel/backends@0.8.27(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2)':
|
||||
dependencies:
|
||||
'@vercel/build-utils': 13.36.0
|
||||
@@ -4920,6 +5118,30 @@ snapshots:
|
||||
'@typescript/typescript-win32-arm64': 7.0.2
|
||||
'@typescript/typescript-win32-x64': 7.0.2
|
||||
|
||||
typescript@7.1.0-beta:
|
||||
optionalDependencies:
|
||||
'@typescript/typescript-aix-ppc64': 7.1.0-beta
|
||||
'@typescript/typescript-android-arm64': 7.1.0-beta
|
||||
'@typescript/typescript-darwin-arm64': 7.1.0-beta
|
||||
'@typescript/typescript-darwin-x64': 7.1.0-beta
|
||||
'@typescript/typescript-freebsd-arm64': 7.1.0-beta
|
||||
'@typescript/typescript-freebsd-x64': 7.1.0-beta
|
||||
'@typescript/typescript-linux-arm': 7.1.0-beta
|
||||
'@typescript/typescript-linux-arm64': 7.1.0-beta
|
||||
'@typescript/typescript-linux-loong64': 7.1.0-beta
|
||||
'@typescript/typescript-linux-mips64el': 7.1.0-beta
|
||||
'@typescript/typescript-linux-ppc64': 7.1.0-beta
|
||||
'@typescript/typescript-linux-riscv64': 7.1.0-beta
|
||||
'@typescript/typescript-linux-s390x': 7.1.0-beta
|
||||
'@typescript/typescript-linux-x64': 7.1.0-beta
|
||||
'@typescript/typescript-netbsd-arm64': 7.1.0-beta
|
||||
'@typescript/typescript-netbsd-x64': 7.1.0-beta
|
||||
'@typescript/typescript-openbsd-arm64': 7.1.0-beta
|
||||
'@typescript/typescript-openbsd-x64': 7.1.0-beta
|
||||
'@typescript/typescript-sunos-x64': 7.1.0-beta
|
||||
'@typescript/typescript-win32-arm64': 7.1.0-beta
|
||||
'@typescript/typescript-win32-x64': 7.1.0-beta
|
||||
|
||||
uid-promise@1.0.0: {}
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env node
|
||||
/* Lists the numbers that open pull requests claim in the repository's
|
||||
* numbered registries, and reports the ones two changes claim differently:
|
||||
*
|
||||
* diagnostic codes a code an added line names that origin/main has no
|
||||
* row for in packages/compiler/src/diagnostics/codes.ts
|
||||
* dyn handle tags an added pinned row X(NAME, value) in
|
||||
* packages/runtime/src/scr_dyn.h
|
||||
*
|
||||
* The current branch (its changes since it left origin/main) counts as one
|
||||
* more claimant. Run it before picking a number, or before pushing a change
|
||||
* that takes one; it needs `gh` access to the repository.
|
||||
*
|
||||
* node scripts/registry-claims.mjs every claim and every collision
|
||||
* node scripts/registry-claims.mjs --check only collisions with this branch;
|
||||
* exits 1 when there is one
|
||||
*
|
||||
* A pull request whose diff cannot be read is named in the output, and the
|
||||
* run exits 2 (unless it found a collision), since its claims are missing. */
|
||||
import { execFile, execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const run = promisify(execFile);
|
||||
const check = process.argv.includes("--check");
|
||||
const git = (...args) =>
|
||||
execFileSync("git", args, {
|
||||
encoding: "utf8",
|
||||
maxBuffer: 1 << 30,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const CODES = "packages/compiler/src/diagnostics/codes.ts";
|
||||
const DYN = "packages/runtime/src/scr_dyn.h";
|
||||
|
||||
git("fetch", "-q", "origin", "main");
|
||||
// The codes origin/main has: its active and retired rows (a reserved code is
|
||||
// still a claim), or, before codes.ts existed there, every code its sources name.
|
||||
const baseCodes = new Map();
|
||||
const codesSource = (() => {
|
||||
try {
|
||||
return git("show", `origin/main:${CODES}`);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
if (codesSource !== null) {
|
||||
const reserved = codesSource.indexOf("export const RESERVED_DIAGNOSTIC_CODES");
|
||||
for (const m of codesSource.matchAll(/^ {2}(SC\d{4}): "([^"]*)"/gm)) {
|
||||
if (reserved < 0 || m.index < reserved) baseCodes.set(m[1], m[2]);
|
||||
}
|
||||
} else {
|
||||
const named = git("grep", "-ohE", "SC[0-9]{4}", "origin/main", "--", "packages/*/src/**");
|
||||
for (const code of named.split("\n").filter(Boolean)) baseCodes.set(code, "");
|
||||
}
|
||||
const baseTags = new Map(
|
||||
[...git("show", `origin/main:${DYN}`).matchAll(/^\s*X\(([A-Z0-9_]+), (\d+)\)/gm)].map(
|
||||
([, name, value]) => [Number(value), name],
|
||||
),
|
||||
);
|
||||
|
||||
/** The claims of one unified diff: [registry, number, label] triples. */
|
||||
function claimsOf(diff) {
|
||||
const claims = [];
|
||||
let file = "";
|
||||
let table = ""; // the codes.ts table the current line is in
|
||||
for (const line of diff.split("\n")) {
|
||||
if (line.startsWith("+++ ")) {
|
||||
file = line.slice(4).replace(/^b\//, "");
|
||||
continue;
|
||||
}
|
||||
const section = /^(?:@@.*@@ |[ +])export const (\w+)/.exec(line);
|
||||
if (section) table = section[1];
|
||||
if (!line.startsWith("+")) continue;
|
||||
// A reserved row holds a code for other work; it is not this change's claim.
|
||||
if (file === CODES && table === "RESERVED_DIAGNOSTIC_CODES") continue;
|
||||
if (file === DYN) {
|
||||
const tag = /^\+\s*X\(([A-Z0-9_]+), (\d+)\)/.exec(line);
|
||||
if (tag) claims.push(["dyn handle tag", Number(tag[2]), tag[1]]);
|
||||
}
|
||||
if (!file.startsWith("packages/")) continue;
|
||||
const row = file === CODES ? /^\+ {2}(SC\d{4}): "([^"]*)"/.exec(line) : null;
|
||||
if (row) {
|
||||
if (baseCodes.get(row[1]) !== row[2]) claims.push(["diagnostic code", row[1], row[2]]);
|
||||
continue;
|
||||
}
|
||||
for (const [code] of line.matchAll(/\bSC\d{4}\b/g)) {
|
||||
if (!baseCodes.has(code))
|
||||
claims.push(["diagnostic code", code, `${file}: ${line.slice(1).trim()}`]);
|
||||
}
|
||||
}
|
||||
return claims;
|
||||
}
|
||||
|
||||
const prs = JSON.parse(
|
||||
(
|
||||
await run("gh", [
|
||||
"pr",
|
||||
"list",
|
||||
"--state",
|
||||
"open",
|
||||
"--limit",
|
||||
"300",
|
||||
"--json",
|
||||
"number,title,headRefName,baseRefName",
|
||||
])
|
||||
).stdout,
|
||||
);
|
||||
const head = git("rev-parse", "--abbrev-ref", "HEAD").trim();
|
||||
const here = `this branch (${head})`;
|
||||
|
||||
// A stack of pull requests (each based on the previous one's branch) is one
|
||||
// claimant: a later PR's diff only shows what it adds to its base.
|
||||
const byHead = new Map(prs.map((pr) => [pr.headRefName, pr]));
|
||||
const stackOf = (pr) => {
|
||||
while (byHead.has(pr.baseRefName) && byHead.get(pr.baseRefName) !== pr)
|
||||
pr = byHead.get(pr.baseRefName);
|
||||
return pr.headRefName;
|
||||
};
|
||||
const stacks = new Map();
|
||||
const claimant = (key) => stacks.get(key) ?? stacks.set(key, { names: [], claims: [] }).get(key);
|
||||
|
||||
const queue = [...prs];
|
||||
// Pull requests whose diff could not be read: their claims are missing.
|
||||
const unread = [];
|
||||
await Promise.all(
|
||||
Array.from({ length: 8 }, async () => {
|
||||
for (let pr = queue.shift(); pr; pr = queue.shift()) {
|
||||
const diff = await run("gh", ["pr", "diff", String(pr.number)], { maxBuffer: 1 << 30 }).then(
|
||||
(r) => r.stdout,
|
||||
(error) => {
|
||||
const reason = String(error.stderr || error.message)
|
||||
.trim()
|
||||
.split("\n")[0];
|
||||
unread.push(`#${pr.number} (${reason})`);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
const stack = claimant(stackOf(pr));
|
||||
stack.names.push({ number: pr.number, title: pr.title });
|
||||
if (diff !== null) stack.claims.push(...claimsOf(diff));
|
||||
}
|
||||
}),
|
||||
);
|
||||
// This branch is its own PR's claimant when it has one open.
|
||||
const mine = claimant(byHead.has(head) ? stackOf(byHead.get(head)) : head);
|
||||
mine.names.push({ number: Infinity, title: here });
|
||||
// Committed and uncommitted changes since the branch left origin/main, and
|
||||
// untracked files as wholly added.
|
||||
const untracked = git("ls-files", "--others", "--exclude-standard", "-z")
|
||||
.split("\0")
|
||||
.filter(Boolean);
|
||||
mine.claims.push(
|
||||
...claimsOf(git("diff", git("merge-base", "origin/main", "HEAD").trim())),
|
||||
...untracked
|
||||
.map((file) => claimsOf(`+++ b/${file}\n` + readFileSync(file, "utf8").replace(/^/gm, "+")))
|
||||
.flat(),
|
||||
);
|
||||
// A stack reads "#1 #2 #3: <the first PR's title>".
|
||||
const sources = [...stacks.values()].map(({ names, claims }) => {
|
||||
names.sort((a, b) => a.number - b.number);
|
||||
const prs = names.filter((n) => n.number !== Infinity).map((n) => `#${n.number}`);
|
||||
const title = names[0].title;
|
||||
const name = prs.length === 0 ? title : `${prs.join(" ")}: ${title}`;
|
||||
return {
|
||||
name: names.some((n) => n.title === here) && prs.length > 0 ? `${name} (${here})` : name,
|
||||
claims,
|
||||
};
|
||||
});
|
||||
|
||||
// registry → number → label → claimants. Each claimant labels a number once:
|
||||
// with the codes.ts row that claims it when it adds one, otherwise with the
|
||||
// first line that names it.
|
||||
const table = new Map();
|
||||
for (const { name, claims } of sources) {
|
||||
const labelOf = new Map();
|
||||
for (const [registry, number, label] of claims) {
|
||||
const key = `${registry}\0${number}`;
|
||||
const row = registry === "dyn handle tag" || !label.includes(": ");
|
||||
if (!labelOf.has(key) || (row && !labelOf.get(key).row))
|
||||
labelOf.set(key, { registry, number, label, row });
|
||||
}
|
||||
for (const { registry, number, label } of labelOf.values()) {
|
||||
const numbers = table.get(registry) ?? table.set(registry, new Map()).get(registry);
|
||||
const labels = numbers.get(number) ?? numbers.set(number, new Map()).get(number);
|
||||
(labels.get(label) ?? labels.set(label, []).get(label)).push(name);
|
||||
}
|
||||
}
|
||||
|
||||
let collisions = 0;
|
||||
for (const [registry, numbers] of [...table].sort()) {
|
||||
for (const [number, labels] of [...numbers].sort(([a], [b]) => (a < b ? -1 : 1))) {
|
||||
const onMain = registry === "dyn handle tag" ? baseTags.get(number) : undefined;
|
||||
const clash = labels.size > 1 || (onMain !== undefined && !labels.has(onMain));
|
||||
const ours = [...labels.values()].some((who) => who.some((name) => name.includes(here)));
|
||||
if (check && !(clash && ours)) continue;
|
||||
if (clash) collisions++;
|
||||
console.log(`${clash ? "COLLISION" : "claimed"} ${registry} ${number}`);
|
||||
if (onMain !== undefined && !labels.has(onMain)) console.log(` origin/main: ${onMain}`);
|
||||
for (const [label, who] of labels) console.log(` ${label}\n ${who.join("\n ")}`);
|
||||
}
|
||||
}
|
||||
if (unread.length > 0) {
|
||||
console.log(
|
||||
`could not read the diff of ${unread.length} pull request${unread.length === 1 ? "" : "s"}; ${unread.length === 1 ? "its" : "their"} claims are missing:`,
|
||||
);
|
||||
for (const pr of unread.sort()) console.log(` ${pr}`);
|
||||
}
|
||||
if (collisions === 0)
|
||||
console.log(
|
||||
`${check ? "no collisions with this branch" : "no collisions"}${unread.length > 0 ? " among the diffs read" : ""}`,
|
||||
);
|
||||
process.exitCode = check && collisions > 0 ? 1 : unread.length > 0 ? 2 : 0;
|
||||
@@ -0,0 +1,30 @@
|
||||
// Localized failure, stage L4, at a forward-captured binding: functions
|
||||
// declared before a const capture it, and the const's initializer has an
|
||||
// expression whose type does not compile. The statement lowers again with
|
||||
// that expression degraded, and it initializes the binding the functions
|
||||
// captured: before the declaration a call throws ReferenceError, after it
|
||||
// the call reads the value.
|
||||
type Hook = Map<string, (x: number) => string> | AbortSignal | (() => number) | Set<string>;
|
||||
interface Plugin {
|
||||
name: string;
|
||||
hook: Hook;
|
||||
}
|
||||
function load(text: string): unknown {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
function main(): void {
|
||||
function nameOf(): string {
|
||||
return plugin.name;
|
||||
}
|
||||
function attempt(): string {
|
||||
try {
|
||||
return nameOf();
|
||||
} catch (e) {
|
||||
return e instanceof ReferenceError ? "ReferenceError" : "other";
|
||||
}
|
||||
}
|
||||
console.log(attempt());
|
||||
const plugin = { name: (load('{"name":"tags","hook":null}') as Plugin).name };
|
||||
console.log(attempt(), nameOf().length);
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,24 @@
|
||||
// Localized failure, stage L4: expressions whose own type does not
|
||||
// compile (casts, literals with an unmappable contextual type, spread
|
||||
// clones, temporaries).
|
||||
type Hook = Map<string, (x: number) => string> | AbortSignal | (() => number) | Set<string>;
|
||||
interface Plugin {
|
||||
name: string;
|
||||
hook: Hook;
|
||||
}
|
||||
function load(text: string): unknown {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
function count(p: { name: string }): number {
|
||||
return p.name.length;
|
||||
}
|
||||
const raw = load('{"name":"tags","hook":null}');
|
||||
// A cast to a type that does not compile.
|
||||
console.log(count(raw as Plugin));
|
||||
// A literal whose contextual type does not compile, used without a slot.
|
||||
console.log(count({ name: "lit", hook: () => 1 } as Plugin));
|
||||
// A spread clone of such a value.
|
||||
const base = { name: "base", hook: new Set(["a"]) };
|
||||
console.log(count({ ...(base as Plugin), name: "clone" }));
|
||||
// A temporary: an array literal of them, mapped.
|
||||
console.log([{ name: "a", hook: () => 2 } as Plugin, { name: "bb", hook: new Set<string>() } as Plugin].map((p) => p.name).join("+"));
|
||||
@@ -0,0 +1,52 @@
|
||||
// Localized failure, stage L3: push-down. A declared type that does not
|
||||
// compile keeps its compilable shape; only the failing component is a
|
||||
// checked dynamic piece.
|
||||
type Hook = Map<string, (x: number) => string> | AbortSignal | (() => number) | Set<string>;
|
||||
|
||||
interface Plugin {
|
||||
name: string;
|
||||
priority: number;
|
||||
hook: Hook;
|
||||
}
|
||||
|
||||
class Registry {
|
||||
plugins: Plugin[] = [];
|
||||
byName = new Map<string, Plugin>();
|
||||
pending: Promise<Plugin> | undefined;
|
||||
add(p: Plugin): void {
|
||||
this.plugins.push(p);
|
||||
this.byName.set(p.name, p);
|
||||
}
|
||||
total(): number {
|
||||
let sum = 0;
|
||||
for (const p of this.plugins) sum += p.priority;
|
||||
return sum;
|
||||
}
|
||||
names(): string {
|
||||
return this.plugins.map((p) => p.name).join(",");
|
||||
}
|
||||
run(name: string): string {
|
||||
const p = this.byName.get(name);
|
||||
if (p === undefined) return "missing";
|
||||
const h = p.hook;
|
||||
return typeof h === "function" ? `${p.name}=${h()}` : h instanceof Set ? `${p.name}#${h.size}` : "other";
|
||||
}
|
||||
}
|
||||
|
||||
type Handler = (event: string, extra: Hook) => number;
|
||||
function dispatch(handlers: Handler[], event: string): number {
|
||||
let n = 0;
|
||||
for (const h of handlers) n += h(event, () => event.length);
|
||||
return n;
|
||||
}
|
||||
|
||||
const r = new Registry();
|
||||
const counter: Plugin = { name: "counter", priority: 2, hook: () => 41 };
|
||||
r.add(counter);
|
||||
r.add({ name: "tags", priority: 5, hook: new Set(["a", "b"]) });
|
||||
console.log(r.total(), r.names(), r.run("counter"), r.run("tags"), r.run("x"));
|
||||
counter.priority = 10;
|
||||
console.log(r.total(), r.byName.get("counter") === counter, JSON.stringify(r.plugins.map((p) => [p.name, p.priority])));
|
||||
r.pending = Promise.resolve(counter);
|
||||
r.pending.then((p) => console.log("pending", p.name, p === counter));
|
||||
console.log(dispatch([(e, x) => e.length, (e, x) => (typeof x === "function" ? x() * 2 : 0)], "hello"));
|
||||
@@ -0,0 +1,68 @@
|
||||
// An array or record passed to a parameter, returned, or bound to a const
|
||||
// under a type that lacks some of its optional fields stays the same value:
|
||||
// identity comparisons and writes through any of the names see one object.
|
||||
|
||||
interface Entry {
|
||||
key: string;
|
||||
hits: number;
|
||||
note?: string;
|
||||
}
|
||||
type Counted = { key: string; hits: number };
|
||||
|
||||
function same(list: Counted[], other: Entry[]): boolean {
|
||||
return (list as unknown) === other;
|
||||
}
|
||||
function keep(list: Counted[]): Counted[] {
|
||||
return list;
|
||||
}
|
||||
function first(list: Counted[]): Counted {
|
||||
return list[0]!;
|
||||
}
|
||||
function grow(list: Counted[], key: string): Counted[] {
|
||||
list.push({ key, hits: 0 });
|
||||
return list;
|
||||
}
|
||||
|
||||
const entries: Entry[] = [{ key: "a", hits: 1, note: "first" }];
|
||||
console.log(same(entries, entries), keep(entries) === entries, first(entries) === entries[0]);
|
||||
const grown = grow(entries, "b");
|
||||
grown.push({ key: "c", hits: 2 });
|
||||
console.log(grown === entries, entries.length, entries.map((e) => e.key).join(","));
|
||||
|
||||
function local(list: Entry[]): boolean {
|
||||
const view: Counted[] = list;
|
||||
const kept = keep(list);
|
||||
view[0]!.hits += 10;
|
||||
return view === list && kept === list;
|
||||
}
|
||||
console.log(local(entries), entries[0]!.hits, entries[0]!.note);
|
||||
|
||||
// An annotated const that kept its initializer's layout is that value when it
|
||||
// is passed on: the callee's writes reach both names.
|
||||
function track(list: Counted[], key: string): void {
|
||||
list.push({ key, hits: 0 });
|
||||
}
|
||||
function viaAlias(): string {
|
||||
const source: Entry[] = [{ key: "s", hits: 1, note: "kept" }];
|
||||
const view: Counted[] = source;
|
||||
track(view, "t");
|
||||
return `${view.length} ${source.length} ${view === source}`;
|
||||
}
|
||||
console.log(viaAlias());
|
||||
const moduleSource: Entry[] = [{ key: "m", hits: 1 }];
|
||||
const moduleView: Counted[] = moduleSource;
|
||||
track(moduleView, "n");
|
||||
console.log(moduleView.length, moduleSource.length, moduleView === moduleSource);
|
||||
|
||||
// The same function body lowered for a wider argument and for its own type:
|
||||
// each lowering passes its own binding on.
|
||||
function addTwice(list: Counted[], key: string): number {
|
||||
const alias: Counted[] = list;
|
||||
track(alias, key);
|
||||
track(alias, `${key}2`);
|
||||
return list.length;
|
||||
}
|
||||
const wideList: Entry[] = [{ key: "w", hits: 1, note: "n" }];
|
||||
const plainList: Counted[] = [{ key: "p", hits: 1 }];
|
||||
console.log(addTwice(wideList, "a"), addTwice(plainList, "b"), addTwice(wideList, "c"));
|
||||
console.log(wideList.length, plainList.length);
|
||||
@@ -0,0 +1,96 @@
|
||||
// An array or record passed to a parameter whose type lacks some of its
|
||||
// optional fields is the caller's value inside the callee: pushes, index
|
||||
// stores, element-field writes and removals through the parameter reach the
|
||||
// caller's array, as in Node.
|
||||
|
||||
interface Param {
|
||||
id: string;
|
||||
name: string;
|
||||
owned?: true;
|
||||
}
|
||||
type Slot = { id: string; name: string };
|
||||
|
||||
function addSlot(params: Slot[], name: string): void {
|
||||
params.push({ id: `${name}.0`, name });
|
||||
}
|
||||
function rename(params: Slot[], index: number, name: string): void {
|
||||
params[index]!.name = name;
|
||||
}
|
||||
function replace(params: Slot[], index: number, slot: Slot): void {
|
||||
params[index] = slot;
|
||||
}
|
||||
function removeFirst(params: Slot[]): string | undefined {
|
||||
return params.shift()?.name;
|
||||
}
|
||||
function forward(params: Slot[], name: string): void {
|
||||
addSlot(params, name);
|
||||
addSlot(params, `${name}2`);
|
||||
}
|
||||
function names(params: Slot[]): string {
|
||||
return params.map((p) => p.name).join(",");
|
||||
}
|
||||
|
||||
const params: Param[] = [{ id: "a.0", name: "a", owned: true }];
|
||||
addSlot(params, "b");
|
||||
rename(params, 0, "first");
|
||||
replace(params, 1, { id: "z.0", name: "z" });
|
||||
forward(params, "c");
|
||||
console.log(params.length, JSON.stringify(params));
|
||||
console.log(removeFirst(params), params.length, params[0]?.owned, names(params));
|
||||
params.splice(1, 1);
|
||||
console.log(names(params));
|
||||
|
||||
const holder = { list: params };
|
||||
addSlot(holder.list, "d");
|
||||
console.log(holder.list.length, params.length, holder.list === params);
|
||||
|
||||
// A record parameter: writes to its fields reach the caller's record, and the
|
||||
// optional field the parameter's type lacks stays as the caller left it.
|
||||
interface Counter {
|
||||
label: string;
|
||||
hits: number;
|
||||
note?: string;
|
||||
}
|
||||
function bump(target: { label: string; hits: number }): void {
|
||||
target.hits += 1;
|
||||
target.label = target.label.toUpperCase();
|
||||
}
|
||||
const counter: Counter = { label: "c", hits: 1 };
|
||||
const noted: Counter = { label: "n", hits: 0, note: "kept" };
|
||||
bump(counter);
|
||||
bump(counter);
|
||||
bump(noted);
|
||||
console.log(JSON.stringify(counter), "note" in counter, JSON.stringify(noted));
|
||||
|
||||
// Recursion through the same parameter.
|
||||
function fill(params: Slot[], n: number): void {
|
||||
if (n === 0) return;
|
||||
addSlot(params, `r${n}`);
|
||||
fill(params, n - 1);
|
||||
}
|
||||
const deep: Param[] = [];
|
||||
fill(deep, 3);
|
||||
console.log(names(deep), deep.length);
|
||||
|
||||
// Recursive layouts: a tree with an optional field passed where a tree
|
||||
// without it is expected.
|
||||
interface Tree {
|
||||
label: string;
|
||||
kids: Tree[];
|
||||
note?: string;
|
||||
}
|
||||
interface Plain {
|
||||
label: string;
|
||||
kids: Plain[];
|
||||
}
|
||||
function addKid(t: Plain, label: string): void {
|
||||
t.kids.push({ label, kids: [] });
|
||||
}
|
||||
function size(t: Plain): number {
|
||||
let n = 1;
|
||||
for (const k of t.kids) n += size(k);
|
||||
return n;
|
||||
}
|
||||
const tree: Tree = { label: "r", kids: [{ label: "a", kids: [], note: "leaf" }] };
|
||||
addKid(tree, "b");
|
||||
console.log(size(tree), tree.kids.map((k) => k.label).join(","), tree.kids[0]!.note);
|
||||
@@ -0,0 +1,27 @@
|
||||
// A dynamic slot read in a loop: SC6101 names the slot, and coverage
|
||||
// counts the dynamic operations on it.
|
||||
type Hook = (() => number) | AbortSignal | Map<string, (x: number) => string>;
|
||||
|
||||
interface Plugin {
|
||||
name: string;
|
||||
hook: Hook;
|
||||
}
|
||||
|
||||
const plugins: Plugin[] = [
|
||||
{ name: "one", hook: () => 1 },
|
||||
{ name: "two", hook: () => 2 },
|
||||
];
|
||||
|
||||
function sum(rounds: number): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
for (const p of plugins) {
|
||||
const hook = p.hook;
|
||||
if (typeof hook === "function") total += hook();
|
||||
else total += (hook as { size?: number }).size ?? 0;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
console.log(sum(10));
|
||||
@@ -0,0 +1,16 @@
|
||||
// Two dynamic slots of one declared type: the operations in the loop are
|
||||
// on `hot`, and `cold` is read once.
|
||||
type Hook = (() => number) | AbortSignal | Map<string, (x: number) => string>;
|
||||
|
||||
let cold: Hook = () => 1;
|
||||
let hot: Hook = () => 2;
|
||||
|
||||
function run(rounds: number): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
if (typeof hot === "function") total += hot();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
console.log(run(10), typeof cold);
|
||||
@@ -0,0 +1,23 @@
|
||||
// A type that degrades as a piece of a container (`Plugin.hook`) and as a
|
||||
// slot of its own (`current`): the operations on `current` belong to
|
||||
// `current`, not to the container.
|
||||
type Hook = (() => number) | AbortSignal | Map<string, (x: number) => string>;
|
||||
|
||||
interface Plugin {
|
||||
name: string;
|
||||
hook: Hook;
|
||||
}
|
||||
|
||||
const plugins: Plugin[] = [{ name: "one", hook: () => 1 }];
|
||||
|
||||
let current: Hook = () => 2;
|
||||
|
||||
function run(rounds: number): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
if (typeof current === "function") total += current();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
console.log(run(10), plugins.length);
|
||||
@@ -1,10 +1,7 @@
|
||||
dynamic tier (tier assignment, report-only; --tier=auto)
|
||||
5 functions already operate on dynamic values (4 statements)
|
||||
2 functions already operate on dynamic values (2 statements)
|
||||
Base.constructor tests/coverage-fixtures/localized-failure.ts:57:1: dynObjLit at tests/coverage-fixtures/localized-failure.ts:58:16
|
||||
Session.constructor tests/coverage-fixtures/localized-failure.ts:12:1: dynArrLit at tests/coverage-fixtures/localized-failure.ts:13:27
|
||||
Session.count tests/coverage-fixtures/localized-failure.ts:14:3: %dyn.class.property at tests/coverage-fixtures/localized-failure.ts:15:12
|
||||
Holder.constructor tests/coverage-fixtures/localized-failure.ts:28:1: dynObjLit at tests/coverage-fixtures/localized-failure.ts:29:16
|
||||
%dyn.class.property.0 tests/coverage-fixtures/localized-failure.ts:15:12: %dyn.class.readPropertiesOrEmpty at tests/coverage-fixtures/localized-failure.ts:15:12
|
||||
|
||||
2 demotion requests (refusals and fences the dynamic tier would replace)
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3500 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4655, "san": 33380 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2940, "san": 38518 }
|
||||
@@ -461,6 +461,8 @@ test("a local or module global whose declared type does not compile becomes a dy
|
||||
["global", "all", "(number | Callbacks)[]", "SC2009"],
|
||||
["local", "local", "Callbacks | undefined", "SC2009"],
|
||||
]);
|
||||
// The loop over `all` is top-level code: its dynamic operations count too.
|
||||
expect(coverage.dynamicSlots?.find((s) => s.name === "all")?.operations).toBeGreaterThan(0);
|
||||
expect(coverage.performance?.filter((d) => d.code === "SC6104")).toHaveLength(3);
|
||||
expect(coveragePasses(coverage, "blockers")).toBe(true);
|
||||
expect(coveragePasses(coverage, "dynamic")).toBe(true);
|
||||
@@ -485,6 +487,34 @@ test("a local or module global whose declared type does not compile becomes a dy
|
||||
expect(strict.diagnostics.map((d) => d.code)).toContain("SC2009");
|
||||
});
|
||||
|
||||
test("dynamic slots rank by their dynamic operations and name themselves in SC6101", () => {
|
||||
const { coverage } = analyze(fixture("dynamic-slot-hot.ts"));
|
||||
const slots = coverage.dynamicSlots ?? [];
|
||||
expect(slots.find((s) => s.name === "plugins")?.partial).toBe(true);
|
||||
expect(slots.some((s) => (s.operations ?? 0) > 0)).toBe(true);
|
||||
const hot = coverage.performance?.find((d) => d.code === "SC6101");
|
||||
expect(hot?.message).toContain("`hook` is a dynamic piece of `Plugin[]` (declared `Hook`");
|
||||
});
|
||||
|
||||
test("a type that is a dynamic piece and a slot of its own counts toward its own slot", () => {
|
||||
const { coverage } = analyze(fixture("dynamic-slot-shared.ts"));
|
||||
const slots = coverage.dynamicSlots ?? [];
|
||||
expect(slots.find((s) => s.name === "current")?.operations).toBeGreaterThan(0);
|
||||
expect(slots.find((s) => s.name === "plugins")?.operations ?? 0).toBe(0);
|
||||
const hot = coverage.performance?.find((d) => d.code === "SC6101");
|
||||
expect(hot?.message).toContain("`current` is a dynamic slot (declared `Hook`");
|
||||
});
|
||||
|
||||
test("dynamic slots of one declared type count their own operations", () => {
|
||||
const { coverage } = analyze(fixture("dynamic-slot-same-type.ts"));
|
||||
const slots = coverage.dynamicSlots ?? [];
|
||||
const hot = slots.find((s) => s.name === "hot")?.operations ?? 0;
|
||||
const cold = slots.find((s) => s.name === "cold")?.operations ?? 0;
|
||||
expect(hot).toBeGreaterThan(cold);
|
||||
const hotWarning = coverage.performance?.find((d) => d.code === "SC6101");
|
||||
expect(hotWarning?.message).toContain("`hot` is a dynamic slot (declared `Hook`");
|
||||
});
|
||||
|
||||
test("CLI: --tier and --explain-dynamic", () => {
|
||||
const scriptcCli = join(repoRoot, "packages/cli/src/main.ts");
|
||||
const outDir = mkdtempSync(join(tmpdir(), "scriptc-tier-"));
|
||||
|
||||
Reference in New Issue
Block a user