Skip to content

Commit 5330149

Browse files
committed
[JSC] Iterate Strings without allocating a JSStringIterator
https://bugs.webkit.org/show_bug.cgi?id=325452 Reviewed by Yusuke Suzuki. 321765@main stopped allocating a JSArrayIterator in FastArray mode by keeping the state of the iteration in registers. FastString mode (for-of and destructuring over a String) still allocates a JSStringIterator every time in LLInt, Baseline and DFG, and it is unobservable for the same reason: the next method was already looked up when the iterator was opened, and while stringIteratorProtocolWatchpointSet is valid, "return" is guaranteed to be absent from the prototype chain. This patch does the same for FastString mode. iterator holds a sentinel of its own, next holds the index as an Int32, and iterable holds the string as before. op_iterator_next and op_iterator_close_check compare iterator with the sentinel for Arrays first and then with the one for Strings. When it is the latter, op_iterator_close_check looks at stringIteratorProtocolWatchpointSet, and creates the JSStringIterator if it is no longer valid. With JSC_useDFGJIT=false: Baseline Patched for-of-string-small 7.5573+-0.1228 ^ 6.6576+-0.1222 ^ definitely 1.1351x faster for-of-string-break 4.6593+-0.0620 ^ 4.2070+-0.1624 ^ definitely 1.1075x faster destructuring-string 25.8768+-0.2164 ^ 21.5336+-0.2443 ^ definitely 1.2017x faster string-iterator-for-of-ascii 149.8134+-1.0006 ^ 140.7087+-1.9843 ^ definitely 1.0647x faster With JSC_useFTLJIT=false: Baseline Patched for-of-string-small 2.2048+-0.0344 ^ 1.7072+-0.0611 ^ definitely 1.2914x faster for-of-string-break 1.7627+-0.0699 ^ 1.3706+-0.0314 ^ definitely 1.2860x faster destructuring-string 7.4364+-0.1370 ^ 5.9434+-0.1328 ^ definitely 1.2512x faster string-iterator-for-of-ascii 44.7161+-0.6120 ^ 37.0484+-0.3477 ^ definitely 1.2070x faster Tests: JSTests/microbenchmarks/destructuring-string.js JSTests/microbenchmarks/for-of-string-break.js JSTests/microbenchmarks/for-of-string-small.js JSTests/stress/for-of-fast-string-and-iterator-with-known-structure.js JSTests/stress/for-of-fast-string-and-other-iterables.js JSTests/stress/for-of-fast-string-close-fresh-realm-assignment.js JSTests/stress/for-of-fast-string-close-fresh-realm-destructuring.js JSTests/stress/for-of-fast-string-close-fresh-realm-loops.js JSTests/stress/for-of-fast-string-close-fresh-realm-replaced-methods.js JSTests/stress/for-of-fast-string-code-points.js JSTests/stress/for-of-fast-string-destructuring.js JSTests/stress/for-of-fast-string-generator.js JSTests/stress/for-of-fast-string-osr-late-close.js JSTests/stress/for-of-fast-string-osr-long-loop.js JSTests/stress/for-of-fast-string-wide-operands.js * JSTests/microbenchmarks/destructuring-string.js: Added. (test): * JSTests/microbenchmarks/for-of-string-break.js: Added. (test): * JSTests/microbenchmarks/for-of-string-small.js: Added. (test): * JSTests/stress/for-of-fast-string-and-iterator-with-known-structure.js: Added. (shouldBe): (second): (collect): (breakAtTwo): (firstTwo): * JSTests/stress/for-of-fast-string-and-other-iterables.js: Added. (shouldBe): (breakAt): (firstTwo): (Symbol.iterator): (breakAtTwo): (breakAtTwoAgain): (shouldThrowTypeError): * JSTests/stress/for-of-fast-string-close-fresh-realm-assignment.js: Added. (shouldBe): (installReturn.where.return): (installReturn): (destructureAssign): (let.o.set a): (destructureSetter): (destructureThrow): (destructureNested): (const.noHook): (run): * JSTests/stress/for-of-fast-string-close-fresh-realm-destructuring.js: Added. (shouldBe): (installReturn.StringIteratorPrototype.return): (installReturn): (firstTwo): (firstOnly): (none): (const.noHook): (run): * JSTests/stress/for-of-fast-string-close-fresh-realm-loops.js: Added. (shouldBe): (installReturn.where.return): (installReturn): (breakOut): (returnOut): (throwOut): (continueOuter): (finallyOut): (runToEnd): (nested): (each): (generatorReturn): (const.noHook): (run): * JSTests/stress/for-of-fast-string-close-fresh-realm-replaced-methods.js: Added. (shouldBe): (installReturn.StringIteratorPrototype.return): (installReturn): (StringIteratorPrototype.next): (nextReplacedMidLoop): (String.prototype.Symbol.iterator): (symbolIteratorReplacedMidLoop): (breakAtTwo): (const.noHook): * JSTests/stress/for-of-fast-string-code-points.js: Added. (shouldBe): (breakAt): (nested): (reassigned): (rope): * JSTests/stress/for-of-fast-string-destructuring.js: Added. (shouldBe): (firstAndRest): (third): (none): (swapped): (parameter): * JSTests/stress/for-of-fast-string-generator.js: Added. (shouldBe): (eachTwice): (firstTwo): (async eachAwaited): * JSTests/stress/for-of-fast-string-osr-late-close.js: Added. (shouldBe): (find): (swap): (StringIteratorPrototype.return): * JSTests/stress/for-of-fast-string-osr-long-loop.js: Added. (shouldBe): (makeLong): (count): (find): (swap): (noInline): * JSTests/stress/for-of-fast-string-wide-operands.js: Added. (shouldBe): (installReturn.StringIteratorPrototype.return): (const.noHook): * Source/JavaScriptCore/bytecode/BytecodeList.rb: * Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp: (JSC::DFG::ByteCodeParser::parseBlock): (JSC::DFG::ByteCodeParser::handleIteratorOpen): (JSC::DFG::ByteCodeParser::handleIteratorNext): * Source/JavaScriptCore/jit/JITCall.cpp: (JSC::JIT::emit_op_iterator_next): (JSC::JIT::emit_op_iterator_close_check): * Source/JavaScriptCore/jit/JITOperations.cpp: (JSC::JSC_DEFINE_JIT_OPERATION): * Source/JavaScriptCore/jit/JITOperations.h: * Source/JavaScriptCore/llint/LowLevelInterpreter64.asm: * Source/JavaScriptCore/runtime/CommonSlowPaths.cpp: (JSC::iteratorOpenTryFastImpl): (JSC::iteratorNextTryFastImpl): (JSC::iteratorNextFastStringImpl): (JSC::JSC_DEFINE_COMMON_SLOW_PATH): * Source/JavaScriptCore/runtime/CommonSlowPaths.h: * Source/JavaScriptCore/runtime/JSGlobalObject.h: (JSC::JSGlobalObject::offsetOfStringIteratorProtocolWatchpointSet): * Source/JavaScriptCore/runtime/JSStringIterator.h: * Source/JavaScriptCore/runtime/VM.cpp: (JSC::VM::VM): (JSC::VM::visitAggregateImpl): * Source/JavaScriptCore/runtime/VM.h: (JSC::VM::fastStringSentinel): (JSC::VM::fastStringValuesSentinel): Deleted. Canonical link: https://commits.webkit.org/322172@main
1 parent 24270be commit 5330149

29 files changed

Lines changed: 888 additions & 86 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const strings = ["ab", "cd", "ef", "gh"];
2+
3+
function test(string) {
4+
let [first, second] = string;
5+
return first.length + second.length;
6+
}
7+
noInline(test);
8+
9+
for (let i = 0; i < 1e6; ++i)
10+
test(strings[i & 3]);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const string = "abcdefghij";
2+
3+
function test(string, wanted) {
4+
let index = 0;
5+
for (let character of string) {
6+
if (character === wanted)
7+
break;
8+
index++;
9+
}
10+
return index;
11+
}
12+
noInline(test);
13+
14+
for (let i = 0; i < 1e5; ++i)
15+
test(string, "e");
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const string = "abcdefghij";
2+
3+
function test(string) {
4+
let count = 0;
5+
for (let character of string)
6+
count += character.length;
7+
return count;
8+
}
9+
noInline(test);
10+
11+
for (let i = 0; i < 1e5; ++i)
12+
test(string);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
function shouldBe(actual, expected, message) {
2+
if (actual !== expected)
3+
throw new Error((message ? message + ": " : "") + "expected " + expected + " but got " + actual);
4+
}
5+
6+
// The loops below see a String, which they iterate without an iterator object, and an iterable whose iterator is a generator
7+
// created by code that gets inlined, so its structure is known. The next and return methods of generators have seen generators
8+
// of more than one structure, so they convert this when they get inlined too.
9+
10+
function* first() { yield "a"; yield "b"; yield "c"; }
11+
function* second() { yield "a"; yield "b"; yield "c"; }
12+
const withGenerator = { [Symbol.iterator]: first };
13+
14+
function collect(iterable) { let seen = []; for (let x of iterable) seen.push(x); return seen.join(); }
15+
function breakAtTwo(iterable) { let seen = []; for (let x of iterable) { seen.push(x); if (seen.length === 2) break; } return seen.join(); }
16+
function firstTwo(iterable) { let [a, b] = iterable; return a + "," + b; }
17+
noInline(collect); noInline(breakAtTwo); noInline(firstTwo);
18+
19+
for (let i = 0; i < testLoopCount; i++) {
20+
shouldBe(collect(second()), "a,b,c");
21+
shouldBe(breakAtTwo(second()), "a,b");
22+
for (let iterable of ["abc", withGenerator]) {
23+
shouldBe(collect(iterable), "a,b,c");
24+
shouldBe(breakAtTwo(iterable), "a,b");
25+
shouldBe(firstTwo(iterable), "a,b");
26+
}
27+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
function shouldBe(actual, expected, message) {
2+
if (actual !== expected)
3+
throw new Error((message ? message + ": " : "") + "expected " + expected + " but got " + actual);
4+
}
5+
6+
function collect(iterable) { let seen = []; for (let x of iterable) seen.push(x); return seen.join("|"); }
7+
function breakAt(iterable, count) { let seen = []; for (let x of iterable) { seen.push(x); if (seen.length === count) break; } return seen.join("|"); }
8+
function firstTwo(iterable) { let [a, b] = iterable; return a + "|" + b; }
9+
noInline(collect); noInline(breakAt); noInline(firstTwo);
10+
11+
// One loop that sees several kinds of iterables.
12+
let iterables = [
13+
["abc", ["a", "b", "c"]],
14+
[[1, 2, 3], [1, 2, 3]],
15+
["\u{1F600}x", ["\u{1F600}", "x"]],
16+
[new Set([1, 2]), [1, 2]],
17+
[new String("xy"), ["x", "y"]],
18+
[{ *[Symbol.iterator]() { yield 1; yield 2; } }, [1, 2]],
19+
];
20+
for (let i = 0; i < testLoopCount; i++) {
21+
let [iterable, expected] = iterables[i % iterables.length];
22+
shouldBe(collect(iterable), expected.join("|"));
23+
shouldBe(breakAt(iterable, 1), expected.slice(0, 1).join("|"));
24+
shouldBe(firstTwo(iterable), expected[0] + "|" + expected[1]);
25+
}
26+
27+
// A close that has only seen Arrays meets a String, and the other way around.
28+
function breakAtTwo(iterable) { let seen = []; for (let x of iterable) { seen.push(x); if (seen.length === 2) break; } return seen.join("|"); }
29+
function breakAtTwoAgain(iterable) { let seen = []; for (let x of iterable) { seen.push(x); if (seen.length === 2) break; } return seen.join("|"); }
30+
noInline(breakAtTwo); noInline(breakAtTwoAgain);
31+
for (let i = 0; i < testLoopCount; i++) {
32+
shouldBe(breakAtTwo([1, 2, 3]), "1|2");
33+
shouldBe(breakAtTwoAgain("xyz"), "x|y");
34+
}
35+
for (let i = 0; i < 10; i++) {
36+
shouldBe(breakAtTwo("xyz"), "x|y");
37+
shouldBe(breakAtTwoAgain([1, 2, 3]), "1|2");
38+
}
39+
40+
// Not iterable after all.
41+
function shouldThrowTypeError(f) {
42+
try {
43+
f();
44+
} catch (error) {
45+
shouldBe(error instanceof TypeError, true);
46+
return;
47+
}
48+
throw new Error("did not throw");
49+
}
50+
shouldThrowTypeError(() => collect(5));
51+
shouldThrowTypeError(() => firstTwo(null));
52+
shouldBe(collect("ab"), "a|b");
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// The elements of a String are never undefined, so a default value cannot make IteratorClose observable while a pattern is
2+
// in the middle of a String. An assignment target can: "return" shows up after the pattern was opened without an iterator
3+
// object, and the close that follows has to call it on a real String Iterator at the right position.
4+
// Each case gets a realm of its own, since a realm where IteratorClose is observable never opens a pattern that way again.
5+
6+
function shouldBe(actual, expected, message) {
7+
if (actual !== expected)
8+
throw new Error((message ? message + ": " : "") + "expected " + expected + " but got " + actual);
9+
}
10+
11+
const source = `
12+
var log = [];
13+
var seenIterators = [];
14+
const StringIteratorPrototype = Object.getPrototypeOf(""[Symbol.iterator]());
15+
const IteratorPrototype = Object.getPrototypeOf(StringIteratorPrototype);
16+
const originalNext = StringIteratorPrototype.next;
17+
var holders = { StringIteratorPrototype, IteratorPrototype, ObjectPrototype: Object.prototype };
18+
function installReturn(where) {
19+
where.return = function () {
20+
let step = originalNext.call(this);
21+
log.push("return " + Object.prototype.toString.call(this) + " " + (Object.getPrototypeOf(this) === StringIteratorPrototype) + " " + JSON.stringify(step));
22+
seenIterators.push(this);
23+
return {};
24+
};
25+
}
26+
function destructureAssign(string, hook) { let o = {}; [o.a, o[(hook(), "b")]] = string; return o.a + "," + o.b; }
27+
function destructureSetter(string, hook) { let o = { set a(v) { hook(); } }; let b; [o.a, b] = string; return "" + b; }
28+
function destructureThrow(string, hook) { let o = { set a(v) { hook(); throw new Error("setter"); } }; try { [o.a] = string; } catch (e) { return e.message; } return "no throw"; }
29+
function destructureNested(string, hook) { let o = { set a(v) { hook(); } }; let b; [[o.a], b] = string; return "" + b; }
30+
`;
31+
32+
const noHook = () => { };
33+
const closedAt = (value) => 'return [object String Iterator] true {"value":' + JSON.stringify(value) + ',"done":false}';
34+
const closedWhenDone = 'return [object String Iterator] true {"done":true}';
35+
36+
function run(name, string, holderName, expected, expectedLog) {
37+
let realm = createGlobalObject();
38+
realm.eval(source);
39+
let f = realm[name];
40+
for (let i = 0; i < testLoopCount; i++)
41+
f(string, noHook);
42+
shouldBe(realm.log.length, 0, name + " quiet");
43+
44+
let message = name + " with return on " + holderName;
45+
shouldBe(f(string, () => realm.installReturn(realm.holders[holderName])), expected, message + " result");
46+
shouldBe(realm.log.join("|"), expectedLog, message + " log");
47+
// Each close saw its own object.
48+
shouldBe(new Set(realm.seenIterators).size, realm.seenIterators.length, message + " distinct iterators");
49+
}
50+
51+
run("destructureAssign", "abcd", "StringIteratorPrototype", "a,b", closedAt("c"));
52+
run("destructureSetter", "abcd", "StringIteratorPrototype", "b", closedAt("c"));
53+
run("destructureSetter", "ab", "StringIteratorPrototype", "b", closedWhenDone);
54+
run("destructureThrow", "abcd", "StringIteratorPrototype", "setter", closedAt("b"));
55+
run("destructureNested", "abcd", "StringIteratorPrototype", "b", closedWhenDone + "|" + closedAt("c"));
56+
57+
// The position counts code units, and the iterator created for the close has to continue after a whole code point.
58+
run("destructureSetter", "a\u{1F600}b", "StringIteratorPrototype", "\u{1F600}", closedAt("b"));
59+
60+
// "return" showing up on Object.prototype instead.
61+
run("destructureSetter", "abcd", "ObjectPrototype", "b", closedAt("c"));
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Once IteratorClose has been made observable in a realm, that realm never again opens a pattern over a String without an
2+
// iterator object. So each case here gets a realm of its own: "return" shows up after the function has run many times without
3+
// it, and the close that follows has to call it on a real String Iterator at the right position.
4+
5+
function shouldBe(actual, expected, message) {
6+
if (actual !== expected)
7+
throw new Error((message ? message + ": " : "") + "expected " + expected + " but got " + actual);
8+
}
9+
10+
const source = `
11+
var log = [];
12+
const StringIteratorPrototype = Object.getPrototypeOf(""[Symbol.iterator]());
13+
const originalNext = StringIteratorPrototype.next;
14+
function installReturn() {
15+
StringIteratorPrototype.return = function () {
16+
log.push("return " + Object.prototype.toString.call(this) + " " + JSON.stringify(originalNext.call(this)));
17+
return {};
18+
};
19+
}
20+
function firstTwo(string, hook) { let [a, b = hook()] = string; return a + "," + b; }
21+
function firstOnly(string, hook) { let [a = hook()] = string; return String(a); }
22+
function none(string, hook) { hook(); let [] = string; return ""; }
23+
`;
24+
25+
const noHook = () => { };
26+
const closedAt = (value) => 'return [object String Iterator] {"value":' + JSON.stringify(value) + ',"done":false}';
27+
28+
function run(name, string, installBeforeCall, expected, expectedLog) {
29+
let realm = createGlobalObject();
30+
realm.eval(source);
31+
let f = realm[name];
32+
for (let i = 0; i < testLoopCount; i++)
33+
f(string, noHook);
34+
shouldBe(realm.log.length, 0, name + " quiet");
35+
36+
if (installBeforeCall)
37+
realm.installReturn();
38+
shouldBe(f(string, realm.installReturn), expected, name + " result");
39+
shouldBe(realm.log.join("|"), expectedLog, name + " log");
40+
}
41+
42+
// The default is evaluated once the String is exhausted, and then there is nothing to close.
43+
run("firstTwo", "a", false, "a,undefined", "");
44+
run("firstOnly", "", false, "undefined", "");
45+
46+
// The default is not evaluated, but the pattern is not exhausted either, so IteratorClose runs.
47+
run("firstTwo", "abc", false, "a,b", "");
48+
run("firstTwo", "abc", true, "a,b", closedAt("c"));
49+
run("firstTwo", "a\u{1F600}b", true, "a,\u{1F600}", closedAt("b"));
50+
run("firstOnly", "abc", true, "a", closedAt("b"));
51+
run("none", "abc", false, "", closedAt("a"));
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Once IteratorClose has been made observable in a realm, that realm never again opens a loop over a String without an
2+
// iterator object. So each case here gets a realm of its own: the loop (or pattern) is opened while nothing is observable,
3+
// "return" shows up in the middle, and the close that follows has to call it on a real String Iterator at the right position.
4+
5+
function shouldBe(actual, expected, message) {
6+
if (actual !== expected)
7+
throw new Error((message ? message + ": " : "") + "expected " + expected + " but got " + actual);
8+
}
9+
10+
const source = `
11+
var log = [];
12+
var seenIterators = [];
13+
const StringIteratorPrototype = Object.getPrototypeOf(""[Symbol.iterator]());
14+
const IteratorPrototype = Object.getPrototypeOf(StringIteratorPrototype);
15+
const originalNext = StringIteratorPrototype.next;
16+
var holders = { StringIteratorPrototype, IteratorPrototype, ObjectPrototype: Object.prototype };
17+
function installReturn(where) {
18+
where.return = function () {
19+
let step = originalNext.call(this);
20+
log.push("return " + Object.prototype.toString.call(this) + " " + (Object.getPrototypeOf(this) === StringIteratorPrototype) + " " + JSON.stringify(step));
21+
seenIterators.push(this);
22+
return {};
23+
};
24+
}
25+
function breakOut(string, at, hook) { let seen = []; for (let x of string) { seen.push(x); if (x === at) { hook(); break; } } return seen.join(); }
26+
function returnOut(string, at, hook) { let seen = []; for (let x of string) { seen.push(x); if (x === at) { hook(); return seen.join(); } } return seen.join(); }
27+
function throwOut(string, at, hook) { let seen = []; try { for (let x of string) { seen.push(x); if (x === at) { hook(); throw new Error("out"); } } } catch (e) { seen.push(e.message); } return seen.join(); }
28+
function continueOuter(string, at, hook) { let seen = []; outer: for (let i = 0; i < 2; i++) { for (let x of string) { seen.push(x); if (x === at) { hook(); continue outer; } } } return seen.join(); }
29+
function finallyOut(string, at, hook) { let seen = []; try { for (let x of string) { seen.push(x); if (x === at) { hook(); return seen.join(); } } } finally { seen.push("finally"); log.push("finally " + seen.join()); } return seen.join(); }
30+
function runToEnd(string, at, hook) { let seen = []; for (let x of string) { seen.push(x); if (x === at) hook(); } return seen.join(); }
31+
function nested(string, at, hook) { let seen = []; for (let x of string) { for (let y of string) { seen.push(x + y); if (y === at) { hook(); break; } } if (x === at) break; } return seen.join(); }
32+
function* each(string, at, hook) { for (let x of string) { if (x === at) hook(); yield x; } }
33+
function generatorReturn(string, at, hook) { let seen = []; let generator = each(string, at, hook); seen.push(generator.next().value); seen.push(generator.next().value); generator.return(); return seen.join(); }
34+
`;
35+
36+
const noHook = () => { };
37+
const closedAt = (value) => 'return [object String Iterator] true {"value":' + JSON.stringify(value) + ',"done":false}';
38+
const closedWhenDone = 'return [object String Iterator] true {"done":true}';
39+
40+
function run(name, string, at, holderName, expected, expectedLog) {
41+
let realm = createGlobalObject();
42+
realm.eval(source);
43+
let f = realm[name];
44+
for (let i = 0; i < testLoopCount; i++)
45+
f(string, at, noHook);
46+
shouldBe(realm.log.filter(s => s.startsWith("return")).length, 0, name + " quiet");
47+
realm.log.length = 0;
48+
49+
let message = name + " with return on " + holderName;
50+
shouldBe(f(string, at, () => realm.installReturn(realm.holders[holderName])), expected, message + " result");
51+
shouldBe(realm.log.join("|"), expectedLog, message + " log");
52+
// Each close saw its own object.
53+
shouldBe(new Set(realm.seenIterators).size, realm.seenIterators.length, message + " distinct iterators");
54+
}
55+
56+
// Every way out of a loop, with "return" showing up on %StringIteratorPrototype%.
57+
run("breakOut", "abcd", "b", "StringIteratorPrototype", "a,b", closedAt("c"));
58+
run("returnOut", "abcd", "c", "StringIteratorPrototype", "a,b,c", closedAt("d"));
59+
run("throwOut", "abcd", "a", "StringIteratorPrototype", "a,out", closedAt("b"));
60+
run("continueOuter", "abcd", "d", "StringIteratorPrototype", "a,b,c,d,a,b,c,d", closedWhenDone + "|" + closedWhenDone);
61+
run("finallyOut", "abcd", "b", "StringIteratorPrototype", "a,b", closedAt("c") + "|finally a,b,finally");
62+
run("runToEnd", "abcd", "b", "StringIteratorPrototype", "a,b,c,d", "");
63+
run("nested", "abcd", "b", "StringIteratorPrototype", "aa,ab,ba,bb", closedAt("c") + "|" + closedAt("c") + "|" + closedAt("c"));
64+
run("generatorReturn", "abcd", "b", "StringIteratorPrototype", "a,b", closedAt("c"));
65+
66+
// The position counts code units, and the iterator created for the close has to continue after a whole code point.
67+
run("breakOut", "a\u{1F600}b\u{1F601}", "\u{1F600}", "StringIteratorPrototype", "a,\u{1F600}", closedAt("b"));
68+
run("breakOut", "a\u{1F600}\u{1F601}", "\u{1F600}", "StringIteratorPrototype", "a,\u{1F600}", closedAt("\u{1F601}"));
69+
run("breakOut", "a\ud83db", "\ud83d", "StringIteratorPrototype", "a,\ud83d", closedAt("b"));
70+
run("breakOut", "ab", "b", "StringIteratorPrototype", "a,b", closedWhenDone);
71+
72+
// "return" showing up on %IteratorPrototype% or Object.prototype instead.
73+
run("breakOut", "abcd", "b", "IteratorPrototype", "a,b", closedAt("c"));
74+
run("breakOut", "abcd", "b", "ObjectPrototype", "a,b", closedAt("c"));
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// The methods of the iteration protocol get replaced in the middle of a loop over a String that runs without an iterator
2+
// object. Each case gets a realm of its own, since the realm stops opening loops that way afterwards.
3+
4+
function shouldBe(actual, expected, message) {
5+
if (actual !== expected)
6+
throw new Error((message ? message + ": " : "") + "expected " + expected + " but got " + actual);
7+
}
8+
9+
const source = `
10+
var log = [];
11+
const StringIteratorPrototype = Object.getPrototypeOf(""[Symbol.iterator]());
12+
const originalNext = StringIteratorPrototype.next;
13+
function installReturn() {
14+
StringIteratorPrototype.return = function () {
15+
log.push("return " + Object.prototype.toString.call(this) + " " + JSON.stringify(originalNext.call(this)));
16+
return {};
17+
};
18+
}
19+
var nextCalls = 0;
20+
function nextReplacedMidLoop(string, replace) {
21+
let seen = [];
22+
for (let x of string) {
23+
seen.push(x);
24+
if (replace && x === "b")
25+
StringIteratorPrototype.next = function () { nextCalls++; return originalNext.call(this); };
26+
}
27+
return seen.join();
28+
}
29+
function symbolIteratorReplacedMidLoop(string, replace) {
30+
let seen = [];
31+
for (let x of string) {
32+
seen.push(x);
33+
if (replace && x === "b")
34+
String.prototype[Symbol.iterator] = function* () { yield "replaced"; };
35+
}
36+
return seen.join();
37+
}
38+
function breakAtTwo(iterable, hook) { let seen = []; for (let x of iterable) { seen.push(x); if (seen.length === 2) { hook(); break; } } return seen.join(); }
39+
`;
40+
41+
const noHook = () => { };
42+
43+
// The spec fixes the next method when the loop is opened: replacing it in the middle must not change what the loop sees.
44+
{
45+
let realm = createGlobalObject();
46+
realm.eval(source);
47+
for (let i = 0; i < testLoopCount; i++)
48+
shouldBe(realm.nextReplacedMidLoop("abcd", false), "a,b,c,d");
49+
shouldBe(realm.nextReplacedMidLoop("abcd", true), "a,b,c,d");
50+
shouldBe(realm.nextCalls, 0);
51+
shouldBe(realm.nextReplacedMidLoop("abcd", false), "a,b,c,d");
52+
shouldBe(realm.nextCalls, 5);
53+
}
54+
{
55+
let realm = createGlobalObject();
56+
realm.eval(source);
57+
for (let i = 0; i < testLoopCount; i++)
58+
shouldBe(realm.symbolIteratorReplacedMidLoop("abcd", false), "a,b,c,d");
59+
shouldBe(realm.symbolIteratorReplacedMidLoop("abcd", true), "a,b,c,d");
60+
shouldBe(realm.symbolIteratorReplacedMidLoop("abcd", false), "replaced");
61+
}
62+
63+
// Making IteratorClose observable for Strings leaves loops over Arrays alone.
64+
{
65+
let realm = createGlobalObject();
66+
realm.eval(source);
67+
for (let i = 0; i < testLoopCount; i++) {
68+
shouldBe(realm.breakAtTwo("abc", noHook), "a,b");
69+
shouldBe(realm.breakAtTwo([1, 2, 3], noHook), "1,2");
70+
}
71+
shouldBe(realm.breakAtTwo("abc", realm.installReturn), "a,b");
72+
shouldBe(realm.log.join("|"), 'return [object String Iterator] {"value":"c","done":false}');
73+
for (let i = 0; i < testLoopCount; i++) {
74+
shouldBe(realm.breakAtTwo([1, 2, 3], noHook), "1,2");
75+
shouldBe(realm.breakAtTwo("abc", noHook), "a,b");
76+
}
77+
shouldBe(realm.log.length, 1 + testLoopCount);
78+
}

0 commit comments

Comments
 (0)