mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-06 08:23:19 +08:00
Return the Set from add() when its result is used
`s.add(a).add(b)`, `s.add(x) === s`, `return s.add(x)` and other reads of add()'s result were refused with "chained Set method calls": the lowered add was a void intrinsic. A used result now reads the receiver once into a slot, adds, and answers the slot, as Map's set() already does. Discarded results (statements, arrow expression bodies, `void`, for headers, comma left operands) keep the plain intrinsic, so existing programs compile unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e1c27176a6
commit
205b552104
@@ -524,6 +524,36 @@ export function lowerMapForEachCall(
|
||||
return { kind: "call", callee: helper, args: [receiver, fnArg], type: VOID, loc };
|
||||
}
|
||||
|
||||
/** True when nothing reads an add() call's result: an expression
|
||||
* statement, an arrow's expression body, a `void` operand, a for header's
|
||||
* initializer or incrementor, a comma's left operand, or a conditional or
|
||||
* comma result that is itself discarded. */
|
||||
function setAddResultDiscarded(call: ts.CallExpression): boolean {
|
||||
let node: ts.Node = call;
|
||||
for (;;) {
|
||||
let parent = node.parent;
|
||||
while (parent !== undefined && ts.isParenthesizedExpression(parent)) {
|
||||
node = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (parent === undefined) return false;
|
||||
if (ts.isExpressionStatement(parent) || ts.isVoidExpression(parent)) return true;
|
||||
if (ts.isArrowFunction(parent)) return parent.body === node;
|
||||
if (ts.isForStatement(parent))
|
||||
return parent.initializer === node || parent.incrementor === node;
|
||||
if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.CommaToken) {
|
||||
if (parent.left === node) return true;
|
||||
node = parent;
|
||||
continue;
|
||||
}
|
||||
if (ts.isConditionalExpression(parent) && parent.condition !== node) {
|
||||
node = parent;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Ambient Set method calls — Map's lowering with the value slot gone.
|
||||
* `add`/`has`/`delete`/`clear` lower to setIntrinsic; `forEach` desugars
|
||||
* like Map's over the shared iteration primitives. Null when this isn't
|
||||
@@ -558,8 +588,8 @@ export function lowerSetMethodCall(
|
||||
const value = lowerer.lowerExpr(access.expression);
|
||||
const receiver =
|
||||
lowerer.runtimeOptionalPropertyReceiver(access.expression, value, receiverIr, name) ?? value;
|
||||
// The lib's `add` returns the Set (chaining typechecks); the lowered
|
||||
// add is a void statement — fence chained receivers like Map's set.
|
||||
// A used add() result lowers to its receiver (below), so a chained
|
||||
// receiver is a Set; anything else lowering to a non-Set stays fenced.
|
||||
if (receiver.type.kind !== "set") {
|
||||
lowerer.noLowering(
|
||||
"chained Set method calls",
|
||||
@@ -582,7 +612,28 @@ export function lowerSetMethodCall(
|
||||
}
|
||||
if (name === "add") {
|
||||
const v = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.elem);
|
||||
return { kind: "setIntrinsic", method: "add", receiver, args: [v], type: VOID, loc };
|
||||
if (setAddResultDiscarded(call)) {
|
||||
return { kind: "setIntrinsic", method: "add", receiver, args: [v], type: VOID, loc };
|
||||
}
|
||||
// add() returns its Set: a used result (`s.add(a).add(b)`,
|
||||
// `s.add(x) === s`) reads the receiver once into a slot, adds, and
|
||||
// answers the slot, as Map's set() does.
|
||||
const slot = lowerer.declareHiddenLocal("%setAddReceiver", receiver.type);
|
||||
const ref = varRef(slot.id, receiver.type, loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: slot.id, init: receiver, loc },
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "setIntrinsic", method: "add", receiver: ref, args: [v], type: VOID, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: ref,
|
||||
type: receiver.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (name === "has" || name === "delete") {
|
||||
const v = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.elem);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6481-set-add-chaining.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Set.prototype.add returns the Set, so its result can be chained,
|
||||
// compared, stored and returned. Results nobody reads keep the plain
|
||||
// statement form.
|
||||
class Item {
|
||||
name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
seen.add("a").add("b").add("a");
|
||||
console.log(seen.size, [...seen].join(","));
|
||||
console.log(seen.add("c") === seen, seen.has("c"));
|
||||
|
||||
const nodes = new Set<Item>();
|
||||
const first = new Item("first");
|
||||
const memo = new Item("memo");
|
||||
nodes.add(first).add(memo);
|
||||
console.log(nodes.size, nodes.has(first), nodes.has(memo));
|
||||
|
||||
function collect(values: number[]): Set<number> {
|
||||
const out = new Set<number>();
|
||||
for (const v of values) out.add(v);
|
||||
return out.add(-1);
|
||||
}
|
||||
console.log([...collect([3, 1, 3, 2])].join(" "));
|
||||
|
||||
const stored = seen.add("d");
|
||||
console.log(stored === seen, stored.size);
|
||||
|
||||
let calls = 0;
|
||||
function target(): Set<string> {
|
||||
calls++;
|
||||
return seen;
|
||||
}
|
||||
target().add("e").add("f");
|
||||
console.log(calls, seen.size);
|
||||
|
||||
const flag = seen.size > 3;
|
||||
const picked = flag ? seen.add("g") : new Set<string>();
|
||||
console.log(picked === seen, seen.has("g"));
|
||||
|
||||
[1, 2, 2, 5].forEach((n) => nodes.add(new Item(`n${n}`)));
|
||||
console.log(nodes.size);
|
||||
Reference in New Issue
Block a user