mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Adopt a contextual union's map arm for a bare new Map()
`new Map()` has no type arguments, and the no-argument constructor gives the checker nothing to infer them from: the expression types as Map<unknown, unknown> however the destination spells it. Construction adopted a contextual type only when it mapped to a map, so a nullable or readonly annotation — `Map<K, V> | undefined`, `Map<K, V> | null`, `ReadonlyMap<K, V> | undefined`, an optional record field — refused the very value its slot was built for (SC2003; pi's extension registry, whose `handlers?: Map<...>` fields start empty). A Map value can only inhabit a union's map arm, so construction now adopts that arm when the contextual union has exactly one: the remainder of the annotation is the nullish absence the value never is. Two different map arms stay refused (SC2003), because the layout would be ambiguous. Corpus 8528 pins the nullable, readonly and optional-field annotations, a parameter slot, iteration after the adoption, and remains byte-exact against Node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
223973a56b
commit
49b133751c
@@ -10071,6 +10071,21 @@ function lowerProgramClassNew(
|
||||
});
|
||||
}
|
||||
|
||||
/** The supported map layout a contextual type pins for `new Map()`: the type
|
||||
* itself, or the single map arm of a union — a nullable annotation such as
|
||||
* `Map<K, V> | undefined`, or `ReadonlyMap<K, V> | undefined`, where a Map
|
||||
* value can only inhabit the map arm and the no-argument constructor gives
|
||||
* the checker nothing to infer the type arguments from. */
|
||||
function contextualMapLayout(lowerer: Lowerer, ctx: ts.Type): IrType | null {
|
||||
const mapped = lowerer.mapTypeOf(ctx);
|
||||
if (mapped?.kind === "map") return mapped;
|
||||
if (mapped?.kind !== "union") return null;
|
||||
const arms = (lowerer.unions.get(mapped.unionId)?.arms ?? []).filter(
|
||||
(arm): arm is IrType & { kind: "map" } => arm.kind === "map",
|
||||
);
|
||||
return arms.length === 1 ? arms[0]! : null;
|
||||
}
|
||||
|
||||
function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType | null {
|
||||
const parent = expr.parent;
|
||||
if (
|
||||
@@ -11064,10 +11079,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
mapped = lowerer.mapTypeOf(inferred);
|
||||
} else if (mapped?.kind !== "map" || contextualSeed) {
|
||||
const ctx = lowerer.checker.getContextualType(expr);
|
||||
const ctxMapped = ctx ? lowerer.mapTypeOf(ctx) : null;
|
||||
if (ctx && ctxMapped?.kind === "map") {
|
||||
const ctxMap = ctx ? contextualMapLayout(lowerer, ctx) : null;
|
||||
if (ctx && ctxMap !== null) {
|
||||
tsType = ctx;
|
||||
mapped = ctxMapped;
|
||||
mapped = ctxMap;
|
||||
}
|
||||
}
|
||||
if (seedArg && !entriesLit && mapped?.kind === "map") {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8528-new-collection-contextual-union.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// `new Map()` (no type arguments) in a contextual slot whose type is a
|
||||
// union: `Map<K, V> | undefined`, `ReadonlyMap<K, V> | null`. A Map value
|
||||
// can only inhabit the map arm, so the construction adopts that layout
|
||||
// instead of the checker's bare Map<unknown, unknown>.
|
||||
|
||||
function lookup(handlers: Map<string, number> | undefined, key: string): string {
|
||||
if (handlers === undefined) return "absent";
|
||||
const found = handlers.get(key);
|
||||
return found === undefined ? "missing" : String(found);
|
||||
}
|
||||
|
||||
const counts: Map<string, number> | undefined = new Map();
|
||||
if (counts !== undefined) {
|
||||
counts.set("a", 1);
|
||||
counts.set("b", counts.size + 2);
|
||||
console.log(counts.size, counts.get("a"), counts.get("b"));
|
||||
}
|
||||
console.log(lookup(counts, "a"), lookup(counts, "z"), lookup(undefined, "a"));
|
||||
console.log(counts instanceof Map);
|
||||
|
||||
// A nullable annotation takes the same arm.
|
||||
const nullable: Map<string, number> | null = new Map();
|
||||
console.log(nullable !== null, nullable instanceof Map);
|
||||
|
||||
// A readonly view announces no writes; the value is still a real Map.
|
||||
const readonlyView: ReadonlyMap<string, number> | undefined = new Map();
|
||||
if (readonlyView !== undefined) {
|
||||
console.log(readonlyView.size, readonlyView.get("missing") === undefined);
|
||||
}
|
||||
|
||||
// The contextual type of an optional record field is the same union.
|
||||
type Registry = {
|
||||
handlers?: Map<string, number>;
|
||||
names?: Set<string>;
|
||||
};
|
||||
|
||||
function createRegistry(): Registry {
|
||||
return { handlers: new Map(), names: new Set() };
|
||||
}
|
||||
|
||||
const registry = createRegistry();
|
||||
if (registry.handlers !== undefined) {
|
||||
registry.handlers.set("on", 7);
|
||||
console.log(registry.handlers.size, registry.handlers.get("on"));
|
||||
}
|
||||
if (registry.names !== undefined) {
|
||||
registry.names.add("first");
|
||||
console.log(registry.names.size, registry.names.has("first"));
|
||||
}
|
||||
|
||||
// A parameter slot infers the same way for Map and Set.
|
||||
function describe(map: Map<string, number> | undefined, set: Set<string> | undefined): string {
|
||||
return `${map === undefined ? "-" : map.size}/${set === undefined ? "-" : set.size}`;
|
||||
}
|
||||
console.log(describe(new Map(), new Set()), describe(undefined, undefined));
|
||||
|
||||
// Iteration and JSON-free surfaces keep the element types.
|
||||
const entries = new Map([["x", 1], ["y", 2]] as [string, number][]);
|
||||
console.log([...entries.keys()].join(","), [...entries.values()].join(","));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3100, "san": 3700 }
|
||||
Reference in New Issue
Block a user