From cd85070d5bc10c420183fb04acbfd2b04845d5b8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 25 Sep 2026 10:01:40 -0500 Subject: [PATCH] Handle optional string-search needles with positions --- .../lowering/containers/string-and-regexp.ts | 17 ++++++++++------- .../test/ts7/baselines/order-parity.json | 6 ++++++ tests/corpus/3029-string-search-union-needle.js | 17 +++++++++++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 tests/corpus/3029-string-search-union-needle.js diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 8a1d5fd6..a3b370ca 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -274,7 +274,8 @@ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression, if ((entry.method === "indexOf" || entry.method === "includes" || entry.method === "startsWith" || entry.method === "endsWith") && call.arguments.length === 2) { const needle = lowerer.lowerExpr(call.arguments[0]!); - if (needle.type.kind !== "string" && needle.type.kind !== "dyn") { + const optionalNeedle = needle.type.kind === "union" && lowerer.runtimeOptionalWidening(needle.type, STRING) !== null; + if (needle.type.kind !== "string" && needle.type.kind !== "dyn" && !optionalNeedle) { lowerer.noLowering(`.${entry.method} with '${lowerer.fmt(needle.type)}' search values`, call); } const defaultPosition: IrExpr = entry.method === "endsWith" @@ -289,8 +290,8 @@ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression, if (!helper) { helper = `%str.positions.${lowerer.widthHelpers.size}`; const params = [receiver, needle, position].map((arg, index) => ({ localId: `arg.${index}`, name: `arg${index}`, type: arg.type })); - const dynamicNeedle = needle.type.kind === "dyn"; - const search: IrExpr = dynamicNeedle ? varRef("search.0", STRING, loc) : varRef("arg.1", STRING, loc); + const coerceNeedle = needle.type.kind !== "string"; + const search: IrExpr = coerceNeedle ? varRef("search.0", STRING, loc) : varRef("arg.1", STRING, loc); const result: IrExpr = { kind: "strIntrinsic", method: entry.method, receiver: varRef("arg.0", STRING, loc), args: [ @@ -301,11 +302,13 @@ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression, }; const locals = params.map(param => ({ id: param.localId, name: param.name, type: param.type, mutable: false })); const body: IrStmt[] = []; - if (dynamicNeedle) { + if (coerceNeedle) { + const value = varRef("arg.1", needle.type, loc); + const init: IrExpr = needle.type.kind === "dyn" + ? { kind: "libCall", fn: "dyn.toStringCoerce", args: [value], type: STRING, loc } + : lowerer.ensureString(value, call.arguments[0]!); locals.push({ id: "search.0", name: "search", type: STRING, mutable: false }); - body.push({ kind: "varDecl", localId: "search.0", init: { - kind: "libCall", fn: "dyn.toStringCoerce", args: [varRef("arg.1", needle.type, loc)], type: STRING, loc, - }, loc }); + body.push({ kind: "varDecl", localId: "search.0", init, loc }); } body.push({ kind: "return", value: result, loc }); lowerer.widthHelpers.set(key, helper); diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 22b4a9d2..bdb78512 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -6943,6 +6943,12 @@ ], "diags": [] }, + "/tests/corpus/3029-string-search-union-needle.js": { + "order": [ + "/tests/corpus/3029-string-search-union-needle.js" + ], + "diags": [] + }, "/tests/corpus/303-break-continue.ts": { "order": [ "/tests/corpus/303-break-continue.ts" diff --git a/tests/corpus/3029-string-search-union-needle.js b/tests/corpus/3029-string-search-union-needle.js new file mode 100644 index 00000000..dd7e1ebb --- /dev/null +++ b/tests/corpus/3029-string-search-union-needle.js @@ -0,0 +1,17 @@ +/** @param {string | undefined} needle */ +function probe(needle) { + const value = "ababa"; + console.log( + value.indexOf(needle), + value.indexOf(needle, 1), + value.includes(needle), + value.includes(needle, 1), + value.startsWith(needle), + value.startsWith(needle, 1), + value.endsWith(needle), + value.endsWith(needle, 2), + ); +} + +probe("ba"); +probe(undefined);