Handle omitted and primitive string search values in static calls

This commit is contained in:
Chris Tate
2026-09-25 11:57:35 -05:00
parent 665f0fe647
commit f6ca08fdfa
11 changed files with 142 additions and 27 deletions
@@ -6,7 +6,7 @@ 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, positionNumber } from "../optional-arguments.js";
import { 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 };
@@ -271,8 +271,13 @@ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression,
? dynReceiver()
: lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text);
const loc = locOf(call);
if ((entry.method === "indexOf" || entry.method === "includes" ||
entry.method === "startsWith" || entry.method === "endsWith") && call.arguments.length === 2) {
const searchMethod = entry.method === "indexOf" || entry.method === "includes" ||
entry.method === "startsWith" || entry.method === "endsWith";
if (searchMethod && call.arguments.length < 2) {
const needle = lowerStringSearchArgument(lowerer, call.arguments[0], loc);
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) {
@@ -32,7 +32,7 @@ import {
import { lowerAbsenceProbe } from "./lower-exprs.js";
import { conditionalSpreadOf, lowerDynObjectLiteral } from "./expressions/object-literals.js";
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
import { defaultAfterUndefined, lowerOptionalArgument, lowerStaticallyUndefinedArgument } from "./optional-arguments.js";
import { defaultAfterUndefined, lowerOptionalArgument, lowerStaticallyUndefinedArgument, lowerStringSearchArgument } from "./optional-arguments.js";
import { HTTP2_CONSTANTS } from "./http2-constants.js";
import { CRYPTO_CIPHERS, CRYPTO_CONSTANTS, CRYPTO_CURVES, CRYPTO_HASHES } from "./crypto-tables.js";
import { generatorMeta, timerStyleCallback, type ParamShape } from "./lower-calls.js";
@@ -8946,19 +8946,7 @@ function staticTextDecoderEncoding(label: string): StaticTextDecoderEncoding | n
const receiver = lowerer.lowerExprExpecting(access.expression, STRING);
const positionNode = call.arguments[1];
if (!positionNode) {
const needleNode = call.arguments[0];
let needle: IrExpr = strLit("undefined", loc);
if (needleNode) {
const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, needleNode);
if (undefinedArg) {
needle = defaultAfterUndefined(undefinedArg, needle);
} else {
const value = lowerer.lowerExpr(needleNode);
needle = isUnitType(value.type)
? defaultAfterUndefined(value, strLit(value.type.kind === "nullT" ? "null" : "undefined", loc))
: lowerer.ensureString(value, needleNode);
}
}
const needle = lowerStringSearchArgument(lowerer, call.arguments[0], loc);
return { kind: "libCall", fn: "string.lastIndexOf", args: [receiver, needle], type: F64, loc };
}
const needle = lowerer.lowerExprExpecting(call.arguments[0]!, STRING);
@@ -1,7 +1,7 @@
import * as ts from "../ts7/adapter.js";
import type { IrExpr, IrType } from "../../ir/ir.js";
import type { IrExpr, IrType, SrcLoc } from "../../ir/ir.js";
import { BOOL, F64, isUnitType, typeEquals } from "../../ir/ir.js";
import { numLit } from "../../ir/build.js";
import { numLit, strLit } from "../../ir/build.js";
import type { Lowerer } from "./lowerer.js";
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
@@ -43,6 +43,19 @@ export function defaultAfterUndefined(value: IrExpr, defaultValue: IrExpr): IrEx
};
}
/** Convert an omitted or supplied string-search value after preserving its effects. */
export function lowerStringSearchArgument(lowerer: Lowerer, node: ts.Expression | undefined, loc: SrcLoc): IrExpr {
const absent = strLit("undefined", loc);
if (!node) return absent;
const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, node);
if (undefinedArg) return defaultAfterUndefined(undefinedArg, absent);
const value = lowerer.lowerExpr(node);
if (isUnitType(value.type)) {
return defaultAfterUndefined(value, strLit(value.type.kind === "nullT" ? "null" : "undefined", loc));
}
return lowerer.ensureString(value, node);
}
/** Lower an optional argument, applying its default only to the undefined arm. */
export function lowerOptionalArgument(
lowerer: Lowerer,
@@ -382,13 +382,13 @@ export const STR_METHODS: Record<
> = {
charCodeAt: { method: "charCodeAt", result: F64, minArgs: 0, maxArgs: 1 },
charAt: { method: "charAt", result: STRING, minArgs: 0, maxArgs: 1 },
indexOf: { method: "indexOf", result: F64, minArgs: 1, maxArgs: 2 },
indexOf: { method: "indexOf", result: F64, minArgs: 0, maxArgs: 2 },
// includes with a position argument is indexOf's clamp exactly (the
// spec routes both through StringIndexOf) — the emitter composes
// scr_str_index_of(...) != -1 for the two-argument form.
includes: { method: "includes", result: BOOL, minArgs: 1, maxArgs: 2 },
startsWith: { method: "startsWith", result: BOOL, minArgs: 1, maxArgs: 2 },
endsWith: { method: "endsWith", result: BOOL, minArgs: 1, maxArgs: 2 },
includes: { method: "includes", result: BOOL, minArgs: 0, maxArgs: 2 },
startsWith: { method: "startsWith", result: BOOL, minArgs: 0, maxArgs: 2 },
endsWith: { method: "endsWith", result: BOOL, minArgs: 0, maxArgs: 2 },
slice: { method: "slice", result: STRING, minArgs: 0, maxArgs: 2 },
// substring: slice's clamp-and-swap sibling (negatives clamp to 0
// instead of counting from the end; start > end swaps).
+4 -4
View File
@@ -3511,21 +3511,21 @@
"kind": "stdlib",
"name": "string.prototype.endsWith",
"status": "static",
"note": "the lowered call form takes 1 to 2 arguments"
"note": "the lowered call form takes 0 to 2 arguments"
},
{
"id": "stdlib.string.includes",
"kind": "stdlib",
"name": "string.prototype.includes",
"status": "static",
"note": "the lowered call form takes 1 to 2 arguments"
"note": "the lowered call form takes 0 to 2 arguments"
},
{
"id": "stdlib.string.indexOf",
"kind": "stdlib",
"name": "string.prototype.indexOf",
"status": "static",
"note": "the lowered call form takes 1 to 2 arguments"
"note": "the lowered call form takes 0 to 2 arguments"
},
{
"id": "stdlib.string.isWellFormed",
@@ -3588,7 +3588,7 @@
"kind": "stdlib",
"name": "string.prototype.startsWith",
"status": "static",
"note": "the lowered call form takes 1 to 2 arguments"
"note": "the lowered call form takes 0 to 2 arguments"
},
{
"id": "stdlib.string.substring",
@@ -6967,6 +6967,12 @@
],
"diags": []
},
"<repo>/tests/corpus/3032-string-search-value.js": {
"order": [
"<repo>/tests/corpus/3032-string-search-value.js"
],
"diags": []
},
"<repo>/tests/corpus/304-compound-incdec.ts": {
"order": [
"<repo>/tests/corpus/304-compound-incdec.ts"
+36
View File
@@ -0,0 +1,36 @@
console.log("undefined".indexOf(), "x".indexOf(), "undefined".includes(), "x".includes());
console.log("undefined".startsWith(), "x".startsWith(), "xundefined".endsWith(), "x".endsWith());
console.log("undefined".indexOf(undefined), "undefined".includes(void 0));
console.log("undefined".startsWith(undefined), "undefined".endsWith(undefined));
console.log("anullb".indexOf(null), "anullb".includes(null));
console.log("nulla".startsWith(null), "anull".endsWith(null));
console.log("truefalse".indexOf(true), "truefalse".includes(false));
console.log("truefalse".startsWith(true), "truefalse".endsWith(false));
console.log("42".indexOf(42), "42".includes(42), "42".startsWith(42), "42".endsWith(42));
console.log("1".indexOf(1n), "1".includes(1n), "1".startsWith(1n), "1".endsWith(1n));
/** @param {string | number | boolean | null | undefined} value */
function search(value) {
const text = "undefined-null-1-true";
console.log(text.indexOf(value), text.includes(value), text.startsWith(value), text.endsWith(value));
}
search(undefined);
search(null);
search(1);
search(true);
search("true");
let trace = "";
function receiver() { trace += "r"; return "undefined"; }
function needleEffect() { trace += "n"; return 1; }
console.log(receiver().indexOf(void needleEffect()), trace);
trace = "";
console.log(receiver().includes(void needleEffect()), trace);
trace = "";
console.log(receiver().startsWith(void needleEffect()), trace);
trace = "";
console.log(receiver().endsWith(void needleEffect()), trace);
function nullEffect() { trace += "n"; return null; }
trace = "";
console.log(receiver().indexOf(nullEffect()), trace);
+6
View File
@@ -63,6 +63,9 @@
"test/built-ins/String/prototype/includes/coerced-values-of-position.js",
"test/built-ins/String/prototype/includes/return-false-with-out-of-bounds-position.js",
"test/built-ins/String/prototype/includes/return-true-if-searchstring-is-empty.js",
"test/built-ins/String/prototype/indexOf/S15.5.4.7_A1_T4.js",
"test/built-ins/String/prototype/indexOf/S15.5.4.7_A1_T5.js",
"test/built-ins/String/prototype/indexOf/S15.5.4.7_A1_T7.js",
"test/built-ins/String/prototype/lastIndexOf/S15.5.4.8_A1_T4.js",
"test/built-ins/String/prototype/lastIndexOf/S15.5.4.8_A1_T5.js",
"test/built-ins/String/prototype/lastIndexOf/S15.5.4.8_A1_T7.js",
@@ -156,6 +159,9 @@
"test/built-ins/String/prototype/includes/coerced-values-of-position.js": "292b6f29f5f223027deb54dafe62726790e2a61ddf2eb5e5036688b7088f105f",
"test/built-ins/String/prototype/includes/return-false-with-out-of-bounds-position.js": "4e65326e88ae9b915771fcc70c52fd7e4aa1f7a6aa8ca047010806555b57d668",
"test/built-ins/String/prototype/includes/return-true-if-searchstring-is-empty.js": "c5552ee08afae1dd6147d388e987a1083571d2faf18ace56b0324204d1ea50b2",
"test/built-ins/String/prototype/indexOf/S15.5.4.7_A1_T4.js": "dee63261be6e97a1a42a9267c98b21144489caf800d6dcc3e93bde61c2cf5103",
"test/built-ins/String/prototype/indexOf/S15.5.4.7_A1_T5.js": "b3e357871ed5fee11206e1c7ecef7c7cc094a3a2a37eacbcfbdaec600ccca754",
"test/built-ins/String/prototype/indexOf/S15.5.4.7_A1_T7.js": "31ee4085c1ab60c5ace3f4c4adfca8afdbc6b35d029f843fc8041d38f31a9435",
"test/built-ins/String/prototype/lastIndexOf/S15.5.4.8_A1_T4.js": "db9a202295d7f2f48cb023cf6774e3e23084e05dbb9f1071f4aa7e3bd93710b3",
"test/built-ins/String/prototype/lastIndexOf/S15.5.4.8_A1_T5.js": "fc3de1c70005ef91a3ab573220e0660255a33cf1bcf8d169d7845622747f02fe",
"test/built-ins/String/prototype/lastIndexOf/S15.5.4.8_A1_T7.js": "d9392644874a8a2ffb6549f52aa2447d6d03c3e43762008fc6a32c18d35dea6a",
@@ -0,0 +1,19 @@
// Copyright 2009 the Sputnik authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
info: String.prototype.indexOf(searchString, position)
es5id: 15.5.4.7_A1_T4
description: >
Call indexOf(searchString, position) function without arguments of
string
---*/
//////////////////////////////////////////////////////////////////////////////
//CHECK#1
//since ToString() evaluates to "" indexOf() evaluates to indexOf("",0)
if ("".indexOf() !== -1) {
throw new Test262Error('#1: "".indexOf() === -1. Actual: ' + ("".indexOf()));
}
//
//////////////////////////////////////////////////////////////////////////////
@@ -0,0 +1,23 @@
// Copyright 2009 the Sputnik authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
info: String.prototype.indexOf(searchString, position)
es5id: 15.5.4.7_A1_T5
description: >
Call indexOf(searchString, position) function with null argument
of function object
---*/
//////////////////////////////////////////////////////////////////////////////
//CHECK#1
//since ToString(null) evaluates to "null" indexOf(null) evaluates to indexOf("",0)
if (function() {
return "gnulluna"
}().indexOf(null) !== 1) {
throw new Test262Error('#1: function(){return "gnulluna"}().indexOf(null) === 1. Actual: ' + function() {
return "gnulluna"
}().indexOf(null));
}
//
//////////////////////////////////////////////////////////////////////////////
@@ -0,0 +1,19 @@
// Copyright 2009 the Sputnik authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
info: String.prototype.indexOf(searchString, position)
es5id: 15.5.4.7_A1_T7
description: >
Call indexOf(searchString, position) function with undefined
argument of string object
---*/
//////////////////////////////////////////////////////////////////////////////
//CHECK#1
//since ToString(undefined) evaluates to "undefined" indexOf(undefined) evaluates to indexOf("undefined",0)
if (String("undefined").indexOf(undefined) !== 0) {
throw new Test262Error('#1: String("undefined").indexOf(undefined) === 0. Actual: ' + String("undefined").indexOf(undefined));
}
//
//////////////////////////////////////////////////////////////////////////////