Merge remote-tracking branch 'origin/ni/1-libnode-loader' into mac/ni1

This commit is contained in:
Malte Ubl
2026-10-26 12:50:45 -07:00
76 changed files with 3798 additions and 746 deletions
+10
View File
@@ -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.
+2 -2
View File
@@ -139,8 +139,8 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
<dt><code>--print &lt;native-link-info|diagnostics&gt;</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 &lt;blockers|divergences|dynamic&gt;</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 &lt;blockers|divergences|dynamic|dynamic-slots&gt;</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 &lt;file&gt;</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>
+34 -1
View File
@@ -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.
+2
View File
@@ -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.
+3
View File
@@ -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:*",
+10 -4
View File
@@ -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);
});
+11 -4
View File
@@ -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);
});
+4 -3
View File
@@ -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
+2 -1
View File
@@ -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;
+11 -2
View File
@@ -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`);
}
});
+162
View File
@@ -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 };
}
+4 -2
View File
@@ -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
+227 -211
View File
@@ -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);
}
+288 -256
View File
@@ -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 });
}
});
+157 -19
View File
@@ -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 &&
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/localized-failure-degraded-initializer.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/localized-failure-expressions.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/localized-failure-push-down.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/width-layout-identity.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/width-layout-parameter-writes.ts"
],
"diags": []
}
+19 -8
View File
@@ -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) {
+58 -14
View File
@@ -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;
}
}
+1 -1
View File
@@ -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]);
+10 -10
View File
@@ -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;
+3 -3
View File
@@ -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 {
+1 -1
View File
@@ -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;
+222
View File
@@ -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: {}
+213
View File
@@ -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"));
+68
View File
@@ -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 }
+30
View File
@@ -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-"));