mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Merge main
This commit is contained in:
@@ -595,6 +595,19 @@ export function emitDynamicExpr(
|
||||
B.br(join);
|
||||
return;
|
||||
}
|
||||
// A declared-only arm without the literal key: the object has no
|
||||
// such own property, so the read answers undefined.
|
||||
if (literal !== null && !shape.indexValue) {
|
||||
const tag = resultDef?.arms.findIndex((a) => a.kind === "undefinedT") ?? -1;
|
||||
if (tag < 0 || e.type.kind !== "union") {
|
||||
throw new InternalCompilerError(
|
||||
"llvm emitter bug: unionKeyGet missing key without an undefined result arm",
|
||||
);
|
||||
}
|
||||
B.line(`store ptr ${host.unitInstanceRef(e.type.unionId, tag)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
return;
|
||||
}
|
||||
// Index-signature arm (or declared-only shape under a runtime
|
||||
// key): the shared keyed-read chain — a literal key naming no
|
||||
// declared field touches only the overflow map.
|
||||
|
||||
@@ -811,10 +811,19 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
const kind = host.emitExpr(e.args[0]!);
|
||||
const code = host.emitExpr(e.args[1]!);
|
||||
const msg = host.emitExpr(e.args[2]!);
|
||||
host.declare(`declare void @scr_throw_node_coded(double, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_throw_node_coded(double ${kind.name}, ptr ${code.name}, ptr ${msg.name})`,
|
||||
);
|
||||
if (e.args.length > 3) {
|
||||
// The fourth argument: the error's own `url` property.
|
||||
const url = host.emitExpr(e.args[3]!);
|
||||
host.declare(`declare void @scr_throw_node_coded_url(double, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_throw_node_coded_url(double ${kind.name}, ptr ${code.name}, ptr ${msg.name}, ptr ${url.name})`,
|
||||
);
|
||||
} else {
|
||||
host.declare(`declare void @scr_throw_node_coded(double, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_throw_node_coded(double ${kind.name}, ptr ${code.name}, ptr ${msg.name})`,
|
||||
);
|
||||
}
|
||||
const ty = host.llType(e.type);
|
||||
if (ty === "void") {
|
||||
host.emitAlwaysPendingUnwind();
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
|
||||
/** A `Pick<C, K>` / `Omit<C, K>` view: the lib alias it instantiates and
|
||||
* its source type argument `C`. */
|
||||
export interface ClassView {
|
||||
name: "Pick" | "Omit";
|
||||
source: ts.Type;
|
||||
}
|
||||
|
||||
/** The lib's Pick or Omit alias (declared only in declaration files). */
|
||||
function libViewName(symbol: ts.Symbol): "Pick" | "Omit" | null {
|
||||
const name = ts.symbolName(symbol);
|
||||
if (name !== "Pick" && name !== "Omit") return null;
|
||||
return symbol.declarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)
|
||||
? name
|
||||
: null;
|
||||
}
|
||||
|
||||
/** `type` as a Pick/Omit view, spelled inline (`Pick<C, K>`) or through
|
||||
* non-generic user aliases (`type View = Pick<C, K>`, `type V2 = View`).
|
||||
* An aliased instantiation carries the USER's alias symbol and no alias
|
||||
* type arguments, so the alias declaration's type node names the view.
|
||||
* Generic user aliases (`type V<T> = Pick<T, K>`) are not resolved. */
|
||||
export function classViewOf(checker: ts.TypeChecker, type: ts.Type, depth = 0): ClassView | null {
|
||||
const alias = type.getAliasSymbol();
|
||||
if (alias === undefined) return null;
|
||||
const direct = libViewName(alias);
|
||||
if (direct !== null) {
|
||||
const source = type.getAliasTypeArguments()[0];
|
||||
return source === undefined ? null : { name: direct, source };
|
||||
}
|
||||
if (depth >= 8 || type.getAliasTypeArguments().length > 0) return null;
|
||||
const decl = alias.declarations.find(ts.isTypeAliasDeclaration);
|
||||
if (decl === undefined || (decl.typeParameters?.nodes.length ?? 0) > 0) return null;
|
||||
let node: ts.TypeNode = decl.type;
|
||||
while (ts.isParenthesizedTypeNode(node)) node = node.type;
|
||||
if (!ts.isTypeReferenceNode(node)) return null;
|
||||
let target = checker.getSymbolAtLocation(node.typeName);
|
||||
if (target !== undefined && (target.flags & ts.SymbolFlags.Alias) !== 0)
|
||||
target = checker.getAliasedSymbol(target);
|
||||
if (target === undefined) return null;
|
||||
const viewName = libViewName(target);
|
||||
if (viewName !== null) {
|
||||
const argument = node.typeArguments?.nodes[0];
|
||||
return argument === undefined
|
||||
? null
|
||||
: { name: viewName, source: checker.getTypeFromTypeNode(argument) };
|
||||
}
|
||||
if ((target.flags & ts.SymbolFlags.TypeAlias) === 0) return null;
|
||||
return classViewOf(checker, checker.getDeclaredTypeOfSymbol(target), depth + 1);
|
||||
}
|
||||
@@ -482,7 +482,7 @@ export function funcCoerceAdapter(
|
||||
strandRet = false;
|
||||
// A record result the slot's result is a structural width of.
|
||||
if (strandRet && lowerer.widthLiftPlan(fromT.ret, toT.ret)) strandRet = false;
|
||||
const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`;
|
||||
const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}${lowerer.coercions.checkedRetags > 0 ? ":checked" : ""}`;
|
||||
const existing = lowerer.coercions.retags.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, "%fn.adapt", key);
|
||||
|
||||
@@ -9,6 +9,13 @@ export class CoercionState {
|
||||
// plan checks its other fields. Remove every assumption on unwind;
|
||||
// generated helpers must intern their names before building their bodies.
|
||||
readonly planning = new Set<string>();
|
||||
/** > 0 while a record width plan plans or builds its fields: method
|
||||
* fields may take checked function adapters (WidthLift funcAdaptChecked). */
|
||||
methodFieldDepth = 0;
|
||||
/** > 0 while a checked function adapter plans or builds: contravariant
|
||||
* parameters may width-lift, and union fields may retag with trapping
|
||||
* arms (WidthLift checkedRetag). */
|
||||
checkedRetags = 0;
|
||||
// Registry revisions invalidate only fully validated copy-only routes.
|
||||
readonly copyRetags = new Map<string, { name: string; shapes: number; unions: number }>();
|
||||
// Provenance for checked extraction; consumers must not infer it from names.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { planRecordUnionLift, planRecordUnionWrap } from "../union-retag.js";
|
||||
import { planRecordUnionLift, planRecordUnionWrap, planUnionRetag } from "../union-retag.js";
|
||||
import type { WidthLift } from "../width-lift.js";
|
||||
import type { IrRecordShape, IrType } from "../../../ir/ir.js";
|
||||
import { canDynCheckTo, isUnitType, typeEquals, UNDEFINED_T } from "../../../ir/ir.js";
|
||||
@@ -57,6 +57,33 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
|
||||
lowerer.armTag(dst.unionId, UNDEFINED_T) >= 0
|
||||
)
|
||||
return { how: "unitRetag" };
|
||||
// Inside a checked method adapter (contravariant positions): source
|
||||
// arms with no destination home trap instead of refusing the pair.
|
||||
if (lowerer.coercions.checkedRetags > 0) {
|
||||
const from = lowerer.unions.get(src.unionId);
|
||||
const to = lowerer.unions.get(dst.unionId);
|
||||
if (from && to) {
|
||||
const trappable: number[] = [];
|
||||
from.arms.forEach((arm, tag) => {
|
||||
if (isUnitType(arm) || lowerer.armTag(dst.unionId, arm) >= 0) return;
|
||||
if (to.arms.some((d) => !isUnitType(d) && lowerer.widthLiftPlan(arm, d) !== null))
|
||||
return;
|
||||
trappable.push(tag);
|
||||
});
|
||||
if (
|
||||
trappable.length > 0 &&
|
||||
trappable.length < from.arms.length &&
|
||||
planUnionRetag(
|
||||
from,
|
||||
to,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(s, d) => lowerer.widthLiftPlan(s, d),
|
||||
new Set(trappable),
|
||||
) !== null
|
||||
)
|
||||
return { how: "checkedRetag", trappable };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// A unit-typed source can't wrap here (unionWrap requires the
|
||||
@@ -71,7 +98,7 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
|
||||
return { how: "discriminantWrap" };
|
||||
return { how: "wrap", tag };
|
||||
}
|
||||
const candidates: { tag: number; arm: IrType }[] = [];
|
||||
const candidates: { tag: number; arm: IrType; checked: boolean }[] = [];
|
||||
def.arms.forEach((arm, i) => {
|
||||
if (isUnitType(arm)) return;
|
||||
const sameFamily =
|
||||
@@ -87,9 +114,15 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
|
||||
arm.kind === "array") ||
|
||||
(src.kind === "object" && arm.kind === "record") ||
|
||||
(src.kind === "record" && arm.kind === "object");
|
||||
if (sameFamily && lowerer.widthLiftPlan(src, arm) !== null) candidates.push({ tag: i, arm });
|
||||
const armLift = sameFamily ? lowerer.widthLiftPlan(src, arm) : null;
|
||||
if (armLift !== null)
|
||||
candidates.push({ tag: i, arm, checked: armLift.how === "funcAdaptChecked" });
|
||||
});
|
||||
let selected = candidates;
|
||||
// A clean function candidate always wins over a checked one, so every
|
||||
// selection that resolves today resolves the same way.
|
||||
if (selected.some((c) => c.checked) && selected.some((c) => !c.checked))
|
||||
selected = selected.filter((c) => !c.checked);
|
||||
if (selected.length > 1 && src.kind === "record") {
|
||||
const source = lowerer.shapes.get(src.shapeId);
|
||||
// An inferred JS union may widen its tag while retaining variants
|
||||
@@ -216,6 +249,22 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
|
||||
if (dst.kind === "func" && src.kind === "func" && lowerer.cleanFuncAdaptable(src, dst)) {
|
||||
return { how: "funcAdapt" };
|
||||
}
|
||||
// A method FIELD of a record width copy whose parameters convert only
|
||||
// through a checked narrowing (method bivariance across instantiations):
|
||||
// a checked adapter. Top-level function conversions never see it.
|
||||
if (
|
||||
dst.kind === "func" &&
|
||||
src.kind === "func" &&
|
||||
lowerer.coercions.methodFieldDepth > 0 &&
|
||||
lowerer.coercions.checkedRetags === 0
|
||||
) {
|
||||
lowerer.coercions.checkedRetags++;
|
||||
try {
|
||||
if (lowerer.cleanFuncAdaptable(src, dst)) return { how: "funcAdaptChecked" };
|
||||
} finally {
|
||||
lowerer.coercions.checkedRetags--;
|
||||
}
|
||||
}
|
||||
if (dst.kind === "record" && src.kind === "record") {
|
||||
// An INDEX-SIGNATURE destination takes the overflow CAPTURE reshape:
|
||||
// declared fields copy off the source, every other source field keys
|
||||
@@ -370,6 +419,7 @@ export function recordWidthPlan(
|
||||
// fields answers "assume coercible" — see CoercionState.planning.
|
||||
if (lowerer.coercions.planning.has(key)) return new Map();
|
||||
lowerer.coercions.planning.add(key);
|
||||
lowerer.coercions.methodFieldDepth++;
|
||||
try {
|
||||
const plan = new Map<string, FieldLift>();
|
||||
for (const tf of to.fields) {
|
||||
@@ -405,6 +455,7 @@ export function recordWidthPlan(
|
||||
}
|
||||
return plan;
|
||||
} finally {
|
||||
lowerer.coercions.methodFieldDepth--;
|
||||
lowerer.coercions.planning.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,34 @@ export function applyWidthLift(
|
||||
throw new InternalCompilerError("lowerer bug: planned retag lift failed to intern");
|
||||
return { kind: "call", callee: retag, args: [value], type: dst, loc };
|
||||
}
|
||||
case "checkedRetag": {
|
||||
if (dst.kind !== "union" || value.type.kind !== "union")
|
||||
throw new InternalCompilerError("lowerer bug: checked retag lift shape");
|
||||
const retag = lowerer.unionRetagHelper(
|
||||
value.type.unionId,
|
||||
dst.unionId,
|
||||
loc,
|
||||
new Set(lift.trappable),
|
||||
);
|
||||
if (!retag)
|
||||
throw new InternalCompilerError("lowerer bug: planned checked retag lift failed to intern");
|
||||
return { kind: "call", callee: retag, args: [value], type: dst, loc };
|
||||
}
|
||||
case "funcAdaptChecked": {
|
||||
if (dst.kind !== "func" || value.type.kind !== "func")
|
||||
throw new InternalCompilerError("lowerer bug: checked funcAdapt lift shape");
|
||||
lowerer.coercions.checkedRetags++;
|
||||
try {
|
||||
const adapter = lowerer.funcCoerceAdapter(value.type, dst, loc);
|
||||
if (!adapter)
|
||||
throw new InternalCompilerError(
|
||||
"lowerer bug: planned checked funcAdapt lift failed to intern",
|
||||
);
|
||||
return { kind: "call", callee: adapter, args: [value], type: dst, loc };
|
||||
} finally {
|
||||
lowerer.coercions.checkedRetags--;
|
||||
}
|
||||
}
|
||||
case "unitRetag": {
|
||||
if (dst.kind !== "union" || value.type.kind !== "union")
|
||||
throw new InternalCompilerError("lowerer bug: unit retag lift shape");
|
||||
@@ -527,11 +555,16 @@ export function recordWidthHelper(
|
||||
lowerer.coercionOriginNode = null;
|
||||
lowerer.coercionFieldOrigin = null;
|
||||
try {
|
||||
lowerer.liftedFns.push(
|
||||
buildRecordProjection(lowerer, name, fromT, to, plan, loc, (field) =>
|
||||
declared === null ? null : fieldOrigin(lowerer.checker, declared, field),
|
||||
),
|
||||
);
|
||||
lowerer.coercions.methodFieldDepth++;
|
||||
try {
|
||||
lowerer.liftedFns.push(
|
||||
buildRecordProjection(lowerer, name, fromT, to, plan, loc, (field) =>
|
||||
declared === null ? null : fieldOrigin(lowerer.checker, declared, field),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
lowerer.coercions.methodFieldDepth--;
|
||||
}
|
||||
} finally {
|
||||
lowerer.coercionOriginNode = outerNode;
|
||||
lowerer.coercionFieldOrigin = outerField;
|
||||
|
||||
@@ -22,12 +22,14 @@ import {
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
arrayOf,
|
||||
canDynCheckTo,
|
||||
isRefCounted,
|
||||
isSupportedIndexValue,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
} from "../../../ir/ir.js";
|
||||
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
|
||||
import { recordMayKeepSource, recordSourceTest } from "../record-sources.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import { type WidthLift } from "../lowerer.js";
|
||||
@@ -1221,8 +1223,47 @@ export function lowerRecordOvfCaptureHelper(
|
||||
),
|
||||
);
|
||||
}
|
||||
// A CLOSED source can still hold own keys its shape does not declare: a
|
||||
// record built from the checked-dynamic tree keeps them beside it (the
|
||||
// record-sources.ts view), and every JS copy — spread, Object.assign,
|
||||
// JSON — carries them. The field-by-field rebuild above cannot see
|
||||
// them, so when the source kept such a view, rebuild the target from
|
||||
// that live view instead: the validated keyed conversion the checked
|
||||
// record builder performs, which copies exactly the keys Node copies.
|
||||
// The declared-field copies before this branch are pure writes into a
|
||||
// fresh local, so the redone work is only wasted slot stores on the
|
||||
// rare path where the view exists.
|
||||
// The conversion is only honest when the checked-dynamic tree can check it,
|
||||
// and the view can only exist if the program converts something into a
|
||||
// closed record at all. Both facts are settled before the shape helpers
|
||||
// finalize their bodies, so the tail is chosen there.
|
||||
const sourcedFromView =
|
||||
fIv === null &&
|
||||
recordMayKeepSource(lowerer, fromT) &&
|
||||
canDynCheckTo(
|
||||
toT,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(id) => lowerer.unions.get(id),
|
||||
);
|
||||
body.push({ kind: "return", value: outRef, loc });
|
||||
const suffix = body.slice(1 + fieldStmts.size);
|
||||
const keptSourceTail = (): IrStmt[] => {
|
||||
const fromView: IrExpr = {
|
||||
kind: "dynCheck",
|
||||
value: { kind: "dynFrom", value: sRef, type: DYN, loc },
|
||||
type: toT,
|
||||
loc,
|
||||
};
|
||||
const fromViewReturn: IrStmt = { kind: "return", value: fromView, loc };
|
||||
const fromViewBranch: IrStmt = {
|
||||
kind: "if",
|
||||
cond: recordSourceTest(sRef, loc),
|
||||
then: [fromViewReturn],
|
||||
else_: null,
|
||||
loc,
|
||||
};
|
||||
return [fromViewBranch, ...suffix];
|
||||
};
|
||||
const fn: IrFunction = {
|
||||
name,
|
||||
params: [{ localId: "s.0", name: "s", type: fromT }],
|
||||
@@ -1244,13 +1285,15 @@ export function lowerRecordOvfCaptureHelper(
|
||||
lowerer.shapeOrderHelperFinalizers.push(() => {
|
||||
const current = lowerer.shapes.get(fromId) ?? from;
|
||||
const currentOrder = current.declaredOrder ?? current.fields.map((f) => f.name);
|
||||
let tail: IrStmt[] = suffix;
|
||||
if (sourcedFromView && lowerer.recordSourcesPossible) tail = keptSourceTail();
|
||||
fn.body = [
|
||||
body[0]!,
|
||||
...currentOrder.flatMap((field) => {
|
||||
const stmt = fieldStmts.get(field);
|
||||
return stmt ? [stmt] : [];
|
||||
}),
|
||||
...suffix,
|
||||
...tail,
|
||||
];
|
||||
});
|
||||
lowerer.liftedFns.push(fn);
|
||||
|
||||
@@ -84,7 +84,13 @@ import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js";
|
||||
import { withInheritedMethods } from "../record-inherited.js";
|
||||
import { lowerClassPrototypeComputedAssignment } from "../class-prototypes.js";
|
||||
import { isClassCallback } from "../class-callbacks.js";
|
||||
import { isKeyedFieldAccess } from "./keyed-field-targets.js";
|
||||
import {
|
||||
isKeyedFieldAccess,
|
||||
keyedFieldRead,
|
||||
keyedFieldWrite,
|
||||
sameTypedFieldKeys,
|
||||
type KeyedFieldTarget,
|
||||
} from "./keyed-field-targets.js";
|
||||
import { lowerFieldCompoundValue } from "./updates.js";
|
||||
import { readsHeldEvolvingArray } from "../lower-stmts.js";
|
||||
import {
|
||||
@@ -1548,6 +1554,36 @@ export function lowerRecordKeyRead(
|
||||
recordKeyResultOk(lowerer, shape, DYN)
|
||||
)
|
||||
declared = DYN;
|
||||
// A key whose literal type names only declared fields of one type, on a
|
||||
// record whose other fields have other types, reads the field it names.
|
||||
// A called access (`r[key]()`) keeps the refusal.
|
||||
const named =
|
||||
!declared && litKey === null && !isCalledAccess(expr)
|
||||
? sameTypedFieldKeys(lowerer, shape, keyNode)
|
||||
: null;
|
||||
if (named) {
|
||||
const stmts: IrStmt[] = [];
|
||||
const settle = (value: IrExpr, name: string, force = false): IrExpr => {
|
||||
if (!force && isSafeToRepeat(value)) return value;
|
||||
const temp = lowerer.declareHiddenLocal(name, value.type);
|
||||
stmts.push({ kind: "varDecl", localId: temp.id, init: value, loc });
|
||||
return varRef(temp.id, temp.type, loc);
|
||||
};
|
||||
const receiver = settle(obj, "%keyedReceiver", !isSafeToDiscard(key));
|
||||
const target: KeyedFieldTarget = {
|
||||
container: "recordKeyed",
|
||||
obj: receiver,
|
||||
shapeId,
|
||||
field: named.keys[0]!,
|
||||
keys: named.keys,
|
||||
key: settle(key, "%keyedKey"),
|
||||
fieldType: named.fieldType,
|
||||
};
|
||||
const get = keyedFieldRead(lowerer, target, loc, true);
|
||||
const read: IrExpr =
|
||||
stmts.length === 0 ? get : { kind: "seqExpr", stmts, result: get, type: get.type, loc };
|
||||
return includeUndefined ? read : lowerer.maybeNarrow(read, expr);
|
||||
}
|
||||
if (!declared) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -2571,6 +2607,48 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
|
||||
};
|
||||
}
|
||||
}
|
||||
// A key whose literal type names only declared fields of one type
|
||||
// writes the field it names. JavaScript evaluates the receiver, then
|
||||
// the key, then the value; the dispatch reads the saved receiver and
|
||||
// key once per named field. Locals are saved too, because the key or
|
||||
// the value can reassign them.
|
||||
const named =
|
||||
litKey === null ? sameTypedFieldKeys(lowerer, shape, target.argumentExpression) : null;
|
||||
if (named && !shape.fields.every((f) => typeEquals(f.type, named.fieldType))) {
|
||||
const loc = locOf(expr);
|
||||
const body: IrStmt[] = [];
|
||||
const save = (value: IrExpr, name: string): IrExpr => {
|
||||
const temp = lowerer.declareHiddenLocal(name, value.type);
|
||||
body.push({ kind: "varDecl", localId: temp.id, init: value, loc });
|
||||
return varRef(temp.id, temp.type, loc);
|
||||
};
|
||||
const obj = save(lowerer.lowerExpr(target.expression), "%keyedReceiver");
|
||||
const rawKey = lowerRecordPropertyKey(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(target.argumentExpression),
|
||||
target.argumentExpression,
|
||||
);
|
||||
if (rawKey.type.kind !== "string") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
target.argumentExpression,
|
||||
"indexing records with non-string or non-number keys",
|
||||
);
|
||||
}
|
||||
const key = save(rawKey, "%keyedKey");
|
||||
const value = lowerer.lowerExprExpecting(expr.right, named.fieldType);
|
||||
const keyed: KeyedFieldTarget = {
|
||||
container: "recordKeyed",
|
||||
obj,
|
||||
shapeId: receiverIr.shapeId,
|
||||
field: named.keys[0]!,
|
||||
keys: named.keys,
|
||||
key,
|
||||
fieldType: named.fieldType,
|
||||
};
|
||||
body.push(keyedFieldWrite(lowerer, keyed, value, loc));
|
||||
return { kind: "block", body, loc };
|
||||
}
|
||||
if (!shape.indexValue) {
|
||||
// A SIGNATURE-FREE shape whose declared fields share ONE type
|
||||
// writes through the same per-shape dispatch (the mockable-clock
|
||||
|
||||
@@ -5,9 +5,12 @@ import {
|
||||
DYN,
|
||||
F64,
|
||||
STRING,
|
||||
shapeHasAccessorSlots,
|
||||
typeEquals,
|
||||
type IrExpr,
|
||||
type IrRecordShape,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../../ir/ir.js";
|
||||
import { isJsSourceFile } from "../../program.js";
|
||||
@@ -141,6 +144,32 @@ export function keyedFieldTarget(
|
||||
};
|
||||
}
|
||||
|
||||
/** The declared fields a computed key names when the key's literal type
|
||||
* allows only names of declared fields stored with one type
|
||||
* (`stats[counter]` with `counter: "hits" | "misses"` on a record whose
|
||||
* other fields have other types). Null for any other key or shape:
|
||||
* accessor-carrying shapes, index-signature shapes and JavaScript sources
|
||||
* keep their own keyed access. */
|
||||
export function sameTypedFieldKeys(
|
||||
lowerer: Lowerer,
|
||||
shape: IrRecordShape,
|
||||
keyNode: ts.Expression,
|
||||
): { keys: string[]; fieldType: IrType } | null {
|
||||
if (
|
||||
shape.tuple ||
|
||||
shape.indexValue !== undefined ||
|
||||
shapeHasAccessorSlots(shape) ||
|
||||
isJsSourceFile(ts.getSourceFileOfNode(keyNode))
|
||||
)
|
||||
return null;
|
||||
const keys = literalFieldKeys(lowerer, keyNode);
|
||||
if (!keys) return null;
|
||||
const fields = keys.map((k) => shape.fields.find((f) => f.name === k));
|
||||
const first = fields[0];
|
||||
if (!first || fields.some((f) => !f || !typeEquals(f.type, first.type))) return null;
|
||||
return { keys, fieldType: first.type };
|
||||
}
|
||||
|
||||
/** Does `key` name `field`? */
|
||||
function keyIs(key: IrExpr, field: string, loc: SrcLoc): IrExpr {
|
||||
return {
|
||||
|
||||
@@ -561,8 +561,7 @@ function fenceAccessorSpreadSource(
|
||||
/** Only definitely present later records replace earlier defaults. A
|
||||
* callback binding may store undefined despite its checker annotation. */
|
||||
function laterSpreadType(lowerer: Lowerer, expr: ts.Expression): IrType | null {
|
||||
let source = expr;
|
||||
while (ts.isParenthesizedExpression(source)) source = source.expression;
|
||||
const source = spreadSourceNode(lowerer, expr);
|
||||
if (ts.isIdentifier(source)) return lowerer.lowerExpr(source).type;
|
||||
return lowerer.mapTypeOf(lowerer.typeOf(source));
|
||||
}
|
||||
@@ -1331,7 +1330,21 @@ export function lowerObjectLiteral(
|
||||
}
|
||||
lowerer.badType(expr, tsType);
|
||||
}
|
||||
let type: IrType = mapped;
|
||||
// An enclosing literal that built at its OWN type (its context had no
|
||||
// layout) asks for the matching own layout of this nested literal. When
|
||||
// this literal's contextual layout cannot reach that slot, build at it:
|
||||
// the enclosing literal's coercion then decides, and the width lifts
|
||||
// (the index-signature overflow capture included) carry the whole tree
|
||||
// into the real destination. Otherwise the two layers disagree and the
|
||||
// SC2002 names the source literal's own type as the expected one.
|
||||
const layout: IrType & { kind: "record" } =
|
||||
expected &&
|
||||
!typeEquals(mapped, expected) &&
|
||||
lowerer.widthLiftPlan(mapped, expected) === null &&
|
||||
typeEquals(lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped, expected)
|
||||
? expected
|
||||
: mapped;
|
||||
let type: IrType = layout;
|
||||
// A fresh, plain literal can construct its fields directly for a known
|
||||
// destination with the same field set. This matters for inferred
|
||||
// conditional records: an empty array in one arm has no element layout
|
||||
@@ -1907,16 +1920,19 @@ export function lowerObjectLiteral(
|
||||
// earlier ones (JS last-write-wins). Evaluate the source exactly
|
||||
// once, including empty sources and those whose fields are all
|
||||
// overridden. Field snapshots above preserve the copy order.
|
||||
let srcNode: ts.Expression = prop.expression;
|
||||
while (ts.isParenthesizedExpression(srcNode)) srcNode = srcNode.expression;
|
||||
const srcNode = spreadSourceNode(lowerer, prop.expression);
|
||||
const condRecord = conditionalRecordSpreadOf(lowerer, prop.expression);
|
||||
const srcLowered = capture(
|
||||
let srcLowered = capture(
|
||||
condRecord !== null
|
||||
? lowerConditionalRecordSpread(lowerer, condRecord, locOf(prop))
|
||||
: prop === expr.properties.nodes[0] && leadingSpreadLowered !== null
|
||||
? leadingSpreadLowered
|
||||
: lowerer.lowerExpr(srcNode),
|
||||
);
|
||||
{
|
||||
const narrowed = checkedSpreadSource(lowerer, srcNode, srcLowered);
|
||||
if (narrowed !== null) srcLowered = capture(narrowed);
|
||||
}
|
||||
// Array callbacks can receive a record-or-undefined ABI even when
|
||||
// TypeScript describes a required record. Select the copy strategy
|
||||
// from that stored representation so absent sources copy nothing.
|
||||
@@ -1938,7 +1954,7 @@ export function lowerObjectLiteral(
|
||||
const recArms = def?.arms.filter((a) => a.kind === "record") ?? [];
|
||||
if (
|
||||
!def ||
|
||||
recArms.length !== 1 ||
|
||||
recArms.length === 0 ||
|
||||
!def.arms.every((a) => a.kind === "record" || isUnitType(a))
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
@@ -1947,8 +1963,6 @@ export function lowerObjectLiteral(
|
||||
`object spread of '${lowerer.fmt(srcType)}' sources (only known record shapes spread — ${NARROW_FIRST})`,
|
||||
);
|
||||
}
|
||||
const recArm = recArms[0]! as IrType & { kind: "record" };
|
||||
const recTag = def.arms.indexOf(recArm);
|
||||
// A checked-dynamic VALUE under the union-mapped checker type
|
||||
// (a JS dyn-holding binding): the present/absent desugar tests
|
||||
// union tags a dyn box does not carry — fence honestly instead
|
||||
@@ -1961,242 +1975,218 @@ export function lowerObjectLiteral(
|
||||
`object spread of a checked-dynamic '${lowerer.fmt(srcType)}' source (${NARROW_FIRST})`,
|
||||
);
|
||||
}
|
||||
const srcShape = lowerer.shapes.get(recArm.shapeId);
|
||||
if (!srcShape)
|
||||
throw new InternalCompilerError(`lowerer bug: spread of unknown shape ${recArm.shapeId}`);
|
||||
fenceAccessorSpreadSource(lowerer, prop, srcShape);
|
||||
if (srcShape.indexValue || shape.indexValue) {
|
||||
const fallback = dynSpreadLiteral(lowerer, expr, type);
|
||||
if (fallback) return fallback;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
prop,
|
||||
"object spread involving index-signature shapes (overflow keys are runtime state — copy the fields you need explicitly)",
|
||||
);
|
||||
}
|
||||
const laterNames = new Set<string>();
|
||||
for (const later of expr.properties.nodes.slice(expr.properties.nodes.indexOf(prop) + 1)) {
|
||||
if (ts.isSpreadAssignment(later)) {
|
||||
if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression))
|
||||
continue;
|
||||
const lt = laterSpreadType(lowerer, later.expression);
|
||||
if (lt?.kind === "record") {
|
||||
// A field the later source may lack defines nothing.
|
||||
for (const lf of lowerer.shapes.get(lt.shapeId)?.fields ?? [])
|
||||
if (!canLackField(lf.type)) laterNames.add(lf.name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
later.name &&
|
||||
(ts.isIdentifier(later.name) ||
|
||||
ts.isStringLiteral(later.name) ||
|
||||
ts.isNumericLiteral(later.name) ||
|
||||
(ts.isComputedPropertyName(later.name) &&
|
||||
literalComputedKey(lowerer, later.name) !== null))
|
||||
) {
|
||||
laterNames.add(propNameText(lowerer, later.name));
|
||||
}
|
||||
}
|
||||
const srcRef = (): IrExpr => srcLowered ?? lowerer.lowerExpr(srcNode);
|
||||
const srcNarrowed = (): IrExpr => ({
|
||||
kind: "unionNarrow",
|
||||
unionId: srcType.unionId,
|
||||
tag: recTag,
|
||||
value: srcRef(),
|
||||
type: recArm,
|
||||
loc: locOf(prop),
|
||||
});
|
||||
// The source PROVEN present: the checked extraction `x!` lowers to
|
||||
// (narrowedArmHelper + checkedNarrowCall). A required target field
|
||||
// with no earlier contributor has no absent representation, so
|
||||
// `{ ...maybeRecord }` into it must find the record: the unit arm
|
||||
// throws the catchable TypeError instead of leaving the slot unset
|
||||
// (divergence 38's lying-assertion stance — the checker typed the
|
||||
// spread's fields optional, so only a cast or an unchecked indexed
|
||||
// read reaches this shape).
|
||||
const srcPresent = (): IrExpr => {
|
||||
const helper = lowerer.narrowedArmHelper(srcType.unionId, recArm, locOf(prop));
|
||||
return helper
|
||||
? lowerer.checkedNarrowCall(helper, srcRef(), recArm, locOf(prop))
|
||||
: srcNarrowed();
|
||||
};
|
||||
orderSegments.push({ kind: "spread", shapeId: recArm.shapeId, maybeAbsent: true });
|
||||
for (const f of srcShape.fields) {
|
||||
if (laterNames.has(f.name)) continue;
|
||||
const targetType = fieldTypes.get(f.name);
|
||||
// No slot on the target shape: the copy DROPS the field
|
||||
// (divergence 36's stance, same as the plain-record spread).
|
||||
if (!targetType) continue;
|
||||
if (conditionalNames.has(f.name)) {
|
||||
const conditionalDyn = conditionalFallback();
|
||||
if (conditionalDyn) return conditionalDyn;
|
||||
// Several record arms: each arm contributes its own fields behind its
|
||||
// tag test, wrapping the earlier contributor, so a key that arm lacks
|
||||
// keeps the earlier value (JS copies only the arm's own keys).
|
||||
for (const recArmCandidate of recArms) {
|
||||
const recArm = recArmCandidate as IrType & { kind: "record" };
|
||||
const recTag = def.arms.indexOf(recArm);
|
||||
const srcShape = lowerer.shapes.get(recArm.shapeId);
|
||||
if (!srcShape)
|
||||
throw new InternalCompilerError(
|
||||
`lowerer bug: spread of unknown shape ${recArm.shapeId}`,
|
||||
);
|
||||
fenceAccessorSpreadSource(lowerer, prop, srcShape);
|
||||
if (srcShape.indexValue || shape.indexValue) {
|
||||
const fallback = dynSpreadLiteral(lowerer, expr, type);
|
||||
if (fallback) return fallback;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
prop,
|
||||
`spread of '${f.name}' over an earlier conditional spread (the desugar keeps one entry per name — restructure so each name has one contributor)`,
|
||||
"object spread involving index-signature shapes (overflow keys are runtime state — copy the fields you need explicitly)",
|
||||
);
|
||||
}
|
||||
const fRead = (present = false): IrExpr => ({
|
||||
kind: "recordGet",
|
||||
obj: present ? srcPresent() : srcNarrowed(),
|
||||
shapeId: recArm.shapeId,
|
||||
field: f.name,
|
||||
type: f.type,
|
||||
loc: locOf(prop),
|
||||
});
|
||||
let cond: IrExpr = {
|
||||
kind: "unionIsTag",
|
||||
const laterNames = new Set<string>();
|
||||
for (const later of expr.properties.nodes.slice(
|
||||
expr.properties.nodes.indexOf(prop) + 1,
|
||||
)) {
|
||||
if (ts.isSpreadAssignment(later)) {
|
||||
if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression))
|
||||
continue;
|
||||
const lt = laterSpreadType(lowerer, later.expression);
|
||||
if (lt?.kind === "record") {
|
||||
// A field the later source may lack defines nothing.
|
||||
for (const lf of lowerer.shapes.get(lt.shapeId)?.fields ?? [])
|
||||
if (!canLackField(lf.type)) laterNames.add(lf.name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
later.name &&
|
||||
(ts.isIdentifier(later.name) ||
|
||||
ts.isStringLiteral(later.name) ||
|
||||
ts.isNumericLiteral(later.name) ||
|
||||
(ts.isComputedPropertyName(later.name) &&
|
||||
literalComputedKey(lowerer, later.name) !== null))
|
||||
) {
|
||||
laterNames.add(propNameText(lowerer, later.name));
|
||||
}
|
||||
}
|
||||
const srcRef = (): IrExpr => srcLowered ?? lowerer.lowerExpr(srcNode);
|
||||
const srcNarrowed = (): IrExpr => ({
|
||||
kind: "unionNarrow",
|
||||
unionId: srcType.unionId,
|
||||
tag: recTag,
|
||||
negated: false,
|
||||
value: srcRef(),
|
||||
type: BOOL,
|
||||
loc: locOf(prop),
|
||||
};
|
||||
let thenVal: IrExpr;
|
||||
// A source field that can be absent copies only when present.
|
||||
const fieldPresent = (): IrExpr => ({
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: cond,
|
||||
right: {
|
||||
kind: "recordHas",
|
||||
obj: {
|
||||
kind: "unionNarrow",
|
||||
unionId: srcType.unionId,
|
||||
tag: recTag,
|
||||
value: srcRef(),
|
||||
type: recArm,
|
||||
loc: locOf(prop),
|
||||
},
|
||||
shapeId: recArm.shapeId,
|
||||
field: f.name,
|
||||
type: BOOL,
|
||||
loc: locOf(prop),
|
||||
},
|
||||
type: BOOL,
|
||||
type: recArm,
|
||||
loc: locOf(prop),
|
||||
});
|
||||
const canBeAbsent =
|
||||
f.type.kind === "union" && lowerer.armTag(f.type.unionId, UNDEFINED_T) >= 0;
|
||||
if (typeEquals(f.type, targetType)) {
|
||||
thenVal = fRead();
|
||||
if (canBeAbsent) cond = fieldPresent();
|
||||
} else if (
|
||||
f.type.kind === "union" &&
|
||||
lowerer.armTag(f.type.unionId, UNDEFINED_T) >= 0 &&
|
||||
typeEquals(lowerer.stripUndefinedArm(f.type), targetType)
|
||||
) {
|
||||
// Optional source field into a required target slot: present
|
||||
// means the source holds the record AND the field its value
|
||||
// arm — the spread-override completion, union-source form.
|
||||
const ftUndef = lowerer.armTag(f.type.unionId, UNDEFINED_T);
|
||||
cond = {
|
||||
// The source PROVEN present: the checked extraction `x!` lowers to
|
||||
// (narrowedArmHelper + checkedNarrowCall). A required target field
|
||||
// with no earlier contributor has no absent representation, so
|
||||
// `{ ...maybeRecord }` into it must find the record: the unit arm
|
||||
// throws the catchable TypeError instead of leaving the slot unset
|
||||
// (divergence 38's lying-assertion stance — the checker typed the
|
||||
// spread's fields optional, so only a cast or an unchecked indexed
|
||||
// read reaches this shape).
|
||||
const srcPresent = (): IrExpr => {
|
||||
const helper = lowerer.narrowedArmHelper(srcType.unionId, recArm, locOf(prop));
|
||||
return helper
|
||||
? lowerer.checkedNarrowCall(helper, srcRef(), recArm, locOf(prop))
|
||||
: srcNarrowed();
|
||||
};
|
||||
orderSegments.push({ kind: "spread", shapeId: recArm.shapeId, maybeAbsent: true });
|
||||
for (const f of srcShape.fields) {
|
||||
if (laterNames.has(f.name)) continue;
|
||||
const targetType = fieldTypes.get(f.name);
|
||||
// No slot on the target shape: the copy DROPS the field
|
||||
// (divergence 36's stance, same as the plain-record spread).
|
||||
if (!targetType) continue;
|
||||
if (conditionalNames.has(f.name)) {
|
||||
const conditionalDyn = conditionalFallback();
|
||||
if (conditionalDyn) return conditionalDyn;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
prop,
|
||||
`spread of '${f.name}' over an earlier conditional spread (the desugar keeps one entry per name — restructure so each name has one contributor)`,
|
||||
);
|
||||
}
|
||||
const fRead = (present = false): IrExpr => ({
|
||||
kind: "recordGet",
|
||||
obj: present ? srcPresent() : srcNarrowed(),
|
||||
shapeId: recArm.shapeId,
|
||||
field: f.name,
|
||||
type: f.type,
|
||||
loc: locOf(prop),
|
||||
});
|
||||
let cond: IrExpr = {
|
||||
kind: "unionIsTag",
|
||||
unionId: srcType.unionId,
|
||||
tag: recTag,
|
||||
negated: false,
|
||||
value: srcRef(),
|
||||
type: BOOL,
|
||||
loc: locOf(prop),
|
||||
};
|
||||
let thenVal: IrExpr;
|
||||
// A source field that can be absent copies only when present.
|
||||
const fieldPresent = (): IrExpr => ({
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: cond,
|
||||
right: {
|
||||
kind: "unionIsTag",
|
||||
unionId: f.type.unionId,
|
||||
tag: ftUndef,
|
||||
negated: true,
|
||||
value: fRead(),
|
||||
kind: "recordHas",
|
||||
obj: {
|
||||
kind: "unionNarrow",
|
||||
unionId: srcType.unionId,
|
||||
tag: recTag,
|
||||
value: srcRef(),
|
||||
type: recArm,
|
||||
loc: locOf(prop),
|
||||
},
|
||||
shapeId: recArm.shapeId,
|
||||
field: f.name,
|
||||
type: BOOL,
|
||||
loc: locOf(prop),
|
||||
},
|
||||
type: BOOL,
|
||||
loc: locOf(prop),
|
||||
};
|
||||
const ftDef = lowerer.unions.get(f.type.unionId);
|
||||
if (targetType.kind === "union") {
|
||||
const retag = lowerer.unionRetagHelper(
|
||||
f.type.unionId,
|
||||
targetType.unionId,
|
||||
locOf(prop),
|
||||
);
|
||||
if (!retag) {
|
||||
});
|
||||
const canBeAbsent =
|
||||
f.type.kind === "union" && lowerer.armTag(f.type.unionId, UNDEFINED_T) >= 0;
|
||||
if (typeEquals(f.type, targetType)) {
|
||||
thenVal = fRead();
|
||||
if (canBeAbsent) cond = fieldPresent();
|
||||
} else if (
|
||||
f.type.kind === "union" &&
|
||||
lowerer.armTag(f.type.unionId, UNDEFINED_T) >= 0 &&
|
||||
typeEquals(lowerer.stripUndefinedArm(f.type), targetType)
|
||||
) {
|
||||
// Optional source field into a required target slot: present
|
||||
// means the source holds the record AND the field its value
|
||||
// arm — the spread-override completion, union-source form.
|
||||
const ftUndef = lowerer.armTag(f.type.unionId, UNDEFINED_T);
|
||||
cond = {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: cond,
|
||||
right: {
|
||||
kind: "unionIsTag",
|
||||
unionId: f.type.unionId,
|
||||
tag: ftUndef,
|
||||
negated: true,
|
||||
value: fRead(),
|
||||
type: BOOL,
|
||||
loc: locOf(prop),
|
||||
},
|
||||
type: BOOL,
|
||||
loc: locOf(prop),
|
||||
};
|
||||
const ftDef = lowerer.unions.get(f.type.unionId);
|
||||
if (targetType.kind === "union") {
|
||||
const retag = lowerer.unionRetagHelper(
|
||||
f.type.unionId,
|
||||
targetType.unionId,
|
||||
locOf(prop),
|
||||
);
|
||||
if (!retag) {
|
||||
lowerer.pushDiag(
|
||||
recordShapeMismatchDiag(
|
||||
lowerer.fmt(type),
|
||||
lowerer.fmt(recArm),
|
||||
locOf(prop),
|
||||
`spread field '${f.name}': '${lowerer.fmt(f.type)}' cannot re-tag into '${lowerer.fmt(targetType)}' behind the present-test`,
|
||||
),
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
thenVal = {
|
||||
kind: "call",
|
||||
callee: retag,
|
||||
args: [fRead()],
|
||||
type: targetType,
|
||||
loc: locOf(prop),
|
||||
};
|
||||
} else if (
|
||||
ftDef &&
|
||||
ftDef.arms.length === 2 &&
|
||||
lowerer.armTag(f.type.unionId, targetType) >= 0
|
||||
) {
|
||||
thenVal = {
|
||||
kind: "unionNarrow",
|
||||
unionId: f.type.unionId,
|
||||
tag: lowerer.armTag(f.type.unionId, targetType),
|
||||
value: fRead(),
|
||||
type: targetType,
|
||||
loc: locOf(prop),
|
||||
};
|
||||
} else {
|
||||
lowerer.pushDiag(
|
||||
recordShapeMismatchDiag(
|
||||
lowerer.fmt(type),
|
||||
lowerer.fmt(recArm),
|
||||
locOf(prop),
|
||||
`spread field '${f.name}': '${lowerer.fmt(f.type)}' cannot re-tag into '${lowerer.fmt(targetType)}' behind the present-test`,
|
||||
`spread field '${f.name}': '${lowerer.fmt(f.type)}' cannot narrow into '${lowerer.fmt(targetType)}' behind the present-test`,
|
||||
),
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
thenVal = {
|
||||
kind: "call",
|
||||
callee: retag,
|
||||
args: [fRead()],
|
||||
type: targetType,
|
||||
loc: locOf(prop),
|
||||
};
|
||||
} else if (
|
||||
ftDef &&
|
||||
ftDef.arms.length === 2 &&
|
||||
lowerer.armTag(f.type.unionId, targetType) >= 0
|
||||
) {
|
||||
thenVal = {
|
||||
kind: "unionNarrow",
|
||||
unionId: f.type.unionId,
|
||||
tag: lowerer.armTag(f.type.unionId, targetType),
|
||||
value: fRead(),
|
||||
type: targetType,
|
||||
loc: locOf(prop),
|
||||
};
|
||||
} else {
|
||||
lowerer.pushDiag(
|
||||
recordShapeMismatchDiag(
|
||||
lowerer.fmt(type),
|
||||
lowerer.fmt(recArm),
|
||||
locOf(prop),
|
||||
`spread field '${f.name}': '${lowerer.fmt(f.type)}' cannot narrow into '${lowerer.fmt(targetType)}' behind the present-test`,
|
||||
),
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
} else {
|
||||
// The width-lift fallback (arm wrap, re-tag, nested
|
||||
// reshape) — the same per-field rule the slot coercion
|
||||
// applies. Runs AFTER the optional-completion branch: a
|
||||
// present-test has its own semantics a re-tag's stranded
|
||||
// undefined trap must not shadow.
|
||||
const lift = lowerer.widthLiftPlan(f.type, targetType);
|
||||
if (!lift) {
|
||||
lowerer.pushDiag(
|
||||
recordShapeMismatchDiag(
|
||||
lowerer.fmt(type),
|
||||
lowerer.fmt(recArm),
|
||||
locOf(prop),
|
||||
`spread field '${f.name}': '${lowerer.fmt(f.type)}' does not lift into '${lowerer.fmt(targetType)}'`,
|
||||
),
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
thenVal = lowerer.applyWidthLift(lift, fRead(), targetType, locOf(prop));
|
||||
if (canBeAbsent) cond = fieldPresent();
|
||||
}
|
||||
const at = fields.findIndex((x) => x.name === f.name);
|
||||
const elseVal =
|
||||
at >= 0 ? fields[at]!.value : lowerer.absentFieldValue(targetType, locOf(prop));
|
||||
if (!elseVal) {
|
||||
if (canBeAbsent) {
|
||||
// The FIELD itself can be missing from a present source:
|
||||
// no extraction makes it exist.
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
prop,
|
||||
`object spread of '${lowerer.fmt(srcType)}' sources where '${f.name}' has no earlier contributor (the absent arm leaves the required field unset — spread defaults first: { ...defaults, ...overrides })`,
|
||||
);
|
||||
}
|
||||
// No earlier contributor and the target slot cannot be absent:
|
||||
// the source must be here. Copy through the checked extraction
|
||||
// (the absent unit arm throws) — the same read the width lift
|
||||
// below wraps, so a lift still applies.
|
||||
let strict = fRead(true);
|
||||
if (!typeEquals(f.type, targetType)) {
|
||||
// The width-lift fallback (arm wrap, re-tag, nested
|
||||
// reshape) — the same per-field rule the slot coercion
|
||||
// applies. Runs AFTER the optional-completion branch: a
|
||||
// present-test has its own semantics a re-tag's stranded
|
||||
// undefined trap must not shadow.
|
||||
const lift = lowerer.widthLiftPlan(f.type, targetType);
|
||||
if (!lift) {
|
||||
lowerer.pushDiag(
|
||||
@@ -2209,21 +2199,56 @@ export function lowerObjectLiteral(
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
strict = lowerer.applyWidthLift(lift, strict, targetType, locOf(prop));
|
||||
thenVal = lowerer.applyWidthLift(lift, fRead(), targetType, locOf(prop));
|
||||
if (canBeAbsent) cond = fieldPresent();
|
||||
}
|
||||
fields.push({ name: f.name, value: strict });
|
||||
continue;
|
||||
const at = fields.findIndex((x) => x.name === f.name);
|
||||
const elseVal =
|
||||
at >= 0 ? fields[at]!.value : lowerer.absentFieldValue(targetType, locOf(prop));
|
||||
if (!elseVal) {
|
||||
if (canBeAbsent) {
|
||||
// The FIELD itself can be missing from a present source:
|
||||
// no extraction makes it exist.
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
prop,
|
||||
`object spread of '${lowerer.fmt(srcType)}' sources where '${f.name}' has no earlier contributor (the absent arm leaves the required field unset — spread defaults first: { ...defaults, ...overrides })`,
|
||||
);
|
||||
}
|
||||
// No earlier contributor and the target slot cannot be absent:
|
||||
// the source must be here. Copy through the checked extraction
|
||||
// (the absent unit arm throws) — the same read the width lift
|
||||
// below wraps, so a lift still applies.
|
||||
let strict = fRead(true);
|
||||
if (!typeEquals(f.type, targetType)) {
|
||||
const lift = lowerer.widthLiftPlan(f.type, targetType);
|
||||
if (!lift) {
|
||||
lowerer.pushDiag(
|
||||
recordShapeMismatchDiag(
|
||||
lowerer.fmt(type),
|
||||
lowerer.fmt(recArm),
|
||||
locOf(prop),
|
||||
`spread field '${f.name}': '${lowerer.fmt(f.type)}' does not lift into '${lowerer.fmt(targetType)}'`,
|
||||
),
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
strict = lowerer.applyWidthLift(lift, strict, targetType, locOf(prop));
|
||||
}
|
||||
fields.push({ name: f.name, value: strict });
|
||||
continue;
|
||||
}
|
||||
const merged: IrExpr = {
|
||||
kind: "ternary",
|
||||
cond,
|
||||
then: thenVal,
|
||||
else_: elseVal,
|
||||
type: targetType,
|
||||
loc: locOf(prop),
|
||||
};
|
||||
if (at >= 0) fields[at] = { name: f.name, value: merged };
|
||||
else fields.push({ name: f.name, value: merged });
|
||||
}
|
||||
const merged: IrExpr = {
|
||||
kind: "ternary",
|
||||
cond,
|
||||
then: thenVal,
|
||||
else_: elseVal,
|
||||
type: targetType,
|
||||
loc: locOf(prop),
|
||||
};
|
||||
if (at >= 0) fields[at] = { name: f.name, value: merged };
|
||||
else fields.push({ name: f.name, value: merged });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -2649,6 +2674,32 @@ export function lowerObjectLiteral(
|
||||
: { kind: "seqExpr", stmts: spreadStmts, result, type, loc };
|
||||
}
|
||||
|
||||
/** A spread source whose stored union holds several record arms while the
|
||||
* checker narrowed this site to fewer (`if (b?.type === "tool") ({ ...b })`
|
||||
* on an element read, or a type predicate's arm): extract the narrowed
|
||||
* value through the CHECKED narrowing, so every arm the checker excludes,
|
||||
* including a hidden absence arm, throws the catchable TypeError instead of
|
||||
* being copied. Null when the stored union has at most one record arm (the
|
||||
* present-test desugar owns that case), when the site type is not a
|
||||
* narrowing of the stored union, or in JavaScript files. */
|
||||
function checkedSpreadSource(lowerer: Lowerer, node: ts.Expression, stored: IrExpr): IrExpr | null {
|
||||
if (stored.type.kind !== "union") return null;
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(node))) return null;
|
||||
const def = lowerer.unions.get(stored.type.unionId);
|
||||
if (!def || def.arms.filter((arm) => arm.kind === "record").length < 2) return null;
|
||||
const site = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
if (!site || typeEquals(site, stored.type)) return null;
|
||||
const loc = stored.loc;
|
||||
if (site.kind === "record") {
|
||||
if (lowerer.armTag(stored.type.unionId, site) < 0) return null;
|
||||
const helper = lowerer.narrowedArmHelper(stored.type.unionId, site, loc);
|
||||
return helper ? lowerer.checkedNarrowCall(helper, stored, site, loc) : null;
|
||||
}
|
||||
if (site.kind !== "union") return null;
|
||||
const helper = lowerer.narrowedRetagHelper(node, stored.type.unionId, site.unionId, loc);
|
||||
return helper ? { kind: "call", callee: helper, args: [stored], type: site, loc } : null;
|
||||
}
|
||||
|
||||
/** The field type is an array, or a union with exactly one array arm. */
|
||||
function singleArrayArm(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (type.kind === "array") return true;
|
||||
@@ -2782,6 +2833,20 @@ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpressio
|
||||
};
|
||||
}
|
||||
}
|
||||
// Any other source (a capture, a runtime-optional binding, a member
|
||||
// read) converts through the same checked re-tag: the arms the site's
|
||||
// checker type excludes, a hidden absence arm included, throw the
|
||||
// catchable TypeError instead of being cloned.
|
||||
if (!typeEquals(source.type, sourceType) && source.type.kind === "union") {
|
||||
const helper = lowerer.narrowedRetagHelper(
|
||||
spread.expression,
|
||||
source.type.unionId,
|
||||
sourceType.unionId,
|
||||
source.loc,
|
||||
);
|
||||
if (helper)
|
||||
source = { kind: "call", callee: helper, args: [source], type: sourceType, loc: source.loc };
|
||||
}
|
||||
if (!typeEquals(source.type, sourceType)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -2984,14 +3049,51 @@ function lowerConditionalRecordSpread(
|
||||
};
|
||||
}
|
||||
|
||||
/** A spread's source expression with parentheses stripped, seeing through
|
||||
* an empty-object default: `...(x ?? {})` and `...(x || {})` spread exactly
|
||||
* what `...x` spreads (a nullish or other falsy value is a primitive with
|
||||
* no own keys, and x evaluates once either way). Only where `x` is a plain
|
||||
* record or a record-or-unit union, which the field copy and the
|
||||
* optional-spread desugar handle; index-signature and other sources keep
|
||||
* their paths. */
|
||||
function spreadSourceNode(lowerer: Lowerer, expr: ts.Expression): ts.Expression {
|
||||
let e: ts.Expression = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (
|
||||
!ts.isBinaryExpression(e) ||
|
||||
(e.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionToken &&
|
||||
e.operatorToken.kind !== ts.SyntaxKind.BarBarToken)
|
||||
)
|
||||
return e;
|
||||
let right: ts.Expression = e.right;
|
||||
while (ts.isParenthesizedExpression(right)) right = right.expression;
|
||||
if (!ts.isObjectLiteralExpression(right) || right.properties.nodes.length !== 0) return e;
|
||||
let left: ts.Expression = e.left;
|
||||
while (ts.isParenthesizedExpression(left)) left = left.expression;
|
||||
const plainRecord = (t: IrType | undefined): boolean =>
|
||||
t?.kind === "record" && lowerer.shapes.get(t.shapeId)?.indexValue === undefined;
|
||||
const source = lowerer.mapTypeOf(lowerer.typeOf(left));
|
||||
if (plainRecord(source ?? undefined)) return left;
|
||||
if (source?.kind === "union") {
|
||||
const arms = lowerer.unions.get(source.unionId)?.arms ?? [];
|
||||
const records = arms.filter((arm) => arm.kind === "record");
|
||||
if (
|
||||
records.length === 1 &&
|
||||
plainRecord(records[0]) &&
|
||||
arms.every((arm) => arm.kind === "record" || isUnitType(arm))
|
||||
)
|
||||
return left;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
/** A spread source the record desugar cannot enumerate: an untyped
|
||||
* (`unknown`) value, or one whose shape carries an index signature
|
||||
* (`Record<string, unknown>`, `{ [key: string]: T }`) so its keys are
|
||||
* runtime state. Node copies own enumerable keys in insertion order, which
|
||||
* the checked-dynamic object reproduces. */
|
||||
function dynSpreadSource(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
let e: ts.Expression = node;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
const e = spreadSourceNode(lowerer, node);
|
||||
if ((lowerer.typeOf(e).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return true;
|
||||
const indexed = (t: IrType | null): boolean => {
|
||||
if (t?.kind === "record") return lowerer.shapes.get(t.shapeId)?.indexValue !== undefined;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
lowerArrayClassStaticCall,
|
||||
} from "./builtins/array-classes.js";
|
||||
import { structuralCallInstance } from "./structural-instances.js";
|
||||
import { classViewOf } from "./class-views.js";
|
||||
import { checkedPrivateMethodReceiver, privateNameClass } from "./private-brands.js";
|
||||
import { lowerStaticPrivateCall } from "./private-statics.js";
|
||||
import { assertFunctionAliasOf } from "./lower-assert.js";
|
||||
@@ -11520,7 +11521,8 @@ function maybePromiseHandlerResult(
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/** `view.m(args)` where `view` is a `Pick<C, K>` / `Omit<C, K>` view of a
|
||||
/** `view.m(args)` where `view` is a `Pick<C, K>` / `Omit<C, K>` view (inline
|
||||
* or through type aliases, classViewOf) of a
|
||||
* program class C and `m` is one of C's GENERIC methods (its overloads
|
||||
* included): generic methods have no dispatchable method value, so the
|
||||
* view's live receiver is extracted as the C instance it holds (a checked
|
||||
@@ -11533,11 +11535,9 @@ function pickViewGenericMethodCall(
|
||||
access: ts.PropertyAccessExpression,
|
||||
recvTs: ts.Type,
|
||||
): IrExpr | null {
|
||||
const alias = recvTs.getAliasSymbol();
|
||||
if (alias === undefined || (ts.symbolName(alias) !== "Pick" && ts.symbolName(alias) !== "Omit"))
|
||||
return null;
|
||||
if (!alias.declarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) return null;
|
||||
const source = recvTs.getAliasTypeArguments()[0];
|
||||
const view = classViewOf(lowerer.checker, recvTs);
|
||||
if (view === null) return null;
|
||||
const source = view.source;
|
||||
const symbol = source?.getSymbol();
|
||||
const info = symbol ? lowerer.classBySymbol.get(symbol) : undefined;
|
||||
if (!info || call.questionDotToken || access.questionDotToken) return null;
|
||||
@@ -11557,7 +11557,7 @@ function pickViewGenericMethodCall(
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
access,
|
||||
`calling the generic method '${access.name.text}' through a '${ts.symbolName(alias)}' view of '${info.def.name}' that holds a '${lowerer.fmt(lowered.type)}'`,
|
||||
`calling the generic method '${access.name.text}' through a '${view.name}' view of '${info.def.name}' that holds a '${lowerer.fmt(lowered.type)}'`,
|
||||
);
|
||||
}
|
||||
const receiver: IrExpr =
|
||||
|
||||
@@ -24,7 +24,12 @@ import {
|
||||
sharedModuleRegistrations,
|
||||
} from "./lower-shared-modules.js";
|
||||
import { dirname as dirnamePath, resolve as resolvePath } from "node:path";
|
||||
import { packageNameOfPath, probeNodeImportRefusal } from "../npm.js";
|
||||
import {
|
||||
packageNameOfPath,
|
||||
probeNodeFileImportRefusal,
|
||||
probeNodeImportRefusal,
|
||||
type NodeImportRefusal,
|
||||
} from "../npm.js";
|
||||
import { isNpmStaticPackage } from "../npm-static.js";
|
||||
import { isJsSourceFileName } from "../tsc-codes.js";
|
||||
import { isRelativeSpecifier } from "../workspace-registry.js";
|
||||
@@ -753,23 +758,38 @@ export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void {
|
||||
* runtime-computed name). */
|
||||
/** The refusal a literal import() rejects with, or null when it loads:
|
||||
* Node's ERR_MODULE_NOT_FOUND for a package the project marks never loaded
|
||||
* (package-shims.ts), or the first refusal in the static graph of a module
|
||||
* only import() loads (LoadResult.dynamicImportRejections). */
|
||||
* (package-shims.ts), the first refusal in the static graph of a module
|
||||
* only import() loads (LoadResult.dynamicImportRejections), or Node's
|
||||
* resolution refusal for a specifier nothing resolves (a package nothing
|
||||
* installed provides, a relative path to a file that does not exist). */
|
||||
function dynamicImportRejectionOf(
|
||||
lowerer: Lowerer,
|
||||
sf: ts.SourceFile,
|
||||
spec: string,
|
||||
call: ts.CallExpression,
|
||||
): StartupCrash | null {
|
||||
if (isUnloadedPackage(spec)) {
|
||||
const refusal = probeNodeImportRefusal(sf.fileName, spec);
|
||||
if (refusal === null) return null;
|
||||
return { message: refusal.message, className: "%Error", code: refusal.code, loc: locOf(call) };
|
||||
}
|
||||
if (lowerer.dynamicImportRejections.size === 0) return null;
|
||||
const rejection = (refusal: NodeImportRefusal | null): StartupCrash | null =>
|
||||
refusal === null
|
||||
? null
|
||||
: {
|
||||
message: refusal.message,
|
||||
className: "%Error",
|
||||
code: refusal.code,
|
||||
...(refusal.url === undefined ? {} : { url: refusal.url }),
|
||||
loc: locOf(call),
|
||||
};
|
||||
if (isUnloadedPackage(spec)) return rejection(probeNodeImportRefusal(sf.fileName, spec));
|
||||
const root =
|
||||
resolveImport(lowerer.program, sf, spec) ?? npmStaticDepSf7(lowerer.program, sf, spec);
|
||||
return root === null ? null : (lowerer.dynamicImportRejections.get(root) ?? null);
|
||||
if (root !== null) return lowerer.dynamicImportRejections.get(root) ?? null;
|
||||
// Builtins, and URL specifiers (file:, data:), are not package names.
|
||||
if (canonicalBuiltinModule(spec) !== null || /^[a-z][a-z0-9+.-]*:/i.test(spec)) return null;
|
||||
return rejection(
|
||||
probeNodeFileImportRefusal(sf.fileName, spec) ??
|
||||
(spec.startsWith(".") || spec.startsWith("/")
|
||||
? null
|
||||
: probeNodeImportRefusal(sf.fileName, spec)),
|
||||
);
|
||||
}
|
||||
|
||||
function collectDynamicImports(lowerer: Lowerer, sf: ts.SourceFile): void {
|
||||
|
||||
@@ -2290,7 +2290,7 @@ function lowerRejectedImport(
|
||||
{ kind: "exprStmt", expr: hop, loc },
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: nodeThrowExpr(errorKind, crash.code ?? "", crash.message, VOID, loc),
|
||||
expr: nodeThrowExpr(errorKind, crash.code ?? "", crash.message, VOID, loc, crash.url),
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: dynUndefinedExpr(loc), loc },
|
||||
|
||||
@@ -40,6 +40,7 @@ import { lowerCollectionSpread } from "./containers/collection-methods.js";
|
||||
import { lowerEventObjectPropertyWrite } from "./lower-event-target.js";
|
||||
import { lowerDynObjectLiteral } from "./expressions/object-literals.js";
|
||||
import { importMetaBindingSource } from "./import-meta.js";
|
||||
import { nullishEmptyPatternSource } from "./nullish-empty-pattern.js";
|
||||
import { anyResidueReplacedBy } from "./any-collection-residue.js";
|
||||
import { iteratorCanStep, iteratorValue } from "./iterator-consumption.js";
|
||||
import { notIterableSpelling } from "./iterable-spelling.js";
|
||||
@@ -2507,6 +2508,7 @@ export function lowerDestructuringDecl(
|
||||
}
|
||||
let init =
|
||||
importMetaBindingSource(lowerer, decl.name, decl.initializer) ??
|
||||
nullishEmptyPatternSource(lowerer, decl.name, decl.initializer) ??
|
||||
lowerer.lowerExpr(decl.initializer);
|
||||
const parseArgsDynObject =
|
||||
init.type.kind === "dyn" &&
|
||||
|
||||
@@ -97,6 +97,7 @@ import {
|
||||
import { identityPreservingWidening } from "./coercions/identity.js";
|
||||
import { scanDivergences } from "./divergences.js";
|
||||
import { applyKeyOrderWitnesses, KeyOrderNotes } from "./key-order-witness.js";
|
||||
import { functionKeepsRecordSources } from "./record-sources.js";
|
||||
import { exprFunctionReference } from "../../ir/function-references.js";
|
||||
import { everyStmtList, observeRecordLiteralRebuilds } from "../../ir/traverse.js";
|
||||
import { checkedClassAssertion } from "./class-assertions.js";
|
||||
@@ -3401,6 +3402,11 @@ export class Lowerer {
|
||||
* Reachability lowers inits before the declarations they discover, so
|
||||
* these rebuild after the worklist restores historical shape metadata. */
|
||||
readonly shapeOrderHelperFinalizers: (() => void)[] = [];
|
||||
/** Whether any lowered function converts a checked-dynamic value into a
|
||||
* closed record, the only conversion that can leave a record holding
|
||||
* undeclared source keys (record-sources.ts). Settled once lowering ends,
|
||||
* before the shape helpers finalize their bodies. */
|
||||
recordSourcesPossible = false;
|
||||
/** Construction witnesses that choose each shape's enumeration order
|
||||
* once lowering settles (key-order-witness.ts). */
|
||||
readonly keyOrder = new KeyOrderNotes();
|
||||
@@ -4560,6 +4566,9 @@ export class Lowerer {
|
||||
// statement.
|
||||
"sharedModules",
|
||||
"sharedInitFlags",
|
||||
// Settled from every lowered function once lowering ends, before the
|
||||
// shape helpers finalize (record-sources.ts).
|
||||
"recordSourcesPossible",
|
||||
"slotOriginMemo",
|
||||
"recordOriginMemo",
|
||||
"boundaryCastOrder",
|
||||
@@ -10720,6 +10729,22 @@ export class Lowerer {
|
||||
...this.implicitFns,
|
||||
]);
|
||||
observeRecordLiteralRebuilds(null);
|
||||
// A program whose functions never convert a checked-dynamic value into a
|
||||
// closed record cannot keep a source view anywhere, so the captures that
|
||||
// consult one keep their static copy (record-sources.ts).
|
||||
const allFunctions = [
|
||||
...orderedUnits,
|
||||
...initFunctions,
|
||||
...instanceFunctions,
|
||||
...this.liftedFns,
|
||||
...this.implicitFns,
|
||||
];
|
||||
const sourceScan = { memo: new Map<string, boolean>(), visiting: new Set<string>() };
|
||||
for (const fn of allFunctions) {
|
||||
if (!functionKeepsRecordSources(this, fn, sourceScan)) continue;
|
||||
this.recordSourcesPossible = true;
|
||||
break;
|
||||
}
|
||||
for (const finalize of this.shapeOrderHelperFinalizers) finalize();
|
||||
this.settleSharedHelperLocations([
|
||||
...orderedUnits,
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { BOOL, type IrExpr, type IrType, isUnitType, typeKey } from "../../ir/ir.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
function stripParens(node: ts.Expression): ts.Expression {
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
return node;
|
||||
}
|
||||
|
||||
/** `const { a, ...rest } = options ?? {}` (also `|| {}`) where the options
|
||||
* record has a member that cannot be absent. The pattern destructures
|
||||
* either the record or an empty object, which has no own keys, so the
|
||||
* source lowers to a union of the record and the empty record: the union
|
||||
* pattern lowering reads each key from the arm that holds it and answers
|
||||
* undefined from the empty arm, and a rest element packs the record's
|
||||
* remaining fields or nothing. The options operand is evaluated once.
|
||||
*
|
||||
* Null (the caller keeps its current lowering) for JavaScript files, other
|
||||
* initializer forms, an operand the checker does not type as one record
|
||||
* plus nullish arms, and a record whose every field may be absent: the
|
||||
* existing lowering already builds the empty object as that record with
|
||||
* every key absent. */
|
||||
export function nullishEmptyPatternSource(
|
||||
lowerer: Lowerer,
|
||||
pattern: ts.BindingName,
|
||||
initializer: ts.Expression,
|
||||
): IrExpr | null {
|
||||
if (!ts.isObjectBindingPattern(pattern)) return null;
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(initializer))) return null;
|
||||
const init = stripParens(initializer);
|
||||
if (
|
||||
!ts.isBinaryExpression(init) ||
|
||||
(init.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionToken &&
|
||||
init.operatorToken.kind !== ts.SyntaxKind.BarBarToken)
|
||||
)
|
||||
return null;
|
||||
const right = stripParens(init.right);
|
||||
if (!ts.isObjectLiteralExpression(right) || right.properties.nodes.length !== 0) return null;
|
||||
const loc = locOf(init);
|
||||
// Decide from the checker's view of the operand before lowering it once.
|
||||
const declared = lowerer.mapTypeOf(lowerer.typeOf(init.left));
|
||||
if (declared?.kind !== "union") return null;
|
||||
const declaredArms = lowerer.unions.get(declared.unionId)?.arms ?? [];
|
||||
const declaredRecords = declaredArms.filter((arm) => arm.kind === "record");
|
||||
if (
|
||||
declaredRecords.length !== 1 ||
|
||||
!declaredArms.every((arm) => arm.kind === "record" || isUnitType(arm))
|
||||
)
|
||||
return null;
|
||||
const declaredShape = lowerer.shapes.get(
|
||||
(declaredRecords[0] as IrType & { kind: "record" }).shapeId,
|
||||
);
|
||||
if (
|
||||
!declaredShape ||
|
||||
declaredShape.tuple ||
|
||||
declaredShape.indexValue ||
|
||||
declaredShape.fields.length === 0 ||
|
||||
declaredShape.fields.every(
|
||||
(field) => lowerer.absentFieldValue(field.type, loc)?.kind === "fieldAbsent",
|
||||
)
|
||||
)
|
||||
return null;
|
||||
const left = lowerer.lowerExpr(init.left);
|
||||
// The stored operand can differ from the checker's view. A stored record
|
||||
// is never nullish (the fallback is dead); any other stored form must be
|
||||
// the same one record plus nullish arms.
|
||||
if (left.type.kind === "record") return left;
|
||||
if (left.type.kind !== "union") {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
init,
|
||||
"destructuring `x ?? {}` where the stored x is not one record or nullish (narrow first)",
|
||||
);
|
||||
}
|
||||
const sourceId = left.type.unionId;
|
||||
const arms = lowerer.unions.get(sourceId)?.arms ?? [];
|
||||
const records = arms.filter((arm) => arm.kind === "record");
|
||||
if (records.length !== 1 || !arms.every((arm) => arm.kind === "record" || isUnitType(arm))) {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
init,
|
||||
"destructuring `x ?? {}` where the stored x is not one record or nullish (narrow first)",
|
||||
);
|
||||
}
|
||||
const record = records[0] as IrType & { kind: "record" };
|
||||
const empty: IrType = { kind: "record", shapeId: lowerer.shapes.intern([]) };
|
||||
const resultArms = [record, empty].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
|
||||
const resultType: IrType = { kind: "union", unionId: lowerer.unions.intern(resultArms) };
|
||||
const sourceTag = arms.indexOf(record);
|
||||
const tmp = lowerer.declareHiddenLocal("%nullishEmpty", left.type);
|
||||
const ref: IrExpr = { kind: "varRef", localId: tmp.id, type: left.type, loc };
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: tmp.id, init: left, loc }],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId: sourceId,
|
||||
tag: sourceTag,
|
||||
negated: false,
|
||||
value: ref,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: {
|
||||
kind: "unionWrap",
|
||||
unionId: resultType.unionId,
|
||||
tag: resultArms.indexOf(record),
|
||||
value: {
|
||||
kind: "unionNarrow",
|
||||
unionId: sourceId,
|
||||
tag: sourceTag,
|
||||
value: ref,
|
||||
type: record,
|
||||
loc,
|
||||
},
|
||||
type: resultType,
|
||||
loc,
|
||||
},
|
||||
else_: {
|
||||
kind: "unionWrap",
|
||||
unionId: resultType.unionId,
|
||||
tag: resultArms.indexOf(empty),
|
||||
value: { kind: "recordLit", fields: [], type: empty, loc },
|
||||
type: resultType,
|
||||
loc,
|
||||
},
|
||||
type: resultType,
|
||||
loc,
|
||||
},
|
||||
type: resultType,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
@@ -8,9 +8,12 @@
|
||||
* serialization, untyped views, inspect, Object.keys/values/entries —
|
||||
* answer through the record's live checked view instead of its static
|
||||
* field list. This module names the record types that can carry such a
|
||||
* source and the runtime test the surfaces branch on. */
|
||||
import type { IrExpr, IrType, SrcLoc } from "../../ir/ir.js";
|
||||
* source, the runtime test the surfaces branch on, and whether the program
|
||||
* converts anything into one at all (a program without such a conversion
|
||||
* answers the test false everywhere, so its captures keep static copies). */
|
||||
import type { IrExpr, IrFunction, IrType, SrcLoc } from "../../ir/ir.js";
|
||||
import { BOOL, DYN, recordTextCodecClass, shapeHasAccessorSlots } from "../../ir/ir.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { isPerfEntryType } from "./builtins/performance.js";
|
||||
@@ -37,6 +40,84 @@ export function recordSourceTest(value: IrExpr, loc: SrcLoc): IrExpr {
|
||||
return { kind: "libCall", fn: "record.sourceHas", args: [value], type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** A union or record revisited while it is being visited answers as if its
|
||||
* cycle member were empty (`keeps: false`), so the caller's answer is
|
||||
* incomplete until the outer one settles. `cycled` marks that incompleteness: such an answer is
|
||||
* never memoized, because another entry point into the same cycle could reach
|
||||
* the missing arm. */
|
||||
interface KeepsSourceAnswer {
|
||||
keeps: boolean;
|
||||
cycled: boolean;
|
||||
}
|
||||
|
||||
/** Shared work list of the whole-program scan: settled union and record
|
||||
* answers, and the unions and records on the current path, by id. */
|
||||
export interface KeepsSourceScan {
|
||||
memo: Map<string, boolean>;
|
||||
visiting: Set<string>;
|
||||
}
|
||||
|
||||
/** Whether a checked conversion into `target` can leave a record holding
|
||||
* undeclared source keys. The conversion reads one record at a time, so the
|
||||
* records nested in a union arm, a promise, an array, a record field or an
|
||||
* index signature's values count too (`Record<string, { x: string }>` keeps
|
||||
* the sources of its closed values). Unions and records are memoized by id;
|
||||
* one revisited on the current path answers `cycled`. */
|
||||
function conversionKeepsSource(
|
||||
lowerer: Lowerer,
|
||||
target: IrType,
|
||||
scan: KeepsSourceScan,
|
||||
): KeepsSourceAnswer {
|
||||
if (target.kind === "promise") return conversionKeepsSource(lowerer, target.inner, scan);
|
||||
if (target.kind === "array") return conversionKeepsSource(lowerer, target.elem, scan);
|
||||
if (target.kind === "record" && recordMayKeepSource(lowerer, target))
|
||||
return { keeps: true, cycled: false };
|
||||
let id: string;
|
||||
let members: IrType[];
|
||||
if (target.kind === "union") {
|
||||
id = target.unionId;
|
||||
members = lowerer.unions.get(target.unionId)?.arms ?? [];
|
||||
} else if (target.kind === "record") {
|
||||
id = target.shapeId;
|
||||
const shape = lowerer.shapes.get(target.shapeId);
|
||||
members = [
|
||||
...(shape?.fields.map((field) => field.type) ?? []),
|
||||
...(shape?.indexValue !== undefined ? [shape.indexValue] : []),
|
||||
];
|
||||
} else return { keeps: false, cycled: false };
|
||||
const known = scan.memo.get(id);
|
||||
if (known !== undefined) return { keeps: known, cycled: false };
|
||||
if (scan.visiting.has(id)) return { keeps: false, cycled: true };
|
||||
scan.visiting.add(id);
|
||||
let keeps = false;
|
||||
let cycled = false;
|
||||
for (const member of members) {
|
||||
const answer = conversionKeepsSource(lowerer, member, scan);
|
||||
keeps = keeps || answer.keeps;
|
||||
cycled = cycled || answer.cycled;
|
||||
if (keeps) break;
|
||||
}
|
||||
scan.visiting.delete(id);
|
||||
if (!cycled || keeps) scan.memo.set(id, keeps);
|
||||
return { keeps, cycled: cycled && !keeps };
|
||||
}
|
||||
|
||||
/** Whether `fn` holds a validated conversion that can note a record source:
|
||||
* a `dynCheck` whose target accepts `recordMayKeepSource`. Only such a
|
||||
* conversion creates the kept view `recordSourceTest` answers, so a program
|
||||
* without one never needs a capture to consult the view. */
|
||||
export function functionKeepsRecordSources(
|
||||
lowerer: Lowerer,
|
||||
fn: IrFunction,
|
||||
scan: KeepsSourceScan,
|
||||
): boolean {
|
||||
return !everyStmtList(fn.body, {
|
||||
expr: (expr) =>
|
||||
expr.kind !== "dynCheck" || !conversionKeepsSource(lowerer, expr.type, scan).keeps,
|
||||
stmt: () => true,
|
||||
});
|
||||
}
|
||||
|
||||
/** A reflective answer over a record value that branches on its kept
|
||||
* source: `staticAnswer` walks the declared fields, `viewAnswer` answers
|
||||
* from the live checked view (typed `type`, the static answer's type). The
|
||||
|
||||
@@ -42,6 +42,13 @@ export type WidthLift =
|
||||
| { how: "dynOut" }
|
||||
| { how: "upcast" }
|
||||
| { how: "funcAdapt" }
|
||||
/** A method field whose parameter records differ per instantiation
|
||||
* (`Provider<"x">` into `Provider<Api>`, admitted by method bivariance):
|
||||
* funcCoerceAdapter built in checked mode. Record-field planning only. */
|
||||
| { how: "funcAdaptChecked" }
|
||||
/** A union into a union where the `trappable` source arms have no home:
|
||||
* they throw the catchable TypeError. Checked mode only. */
|
||||
| { how: "checkedRetag"; trappable: number[] }
|
||||
/** A promise whose payload widens (`Promise<void>` into `Promise<unknown>`):
|
||||
* the checked payload widening coerceToExpected applies at a promise slot,
|
||||
* which keeps the promise's identity and settlement turn. */
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync, realpathSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { probeNodeFileImportRefusal } from "./npm.js";
|
||||
|
||||
// probeNodeFileImportRefusal against Node itself: for each specifier, Node's
|
||||
// import() from the same importer either rejects with ERR_MODULE_NOT_FOUND,
|
||||
// whose code, message and url the probe must reproduce, or does something
|
||||
// else (loads, or refuses with another error), where the probe answers null.
|
||||
// Node runs in an empty directory: it resolves a relative specifier's
|
||||
// "Did you mean" hint against its working directory, so there it adds none.
|
||||
test("relative and absolute import() refusals match Node's", () => {
|
||||
const dir = realpathSync(mkdtempSync(join(tmpdir(), "scriptc-file-import-")));
|
||||
writeFileSync(join(dir, "has space.mjs"), "export {};\n");
|
||||
writeFileSync(join(dir, "plain.mjs"), "export {};\n");
|
||||
writeFileSync(join(dir, "lit%20pct.mjs"), "export {};\n");
|
||||
writeFileSync(join(dir, "helper.js"), "export {};\n");
|
||||
mkdirSync(join(dir, "sub dir"));
|
||||
mkdirSync(join(dir, "pkgdir"));
|
||||
writeFileSync(join(dir, "pkgdir/package.json"), JSON.stringify({ main: "entry" }));
|
||||
writeFileSync(join(dir, "pkgdir/entry.js"), "export {};\n");
|
||||
mkdirSync(join(dir, "indexed"));
|
||||
writeFileSync(join(dir, "indexed/index.json"), "{}\n");
|
||||
mkdirSync(join(dir, "empty"));
|
||||
const importer = join(dir, "main.mjs");
|
||||
writeFileSync(
|
||||
importer,
|
||||
`const results = [];
|
||||
for (const specifier of JSON.parse(process.argv[2])) {
|
||||
try {
|
||||
await import(specifier);
|
||||
results.push(null);
|
||||
} catch (error) {
|
||||
results.push(error.code === "ERR_MODULE_NOT_FOUND" ? { code: error.code, message: error.message, url: error.url } : null);
|
||||
}
|
||||
}
|
||||
process.stdout.write(JSON.stringify(results));
|
||||
`,
|
||||
);
|
||||
const specifiers = [
|
||||
"./no-such-file.mjs",
|
||||
"./no%20such.mjs",
|
||||
"./has%20space.mjs",
|
||||
"./has space.mjs",
|
||||
"./lit%20pct.mjs",
|
||||
"./lit%2520pct.mjs",
|
||||
"./plain.mjs?x=1",
|
||||
"./plain.mjs#frag",
|
||||
"./missing.mjs?x=1",
|
||||
"./missing.mjs#frag",
|
||||
"./sub%2Fx.mjs",
|
||||
"./sub%5Cx.mjs",
|
||||
"./bad%zz.mjs",
|
||||
"./sub dir/",
|
||||
"./no-such-dir/",
|
||||
"./helper",
|
||||
"./pkgdir",
|
||||
"./pkgdir/",
|
||||
"./indexed",
|
||||
`../${basename(dir)}/missing.mjs`,
|
||||
`../${basename(dir)}/helper`,
|
||||
`${dir}/nope.mjs#h`,
|
||||
`${dir}/has%20space.mjs`,
|
||||
`${dir}/helper`,
|
||||
`${dir}/helper?q`,
|
||||
`${dir}/pkgdir/entry`,
|
||||
`${dir}/lit%20pct.mjs`,
|
||||
`${dir}/no-such`,
|
||||
`${dir}/indexed/`,
|
||||
];
|
||||
const node = JSON.parse(
|
||||
execFileSync(process.execPath, [importer, JSON.stringify(specifiers)], {
|
||||
cwd: join(dir, "empty"),
|
||||
encoding: "utf8",
|
||||
}),
|
||||
) as unknown[];
|
||||
const probed = specifiers.map((specifier) => probeNodeFileImportRefusal(importer, specifier));
|
||||
expect(Object.fromEntries(specifiers.map((s, i) => [s, probed[i]]))).toEqual(
|
||||
Object.fromEntries(specifiers.map((s, i) => [s, node[i]])),
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Node package resolution shared by preflight and native module introspection. */
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { packageNameOfSpecifier as packageNameOf } from "./workspace-registry.js";
|
||||
import {
|
||||
trackedDirectoryExists,
|
||||
@@ -142,6 +143,89 @@ export function probeNodeImportRefusal(
|
||||
}
|
||||
}
|
||||
|
||||
/** Node's import() refusal for a relative or absolute specifier whose
|
||||
* target does not exist: ERR_MODULE_NOT_FOUND naming the resolved path, with
|
||||
* the resolved URL. ESM resolution reads the specifier as a URL relative to
|
||||
* the importer's file URL and adds no extension: percent escapes are
|
||||
* decoded in the path, and a query or fragment stays in the URL only. An
|
||||
* existing file or directory answers null, as do other specifier forms and
|
||||
* the specifiers Node refuses with another error (a path ending in "/", an
|
||||
* encoded "/" or "\", a malformed escape). */
|
||||
export function probeNodeFileImportRefusal(
|
||||
fromFile: string,
|
||||
specifier: string,
|
||||
host: NpmGraphHost = realNpmGraphHost,
|
||||
): NodeImportRefusal | null {
|
||||
if (!isRelativeImportSpecifier(specifier) && !specifier.startsWith("/")) return null;
|
||||
const importer = resolve(fromFile);
|
||||
let url: URL;
|
||||
let target: string;
|
||||
try {
|
||||
url = new URL(specifier, pathToFileURL(importer));
|
||||
if (url.pathname.endsWith("/") || /%2f|%5c/i.test(url.pathname)) return null;
|
||||
target = fileURLToPath(url);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (host.isFile(target) || host.isDirectory(target)) return null;
|
||||
const hint = specifier.startsWith("/") ? commonJsResolveFile(specifier, host) : null;
|
||||
return {
|
||||
code: "ERR_MODULE_NOT_FOUND",
|
||||
message:
|
||||
`Cannot find module '${target}' imported from ${importer}` +
|
||||
(hint === null || hint === specifier
|
||||
? ""
|
||||
: `\nDid you mean to import ${JSON.stringify(hint)}?`),
|
||||
url: url.href,
|
||||
};
|
||||
}
|
||||
|
||||
function isRelativeImportSpecifier(specifier: string): boolean {
|
||||
return (
|
||||
specifier.startsWith("./") ||
|
||||
specifier.startsWith("../") ||
|
||||
specifier === "." ||
|
||||
specifier === ".."
|
||||
);
|
||||
}
|
||||
|
||||
/* Node's ERR_MODULE_NOT_FOUND hint names the file CommonJS resolution of
|
||||
* the literal specifier finds: the exact file, the .js/.json/.node
|
||||
* extensions, or a directory's package.json main or index. Node resolves a
|
||||
* relative specifier for the hint against the process's working directory,
|
||||
* which the compiled program only knows when it runs, so only an absolute
|
||||
* specifier gets the hint here; the found path is the hint itself. */
|
||||
const COMMONJS_EXTENSIONS = [".js", ".json", ".node"];
|
||||
|
||||
function commonJsResolveFile(request: string, host: NpmGraphHost): string | null {
|
||||
const file = (path: string): string | null => (host.isFile(path) ? host.realpath(path) : null);
|
||||
const withExtensions = (path: string): string | null => {
|
||||
for (const ext of COMMONJS_EXTENSIONS) {
|
||||
const found = file(path + ext);
|
||||
if (found !== null) return found;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const found = file(request) ?? withExtensions(request);
|
||||
if (found !== null) return found;
|
||||
if (!host.isDirectory(request)) return null;
|
||||
const manifest = host.readFile(join(request, "package.json"));
|
||||
if (manifest !== null) {
|
||||
let main: unknown;
|
||||
try {
|
||||
main = (JSON.parse(manifest) as PkgJson).main;
|
||||
} catch {
|
||||
main = undefined;
|
||||
}
|
||||
if (typeof main === "string" && main !== "") {
|
||||
const entry = resolve(request, main);
|
||||
const found = file(entry) ?? withExtensions(entry) ?? withExtensions(join(entry, "index"));
|
||||
if (found !== null) return found;
|
||||
}
|
||||
}
|
||||
return withExtensions(join(request, "index"));
|
||||
}
|
||||
|
||||
export function probeNodeRequireRefusal(
|
||||
fromFile: string,
|
||||
specifier: string,
|
||||
|
||||
@@ -557,6 +557,9 @@ export interface StartupCrash {
|
||||
loc: { file: string; start: number; end: number };
|
||||
/** Node's error code, for the refusals that carry one. */
|
||||
code?: string;
|
||||
/** The error's own `url` property: the missing file's URL, for Node's
|
||||
* ERR_MODULE_NOT_FOUND of an import() whose target does not exist. */
|
||||
url?: string;
|
||||
}
|
||||
|
||||
/** True when a coverage mapping names one exact bare host module. TypeScript
|
||||
|
||||
@@ -59,13 +59,15 @@ export function dynUndefinedExpr(loc: SrcLoc): IrExpr {
|
||||
|
||||
/** An always-throwing Node error with the replaced expression's type.
|
||||
* Kinds: 0 Error, 1 TypeError, 2 RangeError, 5 ReferenceError, 6 EvalError. An empty
|
||||
* code omits the error code property. */
|
||||
* code omits the error code property; a `url` becomes the error's own `url`
|
||||
* property (Node's ERR_MODULE_NOT_FOUND for a missing file). */
|
||||
export function nodeThrowExpr(
|
||||
kind: 0 | 1 | 2 | 5 | 6,
|
||||
code: string,
|
||||
message: string,
|
||||
type: IrType,
|
||||
loc: SrcLoc,
|
||||
url?: string,
|
||||
): IrExpr {
|
||||
return {
|
||||
kind: "libCall",
|
||||
@@ -74,6 +76,7 @@ export function nodeThrowExpr(
|
||||
{ kind: "numLit", value: kind, type: F64, loc },
|
||||
{ kind: "strLit", value: code, type: STRING, loc },
|
||||
{ kind: "strLit", value: message, type: STRING, loc },
|
||||
...(url === undefined ? [] : [{ kind: "strLit", value: url, type: STRING, loc } as const]),
|
||||
],
|
||||
type,
|
||||
loc,
|
||||
|
||||
@@ -3842,9 +3842,10 @@ function buildLibFnSignatures() {
|
||||
* arms (ERR_INVALID_THIS receivers, ERR_MISSING_ARGS arity ladders,
|
||||
* the symbol-to-string TypeError): args are [error-kind f64 (the
|
||||
* SCR_ERR_* index: 0 Error, 1 TypeError, 2 RangeError), code (empty =
|
||||
* no code slot), message]. ALWAYS THROWS catchably; the result type is
|
||||
* the replaced expression's own (never materialized — the
|
||||
* global.undefRead pattern). May-throw seed. */
|
||||
* no code slot), message], and optionally a fourth string, the error's
|
||||
* own `url` property (ir/validate.ts accepts it). ALWAYS THROWS
|
||||
* catchably; the result type is the replaced expression's own (never
|
||||
* materialized — the global.undefRead pattern). May-throw seed. */
|
||||
"error.nodeThrow": { argTypes: [F64, STRING, STRING], result: VOID },
|
||||
/** JS ToString over a dyn value WITH the object protocol (a user
|
||||
* toString/valueOf member is CALLED and its throw propagates;
|
||||
|
||||
@@ -2183,3 +2183,54 @@ test("a caught-typed global records a module error only from a catch binding", (
|
||||
'in main: assign to catch binding "%error" (frontend must reject)',
|
||||
]);
|
||||
});
|
||||
|
||||
test("a record arm without the literal key answers undefined in unionKeyGet", () => {
|
||||
const uX: IrType = { kind: "union", unionId: "uX" };
|
||||
const uS: IrType = { kind: "union", unionId: "uS" };
|
||||
const rA: IrType = { kind: "record", shapeId: "rA" };
|
||||
const module = (answer: IrType): IrModule => ({
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
records: [
|
||||
{ id: "rA", fields: [{ name: "x", type: uX }] },
|
||||
{ id: "rE", fields: [] },
|
||||
],
|
||||
unions: [
|
||||
{ id: "uX", arms: [STRING, UNDEFINED_T] },
|
||||
{ id: "uS", arms: [rA, { kind: "record", shapeId: "rE" }] },
|
||||
],
|
||||
functions: [
|
||||
{
|
||||
name: "main",
|
||||
params: [{ localId: "p", name: "p", type: uS }],
|
||||
locals: [{ id: "p", name: "p", type: uS, mutable: false }],
|
||||
returnType: answer,
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "unionKeyGet",
|
||||
unionId: "uS",
|
||||
key: { kind: "strLit", value: "x", type: STRING, loc },
|
||||
value: { kind: "varRef", localId: "p", type: uS, loc },
|
||||
type: answer,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
});
|
||||
// The empty arm lacks "x": it has no such own property, so it answers
|
||||
// the result's undefined arm.
|
||||
expect(validateModule(module(uX))).toEqual([]);
|
||||
// Without an undefined arm in the result there is no answer to give.
|
||||
expect(
|
||||
validateModule(module(STRING)).some((d) =>
|
||||
d.message.includes("unionKeyGet: arm 1 of uS has no answer for the key"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -3871,6 +3871,14 @@ function validateFunction(
|
||||
}
|
||||
return;
|
||||
}
|
||||
// A declared-only arm that lacks a literal key has no such own
|
||||
// property: it answers undefined, like a unit arm.
|
||||
if (
|
||||
literal !== null &&
|
||||
!shape.indexValue &&
|
||||
resultUnion?.arms.some((a) => a.kind === "undefinedT")
|
||||
)
|
||||
return;
|
||||
if (!shape.indexValue && !(literal === null && shape.fields.length > 0)) {
|
||||
err(`unionKeyGet: arm ${i} of ${e.unionId} has no answer for the key`, e.loc);
|
||||
}
|
||||
@@ -4189,16 +4197,21 @@ function validateFunction(
|
||||
e.fn === "readable.unpipe" ||
|
||||
e.fn === "rl.create" ||
|
||||
e.fn === "rl.createSignal";
|
||||
// error.nodeThrow takes an optional fourth argument: the error's own
|
||||
// `url` property (a string).
|
||||
const optionalUrl = e.fn === "error.nodeThrow" && e.args.length === 4;
|
||||
// fs.unwatchFile's listener is optional, so its last slot may be
|
||||
// omitted (1 or 2 args) rather than extended.
|
||||
const optionalTail = e.fn === "fs.unwatchFile" ? 1 : 0;
|
||||
const arityOk = variadic
|
||||
? e.args.length >= sig.argTypes.length
|
||||
: e.args.length === sig.argTypes.length ||
|
||||
optionalUrl ||
|
||||
(optionalTail > 0 && e.args.length === sig.argTypes.length - optionalTail);
|
||||
if (!arityOk) {
|
||||
err(`libCall ${e.fn}: ${e.args.length} args, expected ${sig.argTypes.length}`, e.loc);
|
||||
}
|
||||
if (optionalUrl) expectType(e.args[3]!, STRING, `libCall ${e.fn} arg 3`);
|
||||
e.args.forEach((a, i) => {
|
||||
checkExpr(a);
|
||||
const want = sig.argTypes[i];
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile } from "../src/index.js";
|
||||
|
||||
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
||||
|
||||
// A Provider<"beta"> held as Provider<Api> receives an alpha model through
|
||||
// method bivariance: Model<"beta">'s compat holds only undefined, so the
|
||||
// alpha model's compat object cannot convert. Native throws the checked
|
||||
// conversion's TypeError (types don't lie); Node prints `beta cancel a1 lie`
|
||||
// and `passed through` instead, because its method call never checks the
|
||||
// argument's shape. That divergence is the documented checked-conversion
|
||||
// policy, so this case is a native package test rather than a differential
|
||||
// corpus program.
|
||||
const program = `// A Provider<"beta"> held as Provider<Api> receives an alpha model through
|
||||
// method bivariance: Model<"beta">'s compat holds only undefined, so the
|
||||
// alpha model's compat object cannot convert. Native throws the checked
|
||||
// conversion's TypeError (types don't lie); Node passes the lie through.
|
||||
type Api = "alpha" | "beta";
|
||||
interface AlphaCompat { strict?: boolean }
|
||||
interface Model<TApi extends Api> {
|
||||
id: string;
|
||||
api: TApi;
|
||||
compat?: TApi extends "alpha" ? AlphaCompat : never;
|
||||
}
|
||||
interface Provider<TApi extends Api = Api> {
|
||||
readonly id: string;
|
||||
cancel?(model: Model<TApi>, handle: string): Promise<void>;
|
||||
}
|
||||
function betaProvider(): Provider<"beta"> {
|
||||
return {
|
||||
id: "b",
|
||||
cancel: async (model, handle) => {
|
||||
console.log("beta cancel", model.id, handle);
|
||||
},
|
||||
};
|
||||
}
|
||||
async function main(): Promise<void> {
|
||||
const providers: Provider[] = [betaProvider()];
|
||||
const beta = providers[0]!;
|
||||
await beta.cancel?.({ id: "b1", api: "beta" }, "ok");
|
||||
try {
|
||||
await beta.cancel?.({ id: "a1", api: "alpha", compat: { strict: true } }, "lie");
|
||||
console.log("passed through");
|
||||
} catch (error) {
|
||||
console.log(error instanceof TypeError, (error as Error).message);
|
||||
}
|
||||
}
|
||||
main();
|
||||
`;
|
||||
|
||||
test.each(["llvm"] as const)(
|
||||
"checked method-field adapters throw on an instantiation mismatch (%s)",
|
||||
async (backend) => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-method-field-adapters-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
writeFileSync(entry, program);
|
||||
const result = await compile(entry, {
|
||||
backend,
|
||||
sanitize,
|
||||
outDir: dir,
|
||||
outPath: join(dir, "program"),
|
||||
});
|
||||
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
|
||||
if (!result.ok) return;
|
||||
const child = spawnSync(result.binaryPath, { encoding: "utf8" });
|
||||
const stderr =
|
||||
sanitize && process.platform === "linux"
|
||||
? child.stderr.replace(
|
||||
/^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext functions and may produce false positives in some cases!\n/gm,
|
||||
"",
|
||||
)
|
||||
: child.stderr;
|
||||
expect(stderr).toBe("");
|
||||
expect(child.status).toBe(0);
|
||||
expect(child.stdout).toBe(
|
||||
"beta cancel b1 ok\ntrue a '{ strict: boolean | undefined }' value is not representable in the target union (a value narrowed or asserted past it still held it)\n",
|
||||
);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/async-return-contextual-records.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/dynamic-import-not-found.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/method-field-instantiation-adapters.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/nested-literal-own-layout.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/pick-alias-generic-methods.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/pick-alias-view-modules/registry.ts",
|
||||
"<repo>/tests/corpus/pick-alias-view-modules/globals.ts",
|
||||
"<repo>/tests/corpus/pick-alias-view-modules/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/record-literal-key-unions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/record-nullish-empty-destructure.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/record-source-keys-index-capture.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/record-source-keys-index-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/record-union-spreads.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/spread-empty-default.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -1759,6 +1759,10 @@ void scr_errdyn_put(ScrError *e, ScrDyn *d); /* retains both sides */
|
||||
/* Sets an own property on an error: its checked-dynamic node, which casts
|
||||
* and util.inspect read. Moves value. */
|
||||
void scr_error_set_prop(ScrError *e, const char *key, size_t key_len, ScrDyn *value);
|
||||
/* scr_throw_node_coded with an own `url` property (error.nodeThrow's
|
||||
* fourth argument). Borrows the strings; always throws. */
|
||||
SCR_COLD void scr_throw_node_coded_url(double kind, const ScrStr *code, const ScrStr *msg,
|
||||
const ScrStr *url);
|
||||
ScrDyn *scr_errdyn_view_of(const ScrError *e); /* borrowed or NULL */
|
||||
/* ToBoolean over the dyn kind (JS-exact); borrowed, never throws. */
|
||||
bool scr_dyn_truthy(const ScrDyn *d);
|
||||
|
||||
@@ -10798,6 +10798,20 @@ void scr_error_set_prop(ScrError *e, const char *key, size_t key_len, ScrDyn *va
|
||||
scr_dyn_release(view);
|
||||
}
|
||||
|
||||
/* scr_throw_node_coded with Node's `url` own property: the
|
||||
* ERR_MODULE_NOT_FOUND of an import() whose relative target does not
|
||||
* exist carries the target's file URL. Borrows the strings; always throws
|
||||
* (catchably — call sites are compiler-emitted pending checks). */
|
||||
void scr_throw_node_coded_url(double kind, const ScrStr *code, const ScrStr *msg, const ScrStr *url) {
|
||||
ScrError *e = scr_error_new((int)kind, (ScrStr *)msg); /* borrowed in */
|
||||
if (code->len > 0) {
|
||||
scr_str_release(e->code); /* NULL-safe */
|
||||
e->code = scr_str_retain((ScrStr *)code);
|
||||
}
|
||||
scr_error_set_prop(e, "url", 3, scr_dyn_new_str((ScrStr *)url));
|
||||
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||||
}
|
||||
|
||||
/* The error's checked-dynamic node when it has one (borrowed), else NULL:
|
||||
* util.inspect reads its extra own properties. */
|
||||
ScrDyn *scr_errdyn_view_of(const ScrError *e) {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// Object literals returned from async arrows typed by a method signature
|
||||
// (`resolve(input): Promise<AuthResult | undefined>`), with destructured
|
||||
// parameters. The return position's contextual type is
|
||||
// `AuthResult | undefined | PromiseLike<AuthResult | undefined>`; the
|
||||
// literal must build as AuthResult so its nested header record reaches the
|
||||
// pure index record ProviderHeaders (null-valued keys included), exactly as
|
||||
// in a sync function.
|
||||
type ProviderHeaders = Record<string, string | null>;
|
||||
interface ModelAuth {
|
||||
apiKey?: string;
|
||||
headers?: ProviderHeaders;
|
||||
}
|
||||
interface AuthResult {
|
||||
auth: ModelAuth;
|
||||
env?: Record<string, string>;
|
||||
source?: string;
|
||||
}
|
||||
interface Ctx {
|
||||
env(name: string): Promise<string | undefined>;
|
||||
}
|
||||
interface Credential {
|
||||
type: "api_key";
|
||||
key?: string;
|
||||
}
|
||||
interface Auth {
|
||||
name: string;
|
||||
resolve(input: { ctx: Ctx; credential?: Credential; signal: AbortSignal }): Promise<AuthResult | undefined>;
|
||||
peek?(input: { ctx: Ctx }): AuthResult | undefined;
|
||||
}
|
||||
|
||||
function tokenAuth(): Auth {
|
||||
return {
|
||||
name: "token",
|
||||
resolve: async ({ ctx, credential, signal }) => {
|
||||
signal.throwIfAborted();
|
||||
if (credential?.key) return { auth: { apiKey: credential.key }, source: "stored" };
|
||||
const token = await ctx.env("TOKEN");
|
||||
if (token) return { auth: { headers: { Authorization: `Bearer ${token}` } }, source: "TOKEN" };
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function gatewayAuth(): Auth {
|
||||
return {
|
||||
name: "gateway",
|
||||
resolve: async ({ ctx }) => {
|
||||
const key = await ctx.env("KEY");
|
||||
if (!key) return undefined;
|
||||
return {
|
||||
auth: {
|
||||
headers: {
|
||||
"cf-aig-authorization": `Bearer ${key}`,
|
||||
Authorization: null,
|
||||
"x-api-key": null,
|
||||
},
|
||||
},
|
||||
env: { ACCOUNT: "a1" },
|
||||
source: "KEY",
|
||||
};
|
||||
},
|
||||
peek: ({ ctx }) => (ctx ? { auth: { headers: { "x-peek": "1" } } } : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
function boundAuth(): Auth {
|
||||
return {
|
||||
name: "bound",
|
||||
resolve: async ({ ctx }) => {
|
||||
const token = await ctx.env("TOKEN");
|
||||
const headers = { Authorization: `Bearer ${token ?? "none"}` };
|
||||
return { auth: { headers }, source: "bound" };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function plain(flag: boolean): Promise<{ id: string; tags?: Record<string, string | null> } | undefined> {
|
||||
return flag ? { id: "p", tags: { a: "1", b: null } } : undefined;
|
||||
}
|
||||
|
||||
function show(result: AuthResult | undefined): string {
|
||||
if (result === undefined) return "undefined";
|
||||
const headers = result.auth.headers ?? {};
|
||||
const keys = Object.keys(headers).sort();
|
||||
return `${result.source ?? "-"} key=${result.auth.apiKey ?? "-"} ${keys.map((k) => `${k}=${String(headers[k])}`).join(" ")} env=${JSON.stringify(result.env ?? null)}`;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const env = new Map<string, string>([
|
||||
["KEY", "k1"],
|
||||
["TOKEN", "t1"],
|
||||
]);
|
||||
const ctx: Ctx = { env: async (name) => env.get(name) };
|
||||
const signal = new AbortController().signal;
|
||||
const token = tokenAuth();
|
||||
console.log(show(await token.resolve({ ctx, credential: { type: "api_key", key: "s1" }, signal })));
|
||||
console.log(show(await token.resolve({ ctx, signal })));
|
||||
console.log(show(await gatewayAuth().resolve({ ctx, signal })));
|
||||
console.log(show(gatewayAuth().peek?.({ ctx })));
|
||||
console.log(show(await boundAuth().resolve({ ctx, signal })));
|
||||
env.clear();
|
||||
console.log(show(await token.resolve({ ctx, signal })));
|
||||
console.log(show(await gatewayAuth().resolve({ ctx, signal })));
|
||||
console.log(show(await boundAuth().resolve({ ctx, signal })));
|
||||
const p = await plain(true);
|
||||
console.log(p?.id, JSON.stringify(p?.tags), (await plain(false)) === undefined);
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,29 @@
|
||||
// import() of a specifier nothing resolves rejects with Node's
|
||||
// ERR_MODULE_NOT_FOUND when the call runs: a package nothing installed
|
||||
// provides, and a relative path to a file that does not exist, whose error
|
||||
// also carries the missing file's URL. The specifier is a URL: its escapes
|
||||
// are decoded in the message's path, and its query and fragment stay in
|
||||
// the url only. The program itself loads, and each call rejects again when
|
||||
// repeated.
|
||||
const dirs = (text) => text.replace(/\/[^\s']*\//g, "<dir>/");
|
||||
|
||||
async function probe(label, load) {
|
||||
try {
|
||||
await load();
|
||||
console.log(label, "loaded");
|
||||
} catch (e) {
|
||||
console.log(label, e instanceof Error, e.name, e.code);
|
||||
console.log(" ", dirs(e.message));
|
||||
console.log(" url:", e.url === undefined ? "none" : dirs(e.url));
|
||||
}
|
||||
}
|
||||
|
||||
console.log("start");
|
||||
await probe("bare", () => import("no-such-package-anywhere"));
|
||||
await probe("bare subpath", () => import("no-such-package-anywhere/sub.js"));
|
||||
await probe("scoped", () => import("@nobody/no-such-package"));
|
||||
await probe("relative", () => import("./no-such-file.mjs"));
|
||||
await probe("relative again", () => import("./no-such-file.mjs"));
|
||||
await probe("parent", () => import("../no-such-dir/no-such-file.js"));
|
||||
await probe("escaped", () => import("./no%20such%20file.mjs?v=1#top"));
|
||||
console.log("end");
|
||||
@@ -0,0 +1,51 @@
|
||||
// Provider<"alpha"> and Provider<"beta"> into Provider<Api>: method fields
|
||||
// whose parameter is the instantiation-dependent Model<TApi> (a
|
||||
// conditional compat member that is `never` for beta) convert through a
|
||||
// checked adapter. TypeScript admits the pair by method bivariance; every
|
||||
// call here passes each provider its own models, so nothing traps.
|
||||
type Api = "alpha" | "beta";
|
||||
interface AlphaCompat {
|
||||
strict?: boolean;
|
||||
}
|
||||
interface Model<TApi extends Api> {
|
||||
id: string;
|
||||
api: TApi;
|
||||
compat?: TApi extends "alpha" ? AlphaCompat : never;
|
||||
}
|
||||
interface Provider<TApi extends Api = Api> {
|
||||
readonly id: string;
|
||||
models(): Model<TApi>[];
|
||||
cancel?(model: Model<TApi>, handle: string): Promise<void>;
|
||||
}
|
||||
function alphaProvider(): Provider<"alpha"> {
|
||||
return {
|
||||
id: "a",
|
||||
models: () => [{ id: "a1", api: "alpha", compat: { strict: true } }],
|
||||
cancel: async (model, handle) => {
|
||||
console.log("cancel", model.id, handle, String(model.compat?.strict));
|
||||
},
|
||||
};
|
||||
}
|
||||
function betaProvider(): Provider<"beta"> {
|
||||
return {
|
||||
id: "b",
|
||||
models: () => [{ id: "b1", api: "beta" }],
|
||||
cancel: async (model, handle) => {
|
||||
console.log("beta cancel", model.id, handle, model.compat === undefined);
|
||||
},
|
||||
};
|
||||
}
|
||||
function plainProvider(): Provider<"beta"> {
|
||||
return { id: "p", models: () => [] };
|
||||
}
|
||||
function all(): Provider[] {
|
||||
return [alphaProvider(), betaProvider(), plainProvider()];
|
||||
}
|
||||
async function main(): Promise<void> {
|
||||
for (const p of all()) {
|
||||
const [m] = p.models();
|
||||
console.log(p.id, m?.id, m?.api, p.cancel === undefined);
|
||||
if (m) await p.cancel?.(m, "h");
|
||||
}
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,20 @@
|
||||
// The outer literal's context (a union with a WeakRef arm) has no layout,
|
||||
// so it builds at its own type; its nested literals must follow that
|
||||
// layout instead of their own contextual one, and the outer coercion then
|
||||
// lifts the whole tree (headers into the index-signature record).
|
||||
type ProviderHeaders = Record<string, string | null>;
|
||||
interface ModelAuth {
|
||||
apiKey?: string;
|
||||
headers?: ProviderHeaders;
|
||||
}
|
||||
interface AuthResult {
|
||||
auth: ModelAuth;
|
||||
source?: string;
|
||||
}
|
||||
type Answer = AuthResult | WeakRef<object> | undefined;
|
||||
function make(flag: boolean): Answer {
|
||||
return flag ? { auth: { headers: { Authorization: "Bearer x" } }, source: "env" } : undefined;
|
||||
}
|
||||
const r = make(true) as AuthResult;
|
||||
console.log(r.source, r.auth.headers?.Authorization);
|
||||
console.log(make(false) === undefined);
|
||||
@@ -0,0 +1,79 @@
|
||||
// Generic class methods called through a Pick<>/Omit<> view of the class
|
||||
// that is NAMED by a type alias (`type View = Pick<Registry, ...>`), also
|
||||
// through an alias of that alias. Inline Pick<> views already compile; the
|
||||
// alias hides the Pick from the view's type, which then mapped to a
|
||||
// record of erased method fields that a Registry instance cannot convert to.
|
||||
interface ChatModel {
|
||||
kind: "chat";
|
||||
id: string;
|
||||
}
|
||||
interface ImageModel {
|
||||
kind: "image";
|
||||
id: string;
|
||||
size: number;
|
||||
}
|
||||
interface ModelMap {
|
||||
chat: ChatModel;
|
||||
image: ImageModel;
|
||||
}
|
||||
type ModelKind = keyof ModelMap;
|
||||
|
||||
class Registry {
|
||||
private readonly all: (ChatModel | ImageModel)[] = [
|
||||
{ kind: "chat", id: "c1" },
|
||||
{ kind: "image", id: "i1", size: 512 },
|
||||
{ kind: "chat", id: "c2" },
|
||||
];
|
||||
modelsOfKind<K extends ModelKind>(kind: K, provider?: string): readonly ModelMap[K][] {
|
||||
return this.all.filter((m) => m.kind === kind && provider !== "none") as ModelMap[K][];
|
||||
}
|
||||
modelOfKind<K extends ModelKind>(kind: K, id: string): ModelMap[K] | undefined {
|
||||
for (const m of this.modelsOfKind(kind)) if (m.id === id) return m;
|
||||
return undefined;
|
||||
}
|
||||
async availableOfKind<K extends ModelKind>(kind: K, options?: { tag?: string }): Promise<readonly ModelMap[K][]> {
|
||||
return this.modelsOfKind(kind).filter(() => options?.tag !== "none");
|
||||
}
|
||||
label(): string {
|
||||
return "registry";
|
||||
}
|
||||
}
|
||||
|
||||
type RegistryView = Pick<Registry, "modelsOfKind" | "modelOfKind" | "availableOfKind" | "label">;
|
||||
type ReadOnlyView = RegistryView;
|
||||
type NoAsyncView = Omit<Registry, "availableOfKind">;
|
||||
|
||||
function toKind(value: unknown): ModelKind {
|
||||
return value === "image" ? "image" : "chat";
|
||||
}
|
||||
|
||||
function describe(models: RegistryView, args: unknown[]): string {
|
||||
const [kind, id] = args;
|
||||
const list = models.modelsOfKind(toKind(kind)).map((m) => m.id);
|
||||
const one = models.modelOfKind(toKind(kind), String(id));
|
||||
return `${models.label()} ${list.join(",")} ${one === undefined ? "none" : one.id}`;
|
||||
}
|
||||
|
||||
function globals(models: ReadOnlyView): Record<string, (args: unknown[]) => unknown> {
|
||||
return {
|
||||
list: (args) => models.modelsOfKind(toKind(args[0]), args[1] === undefined ? undefined : String(args[1])).length,
|
||||
size: (args) => models.modelOfKind("image", String(args[0]))?.size ?? -1,
|
||||
};
|
||||
}
|
||||
|
||||
function sizes(models: NoAsyncView): string {
|
||||
return models.modelsOfKind("image").map((m) => `${m.id}:${m.size}`).join(",");
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const registry = new Registry();
|
||||
console.log(describe(registry, ["chat", "c2"]));
|
||||
console.log(describe(registry, ["image", "zz"]));
|
||||
const g = globals(registry);
|
||||
console.log(g.list!(["chat"]), g.list!(["chat", "none"]), g.size!(["i1"]), g.size!(["c1"]));
|
||||
console.log(sizes(registry));
|
||||
const view: RegistryView = registry;
|
||||
const available = await view.availableOfKind("image", { tag: "x" });
|
||||
console.log(available.map((m) => `${m.id}:${m.size}`).join(","), (await view.availableOfKind("chat", { tag: "none" })).length);
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,2 @@
|
||||
import type { Registry } from "./registry.ts";
|
||||
export interface Ctx { registry: Registry; label: string }
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Ctx } from "./ctx.ts";
|
||||
import type { ModelKind, RegistryView } from "./registry.ts";
|
||||
function toKind(value: unknown): ModelKind {
|
||||
return value === "image" ? "image" : "chat";
|
||||
}
|
||||
function globals(models: RegistryView): (args: unknown[]) => string {
|
||||
return (args) => {
|
||||
const [kind] = args;
|
||||
return models.modelsOfKind(toKind(kind)).map((m) => m.id).join(",");
|
||||
};
|
||||
}
|
||||
export function run(ctx: Ctx | undefined): string {
|
||||
const list = ctx ? globals(ctx.registry) : undefined;
|
||||
return list ? `${ctx?.label} ${list(["chat"])} ${list(["image"])}` : "none";
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Registry } from "./registry.ts";
|
||||
import { run } from "./globals.ts";
|
||||
console.log(run({ registry: new Registry(), label: "L" }), run(undefined));
|
||||
@@ -0,0 +1,21 @@
|
||||
export interface ChatModel { kind: "chat"; id: string }
|
||||
export interface ImageModel { kind: "image"; id: string; size: number }
|
||||
export interface ModelMap { chat: ChatModel; image: ImageModel }
|
||||
export type ModelKind = keyof ModelMap;
|
||||
export class Registry {
|
||||
private readonly all: (ChatModel | ImageModel)[] = [
|
||||
{ kind: "chat", id: "c1" },
|
||||
{ kind: "image", id: "i1", size: 512 },
|
||||
];
|
||||
modelsOfKind<K extends ModelKind>(kind: K, provider?: string): readonly ModelMap[K][] {
|
||||
return this.all.filter((m) => m.kind === kind && provider !== "none") as ModelMap[K][];
|
||||
}
|
||||
modelOfKind<K extends ModelKind>(kind: K, id: string): ModelMap[K] | undefined {
|
||||
for (const m of this.modelsOfKind(kind)) if (m.id === id) return m;
|
||||
return undefined;
|
||||
}
|
||||
label(): string {
|
||||
return "registry";
|
||||
}
|
||||
}
|
||||
export type RegistryView = Pick<Registry, "modelsOfKind" | "modelOfKind" | "label">;
|
||||
@@ -0,0 +1,98 @@
|
||||
// Keyed reads and writes whose key is a union of literal names of declared
|
||||
// fields stored with one type, on records whose other fields have other
|
||||
// types: plain reads, plain writes, compound updates, keys from parameters
|
||||
// and loops, and JavaScript evaluation order with effectful receivers and
|
||||
// keys.
|
||||
type Stats = { hits: number; misses: number; bytes: number; name: string; ok: boolean };
|
||||
type Counter = "hits" | "misses" | "bytes";
|
||||
type Flags = { name: string; verbose: boolean; strict: boolean; level: number };
|
||||
type Labels = { id: number; title: string; summary: string; tags: string[] };
|
||||
|
||||
function read(s: Stats, key: Counter): number {
|
||||
return s[key];
|
||||
}
|
||||
|
||||
function bump(s: Stats, key: Counter, by: number): void {
|
||||
s[key] = s[key] + by;
|
||||
}
|
||||
|
||||
function toggle(flags: Flags, key: "verbose" | "strict"): boolean {
|
||||
flags[key] = !flags[key];
|
||||
return flags[key];
|
||||
}
|
||||
|
||||
function relabel(labels: Labels, key: "title" | "summary", text: string): string {
|
||||
const before = labels[key];
|
||||
labels[key] = text.toUpperCase();
|
||||
return `${before} -> ${labels[key]}`;
|
||||
}
|
||||
|
||||
const stats: Stats[] = [];
|
||||
for (let i = 0; i < 4; i++) stats.push({ hits: 0, misses: 0, bytes: 0, name: "s" + i, ok: true });
|
||||
|
||||
let seed = 7;
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
const s = stats[seed & 3]!;
|
||||
bump(s, seed % 5 === 0 ? "misses" : "hits", 1);
|
||||
bump(s, "bytes", (seed >>> 8) & 255);
|
||||
}
|
||||
for (const s of stats) console.log(s.name, read(s, "hits"), read(s, "misses"), read(s, "bytes"), s.ok);
|
||||
|
||||
// Keys from a loop over a constant list of names.
|
||||
const counters = ["hits", "misses", "bytes"] as const;
|
||||
const totals: Stats = { hits: 0, misses: 0, bytes: 0, name: "total", ok: false };
|
||||
for (const s of stats) for (const key of counters) totals[key] += s[key];
|
||||
console.log(totals.name, totals.hits, totals.misses, totals.bytes);
|
||||
|
||||
// Booleans and strings.
|
||||
const flags: Flags = { name: "build", verbose: false, strict: true, level: 2 };
|
||||
console.log(toggle(flags, "verbose"), toggle(flags, "strict"), toggle(flags, "verbose"), flags.level);
|
||||
const labels: Labels = { id: 1, title: "draft", summary: "none", tags: ["a"] };
|
||||
console.log(relabel(labels, "title", "final"), relabel(labels, "summary", "short"), labels.tags.length);
|
||||
|
||||
// The receiver, then the key, then the value, each once.
|
||||
const order: string[] = [];
|
||||
function pick(i: number): Stats {
|
||||
order.push("receiver " + i);
|
||||
return stats[i]!;
|
||||
}
|
||||
function name(key: Counter): Counter {
|
||||
order.push("key " + key);
|
||||
return key;
|
||||
}
|
||||
function amount(n: number): number {
|
||||
order.push("value " + n);
|
||||
return n;
|
||||
}
|
||||
pick(1)[name("misses")] = amount(40);
|
||||
console.log(order.join(", "), stats[1]!.misses);
|
||||
order.length = 0;
|
||||
console.log(pick(2)[name("bytes")], order.join(", "));
|
||||
|
||||
// A key that names one field.
|
||||
function only(s: Stats, key: "bytes"): number {
|
||||
s[key] = s[key] * 2;
|
||||
return s[key];
|
||||
}
|
||||
console.log(only(stats[3]!, "bytes"));
|
||||
|
||||
// A key or value that reassigns the receiver or key local: the write goes
|
||||
// to the receiver and key evaluated before them.
|
||||
let target = stats[0]!;
|
||||
let slot: Counter = "hits";
|
||||
target[slot] = ((slot = "misses"), 500);
|
||||
console.log(stats[0]!.hits, stats[0]!.misses, slot);
|
||||
target[slot] = ((target = stats[2]!), 600);
|
||||
console.log(stats[0]!.misses, stats[2]!.misses, target === stats[2]);
|
||||
function retarget(): Counter {
|
||||
target = stats[3]!;
|
||||
return "bytes";
|
||||
}
|
||||
target = stats[1]!;
|
||||
target[retarget()] = 700;
|
||||
console.log(stats[1]!.bytes, stats[3]!.bytes === 700);
|
||||
target = stats[0]!;
|
||||
slot = "hits";
|
||||
target[slot] += ((slot = "bytes"), (target = stats[1]!), 1);
|
||||
console.log(stats[0]!.hits, stats[0]!.bytes, stats[1]!.hits);
|
||||
@@ -0,0 +1,83 @@
|
||||
// `const { a, ...rest } = options ?? {}` (and `|| {}`): the pattern
|
||||
// destructures either the options record or an empty object. Each key reads
|
||||
// undefined from the empty object, a default applies to it, and the rest
|
||||
// element packs the record's remaining own keys or nothing. The options
|
||||
// expression is evaluated once.
|
||||
|
||||
interface EditorOptions {
|
||||
description: string;
|
||||
paddingX: number;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
function editor(options?: EditorOptions): string {
|
||||
const { description, ...editorOptions } = options ?? {};
|
||||
return `${description} ${JSON.stringify(editorOptions)} [${Object.keys(editorOptions).join(",")}]`;
|
||||
}
|
||||
console.log(editor());
|
||||
console.log(editor({ description: "d", paddingX: 2 }));
|
||||
console.log(editor({ description: "d", paddingX: 2, label: "l" }));
|
||||
|
||||
// Every member optional: the empty object is the same layout with every
|
||||
// key absent.
|
||||
interface StreamOptions {
|
||||
apiKey?: string;
|
||||
env?: string;
|
||||
signal?: string;
|
||||
transformHeaders?: (headers: string) => string;
|
||||
}
|
||||
|
||||
function stream(options?: StreamOptions): string {
|
||||
const { transformHeaders, apiKey, ...rest } = options ?? {};
|
||||
const headers = transformHeaders ? transformHeaders("h") : "none";
|
||||
return `${headers} ${apiKey} ${JSON.stringify(rest)} ${"signal" in rest}`;
|
||||
}
|
||||
console.log(stream());
|
||||
console.log(stream({ apiKey: "k", signal: "s" }));
|
||||
console.log(stream({ transformHeaders: (h) => h + "!", env: "e" }));
|
||||
|
||||
// A nested optional record, renamed keys and a default.
|
||||
interface Entry {
|
||||
options?: { deferred: boolean; signal?: string; step: number };
|
||||
}
|
||||
|
||||
function submit(entry: Entry): string {
|
||||
const { deferred: _deferred, signal: _signal, step = -1, ...submission } = entry.options ?? {};
|
||||
return `${step} ${JSON.stringify(submission)}`;
|
||||
}
|
||||
console.log(submit({}));
|
||||
console.log(submit({ options: { deferred: true, step: 4 } }));
|
||||
console.log(submit({ options: { deferred: false, signal: "x", step: 0 } }));
|
||||
|
||||
// `||` and a null arm.
|
||||
interface Retry {
|
||||
headers: string;
|
||||
retries: number;
|
||||
}
|
||||
|
||||
function retry(value: Retry | null): string {
|
||||
const { headers, retries = 3 } = value || {};
|
||||
return `${headers} ${retries} ${typeof retries}`;
|
||||
}
|
||||
console.log(retry(null));
|
||||
console.log(retry({ headers: "h", retries: 2 }));
|
||||
|
||||
// A generic options parameter.
|
||||
function generic<TOptions extends { apiKey?: string; maxTokens: number }>(options?: TOptions): string {
|
||||
const { apiKey, ...providerOptions } = options ?? {};
|
||||
return `${apiKey} ${JSON.stringify(providerOptions)}`;
|
||||
}
|
||||
console.log(generic());
|
||||
console.log(generic({ maxTokens: 3, apiKey: "k" }));
|
||||
|
||||
// The options expression runs once.
|
||||
let calls = 0;
|
||||
function current(): EditorOptions | undefined {
|
||||
calls++;
|
||||
return calls === 1 ? undefined : { description: "second", paddingX: 1 };
|
||||
}
|
||||
const { description: first } = current() ?? {};
|
||||
const { description: second, paddingX } = current() ?? {};
|
||||
console.log(first, second, paddingX, calls);
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,130 @@
|
||||
// A record built from the checked-dynamic tree keeps the own keys its shape
|
||||
// does not declare, and every JS copy carries them. Converting such a record
|
||||
// into an index-signature record must copy them too.
|
||||
//
|
||||
// The shape that lost them: an inline, UNANNOTATED arrow in a generic call
|
||||
// whose member is declared as `(h: H) => H | Promise<H>`. No contextual type
|
||||
// reaches the arrow through the type parameter, so its body is typed by tsc's
|
||||
// own inference, which drops the spread's index signature: the closure is
|
||||
// typed Promise<{ x }>, `{ ...h, x }` lowers through the checked-dynamic
|
||||
// spread into that closed record, and the caller's keys survive only in the
|
||||
// record's kept source view. The call site reads the member through the
|
||||
// declared H and adapts the function value; converting `{ x }` into H must
|
||||
// copy the kept keys. Node prints "accept,x"; before the fix this program
|
||||
// printed "x" for the arrow, local and method lines, with no diagnostic.
|
||||
type H = Record<string, string | null>;
|
||||
|
||||
interface T {
|
||||
transformHeaders?: (h: H) => H | Promise<H>;
|
||||
}
|
||||
|
||||
function keys(r: H): string {
|
||||
return Object.keys(r).sort().join(",") || "(none)";
|
||||
}
|
||||
|
||||
// The failing shape: block-bodied inline arrow, generic parameter.
|
||||
async function viaArrow<O extends T>(o: O | undefined): Promise<H> {
|
||||
let h: H = { accept: "a" };
|
||||
if (o?.transformHeaders) h = await o.transformHeaders(h);
|
||||
return h;
|
||||
}
|
||||
|
||||
// The same call read into a local first (the pi repro's shape).
|
||||
async function viaLocal<O extends T>(o: O | undefined): Promise<H> {
|
||||
let h: H = { accept: "a" };
|
||||
if (o?.transformHeaders) {
|
||||
const r = await o.transformHeaders(h);
|
||||
console.log("local-mid:", keys(r));
|
||||
h = r;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
interface Methods {
|
||||
transform(h: H): Promise<H>;
|
||||
}
|
||||
|
||||
// The method form drops the same keys through the same capture.
|
||||
async function viaMethod<O extends Methods>(o: O): Promise<H> {
|
||||
return await o.transform({ accept: "a" });
|
||||
}
|
||||
|
||||
// A non-generic parameter: the arrow's contextual slot is the declared member,
|
||||
// so these keep the index signature and never needed the capture.
|
||||
async function viaArrowNonGeneric(o: T | undefined): Promise<H> {
|
||||
let h: H = { accept: "a" };
|
||||
if (o?.transformHeaders) h = await o.transformHeaders(h);
|
||||
return h;
|
||||
}
|
||||
|
||||
interface Hooks {
|
||||
transform?: (h: H) => H | Promise<H>;
|
||||
}
|
||||
|
||||
interface Nested {
|
||||
hooks: Hooks;
|
||||
}
|
||||
|
||||
async function viaNested<O extends Nested>(o: O | undefined): Promise<H> {
|
||||
let h: H = { accept: "a" };
|
||||
const t = o?.hooks.transform;
|
||||
if (t) h = await t(h);
|
||||
return h;
|
||||
}
|
||||
|
||||
function syncArrow<O extends { transform?: (h: H) => H }>(o: O): H {
|
||||
return o.transform ? o.transform({ accept: "a" }) : { accept: "a" };
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const arrow = await viaArrow({
|
||||
transformHeaders: async (h) => {
|
||||
return { ...h, x: "1" };
|
||||
},
|
||||
});
|
||||
console.log("arrow:", keys(arrow), JSON.stringify(arrow));
|
||||
|
||||
const local = await viaLocal({
|
||||
transformHeaders: async (h) => {
|
||||
return { ...h, x: "1" };
|
||||
},
|
||||
});
|
||||
console.log("local:", keys(local));
|
||||
|
||||
const method = await viaMethod({
|
||||
async transform(h) {
|
||||
return { ...h, x: "1" };
|
||||
},
|
||||
});
|
||||
console.log("method:", keys(method));
|
||||
|
||||
const generic = await viaArrowNonGeneric({
|
||||
transformHeaders: async (h) => {
|
||||
return { ...h, x: "1" };
|
||||
},
|
||||
});
|
||||
console.log("non-generic:", keys(generic));
|
||||
|
||||
const expression = await viaArrowNonGeneric({
|
||||
transformHeaders: async (h) => ({ ...h, x: "1" }),
|
||||
});
|
||||
console.log("expression:", keys(expression));
|
||||
|
||||
const nested = await viaNested({
|
||||
hooks: {
|
||||
transform: async (h) => {
|
||||
return { ...h, x: "1" };
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log("nested:", keys(nested));
|
||||
|
||||
const sync = syncArrow({
|
||||
transform: (h) => {
|
||||
return { ...h, x: "1" };
|
||||
},
|
||||
});
|
||||
console.log("sync:", keys(sync));
|
||||
}
|
||||
|
||||
void main();
|
||||
@@ -0,0 +1,19 @@
|
||||
// A checked conversion into an index-signature record whose values are
|
||||
// closed records keeps each value's undeclared source keys, so converting
|
||||
// such a value into an index-signature record later copies them. This
|
||||
// program has no checked conversion into a closed record itself, so the
|
||||
// capture learns that kept sources are possible from the index signature's
|
||||
// values. Node prints "extra,x".
|
||||
type H = Record<string, string | null>;
|
||||
interface T {
|
||||
transformHeaders?: (h: H) => H | Promise<H>;
|
||||
}
|
||||
const parsed: Record<string, { x: string }> = JSON.parse('{"p":{"x":"1","extra":"e"}}');
|
||||
async function viaArrow<O extends T>(o: O | undefined): Promise<H> {
|
||||
let h: H = { accept: "a" };
|
||||
if (o?.transformHeaders) h = await o.transformHeaders(h);
|
||||
return h;
|
||||
}
|
||||
viaArrow({ transformHeaders: async (h) => parsed["p"]! }).then((r) =>
|
||||
console.log(Object.keys(r).sort().join(",")),
|
||||
);
|
||||
@@ -0,0 +1,92 @@
|
||||
// Object spreads of unions with several record arms.
|
||||
// 1. The checker narrowed the source to fewer arms than its stored union
|
||||
// holds (an optional-chain discriminant test on an element read, a type
|
||||
// predicate): the spread copies the narrowed arm.
|
||||
// 2. A union source spreads into one declared record: each arm copies only
|
||||
// the keys it holds, so a key the running arm lacks keeps the earlier
|
||||
// value or stays absent.
|
||||
// Key order is printed sorted: per-arm creation order is a separate
|
||||
// (pre-existing) key-order limitation.
|
||||
|
||||
type Block =
|
||||
| { type: "text"; text: string; textSignature?: string }
|
||||
| { type: "tool"; id: string; args: unknown; ns?: string }
|
||||
| { type: "image"; data: string };
|
||||
|
||||
function keys(value: object): string {
|
||||
return Object.keys(value).sort().join(",");
|
||||
}
|
||||
|
||||
function update(content: Block[], index: number, args: unknown): string {
|
||||
const block = content[index];
|
||||
if (block?.type === "tool") {
|
||||
content[index] = { ...block, args };
|
||||
return "updated";
|
||||
}
|
||||
return "skipped";
|
||||
}
|
||||
|
||||
function isTool(block: Block): block is Block & { type: "tool" } {
|
||||
return block.type === "tool";
|
||||
}
|
||||
|
||||
function relabel(content: readonly Block[]): Block[] {
|
||||
return content.map((block) => (isTool(block) ? { ...block, args: "relabelled" } : block));
|
||||
}
|
||||
|
||||
const blocks: Block[] = [
|
||||
{ type: "tool", id: "1", args: 0 },
|
||||
{ type: "text", text: "t" },
|
||||
{ type: "tool", id: "2", args: 0, ns: "n" },
|
||||
{ type: "image", data: "d" },
|
||||
];
|
||||
console.log(update(blocks, 0, 5), update(blocks, 1, 5), update(blocks, 2, [1]), update(blocks, 9, 5));
|
||||
for (const block of blocks) {
|
||||
if (block.type === "tool") console.log(block.id, JSON.stringify(block.args), block.ns, keys(block));
|
||||
else console.log(block.type, keys(block));
|
||||
}
|
||||
for (const block of relabel(blocks)) {
|
||||
if (block.type === "tool") console.log(block.id, JSON.stringify(block.args), keys(block));
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
apiKey?: string;
|
||||
headers?: Record<string, string>;
|
||||
env?: string;
|
||||
maxTokens?: number;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
function describe(options: RequestOptions): string {
|
||||
return [
|
||||
options.apiKey,
|
||||
options.headers?.["x"],
|
||||
options.env,
|
||||
options.maxTokens,
|
||||
options.label,
|
||||
keys(options),
|
||||
"apiKey" in options,
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
// The `cond ? { a, b } : {}` credential idiom.
|
||||
function route(sameProvider: boolean, apiKey: string | undefined, env: string | undefined): string {
|
||||
const auth = sameProvider ? { apiKey, env } : {};
|
||||
return describe({ label: "base", ...auth, maxTokens: 5 });
|
||||
}
|
||||
console.log(route(true, "k", "e"));
|
||||
console.log(route(true, undefined, "e"));
|
||||
console.log(route(false, "k", "e"));
|
||||
|
||||
type Source = { env?: string; apiKey: string } | { label: string; headers?: Record<string, string> } | undefined;
|
||||
|
||||
function merge(base: RequestOptions, source: Source): string {
|
||||
return describe({ ...base, ...source, maxTokens: 1 });
|
||||
}
|
||||
console.log(merge({ headers: { x: "h0" } }, { apiKey: "k", env: "e" }));
|
||||
console.log(merge({ headers: { x: "h0" }, apiKey: "k0" }, { apiKey: "k" }));
|
||||
console.log(merge({ headers: { x: "h0" } }, { label: "l" }));
|
||||
console.log(merge({ headers: { x: "h0" } }, { label: "l", headers: { x: "h1" } }));
|
||||
console.log(merge({ env: "e0" }, undefined));
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,45 @@
|
||||
// `...(options ?? {})` and `...(options || {})` spread exactly what
|
||||
// `...options` spreads: nullish and other falsy values contribute no keys.
|
||||
// The optional source is an Omit<> view of an options interface whose
|
||||
// fields include a function, so a checked-dynamic copy cannot hold it.
|
||||
interface LoaderOptions {
|
||||
cwd: string;
|
||||
agentDir: string;
|
||||
extraPaths?: string[];
|
||||
onLoad?: (paths: string[]) => string[];
|
||||
}
|
||||
interface ServiceOptions {
|
||||
cwd: string;
|
||||
loaderOptions?: Omit<LoaderOptions, "cwd" | "agentDir">;
|
||||
}
|
||||
class Loader {
|
||||
readonly options: LoaderOptions;
|
||||
constructor(options: LoaderOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
describe(): string {
|
||||
const paths = this.options.onLoad?.(this.options.extraPaths ?? []) ?? [];
|
||||
return `${this.options.cwd} ${this.options.agentDir} [${paths.join(",")}]`;
|
||||
}
|
||||
}
|
||||
function viaNullish(options: ServiceOptions): Loader {
|
||||
return new Loader({ ...(options.loaderOptions ?? {}), cwd: options.cwd, agentDir: "/agent" });
|
||||
}
|
||||
function viaOr(options: ServiceOptions): Loader {
|
||||
return new Loader({ cwd: options.cwd, agentDir: "/or", ...(options.loaderOptions || {}) });
|
||||
}
|
||||
let evaluated = 0;
|
||||
function counted(options: ServiceOptions): ServiceOptions["loaderOptions"] {
|
||||
evaluated++;
|
||||
return options.loaderOptions;
|
||||
}
|
||||
function viaCall(options: ServiceOptions): Loader {
|
||||
return new Loader({ ...(counted(options) ?? {}), cwd: options.cwd, agentDir: "/call" });
|
||||
}
|
||||
const bare: ServiceOptions = { cwd: "/w" };
|
||||
const full: ServiceOptions = { cwd: "/w", loaderOptions: { extraPaths: ["x"], onLoad: (p) => [...p, "y"] } };
|
||||
console.log(viaNullish(bare).describe());
|
||||
console.log(viaNullish(full).describe());
|
||||
console.log(viaOr(bare).describe());
|
||||
console.log(viaOr(full).describe());
|
||||
console.log(viaCall(bare).describe(), viaCall(full).describe(), evaluated);
|
||||
@@ -12,9 +12,9 @@ const loose: Loose = { a: 1 };
|
||||
const picked: PickedA = loose;
|
||||
console.log(picked);
|
||||
|
||||
// A method field whose parameter shapes relate only BIVARIANTLY: neither
|
||||
// parameter type converts into the other mechanically, so the field does
|
||||
// not lift.
|
||||
// A method field whose parameter shapes relate only BIVARIANTLY: the field
|
||||
// lifts as a checked adapter, so only the console.log below refuses (a
|
||||
// function value has no inspect lowering).
|
||||
type SrcM = { m(x: { a: number }): void; extra: number };
|
||||
type DstM = { m(x: { a: number; b: number }): void };
|
||||
const srcM: SrcM = {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 9301, "san": 135316 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4900, "san": 6100 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 6706, "san": 112967 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 5031, "san": 84920 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 7191, "san": 12536 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 5283, "san": 8250 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 5800, "san": 62700 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 6298, "san": 72106 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1450, "san": 2550 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4700, "san": 7052 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4589, "san": 7483 }
|
||||
Reference in New Issue
Block a user