mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-24 06:14:01 +08:00
Merge main
This commit is contained in:
@@ -2,6 +2,7 @@ node_modules/
|
||||
.pnpm-store/
|
||||
# test-fixture packages are TEST DATA, committed on purpose
|
||||
!tests/diagnostics/npm-imports/node_modules/
|
||||
!tests/diagnostics/npm-program-type-error/node_modules/
|
||||
!tests/diagnostics/retired-engine-import-refusal/node_modules/
|
||||
!tests/fixtures/commander-calc/node_modules/
|
||||
!tests/fixtures/npm/node_modules/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Supported uses of `any` use native checked values. Operations without native lowering produce diagnostics such as `SC2011`. Use `unknown` with a checked cast where an operation needs a concrete type. Map and Set type arguments may use `any`, with the same checked storage as `unknown`.
|
||||
|
||||
Locals, parameters, and returns can carry untyped values; record and tuple fields can hold `unknown`. JavaScript instance fields can also hold native checked values. Bare fields start as `undefined`, and redeclaring a field with the same checked-value type resets it in initialization order. Explicit TypeScript `any`/`unknown` class fields, static checked-value fields, statically typed array elements, and union members are unsupported.
|
||||
Locals, parameters, and returns can carry untyped values; record and tuple fields can hold `unknown`. JavaScript instance fields and TypeScript class fields declared `unknown` or `object` (also with `null` or `undefined`, or as arrays of those) can also hold native checked values. Bare fields start as `undefined`, and redeclaring a field with the same checked-value type resets it in initialization order. Explicit TypeScript `any` class fields, other checked-value static fields, statically typed array elements, and union members are unsupported.
|
||||
|
||||
Program class instances retain identity through `unknown` and casts back to their exact class or a base class, including classes with internal collections. Inspecting dynamic properties throws a catchable `TypeError` if the class's fields cannot pass through checked storage. Supported native handles also retain identity. JavaScript argument and return conversions preserve native array references; other JSON-shaped record and array conversions can copy values.
|
||||
|
||||
|
||||
@@ -279,25 +279,39 @@ export function lowerPromiseStaticCall(
|
||||
// tick-for-tick in Node for plain results (probed), with the
|
||||
// promise-returning form riding the async return-adoption machinery
|
||||
// (its one-tick residue is SEMANTICS.md 358) — so the lowering IS
|
||||
// that wrapper: one interned async helper per callback type. The
|
||||
// ...args form fences (close over the values instead).
|
||||
// that wrapper: one interned async helper per callback and argument
|
||||
// types. `Promise.try(f, ...args)` evaluates the arguments at the call
|
||||
// and passes them to f, which still runs synchronously.
|
||||
if (member === "try") {
|
||||
const fNode = call.arguments[0];
|
||||
if (call.arguments.length !== 1 || fNode === undefined || ts.isSpreadElement(fNode)) {
|
||||
const argNodes = call.arguments.slice(1);
|
||||
if (fNode === undefined || call.arguments.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(
|
||||
`Promise.try with ${call.arguments.length} arguments`,
|
||||
`Promise.try with ${call.arguments.length === 0 ? "no" : "spread"} arguments`,
|
||||
call,
|
||||
"the ...args form has no lowering — close over the values: Promise.try(() => f(a, b))",
|
||||
"pass the callback and its arguments directly: Promise.try(f, a, b)",
|
||||
);
|
||||
}
|
||||
const f = lowerer.lowerExpr(fNode);
|
||||
if (f.type.kind !== "func" || f.type.params.length !== 0) {
|
||||
// Arguments past the callback's parameters are evaluated and dropped;
|
||||
// only an arrow function, which has no `arguments`, can ignore them.
|
||||
if (
|
||||
f.type.kind !== "func" ||
|
||||
f.type.params.length > argNodes.length ||
|
||||
(f.type.params.length < argNodes.length && !ts.isArrowFunction(fNode))
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
`Promise.try over a callback of type '${lowerer.checker.typeToString(lowerer.typeOf(fNode))}'`,
|
||||
`Promise.try over a callback of type '${lowerer.checker.typeToString(lowerer.typeOf(fNode))}' with ${argNodes.length} argument${argNodes.length === 1 ? "" : "s"}`,
|
||||
fNode,
|
||||
"a zero-parameter function is the supported form",
|
||||
"pass one argument per callback parameter: Promise.try((a, b) => a + b, 1, 2)",
|
||||
);
|
||||
}
|
||||
const fnT = f.type;
|
||||
const args = argNodes.map((node, i) =>
|
||||
i < fnT.params.length
|
||||
? lowerer.lowerExprExpecting(node, fnT.params[i]!)
|
||||
: lowerer.lowerExpr(node),
|
||||
);
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (resultT?.kind !== "promise") {
|
||||
lowerer.noLowering(
|
||||
@@ -323,13 +337,22 @@ export function lowerPromiseStaticCall(
|
||||
"annotate both to one T: Promise.try<T>(f) with f returning T or Promise<T>",
|
||||
);
|
||||
}
|
||||
const key = `promise.try:${typeKey(f.type)}`;
|
||||
const key = `promise.try:${typeKey(f.type)}:${args.map((a) => typeKey(a.type)).join(",")}`;
|
||||
let helper = lowerer.arrHofHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = `%promise.try.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(key, helper);
|
||||
const fRef: IrExpr = { kind: "varRef", localId: "f.0", type: f.type, loc };
|
||||
const callF: IrExpr = { kind: "callValue", callee: fRef, args: [], type: ret, loc };
|
||||
const argLocals = args.map((a, i) => ({ id: `a${i}.0`, name: `a${i}`, type: a.type }));
|
||||
const callF: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee: fRef,
|
||||
args: argLocals
|
||||
.slice(0, fnT.params.length)
|
||||
.map((a) => ({ kind: "varRef" as const, localId: a.id, type: a.type, loc })),
|
||||
type: ret,
|
||||
loc,
|
||||
};
|
||||
// Adopt a promise result before either returning its value or completing void.
|
||||
const value: IrExpr =
|
||||
ret.kind === "promise" ? { kind: "awaitExpr", value: callF, type: ret.inner, loc } : callF;
|
||||
@@ -342,16 +365,22 @@ export function lowerPromiseStaticCall(
|
||||
: [{ kind: "return", value, loc }];
|
||||
const lifted: IrFunction = {
|
||||
name: helper,
|
||||
params: [{ localId: "f.0", name: "f", type: f.type }],
|
||||
params: [
|
||||
{ localId: "f.0", name: "f", type: f.type },
|
||||
...argLocals.map((a) => ({ localId: a.id, name: a.name, type: a.type })),
|
||||
],
|
||||
returnType: inner,
|
||||
locals: [{ id: "f.0", name: "f", type: f.type, mutable: true }],
|
||||
locals: [
|
||||
{ id: "f.0", name: "f", type: f.type, mutable: true },
|
||||
...argLocals.map((a) => ({ ...a, mutable: true })),
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
async: true,
|
||||
};
|
||||
lowerer.liftedFns.push(lifted);
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [f], type: resultT, loc };
|
||||
return { kind: "call", callee: helper, args: [f, ...args], type: resultT, loc };
|
||||
}
|
||||
// Promise.resolve — the already-settled promise. Zero arguments is
|
||||
// Promise<void>; a PROMISE argument is returned as-is (the spec's
|
||||
|
||||
@@ -77,6 +77,48 @@ export function narrowedRetagHelper(
|
||||
return lowerer.unionRetagHelper(fromId, toId, loc, trappable);
|
||||
}
|
||||
|
||||
/** A union conversion whose source arms include an object type with a
|
||||
* required property of type `never` (the error arm of `Result<T, never>`).
|
||||
* No value has that type, but its arm still has a storage layout (never
|
||||
* maps to the f64 placeholder) that no destination arm matches. When the
|
||||
* node's checker type proves a source arm uninhabited, the re-tag traps on
|
||||
* it instead of needing a destination. Optional `never` properties,
|
||||
* `never[]` and functions returning `never` have values and keep their
|
||||
* arms. Null when no source arm is proven uninhabited. */
|
||||
export function uninhabitedArmRetagHelper(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Node,
|
||||
fromId: string,
|
||||
toId: string,
|
||||
loc: SrcLoc,
|
||||
): string | null {
|
||||
const type = lowerer.typeOf(node);
|
||||
if (!type.isUnionType()) return null;
|
||||
const uninhabited = new Set<number>();
|
||||
const inhabited = new Set<number>();
|
||||
for (const part of ts.constituentTypes(type)) {
|
||||
const arm = lowerer.mapTypeOf(part);
|
||||
const tag = arm ? lowerer.armTag(fromId, arm) : -1;
|
||||
if (tag < 0) continue;
|
||||
(hasRequiredNeverProperty(lowerer, part) ? uninhabited : inhabited).add(tag);
|
||||
}
|
||||
// An arm shared with an inhabited constituent holds real values.
|
||||
for (const tag of inhabited) uninhabited.delete(tag);
|
||||
if (uninhabited.size === 0) return null;
|
||||
return lowerer.unionRetagHelper(fromId, toId, loc, uninhabited);
|
||||
}
|
||||
|
||||
function hasRequiredNeverProperty(lowerer: Lowerer, type: ts.Type): boolean {
|
||||
if ((type.flags & ts.TypeFlags.Object) === 0) return false;
|
||||
return lowerer.checker
|
||||
.getPropertiesOfType(type)
|
||||
.some(
|
||||
(property) =>
|
||||
(property.flags & ts.SymbolFlags.Optional) === 0 &&
|
||||
(lowerer.checker.getTypeOfSymbol(property).flags & ts.TypeFlags.Never) !== 0,
|
||||
);
|
||||
}
|
||||
|
||||
/** The stranded-UNIT trap for PLAIN (non-union) slots: a null/undefined
|
||||
* value flowing into a non-nullable typed slot the checker approved —
|
||||
* `null!` and `null as any as T` casts, and the non-strict world's
|
||||
|
||||
@@ -709,6 +709,30 @@ function arrayUnionHofHelper(
|
||||
return name;
|
||||
}
|
||||
|
||||
/** A kept filter element as the result's element type: the value itself,
|
||||
* or its narrowed arm or class view when the predicate narrowed it. */
|
||||
function filterRetainedValue(
|
||||
lowerer: Lowerer,
|
||||
v: IrExpr,
|
||||
elem: IrType,
|
||||
outElem: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
if (typeEquals(outElem, elem)) return v;
|
||||
const classView =
|
||||
outElem.kind === "object" ? checkedClassAssertion(lowerer, v, outElem, loc) : null;
|
||||
if (classView !== null) return classView;
|
||||
if (v.type.kind !== "union") return v;
|
||||
return {
|
||||
kind: "unionNarrow",
|
||||
unionId: v.type.unionId,
|
||||
tag: lowerer.armTag(v.type.unionId, outElem),
|
||||
value: v,
|
||||
type: outElem,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The loop body of one synthetic array HOF, from existing IR nodes:
|
||||
*
|
||||
* map: out = []; n = a.length; for (i = 0; i < n; i++) out.push(f(a[i])); return out;
|
||||
@@ -914,24 +938,7 @@ function buildArrayHofFn(
|
||||
{ id: "v.0", name: "v", type: valueT, mutable: false },
|
||||
);
|
||||
returnType = outT;
|
||||
const classView =
|
||||
!typeEquals(outElem, elem) && outElem.kind === "object"
|
||||
? checkedClassAssertion(lowerer, varRef("v.0", valueT, loc), outElem, loc)
|
||||
: null;
|
||||
const retained = typeEquals(outElem, elem)
|
||||
? varRef("v.0", valueT, loc)
|
||||
: classView !== null
|
||||
? classView
|
||||
: valueT.kind === "union"
|
||||
? {
|
||||
kind: "unionNarrow" as const,
|
||||
unionId: valueT.unionId,
|
||||
tag: lowerer.armTag(valueT.unionId, outElem),
|
||||
value: varRef("v.0", valueT, loc),
|
||||
type: outElem,
|
||||
loc,
|
||||
}
|
||||
: varRef("v.0", valueT, loc);
|
||||
const retained = filterRetainedValue(lowerer, varRef("v.0", valueT, loc), elem, outElem, loc);
|
||||
body = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
@@ -1039,16 +1046,7 @@ export function lowerArrayFindLikeCall(
|
||||
// `packages.some((p) => p.scripts[name])` idiom, whose callback
|
||||
// returns the index read's `string | undefined`).
|
||||
const fnRet = fnArg.type.ret;
|
||||
const truthyRet =
|
||||
fnRet.kind === "bool" ||
|
||||
fnRet.kind === "f64" ||
|
||||
fnRet.kind === "string" ||
|
||||
fnRet.kind === "dyn" ||
|
||||
(fnRet.kind === "union" &&
|
||||
(lowerer.unions.get(fnRet.unionId)?.arms ?? []).every(
|
||||
(a) => a.kind !== "dyn" && a.kind !== "caught",
|
||||
));
|
||||
if (!truthyRet) lowerer.badType(argNode, lowerer.typeOf(argNode));
|
||||
if (!findPredicateOk(lowerer, fnRet)) lowerer.badType(argNode, lowerer.typeOf(argNode));
|
||||
const last = method === "findLast" || method === "findLastIndex";
|
||||
if (
|
||||
method === "some" ||
|
||||
@@ -1116,42 +1114,7 @@ export function lowerArrayFindLikeCall(
|
||||
if (resultT.kind !== "union") lowerer.badType(call, lowerer.typeOf(call)); // defensive: T | undefined always maps to a union
|
||||
const undefTag = lowerer.armTag(resultT.unionId, UNDEFINED_T);
|
||||
if (undefTag < 0) lowerer.badType(call, lowerer.typeOf(call));
|
||||
let retag: string | null = null;
|
||||
if (!lowerer.coercibleValue(valueT, resultT)) {
|
||||
const inline = ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode);
|
||||
if (!unchangedElement && (!inline || argNode.type !== undefined)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
argNode,
|
||||
`narrowing '.${method}' requires an inline callback with an inferred predicate (annotate the return ': boolean' to keep the receiver's element type)`,
|
||||
);
|
||||
}
|
||||
const signature = inline ? lowerer.checker.getSignatureFromDeclaration(argNode) : undefined;
|
||||
const predicate = signature
|
||||
? lowerer.checker.getTypePredicateOfSignature(signature)
|
||||
: undefined;
|
||||
if (
|
||||
(!unchangedElement && (!predicate || predicate.parameterIndex !== 0)) ||
|
||||
valueT.kind !== "union"
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`'.${method}' result narrowing without a predicate over its element`,
|
||||
);
|
||||
}
|
||||
retag = lowerer.narrowedRetagHelper(call, valueT.unionId, resultT.unionId, loc);
|
||||
const present = lowerer.stripUndefinedArm(resultT);
|
||||
if (
|
||||
retag === null &&
|
||||
!(present.kind === "object" && canAssertClassValue(lowerer, valueT, present))
|
||||
)
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`'.${method}' narrowing to an incompatible result layout`,
|
||||
);
|
||||
}
|
||||
const retag = findResultRetag(lowerer, call, argNode, method, valueT, resultT, unchangedElement);
|
||||
const helper = findHelper(
|
||||
lowerer,
|
||||
elem,
|
||||
@@ -1168,6 +1131,67 @@ export function lowerArrayFindLikeCall(
|
||||
return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultT, loc };
|
||||
}
|
||||
|
||||
/** The retag helper that converts a found element into find's result union,
|
||||
* or null when the element converts directly. Only an inline callback with
|
||||
* an inferred predicate over its element can narrow the result; a written
|
||||
* predicate is an unchecked assertion and keeps the refusal. */
|
||||
function findResultRetag(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
argNode: ts.Expression,
|
||||
method: string,
|
||||
valueT: IrType,
|
||||
resultT: IrType & { kind: "union" },
|
||||
unchangedElement: boolean,
|
||||
): string | null {
|
||||
if (lowerer.coercibleValue(valueT, resultT)) return null;
|
||||
const loc = locOf(call);
|
||||
const inline = ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode);
|
||||
if (!unchangedElement && (!inline || argNode.type !== undefined)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
argNode,
|
||||
`narrowing '.${method}' requires an inline callback with an inferred predicate (annotate the return ': boolean' to keep the receiver's element type)`,
|
||||
);
|
||||
}
|
||||
const signature = inline ? lowerer.checker.getSignatureFromDeclaration(argNode) : undefined;
|
||||
const predicate = signature ? lowerer.checker.getTypePredicateOfSignature(signature) : undefined;
|
||||
if (
|
||||
(!unchangedElement && (!predicate || predicate.parameterIndex !== 0)) ||
|
||||
valueT.kind !== "union"
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`'.${method}' result narrowing without a predicate over its element`,
|
||||
);
|
||||
}
|
||||
const retag = lowerer.narrowedRetagHelper(call, valueT.unionId, resultT.unionId, loc);
|
||||
const present = lowerer.stripUndefinedArm(resultT);
|
||||
if (
|
||||
retag === null &&
|
||||
!(present.kind === "object" && canAssertClassValue(lowerer, valueT, present))
|
||||
)
|
||||
lowerer.unsupported("SC1090", call, `'.${method}' narrowing to an incompatible result layout`);
|
||||
return retag;
|
||||
}
|
||||
|
||||
/** The find-like predicate results whose ToBoolean has a static answer:
|
||||
* bool passes through; f64/string by value; checked values and
|
||||
* truthy-answerable unions by their runtime kind or arm. */
|
||||
function findPredicateOk(lowerer: Lowerer, fnRet: IrType): boolean {
|
||||
return (
|
||||
fnRet.kind === "bool" ||
|
||||
fnRet.kind === "f64" ||
|
||||
fnRet.kind === "string" ||
|
||||
fnRet.kind === "dyn" ||
|
||||
(fnRet.kind === "union" &&
|
||||
(lowerer.unions.get(fnRet.unionId)?.arms ?? []).every(
|
||||
(a) => a.kind !== "dyn" && a.kind !== "caught",
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/** ToBoolean of a predicate result inside a synthesized HOF helper — the
|
||||
* ensureBool subset the find-like path admits (bool through; checked
|
||||
* values via dynTest; f64/string/truthy-answerable unions via toBool). */
|
||||
@@ -1208,17 +1232,7 @@ function findHelper(
|
||||
const fnT = funcOf([valueT, F64, arrT].slice(0, arity), fnRet);
|
||||
|
||||
const v = varRef("v.0", valueT, loc);
|
||||
const presentArm = lowerer.stripUndefinedArm(resultT);
|
||||
// A successful inferred class predicate may select a descendant of a
|
||||
// stored base arm. Keep the value read before the callback mutated the array.
|
||||
const classView =
|
||||
retag === null && !lowerer.coercibleValue(valueT, resultT) && presentArm.kind === "object"
|
||||
? checkedClassAssertion(lowerer, v, presentArm, loc)
|
||||
: null;
|
||||
const found: IrExpr =
|
||||
retag === null
|
||||
? lowerer.coerceToExpected(classView ?? v, resultT)
|
||||
: { kind: "call", callee: retag, args: [v], type: resultT, loc };
|
||||
const found = findFoundValue(lowerer, v, resultT, retag, loc);
|
||||
const miss: IrExpr = {
|
||||
kind: "unionWrap",
|
||||
unionId: resultT.unionId,
|
||||
@@ -1272,6 +1286,25 @@ function findHelper(
|
||||
return name;
|
||||
}
|
||||
|
||||
/** A found element as find's result union. A successful inferred class
|
||||
* predicate may select a descendant of a stored base arm. The value is the
|
||||
* one read before the callback ran, even if the callback mutated its slot. */
|
||||
function findFoundValue(
|
||||
lowerer: Lowerer,
|
||||
v: IrExpr,
|
||||
resultT: IrType & { kind: "union" },
|
||||
retag: string | null,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
if (retag !== null) return { kind: "call", callee: retag, args: [v], type: resultT, loc };
|
||||
const presentArm = lowerer.stripUndefinedArm(resultT);
|
||||
const classView =
|
||||
!lowerer.coercibleValue(v.type, resultT) && presentArm.kind === "object"
|
||||
? checkedClassAssertion(lowerer, v, presentArm, loc)
|
||||
: null;
|
||||
return lowerer.coerceToExpected(classView ?? v, resultT);
|
||||
}
|
||||
|
||||
/** The findIndex/findLastIndex loop — find's index-returning sibling, no
|
||||
* union; findLastIndex is the identical loop walked backwards:
|
||||
*
|
||||
@@ -2580,3 +2613,200 @@ function buildDynFilterFn(name: string, elemT: IrType, arity: number, loc: SrcLo
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Tuple `filter` / `find` / `findIndex` / `some` / `every`. `reads` holds
|
||||
* one element read per position, in index order, each over the helper's
|
||||
* `a.0` receiver parameter. The helper visits positions in order and reads
|
||||
* each one only when it is visited, so a callback's write to a later
|
||||
* position is visible; filter and find keep the value read before the
|
||||
* callback ran. find, findIndex, some and every return at the first
|
||||
* deciding position. Predicate results and narrowing follow the array
|
||||
* methods' rules. */
|
||||
export function lowerTuplePredicateCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
method: "filter" | "find" | "findIndex" | "some" | "every",
|
||||
receiver: IrExpr,
|
||||
elem: IrType,
|
||||
reads: IrExpr[],
|
||||
): IrExpr {
|
||||
const loc = locOf(call);
|
||||
const argNode = call.arguments[0]!;
|
||||
const booleanFilter =
|
||||
method === "filter" &&
|
||||
ts.isIdentifier(argNode) &&
|
||||
argNode.text === "Boolean" &&
|
||||
lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode) ?? undefined);
|
||||
if (booleanFilter) {
|
||||
if (elem.kind === "union") lowerer.requireTruthyUnion(elem.unionId, argNode);
|
||||
if (elem.kind === "dyn" || elem.kind === "void" || isUnitType(elem)) {
|
||||
lowerer.badType(argNode, lowerer.typeOf(argNode));
|
||||
}
|
||||
}
|
||||
const { fnArg, arity } = booleanFilter
|
||||
? { fnArg: truthyFilterCallback(lowerer, elem, loc), arity: 1 }
|
||||
: lowerArrayCallback(lowerer, argNode, [elem], receiver.type);
|
||||
const fnRet = fnArg.type.ret;
|
||||
if (method === "filter" ? !filterPredicateOk(lowerer, fnRet) : !findPredicateOk(lowerer, fnRet)) {
|
||||
if (fnRet.kind === "void") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
argNode,
|
||||
`'.${method}()' with a void-returning predicate (the callback return value is erased before its truthiness can be tested)`,
|
||||
);
|
||||
}
|
||||
lowerer.badType(argNode, lowerer.typeOf(argNode));
|
||||
}
|
||||
let resultT: IrType;
|
||||
let outElem: IrType = elem;
|
||||
// A narrowing find retags from the element union widened with undefined,
|
||||
// the value type an array find narrows from.
|
||||
let foundT: IrType = elem;
|
||||
let retag: string | null = null;
|
||||
if (method === "filter") {
|
||||
// The element union is built in position order, so the checker's
|
||||
// result element can list the same arms in another order.
|
||||
const checkerResult = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const arms = (type: IrType): IrType[] =>
|
||||
type.kind === "union" ? lowerer.unions.get(type.unionId)!.arms : [type];
|
||||
const unchanged =
|
||||
checkerResult?.kind === "array" &&
|
||||
arms(checkerResult.elem).length === arms(elem).length &&
|
||||
arms(checkerResult.elem).every((arm) => arms(elem).some((other) => typeEquals(arm, other)));
|
||||
outElem = unchanged
|
||||
? elem
|
||||
: filterResultElem(lowerer, call, argNode, elem, elem, booleanFilter);
|
||||
resultT = arrayOf(outElem);
|
||||
} else if (method === "find") {
|
||||
resultT = lowerer.irTypeOf(call);
|
||||
if (resultT.kind !== "union" || lowerer.armTag(resultT.unionId, UNDEFINED_T) < 0) {
|
||||
lowerer.badType(call, lowerer.typeOf(call));
|
||||
}
|
||||
if (!lowerer.coercibleValue(elem, resultT)) foundT = arrayValueType(lowerer, elem);
|
||||
retag = findResultRetag(lowerer, call, argNode, method, foundT, resultT, false);
|
||||
} else {
|
||||
resultT = method === "findIndex" ? F64 : BOOL;
|
||||
}
|
||||
const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(fnArg.type)}:${typeKey(resultT)}:${arity}${callbackSite(fnArg)}`;
|
||||
let helper = lowerer.arrHofHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(key, helper);
|
||||
const v = varRef("v.0", elem, loc);
|
||||
const out = varRef("out.0", resultT, loc);
|
||||
const test = (index: number): IrExpr => {
|
||||
const invocation: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee: varRef("f.0", fnArg.type, loc),
|
||||
args: [v, numLit(index, loc)].slice(0, arity),
|
||||
type: fnRet,
|
||||
loc,
|
||||
};
|
||||
return method === "filter"
|
||||
? filterCond(invocation, fnRet, loc)
|
||||
: predicateToBoolean(invocation);
|
||||
};
|
||||
const decided = (index: number): IrStmt => {
|
||||
switch (method) {
|
||||
case "filter":
|
||||
return arrayValueStore(
|
||||
lowerer,
|
||||
out,
|
||||
{ kind: "arrIntrinsic", method: "length", receiver: out, args: [], type: F64, loc },
|
||||
filterRetainedValue(lowerer, v, elem, outElem, loc),
|
||||
outElem,
|
||||
loc,
|
||||
);
|
||||
case "find":
|
||||
return {
|
||||
kind: "return",
|
||||
value: findFoundValue(
|
||||
lowerer,
|
||||
lowerer.coerceToExpected(v, foundT),
|
||||
resultT as IrType & { kind: "union" },
|
||||
retag,
|
||||
loc,
|
||||
),
|
||||
loc,
|
||||
};
|
||||
case "findIndex":
|
||||
return { kind: "return", value: numLit(index, loc), loc };
|
||||
case "some":
|
||||
return { kind: "return", value: boolLit(true, loc), loc };
|
||||
case "every":
|
||||
return { kind: "return", value: boolLit(false, loc), loc };
|
||||
}
|
||||
};
|
||||
const body: IrStmt[] =
|
||||
method === "filter"
|
||||
? [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "out.0",
|
||||
init: { kind: "arrayLit", elems: [], type: resultT, loc },
|
||||
loc,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
reads.forEach((read, index) => {
|
||||
body.push(
|
||||
index === 0
|
||||
? { kind: "varDecl", localId: "v.0", init: read, loc }
|
||||
: { kind: "assign", localId: "v.0", value: read, loc },
|
||||
{
|
||||
kind: "if",
|
||||
cond:
|
||||
method === "every"
|
||||
? { kind: "unary", op: "!", operand: test(index), type: BOOL, loc }
|
||||
: test(index),
|
||||
then: [decided(index)],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
);
|
||||
});
|
||||
let done: IrExpr;
|
||||
switch (method) {
|
||||
case "filter":
|
||||
done = out;
|
||||
break;
|
||||
case "find": {
|
||||
const unionId = (resultT as IrType & { kind: "union" }).unionId;
|
||||
done = {
|
||||
kind: "unionWrap",
|
||||
unionId,
|
||||
tag: lowerer.armTag(unionId, UNDEFINED_T),
|
||||
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "findIndex":
|
||||
done = numLit(-1, loc);
|
||||
break;
|
||||
default:
|
||||
done = boolLit(method === "every", loc);
|
||||
}
|
||||
body.push({ kind: "return", value: done, loc });
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [
|
||||
{ localId: "a.0", name: "a", type: receiver.type },
|
||||
{ localId: "f.0", name: "f", type: fnArg.type },
|
||||
],
|
||||
returnType: resultT,
|
||||
locals: [
|
||||
{ id: "a.0", name: "a", type: receiver.type, mutable: false },
|
||||
{ id: "f.0", name: "f", type: fnArg.type, mutable: false },
|
||||
...(reads.length > 0 ? [{ id: "v.0", name: "v", type: elem, mutable: true }] : []),
|
||||
...(method === "filter"
|
||||
? [{ id: "out.0", name: "out", type: resultT, mutable: false }]
|
||||
: []),
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultT, loc };
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
lowerArrayFindLikeCall,
|
||||
lowerArrayFlatMapCall,
|
||||
lowerArrayReduceCall,
|
||||
lowerTuplePredicateCall,
|
||||
} from "./array-callbacks.js";
|
||||
import {
|
||||
lowerArraySpreadItems,
|
||||
@@ -1847,12 +1848,19 @@ export function lowerTupleReadMethodCall(
|
||||
if (lowerer.chainBlocked(access, call)) return null;
|
||||
const method = access.name.text;
|
||||
const search = method === "includes" || method === "indexOf" || method === "lastIndexOf";
|
||||
const predicate =
|
||||
method === "filter" ||
|
||||
method === "find" ||
|
||||
method === "findIndex" ||
|
||||
method === "some" ||
|
||||
method === "every";
|
||||
if (
|
||||
method !== "slice" &&
|
||||
method !== "map" &&
|
||||
method !== "flatMap" &&
|
||||
method !== "join" &&
|
||||
!search
|
||||
!search &&
|
||||
!predicate
|
||||
)
|
||||
return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
@@ -2048,6 +2056,9 @@ export function lowerTupleReadMethodCall(
|
||||
"capture the original tuple explicitly to preserve its identity",
|
||||
);
|
||||
}
|
||||
if (predicate) {
|
||||
return lowerTuplePredicateCall(lowerer, call, method, receiver, elem, fields.map(read));
|
||||
}
|
||||
const { fnArg, arity } = lowerArrayCallback(lowerer, callback, [elem], receiver.type);
|
||||
const ret = fnArg.type.ret;
|
||||
if (ret.kind === "void" || ret.kind === "func") lowerer.badType(call, lowerer.typeOf(call));
|
||||
|
||||
@@ -10230,7 +10230,6 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
lowerOptionalNumber(lowerer, operand, loc, node);
|
||||
const arithmeticLeft = strictEquality ? left : arithmeticNumber(left, expr.left);
|
||||
const arithmeticRight = strictEquality ? right : arithmeticNumber(right, expr.right);
|
||||
const plainBothNum = left.type.kind === "f64" && right.type.kind === "f64";
|
||||
const bothNum = arithmeticLeft.type.kind === "f64" && arithmeticRight.type.kind === "f64";
|
||||
const bothStr = left.type.kind === "string" && right.type.kind === "string";
|
||||
|
||||
@@ -10452,234 +10451,17 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
};
|
||||
}
|
||||
case ts.SyntaxKind.EqualsEqualsEqualsToken:
|
||||
case ts.SyntaxKind.ExclamationEqualsEqualsToken: {
|
||||
const negated = op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
|
||||
const indexed = tryLowerIndexedComparison(lowerer, left, right, negated, loc);
|
||||
if (indexed) return indexed;
|
||||
if (plainBothNum)
|
||||
return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
|
||||
if (bothStr) {
|
||||
if (left.kind === "strLit" && right.kind === "strLit")
|
||||
return {
|
||||
kind: "boolLit",
|
||||
value: (left.value === right.value) !== negated,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
return { kind: "strEq", negated, left, right, type: BOOL, loc };
|
||||
}
|
||||
// bool === bool: a plain value compare (the config-drift checks'
|
||||
// `desired.lanMode !== actual.lanMode` shape).
|
||||
if (left.type.kind === "bool" && right.type.kind === "bool") {
|
||||
return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
|
||||
}
|
||||
const unitTest = lowerer.lowerUnitComparison(left, right, negated, loc);
|
||||
if (unitTest) return unitTest;
|
||||
if (left.type.kind === "union" || right.type.kind === "union") {
|
||||
const compared = lowerUnionEquality(lowerer, left, right, negated, false, loc);
|
||||
if (compared) return compared;
|
||||
const aliasRefusal = (type: IrType): void => {
|
||||
if (tagEqualityMayMissAlias(lowerer, type))
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`comparisons of related class values beside structural union members (${NARROW_FIRST})`,
|
||||
);
|
||||
};
|
||||
|
||||
// JS strict equality of the ARM values, per union tag (a
|
||||
// per-union helper): equal tags compare payloads (f64 ==, string
|
||||
// bytes, bool ==, ref-arm POINTER identity), different tags are
|
||||
// never equal. Two shapes lower: same union on both sides, and
|
||||
// union vs plain arm value (`u === "text"` where the checker
|
||||
// didn't narrow) — the plain side wraps into the union, which
|
||||
// preserves payload identity. Different unions (tsc admits
|
||||
// partially-overlapping ones) still need narrowing first.
|
||||
const ut =
|
||||
left.type.kind === "union" ? left.type : (right.type as IrType & { kind: "union" });
|
||||
const bothUnion = left.type.kind === "union" && right.type.kind === "union";
|
||||
const sameUnion = bothUnion && typeEquals(left.type, right.type);
|
||||
const plainFunc =
|
||||
left.type.kind === "func" ? left : right.type.kind === "func" ? right : null;
|
||||
const unionArms = lowerer.unions.get(ut.unionId)?.arms ?? [];
|
||||
const funcArm = unionArms.find((arm) => arm.kind === "func");
|
||||
// `listeners()` reports original prefix callbacks, but the
|
||||
// checker views an indexed result as function | undefined. For
|
||||
// identity comparison, retain the original closure pointer rather
|
||||
// than adapting it to the tuple signature.
|
||||
if (
|
||||
!bothUnion &&
|
||||
plainFunc !== null &&
|
||||
funcArm?.kind === "func" &&
|
||||
unionArms.filter((arm) => arm.kind === "func").length === 1 &&
|
||||
unionArms.some(isUnitType)
|
||||
) {
|
||||
return {
|
||||
kind: "unionFuncEq",
|
||||
unionId: ut.unionId,
|
||||
tag: lowerer.armTag(ut.unionId, funcArm),
|
||||
union: left.type.kind === "union" ? left : right,
|
||||
func: plainFunc,
|
||||
negated,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if ((sameUnion || !bothUnion) && lowerer.eqComparableUnion(ut.unionId)) {
|
||||
aliasRefusal(ut);
|
||||
return {
|
||||
kind: "unionEq",
|
||||
unionId: ut.unionId,
|
||||
negated,
|
||||
sameValue: false,
|
||||
left: lowerer.coerceInto(expr.left, left, ut),
|
||||
right: lowerer.coerceInto(expr.right, right, ut),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (left.type.kind === "union" && right.type.kind === "union") {
|
||||
const leftDef = lowerer.unions.get(left.type.unionId);
|
||||
const rightDef = lowerer.unions.get(right.type.unionId);
|
||||
if (leftDef && rightDef) {
|
||||
const leftValues = leftDef.arms.filter((arm) => !isUnitType(arm));
|
||||
const rightValues = rightDef.arms.filter((arm) => !isUnitType(arm));
|
||||
const sameValueArms =
|
||||
leftValues.length === rightValues.length &&
|
||||
leftValues.every((arm) =>
|
||||
rightValues.some((candidate) => typeEquals(candidate, arm)),
|
||||
);
|
||||
if (sameValueArms) {
|
||||
const arms = [...leftDef.arms];
|
||||
for (const arm of rightDef.arms) {
|
||||
if (!arms.some((candidate) => typeEquals(candidate, arm))) arms.push(arm);
|
||||
}
|
||||
arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
|
||||
const common: IrType & { kind: "union" } = {
|
||||
kind: "union",
|
||||
unionId: lowerer.unions.intern(arms),
|
||||
};
|
||||
aliasRefusal(common);
|
||||
return {
|
||||
kind: "unionEq",
|
||||
unionId: common.unionId,
|
||||
negated,
|
||||
sameValue: false,
|
||||
left: lowerer.coerceInto(expr.left, left, common),
|
||||
right: lowerer.coerceInto(expr.right, right, common),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
lowerer.unsupported("SC1090", expr, `comparisons of union-typed values (${NARROW_FIRST})`);
|
||||
}
|
||||
// Arrays, maps, functions, class instances and records compare by
|
||||
// reference identity (pointer compare) — JS-exact object equality.
|
||||
// Hierarchy-related classes compare after widening the derived side
|
||||
// (same pointer either way — prefix layout).
|
||||
let idLeft = left;
|
||||
let idRight = right;
|
||||
if (left.type.kind === "object" && right.type.kind === "object") {
|
||||
if (lowerer.isSubclassOf(left.type.className, right.type.className)) {
|
||||
idLeft = lowerer.upcastTo(left, right.type.className);
|
||||
} else if (lowerer.isSubclassOf(right.type.className, left.type.className)) {
|
||||
idRight = lowerer.upcastTo(right, left.type.className);
|
||||
}
|
||||
}
|
||||
// Two function values compare by identity whatever their STATIC
|
||||
// signatures (tsc admits the comparison when one side is
|
||||
// assignable to the other — a tuple-typed listeners() element
|
||||
// against a prefix-declared handler): the pointer answers it.
|
||||
if (idLeft.type.kind === "func" && idRight.type.kind === "func") {
|
||||
return {
|
||||
kind: "bin",
|
||||
op: negated ? "!==" : "===",
|
||||
left: idLeft,
|
||||
right: idRight,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// Two CLASS values compare by identity whatever their static
|
||||
// classes — one immortal object per class, one pointer compare
|
||||
// (the function-identity rule verbatim; `X === D` through a
|
||||
// base-typed slot answers exactly JS).
|
||||
if (idLeft.type.kind === "classval" && idRight.type.kind === "classval") {
|
||||
return {
|
||||
kind: "bin",
|
||||
op: negated ? "!==" : "===",
|
||||
left: idLeft,
|
||||
right: idRight,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (idLeft.type.kind === "moduleNs" && idRight.type.kind === "moduleNs") {
|
||||
return {
|
||||
kind: "bin",
|
||||
op: negated ? "!==" : "===",
|
||||
left: idLeft,
|
||||
right: idRight,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (
|
||||
(idLeft.type.kind === "array" ||
|
||||
idLeft.type.kind === "map" ||
|
||||
idLeft.type.kind === "set" ||
|
||||
idLeft.type.kind === "regex" ||
|
||||
idLeft.type.kind === "url" ||
|
||||
idLeft.type.kind === "object" ||
|
||||
idLeft.type.kind === "record" ||
|
||||
// Symbols ARE identity: `Symbol('a') === Symbol('a')` is false,
|
||||
// a symbol equals exactly itself, and Symbol.for's interned
|
||||
// values compare equal across call sites — all one pointer
|
||||
// compare, JS's spec without approximation.
|
||||
idLeft.type.kind === "symbol" ||
|
||||
// Typed arrays / Buffers ARE objects to ===: pointer identity
|
||||
// (buf === buf.swap16() — the in-place mutators return this).
|
||||
idLeft.type.kind === "bytes" ||
|
||||
idLeft.type.kind === "promise") &&
|
||||
typeEquals(idLeft.type, idRight.type)
|
||||
) {
|
||||
return {
|
||||
kind: "bin",
|
||||
op: negated ? "!==" : "===",
|
||||
left: idLeft,
|
||||
right: idRight,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// Runtime HANDLES are objects to === too: one handle per socket/
|
||||
// request/response, so pointer identity IS JS's object equality
|
||||
// (`c.pause() === c` — Node's chaining assertions). */
|
||||
if (DYN_HANDLE_KINDS.has(idLeft.type.kind) && typeEquals(idLeft.type, idRight.type)) {
|
||||
return {
|
||||
kind: "bin",
|
||||
op: negated ? "!==" : "===",
|
||||
left: idLeft,
|
||||
right: idRight,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (lowerer.dynConvertible(idLeft.type) && lowerer.dynConvertible(idRight.type)) {
|
||||
return {
|
||||
kind: "dynScalarEq",
|
||||
left: lowerer.coerceToExpected(idLeft, DYN),
|
||||
right: lowerer.coerceToExpected(idRight, DYN),
|
||||
...(negated ? { negated: true as const } : {}),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
lowerer.unsupported("SC1043", expr);
|
||||
break;
|
||||
}
|
||||
case ts.SyntaxKind.ExclamationEqualsEqualsToken:
|
||||
return lowerStrictEquality(
|
||||
lowerer,
|
||||
expr,
|
||||
expr.left,
|
||||
expr.right,
|
||||
left,
|
||||
right,
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
||||
loc,
|
||||
);
|
||||
case ts.SyntaxKind.LessThanToken:
|
||||
case ts.SyntaxKind.LessThanEqualsToken:
|
||||
case ts.SyntaxKind.GreaterThanToken:
|
||||
@@ -10728,6 +10510,243 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
);
|
||||
}
|
||||
|
||||
/** `left === right` (`!==` when negated) over lowered operands: JS strict
|
||||
* equality for every pair of static kinds the program can compare.
|
||||
* `node` anchors diagnostics, and `leftNode`/`rightNode` the coercions. */
|
||||
export function lowerStrictEquality(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Node,
|
||||
leftNode: ts.Node,
|
||||
rightNode: ts.Node,
|
||||
left: IrExpr,
|
||||
right: IrExpr,
|
||||
negated: boolean,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const plainBothNum = left.type.kind === "f64" && right.type.kind === "f64";
|
||||
const bothStr = left.type.kind === "string" && right.type.kind === "string";
|
||||
const indexed = tryLowerIndexedComparison(lowerer, left, right, negated, loc);
|
||||
if (indexed) return indexed;
|
||||
if (plainBothNum)
|
||||
return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
|
||||
if (bothStr) {
|
||||
if (left.kind === "strLit" && right.kind === "strLit")
|
||||
return {
|
||||
kind: "boolLit",
|
||||
value: (left.value === right.value) !== negated,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
return { kind: "strEq", negated, left, right, type: BOOL, loc };
|
||||
}
|
||||
// bool === bool: a plain value compare (the config-drift checks'
|
||||
// `desired.lanMode !== actual.lanMode` shape).
|
||||
if (left.type.kind === "bool" && right.type.kind === "bool") {
|
||||
return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
|
||||
}
|
||||
const unitTest = lowerer.lowerUnitComparison(left, right, negated, loc);
|
||||
if (unitTest) return unitTest;
|
||||
if (left.type.kind === "union" || right.type.kind === "union") {
|
||||
const compared = lowerUnionEquality(lowerer, left, right, negated, false, loc);
|
||||
if (compared) return compared;
|
||||
const aliasRefusal = (type: IrType): void => {
|
||||
if (tagEqualityMayMissAlias(lowerer, type))
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
`comparisons of related class values beside structural union members (${NARROW_FIRST})`,
|
||||
);
|
||||
};
|
||||
|
||||
// JS strict equality of the ARM values, per union tag (a
|
||||
// per-union helper): equal tags compare payloads (f64 ==, string
|
||||
// bytes, bool ==, ref-arm POINTER identity), different tags are
|
||||
// never equal. Two shapes lower: same union on both sides, and
|
||||
// union vs plain arm value (`u === "text"` where the checker
|
||||
// didn't narrow) — the plain side wraps into the union, which
|
||||
// preserves payload identity. Different unions (tsc admits
|
||||
// partially-overlapping ones) still need narrowing first.
|
||||
const ut = left.type.kind === "union" ? left.type : (right.type as IrType & { kind: "union" });
|
||||
const bothUnion = left.type.kind === "union" && right.type.kind === "union";
|
||||
const sameUnion = bothUnion && typeEquals(left.type, right.type);
|
||||
const plainFunc = left.type.kind === "func" ? left : right.type.kind === "func" ? right : null;
|
||||
const unionArms = lowerer.unions.get(ut.unionId)?.arms ?? [];
|
||||
const funcArm = unionArms.find((arm) => arm.kind === "func");
|
||||
// `listeners()` reports original prefix callbacks, but the
|
||||
// checker views an indexed result as function | undefined. For
|
||||
// identity comparison, retain the original closure pointer rather
|
||||
// than adapting it to the tuple signature.
|
||||
if (
|
||||
!bothUnion &&
|
||||
plainFunc !== null &&
|
||||
funcArm?.kind === "func" &&
|
||||
unionArms.filter((arm) => arm.kind === "func").length === 1 &&
|
||||
unionArms.some(isUnitType)
|
||||
) {
|
||||
return {
|
||||
kind: "unionFuncEq",
|
||||
unionId: ut.unionId,
|
||||
tag: lowerer.armTag(ut.unionId, funcArm),
|
||||
union: left.type.kind === "union" ? left : right,
|
||||
func: plainFunc,
|
||||
negated,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if ((sameUnion || !bothUnion) && lowerer.eqComparableUnion(ut.unionId)) {
|
||||
aliasRefusal(ut);
|
||||
return {
|
||||
kind: "unionEq",
|
||||
unionId: ut.unionId,
|
||||
negated,
|
||||
sameValue: false,
|
||||
left: lowerer.coerceInto(leftNode, left, ut),
|
||||
right: lowerer.coerceInto(rightNode, right, ut),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (left.type.kind === "union" && right.type.kind === "union") {
|
||||
const leftDef = lowerer.unions.get(left.type.unionId);
|
||||
const rightDef = lowerer.unions.get(right.type.unionId);
|
||||
if (leftDef && rightDef) {
|
||||
const leftValues = leftDef.arms.filter((arm) => !isUnitType(arm));
|
||||
const rightValues = rightDef.arms.filter((arm) => !isUnitType(arm));
|
||||
const sameValueArms =
|
||||
leftValues.length === rightValues.length &&
|
||||
leftValues.every((arm) => rightValues.some((candidate) => typeEquals(candidate, arm)));
|
||||
if (sameValueArms) {
|
||||
const arms = [...leftDef.arms];
|
||||
for (const arm of rightDef.arms) {
|
||||
if (!arms.some((candidate) => typeEquals(candidate, arm))) arms.push(arm);
|
||||
}
|
||||
arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
|
||||
const common: IrType & { kind: "union" } = {
|
||||
kind: "union",
|
||||
unionId: lowerer.unions.intern(arms),
|
||||
};
|
||||
aliasRefusal(common);
|
||||
return {
|
||||
kind: "unionEq",
|
||||
unionId: common.unionId,
|
||||
negated,
|
||||
sameValue: false,
|
||||
left: lowerer.coerceInto(leftNode, left, common),
|
||||
right: lowerer.coerceInto(rightNode, right, common),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
lowerer.unsupported("SC1090", node, `comparisons of union-typed values (${NARROW_FIRST})`);
|
||||
}
|
||||
// Arrays, maps, functions, class instances and records compare by
|
||||
// reference identity (pointer compare) — JS-exact object equality.
|
||||
// Hierarchy-related classes compare after widening the derived side
|
||||
// (same pointer either way — prefix layout).
|
||||
let idLeft = left;
|
||||
let idRight = right;
|
||||
if (left.type.kind === "object" && right.type.kind === "object") {
|
||||
if (lowerer.isSubclassOf(left.type.className, right.type.className)) {
|
||||
idLeft = lowerer.upcastTo(left, right.type.className);
|
||||
} else if (lowerer.isSubclassOf(right.type.className, left.type.className)) {
|
||||
idRight = lowerer.upcastTo(right, left.type.className);
|
||||
}
|
||||
}
|
||||
// Two function values compare by identity whatever their STATIC
|
||||
// signatures (tsc admits the comparison when one side is
|
||||
// assignable to the other — a tuple-typed listeners() element
|
||||
// against a prefix-declared handler): the pointer answers it.
|
||||
if (idLeft.type.kind === "func" && idRight.type.kind === "func") {
|
||||
return {
|
||||
kind: "bin",
|
||||
op: negated ? "!==" : "===",
|
||||
left: idLeft,
|
||||
right: idRight,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// Two CLASS values compare by identity whatever their static
|
||||
// classes — one immortal object per class, one pointer compare
|
||||
// (the function-identity rule verbatim; `X === D` through a
|
||||
// base-typed slot answers exactly JS).
|
||||
if (idLeft.type.kind === "classval" && idRight.type.kind === "classval") {
|
||||
return {
|
||||
kind: "bin",
|
||||
op: negated ? "!==" : "===",
|
||||
left: idLeft,
|
||||
right: idRight,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (idLeft.type.kind === "moduleNs" && idRight.type.kind === "moduleNs") {
|
||||
return {
|
||||
kind: "bin",
|
||||
op: negated ? "!==" : "===",
|
||||
left: idLeft,
|
||||
right: idRight,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (
|
||||
(idLeft.type.kind === "array" ||
|
||||
idLeft.type.kind === "map" ||
|
||||
idLeft.type.kind === "set" ||
|
||||
idLeft.type.kind === "regex" ||
|
||||
idLeft.type.kind === "url" ||
|
||||
idLeft.type.kind === "object" ||
|
||||
idLeft.type.kind === "record" ||
|
||||
// Symbols ARE identity: `Symbol('a') === Symbol('a')` is false,
|
||||
// a symbol equals exactly itself, and Symbol.for's interned
|
||||
// values compare equal across call sites — all one pointer
|
||||
// compare, JS's spec without approximation.
|
||||
idLeft.type.kind === "symbol" ||
|
||||
// Typed arrays / Buffers ARE objects to ===: pointer identity
|
||||
// (buf === buf.swap16() — the in-place mutators return this).
|
||||
idLeft.type.kind === "bytes" ||
|
||||
idLeft.type.kind === "promise") &&
|
||||
typeEquals(idLeft.type, idRight.type)
|
||||
) {
|
||||
return {
|
||||
kind: "bin",
|
||||
op: negated ? "!==" : "===",
|
||||
left: idLeft,
|
||||
right: idRight,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// Runtime HANDLES are objects to === too: one handle per socket/
|
||||
// request/response, so pointer identity IS JS's object equality
|
||||
// (`c.pause() === c` — Node's chaining assertions). */
|
||||
if (DYN_HANDLE_KINDS.has(idLeft.type.kind) && typeEquals(idLeft.type, idRight.type)) {
|
||||
return {
|
||||
kind: "bin",
|
||||
op: negated ? "!==" : "===",
|
||||
left: idLeft,
|
||||
right: idRight,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (lowerer.dynConvertible(idLeft.type) && lowerer.dynConvertible(idRight.type)) {
|
||||
return {
|
||||
kind: "dynScalarEq",
|
||||
left: lowerer.coerceToExpected(idLeft, DYN),
|
||||
right: lowerer.coerceToExpected(idRight, DYN),
|
||||
...(negated ? { negated: true as const } : {}),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return lowerer.unsupported("SC1043", node);
|
||||
}
|
||||
|
||||
function lowerLogicalChain(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr {
|
||||
// Logical chains have a left-nested AST. Keep the native stack bounded
|
||||
// while retaining each pair's checker type and lazy operand evaluation.
|
||||
|
||||
@@ -164,7 +164,11 @@ import {
|
||||
} from "./lower-exprs.js";
|
||||
import { globalSymbolKey } from "./expressions/global-symbols.js";
|
||||
import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js";
|
||||
import { lowerEnvironmentKey, lowerNativeFunctionAssignment } from "./lower-exprs.js";
|
||||
import {
|
||||
lowerEnvironmentKey,
|
||||
lowerNativeFunctionAssignment,
|
||||
lowerStrictEquality,
|
||||
} from "./lower-exprs.js";
|
||||
import { isNativeProxyInitializer } from "./expressions/native-proxy.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerUnionFieldWrite } from "./expressions/union-fields.js";
|
||||
@@ -5156,9 +5160,21 @@ export function lowerSwitch(lowerer: Lowerer, stmt: ts.SwitchStatement): IrStmt
|
||||
const dk = disc.type.kind;
|
||||
if (dk === "dyn") return lowerDynSwitch(lowerer, stmt, disc, labels);
|
||||
if (dk === "union") return lowerUnionSwitch(lowerer, stmt, disc, labels);
|
||||
if (dk !== "f64" && dk !== "string" && dk !== "bool") {
|
||||
lowerer.unsupported("SC1090", stmt.expression, "switch on non-primitive values");
|
||||
}
|
||||
// Objects, arrays, functions and other references match their cases
|
||||
// with ===, which compares them by identity.
|
||||
if (dk !== "f64" && dk !== "string" && dk !== "bool")
|
||||
return lowerBranchSwitch(lowerer, stmt, disc, labels, (stableDisc, test, expression) =>
|
||||
lowerStrictEquality(
|
||||
lowerer,
|
||||
expression,
|
||||
stmt.expression,
|
||||
expression,
|
||||
stableDisc,
|
||||
test,
|
||||
false,
|
||||
locOf(expression),
|
||||
),
|
||||
);
|
||||
// The whole case-body sequence is ONE lexical scope in JS: a bare `let`
|
||||
// in one case is visible in later cases.
|
||||
lowerer.scopes.push(new Map());
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
import {
|
||||
unionRetagMappable,
|
||||
narrowedRetagHelper,
|
||||
uninhabitedArmRetagHelper,
|
||||
strandedUnitTrap,
|
||||
strandedCoercionTrap,
|
||||
recordUnionWrapHelper,
|
||||
@@ -2821,10 +2822,33 @@ export class Lowerer {
|
||||
}
|
||||
|
||||
/** Declared class fields may store a DYN value only when its static type
|
||||
* names checked protocol identity or an owned runtime handle (a Worker
|
||||
* or MessagePort); `unknown` and `any` fields stay refused. */
|
||||
* is `unknown` or `object` (also with `null` or `undefined`, or an array
|
||||
* of those), names checked protocol identity, or is an owned runtime
|
||||
* handle (a Worker or MessagePort); `any` fields stay refused. */
|
||||
classFieldStoresDyn(type: ts.Type): boolean {
|
||||
return this.classImplementedProtocol(type) || isWorkerHandleType(type, this.typeCtx);
|
||||
return (
|
||||
this.opaqueValueType(type) ||
|
||||
this.classImplementedProtocol(type) ||
|
||||
isWorkerHandleType(type, this.typeCtx)
|
||||
);
|
||||
}
|
||||
|
||||
/** `unknown`, `object`, an array of those, or a union of those with
|
||||
* `null` and `undefined`: types whose values use checked-dynamic storage
|
||||
* as they do in JavaScript class fields. */
|
||||
private opaqueValueType(type: ts.Type): boolean {
|
||||
const opaque = (part: ts.Type): boolean =>
|
||||
(part.flags & (ts.TypeFlags.Unknown | ts.TypeFlags.NonPrimitive)) !== 0 ||
|
||||
(this.checker.isArrayType(part) &&
|
||||
this.checker.getTypeArguments(part as ts.TypeReference).every(opaque));
|
||||
if (!type.isUnionType()) return opaque(type);
|
||||
const parts = ts.constituentTypes(type);
|
||||
return (
|
||||
parts.some(opaque) &&
|
||||
parts.every(
|
||||
(part) => (part.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0 || opaque(part),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** The nominal identity of a checker module-namespace type. Program
|
||||
@@ -4124,11 +4148,35 @@ export class Lowerer {
|
||||
// Such a binding reads `T | undefined`, so a typed parameter, local or
|
||||
// field it flows into widens like any other missing element read.
|
||||
const holeyIterationSymbols = new Set<ts.Symbol>();
|
||||
// `xs[i] as number` and `stack.pop() as number`: an assertion to a type
|
||||
// without undefined checks the value (lowerAsExpression narrows to the
|
||||
// asserted type and throws a TypeError for undefined), so its result is
|
||||
// never undefined.
|
||||
const assertsDefined = (node: ts.Expression): boolean => {
|
||||
let e = node;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isAsExpression(e) && !ts.isTypeAssertion(e)) return false;
|
||||
const asserted = typeOrNull(e);
|
||||
const admits = (type: ts.Type): boolean =>
|
||||
(type.flags &
|
||||
(ts.TypeFlags.Any |
|
||||
ts.TypeFlags.Unknown |
|
||||
ts.TypeFlags.Undefined |
|
||||
ts.TypeFlags.Void |
|
||||
ts.TypeFlags.TypeParameter)) !==
|
||||
0;
|
||||
return (
|
||||
asserted !== null &&
|
||||
!admits(asserted) &&
|
||||
!(asserted.isUnionType() && ts.constituentTypes(asserted).some(admits))
|
||||
);
|
||||
};
|
||||
const mayBeOptional = (node: ts.Expression): boolean => {
|
||||
// `xs[i]!` is the explicit proven-present form. Its array read keeps
|
||||
// the established dense bounds trap and must not promote the enclosing
|
||||
// function/global ABI to `T | undefined`.
|
||||
if (explicitlyNonNull(node)) return false;
|
||||
if (assertsDefined(node)) return false;
|
||||
const e = peel(node);
|
||||
if (isIndexedRead(e)) return true;
|
||||
if (ts.isPropertyAccessExpression(e) && isOptionalProcessStreamProperty(this, e)) return true;
|
||||
@@ -8420,7 +8468,9 @@ export class Lowerer {
|
||||
// node's CHECKER type proves those arms away, they trap instead — the
|
||||
// sub-union narrowing bridge (narrowedRetagHelper).
|
||||
if (e.type.kind === "union" && expected.kind === "union" && !typeEquals(e.type, expected)) {
|
||||
const helper = this.narrowedRetagHelper(node, e.type.unionId, expected.unionId, e.loc);
|
||||
const helper =
|
||||
this.narrowedRetagHelper(node, e.type.unionId, expected.unionId, e.loc) ??
|
||||
uninhabitedArmRetagHelper(this, node, e.type.unionId, expected.unionId, e.loc);
|
||||
if (helper) {
|
||||
e = { kind: "call", callee: helper, args: [e], type: expected, loc: e.loc };
|
||||
}
|
||||
|
||||
@@ -102,3 +102,26 @@ test("preserves the original failure and continues cleanup when a disposer throw
|
||||
expect(opened).toHaveLength(3);
|
||||
for (const load of opened) expect(load.dispose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("reports program type errors instead of refusing the package", () => {
|
||||
// The program fails to typecheck against the package's inferred surface
|
||||
// and against its declarations alike: the errors are the program's.
|
||||
preflight.mockReturnValueOnce([]).mockReturnValue([typeError]);
|
||||
const frontend = runFrontend("/main.ts", loader, ["fixture"]);
|
||||
expect(frontend.npmStatic).toEqual([]);
|
||||
expect(frontend.preflight).toEqual([typeError]);
|
||||
frontend.dispose();
|
||||
});
|
||||
|
||||
test("reports program type errors that name the package's module", () => {
|
||||
const missingExport: ScrDiagnostic = {
|
||||
code: "SC0001",
|
||||
message: `Module '"fixture"' has no exported member 'open'.`,
|
||||
loc: { file: "/main.ts", start: 9, end: 13 },
|
||||
};
|
||||
preflight.mockReturnValueOnce([]).mockReturnValue([missingExport]);
|
||||
const frontend = runFrontend("/main.ts", loader, ["fixture"]);
|
||||
expect(frontend.npmStatic).toEqual([]);
|
||||
expect(frontend.preflight).toEqual([missingExport]);
|
||||
frontend.dispose();
|
||||
});
|
||||
|
||||
@@ -337,6 +337,9 @@ function loadFrontend(
|
||||
}
|
||||
}
|
||||
const effective = new Set(requested);
|
||||
// Packages refused only because the program does not typecheck against
|
||||
// their inferred surface (no offender, no package-anchored diagnostic).
|
||||
const typecheckRefused = new Set<string>();
|
||||
while (effective.size > 0) {
|
||||
const reasons = new Map<string, string>(npmStaticOffenders());
|
||||
for (const d of preflight) {
|
||||
@@ -352,6 +355,7 @@ function loadFrontend(
|
||||
}
|
||||
}
|
||||
for (const [pkg, count] of named) {
|
||||
typecheckRefused.add(pkg);
|
||||
reasons.set(
|
||||
pkg,
|
||||
`its inferred export surface breaks ${count} import site${count === 1 ? "" : "s"} in program files (bundler-emitted surfaces type only as far as inference reaches)`,
|
||||
@@ -384,6 +388,7 @@ function loadFrontend(
|
||||
if (effective.size > 0 && preflight.some((d) => d.code === "SC0001")) {
|
||||
const dropWithNote = (p: string): void => {
|
||||
effective.delete(p);
|
||||
typecheckRefused.add(p);
|
||||
statuses.push({
|
||||
package: p,
|
||||
status: "refused",
|
||||
@@ -418,6 +423,16 @@ function loadFrontend(
|
||||
for (const p of requested) {
|
||||
if (effective.has(p)) statuses.push({ package: p, status: "static" });
|
||||
}
|
||||
// The final program types the refused packages by their own
|
||||
// declarations. If it still fails to typecheck, the type errors are the
|
||||
// program's, not the packages': report them at their own locations
|
||||
// instead of a package refusal that hides them.
|
||||
if (typecheckRefused.size > 0 && preflight.some((d) => d.code === "SC0001")) {
|
||||
for (let i = statuses.length - 1; i >= 0; i--) {
|
||||
const s = statuses[i]!;
|
||||
if (s.status === "refused" && typecheckRefused.has(s.package)) statuses.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const finalLoad = load;
|
||||
return {
|
||||
|
||||
@@ -221,3 +221,29 @@ console.log(describe(items[4]!));
|
||||
`);
|
||||
expect(signature(unproven, "describe")).toBe("describe(Item|undefined): string");
|
||||
});
|
||||
|
||||
test("a type assertion keeps an unchecked read from widening its callee", async () => {
|
||||
const module = await lower(`
|
||||
function hot(n: number): number {
|
||||
return (n * 2) | 0;
|
||||
}
|
||||
function cold(xs: number[]): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < xs.length; i++) s = (s + hot(xs[i] as number)) | 0;
|
||||
return s;
|
||||
}
|
||||
function drain(stack: number[]): number {
|
||||
let s = 0;
|
||||
while (stack.length > 0) s = (s + hot(stack.pop() as number)) | 0;
|
||||
return s;
|
||||
}
|
||||
function lookup(m: Map<string, number>): number {
|
||||
return hot(<number>m.get("a"));
|
||||
}
|
||||
console.log(cold([1, 2, 3]), drain([4, 5]), lookup(new Map([["a", 6]])));
|
||||
`);
|
||||
expect(signature(module, "hot")).toBe("hot(f64): f64");
|
||||
const llvm = emitLlvmModule(module);
|
||||
for (const caller of ["cold", "drain", "lookup"])
|
||||
expect(body(llvm, caller)).not.toContain("@scr_union_new");
|
||||
});
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5920-promise-try-args.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5921-promise-try-args-js.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5927-switch-object-identity.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5931-asserted-reads-keep-number-parameters.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5933-class-fields-unknown-object.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5950-tuple-predicate-methods.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5951-result-never-widening.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/npm-program-type-error/main.ts"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Module '\"sqlish\"' has no exported member 'open'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/npm-program-type-error/main.ts",
|
||||
"start": 138,
|
||||
"end": 142
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Expected 2 arguments, but got 1.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/npm-program-type-error/main.ts",
|
||||
"start": 239,
|
||||
"end": 244
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
// net.SocketAddress (options, defaults, parse(), toJSON() and Node's
|
||||
// validation errors) and net.BlockList (address, range, subnet and CIDR
|
||||
// rules, removal, the rule listing order, check() across IPv4 and IPv6,
|
||||
// toJSON()/fromJSON() and PRIVATE_RANGES).
|
||||
// toJSON()/fromJSON() and PRIVATE_RANGES). Node lists the address rules
|
||||
// first, in the iteration order of a C++ unordered_map (which differs between
|
||||
// platforms), then the subnets and the ranges, each newest first; the program
|
||||
// sorts the address rules before printing.
|
||||
import * as net from "node:net";
|
||||
import { inspect } from "node:util";
|
||||
|
||||
function ordered(rules: readonly string[]): string[] {
|
||||
const addresses = rules.filter((rule) => rule.startsWith("Address: "));
|
||||
return [...addresses.sort(), ...rules.slice(addresses.length)];
|
||||
}
|
||||
|
||||
function show(label: string, f: () => unknown): void {
|
||||
try {
|
||||
const v = f();
|
||||
@@ -43,9 +51,13 @@ list.addRange("10.0.0.1", "10.0.0.5");
|
||||
list.addSubnet("192.168.0.0", 16);
|
||||
list.addCIDR("fe80::/10");
|
||||
list.addAddresses(["7.7.7.7", sa]);
|
||||
show("rules", () => list.rules);
|
||||
show("rules", () => ordered(list.rules));
|
||||
show("size", () => list.size);
|
||||
show("inspect", () => list);
|
||||
const single = new net.BlockList();
|
||||
single.addAddress("3.3.3.3");
|
||||
single.addRange("10.0.0.1", "10.0.0.5");
|
||||
single.addSubnet("192.168.0.0", 16);
|
||||
show("inspect", () => single);
|
||||
show("check", () => [
|
||||
list.check("1.2.3.4"),
|
||||
list.check("10.0.0.3"),
|
||||
@@ -66,7 +78,7 @@ show("bad cidr", () => list.addCIDR("10.0.0.0"));
|
||||
list.removeAddress("2.2.2.2");
|
||||
list.removeRange("10.0.0.1", "10.0.0.5");
|
||||
list.removeCIDR("fe80::/10");
|
||||
show("after removal", () => list.rules);
|
||||
show("after removal", () => ordered(list.rules));
|
||||
|
||||
const mapped = new net.BlockList();
|
||||
mapped.addSubnet("::", 64, "ipv6");
|
||||
@@ -74,7 +86,7 @@ show("mapped", () => [mapped.check("9.9.9.9"), mapped.check("::1:0:0:0:1", "ipv6
|
||||
|
||||
const copy = new net.BlockList();
|
||||
copy.fromJSON(list.toJSON());
|
||||
show("fromJSON", () => copy.rules);
|
||||
show("fromJSON", () => ordered(copy.rules));
|
||||
const parsed = new net.BlockList();
|
||||
parsed.fromJSON(JSON.stringify(["Subnet: IPv6 ::/64", "x Address: IPv4 9.9.9.9 y", "Range: IPv4 1.1.1.1-1.1.1.2"]));
|
||||
show("fromJSON string", () => parsed.rules);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Promise.try(f, ...args): the arguments are evaluated at the call and passed
|
||||
// to f, which runs synchronously; throws reject and returned promises are
|
||||
// adopted.
|
||||
Promise.try(() => 5).then((v) => console.log("value", v));
|
||||
Promise.try((): number => { throw new Error("sync"); }).catch((e) => console.log("caught", (e as Error).message));
|
||||
Promise.try(() => Promise.resolve("inner")).then((v) => console.log("adopted", v));
|
||||
Promise.try(async () => 7).then((v) => console.log("async", v));
|
||||
const order: string[] = [];
|
||||
const p = Promise.try(() => { order.push("run"); return 1; });
|
||||
order.push("after");
|
||||
p.then(() => console.log(order.join(",")));
|
||||
Promise.try((a: number, b: number) => a + b, 1, 2).then((v) => console.log("args", v));
|
||||
Promise.try(() => undefined).then((v) => console.log("void", v));
|
||||
const log: string[] = [];
|
||||
const v = (x: number): number => { log.push("arg" + x); return x; };
|
||||
const q = Promise.try((a: number, b: number) => { log.push("call"); return a + b; }, v(1), v(2));
|
||||
log.push("after");
|
||||
q.then((r) => console.log(r, log.join(",")));
|
||||
Promise.try((s: string) => { throw new Error(s); }, "bad").catch((e) => console.log("caught", (e as Error).message));
|
||||
Promise.try((n: number) => Promise.resolve(n * 2), 21).then((r) => console.log("adopted", r));
|
||||
function named(this: void, x: number): number { return x + 1; }
|
||||
Promise.try(named, 4).then((r) => console.log("named", r));
|
||||
@@ -0,0 +1,7 @@
|
||||
// Promise.try(f, ...args) in JavaScript: unannotated parameters take the
|
||||
// passed values.
|
||||
Promise.try((a, b) => a * b, 6, 7).then((v) => console.log("product", v));
|
||||
Promise.try((s) => s.toUpperCase(), "js").then((v) => console.log(v));
|
||||
Promise.try((x) => {
|
||||
throw new RangeError("r" + x);
|
||||
}, 1).catch((e) => console.log(e.name, e.message));
|
||||
@@ -0,0 +1,71 @@
|
||||
// switch over objects, arrays, records and functions matches cases with ===:
|
||||
// by identity, with case expressions evaluated lazily in source order.
|
||||
class Mode {
|
||||
readonly name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
const fast = new Mode("fast");
|
||||
const slow = new Mode("slow");
|
||||
function label(mode: Mode | undefined): string {
|
||||
switch (mode) {
|
||||
case fast:
|
||||
return "fast";
|
||||
case slow:
|
||||
return "slow";
|
||||
case undefined:
|
||||
return "none";
|
||||
default:
|
||||
return "other";
|
||||
}
|
||||
}
|
||||
console.log(label(fast), label(slow), label(undefined), label(new Mode("fast")));
|
||||
type Point = { x: number };
|
||||
const origin: Point = { x: 0 };
|
||||
function where(p: Point): string {
|
||||
switch (p) {
|
||||
case origin:
|
||||
return "origin";
|
||||
default:
|
||||
return `at ${p.x}`;
|
||||
}
|
||||
}
|
||||
console.log(where(origin), where({ x: 0 }));
|
||||
const list = [1, 2];
|
||||
const other = [1, 2];
|
||||
for (const arr of [list, other]) {
|
||||
switch (arr) {
|
||||
case list:
|
||||
console.log("list");
|
||||
break;
|
||||
case other:
|
||||
console.log("other");
|
||||
}
|
||||
}
|
||||
const order: string[] = [];
|
||||
const pick = (m: Mode): Mode => {
|
||||
order.push(m.name);
|
||||
return m;
|
||||
};
|
||||
switch (slow) {
|
||||
case pick(fast):
|
||||
console.log("no");
|
||||
break;
|
||||
case pick(slow):
|
||||
console.log("matched second");
|
||||
break;
|
||||
case pick(fast):
|
||||
console.log("never evaluated");
|
||||
}
|
||||
console.log(order.join(","));
|
||||
const f = (): number => 1;
|
||||
const g = (): number => 2;
|
||||
switch (g as () => number) {
|
||||
case f:
|
||||
console.log("f");
|
||||
break;
|
||||
case g:
|
||||
console.log("g");
|
||||
break;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Arguments asserted to a type without undefined (`xs[i] as number`,
|
||||
// `stack.pop() as number`, `map.get(k) as Item`) pass present values.
|
||||
// A callee that also receives an unchecked missing read still observes
|
||||
// undefined there.
|
||||
class Item {
|
||||
weight: number;
|
||||
constructor(weight: number) {
|
||||
this.weight = weight;
|
||||
}
|
||||
}
|
||||
|
||||
function hot(n: number): number {
|
||||
return (n * 2) | 0;
|
||||
}
|
||||
|
||||
function describe(n: number): string {
|
||||
return n === undefined ? "missing" : `value ${n}`;
|
||||
}
|
||||
|
||||
function weigh(item: Item): number {
|
||||
return item.weight;
|
||||
}
|
||||
|
||||
function cold(xs: number[]): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < xs.length; i++) s = (s + hot(xs[i] as number)) | 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
function drain(stack: number[]): number {
|
||||
let s = 0;
|
||||
while (stack.length > 0) s = (s + hot(stack.pop() as number)) | 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
function total(items: Map<string, Item>, keys: string[]): number {
|
||||
let sum = 0;
|
||||
for (const key of keys) if (items.has(key)) sum += weigh(items.get(key) as Item);
|
||||
return sum;
|
||||
}
|
||||
|
||||
const xs = [1, 2, 3];
|
||||
console.log(cold(xs), drain([4, 5, 6]));
|
||||
console.log(describe(xs[1] as number), describe(xs[7]));
|
||||
const items = new Map([
|
||||
["a", new Item(10)],
|
||||
["b", new Item(20)],
|
||||
]);
|
||||
console.log(total(items, ["a", "b", "c"]));
|
||||
let t = 0;
|
||||
for (let i = 0; i < 1000; i++) t = (t + hot(i)) | 0;
|
||||
console.log(t);
|
||||
@@ -0,0 +1,99 @@
|
||||
// TypeScript class fields declared `unknown` or `object` (also with
|
||||
// `null` or `undefined`, or as arrays) hold any value of those types.
|
||||
// Reads narrow with typeof and instanceof, stored references keep their
|
||||
// identity, and static fields and parameter properties work the same way.
|
||||
class Item {
|
||||
name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
greet(): string {
|
||||
return `item ${this.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
class Slot {
|
||||
value: unknown = null;
|
||||
data: object | null = null;
|
||||
list: unknown[] = [];
|
||||
extra?: unknown;
|
||||
set(next: unknown): void {
|
||||
this.value = next;
|
||||
}
|
||||
describe(): string {
|
||||
if (typeof this.value === "string") return `string of ${this.value.length}`;
|
||||
if (typeof this.value === "number") return `number ${this.value * 2}`;
|
||||
if (this.value instanceof Item) return this.value.greet();
|
||||
if (Array.isArray(this.value)) return `array of ${this.value.length}`;
|
||||
return `other ${String(this.value)}`;
|
||||
}
|
||||
}
|
||||
|
||||
class Holder {
|
||||
payload: unknown;
|
||||
readonly meta: object;
|
||||
constructor(payload: unknown, meta: object) {
|
||||
this.payload = payload;
|
||||
this.meta = meta;
|
||||
}
|
||||
}
|
||||
|
||||
class Registry {
|
||||
static current: unknown = null;
|
||||
static entries: object[] = [];
|
||||
}
|
||||
|
||||
class Cell<T> {
|
||||
item: T;
|
||||
note: unknown = undefined;
|
||||
constructor(item: T) {
|
||||
this.item = item;
|
||||
}
|
||||
}
|
||||
|
||||
const slot = new Slot();
|
||||
console.log(slot.value, slot.data, slot.list, slot.extra, slot.describe());
|
||||
const item = new Item("a");
|
||||
for (const next of ["text", 21, item, [1, 2, 3], true, undefined] as unknown[]) {
|
||||
slot.set(next);
|
||||
console.log(slot.describe());
|
||||
}
|
||||
|
||||
slot.value = item;
|
||||
item.name = "b";
|
||||
console.log((slot.value as Item).name, slot.value === item);
|
||||
const record = { count: 1 };
|
||||
slot.data = record;
|
||||
record.count = 2;
|
||||
console.log(slot.data, slot.data === record);
|
||||
const numbers = [1, 2];
|
||||
slot.data = numbers;
|
||||
numbers.push(3);
|
||||
console.log(slot.data, slot.data === numbers);
|
||||
const lookup = new Map<string, number>([["k", 1]]);
|
||||
slot.value = lookup;
|
||||
lookup.set("j", 2);
|
||||
console.log(slot.value);
|
||||
const double = (x: number): number => x * 2;
|
||||
slot.value = double;
|
||||
if (typeof slot.value === "function") console.log(slot.value === double, (slot.value as (x: number) => number)(4));
|
||||
slot.list.push(item, 1, null, "s");
|
||||
console.log(slot.list.length, slot.list[0] === item, slot.list.slice(1));
|
||||
slot.extra = { nested: [1, { deep: true }] };
|
||||
console.log(JSON.stringify(slot.extra), slot.extra ?? "none");
|
||||
slot.value = undefined;
|
||||
console.log(slot.value === undefined, slot.value ?? "fallback");
|
||||
|
||||
const holder = new Holder([1, 2, 3], { tag: "m" });
|
||||
console.log(Array.isArray(holder.payload), holder.payload, holder.meta);
|
||||
const values: unknown[] = [holder.payload, holder.meta];
|
||||
console.log(values.length, values[0] === holder.payload);
|
||||
|
||||
console.log(Registry.current, Registry.entries);
|
||||
Registry.current = { a: 1 };
|
||||
Registry.entries.push(record, numbers);
|
||||
console.log(Registry.current, Registry.entries.length, Registry.entries[0] === record);
|
||||
|
||||
const cell = new Cell<number>(1);
|
||||
cell.note = "note";
|
||||
console.log(cell.item, cell.note, typeof cell.note);
|
||||
@@ -0,0 +1,112 @@
|
||||
// filter, find, findIndex, some and every on fixed tuples, readonly and
|
||||
// mutable: each position is read when it is visited, find/findIndex/some/
|
||||
// every stop at the first deciding position, and results match arrays.
|
||||
export {};
|
||||
|
||||
const tools = ["read", "write", "search"] as const;
|
||||
const allowed: string[] = ["read", "search"];
|
||||
console.log(JSON.stringify(tools.filter((name) => allowed.includes(name))));
|
||||
console.log(tools.find((name) => name.startsWith("w")), tools.find((name) => name === allowed[5]));
|
||||
console.log(tools.findIndex((name) => name === "search"), tools.findIndex((name) => name.length > 9));
|
||||
console.log(tools.some((name) => name.length > 5), tools.some((name) => name.length > 9));
|
||||
console.log(tools.every((name) => name.length > 3), tools.every((name) => name.length > 4));
|
||||
console.log(tools.filter((_, index) => index !== 1).join(","));
|
||||
|
||||
// Mixed element types, with and without inferred narrowing.
|
||||
const mixed: readonly [number, string, boolean, number] = [7, "eight", true, 0];
|
||||
console.log(JSON.stringify(mixed.filter((value): boolean => typeof value !== "boolean")));
|
||||
const numbers = mixed.filter((value) => typeof value === "number");
|
||||
console.log(numbers.reduce((sum, value) => sum + value, 0), numbers.length);
|
||||
const text = mixed.find((value) => typeof value === "string");
|
||||
console.log(text === undefined ? "none" : text.toUpperCase());
|
||||
console.log(mixed.findIndex((value) => value === true), mixed.some((value) => value === "eight"));
|
||||
console.log(mixed.every((value) => typeof value !== "object"));
|
||||
|
||||
// Truthiness of non-boolean predicate results, and Boolean.
|
||||
const counts: [number, number, number] = [0, 3, 0];
|
||||
console.log(counts.filter((value) => value).join(","), counts.findIndex((value) => value));
|
||||
console.log(counts.some((value) => value), counts.every((value) => value));
|
||||
const labels: [string, string, string] = ["", "b", "c"];
|
||||
console.log(labels.filter(Boolean).join(","), labels.find((value) => value));
|
||||
const maybe: [string | undefined, string | undefined] = [undefined, "x"];
|
||||
console.log(JSON.stringify(maybe.filter(Boolean)), maybe.findIndex((value) => value));
|
||||
|
||||
// A single-element tuple.
|
||||
const single: [number] = [4];
|
||||
console.log(JSON.stringify(single.filter((value) => value > 4)), single.find((value) => value === 4));
|
||||
|
||||
// A callback's write to a later position is visible when it is visited;
|
||||
// filter and find keep the value read before the callback ran.
|
||||
const mutable: [number, number, number] = [1, 2, 3];
|
||||
console.log(mutable.filter((value, index) => {
|
||||
if (index === 0) mutable[1] = 20;
|
||||
if (index === 1) mutable[1] = 200;
|
||||
return value > 1;
|
||||
}).join(","), mutable.join(","));
|
||||
const slots: [number, number] = [5, 6];
|
||||
console.log(slots.find((value) => {
|
||||
slots[0] = 50;
|
||||
return value === 5;
|
||||
}), slots[0]);
|
||||
const later: [number, number, number] = [1, 2, 3];
|
||||
console.log(later.some((value, index) => {
|
||||
if (index === 0) later[2] = 30;
|
||||
return value === 30;
|
||||
}));
|
||||
|
||||
// Reference identity of kept and found elements.
|
||||
const a = { id: "a" };
|
||||
const b = { id: "b" };
|
||||
const objects: [typeof a, typeof b] = [a, b];
|
||||
const kept = objects.filter((value) => value.id === "b");
|
||||
const found = objects.find((value) => value.id === "a");
|
||||
console.log(kept[0] === b, found === a);
|
||||
b.id = "B";
|
||||
console.log(kept[0]!.id);
|
||||
|
||||
// Short-circuiting and visit order.
|
||||
let visited = "";
|
||||
const letters = ["p", "q", "r"] as const;
|
||||
console.log(letters.find((value) => { visited += value; return value === "q"; }), visited);
|
||||
visited = "";
|
||||
console.log(letters.findIndex((value) => { visited += value; return value === "p"; }), visited);
|
||||
visited = "";
|
||||
console.log(letters.some((value) => { visited += value; return value === "q"; }), visited);
|
||||
visited = "";
|
||||
console.log(letters.every((value) => { visited += value; return value === "p"; }), visited);
|
||||
visited = "";
|
||||
console.log(letters.filter((value) => { visited += value; return true; }).length, visited);
|
||||
|
||||
// The receiver evaluates before the callback expression.
|
||||
let calls = "";
|
||||
function receiver(): [number, number] { calls += "R"; return [8, 9]; }
|
||||
function predicate(): (value: number) => boolean {
|
||||
calls += "C";
|
||||
return (value) => { calls += String(value); return value === 9; };
|
||||
}
|
||||
console.log(receiver().filter(predicate()).join(","), calls);
|
||||
calls = "";
|
||||
console.log(receiver().find(predicate()), calls);
|
||||
|
||||
// A throwing callback stops the visit.
|
||||
visited = "";
|
||||
try {
|
||||
letters.filter((value, index) => {
|
||||
visited += value;
|
||||
if (index === 1) throw new Error("stop");
|
||||
return true;
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) console.log(error.message, visited);
|
||||
}
|
||||
|
||||
// Generic consumers.
|
||||
function firstMatch<T>(value: T, other: T): T | undefined {
|
||||
const pair: [T, T] = [value, other];
|
||||
return pair.find((item) => item === other);
|
||||
}
|
||||
function anyEqual<T>(value: T, other: T): boolean {
|
||||
const pair: readonly [T, T] = [value, other];
|
||||
return pair.some((item, index) => index > 0 && item === value);
|
||||
}
|
||||
console.log(firstMatch(1, 2), firstMatch("x", "y"), anyEqual(3, 3), anyEqual("u", "v"));
|
||||
@@ -0,0 +1,83 @@
|
||||
// A union arm with a required `never` property has no values, so
|
||||
// Result<T, never> and Result<never, E> widen into Result<T, E> at returns,
|
||||
// initializers, arguments, array elements and async returns.
|
||||
export {};
|
||||
|
||||
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
|
||||
|
||||
function ok<T>(value: T): Result<T, never> {
|
||||
return { ok: true, value };
|
||||
}
|
||||
|
||||
function err<E>(error: E): Result<never, E> {
|
||||
return { ok: false, error };
|
||||
}
|
||||
|
||||
function parse(value: string): Result<string, string> {
|
||||
return value.length > 0 ? ok(value) : { ok: false, error: "empty" };
|
||||
}
|
||||
|
||||
function check(value: number): Result<number, string> {
|
||||
if (value < 0) return err("negative");
|
||||
return ok(value);
|
||||
}
|
||||
|
||||
function show(result: Result<string, string>): string {
|
||||
return result.ok ? `ok ${result.value}` : `error ${result.error}`;
|
||||
}
|
||||
|
||||
function showNumber(result: Result<number, string>): string {
|
||||
return result.ok ? `ok ${result.value}` : `error ${result.error}`;
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(parse("hello")), JSON.stringify(parse("")));
|
||||
console.log(showNumber(check(4)), showNumber(check(-1)));
|
||||
|
||||
const fromOk: Result<string, string> = ok("bound");
|
||||
const fromErr: Result<string, string> = err("failed");
|
||||
console.log(show(fromOk), show(fromErr));
|
||||
console.log(show(ok("argument")), showNumber(err("argument")));
|
||||
|
||||
const all: Result<number, string>[] = [ok(1), err("two"), ok(3)];
|
||||
console.log(all.map((result) => showNumber(result)).join(", "));
|
||||
|
||||
let current: Result<string, string> = ok("first");
|
||||
console.log(show(current));
|
||||
current = err("reassigned");
|
||||
console.log(show(current));
|
||||
current = ok("third");
|
||||
console.log(show(current));
|
||||
|
||||
function wrap<T>(value: T): Result<T, string> {
|
||||
return ok(value);
|
||||
}
|
||||
console.log(showNumber(wrap(5)), show(wrap("five")));
|
||||
|
||||
// The never arm's fields stay readable in branches that cannot run.
|
||||
const only = ok(10);
|
||||
if (only.ok) console.log("value", only.value);
|
||||
else console.log("unreachable", only.error);
|
||||
|
||||
// Optional never properties, never[] and never-returning callbacks have
|
||||
// values, so their arms convert as before.
|
||||
type Loose =
|
||||
| { ok: true; value: string; error?: never }
|
||||
| { ok: false; value?: never; error: string };
|
||||
function looseOk(value: string): Loose {
|
||||
return { ok: true, value };
|
||||
}
|
||||
const loose: Loose | null = looseOk("loose");
|
||||
console.log(JSON.stringify(loose));
|
||||
|
||||
type Batch = { ok: true; items: string[] } | { ok: false; errors: never[] };
|
||||
function emptyFailure(): Batch {
|
||||
return { ok: false, errors: [] };
|
||||
}
|
||||
const batch: Batch | undefined = emptyFailure();
|
||||
console.log(JSON.stringify(batch));
|
||||
|
||||
async function load(name: string): Promise<Result<string, string>> {
|
||||
return name === "" ? err("missing") : ok(`loaded ${name}`);
|
||||
}
|
||||
void load("config").then((result) => console.log(show(result)));
|
||||
void load("").then((result) => console.log(show(result)));
|
||||
@@ -1,6 +1,6 @@
|
||||
export class Crate {
|
||||
readonly label: string;
|
||||
contents: unknown;
|
||||
contents: any;
|
||||
constructor(label: string) {
|
||||
this.label = label;
|
||||
this.contents = undefined;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// ArrayBuffers and transfer, SharedArrayBuffer.grow, Atomics.waitAsync,
|
||||
// Float16Array and its Math/DataView companions, first-class iterator
|
||||
// objects, and the unlowered forms of the implemented statics
|
||||
// (Promise.try's ...args, groupBy's literal-union keys, a set-like
|
||||
// non-Set argument).
|
||||
// (Promise.try with fewer arguments than parameters, groupBy's
|
||||
// literal-union keys, a set-like non-Set argument).
|
||||
|
||||
// Resizable ArrayBuffer + transfer (ES2024).
|
||||
const ab = new ArrayBuffer(8, { maxByteLength: 16 });
|
||||
@@ -31,8 +31,8 @@ dv.setFloat16(0, 1.5);
|
||||
// First-class iterator objects.
|
||||
const it = Iterator.from([1, 2, 3].values());
|
||||
|
||||
// Promise.try's ...args form (close over the values instead).
|
||||
const tried = Promise.try((a: number, b: number) => a + b, 1, 2);
|
||||
// Promise.try with fewer arguments than the callback's parameters.
|
||||
const tried = Promise.try((a: number, b?: number) => a + (b ?? 0), 1);
|
||||
|
||||
// Object.groupBy at a literal-union key type (a fixed-field record).
|
||||
const grouped = Object.groupBy([1, 2, 3], (n) => (n % 2 === 0 ? "even" : "odd"));
|
||||
|
||||
@@ -39,7 +39,7 @@ function localCapture(): () => number {
|
||||
return () => local as number;
|
||||
}
|
||||
class Holder {
|
||||
data: unknown = JSON.parse("{}");
|
||||
data: unknown = JSON.parse("{}"); // unknown class fields use checked-dynamic storage — no fence
|
||||
}
|
||||
const anything: any = 5; // checker-`any` bindings ride the checked-dynamic tree now — no fence
|
||||
const dynArray: unknown[] = []; // unknown[] IS the dyn array now — no fence (corpus 2585)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Type errors in program code report at their own locations, not as
|
||||
// SC2013 refusals of the npm packages the program imports.
|
||||
import { open } from "sqlish";
|
||||
import { route } from "routerish";
|
||||
|
||||
console.log(open(":memory:"));
|
||||
console.log(route("/users"));
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function route(method: string, path: string): string;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export function route(method, path) {
|
||||
return `${method} ${path}`;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "routerish",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts"
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function connect(path: string): { path: string };
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export function connect(path) {
|
||||
return { path };
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "sqlish",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
deferred-class-binding/crate.ts:3:3 - error SC1090: 'unknown'-typed class fields are not supported yet
|
||||
|
||||
2 | readonly label: string;
|
||||
3 | contents: unknown;
|
||||
3 | contents: any;
|
||||
| ^~~~~~~~
|
||||
4 | constructor(label: string) {
|
||||
|
||||
|
||||
@@ -97,14 +97,14 @@ es2025-fences.ts:32:12 - error SC2020: 'Iterator.from' is part of the standard l
|
||||
|
||||
hint: first-class iterator objects have no lowering — iterator helpers compile as one chain on an array iterator, consumed in place: arr.values().map(f).take(n).toArray()
|
||||
|
||||
es2025-fences.ts:35:15 - error SC2020: 'Promise.try with 3 arguments' is part of the standard library types but has no scriptc lowering yet
|
||||
es2025-fences.ts:35:27 - error SC2020: 'Promise.try over a callback of type '(a: number, b?: number | undefined) => number' with 1 argument' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
34 | // Promise.try's ...args form (close over the values instead).
|
||||
35 | const tried = Promise.try((a: number, b: number) => a + b, 1, 2);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
34 | // Promise.try with fewer arguments than the callback's parameters.
|
||||
35 | const tried = Promise.try((a: number, b?: number) => a + (b ?? 0), 1);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
36 |
|
||||
|
||||
hint: the ...args form has no lowering — close over the values: Promise.try(() => f(a, b))
|
||||
hint: pass one argument per callback parameter: Promise.try((a, b) => a + b, 1, 2)
|
||||
|
||||
es2025-fences.ts:38:17 - error SC2020: 'Object.groupBy at this result type' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
|
||||
@@ -5,13 +5,6 @@ json-dyn.ts:25:41 - error SC1090: JSON.stringify of '(number) => number' values
|
||||
| ^~~~~~~~~~~~~~~~~~~~
|
||||
26 | class Point {
|
||||
|
||||
json-dyn.ts:42:3 - error SC1090: 'unknown'-typed class fields are not supported yet
|
||||
|
||||
41 | class Holder {
|
||||
42 | data: unknown = JSON.parse("{}");
|
||||
| ^~~~
|
||||
43 | }
|
||||
|
||||
json-dyn.ts:46:7 - error SC2007: values of type '{ (text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined): any; (text: string): unknown; (text: string, reviver: (this: unknown, key: string, value: unknown) => unknown): unknown; }' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature
|
||||
|
||||
45 | const dynArray: unknown[] = []; // unknown[] IS the dyn array now — no fence (corpus 2585)
|
||||
@@ -26,11 +19,4 @@ json-dyn.ts:46:18 - error SC1090: JSON methods as values (call 'parse' directly)
|
||||
45 | const dynArray: unknown[] = []; // unknown[] IS the dyn array now — no fence (corpus 2585)
|
||||
46 | const parseRef = JSON.parse;
|
||||
| ^~~~~~~~~~
|
||||
47 | // The exact `string | undefined` root now lowers: strings serialize while
|
||||
|
||||
json-dyn.ts:63:1 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet
|
||||
|
||||
62 | // them relevant; these references are what makes them count.
|
||||
63 | new Holder();
|
||||
| ^~~~~~~~~~~~
|
||||
64 | // End with a nonblank context line so diagnostic snapshots carry no trailing whitespace.
|
||||
47 | // The exact `string | undefined` root now lowers: strings serialize while
|
||||
@@ -0,0 +1,13 @@
|
||||
npm-program-type-error/main.ts:3:10 - error SC0001: Module '"sqlish"' has no exported member 'open'.
|
||||
|
||||
2 | // SC2013 refusals of the npm packages the program imports.
|
||||
3 | import { open } from "sqlish";
|
||||
| ^~~~
|
||||
4 | import { route } from "routerish";
|
||||
|
||||
npm-program-type-error/main.ts:7:13 - error SC0001: Expected 2 arguments, but got 1.
|
||||
|
||||
6 | console.log(open(":memory:"));
|
||||
7 | console.log(route("/users"));
|
||||
| ^~~~~
|
||||
8 |
|
||||
Reference in New Issue
Block a user