Handle coerced positions in static string search calls (#424)

* Coerce static string search positions

* Record TypeScript parity for string search fixture
This commit is contained in:
Chris Tate
2026-09-25 08:30:18 -05:00
committed by GitHub
parent 58bd6749b5
commit 2367c081bf
7 changed files with 161 additions and 8 deletions
@@ -320,14 +320,15 @@ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression,
? dynReceiver()
: lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text);
const loc = locOf(call);
if ((entry.method === "startsWith" || entry.method === "endsWith") && call.arguments.length === 2) {
if ((entry.method === "indexOf" || entry.method === "includes" ||
entry.method === "startsWith" || entry.method === "endsWith") && call.arguments.length === 2) {
const needle = lowerer.lowerExpr(call.arguments[0]!);
const defaultPosition: IrExpr = entry.method === "startsWith"
? numLit(0, loc)
: { kind: "bin", op: "/", left: numLit(1, loc), right: numLit(0, loc), type: F64, loc };
const defaultPosition: IrExpr = entry.method === "endsWith"
? { kind: "bin", op: "/", left: numLit(1, loc), right: numLit(0, loc), type: F64, loc }
: numLit(0, loc);
const position = lowerStringPositionArgument(lowerer, call.arguments[1], defaultPosition);
if (position.type.kind === "f64" || position.type.kind === "jsval") {
return { kind: "strIntrinsic", method: entry.method, receiver, args: [needle, position], type: BOOL, loc };
return { kind: "strIntrinsic", method: entry.method, receiver, args: [needle, position], type: entry.result, loc };
}
const key = `str.positions:${entry.method}:${typeKey(needle.type)}:${typeKey(position.type)}`;
let helper = lowerer.widthHelpers.get(key);
@@ -340,16 +341,16 @@ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression,
varRef("arg.1", needle.type, loc),
stringPositionNumber(lowerer, varRef("arg.2", position.type, loc), defaultPosition, call.arguments[1]!),
],
type: BOOL, loc,
type: entry.result, loc,
};
lowerer.widthHelpers.set(key, helper);
lowerer.liftedFns.push({
name: helper, params, returnType: BOOL,
name: helper, params, returnType: entry.result,
locals: params.map(param => ({ id: param.localId, name: param.name, type: param.type, mutable: false })),
body: [{ kind: "return", value: result, loc }], loc,
});
}
return { kind: "call", callee: helper, args: [receiver, needle, position], type: BOOL, loc };
return { kind: "call", callee: helper, args: [receiver, needle, position], type: entry.result, loc };
}
if (entry.method === "charAt" || entry.method === "charCodeAt" || entry.method === "slice" || entry.method === "substring") {
const defaults: IrExpr[] = [numLit(0, loc)];
@@ -6919,6 +6919,12 @@
],
"diags": []
},
"<repo>/tests/corpus/3025-string-search-position-coercion.js": {
"order": [
"<repo>/tests/corpus/3025-string-search-position-coercion.js"
],
"diags": []
},
"<repo>/tests/corpus/303-break-continue.ts": {
"order": [
"<repo>/tests/corpus/303-break-continue.ts"
@@ -0,0 +1,19 @@
let trace = "";
function receiver() { trace += "r"; return "abcde"; }
function needle() { trace += "n"; return "bc"; }
function position() { trace += "p"; return "1.9"; }
console.log(receiver().indexOf(needle(), position()), trace);
trace = "";
console.log(receiver().includes(needle(), position()), trace);
trace = "";
console.log(receiver().indexOf(needle(), void position()), trace);
trace = "";
console.log(receiver().includes(needle(), void position()), trace);
/** @type {(number | string | boolean | null | undefined)[]} */
const positions = [undefined, null, false, true, "", "2.9", "bad", NaN, -Infinity, -3, 0, 1, 3, Infinity, 99];
for (const value of positions) {
const text = "a😀bc😀d";
console.log(text.indexOf("😀", value), text.indexOf("", value));
console.log(text.includes("bc", value), text.includes("", value));
}
+6
View File
@@ -51,6 +51,9 @@
"test/built-ins/String/prototype/endsWith/return-true-if-searchstring-is-empty.js",
"test/built-ins/String/prototype/endsWith/searchstring-found-with-position.js",
"test/built-ins/String/prototype/endsWith/searchstring-not-found-with-position.js",
"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/lastIndexOf/not-a-substring.js",
"test/built-ins/String/prototype/slice/S15.5.4.13_A1_T4.js",
"test/built-ins/String/prototype/startsWith/out-of-bounds-position.js",
@@ -129,6 +132,9 @@
"test/built-ins/String/prototype/endsWith/return-true-if-searchstring-is-empty.js": "708b505a6dc830c907bb483b99c124582de700a9f2c0251985ace72361afaa72",
"test/built-ins/String/prototype/endsWith/searchstring-found-with-position.js": "532eb90be62d4faa2faa230ee91180ea260dc330b4427b9a08a696ac7417a3a4",
"test/built-ins/String/prototype/endsWith/searchstring-not-found-with-position.js": "5995922f82d645d02eabbbd75ec1b5e2d250f93870a3edd20ee491691a953743",
"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/lastIndexOf/not-a-substring.js": "1fa0e88ffe6a342e6204c925918ae6fc29b003d47007210fc377c24bc56bd41d",
"test/built-ins/String/prototype/slice/S15.5.4.13_A1_T4.js": "1f67b7089a1e53f850b6ceb962193f49d8a2c114fc37a0377ac6553108c83ac0",
"test/built-ins/String/prototype/startsWith/out-of-bounds-position.js": "2c27bbb2505d166a6a0fbab388ca9660780c6b396e0634e87a3e855c11865cb3",
@@ -0,0 +1,40 @@
// Copyright (C) 2015 the V8 project authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
es6id: 21.1.3.7
description: >
Returns based on coerced values of position.
info: |
21.1.3.7 String.prototype.includes ( searchString [ , position ] )
...
9. Let pos be ToInteger(position). (If position is undefined, this step
produces the value 0).
10. ReturnIfAbrupt(pos).
11. Let len be the number of elements in S.
12. Let start be min(max(pos, 0), len).
13. Let searchLen be the number of elements in searchStr.
14. If there exists any integer k not smaller than start such that k +
searchLen is not greater than len, and for all nonnegative integers j less
than searchLen, the code unit at index k+j of S is the same as the code unit
at index j of searchStr, return true; but if there is no such integer k,
return false.
...
features: [String.prototype.includes]
---*/
var str = 'The future is cool!';
assert(str.includes('The future', NaN), 'NaN coerced to 0');
assert(str.includes('The future', null), 'null coerced to 0');
assert(str.includes('The future', false), 'false coerced to 0');
assert(str.includes('The future', ''), '"" coerced to 0');
assert(str.includes('The future', '0'), '"0" coerced to 0');
assert(str.includes('The future', undefined), 'undefined coerced to 0');
assert(str.includes('The future', 0.4), '0.4 coerced to 0');
assert(str.includes('The future', -1));
assert.sameValue(str.includes('The future', true), false, 'true coerced to 1');
assert.sameValue(str.includes('The future', '1'), false, '"1" coerced to 1');
assert.sameValue(str.includes('The future', 1.4), false, '1.4 coerced to 1');
@@ -0,0 +1,43 @@
// Copyright (C) 2015 the V8 project authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
es6id: 21.1.3.7
description: >
Returns false if position is >= this.length and searchString.length > 0.
info: |
21.1.3.7 String.prototype.includes ( searchString [ , position ] )
...
11. Let len be the number of elements in S.
12. Let start be min(max(pos, 0), len).
13. Let searchLen be the number of elements in searchStr.
14. If there exists any integer k not smaller than start such that k +
searchLen is not greater than len, and for all nonnegative integers j less
than searchLen, the code unit at index k+j of S is the same as the code unit
at index j of searchStr, return true; but if there is no such integer k,
return false.
...
features: [String.prototype.includes]
---*/
var str = 'The future is cool!';
assert.sameValue(
str.includes('!', str.length + 1), false,
'str.includes("!", str.length + 1) returns false'
);
assert.sameValue(
str.includes('!', 100), false,
'str.includes("!", 100) returns false'
);
assert.sameValue(
str.includes('!', Infinity), false,
'str.includes("!", Infinity) returns false'
);
assert.sameValue(
str.includes('!', str.length), false,
'str.includes("!", str.length) returns false'
);
@@ -0,0 +1,38 @@
// Copyright (C) 2015 the V8 project authors. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
es6id: 21.1.3.7
description: >
Returns true if searchString.length == 0.
info: |
21.1.3.7 String.prototype.includes ( searchString [ , position ] )
...
11. Let len be the number of elements in S.
12. Let start be min(max(pos, 0), len).
13. Let searchLen be the number of elements in searchStr.
14. If there exists any integer k not smaller than start such that k +
searchLen is not greater than len, and for all nonnegative integers j less
than searchLen, the code unit at index k+j of S is the same as the code unit
at index j of searchStr, return true; but if there is no such integer k,
return false.
...
features: [String.prototype.includes]
---*/
var str = 'The future is cool!';
assert(
str.includes('', str.length),
'str.includes("", str.length) returns true'
);
assert(
str.includes(''),
'str.includes("") returns true'
);
assert(
str.includes('', Infinity),
'str.includes("", Infinity) returns true'
);