Merge main

This commit is contained in:
Chris Tate
2026-10-17 16:37:00 -05:00
42 changed files with 1374 additions and 359 deletions
+1
View File
@@ -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 -1
View File
@@ -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");
});
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5920-promise-try-args.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5921-promise-try-args-js.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5927-switch-object-identity.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5931-asserted-reads-keep-number-parameters.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5933-class-fields-unknown-object.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5950-tuple-predicate-methods.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5951-result-never-widening.ts"
],
"diags": []
}
@@ -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);
+22
View File
@@ -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));
+7
View File
@@ -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;
+4 -4
View File
@@ -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"));
+1 -1
View File
@@ -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"));
@@ -0,0 +1 @@
export declare function route(method: string, path: string): string;
@@ -0,0 +1,3 @@
export function route(method, path) {
return `${method} ${path}`;
}
@@ -0,0 +1,7 @@
{
"name": "routerish",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"types": "index.d.ts"
}
@@ -0,0 +1 @@
export declare function connect(path: string): { path: string };
@@ -0,0 +1,3 @@
export function connect(path) {
return { path };
}
@@ -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
+1 -15
View File
@@ -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 |