Handle scalar search values in positioned string methods (#436)

This commit is contained in:
Chris Tate
2026-09-25 13:07:51 -05:00
committed by GitHub
parent 266e22b151
commit 9f897fbec6
4 changed files with 62 additions and 7 deletions
@@ -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 });
}
@@ -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));
}
@@ -6985,6 +6985,12 @@
],
"diags": []
},
"<repo>/tests/corpus/3035-string-positioned-search-value.js": {
"order": [
"<repo>/tests/corpus/3035-string-positioned-search-value.js"
],
"diags": []
},
"<repo>/tests/corpus/304-compound-incdec.ts": {
"order": [
"<repo>/tests/corpus/304-compound-incdec.ts"
@@ -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);