From 9f897fbec6fe714b9e715c0a9489d98e42529b37 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 25 Sep 2026 13:07:51 -0500 Subject: [PATCH] Handle scalar search values in positioned string methods (#436) --- .../lowering/containers/string-and-regexp.ts | 20 +++++++--- .../frontend/lowering/optional-arguments.ts | 6 ++- .../test/ts7/baselines/order-parity.json | 6 +++ .../3035-string-positioned-search-value.js | 37 +++++++++++++++++++ 4 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 tests/corpus/3035-string-positioned-search-value.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 887cf5ee..9c8d6879 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -1,12 +1,12 @@ import * as ts from "../../ts7/adapter.js"; import { BOOL, F64, IrExpr, IrFunction, IrStmt, IrType, STRING, SrcLoc, arrayOf, isUnitType, typeEquals, typeKey } from "../../../ir/ir.js"; -import { numLit, varRef } from "../../../ir/build.js"; +import { numLit, strLit, varRef } from "../../../ir/build.js"; import { locOf } from "../../program.js"; import type { Lowerer } from "../lowerer.js"; import { own } from "../lowerer.js"; import { isRequireMainFilename } from "../expressions/optional-chains.js"; import { STR_METHODS } from "../surfaces.js"; -import { defaultAfterUndefined, lowerOptionalArgument, lowerPositionArgument, lowerStaticallyUndefinedArgument, lowerStringSearchArgument, positionNumber } from "../optional-arguments.js"; +import { coerceStringSearchValue, defaultAfterUndefined, lowerOptionalArgument, lowerPositionArgument, lowerStaticallyUndefinedArgument, lowerStringSearchArgument, positionNumber } from "../optional-arguments.js"; function lowerSplitLimitArg(lowerer: Lowerer, node: ts.Expression | undefined, loc: SrcLoc): IrExpr { const defaultValue: IrExpr = { kind: "numLit", value: 4294967295, type: F64, loc }; @@ -278,9 +278,17 @@ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression, return { kind: "strIntrinsic", method: entry.method, receiver, args: [needle], type: entry.result, loc }; } if (searchMethod && call.arguments.length === 2) { - const needle = lowerer.lowerExpr(call.arguments[0]!); - const optionalNeedle = needle.type.kind === "union" && lowerer.runtimeOptionalWidening(needle.type, STRING) !== null; - if (needle.type.kind !== "string" && needle.type.kind !== "dyn" && !optionalNeedle) { + const needleNode = call.arguments[0]!; + const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, needleNode); + let needle = undefinedArg + ? defaultAfterUndefined(undefinedArg, strLit("undefined", loc)) + : lowerer.lowerExpr(needleNode); + if (isUnitType(needle.type)) needle = coerceStringSearchValue(lowerer, needle, needleNode, loc); + const scalarUnion = needle.type.kind === "union" && + (lowerer.unions.get(needle.type.unionId)?.arms.every((arm) => + arm.kind === "string" || arm.kind === "f64" || arm.kind === "bool" || arm.kind === "bigint" || isUnitType(arm)) ?? false); + const scalarNeedle = needle.type.kind === "f64" || needle.type.kind === "bool" || needle.type.kind === "bigint" || scalarUnion; + if (needle.type.kind !== "string" && needle.type.kind !== "dyn" && !scalarNeedle) { lowerer.noLowering(`.${entry.method} with '${lowerer.fmt(needle.type)}' search values`, call); } const defaultPosition: IrExpr = entry.method === "endsWith" @@ -311,7 +319,7 @@ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression, 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]!); + : coerceStringSearchValue(lowerer, value, needleNode, loc); locals.push({ id: "search.0", name: "search", type: STRING, mutable: false }); body.push({ kind: "varDecl", localId: "search.0", init, loc }); } diff --git a/packages/compiler/src/frontend/lowering/optional-arguments.ts b/packages/compiler/src/frontend/lowering/optional-arguments.ts index 02f398ba..589273b8 100644 --- a/packages/compiler/src/frontend/lowering/optional-arguments.ts +++ b/packages/compiler/src/frontend/lowering/optional-arguments.ts @@ -49,7 +49,11 @@ export function lowerStringSearchArgument(lowerer: Lowerer, node: ts.Expression if (!node) return absent; const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, node); if (undefinedArg) return defaultAfterUndefined(undefinedArg, absent); - const value = lowerer.lowerExpr(node); + return coerceStringSearchValue(lowerer, lowerer.lowerExpr(node), node, loc); +} + +/** Convert a lowered string-search value while preserving unit-value effects. */ +export function coerceStringSearchValue(lowerer: Lowerer, value: IrExpr, node: ts.Expression, loc: SrcLoc): IrExpr { if (isUnitType(value.type)) { return defaultAfterUndefined(value, strLit(value.type.kind === "nullT" ? "null" : "undefined", loc)); } diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 8d87f0ea..3d36d9e2 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -6985,6 +6985,12 @@ ], "diags": [] }, + "/tests/corpus/3035-string-positioned-search-value.js": { + "order": [ + "/tests/corpus/3035-string-positioned-search-value.js" + ], + "diags": [] + }, "/tests/corpus/304-compound-incdec.ts": { "order": [ "/tests/corpus/304-compound-incdec.ts" diff --git a/tests/corpus/3035-string-positioned-search-value.js b/tests/corpus/3035-string-positioned-search-value.js new file mode 100644 index 00000000..a48093ee --- /dev/null +++ b/tests/corpus/3035-string-positioned-search-value.js @@ -0,0 +1,37 @@ +console.log("xundefinedy".indexOf(undefined, 1), "xundefinedy".includes(undefined, 1)); +console.log("xundefinedy".startsWith(undefined, 1), "xundefinedy".endsWith(undefined, 10)); +console.log("xnullnull".indexOf(null, 2), "xnullnull".includes(null, 5)); +console.log("xnull".startsWith(null, 1), "xnull".endsWith(null, 5)); +console.log("xtruefalse".indexOf(true, 1), "xtruefalse".includes(false, 5)); +console.log("xtruefalse".startsWith(true, 1), "xtruefalse".endsWith(false, 10)); +console.log("x42".indexOf(42, 1), "x42".includes(42, 1), "x42".startsWith(42, 1), "x42".endsWith(42, 3)); +console.log("x1".indexOf(1n, 1), "x1".includes(1n, 1), "x1".startsWith(1n, 1), "x1".endsWith(1n, 2)); +console.log("x0NaNInfinity".indexOf(-0, 1), "x0NaNInfinity".includes(NaN, 2)); +console.log("xInfinity".startsWith(Infinity, 1), "xInfinity".endsWith(Infinity, 9)); + +/** @param {string | number | boolean | null | undefined} needle */ +function search(needle) { + const text = "xundefined-null-1-true"; + console.log(text.indexOf(needle, 1), text.includes(needle, 1), text.startsWith(needle, 1), text.endsWith(needle, 11)); +} +search(undefined); +search(null); +search(1); +search(true); +search("undefined"); + +let trace = ""; +function receiver() { trace += "r"; return "xundefinedy"; } +function needleEffect() { trace += "n"; return 1; } +function position() { trace += "p"; return "1"; } +console.log(receiver().indexOf(void needleEffect(), position()), trace); +trace = ""; +console.log(receiver().includes(void needleEffect(), position()), trace); +trace = ""; +console.log(receiver().startsWith(void needleEffect(), position()), trace); +trace = ""; +console.log(receiver().endsWith(void needleEffect(), position()), trace); + +function nullEffect() { trace += "n"; return null; } +trace = ""; +console.log(receiver().indexOf(nullEffect(), position()), trace);