diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index a8b782b51..2b1485c72 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -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); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6481-set-add-chaining.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6481-set-add-chaining.ts.json new file mode 100644 index 000000000..1168a1cbc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6481-set-add-chaining.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6481-set-add-chaining.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6481-set-add-chaining.ts b/tests/corpus/6481-set-add-chaining.ts new file mode 100644 index 000000000..13ffda558 --- /dev/null +++ b/tests/corpus/6481-set-add-chaining.ts @@ -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(); +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(); +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 { + const out = new Set(); + 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 { + 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(); +console.log(picked === seen, seen.has("g")); + +[1, 2, 2, 5].forEach((n) => nodes.add(new Item(`n${n}`))); +console.log(nodes.size);