Skip to content

Commit 8364a66

Browse files
committed
Add a test for Iterator.concat not closing the underlying iterator
The only IteratorClose in the closure of Iterator.concat is the one performed for an abrupt completion of the Yield. An abrupt completion from IteratorStepValue is propagated as is, so a throwing next method, a throwing done or value getter, and a next method returning a non-object all leave the iterator alone. Note that V8 fails this test: it closes the iterator in all four cases, unlike its own iterator helpers, which do not. JavaScriptCore does not close it.
1 parent 38c1a42 commit 8364a66

1 file changed

Lines changed: 112 additions & 0 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright (C) 2026 Saúl Ibarra Corretgé. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-iterator.concat
6+
description: >
7+
Underlying iterator is not closed when stepping it throws
8+
info: |
9+
Iterator.concat ( ...items )
10+
11+
...
12+
3. Let closure be a new Abstract Closure with no parameters that captures iterables and performs the following steps when called:
13+
a. For each Record iterable of iterables, do
14+
...
15+
v. Repeat, while innerAlive is true,
16+
1. Let innerValue be ? IteratorStepValue(iteratorRecord).
17+
...
18+
19+
The only IteratorClose in the closure is the one performed for an abrupt
20+
completion of the Yield; an abrupt completion from IteratorStepValue is
21+
propagated as is, which only marks the iterator record done:
22+
23+
IteratorStepValue ( iteratorRecord )
24+
25+
1. Let result be Completion(IteratorNext(iteratorRecord)).
26+
2. If result is a throw completion, then
27+
a. Set iteratorRecord.[[Done]] to true.
28+
b. Return ? result.
29+
...
30+
4. Let done be Completion(IteratorComplete(result)).
31+
5. If done is a throw completion, then
32+
a. Set iteratorRecord.[[Done]] to true.
33+
b. Return ? done.
34+
...
35+
8. Let value be Completion(Get(result, "value")).
36+
9. If value is a throw completion, then
37+
a. Set iteratorRecord.[[Done]] to true.
38+
10. Return ? value.
39+
features: [iterator-sequencing]
40+
---*/
41+
42+
let returnCalls = 0;
43+
44+
function concatWithStep(next) {
45+
let iterable = {
46+
[Symbol.iterator]() {
47+
return {
48+
next,
49+
return() {
50+
++returnCalls;
51+
return {};
52+
},
53+
};
54+
}
55+
};
56+
57+
return Iterator.concat(iterable);
58+
}
59+
60+
// Underlying iterator has a throwing next method.
61+
let iterator = concatWithStep(function() {
62+
throw new Test262Error();
63+
});
64+
65+
assert.throws(Test262Error, function() {
66+
iterator.next();
67+
});
68+
69+
assert.sameValue(returnCalls, 0, 'return is not called on the underlying iterator');
70+
71+
// Underlying iterator next returns an object with a throwing done getter.
72+
iterator = concatWithStep(function() {
73+
return {
74+
get done() {
75+
throw new Test262Error();
76+
},
77+
value: 1,
78+
};
79+
});
80+
81+
assert.throws(Test262Error, function() {
82+
iterator.next();
83+
});
84+
85+
assert.sameValue(returnCalls, 0, 'return is not called on the underlying iterator');
86+
87+
// Underlying iterator next returns an object with a throwing value getter.
88+
iterator = concatWithStep(function() {
89+
return {
90+
done: false,
91+
get value() {
92+
throw new Test262Error();
93+
},
94+
};
95+
});
96+
97+
assert.throws(Test262Error, function() {
98+
iterator.next();
99+
});
100+
101+
assert.sameValue(returnCalls, 0, 'return is not called on the underlying iterator');
102+
103+
// Underlying iterator next returns a non-object.
104+
iterator = concatWithStep(function() {
105+
return null;
106+
});
107+
108+
assert.throws(TypeError, function() {
109+
iterator.next();
110+
});
111+
112+
assert.sameValue(returnCalls, 0, 'return is not called on the underlying iterator');

0 commit comments

Comments
 (0)