mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Merge main
This commit is contained in:
@@ -172,6 +172,25 @@ Each key names a package and a semver range of the installed versions the patch
|
||||
|
||||
A declaration is named by its path: `Options.name` for a member of a type, `parse()` for a function's return type, `parse(text)` for a parameter, `list()[]` for an array's elements, and `Env[string]` for an index signature. `export=()` names the return type of the function the module exports, `default` its default export, and a file prefix such as `index.js:parse()` names a declaration inside one of the package's files. One patch applies to the package's own declarations, its JSDoc, and its `@types/` package. The patched type then holds everywhere, so your code has to handle the `undefined` that a widened declaration allows. An optional `"note"` string can link the upstream issue.
|
||||
|
||||
For larger fixes, replace a whole declaration file, or give a package without declarations its own:
|
||||
|
||||
```json:package.json
|
||||
{
|
||||
"scriptc": {
|
||||
"typePatches": {
|
||||
"some-client@^3.2.0": {
|
||||
"files": { "dist/types/options.d.ts": "./type-patches/options.d.ts" }
|
||||
},
|
||||
"untyped-helper@1.x": {
|
||||
"types": "./type-patches/untyped-helper.d.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`files` maps a declaration file inside the package, or inside its `@types/` package written as `@types/<name>/<path>`, to a local `.d.ts` file. `types` supplies the package's declaration entry. Paths of local files are relative to the `package.json`, and a replaced file can import the package's other declaration files with the same relative paths as the file it replaces.
|
||||
|
||||
Patches change only declarations, never the package's code, and values are still checked against the patched types. A build warns when a patch doesn't apply as written: when no range covers the installed version, when a declaration path matches nothing, when the declaration already has the type, or when a path is ambiguous. Report wrong declarations to the package too, so the patch can be removed later.
|
||||
|
||||
## Provenance sources (experimental)
|
||||
|
||||
@@ -25,3 +25,10 @@ Index-signature records list declared fields before the other keys.
|
||||
|
||||
Optional fields and properties added through a record's `unknown` view can be deleted.
|
||||
A required field has no absent state, so deleting it through the view throws `TypeError: Cannot delete property 'x' of #<Object>` and leaves the record unchanged, where Node.js removes the property.
|
||||
|
||||
Records inherit `toString`, `toLocaleString`, `valueOf`, `hasOwnProperty`, `isPrototypeOf` and `propertyIsEnumerable` from `Object.prototype`.
|
||||
A call of one of these methods by name runs the built-in with the record as its receiver, unless the record holds an own function under that name.
|
||||
A keyed read such as `handlers[name]` of a record whose values are functions answers the built-in when the record lacks the key, so truthiness, `typeof` and calls match Node.js.
|
||||
A keyed read of a record whose values are not functions, such as `counts["toString"]` on a `Record<string, number>`, answers `undefined` where Node.js answers the inherited function, so `counts[word] ?? 0` counts such a word as new.
|
||||
Calling a built-in read from a record of functions throws a `TypeError` when its answer does not fit the record's function type, for example `valueOf` read from a `Record<string, (x: number) => number>`.
|
||||
Other inherited names, such as `constructor`, read `undefined` from typed records.
|
||||
|
||||
@@ -2,11 +2,12 @@ import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { isRefCounted, isUnitType, typeEquals, type IrExpr } from "../../ir/ir.js";
|
||||
import { isRefCounted, isUnitType, typeEquals, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import { DYN_KIND } from "./dyn.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { emitRecordKeyGetOr } from "./expr-records.js";
|
||||
import { NULLABLE_NULL, SHAPE_WORD_INDEX, shapeWordHeader } from "./nullable-unions.js";
|
||||
import { emitUnionTag } from "./union-repr.js";
|
||||
|
||||
@@ -397,6 +398,13 @@ export function emitControlExpr(
|
||||
throw new InternalCompilerError("llvm emitter bug: nullish union lacks unit arms");
|
||||
const fused = emitNullishOptionalChain(host, e);
|
||||
if (fused) return fused;
|
||||
if (
|
||||
e.left.kind === "recordKeyGet" &&
|
||||
e.left.missNaN !== true &&
|
||||
typeEquals(e.type, e.left.type) &&
|
||||
storesNoUnit(host, e.left)
|
||||
)
|
||||
return emitRecordKeyGetOr(host, e.left, e.right);
|
||||
if (
|
||||
!typeEquals(e.type, e.left.type) &&
|
||||
(e.left.kind === "varRef" ||
|
||||
@@ -700,3 +708,15 @@ function emitUnionTagSet(host: LlvmEmitterContext, set: TagSet): string {
|
||||
B.line(`${t} = phi i1 [ true, %${yes} ], [ false, %${no} ]`);
|
||||
return t;
|
||||
}
|
||||
|
||||
/** Every value a keyed read can hit is present (no stored undefined or
|
||||
* null), so only a missing key yields the read's unit arm. */
|
||||
function storesNoUnit(host: LlvmEmitterContext, e: ExprOf<"recordKeyGet">): boolean {
|
||||
const shape = host.recordShape(e.shapeId);
|
||||
const holdsUnit = (t: IrType): boolean =>
|
||||
isUnitType(t) ||
|
||||
t.kind === "dyn" ||
|
||||
(t.kind === "union" && (host.unionsById.get(t.unionId)?.arms ?? []).some(isUnitType));
|
||||
if (shape.indexValue !== undefined && holdsUnit(shape.indexValue)) return false;
|
||||
return e.overflowOnly === true || !shape.fields.some((f) => holdsUnit(f.type));
|
||||
}
|
||||
|
||||
@@ -379,6 +379,39 @@ export function emitRegexIntrinsic(
|
||||
}
|
||||
}
|
||||
|
||||
/** `r[k] ?? d` in its pass-through shape: a missing key evaluates `d`
|
||||
* where the read would store undefined, so a hit costs nothing extra. */
|
||||
export function emitRecordKeyGetOr(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "recordKeyGet" },
|
||||
fallback: IrExpr,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const obj = host.emitExpr(e.obj);
|
||||
const k = host.emitExpr(e.key);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
const join = B.newLabel("rkg.j");
|
||||
keyedRecordReadInto(
|
||||
host,
|
||||
slot,
|
||||
join,
|
||||
obj.name,
|
||||
k.name,
|
||||
e.shapeId,
|
||||
e.type,
|
||||
e.overflowOnly === true,
|
||||
e.loc,
|
||||
false,
|
||||
() => host.emitBranchInto(slot, fallback),
|
||||
);
|
||||
B.startBlock(join);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
export function emitRecordKeyGet(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "recordKeyGet" },
|
||||
@@ -526,6 +559,7 @@ export function keyedRecordReadInto(
|
||||
overflowOnly: boolean,
|
||||
loc?: SrcLoc,
|
||||
missNaN = false,
|
||||
missInto: (() => void) | null = null,
|
||||
): void {
|
||||
const B = host.B;
|
||||
const shape = host.recordShape(shapeId);
|
||||
@@ -728,8 +762,10 @@ export function keyedRecordReadInto(
|
||||
}
|
||||
}
|
||||
if (def !== null && resultType.kind === "union" && undefTag >= 0) {
|
||||
// The miss path: the result union's undefined arm.
|
||||
B.line(`store ptr ${host.unitInstanceRef(resultType.unionId, undefTag)}, ptr ${slot}`);
|
||||
// The miss path: the result union's undefined arm, or the caller's
|
||||
// own value for a miss (missInto stores it into the slot).
|
||||
if (missInto !== null) missInto();
|
||||
else B.line(`store ptr ${host.unitInstanceRef(resultType.unionId, undefTag)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ import { InternalCompilerError } from "../../../errors.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import { npmStaticPackageOfPath } from "../../npm-static.js";
|
||||
import { isSafeToRepeat } from "./evaluation-safety.js";
|
||||
import { withInheritedMethods } from "../record-inherited.js";
|
||||
import { lowerClassPrototypeComputedAssignment } from "../class-prototypes.js";
|
||||
import { isClassCallback } from "../class-callbacks.js";
|
||||
import { isKeyedFieldAccess } from "./keyed-field-targets.js";
|
||||
@@ -1445,7 +1446,17 @@ export function lowerRecordKeyRead(
|
||||
loc,
|
||||
};
|
||||
if (nanCoded) return read;
|
||||
return includeUndefined || optionalRead ? read : lowerer.maybeNarrow(read, expr);
|
||||
const answered = withInheritedMethods(lowerer, read, litKey, isCalledAccess(expr));
|
||||
return includeUndefined || optionalRead ? answered : lowerer.maybeNarrow(answered, expr);
|
||||
}
|
||||
|
||||
/** The access is the callee of a call (`o[k](…)`, `o[k]?.(…)`, `o[k]!(…)`),
|
||||
* which passes the object as `this`. */
|
||||
function isCalledAccess(node: ts.Expression): boolean {
|
||||
let child: ts.Expression = node;
|
||||
while (ts.isParenthesizedExpression(child.parent) || ts.isNonNullExpression(child.parent))
|
||||
child = child.parent;
|
||||
return ts.isCallExpression(child.parent) && child.parent.expression === child;
|
||||
}
|
||||
|
||||
/** ToPropertyKey for represented record keys. Missing array-derived string
|
||||
|
||||
@@ -115,6 +115,7 @@ import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../.
|
||||
import { typedArrayLacksMember } from "../builtins/typed-array-members.js";
|
||||
import { TEMPORAL_GLOBAL } from "../../builtin-modules.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
import { lowerRecordInheritedMember } from "../record-inherited.js";
|
||||
import { DATE_METHOD_HINT } from "../builtins/dates.js";
|
||||
import {
|
||||
cjsClassExprWholeExportOf,
|
||||
@@ -3188,6 +3189,12 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi
|
||||
loc: locOf(expr),
|
||||
};
|
||||
}
|
||||
// Object.prototype's methods read as values: an own entry of the
|
||||
// member's function type, else the built-in.
|
||||
if (actual?.type.kind === "record") {
|
||||
const inherited = lowerRecordInheritedMember(lowerer, actual, expr);
|
||||
if (inherited) return inherited;
|
||||
}
|
||||
// A member the checker declares on the receiver, stored in a pure
|
||||
// index-signature record (an object literal that spread one, see
|
||||
// dictSpreadLiteralShape): the key is always present, so the keyed
|
||||
|
||||
@@ -280,6 +280,7 @@ import {
|
||||
} from "./generic-functions.js";
|
||||
|
||||
import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generation.js";
|
||||
import { lowerRecordInheritedCall } from "./record-inherited.js";
|
||||
|
||||
const ZLIB_STREAM_METHODS: ReadonlySet<string> = new Set(["flush", "params", "close", "reset"]);
|
||||
|
||||
@@ -1643,6 +1644,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
// Union receivers whose every arm has a text — the ngrok
|
||||
// `(chunk: Buffer | string) => chunk.toString()` idiom.
|
||||
lowerUnionToStringCall(lowerer, expr, expr.expression) ??
|
||||
// Object.prototype's methods called by name on records: an own
|
||||
// callable entry, else the built-in with the record as receiver.
|
||||
lowerRecordInheritedCall(lowerer, expr, expr.expression) ??
|
||||
// Object.prototype.toString's default answer on records and
|
||||
// override-free program classes — "[object Object]", folded.
|
||||
lowerDefaultToStringCall(lowerer, expr, expr.expression) ??
|
||||
|
||||
@@ -5422,6 +5422,12 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration)
|
||||
const scope = decl.parent?.parent?.parent;
|
||||
if (symbol === undefined || (!ts.isBlock(scope) && !ts.isSourceFile(scope))) return null;
|
||||
|
||||
// A shorthand property's name (`{ width }`) resolves to the property
|
||||
// symbol, but it reads the binding: the checker's shorthand-value symbol.
|
||||
const refersToBinding = (node: ts.Identifier): boolean =>
|
||||
(ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node
|
||||
? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent)
|
||||
: lowerer.checker.getSymbolAtLocation(node)) === symbol;
|
||||
const isPlainAssignmentTarget = (node: ts.Identifier): boolean => {
|
||||
let target: ts.Expression = node;
|
||||
while (ts.isParenthesizedExpression(target.parent) && target.parent.expression === target) {
|
||||
@@ -5437,11 +5443,7 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration)
|
||||
const captures = (fn: ts.Node): boolean => {
|
||||
let captured = false;
|
||||
ts.walkPreorder(fn, (node) => {
|
||||
if (
|
||||
node !== fn &&
|
||||
ts.isIdentifier(node) &&
|
||||
lowerer.checker.getSymbolAtLocation(node) === symbol
|
||||
) {
|
||||
if (node !== fn && ts.isIdentifier(node) && refersToBinding(node)) {
|
||||
captured = true;
|
||||
return "stop";
|
||||
}
|
||||
@@ -5493,7 +5495,7 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration)
|
||||
if (
|
||||
ts.isIdentifier(node) &&
|
||||
node !== decl.name &&
|
||||
lowerer.checker.getSymbolAtLocation(node) === symbol &&
|
||||
refersToBinding(node) &&
|
||||
!isPlainAssignmentTarget(node)
|
||||
) {
|
||||
reads.push(node);
|
||||
|
||||
@@ -5234,11 +5234,11 @@ export class Lowerer {
|
||||
const sourceFiles = parts.map((p) => p.sf);
|
||||
const fnDecls = parts.flatMap((p) => p.fnDecls);
|
||||
// This fixed-point scan intentionally sees deferred bodies before
|
||||
// reachability is known. Batch their binding queries through the
|
||||
// facade's panic fence, then keep lookup free of lowering side effects:
|
||||
// resolveValueSymbol also flushes deferred diagnostics and applies
|
||||
// merged-namespace fences, whose authority remains ordinary lowering.
|
||||
this.checker.prefetchSymbolRoots(sourceFiles, true);
|
||||
// reachability is known. Its binding queries were batched through the
|
||||
// facade's panic fence before collection (prefetchProgramSymbols); keep
|
||||
// lookup free of lowering side effects: resolveValueSymbol also flushes
|
||||
// deferred diagnostics and applies merged-namespace fences, whose
|
||||
// authority remains ordinary lowering.
|
||||
// The facts each fixed-point site reads, by the symbols, nodes and
|
||||
// element keys they belong to (created with the site list below).
|
||||
let siteDependencies: SiteDependencies | null = null;
|
||||
@@ -8040,6 +8040,7 @@ export class Lowerer {
|
||||
if (this.remainder) {
|
||||
for (const fp of parts) this.checker.prefetchSourceFile(fp.sf);
|
||||
}
|
||||
this.prefetchProgramSymbols(parts);
|
||||
this.collectProgram(parts);
|
||||
this.speculativeTypeQueries(() => {
|
||||
extendAssertedRecordMembers(
|
||||
@@ -8499,6 +8500,7 @@ export class Lowerer {
|
||||
this.onEdge = (name) => {
|
||||
collectionEdges.add(name);
|
||||
};
|
||||
this.prefetchProgramSymbols(parts);
|
||||
this.collectProgram(parts);
|
||||
this.speculativeTypeQueries(() => {
|
||||
extendAssertedRecordMembers(
|
||||
@@ -12209,6 +12211,17 @@ export class Lowerer {
|
||||
return lowerClassExpressionInfo(this, expr);
|
||||
}
|
||||
|
||||
/** Batch-prefetches the symbols of every runtime identifier of the
|
||||
* program's files. Collection's early-binding scan and the runtime-
|
||||
* optional analysis look them up across every body, reached or not;
|
||||
* one batch answers both, instead of one request per identifier. */
|
||||
private prefetchProgramSymbols(parts: FileParts[]): void {
|
||||
this.checker.prefetchSymbolRoots(
|
||||
parts.map((fp) => fp.sf),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
/** Depth of the whole-program analyses that query types across every
|
||||
* file before reachability is known (speculativeTypeQueries). */
|
||||
private speculativeTypeQueryDepth = 0;
|
||||
|
||||
@@ -0,0 +1,911 @@
|
||||
/* Object.prototype's methods on typed records.
|
||||
*
|
||||
* A record is an ordinary object, so `r.toString()`, `r.hasOwnProperty(k)`
|
||||
* and the other Object.prototype methods answer from its prototype unless
|
||||
* the record holds an own entry under that name. Method calls with a
|
||||
* constant name call an own entry when the record's type lets one be
|
||||
* callable and otherwise run the built-in natively with the record as its
|
||||
* receiver. Keyed reads of records whose values are functions or checked
|
||||
* values answer the built-in as a native function value on the miss path,
|
||||
* so truthiness, typeof and calls behave as in Node; hits are untouched.
|
||||
* Records of other value types keep reading undefined for these names
|
||||
* (a documented limitation). No dynamic-tier unit is involved: every
|
||||
* built-in is a small lifted function over the record. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
STRING,
|
||||
UNDEFINED_T,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
type IrExpr,
|
||||
type IrLocal,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
||||
import { nullProtoRecordShape, recordNullProtoTest } from "./null-prototype.js";
|
||||
import { recordHasOwnHelper } from "./lower-calls.js";
|
||||
import { OTHER_ARM_FAILURE_MESSAGE } from "./coercions/unions.js";
|
||||
|
||||
/** Object.prototype's methods a record answers from its prototype. */
|
||||
export const RECORD_INHERITED_METHODS: ReadonlySet<string> = new Set([
|
||||
"toString",
|
||||
"toLocaleString",
|
||||
"valueOf",
|
||||
"hasOwnProperty",
|
||||
"isPrototypeOf",
|
||||
"propertyIsEnumerable",
|
||||
]);
|
||||
|
||||
type FuncType = IrType & { kind: "func" };
|
||||
|
||||
/** Declares a hidden local in the function being built: the lowerer's
|
||||
* current function, or a lifted helper's own local list. */
|
||||
type DeclareLocal = (name: string, type: IrType) => string;
|
||||
|
||||
function liftedLocals(): { declare: DeclareLocal; locals: IrLocal[] } {
|
||||
const locals: IrLocal[] = [];
|
||||
return {
|
||||
locals,
|
||||
declare: (name, type) => {
|
||||
const id = `${name}.${locals.length}`;
|
||||
locals.push({ id, name, type, mutable: false });
|
||||
return id;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** The form a read of `type` gives a built-in method value: the union's one
|
||||
* function arm (beside an undefined arm), a checked value, or null when the
|
||||
* type cannot hold a function. */
|
||||
function methodValueForm(lowerer: Lowerer, type: IrType): FuncType | "dyn" | null {
|
||||
if (type.kind === "dyn") return "dyn";
|
||||
if (type.kind !== "union") return null;
|
||||
const arms = lowerer.unions.get(type.unionId)?.arms ?? [];
|
||||
if (!arms.some((arm) => arm.kind === "undefinedT")) return null;
|
||||
const funcs = arms.filter((arm): arm is FuncType => arm.kind === "func");
|
||||
if (funcs.length !== 1 || funcs[0]!.restAbi !== undefined) return null;
|
||||
return funcs[0]!;
|
||||
}
|
||||
|
||||
/** A value of `type` may be a function that a call can enter. */
|
||||
function callableType(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (type.kind === "func" || type.kind === "dyn") return true;
|
||||
if (type.kind !== "union") return false;
|
||||
return (lowerer.unions.get(type.unionId)?.arms ?? []).some(
|
||||
(arm) => arm.kind === "func" || arm.kind === "dyn",
|
||||
);
|
||||
}
|
||||
|
||||
/** A keyed record read (`r[k]`, `r["toString"]`) whose miss answers the
|
||||
* inherited Object.prototype method for those names. Hits keep the
|
||||
* ordinary read; only the miss path tests the key. `bound`: the read is
|
||||
* the callee of a call (`r[k](…)`, `r[k]?.(…)`), whose `this` is the
|
||||
* record; any other read yields the method as a plain value, whose calls
|
||||
* see an undefined `this` as in Node. */
|
||||
export function withInheritedMethods(
|
||||
lowerer: Lowerer,
|
||||
read: IrExpr & { kind: "recordKeyGet" },
|
||||
litKey: string | null,
|
||||
bound: boolean,
|
||||
): IrExpr {
|
||||
if (litKey !== null && !RECORD_INHERITED_METHODS.has(litKey)) return read;
|
||||
if (read.missNaN === true) return read;
|
||||
const form = methodValueForm(lowerer, read.type);
|
||||
const shape = lowerer.shapes.get(read.shapeId);
|
||||
if (form === null || !shape || shape.tuple) return read;
|
||||
const loc = read.loc;
|
||||
const stmts: IrStmt[] = [];
|
||||
const repeatable = (value: IrExpr): boolean => isSafeToRepeat(value) || value.kind === "strLit";
|
||||
const stable = (value: IrExpr, name: string, keep: boolean): IrExpr => {
|
||||
if (keep) return value;
|
||||
const local = lowerer.declareHiddenLocal(name, value.type);
|
||||
stmts.push({ kind: "varDecl", localId: local.id, init: value, loc });
|
||||
return varRef(local.id, value.type, loc);
|
||||
};
|
||||
// The receiver is evaluated before the key: when computing the key runs
|
||||
// code (it may reassign the receiver's variable), the receiver is held
|
||||
// first, even when reading it again would be safe on its own.
|
||||
const keyRepeatable = repeatable(read.key);
|
||||
const obj = stable(read.obj, "%inheritedRecv", repeatable(read.obj) && keyRepeatable);
|
||||
const key = stable(read.key, "%inheritedKey", keyRepeatable);
|
||||
const helper = inheritedReadHelper(lowerer, read.shapeId, read.type, form, bound, loc);
|
||||
const fallback: IrExpr = { kind: "call", callee: helper, args: [obj, key], type: read.type, loc };
|
||||
if (
|
||||
form !== "dyn" &&
|
||||
read.type.kind === "union" &&
|
||||
!(lowerer.unions.get(read.type.unionId)?.arms ?? []).some((arm) => arm.kind === "nullT")
|
||||
) {
|
||||
// `r[k] ?? inherited(r, k)`: the hit is the read's own value, untouched.
|
||||
const coalesced: IrExpr = {
|
||||
kind: "nullish",
|
||||
left: { ...read, obj, key },
|
||||
right: fallback,
|
||||
type: read.type,
|
||||
loc,
|
||||
};
|
||||
return stmts.length === 0
|
||||
? coalesced
|
||||
: { kind: "seqExpr", stmts, result: coalesced, type: read.type, loc };
|
||||
}
|
||||
const value = lowerer.declareHiddenLocal("%inheritedRead", read.type);
|
||||
stmts.push({ kind: "varDecl", localId: value.id, init: { ...read, obj, key }, loc });
|
||||
const found = varRef(value.id, read.type, loc);
|
||||
let missing: IrExpr;
|
||||
if (form === "dyn") {
|
||||
missing = { kind: "dynTest", test: "undefined", value: found, type: BOOL, loc } as IrExpr;
|
||||
} else {
|
||||
const unionId = (read.type as IrType & { kind: "union" }).unionId;
|
||||
const tag = (lowerer.unions.get(unionId)?.arms ?? []).findIndex(
|
||||
(arm) => arm.kind === "undefinedT",
|
||||
);
|
||||
missing = { kind: "unionIsTag", unionId, tag, negated: false, value: found, type: BOOL, loc };
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts,
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: missing,
|
||||
then: fallback,
|
||||
else_: found,
|
||||
type: read.type,
|
||||
loc,
|
||||
},
|
||||
type: read.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `%rec.inherited.<n>(o, k)`: the inherited method `k` names as a value
|
||||
* of `readType` (bound to `o`), or undefined. */
|
||||
function inheritedReadHelper(
|
||||
lowerer: Lowerer,
|
||||
shapeId: string,
|
||||
readType: IrType,
|
||||
form: FuncType | "dyn",
|
||||
bound: boolean,
|
||||
loc: SrcLoc,
|
||||
): string {
|
||||
const key = `rec.inherited:${bound ? "bound" : "value"}:${shapeId}:${typeKey(readType)}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, "%rec.inherited", key);
|
||||
lowerer.arrHofHelpers.set(key, name);
|
||||
const recT: IrType = { kind: "record", shapeId };
|
||||
const o = varRef("o.0", recT, loc);
|
||||
const k = varRef("k.0", STRING, loc);
|
||||
const fnType: FuncType =
|
||||
form === "dyn" ? ({ kind: "func", params: [DYN], ret: DYN } as FuncType) : form;
|
||||
const undefinedValue: IrExpr =
|
||||
form === "dyn"
|
||||
? dynUndefinedExpr(loc)
|
||||
: lowerer.coerceToExpected(
|
||||
{ kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
|
||||
readType,
|
||||
);
|
||||
const named = (method: string): IrExpr => ({
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: k,
|
||||
right: { kind: "strLit", value: method, type: STRING, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const methods = [...RECORD_INHERITED_METHODS];
|
||||
let anyName: IrExpr = named(methods[0]!);
|
||||
for (const method of methods.slice(1))
|
||||
anyName = { kind: "logical", op: "||", left: anyName, right: named(method), type: BOOL, loc };
|
||||
const body: IrStmt[] = [
|
||||
// Most misses name no inherited method: one comparison chain, nothing else.
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "unary", op: "!", operand: anyName, type: BOOL, loc },
|
||||
then: [{ kind: "return", value: { ...undefinedValue }, loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
// An own key holding undefined shadows the prototype like any own key.
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "call",
|
||||
callee: recordHasOwnHelper(lowerer, shapeId, loc),
|
||||
args: [o, k],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [{ kind: "return", value: { ...undefinedValue }, loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
];
|
||||
if (nullProtoRecordShape(lowerer, recT))
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: recordNullProtoTest(o, shapeId, loc),
|
||||
then: [{ kind: "return", value: { ...undefinedValue }, loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
for (const method of methods) {
|
||||
const value: IrExpr = bound
|
||||
? {
|
||||
kind: "call",
|
||||
callee: boundMethodFactory(lowerer, shapeId, method, fnType, loc),
|
||||
args: [o],
|
||||
type: fnType,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "closure",
|
||||
fnName: unboundMethodImpl(lowerer, method, fnType, loc),
|
||||
captures: [],
|
||||
type: fnType,
|
||||
loc,
|
||||
};
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: named(method),
|
||||
then: [
|
||||
{
|
||||
kind: "return",
|
||||
value: lowerer.coerceToExpected(value, form === "dyn" ? DYN : readType),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
body.push({ kind: "return", value: { ...undefinedValue }, loc });
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [
|
||||
{ localId: "o.0", name: "o", type: recT },
|
||||
{ localId: "k.0", name: "k", type: STRING },
|
||||
],
|
||||
returnType: readType,
|
||||
locals: [
|
||||
{ id: "o.0", name: "o", type: recT, mutable: false },
|
||||
{ id: "k.0", name: "k", type: STRING, mutable: false },
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
/** `%rec.bind.<n>(o)`: the built-in `method` bound to `o` as a closure. */
|
||||
function boundMethodFactory(
|
||||
lowerer: Lowerer,
|
||||
shapeId: string,
|
||||
method: string,
|
||||
fnType: FuncType,
|
||||
loc: SrcLoc,
|
||||
): string {
|
||||
const key = `rec.bind:${shapeId}:${method}:${typeKey(fnType)}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, "%rec.bind", key);
|
||||
lowerer.arrHofHelpers.set(key, name);
|
||||
const recT: IrType = { kind: "record", shapeId };
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "o.0", name: "o", type: recT }],
|
||||
returnType: fnType,
|
||||
locals: [{ id: "o.0", name: "o", type: recT, mutable: false, boxed: true }],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "closure",
|
||||
fnName: inheritedMethodImpl(lowerer, shapeId, method, fnType, loc),
|
||||
captures: ["o.0"],
|
||||
type: fnType,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
/** The lifted body of one built-in bound to its record, with the read's
|
||||
* function signature: the built-in's answer converted to the signature's
|
||||
* return type, or the checked-conversion TypeError where it cannot be. */
|
||||
function inheritedMethodImpl(
|
||||
lowerer: Lowerer,
|
||||
shapeId: string,
|
||||
method: string,
|
||||
fnType: FuncType,
|
||||
loc: SrcLoc,
|
||||
): string {
|
||||
const key = `rec.inheritedImpl:${shapeId}:${method}:${typeKey(fnType)}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, `%rec.${method}`, key);
|
||||
lowerer.arrHofHelpers.set(key, name);
|
||||
const recT: IrType = { kind: "record", shapeId };
|
||||
const o = varRef("o.0", recT, loc);
|
||||
const params = fnType.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type }));
|
||||
const args = params.map((p) => varRef(p.localId, p.type, loc));
|
||||
const body: IrStmt[] = [];
|
||||
const hidden = liftedLocals();
|
||||
const answer = builtinAnswer(lowerer, hidden.declare, o, shapeId, method, args, fnType.ret, loc);
|
||||
body.push(...returnAs(lowerer, answer, fnType.ret, loc));
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params,
|
||||
returnType: fnType.ret,
|
||||
captures: [{ localId: "o.0", name: "o", type: recT }],
|
||||
locals: [
|
||||
{ id: "o.0", name: "o", type: recT, mutable: false, boxed: true },
|
||||
...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
|
||||
...hidden.locals,
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
/** One built-in as a plain function value (no receiver): its answer for an
|
||||
* undefined `this`, converted like inheritedMethodImpl's. */
|
||||
function unboundMethodImpl(
|
||||
lowerer: Lowerer,
|
||||
method: string,
|
||||
fnType: FuncType,
|
||||
loc: SrcLoc,
|
||||
): string {
|
||||
const key = `rec.unboundImpl:${method}:${typeKey(fnType)}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, `%obj.${method}`, key);
|
||||
lowerer.arrHofHelpers.set(key, name);
|
||||
const params = fnType.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type }));
|
||||
const arg: IrExpr | undefined = params[0] && varRef(params[0].localId, params[0].type, loc);
|
||||
const coerce = "Cannot convert undefined or null to object";
|
||||
let answer: IrExpr;
|
||||
switch (method) {
|
||||
case "toString":
|
||||
answer = { kind: "strLit", value: "[object Undefined]", type: STRING, loc };
|
||||
break;
|
||||
case "toLocaleString":
|
||||
answer = nodeThrowExpr(
|
||||
1,
|
||||
"",
|
||||
"Object.prototype.toLocaleString called on null or undefined",
|
||||
STRING,
|
||||
loc,
|
||||
);
|
||||
break;
|
||||
case "isPrototypeOf": {
|
||||
// A primitive argument answers false before `this` converts.
|
||||
const no: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
const kind = arg?.type.kind;
|
||||
if (
|
||||
arg === undefined ||
|
||||
isUnitType(arg.type) ||
|
||||
kind === "string" ||
|
||||
kind === "f64" ||
|
||||
kind === "bool" ||
|
||||
kind === "bigint"
|
||||
)
|
||||
answer = no;
|
||||
else if (kind === "dyn")
|
||||
answer = {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: { kind: "dynTest", test: "object", value: arg, type: BOOL, loc },
|
||||
right: { kind: "dynTest", test: "function", value: arg, type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
} as IrExpr,
|
||||
then: nodeThrowExpr(1, "", coerce, BOOL, loc),
|
||||
else_: no,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
else answer = nodeThrowExpr(1, "", coerce, BOOL, loc);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
answer = nodeThrowExpr(1, "", coerce, method === "valueOf" ? DYN : BOOL, loc);
|
||||
}
|
||||
const body: IrStmt[] =
|
||||
answer.kind === "libCall" && answer.fn === "error.nodeThrow"
|
||||
? [
|
||||
{
|
||||
kind: "return",
|
||||
value: nodeThrowExpr(
|
||||
1,
|
||||
"",
|
||||
(answer.args[2] as IrExpr & { kind: "strLit" }).value,
|
||||
fnType.ret,
|
||||
loc,
|
||||
),
|
||||
loc,
|
||||
},
|
||||
]
|
||||
: returnAs(lowerer, answer, fnType.ret, loc);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params,
|
||||
returnType: fnType.ret,
|
||||
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
/** The built-in's answer with `o` as its receiver. `want` is the type the
|
||||
* caller needs (valueOf answers the record itself). */
|
||||
function builtinAnswer(
|
||||
lowerer: Lowerer,
|
||||
declare: DeclareLocal,
|
||||
o: IrExpr,
|
||||
shapeId: string,
|
||||
method: string,
|
||||
args: readonly IrExpr[],
|
||||
want: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
switch (method) {
|
||||
case "toString":
|
||||
return { kind: "strLit", value: "[object Object]", type: STRING, loc };
|
||||
case "toLocaleString":
|
||||
// Object.prototype.toLocaleString calls this.toString().
|
||||
return callOwnOrBuiltin(
|
||||
lowerer,
|
||||
declare,
|
||||
o,
|
||||
shapeId,
|
||||
"toString",
|
||||
[],
|
||||
STRING,
|
||||
"toString",
|
||||
loc,
|
||||
);
|
||||
case "valueOf":
|
||||
return want.kind === "dyn" ? lowerer.coerceToExpected(o, DYN) : o;
|
||||
case "isPrototypeOf":
|
||||
// No object is created with a typed record as its prototype.
|
||||
return { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
default: {
|
||||
// hasOwnProperty and propertyIsEnumerable: ToPropertyKey keeps a
|
||||
// symbol a symbol, and a record's own keys are strings, so a symbol
|
||||
// key is no entry of it.
|
||||
const no: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
const arg = args[0];
|
||||
if (arg?.type.kind === "symbol") return sequence(arg, no, loc);
|
||||
const has = (key: IrExpr): IrExpr => ({
|
||||
kind: "call",
|
||||
callee: recordHasOwnHelper(lowerer, shapeId, loc),
|
||||
args: [o, key],
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
if (arg === undefined || !mayBeSymbol(arg.type)) return has(propertyKeyOf(lowerer, arg, loc));
|
||||
const id = declare("%protoKey", DYN);
|
||||
const held = varRef(id, DYN, loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: id, init: lowerer.coerceToExpected(arg, DYN), loc }],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test: "symbol", value: held, type: BOOL, loc } as IrExpr,
|
||||
then: no,
|
||||
else_: has(propertyKeyOf(lowerer, held, loc)),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a value of `type` may be a symbol at run time. */
|
||||
function mayBeSymbol(type: IrType): boolean {
|
||||
return !(
|
||||
isUnitType(type) ||
|
||||
type.kind === "string" ||
|
||||
type.kind === "f64" ||
|
||||
type.kind === "bool"
|
||||
);
|
||||
}
|
||||
|
||||
/** ToPropertyKey of a built-in's key argument (absent: "undefined"), for
|
||||
* keys that are not symbols. */
|
||||
function propertyKeyOf(lowerer: Lowerer, arg: IrExpr | undefined, loc: SrcLoc): IrExpr {
|
||||
if (arg === undefined || isUnitType(arg.type))
|
||||
return {
|
||||
kind: "strLit",
|
||||
value: arg?.type.kind === "nullT" ? "null" : "undefined",
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
if (arg.type.kind === "string") return arg;
|
||||
if (arg.type.kind === "f64" || arg.type.kind === "bool")
|
||||
return { kind: "toString", operand: arg, type: STRING, loc };
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.toStringCoerce",
|
||||
args: [lowerer.coerceToExpected(arg, DYN)],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `value` returned as `ret`: as is, wrapped into a union arm or a checked
|
||||
* value, discarded for void, or the checked-conversion TypeError. */
|
||||
function returnAs(lowerer: Lowerer, value: IrExpr, ret: IrType, loc: SrcLoc): IrStmt[] {
|
||||
const converted = convertTo(lowerer, value, ret);
|
||||
if (converted !== null) return [{ kind: "return", value: converted, loc }];
|
||||
if (ret.kind === "void")
|
||||
return [
|
||||
{ kind: "exprStmt", expr: value, loc },
|
||||
{ kind: "return", value: null, loc },
|
||||
];
|
||||
return [
|
||||
{ kind: "exprStmt", expr: value, loc },
|
||||
{ kind: "return", value: nodeThrowExpr(1, "", OTHER_ARM_FAILURE_MESSAGE, ret, loc), loc },
|
||||
];
|
||||
}
|
||||
|
||||
function convertTo(lowerer: Lowerer, value: IrExpr, want: IrType): IrExpr | null {
|
||||
if (typeEquals(value.type, want)) return value;
|
||||
if (want.kind === "dyn" && lowerer.dynConvertible(value.type))
|
||||
return lowerer.coerceToExpected(value, DYN);
|
||||
if (want.kind === "union" && lowerer.armTag(want.unionId, value.type) >= 0)
|
||||
return lowerer.coerceToExpected(value, want);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `o.method(...args)` on a record: an own callable entry when the record
|
||||
* holds one, else the built-in. `calleeText` names the callee for Node's
|
||||
* "is not a function". */
|
||||
function callOwnOrBuiltin(
|
||||
lowerer: Lowerer,
|
||||
declare: DeclareLocal,
|
||||
o: IrExpr,
|
||||
shapeId: string,
|
||||
method: string,
|
||||
args: readonly IrExpr[],
|
||||
want: IrType,
|
||||
calleeText: string,
|
||||
loc: SrcLoc,
|
||||
resolved?: { has: IrExpr; own: IrExpr },
|
||||
): IrExpr {
|
||||
const shape = lowerer.shapes.get(shapeId)!;
|
||||
const field = shape.fields.find((f) => f.name === method);
|
||||
const ownType = field ? field.type : shape.indexValue;
|
||||
let builtin = builtinAnswer(lowerer, declare, o, shapeId, method, args, want, loc);
|
||||
const recT: IrType = { kind: "record", shapeId };
|
||||
if (nullProtoRecordShape(lowerer, recT))
|
||||
builtin = {
|
||||
kind: "ternary",
|
||||
cond: recordNullProtoTest(o, shapeId, loc),
|
||||
then: nodeThrowExpr(1, "", `${calleeText} is not a function`, builtin.type, loc),
|
||||
else_: builtin,
|
||||
type: builtin.type,
|
||||
loc,
|
||||
};
|
||||
const builtinAsWant = convertTo(lowerer, builtin, want) ?? builtin;
|
||||
if (ownType === undefined || !callableType(lowerer, ownType)) return builtinAsWant;
|
||||
const own = resolved?.own ?? ownEntryRead(lowerer, o, shapeId, method, loc);
|
||||
const called = callOwn(lowerer, declare, own, args, builtinAsWant.type, calleeText, loc);
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: resolved?.has ?? ownEntryTest(lowerer, o, shapeId, method, loc),
|
||||
then: called,
|
||||
else_: builtinAsWant,
|
||||
type: builtinAsWant.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether the record holds an own entry named `method`. */
|
||||
function ownEntryTest(
|
||||
lowerer: Lowerer,
|
||||
o: IrExpr,
|
||||
shapeId: string,
|
||||
method: string,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
return {
|
||||
kind: "call",
|
||||
callee: recordHasOwnHelper(lowerer, shapeId, loc),
|
||||
args: [o, { kind: "strLit", value: method, type: STRING, loc }],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The record's own entry named `method`: its field, or its overflow entry
|
||||
* (undefined when absent). */
|
||||
function ownEntryRead(
|
||||
lowerer: Lowerer,
|
||||
o: IrExpr,
|
||||
shapeId: string,
|
||||
method: string,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const shape = lowerer.shapes.get(shapeId)!;
|
||||
const field = shape.fields.find((f) => f.name === method);
|
||||
return field
|
||||
? { kind: "recordGet", obj: o, shapeId, field: method, type: field.type, loc }
|
||||
: {
|
||||
kind: "recordKeyGet",
|
||||
obj: o,
|
||||
shapeId,
|
||||
key: { kind: "strLit", value: method, type: STRING, loc },
|
||||
overflowOnly: true,
|
||||
type: lowerer.withUndefinedArmOf(shape.indexValue!) ?? shape.indexValue!,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Call the own entry `own` (present, of any stored type) with `args`. A
|
||||
* non-function throws Node's "is not a function"; a function whose
|
||||
* parameters or result cannot carry the call throws the checked-conversion
|
||||
* TypeError (the program's types claimed otherwise). */
|
||||
function callOwn(
|
||||
lowerer: Lowerer,
|
||||
declare: DeclareLocal,
|
||||
own: IrExpr,
|
||||
args: readonly IrExpr[],
|
||||
want: IrType,
|
||||
calleeText: string,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const notFunction = nodeThrowExpr(1, "", `${calleeText} is not a function`, want, loc);
|
||||
const lie = nodeThrowExpr(1, "", OTHER_ARM_FAILURE_MESSAGE, want, loc);
|
||||
const localId = declare("%ownMethod", own.type);
|
||||
const value = varRef(localId, own.type, loc);
|
||||
const enter = (fn: IrExpr): IrExpr => {
|
||||
if (fn.type.kind === "dyn") {
|
||||
const result: IrExpr = {
|
||||
kind: "dynCall",
|
||||
callee: fn,
|
||||
calleeName: calleeText,
|
||||
args: args.map((a) => lowerer.coerceToExpected(a, DYN)),
|
||||
type: DYN,
|
||||
loc,
|
||||
} as IrExpr;
|
||||
return convertTo(lowerer, result, want) ?? sequence(result, lie, loc);
|
||||
}
|
||||
const sig = fn.type as FuncType;
|
||||
if (sig.restAbi !== undefined) return lie;
|
||||
const passed: IrExpr[] = [];
|
||||
for (let i = 0; i < sig.params.length; i++) {
|
||||
const param = sig.params[i]!;
|
||||
const arg = args[i];
|
||||
const given =
|
||||
arg === undefined
|
||||
? convertTo(
|
||||
lowerer,
|
||||
{ kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
|
||||
param,
|
||||
)
|
||||
: convertTo(lowerer, arg, param);
|
||||
if (given === null) return lie;
|
||||
passed.push(given);
|
||||
}
|
||||
const result: IrExpr = { kind: "callValue", callee: fn, args: passed, type: sig.ret, loc };
|
||||
if (sig.ret.kind === "void") return sequence(result, lie, loc);
|
||||
return convertTo(lowerer, result, want) ?? sequence(result, lie, loc);
|
||||
};
|
||||
let call: IrExpr;
|
||||
if (own.type.kind === "func" || own.type.kind === "dyn") call = enter(value);
|
||||
else if (own.type.kind === "union") {
|
||||
const unionId = own.type.unionId;
|
||||
const arms = lowerer.unions.get(unionId)?.arms ?? [];
|
||||
call = notFunction;
|
||||
for (let tag = arms.length - 1; tag >= 0; tag--) {
|
||||
const arm = arms[tag]!;
|
||||
if (arm.kind !== "func" && arm.kind !== "dyn") continue;
|
||||
call = {
|
||||
kind: "ternary",
|
||||
cond: { kind: "unionIsTag", unionId, tag, negated: false, value, type: BOOL, loc },
|
||||
then: enter({ kind: "unionNarrow", unionId, tag, value, type: arm, loc }),
|
||||
else_: call,
|
||||
type: want,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
} else call = notFunction;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId, init: own, loc }],
|
||||
result: call,
|
||||
type: want,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function sequence(first: IrExpr, result: IrExpr, loc: SrcLoc): IrExpr {
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: first, loc }],
|
||||
result,
|
||||
type: result.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `r.toString()`, `r.hasOwnProperty(k)` and the other Object.prototype
|
||||
* methods called by name on a typed record (the checker resolved the
|
||||
* member to Object.prototype's). Null for other calls. */
|
||||
export function lowerRecordInheritedCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
const method = access.name.text;
|
||||
if (
|
||||
call.questionDotToken ||
|
||||
access.questionDotToken ||
|
||||
!RECORD_INHERITED_METHODS.has(method) ||
|
||||
call.arguments.nodes.some(ts.isSpreadElement) ||
|
||||
!lowerer.isStdlibMember(access)
|
||||
)
|
||||
return null;
|
||||
const recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
if (recvT?.kind !== "record") return null;
|
||||
const shape = lowerer.shapes.get(recvT.shapeId);
|
||||
if (!shape || shape.tuple) return null;
|
||||
const want =
|
||||
method === "valueOf"
|
||||
? (lowerer.mapTypeOf(lowerer.typeOf(call)) ?? recvT)
|
||||
: method === "toString" || method === "toLocaleString"
|
||||
? STRING
|
||||
: BOOL;
|
||||
// Without a possible own function, toString and the own-key tests keep
|
||||
// their existing lowerings (with the special record views they know).
|
||||
if (
|
||||
(method === "hasOwnProperty" || method === "propertyIsEnumerable" || method === "toString") &&
|
||||
!shape.fields.some((f) => f.name === method && callableType(lowerer, f.type)) &&
|
||||
!(shape.indexValue && callableType(lowerer, shape.indexValue))
|
||||
)
|
||||
return null;
|
||||
const loc = locOf(call);
|
||||
const stmts: IrStmt[] = [];
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
if (receiver.type.kind !== "record") return null;
|
||||
const recvLocal = lowerer.declareHiddenLocal("%protoRecv", receiver.type);
|
||||
stmts.push({ kind: "varDecl", localId: recvLocal.id, init: receiver, loc });
|
||||
const o = varRef(recvLocal.id, receiver.type, loc);
|
||||
// `o.m(...)` reads the member before its arguments run: an argument that
|
||||
// adds, deletes or replaces the own method does not change the callee.
|
||||
const field = shape.fields.find((f) => f.name === method);
|
||||
const ownType = field ? field.type : shape.indexValue;
|
||||
let resolved: { has: IrExpr; own: IrExpr } | undefined;
|
||||
if (ownType !== undefined && callableType(lowerer, ownType)) {
|
||||
const shapeId = receiver.type.shapeId;
|
||||
const hasLocal = lowerer.declareHiddenLocal("%protoHas", BOOL);
|
||||
stmts.push({
|
||||
kind: "varDecl",
|
||||
localId: hasLocal.id,
|
||||
init: ownEntryTest(lowerer, o, shapeId, method, loc),
|
||||
loc,
|
||||
});
|
||||
const read = ownEntryRead(lowerer, o, shapeId, method, loc);
|
||||
const ownLocal = lowerer.declareHiddenLocal("%protoOwn", read.type);
|
||||
stmts.push({ kind: "varDecl", localId: ownLocal.id, init: read, loc });
|
||||
resolved = { has: varRef(hasLocal.id, BOOL, loc), own: varRef(ownLocal.id, read.type, loc) };
|
||||
}
|
||||
const args: IrExpr[] = [];
|
||||
for (const node of call.arguments.nodes) {
|
||||
const arg = lowerer.lowerExpr(node);
|
||||
const local = lowerer.declareHiddenLocal("%protoArg", arg.type);
|
||||
stmts.push({ kind: "varDecl", localId: local.id, init: arg, loc });
|
||||
args.push(varRef(local.id, arg.type, loc));
|
||||
}
|
||||
const result = callOwnOrBuiltin(
|
||||
lowerer,
|
||||
(name, type) => lowerer.declareHiddenLocal(name, type).id,
|
||||
o,
|
||||
receiver.type.shapeId,
|
||||
method,
|
||||
args,
|
||||
want,
|
||||
ts.getTextOfNode(access),
|
||||
loc,
|
||||
resolved,
|
||||
);
|
||||
return { kind: "seqExpr", stmts, result, type: result.type, loc };
|
||||
}
|
||||
|
||||
/** `r.toString` and the other Object.prototype methods read by name (not
|
||||
* called) on an index-signature record: an own entry of the member's
|
||||
* function type when the record holds one, else the built-in as a plain
|
||||
* function value. An own entry of another type than the member's (a
|
||||
* function of another signature) throws the checked-conversion TypeError:
|
||||
* the program's types claimed the member. Null when the checker's member
|
||||
* type is not a function. */
|
||||
export function lowerRecordInheritedMember(
|
||||
lowerer: Lowerer,
|
||||
receiver: IrExpr,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
const method = expr.name.text;
|
||||
if (!RECORD_INHERITED_METHODS.has(method) || !lowerer.isStdlibMember(expr)) return null;
|
||||
if (receiver.type.kind !== "record") return null;
|
||||
const want = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (want?.kind !== "func" || want.restAbi !== undefined) return null;
|
||||
const shapeId = receiver.type.shapeId;
|
||||
const shape = lowerer.shapes.get(shapeId);
|
||||
if (!shape || shape.tuple) return null;
|
||||
const loc = locOf(expr);
|
||||
const builtin: IrExpr = {
|
||||
kind: "closure",
|
||||
fnName: unboundMethodImpl(lowerer, method, want, loc),
|
||||
captures: [],
|
||||
type: want,
|
||||
loc,
|
||||
};
|
||||
const recvLocal = lowerer.declareHiddenLocal("%protoRecv", receiver.type);
|
||||
const o = varRef(recvLocal.id, receiver.type, loc);
|
||||
const stmts: IrStmt[] = [{ kind: "varDecl", localId: recvLocal.id, init: receiver, loc }];
|
||||
let result: IrExpr = builtin;
|
||||
if (shape.indexValue && typeEquals(shape.indexValue, want)) {
|
||||
const ownType = lowerer.withUndefinedArmOf(want);
|
||||
if (ownType?.kind === "union") {
|
||||
const read: IrExpr = {
|
||||
kind: "recordKeyGet",
|
||||
obj: o,
|
||||
shapeId,
|
||||
key: { kind: "strLit", value: method, type: STRING, loc },
|
||||
overflowOnly: true,
|
||||
type: ownType,
|
||||
loc,
|
||||
};
|
||||
const tag = lowerer.armTag(ownType.unionId, want);
|
||||
const ownLocal = lowerer.declareHiddenLocal("%ownMethod", ownType);
|
||||
const own = varRef(ownLocal.id, ownType, loc);
|
||||
stmts.push({ kind: "varDecl", localId: ownLocal.id, init: read, loc });
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId: ownType.unionId,
|
||||
tag,
|
||||
negated: false,
|
||||
value: own,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: { kind: "unionNarrow", unionId: ownType.unionId, tag, value: own, type: want, loc },
|
||||
else_: builtin,
|
||||
type: want,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
} else if (shape.indexValue && callableType(lowerer, shape.indexValue)) {
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: ownEntryTest(lowerer, o, shapeId, method, loc),
|
||||
then: nodeThrowExpr(1, "", OTHER_ARM_FAILURE_MESSAGE, want, loc),
|
||||
else_: builtin,
|
||||
type: want,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return { kind: "seqExpr", stmts, result, type: want, loc };
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
trackedRealpath,
|
||||
} from "./input-tracker.js";
|
||||
import { npmBuiltinAliasOf } from "./builtin-modules.js";
|
||||
import { packageSourceText } from "./type-patches/layer.js";
|
||||
import { packageSourceText, typePatchSuppliesTypes } from "./type-patches/layer.js";
|
||||
|
||||
let activePackages: ReadonlySet<string> = new Set();
|
||||
let declarationOverloads: ReadonlyMap<string, NpmStaticDeclarationOverloads> = new Map();
|
||||
@@ -374,6 +374,7 @@ function packageIsUntyped(path: string): boolean {
|
||||
/* no package.json — keep probing */
|
||||
}
|
||||
if (untyped && trackedExists(`${pkgDir}/index.d.ts`)) untyped = false;
|
||||
if (untyped && typePatchSuppliesTypes(pkgDir)) untyped = false;
|
||||
if (untyped) {
|
||||
// the @types twin, hoisted anywhere up the realm chain
|
||||
const mangled = mangledTypesName(dirName);
|
||||
|
||||
@@ -37,9 +37,11 @@ import {
|
||||
trackedRealpath,
|
||||
} from "./input-tracker.js";
|
||||
import { isRelativeSpecifier, packageNameOfSpecifier } from "./workspace-registry.js";
|
||||
import { typePatchFileExists } from "./type-patches/layer.js";
|
||||
|
||||
function isFile(path: string): boolean {
|
||||
return trackedFileExists(path);
|
||||
// A declaration entry a project type patch supplies (type-patches/layer.ts).
|
||||
return trackedFileExists(path) || typePatchFileExists(path);
|
||||
}
|
||||
|
||||
function isDirectory(path: string): boolean {
|
||||
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
trackedRealpath,
|
||||
} from "../input-tracker.js";
|
||||
import { packageShimDeclarationText } from "../package-shims.js";
|
||||
import { typePatchedText } from "../type-patches/layer.js";
|
||||
import { typePatchedText, typePatchFileExists } from "../type-patches/layer.js";
|
||||
import { ambientDtsPath, isFallbackDtsPath, overridesDtsPath } from "../dts-paths.js";
|
||||
import { reusedLibDiagnostics } from "./lib-diagnostics.js";
|
||||
import { ts7Diagnostic } from "./diagnostic.js";
|
||||
@@ -190,7 +190,7 @@ export class Ts7Host {
|
||||
fileExists: (fileName) => {
|
||||
if (virtualFiles.has(tsgoPath(fileName))) return true;
|
||||
if (shadow !== null && shadow.hideFile(fileName)) return false;
|
||||
return trackedFileExists(fileName);
|
||||
return trackedFileExists(fileName) || typePatchFileExists(fileName);
|
||||
},
|
||||
directoryExists: (path) => trackedDirectoryExists(path),
|
||||
realpath: (path) =>
|
||||
|
||||
@@ -27,7 +27,7 @@ import { dirname, relative } from "node:path";
|
||||
import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js";
|
||||
import { typePatchDiag } from "../../diagnostics/diagnostic.js";
|
||||
import { readProjectConfig, type TypePatchEntry } from "../../project-config.js";
|
||||
import { trackedReadFile } from "../input-tracker.js";
|
||||
import { trackedFileExists, trackedReadFile } from "../input-tracker.js";
|
||||
import type { SourceFile } from "../ts7/syntax.js";
|
||||
import { applyPatchEdits, patchFile, type PatchMatch, type PatchRequest } from "./locate.js";
|
||||
import { parseSelector, parseTypeText, type Selector, type TypePatchOp } from "./selector.js";
|
||||
@@ -49,6 +49,11 @@ interface CopyInfo {
|
||||
root: string;
|
||||
/** Requests whose entry's range the version satisfies. */
|
||||
requests: Request[];
|
||||
/** Replaced declaration files: package-relative path → local file. */
|
||||
files: Map<string, string>;
|
||||
/** Declaration entries a `types` patch supplies: package-relative path
|
||||
* → local file (existing entries are replaced, missing ones appear). */
|
||||
entries: Map<string, string>;
|
||||
}
|
||||
|
||||
interface MatchRecord extends PatchMatch {
|
||||
@@ -63,9 +68,11 @@ interface State {
|
||||
projectDir: string;
|
||||
manifestText: string;
|
||||
parse: TypePatchParse;
|
||||
byPackage: Map<string, Request[]>;
|
||||
byPackage: Map<string, { entries: TypePatchEntry[]; requests: Request[] }>;
|
||||
copies: Map<string, CopyInfo | null>;
|
||||
files: Map<string, string | null>;
|
||||
/** `files` targets that do not exist in a copy. */
|
||||
missingFiles: { entry: TypePatchEntry; root: string; rel: string }[];
|
||||
/** Package copies whose version no patch range covers. */
|
||||
unmatchedCopies: Map<string, { pkg: string; version: string | null; entries: TypePatchEntry[] }>;
|
||||
/** Requests applicable to a copy this load read files of. */
|
||||
@@ -86,22 +93,23 @@ export function configureTypePatches(entryPath: string, parse: TypePatchParse):
|
||||
state = null;
|
||||
return;
|
||||
}
|
||||
const byPackage = new Map<string, Request[]>();
|
||||
const byPackage = new Map<string, { entries: TypePatchEntry[]; requests: Request[] }>();
|
||||
let id = 0;
|
||||
for (const entry of config.typePatches) {
|
||||
const pkg = byPackage.get(entry.pkg) ?? { entries: [], requests: [] };
|
||||
byPackage.set(entry.pkg, pkg);
|
||||
pkg.entries.push(entry);
|
||||
for (const decl of entry.declarations) {
|
||||
const selector = parseSelector(decl.selector);
|
||||
const op = parseTypeText(decl.type);
|
||||
if ("error" in selector || "error" in op) continue; // reported as SC8004
|
||||
const list = byPackage.get(entry.pkg) ?? [];
|
||||
list.push({
|
||||
pkg.requests.push({
|
||||
id: id++,
|
||||
selector: selector as Selector,
|
||||
op: op as TypePatchOp,
|
||||
entry,
|
||||
offset: decl.offset,
|
||||
});
|
||||
byPackage.set(entry.pkg, list);
|
||||
}
|
||||
}
|
||||
state = {
|
||||
@@ -112,6 +120,7 @@ export function configureTypePatches(entryPath: string, parse: TypePatchParse):
|
||||
byPackage,
|
||||
copies: new Map(),
|
||||
files: new Map(),
|
||||
missingFiles: [],
|
||||
unmatchedCopies: new Map(),
|
||||
touched: new Set(),
|
||||
matches: [],
|
||||
@@ -164,7 +173,7 @@ function twinRuntimeVersion(s: State, nodeModules: string, pkg: string): string
|
||||
/** The installed package copy a file belongs to, with the requests that
|
||||
* apply to it; null for files outside node_modules or of unpatched
|
||||
* packages. */
|
||||
function copyOf(s: State, path: string): { copy: CopyInfo; rel: string } | null {
|
||||
function copyOf(s: State, path: string, touch = true): { copy: CopyInfo; rel: string } | null {
|
||||
const marker = "/node_modules/";
|
||||
const at = path.lastIndexOf(marker);
|
||||
if (at < 0) return null;
|
||||
@@ -178,33 +187,122 @@ function copyOf(s: State, path: string): { copy: CopyInfo; rel: string } | null
|
||||
let copy = s.copies.get(root);
|
||||
if (copy === undefined) {
|
||||
copy = null;
|
||||
const pkg = name.startsWith("@types/") ? unmangleTypesName(name) : name;
|
||||
const requests = s.byPackage.get(pkg);
|
||||
if (requests !== undefined) {
|
||||
const twin = name.startsWith("@types/");
|
||||
const pkg = twin ? unmangleTypesName(name) : name;
|
||||
const patches = s.byPackage.get(pkg);
|
||||
if (patches !== undefined) {
|
||||
// A twin is matched by the runtime package's version, never its own.
|
||||
const version = name.startsWith("@types/")
|
||||
const version = twin
|
||||
? twinRuntimeVersion(s, path.slice(0, at + marker.length), pkg)
|
||||
: versionAt(root);
|
||||
const applicable = requests.filter(
|
||||
(r) => version !== null && satisfies(version, r.entry.range),
|
||||
const applicable = patches.entries.filter(
|
||||
(e) => version !== null && satisfies(version, e.range),
|
||||
);
|
||||
// Later entries win on the same selector.
|
||||
const bySelector = new Map<string, Request>();
|
||||
for (const r of applicable) bySelector.set(r.selector.text, r);
|
||||
const chosen = [...bySelector.values()];
|
||||
if (chosen.length === 0) {
|
||||
const entries = [...new Set(requests.map((r) => r.entry))];
|
||||
s.unmatchedCopies.set(root, { pkg, version, entries });
|
||||
if (applicable.length === 0) {
|
||||
s.unmatchedCopies.set(root, { pkg, version, entries: patches.entries });
|
||||
} else {
|
||||
// Later entries win on the same selector, file or entry.
|
||||
const bySelector = new Map<string, Request>();
|
||||
for (const r of patches.requests)
|
||||
if (applicable.includes(r.entry)) bySelector.set(r.selector.text, r);
|
||||
const files = new Map<string, string>();
|
||||
let types: string | null = null;
|
||||
const prefix = `${name}/`;
|
||||
for (const e of applicable) {
|
||||
for (const [target, local] of e.files) {
|
||||
// `@types/<name>/<path>` names the twin's file; plain paths the
|
||||
// package's own.
|
||||
const forTwin = target.startsWith("@types/");
|
||||
if (forTwin !== twin) continue;
|
||||
const rel = forTwin
|
||||
? target.startsWith(prefix)
|
||||
? target.slice(prefix.length)
|
||||
: null
|
||||
: target;
|
||||
if (rel === null) continue;
|
||||
if (!trackedFileExists(`${root}/${rel}`)) {
|
||||
s.missingFiles.push({ entry: e, root, rel });
|
||||
continue;
|
||||
}
|
||||
files.set(rel, local);
|
||||
}
|
||||
if (e.types !== null && !twin) types = e.types;
|
||||
}
|
||||
copy = {
|
||||
pkg,
|
||||
version,
|
||||
root,
|
||||
requests: [...bySelector.values()],
|
||||
files,
|
||||
entries: types === null ? new Map() : declarationEntries(root, types),
|
||||
};
|
||||
}
|
||||
copy = chosen.length === 0 ? null : { pkg, version, root, requests: chosen };
|
||||
}
|
||||
s.copies.set(root, copy);
|
||||
}
|
||||
if (copy === null) return null;
|
||||
for (const r of copy.requests) s.touched.add(r);
|
||||
if (touch) for (const r of copy.requests) s.touched.add(r);
|
||||
return { copy, rel };
|
||||
}
|
||||
|
||||
/** The declaration entry paths of a package a `types` patch stands for:
|
||||
* its `types`/`typings` file and every declaration TypeScript probes
|
||||
* beside a JavaScript entry (`main`, the `exports` targets of "."). */
|
||||
function declarationEntries(root: string, local: string): Map<string, string> {
|
||||
const out = new Map<string, string>();
|
||||
let pkg: Record<string, unknown> = {};
|
||||
try {
|
||||
pkg = JSON.parse(trackedReadFile(`${root}/package.json`) ?? "{}") as Record<string, unknown>;
|
||||
} catch {
|
||||
pkg = {};
|
||||
}
|
||||
const add = (target: unknown): void => {
|
||||
if (typeof target !== "string") return;
|
||||
const rel = target.replace(/^\.\//, "");
|
||||
if (/\.d\.[cm]?ts$/.test(rel)) out.set(rel, local);
|
||||
else if (/\.[cm]?js$/.test(rel)) out.set(rel.replace(/\.([cm]?)js$/, ".d.$1ts"), local);
|
||||
// TypeScript probes x.tsx and then x.d.ts for x.jsx.
|
||||
else if (rel.endsWith(".jsx")) out.set(`${rel.slice(0, -".jsx".length)}.d.ts`, local);
|
||||
else if (!/\.\w+$/.test(rel)) out.set(`${rel}.d.ts`, local);
|
||||
};
|
||||
add(pkg["types"]);
|
||||
add(pkg["typings"]);
|
||||
add(typeof pkg["main"] === "string" ? pkg["main"] : "index.js");
|
||||
const exportsField = pkg["exports"];
|
||||
const dot =
|
||||
exportsField !== null && typeof exportsField === "object" && "." in exportsField
|
||||
? (exportsField as Record<string, unknown>)["."]
|
||||
: exportsField;
|
||||
const visit = (v: unknown): void => {
|
||||
if (typeof v === "string") add(v);
|
||||
else if (v !== null && typeof v === "object") for (const x of Object.values(v)) visit(x);
|
||||
};
|
||||
// Subpath exports ("./x") are not the entry.
|
||||
if (dot !== null && typeof dot === "object" && Object.keys(dot).some((k) => k.startsWith(".")))
|
||||
return out;
|
||||
visit(dot);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** True for a declaration entry a `types` patch supplies that does not
|
||||
* exist on disk (a package without declarations gains one). */
|
||||
export function typePatchFileExists(fileName: string): boolean {
|
||||
const s = state;
|
||||
if (s === null) return false;
|
||||
const path = fileName.split("\\").join("/");
|
||||
if (!/\.d\.[cm]?ts$/.test(path)) return false;
|
||||
const located = copyOf(s, path, false);
|
||||
return located !== null && located.copy.entries.has(located.rel);
|
||||
}
|
||||
|
||||
/** True when a `types` patch gives the package at `pkgDir` declarations. */
|
||||
export function typePatchSuppliesTypes(pkgDir: string): boolean {
|
||||
const s = state;
|
||||
if (s === null) return false;
|
||||
const located = copyOf(s, `${pkgDir.split("\\").join("/")}/package.json`, false);
|
||||
return located !== null && located.copy.entries.size > 0;
|
||||
}
|
||||
|
||||
/** Names a request's root can appear under in a file's text. */
|
||||
function rootWords(sel: Selector): string[] {
|
||||
if (sel.root === "export=") return ["exports", "export"];
|
||||
@@ -224,7 +322,12 @@ export function typePatchedText(fileName: string): string | undefined {
|
||||
let result: string | null = null;
|
||||
const located = copyOf(s, path);
|
||||
if (located !== null) {
|
||||
const text = trackedReadFile(path);
|
||||
// A replaced file or a supplied entry reads as the local file; the
|
||||
// declaration patches then apply to it like to the package's own.
|
||||
const local = located.copy.files.get(located.rel) ?? located.copy.entries.get(located.rel);
|
||||
const replaced = local === undefined ? null : trackedReadFile(local);
|
||||
const text = replaced ?? trackedReadFile(path);
|
||||
if (replaced !== null) result = replaced;
|
||||
const requests = located.copy.requests.filter(
|
||||
(r) =>
|
||||
(r.selector.file === null || r.selector.file === located.rel) &&
|
||||
@@ -333,6 +436,16 @@ export function typePatchDiagnostics(): {
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const missing of s.missingFiles) {
|
||||
out.push(
|
||||
typePatchDiag(
|
||||
"SC8031",
|
||||
`type patch '${missing.entry.key}': ${missing.rel} is not a file of ${missing.entry.pkg} at ${relativeToManifest(s, missing.root)}`,
|
||||
"name a declaration file the installed package ships; the package may have moved it",
|
||||
loc(s, missing.entry.offset),
|
||||
),
|
||||
);
|
||||
}
|
||||
for (const r of s.touched) {
|
||||
const mine = s.matches.filter((m) => m.id === r.id);
|
||||
// A selector that matched somewhere (the .d.ts) is not a problem where
|
||||
|
||||
@@ -214,7 +214,7 @@ test("typePatches: malformed keys, ranges, selectors and types are SC8004", () =
|
||||
"A.c": "string\nnumber",
|
||||
"A.d": "| undefined",
|
||||
},
|
||||
files: {},
|
||||
patches: {},
|
||||
},
|
||||
"other@^1.0.0": [],
|
||||
},
|
||||
@@ -225,7 +225,7 @@ test("typePatches: malformed keys, ranges, selectors and types are SC8004", () =
|
||||
`package.json 'scriptc.typePatches.ms@^banana' has the version range "^banana", which is not a semver range`,
|
||||
"package.json 'scriptc.typePatches.@types/node@*' patches @types/node, whose declarations ship with scriptc or TypeScript",
|
||||
"package.json 'scriptc.typePatches.typescript@5' patches typescript, whose declarations ship with scriptc or TypeScript",
|
||||
"package.json 'scriptc.typePatches.lib@^1.0.0.files' is not a known type patch setting",
|
||||
"package.json 'scriptc.typePatches.lib@^1.0.0.patches' is not a known type patch setting",
|
||||
"package.json 'scriptc.typePatches.lib@^1.0.0.declarations.1bad' is not a selector: it must start with a declared name, 'export=' or 'default'",
|
||||
"package.json 'scriptc.typePatches.lib@^1.0.0.declarations.A.b' must map to a type string, not 3",
|
||||
"package.json 'scriptc.typePatches.lib@^1.0.0.declarations.A.c' has a type that must be one line",
|
||||
@@ -241,3 +241,68 @@ test("typePatches: malformed keys, ranges, selectors and types are SC8004", () =
|
||||
});
|
||||
expect(partial.typePatches[0]!.declarations.map((d) => d.selector)).toEqual(["A.d"]);
|
||||
});
|
||||
|
||||
test("typePatches: files and types name local declaration files", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "scriptc-project-config-"));
|
||||
try {
|
||||
mkdirSync(join(root, "patches"));
|
||||
writeFileSync(join(root, "patches", "entry.d.ts"), "export {};\n");
|
||||
const text = JSON.stringify({
|
||||
scriptc: {
|
||||
typePatches: {
|
||||
"lib@^1.0.0": {
|
||||
files: {
|
||||
"dist/entry.d.ts": "./patches/entry.d.ts",
|
||||
"@types/lib/index.d.ts": "patches/entry.d.ts",
|
||||
"dist/index.js": "./patches/entry.d.ts",
|
||||
"../outside.d.ts": "./patches/entry.d.ts",
|
||||
"dist/other.d.ts": "./patches/missing.d.ts",
|
||||
},
|
||||
types: "./patches/entry.d.ts",
|
||||
},
|
||||
"other@^1.0.0": { types: "./patches/entry.ts" },
|
||||
},
|
||||
},
|
||||
});
|
||||
const c = parseProjectConfig(join(root, "package.json"), text);
|
||||
expect(c.diagnostics.map((d) => d.message)).toEqual([
|
||||
"package.json 'scriptc.typePatches.lib@^1.0.0.files.dist/index.js' must name a declaration file inside the package",
|
||||
"package.json 'scriptc.typePatches.lib@^1.0.0.files.../outside.d.ts' must name a declaration file inside the package",
|
||||
`package.json 'scriptc.typePatches.lib@^1.0.0.files.dist/other.d.ts' names "./patches/missing.d.ts", which does not exist`,
|
||||
`package.json 'scriptc.typePatches.other@^1.0.0.types' names "./patches/entry.ts", which is not a declaration file`,
|
||||
]);
|
||||
const entry = c.typePatches.find((e) => e.pkg === "lib")!;
|
||||
expect([...entry.files]).toEqual([
|
||||
["dist/entry.d.ts", join(root, "patches/entry.d.ts")],
|
||||
["@types/lib/index.d.ts", join(root, "patches/entry.d.ts")],
|
||||
]);
|
||||
expect(entry.types).toBe(join(root, "patches/entry.d.ts"));
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("readProjectConfig checks local type patch files again for an unchanged package.json", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "scriptc-project-config-"));
|
||||
try {
|
||||
writeFileSync(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({ scriptc: { typePatches: { "lib@^1.0.0": { types: "./lib.d.ts" } } } }),
|
||||
);
|
||||
const entry = join(root, "main.ts");
|
||||
const missing = readProjectConfig(entry);
|
||||
expect(missing.diagnostics.map((d) => d.message)).toEqual([
|
||||
`package.json 'scriptc.typePatches.lib@^1.0.0.types' names "./lib.d.ts", which does not exist`,
|
||||
]);
|
||||
expect(readProjectConfig(entry)).toBe(missing);
|
||||
writeFileSync(join(root, "lib.d.ts"), "export {};\n");
|
||||
const created = readProjectConfig(entry);
|
||||
expect(created.diagnostics).toEqual([]);
|
||||
expect(created.typePatches[0]!.types).toBe(join(root, "lib.d.ts"));
|
||||
expect(readProjectConfig(entry)).toBe(created);
|
||||
rmSync(join(root, "lib.d.ts"));
|
||||
expect(readProjectConfig(entry).diagnostics.map((d) => d.code)).toEqual(["SC8004"]);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
* to their own readers. */
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { projectConfigDiag, type ScrDiagnostic } from "./diagnostics/diagnostic.js";
|
||||
import { trackedReadFile } from "./frontend/input-tracker.js";
|
||||
import { trackedFileExists, trackedReadFile } from "./frontend/input-tracker.js";
|
||||
import { parseSelector, parseTypeText } from "./frontend/type-patches/selector.js";
|
||||
import { isValidRange } from "./frontend/type-patches/semver.js";
|
||||
|
||||
@@ -79,6 +79,13 @@ export interface TypePatchEntry {
|
||||
pkg: string;
|
||||
range: string;
|
||||
declarations: TypePatchDeclaration[];
|
||||
/** `files`: declaration files of the package (or of its `@types/` twin,
|
||||
* written `@types/<name>/<path>`) replaced by local files, by
|
||||
* package-relative path. The local paths are absolute. */
|
||||
files: Map<string, string>;
|
||||
/** `types`: a local declaration file that becomes the package's
|
||||
* declaration entry (absolute), or null. */
|
||||
types: string | null;
|
||||
/** Offset of the key in the manifest. */
|
||||
offset: number;
|
||||
}
|
||||
@@ -94,6 +101,10 @@ export interface ProjectConfig {
|
||||
wasm: string[];
|
||||
/** `scriptc.typePatches`, in manifest order. */
|
||||
typePatches: TypePatchEntry[];
|
||||
/** The local files the configuration checked for existence, with the
|
||||
* answer. A configuration read again from an unchanged package.json is
|
||||
* reused only while every answer still holds. */
|
||||
localFiles: { path: string; exists: boolean }[];
|
||||
/** SC8004 for every unknown key or value. */
|
||||
diagnostics: ScrDiagnostic[];
|
||||
}
|
||||
@@ -146,6 +157,7 @@ export function emptyProjectConfig(): ProjectConfig {
|
||||
nodeIslands: defaultNodeIslands(),
|
||||
wasm: [],
|
||||
typePatches: [],
|
||||
localFiles: [],
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
@@ -341,7 +353,8 @@ export function parseProjectConfig(manifest: string, text: string): ProjectConfi
|
||||
return config;
|
||||
}
|
||||
|
||||
const TYPE_PATCH_KEYS = ["declarations", "note"];
|
||||
const TYPE_PATCH_KEYS = ["declarations", "files", "types", "note"];
|
||||
const DECLARATION_FILE = /\.d\.[cm]?ts$/;
|
||||
/** Packages whose declarations ship with scriptc or TypeScript: a wrong
|
||||
* one is a bug to report, not a project-local patch. */
|
||||
const UNPATCHABLE = /^(?:@types\/node|typescript|@typescript\/.*)$/;
|
||||
@@ -404,8 +417,66 @@ function readTypePatches(
|
||||
pkg,
|
||||
range,
|
||||
declarations: [],
|
||||
files: new Map(),
|
||||
types: null,
|
||||
offset: locate.offsetOf(["scriptc", "typePatches", key]),
|
||||
};
|
||||
const base = dirname(config.manifest!);
|
||||
const localFile = (path: string[], v: unknown): string | null => {
|
||||
if (typeof v !== "string" || v === "") {
|
||||
problem(
|
||||
path,
|
||||
`must be the path of a local declaration file, not ${shown(v)}`,
|
||||
`write "./type-patches/${pkg}.d.ts"`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const abs = resolve(base, v);
|
||||
if (!DECLARATION_FILE.test(abs)) {
|
||||
problem(
|
||||
path,
|
||||
`names "${v}", which is not a declaration file`,
|
||||
"use a .d.ts, .d.mts or .d.cts file",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const exists = trackedFileExists(abs);
|
||||
config.localFiles.push({ path: abs, exists });
|
||||
if (!exists) {
|
||||
problem(
|
||||
path,
|
||||
`names "${v}", which does not exist`,
|
||||
"give the path relative to this package.json",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return abs;
|
||||
};
|
||||
const files = value["files"];
|
||||
if (files !== undefined && !isRecord(files)) {
|
||||
problem(
|
||||
["typePatches", key, "files"],
|
||||
`must be an object mapping declaration files of the package to local files, not ${shown(files)}`,
|
||||
`write "files": { "dist/index.d.ts": "./type-patches/${pkg}.d.ts" }`,
|
||||
);
|
||||
} else if (files !== undefined) {
|
||||
for (const target of Object.keys(files)) {
|
||||
const path = ["typePatches", key, "files", target];
|
||||
const rel = target.replace(/^\.\//, "");
|
||||
if (!DECLARATION_FILE.test(rel) || rel.startsWith("/") || rel.split("/").includes("..")) {
|
||||
problem(
|
||||
path,
|
||||
"must name a declaration file inside the package",
|
||||
`write a package-relative .d.ts path such as "dist/index.d.ts", or "@types/<name>/index.d.ts" for its @types package`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const abs = localFile(path, files[target]);
|
||||
if (abs !== null) entry.files.set(rel, abs);
|
||||
}
|
||||
}
|
||||
if (value["types"] !== undefined)
|
||||
entry.types = localFile(["typePatches", key, "types"], value["types"]);
|
||||
let valid = true;
|
||||
for (const k of Object.keys(value)) {
|
||||
if (!TYPE_PATCH_KEYS.includes(k)) {
|
||||
@@ -482,7 +553,12 @@ export function readProjectConfig(entryPath: string): ProjectConfig {
|
||||
const manifest = join(dir, "package.json");
|
||||
const text = trackedReadFile(manifest);
|
||||
if (text !== null) {
|
||||
if (cachedConfig !== null && cachedManifest === manifest && cachedText === text)
|
||||
if (
|
||||
cachedConfig !== null &&
|
||||
cachedManifest === manifest &&
|
||||
cachedText === text &&
|
||||
cachedConfig.localFiles.every((file) => trackedFileExists(file.path) === file.exists)
|
||||
)
|
||||
return cachedConfig;
|
||||
// A manifest without a `scriptc` key (none spelled out, none behind a
|
||||
// \u escape) has the empty configuration; builds of such projects
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8360-record-prototype-method-calls.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8361-record-function-table-inherited.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8362-record-inherited-order.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/evolving-let-shorthand-reads.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/type-patch-files/main.ts"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Property 'toUpperCase' does not exist on type 'number'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/fixtures/npm/cases/type-patch-files/main.ts",
|
||||
"start": 346,
|
||||
"end": 357
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "This comparison appears to be unintentional because the types 'number' and 'string' have no overlap.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/fixtures/npm/cases/type-patch-files/main.ts",
|
||||
"start": 429,
|
||||
"end": 447
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/type-patch-types-entry/main.ts"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Could not find a declaration file for module 'pointjs'. '<repo>/tests/fixtures/npm/cases/type-patch-types-entry/node_modules/pointjs/lib/point.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/pointjs` if it exists or add a new declaration (.d.ts) file containing `declare module 'pointjs';`",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/fixtures/npm/cases/type-patch-types-entry/main.ts",
|
||||
"start": 246,
|
||||
"end": 255
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
typePatchDiagnostics,
|
||||
typePatchedSpans,
|
||||
typePatchedText,
|
||||
typePatchFileExists,
|
||||
typePatchMatches,
|
||||
typePatchSuppliesTypes,
|
||||
} from "../src/frontend/type-patches/layer.js";
|
||||
|
||||
const fixtures = join(import.meta.dirname, "../../../tests/fixtures");
|
||||
@@ -217,3 +219,82 @@ test("an export before its hoisted function is patched, and string text that loo
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("files replaces a declaration file and declarations still apply on top", () => {
|
||||
const root = join(fixtures, "npm/cases/type-patch-files");
|
||||
configureTypePatches(join(root, "main.ts"), parse);
|
||||
const entry = typePatchedText(join(root, "node_modules/eventlog/dist/entry.d.ts"));
|
||||
expect(entry).toBe(readFileSync(join(root, "type-patches/entry.d.ts"), "utf8"));
|
||||
expect(typePatchedText(join(root, "node_modules/eventlog/dist/index.d.ts"))).toContain(
|
||||
"{ count: number; last: string | undefined }",
|
||||
);
|
||||
expect(typePatchDiagnostics()!.diagnostics).toEqual([]);
|
||||
});
|
||||
|
||||
test("types gives a package without declarations an entry beside its main", () => {
|
||||
const root = join(fixtures, "npm/cases/type-patch-types-entry");
|
||||
configureTypePatches(join(root, "main.ts"), parse);
|
||||
const pkg = join(root, "node_modules/pointjs");
|
||||
expect(typePatchFileExists(join(pkg, "lib/point.d.ts"))).toBe(true);
|
||||
expect(typePatchFileExists(join(pkg, "index.d.ts"))).toBe(false);
|
||||
expect(typePatchFileExists(join(pkg, "lib/other.d.ts"))).toBe(false);
|
||||
expect(typePatchSuppliesTypes(pkg)).toBe(true);
|
||||
expect(typePatchedText(join(pkg, "lib/point.d.ts"))).toBe(
|
||||
readFileSync(join(root, "type-patches/pointjs.d.ts"), "utf8"),
|
||||
);
|
||||
// Without patches nothing exists that is not on disk.
|
||||
configureTypePatches(join(tmpdir(), "none", "main.ts"), parse);
|
||||
expect(typePatchFileExists(join(pkg, "lib/point.d.ts"))).toBe(false);
|
||||
expect(typePatchSuppliesTypes(pkg)).toBe(false);
|
||||
});
|
||||
|
||||
test("types gives a package whose main is a .jsx file an entry beside it", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "scriptc-type-patches-"));
|
||||
try {
|
||||
const pkg = join(root, "node_modules/viewjs");
|
||||
mkdirSync(join(pkg, "lib"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(pkg, "package.json"),
|
||||
JSON.stringify({ name: "viewjs", version: "1.0.0", main: "lib/view.jsx" }),
|
||||
);
|
||||
writeFileSync(join(pkg, "lib/view.jsx"), "export const view = 1;\n");
|
||||
writeFileSync(join(root, "viewjs.d.ts"), "export declare const view: number;\n");
|
||||
writeFileSync(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({ scriptc: { typePatches: { "viewjs@^1.0.0": { types: "./viewjs.d.ts" } } } }),
|
||||
);
|
||||
configureTypePatches(join(root, "main.ts"), parse);
|
||||
expect(typePatchFileExists(join(pkg, "lib/view.d.ts"))).toBe(true);
|
||||
expect(typePatchSuppliesTypes(pkg)).toBe(true);
|
||||
expect(typePatchedText(join(pkg, "lib/view.d.ts"))).toBe(
|
||||
"export declare const view: number;\n",
|
||||
);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("a files target the installed package does not ship warns", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "scriptc-type-patches-"));
|
||||
try {
|
||||
const twin = join(fixtures, "npm/cases/type-patch-files");
|
||||
writeFileSync(join(root, "local.d.ts"), "export type Entry = {};\n");
|
||||
writeFileSync(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({
|
||||
scriptc: {
|
||||
typePatches: { "eventlog@^2.0.0": { files: { "dist/gone.d.ts": "./local.d.ts" } } },
|
||||
},
|
||||
}),
|
||||
);
|
||||
configureTypePatches(join(root, "main.ts"), parse);
|
||||
expect(typePatchedText(join(twin, "node_modules/eventlog/dist/index.d.ts"))).toBeUndefined();
|
||||
const diags = typePatchDiagnostics()!.diagnostics;
|
||||
expect(diags.map((d) => d.code)).toEqual(["SC8031"]);
|
||||
expect(diags[0]!.message).toMatch(
|
||||
/^type patch 'eventlog@\^2\.0\.0': dist\/gone\.d\.ts is not a file of eventlog at .*type-patch-files\/node_modules\/eventlog$/,
|
||||
);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Object.prototype's methods called by name on typed records: the built-in
|
||||
// with the record as its receiver, or an own entry that shadows it.
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
const counts: Record<string, number> = { apple: 2, pear: 0 };
|
||||
const point: Point = { x: 1, y: 2 };
|
||||
const handlers: Record<string, (n: number) => number> = { double: (n) => n * 2 };
|
||||
const own: Record<string, () => string> = {
|
||||
toString: () => "own toString",
|
||||
valueOf: () => "own valueOf",
|
||||
};
|
||||
|
||||
function line(...parts: unknown[]): void {
|
||||
console.log(parts.map(String).join(" | "));
|
||||
}
|
||||
|
||||
line("toString", counts.toString(), point.toString(), handlers.toString(), String(counts));
|
||||
line("toLocaleString", counts.toLocaleString(), point.toLocaleString());
|
||||
line("valueOf", counts.valueOf() === counts, point.valueOf() === point);
|
||||
line(
|
||||
"hasOwnProperty",
|
||||
counts.hasOwnProperty("apple"),
|
||||
counts.hasOwnProperty("pear"),
|
||||
counts.hasOwnProperty("plum"),
|
||||
counts.hasOwnProperty("toString"),
|
||||
point.hasOwnProperty("x"),
|
||||
point.hasOwnProperty("z"),
|
||||
handlers.hasOwnProperty("double"),
|
||||
handlers.hasOwnProperty("valueOf"),
|
||||
);
|
||||
line("propertyIsEnumerable", counts.propertyIsEnumerable("apple"), point.propertyIsEnumerable("toString"));
|
||||
line("isPrototypeOf", counts.isPrototypeOf(point), point.isPrototypeOf(counts));
|
||||
|
||||
// Own entries shadow the built-ins; toLocaleString calls the own toString.
|
||||
line("own", own.toString(), own.toLocaleString(), own.valueOf(), own.hasOwnProperty("toString"));
|
||||
delete own["toString"];
|
||||
line("own deleted", own.toString(), own.toLocaleString());
|
||||
own.toString = () => "own again";
|
||||
line("own again", own.toString(), `${String(own.toString())}`);
|
||||
|
||||
// Keys added at run time are own keys too.
|
||||
const later: Record<string, number> = {};
|
||||
for (const word of "toString valueOf plain toString".split(" ")) {
|
||||
later[word] = (later.hasOwnProperty(word) ? later[word]! : 0) + 1;
|
||||
}
|
||||
line("later", JSON.stringify(later), later.hasOwnProperty("toString"), later.hasOwnProperty("constructor"));
|
||||
|
||||
// A null-prototype record has no inherited methods.
|
||||
const groups = Object.groupBy([1, 2, 3], (n): string => (n % 2 ? "odd" : "even"));
|
||||
try {
|
||||
line("groups", groups.toString());
|
||||
} catch (e) {
|
||||
line("groups threw", (e as Error).name + ": " + (e as Error).message);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Records of functions used as dispatch tables meet Object.prototype's method
|
||||
// names: a keyed read answers the inherited method, bound to the record when
|
||||
// the read is called at once and a plain function value otherwise.
|
||||
type Command = (arg: string) => unknown;
|
||||
const commands: Record<string, Command> = {
|
||||
echo: (arg) => arg,
|
||||
shout: (arg) => arg.toUpperCase(),
|
||||
};
|
||||
const inputs = process.argv.length > 99 ? [] : ["echo", "toString", "valueOf", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "shout", "nope"];
|
||||
function show(v: unknown): string {
|
||||
return v === commands ? "<commands>" : typeof v + ":" + String(v);
|
||||
}
|
||||
for (const name of inputs) {
|
||||
const handler = commands[name];
|
||||
if (!handler) {
|
||||
console.log(name, "unknown handler");
|
||||
continue;
|
||||
}
|
||||
console.log(name, typeof handler, "direct", show(commands[name](name === "hasOwnProperty" ? "echo" : "x")));
|
||||
console.log(name, "optional", show(commands[name]?.("shout")));
|
||||
try {
|
||||
console.log(name, "detached", show(handler("echo")));
|
||||
} catch (e) {
|
||||
console.log(name, "detached threw", (e as Error).name + ": " + (e as Error).message);
|
||||
}
|
||||
}
|
||||
console.log(typeof commands["toString"], typeof commands["valueOf"], commands["toString"] === undefined, commands["toString"] === commands["toString"]);
|
||||
console.log(String(commands["toString"]("x")), commands["hasOwnProperty"]("shout"), commands["hasOwnProperty"]("nope"));
|
||||
const literal: Record<string, Command> = {};
|
||||
console.log(Object.keys(literal).length, literal["isPrototypeOf"]?.("x"), typeof literal.toString);
|
||||
|
||||
// A word count whose words are not Object.prototype names keeps `?? 0`.
|
||||
const freq: Record<string, number> = {};
|
||||
for (const w of "a rose is a rose is a rose".split(" ")) freq[w] = (freq[w] ?? 0) + 1;
|
||||
console.log(JSON.stringify(freq), freq["tulip"] ?? 0);
|
||||
|
||||
// Read by name without a call: a plain function value.
|
||||
const counts: Record<string, number> = { a: 1 };
|
||||
const named = counts.toString;
|
||||
console.log(typeof counts.toString, typeof commands.valueOf, String(named()));
|
||||
@@ -0,0 +1,40 @@
|
||||
// Object.prototype's methods on typed records keep JavaScript's evaluation
|
||||
// order and key conversions.
|
||||
|
||||
// A function-table key that reassigns the receiver reads the table
|
||||
// evaluated before the key.
|
||||
let table: Record<string, () => string> = { a: () => "first" };
|
||||
const other: Record<string, () => string> = { a: () => "second" };
|
||||
function key(): string {
|
||||
table = other;
|
||||
return "a";
|
||||
}
|
||||
console.log(String(table[key()]()));
|
||||
|
||||
// hasOwnProperty and propertyIsEnumerable with a symbol key on a record of
|
||||
// functions test the symbol key.
|
||||
const fns: Record<string, (n: number) => number> = { double: (n) => n * 2 };
|
||||
const sym = Symbol("s");
|
||||
console.log(fns.hasOwnProperty(sym as unknown as string), fns.propertyIsEnumerable(sym as unknown as string));
|
||||
|
||||
// A call by name reads the method before its arguments run: an argument
|
||||
// that adds, replaces or deletes the own method does not change the callee.
|
||||
type Has = Record<string, (key: string) => boolean>;
|
||||
function add(r: Has): string {
|
||||
r.hasOwnProperty = () => true;
|
||||
return "zzz";
|
||||
}
|
||||
function swap(r: Has): string {
|
||||
r.hasOwnProperty = (key) => key === "b";
|
||||
return "a";
|
||||
}
|
||||
function drop(r: Has): string {
|
||||
delete r["hasOwnProperty"];
|
||||
return "zzz";
|
||||
}
|
||||
const added: Has = {};
|
||||
console.log(added.hasOwnProperty(add(added)), added.hasOwnProperty("zzz"));
|
||||
const swapped: Has = { hasOwnProperty: (key) => key === "a" };
|
||||
console.log(swapped.hasOwnProperty(swap(swapped)), swapped.hasOwnProperty("a"));
|
||||
const removed: Has = { hasOwnProperty: () => true };
|
||||
console.log(removed.hasOwnProperty(drop(removed)), removed.hasOwnProperty("zzz"));
|
||||
@@ -0,0 +1,41 @@
|
||||
// An initializer-less JavaScript `let` read through a shorthand property
|
||||
// (`{ width }`) still reads undefined on the paths that never assign it, as
|
||||
// npm packages do when they build an options or size object.
|
||||
function size(tty) {
|
||||
let width;
|
||||
if (tty) width = 80;
|
||||
return { width };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} tty
|
||||
* @returns {{ width: number | undefined }}
|
||||
*/
|
||||
function documented(tty) {
|
||||
/** @type {number | undefined} */
|
||||
let width;
|
||||
if (tty) width = 80;
|
||||
return { width };
|
||||
}
|
||||
|
||||
function captured(tty) {
|
||||
let width;
|
||||
const read = () => ({ width });
|
||||
const before = read().width;
|
||||
if (tty) width = 80;
|
||||
return [before, read().width];
|
||||
}
|
||||
|
||||
function nested(flags) {
|
||||
let columns;
|
||||
let rows;
|
||||
if (flags.columns) columns = 120;
|
||||
if (flags.rows) rows = 40;
|
||||
return { size: { columns, rows } };
|
||||
}
|
||||
|
||||
console.log(size(true).width, size(false).width);
|
||||
console.log(documented(true).width, documented(false).width);
|
||||
console.log(JSON.stringify(size(false)), "width" in size(false));
|
||||
console.log(captured(true), captured(false));
|
||||
console.log(nested({ columns: true }), nested({ rows: true }), nested({}));
|
||||
@@ -0,0 +1,10 @@
|
||||
// A package declaration file replaced by a local one (scriptc.typePatches
|
||||
// files), plus a declaration patch on another file of the same package.
|
||||
import { parseLog, summarize } from "eventlog";
|
||||
|
||||
const entries = parseLog("10 info ada started\n12 warn - disk low\n15 error lin crashed hard\n");
|
||||
for (const e of entries) console.log(e.at, e.level.toUpperCase(), e.user ?? "(system)", e.message);
|
||||
console.log(entries.filter((e) => e.level !== "info").length, JSON.stringify(entries[1]));
|
||||
const all = summarize(entries);
|
||||
const none = summarize([]);
|
||||
console.log(all.count, all.last ?? "-", none.count, none.last ?? "-");
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// Out of date: the JavaScript reports levels as strings and has no
|
||||
// `user` for system entries.
|
||||
export type Entry = {
|
||||
at: number;
|
||||
level: number;
|
||||
user: string;
|
||||
message: string;
|
||||
};
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export type { Entry } from "./entry";
|
||||
import type { Entry } from "./entry";
|
||||
export declare function parseLog(text: string): Entry[];
|
||||
export declare function summarize(entries: Entry[]): { count: number; last: string };
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
function parseLog(text) {
|
||||
const rows = String(text)
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [at, level, user, ...rest] = line.split(" ");
|
||||
return { at: Number(at), level: level, user: user === "-" ? undefined : user, message: rest.join(" ") };
|
||||
});
|
||||
// Entries travel as JSON (the log format's own wire form).
|
||||
return JSON.parse(JSON.stringify(rows));
|
||||
}
|
||||
function summarize(entries) {
|
||||
return { count: entries.length, last: entries.length === 0 ? undefined : entries[entries.length - 1].message };
|
||||
}
|
||||
exports.parseLog = parseLog;
|
||||
exports.summarize = summarize;
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "eventlog", "version": "2.3.1", "main": "dist/index.js", "types": "dist/index.d.ts" }
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "type-patch-files-case",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scriptc": {
|
||||
"typePatches": {
|
||||
"eventlog@^2.0.0": {
|
||||
"files": { "dist/entry.d.ts": "./type-patches/entry.d.ts" },
|
||||
"declarations": { "summarize().last": "| undefined" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// The project's replacement for eventlog's dist/entry.d.ts.
|
||||
export type Entry = {
|
||||
at: number;
|
||||
level: "info" | "warn" | "error";
|
||||
user?: string;
|
||||
message: string;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
// A package without declarations, typed by a local declaration file
|
||||
// (scriptc.typePatches types): user code is checked against it, and the
|
||||
// values cross the declaration boundary as native records.
|
||||
import { centroid, point, type Point } from "pointjs";
|
||||
|
||||
const points: Point[] = [point(0, 0), point(4, 0), point(4, 3)];
|
||||
const c = centroid(points);
|
||||
console.log(c === null ? "none" : `${c.x.toFixed(2)},${c.y.toFixed(2)}`);
|
||||
console.log(centroid([]) === null, JSON.stringify(points.map((p) => p.x + p.y)));
|
||||
Generated
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
// Plain JavaScript without declarations.
|
||||
function point(x, y) {
|
||||
return { x: x, y: y };
|
||||
}
|
||||
function centroid(points) {
|
||||
if (points.length === 0) return null;
|
||||
var sx = 0;
|
||||
var sy = 0;
|
||||
for (var i = 0; i < points.length; i++) {
|
||||
sx += points[i].x;
|
||||
sy += points[i].y;
|
||||
}
|
||||
return point(sx / points.length, sy / points.length);
|
||||
}
|
||||
exports.point = point;
|
||||
exports.centroid = centroid;
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "pointjs", "version": "1.4.0", "main": "lib/point.js" }
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "type-patch-types-entry-case",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scriptc": {
|
||||
"typePatches": {
|
||||
"pointjs@1.x": { "types": "./type-patches/pointjs.d.ts" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// The project's declarations for pointjs, which ships none.
|
||||
export type Point = { x: number; y: number };
|
||||
export declare function point(x: number, y: number): Point;
|
||||
export declare function centroid(points: Point[]): Point | null;
|
||||
@@ -0,0 +1,12 @@
|
||||
// A dot read of a prototype-named member whose own entry has another
|
||||
// signature than the member's (tests/harness/record-inherited-member.test.ts).
|
||||
const plain: Record<string, (x: number) => number> = { double: (x) => x * 2 };
|
||||
const builtin = plain.toString;
|
||||
console.log(typeof builtin);
|
||||
const numeric: Record<string, (x: number) => number> = { toString: (x: number) => x + 1 };
|
||||
try {
|
||||
const own = numeric.toString;
|
||||
console.log(typeof own);
|
||||
} catch (e) {
|
||||
console.log(e instanceof TypeError, (e as Error).message);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3200, "san": 3900 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3200, "san": 3900 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2000, "san": 2000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3700, "san": 2800 }
|
||||
@@ -0,0 +1,32 @@
|
||||
/* Object.prototype's methods on typed records (frontend/lowering/
|
||||
* record-inherited.ts): a dot read of a prototype-named member answers the
|
||||
* built-in when the record has no own entry, and throws the checked
|
||||
* conversion's TypeError when its own entry is a function of another
|
||||
* signature than the member's, which the program's types claimed. Node reads
|
||||
* the own function there, so the program cannot be a corpus case. */
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile } from "@scriptc/compiler";
|
||||
import { OTHER_ARM_FAILURE_MESSAGE } from "../../packages/compiler/src/frontend/lowering/coercions/unions.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = join(import.meta.dirname, "../..");
|
||||
const entry = join(repoRoot, "tests/fixtures/record-inherited/member-signature.ts");
|
||||
const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/record-inherited-member");
|
||||
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
||||
|
||||
test(
|
||||
"a prototype-named member read with an own function of another signature throws",
|
||||
{ timeout: 300_000 },
|
||||
async () => {
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const result = await compile(entry, { outPath: join(outDir, "main"), outDir, sanitize });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const { stdout } = await execFileAsync(result.binaryPath, [], { encoding: "utf8" });
|
||||
expect(stdout).toBe(`function\ntrue ${OTHER_ARM_FAILURE_MESSAGE}\n`);
|
||||
},
|
||||
);
|
||||
@@ -4,8 +4,10 @@
|
||||
* value (the shape of commander's HelpContext.helpWidth), and ms's @return
|
||||
* tags, compile natively and byte-match Node once patched; without the
|
||||
* patches the same declarations throw where Node runs. Patches that do not
|
||||
* apply as written warn (SC8030-SC8033). The `.d.ts` and `@types` cases
|
||||
* run in npm.test.ts (type-patch-dts, type-patch-types-twin), and
|
||||
* apply as written warn (SC8030-SC8033). The `.d.ts`, `@types`, replaced
|
||||
* file and supplied entry cases run in npm.test.ts (type-patch-dts,
|
||||
* type-patch-types-twin, type-patch-files, type-patch-types-entry); here
|
||||
* their unpatched copies show the patches are what makes them compile. And
|
||||
* commander-calc carries the HelpContext.helpWidth patch. */
|
||||
import { execFile } from "node:child_process";
|
||||
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
@@ -121,6 +123,48 @@ describe("type patches", () => {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"without its patch, code using the out-of-date declaration file does not type-check",
|
||||
{ timeout: 600_000 },
|
||||
async () => {
|
||||
const copy = unpatched(join(repoRoot, "tests/fixtures/npm/cases/type-patch-files"));
|
||||
try {
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const result = await compile(join(copy, "main.ts"), {
|
||||
outPath: join(outDir, "files-unpatched"),
|
||||
outDir,
|
||||
sanitize,
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.diagnostics.map((d) => d.message)).toContain(
|
||||
"Property 'toUpperCase' does not exist on type 'number'.",
|
||||
);
|
||||
} finally {
|
||||
rmSync(copy, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"without its patch, a package without declarations is not compiled for a typed import",
|
||||
{ timeout: 600_000 },
|
||||
async () => {
|
||||
const copy = unpatched(join(repoRoot, "tests/fixtures/npm/cases/type-patch-types-entry"));
|
||||
try {
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const result = await compile(join(copy, "main.ts"), {
|
||||
outPath: join(outDir, "types-entry-unpatched"),
|
||||
outDir,
|
||||
sanitize,
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
} finally {
|
||||
rmSync(copy, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"patches that do not apply as written warn and the build goes on",
|
||||
{ timeout: 600_000 },
|
||||
|
||||
Reference in New Issue
Block a user