diff --git a/features.txt b/features.txt index f48d5412c32..81dc186f969 100644 --- a/features.txt +++ b/features.txt @@ -80,6 +80,10 @@ canonical-tz # https://github.com/tc39/proposal-upsert upsert +# Joint Iteration +# https://github.com/tc39/proposal-joint-iteration +joint-iteration + # Immutable Array Buffer # https://github.com/tc39/proposal-immutable-arraybuffer immutable-arraybuffer diff --git a/harness/iteratorZipUtils.js b/harness/iteratorZipUtils.js new file mode 100644 index 00000000000..d27a90cc2e7 --- /dev/null +++ b/harness/iteratorZipUtils.js @@ -0,0 +1,219 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + Utility functions for testing Iterator.prototype.zip and Iterator.prototype.zipKeyed. Requires inclusion of propertyHelper.js. +defines: + - forEachSequenceCombination + - forEachSequenceCombinationKeyed + - assertZipped + - assertZippedKeyed + - assertIteratorResult + - assertIsPackedArray +---*/ + +// Assert |result| is an object created by CreateIteratorResultObject. +function assertIteratorResult(result, value, done, label) { + assert.sameValue( + Object.getPrototypeOf(result), + Object.prototype, + label + ": [[Prototype]] of iterator result is Object.prototype" + ); + + assert(Object.isExtensible(result), label + ": iterator result is extensible"); + + var ownKeys = Reflect.ownKeys(result); + assert.compareArray(ownKeys, ["value", "done"], label + ": iterator result properties"); + + verifyProperty(result, "value", { + value: value, + writable: true, + enumerable: true, + configurable: true, + }); + + verifyProperty(result, "done", { + value: done, + writable: true, + enumerable: true, + configurable: true, + }); +} + +// Assert |array| is a packed array with default property attributes. +function assertIsPackedArray(array, label) { + assert(Array.isArray(array), label + ": array is an array exotic object"); + + assert.sameValue( + Object.getPrototypeOf(array), + Array.prototype, + label + ": [[Prototype]] of array is Array.prototype" + ); + + assert(Object.isExtensible(array), label + ": array is extensible"); + + // Ensure "length" property has its default property attributes. + verifyProperty(array, "length", { + writable: true, + enumerable: false, + configurable: false, + }); + + // Ensure no holes and all elements have the default property attributes. + for (var i = 0; i < array.length; i++) { + verifyProperty(array, i, { + writable: true, + enumerable: true, + configurable: true, + }); + } +} + +// Assert |array| is an extensible null-prototype object with default property attributes. +function _assertIsNullProtoMutableObject(object, label) { + assert.sameValue( + Object.getPrototypeOf(object), + null, + label + ": [[Prototype]] of object is null" + ); + + assert(Object.isExtensible(object), label + ": object is extensible"); + + // Ensure all properties have the default property attributes. + var keys = Object.getOwnPropertyNames(object); + for (var i = 0; i < keys.length; i++) { + verifyProperty(object, keys[i], { + writable: true, + enumerable: true, + configurable: true, + }); + } +} + +// Assert that the `zipped` iterator yields the first `count` outputs of Iterator.zip. +// Assumes `inputs` is an array of arrays, each with length >= `count`. +// Advances `zipped` by `count` steps. +function assertZipped(zipped, inputs, count, label) { + // Last returned elements array. + var last = null; + + for (var i = 0; i < count; i++) { + var itemLabel = label + ", step " + i; + + var result = zipped.next(); + var value = result.value; + + // Test IteratorResult structure. + assertIteratorResult(result, value, false, itemLabel); + + // Ensure value is a new array. + assert.notSameValue(value, last, itemLabel + ": returns a new array"); + last = value; + + // Ensure all array elements have the expected value. + var expected = inputs.map(function (array) { + return array[i]; + }); + assert.compareArray(value, expected, itemLabel + ": values"); + + // Ensure value is a packed array with default data properties. + assertIsPackedArray(value, itemLabel); + } +} + +// Assert that the `zipped` iterator yields the first `count` outputs of Iterator.zipKeyed. +// Assumes `inputs` is an object whose values are arrays, each with length >= `count`. +// Advances `zipped` by `count` steps. +function assertZippedKeyed(zipped, inputs, count, label) { + // Last returned elements array. + var last = null; + + var expectedKeys = Object.keys(inputs); + + for (var i = 0; i < count; i++) { + var itemLabel = label + ", step " + i; + + var result = zipped.next(); + var value = result.value; + + // Test IteratorResult structure. + assertIteratorResult(result, value, false, itemLabel); + + // Ensure resulting object is a new object. + assert.notSameValue(value, last, itemLabel + ": returns a new object"); + last = value; + + // Ensure resulting object has the expected keys and values. + assert.compareArray(Reflect.ownKeys(value), expectedKeys, itemLabel + ": result object keys"); + + var expectedValues = Object.values(inputs).map(function (array) { + return array[i]; + }); + assert.compareArray(Object.values(value), expectedValues, itemLabel + ": result object values"); + + // Ensure resulting object is a null-prototype mutable object with default data properties. + _assertIsNullProtoMutableObject(value, itemLabel); + } +} + +function forEachSequenceCombinationKeyed(callback) { + return forEachSequenceCombination(function(inputs, inputsLabel, min, max) { + var object = {}; + for (var i = 0; i < inputs.length; ++i) { + object["prop_" + i] = inputs[i]; + } + inputsLabel = "inputs = " + JSON.stringify(object); + callback(object, inputsLabel, min, max); + }); +} + +function forEachSequenceCombination(callback) { + function test(inputs) { + if (inputs.length === 0) { + callback(inputs, "inputs = []", 0, 0); + return; + } + + var lengths = inputs.map(function(array) { + return array.length; + }); + + var min = Math.min.apply(null, lengths); + var max = Math.max.apply(null, lengths); + + var inputsLabel = "inputs = " + JSON.stringify(inputs); + + callback(inputs, inputsLabel, min, max); + } + + // Yield all prefixes of the string |s|. + function* prefixes(s) { + for (var i = 0; i <= s.length; ++i) { + yield s.slice(0, i); + } + } + + // Zip an empty iterable. + test([]); + + // Zip a single iterator. + for (var prefix of prefixes("abcd")) { + test([prefix.split("")]); + } + + // Zip two iterators. + for (var prefix1 of prefixes("abcd")) { + for (var prefix2 of prefixes("efgh")) { + test([prefix1.split(""), prefix2.split("")]); + } + } + + // Zip three iterators. + for (var prefix1 of prefixes("abcd")) { + for (var prefix2 of prefixes("efgh")) { + for (var prefix3 of prefixes("ijkl")) { + test([prefix1.split(""), prefix2.split(""), prefix3.split("")]); + } + } + } +} diff --git a/test/built-ins/Iterator/zip/basic-longest.js b/test/built-ins/Iterator/zip/basic-longest.js new file mode 100644 index 00000000000..c74895e8e28 --- /dev/null +++ b/test/built-ins/Iterator/zip/basic-longest.js @@ -0,0 +1,85 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Basic Iterator.zip test with "longest" mode. +includes: [compareArray.js, propertyHelper.js, iteratorZipUtils.js] +features: [joint-iteration] +---*/ + +function testSequence(inputs, inputsLabel, minLength, maxLength) { + function test(options, optionsLabel, getPaddingForInput) { + var label = optionsLabel + ", " + inputsLabel; + var it = Iterator.zip(inputs, options); + assertZipped(it, inputs, minLength, label); + + for (var i = minLength; i < maxLength; i++) { + var itemLabel = label + ", step " + i; + + var result = it.next(); + var value = result.value; + + // Test IteratorResult structure. + assertIteratorResult(result, value, false, itemLabel); + + var expected = inputs.map(function (input, j) { + return i < input.length ? input[i] : getPaddingForInput(j); + }); + assert.compareArray(value, expected, itemLabel + ": values"); + } + assertIteratorResult(it.next(), undefined, true, label + ": after completion"); + } + + test( + { mode: "longest" }, + "options = { mode: 'longest' }", + function () { + return undefined; + }, + ); + + test( + { mode: "longest", padding: [] }, + "options = { mode: 'longest', padding: [] }", + function () { + return undefined; + }, + ); + + test( + { mode: "longest", padding: ["pad"] }, + "options = { mode: 'longest', padding: ['pad'] }", + function (idx) { + return idx === 0 ? "pad" : undefined; + }, + ); + + test( + { mode: "longest", padding: Array(inputs.length).fill("pad") }, + "options = { mode: 'longest', padding: ['pad', 'pad', ..., 'pad'] }", + function (idx) { + return "pad"; + }, + ); + + // Yield an infinite amount of numbers. + var numbers = { + *[Symbol.iterator]() { + var i = 0; + while (true) { + yield 100 + i++; + } + } + }; + test( + { mode: "longest", padding: numbers }, + "options = { mode: 'longest', padding: numbers }", + function (idx) { + return 100 + idx; + }, + ); +} + +forEachSequenceCombination(testSequence); diff --git a/test/built-ins/Iterator/zip/basic-shortest.js b/test/built-ins/Iterator/zip/basic-shortest.js new file mode 100644 index 00000000000..8487c0fe7e9 --- /dev/null +++ b/test/built-ins/Iterator/zip/basic-shortest.js @@ -0,0 +1,29 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Basic Iterator.zip test with "shortest" mode. +includes: [compareArray.js, propertyHelper.js, iteratorZipUtils.js] +features: [joint-iteration] +---*/ + +function testSequence(inputs, inputsLabel, minLength, maxLength) { + function test(options, optionsLabel) { + var label = optionsLabel + ", " + inputsLabel; + var it = Iterator.zip(inputs, options); + assertZipped(it, inputs, minLength, label); + + // Iterator is closed after `minLength` items. + assertIteratorResult(it.next(), undefined, true, label + ": after completion"); + } + + test(undefined, "options = undefined"); + + test({}, "options = {}"); + + test({ mode: "shortest" }, "options = { mode: 'shortest' }"); +} + +forEachSequenceCombination(testSequence); diff --git a/test/built-ins/Iterator/zip/basic-strict.js b/test/built-ins/Iterator/zip/basic-strict.js new file mode 100644 index 00000000000..d41ec7e56bf --- /dev/null +++ b/test/built-ins/Iterator/zip/basic-strict.js @@ -0,0 +1,30 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Basic Iterator.zip test with "strict" mode. +includes: [compareArray.js, propertyHelper.js, iteratorZipUtils.js] +features: [joint-iteration] +---*/ + +function testSequence(inputs, inputsLabel, minLength, maxLength) { + function test(options, optionsLabel) { + var label = optionsLabel + ", " + inputsLabel; + var it = Iterator.zip(inputs, options); + assertZipped(it, inputs, minLength, label); + + if (minLength === maxLength) { + assertIteratorResult(it.next(), undefined, true, label + ": after completion"); + } else { + assert.throws(TypeError, function() { + it.next(); + }, label + " should throw after " + minLength + " items."); + } + } + + test({ mode: "strict" }, "options = { mode: 'strict' }"); +} + +forEachSequenceCombination(testSequence); diff --git a/test/built-ins/Iterator/zip/is-function.js b/test/built-ins/Iterator/zip/is-function.js new file mode 100644 index 00000000000..fe2167e94c4 --- /dev/null +++ b/test/built-ins/Iterator/zip/is-function.js @@ -0,0 +1,15 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Iterator.zip is a built-in function +features: [joint-iteration] +---*/ + +assert.sameValue( + typeof Iterator.zip, + "function", + "The value of `typeof Iterator.zip` is 'function'" +); diff --git a/test/built-ins/Iterator/zip/iterables-containing-string-objects.js b/test/built-ins/Iterator/zip/iterables-containing-string-objects.js new file mode 100644 index 00000000000..12a402e508f --- /dev/null +++ b/test/built-ins/Iterator/zip/iterables-containing-string-objects.js @@ -0,0 +1,17 @@ +// Copyright (C) 2025 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Accepts String objects as inputs. +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var result = Array.from(Iterator.zip([Object("abc"), Object("123")])); + +assert.sameValue(result.length, 3); +assert.compareArray(result[0], ["a", "1"]); +assert.compareArray(result[1], ["b", "2"]); +assert.compareArray(result[2], ["c", "3"]); diff --git a/test/built-ins/Iterator/zip/iterables-iteration-after-reading-options.js b/test/built-ins/Iterator/zip/iterables-iteration-after-reading-options.js new file mode 100644 index 00000000000..539b66267fe --- /dev/null +++ b/test/built-ins/Iterator/zip/iterables-iteration-after-reading-options.js @@ -0,0 +1,73 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Perform iteration of the "iterables" argument after reading all properties. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 3. Let mode be ? Get(options, "mode"). + ... + 7. If mode is "longest", then + a. Set paddingOption to ? Get(options, "padding"). + ... + 10. Let inputIter be ? GetIterator(iterables, sync). + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var log = []; + +var iterables = { + [Symbol.iterator]() { + log.push("get iterator"); + return this; + }, + next() { + return {done: true}; + } +}; + +var options = { + get mode() { + log.push("get mode"); + return "longest"; + }, + get padding() { + log.push("get padding"); + return []; + } +}; + +Iterator.zip(iterables, options); + +assert.compareArray(log, [ + "get mode", + "get padding", + "get iterator", +]); + +for (var mode of [undefined, "shortest", "strict"]) { + log.length = 0; + + options = { + get mode() { + log.push("get mode"); + return mode; + }, + get padding() { + log.push("unexpected get padding"); + return []; + } + }; + + Iterator.zip(iterables, options); + + assert.compareArray(log, [ + "get mode", + "get iterator", + ]); +} diff --git a/test/built-ins/Iterator/zip/iterables-iteration-get-iterator-flattenable-abrupt-completion.js b/test/built-ins/Iterator/zip/iterables-iteration-get-iterator-flattenable-abrupt-completion.js new file mode 100644 index 00000000000..948006fbe2b --- /dev/null +++ b/test/built-ins/Iterator/zip/iterables-iteration-get-iterator-flattenable-abrupt-completion.js @@ -0,0 +1,165 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Handle abrupt completions during iterables iteration. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 12. Repeat, while next is not done, + ... + c. If next is not done, then + i. Let iter be Completion(GetIteratorFlattenable(next, reject-strings)). + ii. IfAbruptCloseIterators(iter, the list-concatenation of « inputIter » and iters). + ... + + GetIteratorFlattenable ( obj, primitiveHandling ) + 1. If obj is not an Object, then + a. If primitiveHandling is reject-primitives, throw a TypeError exception. + b. Assert: primitiveHandling is iterate-string-primitives. + c. If obj is not a String, throw a TypeError exception. + 2. Let method be ? GetMethod(obj, %Symbol.iterator%). + 3. If method is undefined, then + a. Let iterator be obj. + 4. Else, + a. Let iterator be ? Call(method, obj). + 5. If iterator is not an Object, throw a TypeError exception. + 6. Return ? GetIteratorDirect(iterator). + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +class ExpectedError extends Error {} + +var badIterators = [ + // Throw TypeError in GetIteratorFlattenable because strings are rejected. + { + iterator: "bad iterator", + error: TypeError + }, + + // Throw an error when GetIteratorFlattenable performs GetMethod. + { + iterator: { + get [Symbol.iterator]() { + throw new ExpectedError(); + } + }, + error: ExpectedError, + }, + + // Throw an error when GetIteratorFlattenable performs Call. + { + iterator: { + [Symbol.iterator]() { + throw new ExpectedError(); + } + }, + error: ExpectedError, + }, + + // Throw an error when GetIteratorFlattenable performs GetIteratorDirect. + { + iterator: { + get next() { + throw new ExpectedError(); + } + }, + error: ExpectedError, + }, +]; + +function makeIterables(badIterator) { + var log = []; + + var first = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("close first iterator"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + }, + }; + + var second = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, second); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("close second iterator"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + }, + }; + + var elements = [first, second, badIterator]; + var elementsIter = elements.values(); + + var iterables = { + [Symbol.iterator]() { + return this; + }, + next() { + log.push("call next"); + return elementsIter.next(); + }, + return() { + log.push("close iterables iterator"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + }, + }; + + return {log, iterables}; +} + +for (var {iterator, error} of badIterators) { + var {log, iterables} = makeIterables(iterator); + + assert.throws(error, function() { + Iterator.zip(iterables); + }); + + // Ensure iterators are closed in the correct order. + assert.compareArray(log, [ + "call next", + "call next", + "call next", + "close second iterator", + "close first iterator", + "close iterables iterator", + ]); +} diff --git a/test/built-ins/Iterator/zip/iterables-iteration-iterator-step-value-abrupt-completion.js b/test/built-ins/Iterator/zip/iterables-iteration-iterator-step-value-abrupt-completion.js new file mode 100644 index 00000000000..a9c15a21bff --- /dev/null +++ b/test/built-ins/Iterator/zip/iterables-iteration-iterator-step-value-abrupt-completion.js @@ -0,0 +1,113 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Handle abrupt completions during iterables iteration. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 12. Repeat, while next is not done, + a. Set next to Completion(IteratorStepValue(inputIter)). + b. IfAbruptCloseIterators(next, iters). + ... + + IfAbruptCloseIterators ( value, iteratorRecords ) + 1. Assert: value is a Completion Record. + 2. If value is an abrupt completion, return ? IteratorCloseAll(iteratorRecords, value). + 3. Else, set value to value.[[Value]]. + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +class ExpectedError extends Error {} + +var log = []; + +var first = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("close first iterator"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + }, +}; + +var second = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, second); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("close second iterator"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + }, +}; + +var elements = [first, second]; +var elementsIter = elements.values(); + +var iterables = { + [Symbol.iterator]() { + return this; + }, + next() { + log.push("call next"); + var result = elementsIter.next(); + if (result.done) { + throw new ExpectedError(); + } + return result; + }, + return() { + // This method shouldn't be called. + log.push("UNEXPECTED - close iterables iterator"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + }, +}; + +assert.throws(ExpectedError, function() { + Iterator.zip(iterables); +}); + +// Ensure iterators are closed in the correct order. +assert.compareArray(log, [ + "call next", + "call next", + "call next", + "close second iterator", + "close first iterator", +]); diff --git a/test/built-ins/Iterator/zip/iterables-iteration.js b/test/built-ins/Iterator/zip/iterables-iteration.js new file mode 100644 index 00000000000..e1f97f0806e --- /dev/null +++ b/test/built-ins/Iterator/zip/iterables-iteration.js @@ -0,0 +1,164 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Perform iteration of the "iterables" argument. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 10. Let inputIter be ? GetIterator(iterables, sync). + 11. Let next be not-started. + 12. Repeat, while next is not done, + a. Set next to Completion(IteratorStepValue(inputIter)). + b. IfAbruptCloseIterators(next, iters). + c. If next is not done, then + i. Let iter be Completion(GetIteratorFlattenable(next, reject-strings)). + ii. IfAbruptCloseIterators(iter, the list-concatenation of « inputIter » and iters). + iii. Append iter to iters. + ... + + GetIterator ( obj, kind ) + ... + 2. Else, + a. Let method be ? GetMethod(obj, %Symbol.iterator%). + 3. If method is undefined, throw a TypeError exception. + 4. Return ? GetIteratorFromMethod(obj, method). + + GetIteratorFromMethod ( obj, method ) + 1. Let iterator be ? Call(method, obj). + 2. If iterator is not an Object, throw a TypeError exception. + 3. Return ? GetIteratorDirect(iterator). + + GetIteratorDirect ( obj ) + 1. Let nextMethod be ? Get(obj, "next"). + 2. Let iteratorRecord be the Iterator Record { [[Iterator]]: obj, [[NextMethod]]: nextMethod, [[Done]]: false }. + 3. Return iteratorRecord. + + GetIteratorFlattenable ( obj, primitiveHandling ) + 1. If obj is not an Object, then + a. If primitiveHandling is reject-primitives, throw a TypeError exception. + b. Assert: primitiveHandling is iterate-string-primitives. + c. If obj is not a String, throw a TypeError exception. + 2. Let method be ? GetMethod(obj, %Symbol.iterator%). + 3. If method is undefined, then + a. Let iterator be obj. + 4. Else, + a. Let iterator be ? Call(method, obj). + 5. If iterator is not an Object, throw a TypeError exception. + 6. Return ? GetIteratorDirect(iterator). +includes: [proxyTrapsHelper.js, compareArray.js] +features: [joint-iteration] +---*/ + +// Object implementing Iterator protocol, but throws when calling any Iterator methods. +var throwingIterator = { + next() { + throw new Test262Error(); + }, + return() { + throw new Test262Error(); + } +}; + +var iterableReturningThrowingIterator = { + [Symbol.iterator]() { + return throwingIterator; + } +}; + +// "iterables" argument must be an iterable. +assert.throws(TypeError, function() { + Iterator.zip(Object.create(null)); +}); + +// GetIteratorFlattenable accepts both iterables and iterators. +Iterator.zip([ + throwingIterator, + iterableReturningThrowingIterator, +]); + +// GetIteratorFlattenable rejects non-objects. +var badIterators = [ + undefined, + null, + true, + "", + Symbol(), + 0, + 0n, +]; + +for (var iterator of badIterators) { + assert.throws(TypeError, function() { + Iterator.zip([iterator]); + }); +} + +// GetIterator and GetIteratorFlattenable read properties in the correct order. +var log = []; + +function makeProxyWithGetHandler(name, obj) { + return new Proxy(obj, allowProxyTraps({ + get(target, propertyKey, receiver) { + log.push(`${name}::${String(propertyKey)}`); + return Reflect.get(target, propertyKey, receiver); + } + })); +} + +var elements = [ + // An iterator. + makeProxyWithGetHandler("first", throwingIterator), + + // An iterable. + makeProxyWithGetHandler("second", iterableReturningThrowingIterator), + + // An object without any iteration methods. + makeProxyWithGetHandler("third", Object.create(null)), +]; + +var elementsIter = elements.values(); + +var iterables = makeProxyWithGetHandler("iterables", { + [Symbol.iterator]() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, iterables); + assert.sameValue(arguments.length, 0); + + return this; + }, + next() { + log.push("call next"); + + // Called with the correct receiver and no arguments. + assert.sameValue(this, iterables); + assert.sameValue(arguments.length, 0); + + return elementsIter.next(); + }, + return() { + throw new Test262Error("unexpected call to return method"); + } +}); + +Iterator.zip(iterables); + +assert.compareArray(log, [ + "iterables::Symbol(Symbol.iterator)", + "iterables::next", + + "call next", + "first::Symbol(Symbol.iterator)", + "first::next", + + "call next", + "second::Symbol(Symbol.iterator)", + + "call next", + "third::Symbol(Symbol.iterator)", + "third::next", + + "call next", +]); diff --git a/test/built-ins/Iterator/zip/iterables-primitive.js b/test/built-ins/Iterator/zip/iterables-primitive.js new file mode 100644 index 00000000000..62cf113cc24 --- /dev/null +++ b/test/built-ins/Iterator/zip/iterables-primitive.js @@ -0,0 +1,50 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Throws a TypeError when the "iterables" argument is not an object. +info: | + Iterator.zip ( iterables [ , options ] ) + 1. If iterables is not an Object, throw a TypeError exception. + ... +features: [joint-iteration] +---*/ + +var invalidIterables = [ + undefined, + null, + true, + "", + Symbol(), + 0, + 0n, +]; + +// Throws when the "iterables" argument is absent. +assert.throws(TypeError, function() { + Iterator.zip(); +}); + +// Throws a TypeError for invalid iterables values. +for (var iterables of invalidIterables) { + assert.throws(TypeError, function() { + Iterator.zip(iterables); + }); +} + +// Options argument not read when iterables is not an object. +var badOptions = { + get mode() { + throw new Test262Error(); + }, + get padding() { + throw new Test262Error(); + } +}; +for (var iterables of invalidIterables) { + assert.throws(TypeError, function() { + Iterator.zip(iterables, badOptions); + }); +} diff --git a/test/built-ins/Iterator/zip/iterator-non-iterable.js b/test/built-ins/Iterator/zip/iterator-non-iterable.js new file mode 100644 index 00000000000..51277a481c4 --- /dev/null +++ b/test/built-ins/Iterator/zip/iterator-non-iterable.js @@ -0,0 +1,24 @@ +// Copyright (C) 2025 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Throws a TypeError when the "iterables" argument is not iterable. +features: [joint-iteration] +---*/ + +var invalidIterables = [ + Object.create(null), + Object.create(null, { + next: { value: function(){} }, + return: { value: function(){} }, + }), +]; + +// Throws a TypeError for invalid iterables values. +for (var iterables of invalidIterables) { + assert.throws(TypeError, function() { + Iterator.zip(iterables); + }); +} diff --git a/test/built-ins/Iterator/zip/iterator-zip-iteration-iterator-close-abrupt-completion.js b/test/built-ins/Iterator/zip/iterator-zip-iteration-iterator-close-abrupt-completion.js new file mode 100644 index 00000000000..5e832ae5be0 --- /dev/null +++ b/test/built-ins/Iterator/zip/iterator-zip-iteration-iterator-close-abrupt-completion.js @@ -0,0 +1,116 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Handle abrupt completion from IteratorStep in IteratorZip. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + v. Let completion be Completion(Yield(results)). + vi. If completion is an abrupt completion, then + 1. Return ? IteratorCloseAll(openIters, completion). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +function ExpectedError() {} + +var log = []; + +var first = { + next() { + log.push("call first next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call first return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var second = { + next() { + log.push("call second next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, second); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call second return"); + + throw new ExpectedError(); + } +}; + +var third = { + next() { + log.push("call third next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call third return"); + + return {}; + } +}; + +var it = Iterator.zip([first, second, third]); + +it.next(); + +assert.throws(ExpectedError, function() { + it.return(); +}); + +assert.compareArray(log, [ + "call first next", + "call second next", + "call third next", + + "call third return", + "call second return", + "call first return", +]); diff --git a/test/built-ins/Iterator/zip/iterator-zip-iteration-iterator-step-value-abrupt-completion.js b/test/built-ins/Iterator/zip/iterator-zip-iteration-iterator-step-value-abrupt-completion.js new file mode 100644 index 00000000000..b1972c4e917 --- /dev/null +++ b/test/built-ins/Iterator/zip/iterator-zip-iteration-iterator-step-value-abrupt-completion.js @@ -0,0 +1,145 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Handle abrupt completion from IteratorStepValue in IteratorZip. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + iii. For each integer i such that 0 ≤ i < iterCount, in ascending order, do + ... + 3. Else, + a. Let result be Completion(IteratorStepValue(iter)). + b. If result is an abrupt completion, then + i. Remove iter from openIters. + ii. Return ? IteratorCloseAll(openIters, result). + ... + d. If result is done, then + i. Remove iter from openIters. + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var modes = [ + "shortest", + "longest", + "strict", +]; + +function ExpectedError() {} + +var log = []; + +var first = { + next() { + log.push("call first next"); + throw new ExpectedError(); + }, + return() { + log.push("unexpected call first return"); + } +}; + +var second = { + next() { + log.push("unexpected call second next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, second); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call second return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var third = { + next() { + log.push("unexpected call third next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call third return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +// Empty iterator to ensure |return| is not called for closed iterators. +var empty = { + next() { + log.push("call empty next"); + return {done: true}; + }, + return() { + log.push("unexpected call empty return"); + } +}; + +for (var mode of modes) { + var it = Iterator.zip([first, second, third], {mode}); + + assert.throws(ExpectedError, function() { + it.next(); + }); + + assert.compareArray(log, [ + "call first next", + "call third return", + "call second return", + ]); + + // Clear log. + log.length = 0; +} + +// This case applies only when mode is "longest". +var it = Iterator.zip([empty, first, second, third], {mode: "longest"}); + +assert.throws(ExpectedError, function() { + it.next(); +}); + +assert.compareArray(log, [ + "call empty next", + "call first next", + "call third return", + "call second return", +]); diff --git a/test/built-ins/Iterator/zip/iterator-zip-iteration-longest-iterator-close-abrupt-completion.js b/test/built-ins/Iterator/zip/iterator-zip-iteration-longest-iterator-close-abrupt-completion.js new file mode 100644 index 00000000000..14e9446ea09 --- /dev/null +++ b/test/built-ins/Iterator/zip/iterator-zip-iteration-longest-iterator-close-abrupt-completion.js @@ -0,0 +1,127 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Handle abrupt completion from IteratorStep in IteratorZip. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + v. Let completion be Completion(Yield(results)). + vi. If completion is an abrupt completion, then + 1. Return ? IteratorCloseAll(openIters, completion). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +function ExpectedError() {} + +var log = []; + +var first = { + next() { + log.push("call first next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call first return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var second = { + next() { + log.push("call second next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, second); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call second return"); + + throw new ExpectedError(); + } +}; + +var third = { + next() { + log.push("call third next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call third return"); + + return {}; + } +}; + +var fourth = { + next() { + log.push("call fourth next"); + return {done: true}; + }, + return() { + log.push("unexpected call fourth return"); + } +}; + +var it = Iterator.zip([first, second, third, fourth], {mode: "longest"}); + +it.next(); + +assert.throws(ExpectedError, function() { + it.return(); +}); + +assert.compareArray(log, [ + "call first next", + "call second next", + "call third next", + "call fourth next", + + "call third return", + "call second return", + "call first return", +]); diff --git a/test/built-ins/Iterator/zip/iterator-zip-iteration-shortest-iterator-close-abrupt-completion.js b/test/built-ins/Iterator/zip/iterator-zip-iteration-shortest-iterator-close-abrupt-completion.js new file mode 100644 index 00000000000..0ea88a04e5a --- /dev/null +++ b/test/built-ins/Iterator/zip/iterator-zip-iteration-shortest-iterator-close-abrupt-completion.js @@ -0,0 +1,125 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Handle abrupt completion from IteratorCloseAll in IteratorZip. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + iii. For each integer i such that 0 ≤ i < iterCount, in ascending order, do + ... + 3. Else, + ... + d. If result is done, then + i. Remove iter from openIters. + ii. If mode is "shortest", then + i. Return ? IteratorCloseAll(openIters, ReturnCompletion(undefined)). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +function ExpectedError() {} + +var log = []; + +var first = { + next() { + log.push("call first next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call first return"); + + return {}; + } +}; + +var second = { + next() { + log.push("call second next"); + return {done: true}; + }, + return() { + log.push("unexpected call second return"); + } +}; + +var third = { + next() { + log.push("unexpected call third next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call third return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var fourth = { + next() { + log.push("unexpected call fourth next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, fourth); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call fourth return"); + + throw new ExpectedError(); + } +}; + +var it = Iterator.zip([first, second, third, fourth], {mode: "shortest"}); + +assert.throws(ExpectedError, function() { + it.next(); +}); + +assert.compareArray(log, [ + "call first next", + "call second next", + "call fourth return", + "call third return", + "call first return", +]); diff --git a/test/built-ins/Iterator/zip/iterator-zip-iteration-strict-iterator-close-i-is-not-zero-abrupt-completion.js b/test/built-ins/Iterator/zip/iterator-zip-iteration-strict-iterator-close-i-is-not-zero-abrupt-completion.js new file mode 100644 index 00000000000..883a6dff0b9 --- /dev/null +++ b/test/built-ins/Iterator/zip/iterator-zip-iteration-strict-iterator-close-i-is-not-zero-abrupt-completion.js @@ -0,0 +1,108 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Handle abrupt completion from IteratorCloseAll in IteratorZip. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + iii. For each integer i such that 0 ≤ i < iterCount, in ascending order, do + ... + 3. Else, + ... + d. If result is done, then + i. Remove iter from openIters. + ... + iii. Else if mode is "strict", then + i. If i ≠ 0, then + i. Return ? IteratorCloseAll(openIters, ThrowCompletion(a newly created TypeError object)). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var log = []; + +var first = { + next() { + log.push("call first next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call first return"); + + return {}; + } +}; + +var second = { + next() { + log.push("call second next"); + return {done: true}; + }, + return() { + log.push("unexpected call second return"); + } +}; + +var third = { + next() { + log.push("unexpected call third next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call third return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var it = Iterator.zip([first, second, third], {mode: "strict"}); + +assert.throws(TypeError, function() { + it.next(); +}); + +assert.compareArray(log, [ + "call first next", + "call second next", + "call third return", + "call first return", +]); diff --git a/test/built-ins/Iterator/zip/iterator-zip-iteration-strict-iterator-close-i-is-zero-abrupt-completion.js b/test/built-ins/Iterator/zip/iterator-zip-iteration-strict-iterator-close-i-is-zero-abrupt-completion.js new file mode 100644 index 00000000000..63646bd924d --- /dev/null +++ b/test/built-ins/Iterator/zip/iterator-zip-iteration-strict-iterator-close-i-is-zero-abrupt-completion.js @@ -0,0 +1,123 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Handle abrupt completion from IteratorCloseAll in IteratorZip. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + iii. For each integer i such that 0 ≤ i < iterCount, in ascending order, do + ... + 3. Else, + ... + d. If result is done, then + i. Remove iter from openIters. + ... + iii. Else if mode is "strict", then + ... + ii. For each integer k such that 1 ≤ k < iterCount, in ascending order, do + ... + iv. Else, + i. Return ? IteratorCloseAll(openIters, ThrowCompletion(a newly created TypeError object)). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var log = []; + +var first = { + next() { + log.push("call first next"); + return {done: true}; + }, + return() { + log.push("unexpected call first return"); + } +}; + +var second = { + next() { + log.push("call second next"); + return {done: true}; + }, + return() { + log.push("unexpected call second return"); + } +}; + +var third = { + next() { + log.push("call third next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call third return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var fourth = { + next() { + log.push("unexpected call fourth next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, fourth); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call fourth return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var it = Iterator.zip([first, second, third, fourth], {mode: "strict"}); + +assert.throws(TypeError, function() { + it.next(); +}); + +assert.compareArray(log, [ + "call first next", + "call second next", + "call third next", + "call fourth return", + "call third return", +]); diff --git a/test/built-ins/Iterator/zip/iterator-zip-iteration-strict-iterator-step-abrupt-completion.js b/test/built-ins/Iterator/zip/iterator-zip-iteration-strict-iterator-step-abrupt-completion.js new file mode 100644 index 00000000000..9ef82da6a69 --- /dev/null +++ b/test/built-ins/Iterator/zip/iterator-zip-iteration-strict-iterator-step-abrupt-completion.js @@ -0,0 +1,125 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Handle abrupt completion from IteratorStep in IteratorZip. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + iii. For each integer i such that 0 ≤ i < iterCount, in ascending order, do + ... + 3. Else, + ... + d. If result is done, then + i. Remove iter from openIters. + ... + iii. Else if mode is "strict", then + ... + ii. For each integer k such that 1 ≤ k < iterCount, in ascending order, do + ... + ii. Let open be Completion(IteratorStep(iters[k])). + iii. If open is an abrupt completion, then + i. Remove iters[k] from openIters. + ii. Return ? IteratorCloseAll(openIters, open). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +function ExpectedError() {} + +var log = []; + +var first = { + next() { + log.push("call first next"); + return {done: true}; + }, + return() { + log.push("unexpected call first return"); + } +}; + +var second = { + next() { + log.push("call second next"); + throw new ExpectedError(); + }, + return() { + log.push("unexpected call second return"); + } +}; + +var third = { + next() { + log.push("unexpected call third next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call third return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var fourth = { + next() { + log.push("unexpected call fourth next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, fourth); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call fourth return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var it = Iterator.zip([first, second, third, fourth], {mode: "strict"}); + +assert.throws(ExpectedError, function() { + it.next(); +}); + +assert.compareArray(log, [ + "call first next", + "call second next", + "call fourth return", + "call third return", +]); diff --git a/test/built-ins/Iterator/zip/iterator-zip-iteration.js b/test/built-ins/Iterator/zip/iterator-zip-iteration.js new file mode 100644 index 00000000000..7d4a2d33ae6 --- /dev/null +++ b/test/built-ins/Iterator/zip/iterator-zip-iteration.js @@ -0,0 +1,155 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Perform iteration in IteratorZip. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + iii. For each integer i such that 0 ≤ i < iterCount, in ascending order, do + ... + 3. Else, + a. Let result be Completion(IteratorStepValue(iter)). + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var modes = [ + "shortest", + "longest", + "strict", +]; + +function makeIterator(log, name, elements) { + var elementsIter = elements.values(); + var iterator = { + next() { + log.push(`call ${name} next`); + + // Called with the correct receiver and no arguments. + assert.sameValue(this, iterator); + assert.sameValue(arguments.length, 0); + + var result = elementsIter.next(); + return { + get done() { + log.push(`get ${name}.result.done`); + return result.done; + }, + get value() { + log.push(`get ${name}.result.value`); + return result.value; + }, + }; + }, + return() { + log.push(`call ${name} return`); + + // Called with the correct receiver and no arguments. + assert.sameValue(this, iterator); + assert.sameValue(arguments.length, 0); + + return { + get done() { + log.push(`unexpected get ${name}.result.done`); + return result.done; + }, + get value() { + log.push(`unexpected get ${name}.result.value`); + return result.value; + }, + }; + } + }; + return iterator; +} + +for (var mode of modes) { + var log = []; + var iterables = [ + makeIterator(log, "first", [1, 2, 3]), + makeIterator(log, "second", [4, 5, 6]), + makeIterator(log, "third", [7, 8, 9]), + ]; + var it = Iterator.zip(iterables, {mode}); + + log.push("start"); + for (var v of it) { + log.push("loop"); + } + + var expected = [ + "start", + + "call first next", + "get first.result.done", + "get first.result.value", + "call second next", + "get second.result.done", + "get second.result.value", + "call third next", + "get third.result.done", + "get third.result.value", + "loop", + + "call first next", + "get first.result.done", + "get first.result.value", + "call second next", + "get second.result.done", + "get second.result.value", + "call third next", + "get third.result.done", + "get third.result.value", + "loop", + + "call first next", + "get first.result.done", + "get first.result.value", + "call second next", + "get second.result.done", + "get second.result.value", + "call third next", + "get third.result.done", + "get third.result.value", + "loop", + ]; + + switch (mode) { + case "shortest": { + expected.push( + "call first next", + "get first.result.done", + "call third return", + "call second return", + ); + break; + } + case "longest": + case "strict": { + expected.push( + "call first next", + "get first.result.done", + "call second next", + "get second.result.done", + "call third next", + "get third.result.done", + ); + break; + } + } + + assert.compareArray(log, expected); +} diff --git a/test/built-ins/Iterator/zip/length.js b/test/built-ins/Iterator/zip/length.js new file mode 100644 index 00000000000..418ca6dce4d --- /dev/null +++ b/test/built-ins/Iterator/zip/length.js @@ -0,0 +1,23 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Iterator.zip has a "length" property whose value is 1. +info: | + ECMAScript Standard Built-in Objects + + Unless otherwise specified, the length property of a built-in + Function object has the attributes { [[Writable]]: false, [[Enumerable]]: + false, [[Configurable]]: true }. +features: [joint-iteration] +includes: [propertyHelper.js] +---*/ + +verifyProperty(Iterator.zip, "length", { + value: 1, + writable: false, + enumerable: false, + configurable: true, +}); diff --git a/test/built-ins/Iterator/zip/name.js b/test/built-ins/Iterator/zip/name.js new file mode 100644 index 00000000000..e147c5d6279 --- /dev/null +++ b/test/built-ins/Iterator/zip/name.js @@ -0,0 +1,30 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + The "name" property of Iterator.zip +info: | + 17 ECMAScript Standard Built-in Objects + + Every built-in Function object, including constructors, that is not + identified as an anonymous function has a name property whose value is a + String. Unless otherwise specified, this value is the name that is given to + the function in this specification. + + ... + + Unless otherwise specified, the name property of a built-in Function + object, if it exists, has the attributes { [[Writable]]: false, + [[Enumerable]]: false, [[Configurable]]: true }. +features: [joint-iteration] +includes: [propertyHelper.js] +---*/ + +verifyProperty(Iterator.zip, "name", { + value: "zip", + writable: false, + enumerable: false, + configurable: true, +}); diff --git a/test/built-ins/Iterator/zip/non-constructible.js b/test/built-ins/Iterator/zip/non-constructible.js new file mode 100644 index 00000000000..6132d244b3e --- /dev/null +++ b/test/built-ins/Iterator/zip/non-constructible.js @@ -0,0 +1,17 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Iterator.zip is not constructible. + + Built-in function objects that are not identified as constructors do not + implement the [[Construct]] internal method unless otherwise specified in the + description of a particular function. +features: [joint-iteration] +---*/ + +assert.throws(TypeError, () => { + new Iterator.zip([]); +}); diff --git a/test/built-ins/Iterator/zip/options-mode.js b/test/built-ins/Iterator/zip/options-mode.js new file mode 100644 index 00000000000..b73aac997f5 --- /dev/null +++ b/test/built-ins/Iterator/zip/options-mode.js @@ -0,0 +1,88 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + The "mode" option must be undefined or a valid string mode. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 3. Let mode be ? Get(options, "mode"). + 4. If mode is undefined, set mode to "shortest". + 5. If mode is not one of "shortest", "longest", or "strict", throw a TypeError exception. + ... +features: [joint-iteration] +---*/ + +var validModes = [ + undefined, + "shortest", + "longest", + "strict", +]; + +var invalidModes = [ + null, + false, + "", + "short", + "long", + "loose", + Symbol(), + 123, + 123n, + {}, +]; + +// Absent "mode" option. +Iterator.zip([], {}); + +// All valid mode values are accepted. +for (var mode of validModes) { + Iterator.zip([], {mode}); +} + +// Throws a TypeError for invalid mode options. +for (var mode of invalidModes) { + assert.throws(TypeError, function() { + Iterator.zip([], {mode}); + }); +} + +// "padding" option is not retrieved when "mode" option is invalid. +for (var mode of invalidModes) { + var options = { + mode, + get padding() { + throw new Test262Error(); + } + }; + assert.throws(TypeError, function() { + Iterator.zip([], options); + }); +} + +// String wrappers are not accepted. +for (var mode of validModes) { + var options = {mode: new String(mode)}; + assert.throws(TypeError, function() { + Iterator.zip([], options); + }); +} + +// Does not call any of `toString`, `valueOf`, `Symbol.toPrimitive`. +var badMode = { + toString() { + throw new Test262Error(); + }, + valueOf() { + throw new Test262Error(); + }, + [Symbol.toPrimitive]() { + throw new Test262Error(); + }, +}; +assert.throws(TypeError, function() { + Iterator.zip([], {mode: badMode}); +}); diff --git a/test/built-ins/Iterator/zip/options-padding.js b/test/built-ins/Iterator/zip/options-padding.js new file mode 100644 index 00000000000..fabad71466c --- /dev/null +++ b/test/built-ins/Iterator/zip/options-padding.js @@ -0,0 +1,55 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + The "padding" option must be undefined or an object. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 6. Let paddingOption be undefined. + 7. If mode is "longest", then + a. Set paddingOption to ? Get(options, "padding"). + b. If paddingOption is not undefined and paddingOption is not an Object, throw a TypeError exception. + ... +features: [joint-iteration] +---*/ + +var validPadding = [ + undefined, + [], + Object("string"), +]; + +var invalidPadding = [ + null, + false, + "", + Symbol(), + 123, + 123n, +]; + +// Absent "padding" option. +Iterator.zip([], {mode: "longest"}); + +// All valid padding values are accepted. +for (var padding of validPadding) { + Iterator.zip([], {mode: "longest", padding}); +} + +// Throws a TypeError for invalid padding options. +for (var padding of invalidPadding) { + assert.throws(TypeError, function() { + Iterator.zip([], {mode: "longest", padding}); + }); +} + +// Invalid padding options are okay when mode is not "longest" because the padding option is not read. +for (var padding of invalidPadding) { + Iterator.zip([], {padding}); + Iterator.zip([], {mode: undefined, padding}); + Iterator.zip([], {mode: "shortest", padding}); + Iterator.zip([], {mode: "strict", padding}); +} diff --git a/test/built-ins/Iterator/zip/options.js b/test/built-ins/Iterator/zip/options.js new file mode 100644 index 00000000000..d523a65e006 --- /dev/null +++ b/test/built-ins/Iterator/zip/options.js @@ -0,0 +1,50 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + The "options" argument can either be undefined or an object. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 2. Set options to ? GetOptionsObject(options). + ... + + GetOptionsObject ( options ) + 1. If options is undefined, then + a. Return OrdinaryObjectCreate(null). + 2. If options is an Object, then + a. Return options. + 3. Throw a TypeError exception. +features: [joint-iteration] +---*/ + +var validOptions = [ + undefined, + {}, +]; + +var invalidOptions = [ + null, + true, + "", + Symbol(), + 0, + 0n, +]; + +// The "options" argument can also be absent. +Iterator.zip([]); + +// All valid option values are accepted. +for (var options of validOptions) { + Iterator.zip([], options); +} + +// Throws a TypeError for invalid option values. +for (var options of invalidOptions) { + assert.throws(TypeError, function() { + Iterator.zip([], options); + }); +} diff --git a/test/built-ins/Iterator/zip/padding-iteration-get-iterator-abrupt-completion.js b/test/built-ins/Iterator/zip/padding-iteration-get-iterator-abrupt-completion.js new file mode 100644 index 00000000000..0a5c9b0579b --- /dev/null +++ b/test/built-ins/Iterator/zip/padding-iteration-get-iterator-abrupt-completion.js @@ -0,0 +1,144 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Abrupt completion for GetIterator in "padding" option iteration. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 14. If mode is "longest", then + ... + b. Else, + i. Let paddingIter be Completion(GetIterator(paddingOption, sync)). + ii. IfAbruptCloseIterators(paddingIter, iters). + ... + + IfAbruptCloseIterators ( value, iteratorRecords ) + 1. Assert: value is a Completion Record. + 2. If value is an abrupt completion, return ? IteratorCloseAll(iteratorRecords, value). + 3. Else, set value to value.[[Value]]. + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var log = []; + +var first = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("first return"); + + // This exception is ignored. + throw new Test262Error(); + } +}; + +var second = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, second); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("second return"); + + // This exception is ignored. + throw new Test262Error(); + } +}; + +var third = { + next() { + log.push("unexpected call to next method"); + }, + get return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("third return"); + + // This exception is ignored. + throw new Test262Error(); + } +}; + +function ExpectedError() {} + +// Padding iterator throws from |Symbol.iterator|. +var padding = { + [Symbol.iterator]() { + throw new ExpectedError(); + }, + next() { + log.push("unexpected call to next method"); + }, + return() { + log.push("unexpected call to return method"); + }, +}; + +assert.throws(ExpectedError, function() { + Iterator.zip([first, second, third], {mode: "longest", padding}); +}); + +assert.compareArray(log, [ + "third return", + "second return", + "first return", +]); + +// Clear log +log.length = 0; + +// Padding iterator throws from |next|. +var padding = { + [Symbol.iterator]() { + return this; + }, + next() { + throw new ExpectedError(); + }, + return() { + log.push("unexpected call to return method"); + }, +}; + +assert.throws(ExpectedError, function() { + Iterator.zip([first, second, third], {mode: "longest", padding}); +}); + +assert.compareArray(log, [ + "third return", + "second return", + "first return", +]); diff --git a/test/built-ins/Iterator/zip/padding-iteration-iterator-close-abrupt-completion.js b/test/built-ins/Iterator/zip/padding-iteration-iterator-close-abrupt-completion.js new file mode 100644 index 00000000000..0613da2ff5d --- /dev/null +++ b/test/built-ins/Iterator/zip/padding-iteration-iterator-close-abrupt-completion.js @@ -0,0 +1,124 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Abrupt completion for IteratorClose in "padding" option iteration. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 14. If mode is "longest", then + ... + b. Else, + i. Let paddingIter be Completion(GetIterator(paddingOption, sync)). + ... + v. If usingIterator is true, then + 1. Let completion be Completion(IteratorClose(paddingIter, NormalCompletion(unused))). + 2. IfAbruptCloseIterators(completion, iters). + ... + + IfAbruptCloseIterators ( value, iteratorRecords ) + 1. Assert: value is a Completion Record. + 2. If value is an abrupt completion, return ? IteratorCloseAll(iteratorRecords, value). + 3. Else, set value to value.[[Value]]. + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var log = []; + +var first = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("first return"); + + // This exception is ignored. + throw new Test262Error(); + } +}; + +var second = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, second); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("second return"); + + // This exception is ignored. + throw new Test262Error(); + } +}; + +var third = { + next() { + log.push("unexpected call to next method"); + }, + get return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("third return"); + + // This exception is ignored. + throw new Test262Error(); + } +}; + +function ExpectedError() {} + +// Padding iterator throws from |Symbol.iterator|. +var padding = { + [Symbol.iterator]() { + return this; + }, + next() { + return {done: false}; + }, + return() { + log.push("padding return"); + + throw new ExpectedError(); + }, +}; + +assert.throws(ExpectedError, function() { + Iterator.zip([first, second, third], {mode: "longest", padding}); +}); + +assert.compareArray(log, [ + "padding return", + "third return", + "second return", + "first return", +]); diff --git a/test/built-ins/Iterator/zip/padding-iteration-iterator-step-value-abrupt-completion.js b/test/built-ins/Iterator/zip/padding-iteration-iterator-step-value-abrupt-completion.js new file mode 100644 index 00000000000..833977eab14 --- /dev/null +++ b/test/built-ins/Iterator/zip/padding-iteration-iterator-step-value-abrupt-completion.js @@ -0,0 +1,187 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Abrupt completion for IteratorStepValue in "padding" option iteration. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 14. If mode is "longest", then + ... + b. Else, + i. Let paddingIter be Completion(GetIterator(paddingOption, sync)). + ... + iv. Perform the following steps iterCount times: + 1. If usingIterator is true, then + a. Set next to Completion(IteratorStepValue(paddingIter)). + b. IfAbruptCloseIterators(next, iters). + ... + + IfAbruptCloseIterators ( value, iteratorRecords ) + 1. Assert: value is a Completion Record. + 2. If value is an abrupt completion, return ? IteratorCloseAll(iteratorRecords, value). + 3. Else, set value to value.[[Value]]. + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var log = []; + +var first = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("first return"); + + // This exception is ignored. + throw new Test262Error(); + } +}; + +var second = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, second); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("second return"); + + // This exception is ignored. + throw new Test262Error(); + } +}; + +var third = { + next() { + log.push("unexpected call to next method"); + }, + get return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("third return"); + + // This exception is ignored. + throw new Test262Error(); + } +}; + +function ExpectedError() {} + +// Padding iterator throws from |next|. +var padding = { + [Symbol.iterator]() { + return this; + }, + next() { + throw new ExpectedError(); + }, + return() { + log.push("unexpected call to return method"); + }, +}; + +assert.throws(ExpectedError, function() { + Iterator.zip([first, second, third], {mode: "longest", padding}); +}); + +assert.compareArray(log, [ + "third return", + "second return", + "first return", +]); + +// Clear log +log.length = 0; + +// Padding iterator throws from |done|. +var padding = { + [Symbol.iterator]() { + return this; + }, + next() { + return { + get done() { + throw new ExpectedError(); + }, + get value() { + log.push("unexpected access to value"); + }, + }; + }, + return() { + log.push("unexpected call to return method"); + }, +}; + +assert.throws(ExpectedError, function() { + Iterator.zip([first, second, third], {mode: "longest", padding}); +}); + +assert.compareArray(log, [ + "third return", + "second return", + "first return", +]); + + +// Clear log +log.length = 0; + +// Padding iterator throws from |value|. +var padding = { + [Symbol.iterator]() { + return this; + }, + next() { + return { + done: false, + get value() { + throw new ExpectedError(); + }, + }; + }, + return() { + log.push("unexpected call to return method"); + }, +}; + +assert.throws(ExpectedError, function() { + Iterator.zip([first, second, third], {mode: "longest", padding}); +}); + +assert.compareArray(log, [ + "third return", + "second return", + "first return", +]); diff --git a/test/built-ins/Iterator/zip/padding-iteration.js b/test/built-ins/Iterator/zip/padding-iteration.js new file mode 100644 index 00000000000..9797e220d5f --- /dev/null +++ b/test/built-ins/Iterator/zip/padding-iteration.js @@ -0,0 +1,135 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Perform iteration of the "padding" option. +info: | + Iterator.zip ( iterables [ , options ] ) + ... + 14. If mode is "longest", then + ... + b. Else, + i. Let paddingIter be Completion(GetIterator(paddingOption, sync)). + ... + iii. Let usingIterator be true. + iv. Perform the following steps iterCount times: + 1. If usingIterator is true, then + a. Set next to Completion(IteratorStepValue(paddingIter)). + ... + c. If next is done, then + i. Set usingIterator to false. + d. Else, + i. Append next to padding. + 2. If usingIterator is false, append undefined to padding. + v. If usingIterator is true, then + 1. Let completion be Completion(IteratorClose(paddingIter, NormalCompletion(unused))). + ... + ... + + GetIterator ( obj, kind ) + ... + 2. Else, + a. Let method be ? GetMethod(obj, %Symbol.iterator%). + 3. If method is undefined, throw a TypeError exception. + 4. Return ? GetIteratorFromMethod(obj, method). + + GetIteratorFromMethod ( obj, method ) + 1. Let iterator be ? Call(method, obj). + 2. If iterator is not an Object, throw a TypeError exception. + 3. Return ? GetIteratorDirect(iterator). + + GetIteratorDirect ( obj ) + 1. Let nextMethod be ? Get(obj, "next"). + 2. Let iteratorRecord be the Iterator Record { [[Iterator]]: obj, [[NextMethod]]: nextMethod, [[Done]]: false }. + 3. Return iteratorRecord. +includes: [proxyTrapsHelper.js, compareArray.js] +features: [joint-iteration] +---*/ + +function makeProxyWithGetHandler(log, name, obj) { + return new Proxy(obj, allowProxyTraps({ + get(target, propertyKey, receiver) { + log.push(`${name}::${String(propertyKey)}`); + return Reflect.get(target, propertyKey, receiver); + } + })); +} + +// "padding" option must be an iterable. +assert.throws(TypeError, function() { + Iterator.zip([], { + mode: "longest", + padding: {}, + }); +}); + +for (var n = 0; n <= 5; ++n) { + var iterables = Array(n).fill([]); + + for (var k = 0; k <= n + 2; ++k) { + var elements = Array(k).fill(0); + var elementsIter = elements.values(); + + var log = []; + + var padding = makeProxyWithGetHandler(log, "padding", { + [Symbol.iterator]() { + log.push("call iterator"); + + // Called with the correct receiver and no arguments. + assert.sameValue(this, padding); + assert.sameValue(arguments.length, 0); + + return this; + }, + next() { + log.push("call next"); + + // Called with the correct receiver and no arguments. + assert.sameValue(this, padding); + assert.sameValue(arguments.length, 0); + + return elementsIter.next(); + }, + return() { + log.push("call return"); + + // Called with the correct receiver and no arguments. + assert.sameValue(this, padding); + assert.sameValue(arguments.length, 0); + + return {}; + } + }); + + Iterator.zip(iterables, {mode: "longest", padding}); + + // Property reads and calls from GetIterator. + var expected = [ + "padding::Symbol(Symbol.iterator)", + "call iterator", + "padding::next", + ]; + + // Call the "next" method |n| times until the padding iterator is exhausted. + for (var i = 0; i < Math.min(n, k); ++i) { + expected.push("call next"); + } + + // If |n| is larger than |k|, then there was one final call to the "next" + // method. Otherwise the "return" method was called to close the padding + // iterator. + if (n > k) { + expected.push("call next"); + } else { + expected.push( + "padding::return", + "call return" + ); + } + + assert.compareArray(log, expected); + } +} diff --git a/test/built-ins/Iterator/zip/prop-desc.js b/test/built-ins/Iterator/zip/prop-desc.js new file mode 100644 index 00000000000..7e07c43ac98 --- /dev/null +++ b/test/built-ins/Iterator/zip/prop-desc.js @@ -0,0 +1,23 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Property descriptor of Iterator.zip +info: | + 17 ECMAScript Standard Built-in Objects + + Every other data property described in clauses 18 through 26 and in Annex B.2 + has the attributes { [[Writable]]: true, [[Enumerable]]: false, + [[Configurable]]: true } unless otherwise specified. +features: [joint-iteration] +includes: [propertyHelper.js] +---*/ + +verifyProperty(Iterator, "zip", { + value: Iterator.zip, + writable: true, + enumerable: false, + configurable: true, +}); diff --git a/test/built-ins/Iterator/zip/proto.js b/test/built-ins/Iterator/zip/proto.js new file mode 100644 index 00000000000..21727bc518f --- /dev/null +++ b/test/built-ins/Iterator/zip/proto.js @@ -0,0 +1,16 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + The value of the [[Prototype]] internal slot of Iterator.zip is the + intrinsic object %FunctionPrototype%. +features: [joint-iteration] +---*/ + +assert.sameValue( + Object.getPrototypeOf(Iterator.zip), + Function.prototype, + "Object.getPrototypeOf(Iterator.zip) must return the value of Function.prototype" +); diff --git a/test/built-ins/Iterator/zip/result-is-iterator.js b/test/built-ins/Iterator/zip/result-is-iterator.js new file mode 100644 index 00000000000..f85d17fafb3 --- /dev/null +++ b/test/built-ins/Iterator/zip/result-is-iterator.js @@ -0,0 +1,20 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + The value of the [[Prototype]] internal slot of the return value of Iterator.zip + is the intrinsic object %IteratorHelperPrototype%. +includes: [wellKnownIntrinsicObjects.js] +features: [joint-iteration] +---*/ + +var iter = Iterator.zip([]); +assert(iter instanceof Iterator, "Iterator.zip([]) must return an Iterator"); + +assert.sameValue( + Object.getPrototypeOf(iter), + getWellKnownIntrinsicObject("%IteratorHelperPrototype%"), + "[[Prototype]] is %IteratorHelperPrototype%" +); diff --git a/test/built-ins/Iterator/zip/suspended-start-iterator-close-calls-next.js b/test/built-ins/Iterator/zip/suspended-start-iterator-close-calls-next.js new file mode 100644 index 00000000000..916ad840981 --- /dev/null +++ b/test/built-ins/Iterator/zip/suspended-start-iterator-close-calls-next.js @@ -0,0 +1,78 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Generator is closed from suspended-start state and IteratorClose calls next. +info: | + %IteratorHelperPrototype%.return ( ) + ... + 4. If O.[[GeneratorState]] is suspended-start, then + a. Set O.[[GeneratorState]] to completed. + ... + c. Perform ? IteratorCloseAll(O.[[UnderlyingIterators]], ReturnCompletion(undefined)). + d. Return CreateIteratorResultObject(undefined, true). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + ... + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + ... + + %IteratorHelperPrototype%.next ( ) + 1. Return ? GeneratorResume(this value, undefined, "Iterator Helper"). + + GeneratorResume ( generator, value, generatorBrand ) + 1. Let state be ? GeneratorValidate(generator, generatorBrand). + 2. If state is completed, return CreateIteratorResultObject(undefined, true). + ... + + GeneratorValidate ( generator, generatorBrand ) + ... + 5. Let state be generator.[[GeneratorState]]. + 6. If state is executing, throw a TypeError exception. + 7. Return state. +features: [joint-iteration] +---*/ + +var returnCallCount = 0; + +var underlying = { + next() { + throw new Test262Error("Unexpected call to next"); + }, + return() { + returnCallCount += 1; + + // The generator state is already set to "completed". The generator state is + // not "executing", so `GeneratorValidate` succeeds and `GeneratorResume` + // returns with `CreateIteratorResultObject()`. + var result = it.next(); + assert.sameValue(result.value, undefined); + assert.sameValue(result.done, true); + + return {}; + }, +}; + +var it = Iterator.zip([underlying]); + +assert.sameValue(returnCallCount, 0); + +// This `return()` call sets the generator state to "completed" and then calls +// `IteratorClose()`. +var result = it.return(); +assert.sameValue(result.value, undefined); +assert.sameValue(result.done, true); + +assert.sameValue(returnCallCount, 1); diff --git a/test/built-ins/Iterator/zip/suspended-start-iterator-close-calls-return.js b/test/built-ins/Iterator/zip/suspended-start-iterator-close-calls-return.js new file mode 100644 index 00000000000..20cd09d631b --- /dev/null +++ b/test/built-ins/Iterator/zip/suspended-start-iterator-close-calls-return.js @@ -0,0 +1,82 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Generator is closed from suspended-start state and IteratorClose calls return. +info: | + %IteratorHelperPrototype%.return ( ) + ... + 4. If O.[[GeneratorState]] is suspended-start, then + a. Set O.[[GeneratorState]] to completed. + ... + c. Perform ? IteratorCloseAll(O.[[UnderlyingIterators]], ReturnCompletion(undefined)). + d. Return CreateIteratorResultObject(undefined, true). + 5. Let C be ReturnCompletion(undefined). + 6. Return ? GeneratorResumeAbrupt(O, C, "Iterator Helper"). + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + ... + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + ... + 8. Return ? completion. + + GeneratorResumeAbrupt ( generator, abruptCompletion, generatorBrand ) + 1. Let state be ? GeneratorValidate(generator, generatorBrand). + 2. If state is suspended-start, then + ... + 3. If state is completed, then + a. If abruptCompletion is a return completion, then + i. Return CreateIteratorResultObject(abruptCompletion.[[Value]], true). + ... + + GeneratorValidate ( generator, generatorBrand ) + ... + 5. Let state be generator.[[GeneratorState]]. + 6. If state is executing, throw a TypeError exception. + 7. Return state. +features: [joint-iteration] +---*/ + +var returnCallCount = 0; + +var underlying = { + next() { + throw new Test262Error("Unexpected call to next"); + }, + return() { + returnCallCount += 1; + + // The generator state is already set to "completed", so this `return()` + // call proceeds to `GeneratorResumeAbrupt`. The generator state is not + // "executing", so `GeneratorValidate` succeeds and `GeneratorResumeAbrupt` + // returns with `CreateIteratorResultObject()`. + var result = it.return(); + assert.sameValue(result.value, undefined); + assert.sameValue(result.done, true); + + return {}; + }, +}; + +var it = Iterator.zip([underlying]); + +assert.sameValue(returnCallCount, 0); + +// This `return()` call sets the generator state to "completed" and then calls +// `IteratorClose()`. +var result = it.return(); +assert.sameValue(result.value, undefined); +assert.sameValue(result.done, true); + +assert.sameValue(returnCallCount, 1); diff --git a/test/built-ins/Iterator/zip/suspended-yield-iterator-close-calls-next.js b/test/built-ins/Iterator/zip/suspended-yield-iterator-close-calls-next.js new file mode 100644 index 00000000000..1a5683bf65a --- /dev/null +++ b/test/built-ins/Iterator/zip/suspended-yield-iterator-close-calls-next.js @@ -0,0 +1,102 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Generator is closed from suspended-yield state and IteratorClose calls next. +info: | + %IteratorHelperPrototype%.return ( ) + ... + 5. Let C be ReturnCompletion(undefined). + 6. Return ? GeneratorResumeAbrupt(O, C, "Iterator Helper"). + + GeneratorResumeAbrupt ( generator, abruptCompletion, generatorBrand ) + 1. Let state be ? GeneratorValidate(generator, generatorBrand). + ... + 4. Assert: state is suspended-yield. + ... + 8. Set generator.[[GeneratorState]] to executing. + ... + 10. Resume the suspended evaluation of genContext using abruptCompletion as + the result of the operation that suspended it. Let result be the + Completion Record returned by the resumed computation. + ... + + GeneratorValidate ( generator, generatorBrand ) + ... + 5. Let state be generator.[[GeneratorState]]. + 6. If state is executing, throw a TypeError exception. + 7. Return state. + + IteratorZip ( iters, mode, padding, finishResults ) + ... + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + v. Let completion be Completion(Yield(results)). + vi. If completion is an abrupt completion, then + 1. Return ? IteratorCloseAll(openIters, completion). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + ... + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + ... + + %IteratorHelperPrototype%.next ( ) + 1. Return ? GeneratorResume(this value, undefined, "Iterator Helper"). + + GeneratorResume ( generator, value, generatorBrand ) + 1. Let state be ? GeneratorValidate(generator, generatorBrand). + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var returnCallCount = 0; + +var underlying = { + next() { + return {value: 123, done: false}; + }, + return() { + returnCallCount += 1; + + // The generator state is set to "executing", so this `next()` call throws + // a TypeError when `GeneratorResume` performs `GeneratorValidate`. + assert.throws(TypeError, function() { + it.next(); + }); + + return {}; + }, +}; + +var it = Iterator.zip([underlying]); + +// Move generator into "suspended-yield" state. +var result = it.next(); +assert.compareArray(result.value, [123]); +assert.sameValue(result.done, false); + +assert.sameValue(returnCallCount, 0); + +// This `return()` call continues execution in the suspended generator. +var result = it.return(); +assert.sameValue(result.value, undefined); +assert.sameValue(result.done, true); + +assert.sameValue(returnCallCount, 1); diff --git a/test/built-ins/Iterator/zip/suspended-yield-iterator-close-calls-return.js b/test/built-ins/Iterator/zip/suspended-yield-iterator-close-calls-return.js new file mode 100644 index 00000000000..be55f0128e2 --- /dev/null +++ b/test/built-ins/Iterator/zip/suspended-yield-iterator-close-calls-return.js @@ -0,0 +1,95 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zip +description: > + Generator is closed from suspended-yield state and IteratorClose calls return. +info: | + %IteratorHelperPrototype%.return ( ) + ... + 5. Let C be ReturnCompletion(undefined). + 6. Return ? GeneratorResumeAbrupt(O, C, "Iterator Helper"). + + GeneratorResumeAbrupt ( generator, abruptCompletion, generatorBrand ) + 1. Let state be ? GeneratorValidate(generator, generatorBrand). + ... + 4. Assert: state is suspended-yield. + ... + 8. Set generator.[[GeneratorState]] to executing. + ... + 10. Resume the suspended evaluation of genContext using abruptCompletion as + the result of the operation that suspended it. Let result be the + Completion Record returned by the resumed computation. + ... + + GeneratorValidate ( generator, generatorBrand ) + ... + 5. Let state be generator.[[GeneratorState]]. + 6. If state is executing, throw a TypeError exception. + 7. Return state. + + IteratorZip ( iters, mode, padding, finishResults ) + ... + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + v. Let completion be Completion(Yield(results)). + vi. If completion is an abrupt completion, then + 1. Return ? IteratorCloseAll(openIters, completion). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + ... + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var returnCallCount = 0; + +var underlying = { + next() { + return {value: 123, done: false}; + }, + return() { + returnCallCount += 1; + + // The generator state is set to "executing", so this `return()` call throws + // a TypeError when `GeneratorResumeAbrupt` performs `GeneratorValidate`. + assert.throws(TypeError, function() { + it.return(); + }); + + return {}; + }, +}; + +var it = Iterator.zip([underlying]); + +// Move generator into "suspended-yield" state. +var result = it.next(); +assert.compareArray(result.value, [123]); +assert.sameValue(result.done, false); + +assert.sameValue(returnCallCount, 0); + +// This `return()` call continues execution in the suspended generator. +var result = it.return(); +assert.sameValue(result.value, undefined); +assert.sameValue(result.done, true); + +assert.sameValue(returnCallCount, 1); diff --git a/test/built-ins/Iterator/zipKeyed/basic-longest.js b/test/built-ins/Iterator/zipKeyed/basic-longest.js new file mode 100644 index 00000000000..2bf04ab5da8 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/basic-longest.js @@ -0,0 +1,105 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Basic Iterator.zipKeyed test with "longest" mode. +includes: [compareArray.js, propertyHelper.js, iteratorZipUtils.js] +features: [joint-iteration] +---*/ + +function testSequence(inputs, inputsLabel, minLength, maxLength) { + function test(options, optionsLabel, getPaddingForInput) { + var label = optionsLabel + ", " + inputsLabel; + var it = Iterator.zipKeyed(inputs, options); + assertZippedKeyed(it, inputs, minLength, label); + + var expectedKeys = Object.keys(inputs); + + for (var i = minLength; i < maxLength; i++) { + var itemLabel = label + ", step " + i; + + var result = it.next(); + var value = result.value; + + // Test IteratorResult structure. + assertIteratorResult(result, value, false, itemLabel); + + // Ensure resulting object has the expected keys and values. + assert.compareArray(Object.keys(value), expectedKeys, itemLabel + ": result object keys"); + + var expectedValues = Object.values(inputs).map(function (input, j) { + return i < input.length ? input[i] : getPaddingForInput(j); + }); + assert.compareArray(Object.values(value), expectedValues, itemLabel + ": result object values"); + } + assertIteratorResult(it.next(), undefined, true, label + ": after completion"); + } + + test( + { mode: "longest" }, + "options = { mode: 'longest' }", + function () { + return undefined; + }, + ); + + test( + { mode: "longest", padding: {} }, + "options = { mode: 'longest', padding: {} }", + function () { + return undefined; + }, + ); + + test( + { mode: "longest", padding: { prop_0: "pad" } }, + "options = { mode: 'longest', padding: { prop_0: 'pad' } }", + function (idx) { + return idx === 0 ? "pad" : undefined; + }, + ); + + test( + { mode: "longest", padding: { prop_1: "pad" } }, + "options = { mode: 'longest', padding: { prop_1: 'pad' } }", + function (idx) { + return idx === 1 ? "pad" : undefined; + }, + ); + + var padding = {}; + for (var key in inputs) { + padding[key] = "pad"; + } + test( + { mode: "longest", padding: padding }, + "options = { mode: 'longest', padding: { prop_0: 'pad', ..., prop_N: 'pad' } }", + function (idx) { + return "pad"; + }, + ); + + // Object with many properties. + padding = new Proxy({}, { + has(target, key) { + return key.indexOf('_') !== -1; + }, + get(target, key, receiver) { + var split = key.split('_'); + if (split.length !== 2) return undefined; + return 'pad_' + split[1]; + } + }); + test( + { mode: "longest", padding: padding }, + "options = { mode: 'longest', padding: { prop_0: 'pad_1', ..., prop_N: 'pad_N' } }", + function (idx) { + return 'pad_' + idx; + }, + ); + +} + +forEachSequenceCombinationKeyed(testSequence); diff --git a/test/built-ins/Iterator/zipKeyed/basic-shortest.js b/test/built-ins/Iterator/zipKeyed/basic-shortest.js new file mode 100644 index 00000000000..b217e09c9ea --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/basic-shortest.js @@ -0,0 +1,29 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Basic Iterator.zipkeyed test with "shortest" mode. +includes: [compareArray.js, propertyHelper.js, iteratorZipUtils.js] +features: [joint-iteration] +---*/ + +function testSequence(inputs, inputsLabel, minLength, maxLength) { + function test(options, optionsLabel) { + var label = optionsLabel + ", " + inputsLabel; + var it = Iterator.zipKeyed(inputs, options); + assertZippedKeyed(it, inputs, minLength, label); + + // Iterator is closed after `minLength` items. + assertIteratorResult(it.next(), undefined, true, label + ": after completion"); + } + + test(undefined, "options = undefined"); + + test({}, "options = {}"); + + test({ mode: "shortest" }, "options = { mode: 'shortest' }"); +} + +forEachSequenceCombinationKeyed(testSequence); diff --git a/test/built-ins/Iterator/zipKeyed/basic-strict.js b/test/built-ins/Iterator/zipKeyed/basic-strict.js new file mode 100644 index 00000000000..52dc509835d --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/basic-strict.js @@ -0,0 +1,30 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Basic Iterator.zipkeyed test with "strict" mode. +includes: [compareArray.js, propertyHelper.js, iteratorZipUtils.js] +features: [joint-iteration] +---*/ + +function testSequence(inputs, inputsLabel, minLength, maxLength) { + function test(options, optionsLabel) { + var label = optionsLabel + ", " + inputsLabel; + var it = Iterator.zipKeyed(inputs, options); + assertZippedKeyed(it, inputs, minLength, label); + + if (minLength === maxLength) { + assertIteratorResult(it.next(), undefined, true, label + ": after completion"); + } else { + assert.throws(TypeError, function() { + it.next(); + }, label + " should throw after " + minLength + " items."); + } + } + + test({ mode: "strict" }, "options = { mode: 'strict' }"); +} + +forEachSequenceCombinationKeyed(testSequence); diff --git a/test/built-ins/Iterator/zipKeyed/is-function.js b/test/built-ins/Iterator/zipKeyed/is-function.js new file mode 100644 index 00000000000..840cca95f65 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/is-function.js @@ -0,0 +1,15 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Iterator.zipKeyed is a built-in function +features: [joint-iteration] +---*/ + +assert.sameValue( + typeof Iterator.zipKeyed, + "function", + "The value of `typeof Iterator.zipKeyed` is 'function'" +); diff --git a/test/built-ins/Iterator/zipKeyed/iterables-containing-string-objects.js b/test/built-ins/Iterator/zipKeyed/iterables-containing-string-objects.js new file mode 100644 index 00000000000..3ca694ca6c4 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterables-containing-string-objects.js @@ -0,0 +1,23 @@ +// Copyright (C) 2025 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipKeyed +description: > + Accepts String objects as inputs. +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var result = Array.from(Iterator.zipKeyed({ + a: Object("abc"), + b: Object("123"), +})); + +assert.sameValue(result.length, 3); +result.forEach(function (object) { + assert.compareArray(Object.keys(object), ["a", "b"]); +}); +assert.compareArray(Object.values(result[0]), ["a", "1"]); +assert.compareArray(Object.values(result[1]), ["b", "2"]); +assert.compareArray(Object.values(result[2]), ["c", "3"]); diff --git a/test/built-ins/Iterator/zipKeyed/iterables-iteration-after-reading-options.js b/test/built-ins/Iterator/zipKeyed/iterables-iteration-after-reading-options.js new file mode 100644 index 00000000000..d0fa3f107e5 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterables-iteration-after-reading-options.js @@ -0,0 +1,71 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Perform [[OwnPropertyKeys]] on the "iterables" argument after reading all properties. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 3. Let mode be ? Get(options, "mode"). + ... + 7. If mode is "longest", then + a. Set paddingOption to ? Get(options, "padding"). + ... + 10. Let allKeys be ? iterables.[[OwnPropertyKeys]](). + ... +includes: [proxyTrapsHelper.js, compareArray.js] +features: [joint-iteration] +---*/ + +var log = []; + +var iterables = new Proxy({}, allowProxyTraps({ + ownKeys(target) { + log.push("own-keys"); + return Reflect.ownKeys(target); + }, +})); + + +var options = { + get mode() { + log.push("get mode"); + return "longest"; + }, + get padding() { + log.push("get padding"); + return []; + } +}; + +Iterator.zipKeyed(iterables, options); + +assert.compareArray(log, [ + "get mode", + "get padding", + "own-keys", +]); + +for (var mode of [undefined, "shortest", "strict"]) { + log.length = 0; + + options = { + get mode() { + log.push("get mode"); + return mode; + }, + get padding() { + log.push("unexpected get padding"); + return []; + } + }; + + Iterator.zipKeyed(iterables, options); + + assert.compareArray(log, [ + "get mode", + "own-keys", + ]); +} diff --git a/test/built-ins/Iterator/zipKeyed/iterables-iteration-deleted.js b/test/built-ins/Iterator/zipKeyed/iterables-iteration-deleted.js new file mode 100644 index 00000000000..4a855cb782e --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterables-iteration-deleted.js @@ -0,0 +1,51 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Deleted properties are skipped in "iterables" iteration. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 10. Let allKeys be ? iterables.[[OwnPropertyKeys]](). + 11. Let keys be a new empty List. + 12. For each element key of allKeys, do + a. Let desc be Completion(iterables.[[GetOwnProperty]](key)). + ... + c. If desc is not undefined and desc.[[Enumerable]] is true, then + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var log = []; + +var iterables = { + get a() { + log.push("get a"); + + // Delete property "b". + delete iterables.b; + + return []; + }, + get b() { + throw new Test262Error("unexpected get b"); + }, + get c() { + log.push("get c"); + + // Add new property "d". + iterables.d = null; + + return []; + }, +}; + +Iterator.zipKeyed(iterables); + +assert.compareArray(log, [ + "get a", + "get c", +]); diff --git a/test/built-ins/Iterator/zipKeyed/iterables-iteration-enumerable.js b/test/built-ins/Iterator/zipKeyed/iterables-iteration-enumerable.js new file mode 100644 index 00000000000..cddbadab7b8 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterables-iteration-enumerable.js @@ -0,0 +1,84 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Non-enumerable properties are skipped in "iterables" iteration. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 10. Let allKeys be ? iterables.[[OwnPropertyKeys]](). + 11. Let keys be a new empty List. + 12. For each element key of allKeys, do + a. Let desc be Completion(iterables.[[GetOwnProperty]](key)). + ... + c. If desc is not undefined and desc.[[Enumerable]] is true, then + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var log = []; + +var iterables = Object.create(null, { + a: { + enumerable: false, + get() { + throw new Test262Error("unexpected get a"); + } + }, + b: { + enumerable: true, + get() { + log.push("get b"); + + // Change enumerable of property "c". + Object.defineProperty(iterables, "c", { + enumerable: false, + }); + + return ['value for b']; + } + }, + c: { + enumerable: true, + configurable: true, + get() { + throw new Test262Error("unexpected get c"); + } + }, + d: { + enumerable: true, + get() { + log.push("get d"); + + // Change enumerable of property "e". + Object.defineProperty(iterables, "e", { + enumerable: true, + }); + + return ['value for d']; + } + }, + e: { + enumerable: false, + configurable: true, + get() { + log.push("get e"); + return ['value for e']; + } + }, +}); + +var result = Array.from(Iterator.zipKeyed(iterables)); + +assert.compareArray(log, [ + "get b", + "get d", + "get e", +]); + +assert.sameValue(result.length, 1); +assert.compareArray(Object.keys(result[0]), ["b", "d", "e"]); +assert.compareArray(Object.values(result[0]), ["value for b", "value for d", "value for e"]); diff --git a/test/built-ins/Iterator/zipKeyed/iterables-iteration-get-abrupt-completion.js b/test/built-ins/Iterator/zipKeyed/iterables-iteration-get-abrupt-completion.js new file mode 100644 index 00000000000..90a1176656f --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterables-iteration-get-abrupt-completion.js @@ -0,0 +1,125 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Handle abrupt completions during iterables iteration. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 12. For each element key of allKeys, do + ... + 3. If desc is not undefined and desc.[[Enumerable]] is true, then + i. Let value be Completion(Get(iterables, key)). + ii. IfAbruptCloseIterators(value, iters). + ... + + IfAbruptCloseIterators ( value, iteratorRecords ) + 1. Assert: value is a Completion Record. + 2. If value is an abrupt completion, return ? IteratorCloseAll(iteratorRecords, value). + 3. Else, set value to value.[[Value]]. + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +class ExpectedError extends Error {} + +var log = []; + +var first = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("close first iterator"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + }, +}; + +var second = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, second); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("close second iterator"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + }, +}; + +var symbol = { + next() { + log.push("unexpected call to next method"); + }, + return() { + log.push("unexpected call to return method"); + }, +}; + +var arrayIndex = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, arrayIndex); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("close array-indexed iterator"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + }, +}; + +var iterables = { + [Symbol()]: symbol, + first, + second, + get third() { + throw new ExpectedError(); + }, + 5: arrayIndex, +}; + +assert.throws(ExpectedError, function() { + Iterator.zipKeyed(iterables); +}); + +// Ensure iterators are closed in the correct order. +assert.compareArray(log, [ + "close second iterator", + "close first iterator", + "close array-indexed iterator", +]); diff --git a/test/built-ins/Iterator/zipKeyed/iterables-iteration-get-iterator-flattenable-abrupt-completion.js b/test/built-ins/Iterator/zipKeyed/iterables-iteration-get-iterator-flattenable-abrupt-completion.js new file mode 100644 index 00000000000..5c6448ca3b2 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterables-iteration-get-iterator-flattenable-abrupt-completion.js @@ -0,0 +1,147 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Handle abrupt completions during iterables iteration. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 12. For each element key of allKeys, do + ... + c. If desc is not undefined and desc.[[Enumerable]] is true, then + ... + iii. If value is not undefined, then + ... + 2. Let iter be Completion(GetIteratorFlattenable(value, reject-strings)). + 3. IfAbruptCloseIterators(iter, iters). + ... + + GetIteratorFlattenable ( obj, primitiveHandling ) + 1. If obj is not an Object, then + a. If primitiveHandling is reject-primitives, throw a TypeError exception. + b. Assert: primitiveHandling is iterate-string-primitives. + c. If obj is not a String, throw a TypeError exception. + 2. Let method be ? GetMethod(obj, %Symbol.iterator%). + 3. If method is undefined, then + a. Let iterator be obj. + 4. Else, + a. Let iterator be ? Call(method, obj). + 5. If iterator is not an Object, throw a TypeError exception. + 6. Return ? GetIteratorDirect(iterator). + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +class ExpectedError extends Error {} + +var badIterators = [ + // Throw TypeError in GetIteratorFlattenable because strings are rejected. + { + iterator: "bad iterator", + error: TypeError + }, + + // Throw an error when GetIteratorFlattenable performs GetMethod. + { + iterator: { + get [Symbol.iterator]() { + throw new ExpectedError(); + } + }, + error: ExpectedError, + }, + + // Throw an error when GetIteratorFlattenable performs Call. + { + iterator: { + [Symbol.iterator]() { + throw new ExpectedError(); + } + }, + error: ExpectedError, + }, + + // Throw an error when GetIteratorFlattenable performs GetIteratorDirect. + { + iterator: { + get next() { + throw new ExpectedError(); + } + }, + error: ExpectedError, + }, +]; + +function makeIterables(badIterator) { + var log = []; + + var first = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("close first iterator"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + }, + }; + + var second = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, second); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("close second iterator"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + }, + }; + + var iterables = {first, second, badIterator}; + + return {log, iterables}; +} + +for (var {iterator, error} of badIterators) { + var {log, iterables} = makeIterables(iterator); + + assert.throws(error, function() { + Iterator.zipKeyed(iterables); + }); + + // Ensure iterators are closed in the correct order. + assert.compareArray(log, [ + "close second iterator", + "close first iterator", + ]); +} diff --git a/test/built-ins/Iterator/zipKeyed/iterables-iteration-get-own-property-abrupt-completion.js b/test/built-ins/Iterator/zipKeyed/iterables-iteration-get-own-property-abrupt-completion.js new file mode 100644 index 00000000000..8265a917ba3 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterables-iteration-get-own-property-abrupt-completion.js @@ -0,0 +1,95 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Handle abrupt completions during iterables iteration. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 12. For each element key of allKeys, do + a. Let desc be Completion(iterables.[[GetOwnProperty]](key)). + b. IfAbruptCloseIterators(desc, iters). + ... + + IfAbruptCloseIterators ( value, iteratorRecords ) + 1. Assert: value is a Completion Record. + 2. If value is an abrupt completion, return ? IteratorCloseAll(iteratorRecords, value). + 3. Else, set value to value.[[Value]]. + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +class ExpectedError extends Error {} + +var log = []; + +var first = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("close first iterator"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + }, +}; + +var second = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, second); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("close second iterator"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + }, +}; + +var iterables = new Proxy({first, second, third: null}, { + getOwnPropertyDescriptor(target, propertyKey) { + if (propertyKey === "third") { + throw new ExpectedError(); + } + return Reflect.getOwnPropertyDescriptor(target, propertyKey); + } +}); + +assert.throws(ExpectedError, function() { + Iterator.zipKeyed(iterables); +}); + +// Ensure iterators are closed in the correct order. +assert.compareArray(log, [ + "close second iterator", + "close first iterator", +]); diff --git a/test/built-ins/Iterator/zipKeyed/iterables-iteration-inherited.js b/test/built-ins/Iterator/zipKeyed/iterables-iteration-inherited.js new file mode 100644 index 00000000000..15e49691cb2 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterables-iteration-inherited.js @@ -0,0 +1,27 @@ +// Copyright (C) 2025 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Inherited properties are skipped in "iterables" iteration. +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var parent = { + get a() { + throw new Test262Error("inherited properties should not be examined"); + }, +} + +var iterables = { + __proto__: parent, + b: ['value for b'], +}; + +var result = Array.from(Iterator.zipKeyed(iterables)); + +assert.sameValue(result.length, 1); +assert.compareArray(Object.keys(result[0]), ["b"]); +assert.compareArray(Object.values(result[0]), ["value for b"]); diff --git a/test/built-ins/Iterator/zipKeyed/iterables-iteration-symbol-key.js b/test/built-ins/Iterator/zipKeyed/iterables-iteration-symbol-key.js new file mode 100644 index 00000000000..e6b5ce911a6 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterables-iteration-symbol-key.js @@ -0,0 +1,24 @@ +// Copyright (C) 2025 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Symbol properties are used during "iterables" iteration. +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var symbolA = Symbol('a'); + +var iterables = { + [symbolA]: ['value for a'], + b: ['value for b'], +}; + +var result = Array.from(Iterator.zipKeyed(iterables)); + +assert.sameValue(result.length, 1); +assert.compareArray(Reflect.ownKeys(result[0]), ["b", symbolA]); +assert.sameValue(result[0].b, "value for b"); +assert.sameValue(result[0][symbolA], "value for a"); diff --git a/test/built-ins/Iterator/zipKeyed/iterables-iteration-undefined.js b/test/built-ins/Iterator/zipKeyed/iterables-iteration-undefined.js new file mode 100644 index 00000000000..6a7bec78ef6 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterables-iteration-undefined.js @@ -0,0 +1,22 @@ +// Copyright (C) 2025 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + undefined-valued properties are skipped in "iterables" iteration. +includes: [compareArray.js] +features: [joint-iteration] +---*/ + + +var iterables = { + a: undefined, + b: ['value for b'], +}; + +var result = Array.from(Iterator.zipKeyed(iterables)); + +assert.sameValue(result.length, 1); +assert.compareArray(Reflect.ownKeys(result[0]), ["b"]); +assert.compareArray(Object.values(result[0]), ["value for b"]); diff --git a/test/built-ins/Iterator/zipKeyed/iterables-iteration.js b/test/built-ins/Iterator/zipKeyed/iterables-iteration.js new file mode 100644 index 00000000000..78906027422 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterables-iteration.js @@ -0,0 +1,118 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Perform iteration of the "iterables" argument. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 10. Let allKeys be ? iterables.[[OwnPropertyKeys]](). + 11. Let keys be a new empty List. + 12. For each element key of allKeys, do + a. Let desc be Completion(iterables.[[GetOwnProperty]](key)). + ... + c. If desc is not undefined and desc.[[Enumerable]] is true, then + i. Let value be Completion(Get(iterables, key)). + ... + iii. If value is not undefined, then + 1. Append key to keys. + 2. Let iter be Completion(GetIteratorFlattenable(value, reject-strings)). + ... + 4. Append iter to iters. + ... +includes: [proxyTrapsHelper.js, compareArray.js] +features: [joint-iteration] +---*/ + +// Object implementing Iterator protocol, but throws when calling any Iterator methods. +var throwingIterator = { + next() { + throw new Test262Error(); + }, + return() { + throw new Test262Error(); + } +}; + +var iterableReturningThrowingIterator = { + [Symbol.iterator]() { + return throwingIterator; + } +}; + +// GetIteratorFlattenable accepts both iterables and iterators. +Iterator.zipKeyed({ + a: throwingIterator, + b: iterableReturningThrowingIterator, +}); + +// GetIteratorFlattenable rejects non-objects. +var badIterators = [ + null, + true, + "", + Symbol(), + 0, + 0n, + // undefined is handled separately +]; + +for (var iterator of badIterators) { + assert.throws(TypeError, function() { + Iterator.zipKeyed({a: iterator}); + }); +} + +// [[OwnPropertyKeys]], [[GetOwnProperty]], [[Get]], and GetIteratorFlattenable +// read properties in the correct order. +var log = []; + +function makeProxyWithGetHandler(name, obj) { + return new Proxy(obj, allowProxyTraps({ + ownKeys(target) { + log.push(`${name}::[[OwnPropertyKeys]]}`); + return Reflect.ownKeys(target); + }, + getOwnPropertyDescriptor(target, propertyKey) { + log.push(`${name}::[[GetOwnProperty]](${String(propertyKey)})`); + return Reflect.getOwnPropertyDescriptor(target, propertyKey); + }, + get(target, propertyKey, receiver) { + log.push(`${name}::[[Get]](${String(propertyKey)})`); + return Reflect.get(target, propertyKey, receiver); + }, + })); +} + +var iterables = makeProxyWithGetHandler("iterables", { + // An iterator. + a: makeProxyWithGetHandler("first", throwingIterator), + + // An iterable. + b: makeProxyWithGetHandler("second", iterableReturningThrowingIterator), + + // An object without any iteration methods. + c: makeProxyWithGetHandler("third", {}), +}); + +Iterator.zipKeyed(iterables); + +assert.compareArray(log, [ + "iterables::[[OwnPropertyKeys]]}", + + "iterables::[[GetOwnProperty]](a)", + "iterables::[[Get]](a)", + "first::[[Get]](Symbol(Symbol.iterator))", + "first::[[Get]](next)", + + "iterables::[[GetOwnProperty]](b)", + "iterables::[[Get]](b)", + "second::[[Get]](Symbol(Symbol.iterator))", + + "iterables::[[GetOwnProperty]](c)", + "iterables::[[Get]](c)", + "third::[[Get]](Symbol(Symbol.iterator))", + "third::[[Get]](next)", +]); diff --git a/test/built-ins/Iterator/zipKeyed/iterables-primitive.js b/test/built-ins/Iterator/zipKeyed/iterables-primitive.js new file mode 100644 index 00000000000..a0d6ac1f468 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterables-primitive.js @@ -0,0 +1,50 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Throws a TypeError when the "iterables" argument is not an object. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + 1. If iterables is not an Object, throw a TypeError exception. + ... +features: [joint-iteration] +---*/ + +var invalidIterables = [ + undefined, + null, + true, + "", + Symbol(), + 0, + 0n, +]; + +// Throws when the "iterables" argument is absent. +assert.throws(TypeError, function() { + Iterator.zipKeyed(); +}); + +// Throws a TypeError for invalid iterables values. +for (var iterables of invalidIterables) { + assert.throws(TypeError, function() { + Iterator.zipKeyed(iterables); + }); +} + +// Options argument not read when iterables is not an object. +var badOptions = { + get mode() { + throw new Test262Error(); + }, + get padding() { + throw new Test262Error(); + } +}; +for (var iterables of invalidIterables) { + assert.throws(TypeError, function() { + Iterator.zipKeyed(iterables, badOptions); + }); +} diff --git a/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-iterator-close-abrupt-completion.js b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-iterator-close-abrupt-completion.js new file mode 100644 index 00000000000..a1d24e423c0 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-iterator-close-abrupt-completion.js @@ -0,0 +1,116 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Handle abrupt completion from IteratorStep in IteratorZip. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + v. Let completion be Completion(Yield(results)). + vi. If completion is an abrupt completion, then + 1. Return ? IteratorCloseAll(openIters, completion). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +function ExpectedError() {} + +var log = []; + +var first = { + next() { + log.push("call first next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call first return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var second = { + next() { + log.push("call second next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, second); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call second return"); + + throw new ExpectedError(); + } +}; + +var third = { + next() { + log.push("call third next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call third return"); + + return {}; + } +}; + +var it = Iterator.zipKeyed({first, second, third}); + +it.next(); + +assert.throws(ExpectedError, function() { + it.return(); +}); + +assert.compareArray(log, [ + "call first next", + "call second next", + "call third next", + + "call third return", + "call second return", + "call first return", +]); diff --git a/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-iterator-step-value-abrupt-completion.js b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-iterator-step-value-abrupt-completion.js new file mode 100644 index 00000000000..007acbc1b52 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-iterator-step-value-abrupt-completion.js @@ -0,0 +1,145 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Handle abrupt completion from IteratorStepValue in IteratorZip. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + iii. For each integer i such that 0 ≤ i < iterCount, in ascending order, do + ... + 3. Else, + a. Let result be Completion(IteratorStepValue(iter)). + b. If result is an abrupt completion, then + i. Remove iter from openIters. + ii. Return ? IteratorCloseAll(openIters, result). + ... + d. If result is done, then + i. Remove iter from openIters. + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var modes = [ + "shortest", + "longest", + "strict", +]; + +function ExpectedError() {} + +var log = []; + +var first = { + next() { + log.push("call first next"); + throw new ExpectedError(); + }, + return() { + log.push("unexpected call first return"); + } +}; + +var second = { + next() { + log.push("unexpected call second next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, second); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call second return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var third = { + next() { + log.push("unexpected call third next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call third return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +// Empty iterator to ensure |return| is not called for closed iterators. +var empty = { + next() { + log.push("call empty next"); + return {done: true}; + }, + return() { + log.push("unexpected call empty return"); + } +}; + +for (var mode of modes) { + var it = Iterator.zipKeyed({first, second, third}, {mode}); + + assert.throws(ExpectedError, function() { + it.next(); + }); + + assert.compareArray(log, [ + "call first next", + "call third return", + "call second return", + ]); + + // Clear log. + log.length = 0; +} + +// This case applies only when mode is "longest". +var it = Iterator.zipKeyed({empty, first, second, third}, {mode: "longest"}); + +assert.throws(ExpectedError, function() { + it.next(); +}); + +assert.compareArray(log, [ + "call empty next", + "call first next", + "call third return", + "call second return", +]); diff --git a/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-longest-iterator-close-abrupt-completion.js b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-longest-iterator-close-abrupt-completion.js new file mode 100644 index 00000000000..f7d7f5d1c0c --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-longest-iterator-close-abrupt-completion.js @@ -0,0 +1,127 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Handle abrupt completion from IteratorStep in IteratorZip. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + v. Let completion be Completion(Yield(results)). + vi. If completion is an abrupt completion, then + 1. Return ? IteratorCloseAll(openIters, completion). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +function ExpectedError() {} + +var log = []; + +var first = { + next() { + log.push("call first next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call first return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var second = { + next() { + log.push("call second next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, second); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call second return"); + + throw new ExpectedError(); + } +}; + +var third = { + next() { + log.push("call third next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call third return"); + + return {}; + } +}; + +var fourth = { + next() { + log.push("call fourth next"); + return {done: true}; + }, + return() { + log.push("unexpected call fourth return"); + } +}; + +var it = Iterator.zipKeyed({first, second, third, fourth}, {mode: "longest"}); + +it.next(); + +assert.throws(ExpectedError, function() { + it.return(); +}); + +assert.compareArray(log, [ + "call first next", + "call second next", + "call third next", + "call fourth next", + + "call third return", + "call second return", + "call first return", +]); diff --git a/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-shortest-iterator-close-abrupt-completion.js b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-shortest-iterator-close-abrupt-completion.js new file mode 100644 index 00000000000..cf3b0dbedbd --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-shortest-iterator-close-abrupt-completion.js @@ -0,0 +1,125 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Handle abrupt completion from IteratorCloseAll in IteratorZip. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + iii. For each integer i such that 0 ≤ i < iterCount, in ascending order, do + ... + 3. Else, + ... + d. If result is done, then + i. Remove iter from openIters. + ii. If mode is "shortest", then + i. Return ? IteratorCloseAll(openIters, ReturnCompletion(undefined)). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +function ExpectedError() {} + +var log = []; + +var first = { + next() { + log.push("call first next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call first return"); + + return {}; + } +}; + +var second = { + next() { + log.push("call second next"); + return {done: true}; + }, + return() { + log.push("unexpected call second return"); + } +}; + +var third = { + next() { + log.push("unexpected call third next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call third return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var fourth = { + next() { + log.push("unexpected call fourth next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, fourth); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call fourth return"); + + throw new ExpectedError(); + } +}; + +var it = Iterator.zipKeyed({first, second, third, fourth}, {mode: "shortest"}); + +assert.throws(ExpectedError, function() { + it.next(); +}); + +assert.compareArray(log, [ + "call first next", + "call second next", + "call fourth return", + "call third return", + "call first return", +]); diff --git a/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-strict-iterator-close-i-is-not-zero-abrupt-completion.js b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-strict-iterator-close-i-is-not-zero-abrupt-completion.js new file mode 100644 index 00000000000..f9dbc90e7f3 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-strict-iterator-close-i-is-not-zero-abrupt-completion.js @@ -0,0 +1,108 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Handle abrupt completion from IteratorCloseAll in IteratorZip. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + iii. For each integer i such that 0 ≤ i < iterCount, in ascending order, do + ... + 3. Else, + ... + d. If result is done, then + i. Remove iter from openIters. + ... + iii. Else if mode is "strict", then + i. If i ≠ 0, then + i. Return ? IteratorCloseAll(openIters, ThrowCompletion(a newly created TypeError object)). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var log = []; + +var first = { + next() { + log.push("call first next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call first return"); + + return {}; + } +}; + +var second = { + next() { + log.push("call second next"); + return {done: true}; + }, + return() { + log.push("unexpected call second return"); + } +}; + +var third = { + next() { + log.push("unexpected call third next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call third return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var it = Iterator.zipKeyed({first, second, third}, {mode: "strict"}); + +assert.throws(TypeError, function() { + it.next(); +}); + +assert.compareArray(log, [ + "call first next", + "call second next", + "call third return", + "call first return", +]); diff --git a/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-strict-iterator-close-i-is-zero-abrupt-completion.js b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-strict-iterator-close-i-is-zero-abrupt-completion.js new file mode 100644 index 00000000000..b7998512b64 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-strict-iterator-close-i-is-zero-abrupt-completion.js @@ -0,0 +1,123 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Handle abrupt completion from IteratorCloseAll in IteratorZip. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + iii. For each integer i such that 0 ≤ i < iterCount, in ascending order, do + ... + 3. Else, + ... + d. If result is done, then + i. Remove iter from openIters. + ... + iii. Else if mode is "strict", then + ... + ii. For each integer k such that 1 ≤ k < iterCount, in ascending order, do + ... + iv. Else, + i. Return ? IteratorCloseAll(openIters, ThrowCompletion(a newly created TypeError object)). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var log = []; + +var first = { + next() { + log.push("call first next"); + return {done: true}; + }, + return() { + log.push("unexpected call first return"); + } +}; + +var second = { + next() { + log.push("call second next"); + return {done: true}; + }, + return() { + log.push("unexpected call second return"); + } +}; + +var third = { + next() { + log.push("call third next"); + return {done: false}; + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call third return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var fourth = { + next() { + log.push("unexpected call fourth next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, fourth); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call fourth return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var it = Iterator.zipKeyed({first, second, third, fourth}, {mode: "strict"}); + +assert.throws(TypeError, function() { + it.next(); +}); + +assert.compareArray(log, [ + "call first next", + "call second next", + "call third next", + "call fourth return", + "call third return", +]); diff --git a/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-strict-iterator-step-abrupt-completion.js b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-strict-iterator-step-abrupt-completion.js new file mode 100644 index 00000000000..a3e24dc983a --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration-strict-iterator-step-abrupt-completion.js @@ -0,0 +1,125 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Handle abrupt completion from IteratorStep in IteratorZip. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + iii. For each integer i such that 0 ≤ i < iterCount, in ascending order, do + ... + 3. Else, + ... + d. If result is done, then + i. Remove iter from openIters. + ... + iii. Else if mode is "strict", then + ... + ii. For each integer k such that 1 ≤ k < iterCount, in ascending order, do + ... + ii. Let open be Completion(IteratorStep(iters[k])). + iii. If open is an abrupt completion, then + i. Remove iters[k] from openIters. + ii. Return ? IteratorCloseAll(openIters, open). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +function ExpectedError() {} + +var log = []; + +var first = { + next() { + log.push("call first next"); + return {done: true}; + }, + return() { + log.push("unexpected call first return"); + } +}; + +var second = { + next() { + log.push("call second next"); + throw new ExpectedError(); + }, + return() { + log.push("unexpected call second return"); + } +}; + +var third = { + next() { + log.push("unexpected call third next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call third return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var fourth = { + next() { + log.push("unexpected call fourth next"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, fourth); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("call fourth return"); + + // IteratorClose ignores new exceptions when called with a Throw completion. + throw new Test262Error(); + } +}; + +var it = Iterator.zipKeyed({first, second, third, fourth}, {mode: "strict"}); + +assert.throws(ExpectedError, function() { + it.next(); +}); + +assert.compareArray(log, [ + "call first next", + "call second next", + "call fourth return", + "call third return", +]); diff --git a/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration.js b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration.js new file mode 100644 index 00000000000..45ff94572ab --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/iterator-zip-iteration.js @@ -0,0 +1,155 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Perform iteration in IteratorZip. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 16. Return IteratorZip(iters, mode, padding, finishResults). + + IteratorZip ( iters, mode, padding, finishResults ) + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + iii. For each integer i such that 0 ≤ i < iterCount, in ascending order, do + ... + 3. Else, + a. Let result be Completion(IteratorStepValue(iter)). + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var modes = [ + "shortest", + "longest", + "strict", +]; + +function makeIterator(log, name, elements) { + var elementsIter = elements.values(); + var iterator = { + next() { + log.push(`call ${name} next`); + + // Called with the correct receiver and no arguments. + assert.sameValue(this, iterator); + assert.sameValue(arguments.length, 0); + + var result = elementsIter.next(); + return { + get done() { + log.push(`get ${name}.result.done`); + return result.done; + }, + get value() { + log.push(`get ${name}.result.value`); + return result.value; + }, + }; + }, + return() { + log.push(`call ${name} return`); + + // Called with the correct receiver and no arguments. + assert.sameValue(this, iterator); + assert.sameValue(arguments.length, 0); + + return { + get done() { + log.push(`unexpected get ${name}.result.done`); + return result.done; + }, + get value() { + log.push(`unexpected get ${name}.result.value`); + return result.value; + }, + }; + } + }; + return iterator; +} + +for (var mode of modes) { + var log = []; + var iterables = { + first: makeIterator(log, "first", [1, 2, 3]), + second: makeIterator(log, "second", [4, 5, 6]), + third: makeIterator(log, "third", [7, 8, 9]), + }; + var it = Iterator.zipKeyed(iterables, {mode}); + + log.push("start"); + for (var v of it) { + log.push("loop"); + } + + var expected = [ + "start", + + "call first next", + "get first.result.done", + "get first.result.value", + "call second next", + "get second.result.done", + "get second.result.value", + "call third next", + "get third.result.done", + "get third.result.value", + "loop", + + "call first next", + "get first.result.done", + "get first.result.value", + "call second next", + "get second.result.done", + "get second.result.value", + "call third next", + "get third.result.done", + "get third.result.value", + "loop", + + "call first next", + "get first.result.done", + "get first.result.value", + "call second next", + "get second.result.done", + "get second.result.value", + "call third next", + "get third.result.done", + "get third.result.value", + "loop", + ]; + + switch (mode) { + case "shortest": { + expected.push( + "call first next", + "get first.result.done", + "call third return", + "call second return", + ); + break; + } + case "longest": + case "strict": { + expected.push( + "call first next", + "get first.result.done", + "call second next", + "get second.result.done", + "call third next", + "get third.result.done", + ); + break; + } + } + + assert.compareArray(log, expected); +} diff --git a/test/built-ins/Iterator/zipKeyed/length.js b/test/built-ins/Iterator/zipKeyed/length.js new file mode 100644 index 00000000000..c76a18f0b91 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/length.js @@ -0,0 +1,23 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Iterator.zipKeyed has a "length" property whose value is 1. +info: | + ECMAScript Standard Built-in Objects + + Unless otherwise specified, the length property of a built-in + Function object has the attributes { [[Writable]]: false, [[Enumerable]]: + false, [[Configurable]]: true }. +features: [joint-iteration] +includes: [propertyHelper.js] +---*/ + +verifyProperty(Iterator.zipKeyed, "length", { + value: 1, + writable: false, + enumerable: false, + configurable: true, +}); diff --git a/test/built-ins/Iterator/zipKeyed/name.js b/test/built-ins/Iterator/zipKeyed/name.js new file mode 100644 index 00000000000..387cc7becea --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/name.js @@ -0,0 +1,30 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + The "name" property of Iterator.zipKeyed +info: | + 17 ECMAScript Standard Built-in Objects + + Every built-in Function object, including constructors, that is not + identified as an anonymous function has a name property whose value is a + String. Unless otherwise specified, this value is the name that is given to + the function in this specification. + + ... + + Unless otherwise specified, the name property of a built-in Function + object, if it exists, has the attributes { [[Writable]]: false, + [[Enumerable]]: false, [[Configurable]]: true }. +features: [joint-iteration] +includes: [propertyHelper.js] +---*/ + +verifyProperty(Iterator.zipKeyed, "name", { + value: "zipKeyed", + writable: false, + enumerable: false, + configurable: true, +}); diff --git a/test/built-ins/Iterator/zipKeyed/non-constructible.js b/test/built-ins/Iterator/zipKeyed/non-constructible.js new file mode 100644 index 00000000000..0e5a43df492 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/non-constructible.js @@ -0,0 +1,17 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Iterator.zipKeyed is not constructible. + + Built-in function objects that are not identified as constructors do not + implement the [[Construct]] internal method unless otherwise specified in the + description of a particular function. +features: [joint-iteration] +---*/ + +assert.throws(TypeError, () => { + new Iterator.zipKeyed({}); +}); diff --git a/test/built-ins/Iterator/zipKeyed/options-mode.js b/test/built-ins/Iterator/zipKeyed/options-mode.js new file mode 100644 index 00000000000..2649bab6925 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/options-mode.js @@ -0,0 +1,88 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + The "mode" option must be undefined or a valid string mode. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 3. Let mode be ? Get(options, "mode"). + 4. If mode is undefined, set mode to "shortest". + 5. If mode is not one of "shortest", "longest", or "strict", throw a TypeError exception. + ... +features: [joint-iteration] +---*/ + +var validModes = [ + undefined, + "shortest", + "longest", + "strict", +]; + +var invalidModes = [ + null, + false, + "", + "short", + "long", + "loose", + Symbol(), + 123, + 123n, + {}, +]; + +// Absent "mode" option. +Iterator.zipKeyed({}, {}); + +// All valid mode values are accepted. +for (var mode of validModes) { + Iterator.zipKeyed({}, {mode}); +} + +// Throws a TypeError for invalid mode options. +for (var mode of invalidModes) { + assert.throws(TypeError, function() { + Iterator.zipKeyed({}, {mode}); + }); +} + +// "padding" option is not retrieved when "mode" option is invalid. +for (var mode of invalidModes) { + var options = { + mode, + get padding() { + throw new Test262Error(); + } + }; + assert.throws(TypeError, function() { + Iterator.zipKeyed({}, options); + }); +} + +// String wrappers are not accepted. +for (var mode of validModes) { + var options = {mode: new String(mode)}; + assert.throws(TypeError, function() { + Iterator.zipKeyed({}, options); + }); +} + +// Does not call any of `toString`, `valueOf`, `Symbol.toPrimitive`. +var badMode = { + toString() { + throw new Test262Error(); + }, + valueOf() { + throw new Test262Error(); + }, + [Symbol.toPrimitive]() { + throw new Test262Error(); + }, +}; +assert.throws(TypeError, function() { + Iterator.zipKeyed({}, {mode: badMode}); +}); diff --git a/test/built-ins/Iterator/zipKeyed/options-padding.js b/test/built-ins/Iterator/zipKeyed/options-padding.js new file mode 100644 index 00000000000..de2f52ec8d1 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/options-padding.js @@ -0,0 +1,54 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + The "padding" option must be undefined or an object. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 6. Let paddingOption be undefined. + 7. If mode is "longest", then + a. Set paddingOption to ? Get(options, "padding"). + b. If paddingOption is not undefined and paddingOption is not an Object, throw a TypeError exception. + ... +features: [joint-iteration] +---*/ + +var validPadding = [ + undefined, + {}, +]; + +var invalidPadding = [ + null, + false, + "", + Symbol(), + 123, + 123n, +]; + +// Absent "padding" option. +Iterator.zipKeyed({}, {mode: "longest"}); + +// All valid padding values are accepted. +for (var padding of validPadding) { + Iterator.zipKeyed({}, {mode: "longest", padding}); +} + +// Throws a TypeError for invalid padding options. +for (var padding of invalidPadding) { + assert.throws(TypeError, function() { + Iterator.zipKeyed({}, {mode: "longest", padding}); + }); +} + +// Invalid padding options are okay when mode is not "longest" because the padding option is not read. +for (var padding of invalidPadding) { + Iterator.zipKeyed({}, {padding}); + Iterator.zipKeyed({}, {mode: undefined, padding}); + Iterator.zipKeyed({}, {mode: "shortest", padding}); + Iterator.zipKeyed({}, {mode: "strict", padding}); +} diff --git a/test/built-ins/Iterator/zipKeyed/options.js b/test/built-ins/Iterator/zipKeyed/options.js new file mode 100644 index 00000000000..ac518b4520f --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/options.js @@ -0,0 +1,50 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + The "options" argument can either be undefined or an object. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 2. Set options to ? GetOptionsObject(options). + ... + + GetOptionsObject ( options ) + 1. If options is undefined, then + a. Return OrdinaryObjectCreate(null). + 2. If options is an Object, then + a. Return options. + 3. Throw a TypeError exception. +features: [joint-iteration] +---*/ + +var validOptions = [ + undefined, + {}, +]; + +var invalidOptions = [ + null, + true, + "", + Symbol(), + 0, + 0n, +]; + +// The "options" argument can also be absent. +Iterator.zipKeyed({}); + +// All valid option values are accepted. +for (var options of validOptions) { + Iterator.zipKeyed({}, options); +} + +// Throws a TypeError for invalid option values. +for (var options of invalidOptions) { + assert.throws(TypeError, function() { + Iterator.zipKeyed({}, options); + }); +} diff --git a/test/built-ins/Iterator/zipKeyed/padding-iteration-get-abrupt-completion.js b/test/built-ins/Iterator/zipKeyed/padding-iteration-get-abrupt-completion.js new file mode 100644 index 00000000000..e5b73f7581c --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/padding-iteration-get-abrupt-completion.js @@ -0,0 +1,126 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Abrupt completion for Get in "padding" option iteration. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 14. If mode is "longest", then + ... + b. Else, + i. For each element key of keys, do + 1. Let value be Completion(Get(paddingOption, key)). + 2. IfAbruptCloseIterators(value, iters). + ... + + IfAbruptCloseIterators ( value, iteratorRecords ) + 1. Assert: value is a Completion Record. + 2. If value is an abrupt completion, return ? IteratorCloseAll(iteratorRecords, value). + 3. Else, set value to value.[[Value]]. + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + 1. Assert: iteratorRecord.[[Iterator]] is an Object. + 2. Let iterator be iteratorRecord.[[Iterator]]. + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + 5. If completion is a throw completion, return ? completion. + ... +includes: [compareArray.js] +features: [joint-iteration] +---*/ + +var log = []; + +var first = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, first); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("first return"); + + // This exception is ignored. + throw new Test262Error(); + } +}; + +var second = { + next() { + log.push("unexpected call to next method"); + }, + return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, second); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("second return"); + + // This exception is ignored. + throw new Test262Error(); + } +}; + +var third = { + next() { + log.push("unexpected call to next method"); + }, + get return() { + // Called with the correct receiver and no arguments. + assert.sameValue(this, third); + assert.sameValue(arguments.length, 0); + + // NB: Log after above asserts, because failures aren't propagated. + log.push("third return"); + + // This exception is ignored. + throw new Test262Error(); + } +}; + +function ExpectedError() {} + +// Padding object throws from |get second|. +var padding = { + get first() { + log.push("padding first"); + }, + get second() { + log.push("padding second"); + throw new ExpectedError(); + }, + get third() { + log.push("unexpected padding third"); + }, +}; + +assert.throws(ExpectedError, function() { + Iterator.zipKeyed({ + first, + second, + third + }, {mode: "longest", padding}); +}); + +assert.compareArray(log, [ + "padding first", + "padding second", + "third return", + "second return", + "first return", +]); diff --git a/test/built-ins/Iterator/zipKeyed/padding-iteration.js b/test/built-ins/Iterator/zipKeyed/padding-iteration.js new file mode 100644 index 00000000000..5062f787082 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/padding-iteration.js @@ -0,0 +1,55 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Perform keys iteration on the "padding" option. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 14. If mode is "longest", then + ... + b. Else, + i. For each element key of keys, do + 1. Let value be Completion(Get(paddingOption, key)). + ... +includes: [proxyTrapsHelper.js, compareArray.js] +features: [joint-iteration] +---*/ + +function makeKeys(k) { + var str = "abcdefgh"; + assert(k <= str.length, "more than eight keys are unsupported"); + return str.slice(0, k).split(""); +} + +function fromKeys(keys, value) { + return Object.fromEntries(keys.map(function(k) { + return [k, value]; + })); +} + +for (var n = 0; n <= 5; ++n) { + // Create an object with |n| properties. + var keys = makeKeys(n); + var iterables = fromKeys(keys, []); + + for (var k = 0; k <= n + 2; ++k) { + var log = []; + + // Create a padding object with |k| properties. Ensure only [[Get]] is + // called. + var padding = new Proxy(fromKeys(makeKeys(k), undefined), allowProxyTraps({ + get(target, propertyKey, receiver) { + log.push(propertyKey); + return Reflect.get(target, propertyKey, receiver); + }, + })); + + Iterator.zipKeyed(iterables, {mode: "longest", padding}); + + // [[Get]] happened for all keys from |keys|. + assert.compareArray(log, keys); + } +} diff --git a/test/built-ins/Iterator/zipKeyed/prop-desc.js b/test/built-ins/Iterator/zipKeyed/prop-desc.js new file mode 100644 index 00000000000..43328f59c0a --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/prop-desc.js @@ -0,0 +1,23 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Property descriptor of Iterator.zipKeyed +info: | + 17 ECMAScript Standard Built-in Objects + + Every other data property described in clauses 18 through 26 and in Annex B.2 + has the attributes { [[Writable]]: true, [[Enumerable]]: false, + [[Configurable]]: true } unless otherwise specified. +features: [joint-iteration] +includes: [propertyHelper.js] +---*/ + +verifyProperty(Iterator, "zipKeyed", { + value: Iterator.zipKeyed, + writable: true, + enumerable: false, + configurable: true, +}); diff --git a/test/built-ins/Iterator/zipKeyed/proto.js b/test/built-ins/Iterator/zipKeyed/proto.js new file mode 100644 index 00000000000..6cfc0ad12cf --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/proto.js @@ -0,0 +1,16 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + The value of the [[Prototype]] internal slot of Iterator.zipKeyed is the + intrinsic object %FunctionPrototype%. +features: [joint-iteration] +---*/ + +assert.sameValue( + Object.getPrototypeOf(Iterator.zipKeyed), + Function.prototype, + "Object.getPrototypeOf(Iterator.zipKeyed) must return the value of Function.prototype" +); diff --git a/test/built-ins/Iterator/zipKeyed/result-is-iterator.js b/test/built-ins/Iterator/zipKeyed/result-is-iterator.js new file mode 100644 index 00000000000..220f4182734 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/result-is-iterator.js @@ -0,0 +1,20 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + The value of the [[Prototype]] internal slot of the return value of Iterator.zipKeyed + is the intrinsic object %IteratorHelperPrototype%. +includes: [wellKnownIntrinsicObjects.js] +features: [joint-iteration] +---*/ + +var iter = Iterator.zipKeyed({}); +assert(iter instanceof Iterator, "Iterator.zipKeyed({}) must return an Iterator"); + +assert.sameValue( + Object.getPrototypeOf(iter), + getWellKnownIntrinsicObject("%IteratorHelperPrototype%"), + "[[Prototype]] is %IteratorHelperPrototype%" +); diff --git a/test/built-ins/Iterator/zipKeyed/results-object-from-array.js b/test/built-ins/Iterator/zipKeyed/results-object-from-array.js new file mode 100644 index 00000000000..107f947c549 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/results-object-from-array.js @@ -0,0 +1,51 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Calling Iterator.zipKeyed with an array object. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 15. Let finishResults be a new Abstract Closure with parameters (results) that captures keys and iterCount and performs the following steps when called: + a. Let obj be OrdinaryObjectCreate(null). + b. For each integer i such that 0 ≤ i < iterCount, in ascending order, do + i. Perform ! CreateDataPropertyOrThrow(obj, keys[i], results[i]). + c. Return obj. + ... +features: [joint-iteration] +---*/ + +var iterables = [ + [1, 2, 3], + [4, 5, 6], +]; + +var it = Iterator.zipKeyed(iterables); + +for (var i = 0; i < iterables[0].length; ++i) { + var results = it.next().value; + + assert.sameValue( + Object.getPrototypeOf(results), + null, + "results prototype is null" + ); + + assert.sameValue( + Reflect.ownKeys(results).length, + iterables.length, + "results has correct number of properties" + ); + + for (var j = 0; j < iterables.length; ++j) { + assert.sameValue( + results[j], + iterables[j][i], + "results property value has the correct value" + ); + } +} + +assert.sameValue(it.next().done, true, "iterator is exhausted"); diff --git a/test/built-ins/Iterator/zipKeyed/results-object-has-default-attributes.js b/test/built-ins/Iterator/zipKeyed/results-object-has-default-attributes.js new file mode 100644 index 00000000000..e25cfa55832 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/results-object-has-default-attributes.js @@ -0,0 +1,106 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Returned object has the correct prototype and default property attributes. +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 15. Let finishResults be a new Abstract Closure with parameters (results) that captures keys and iterCount and performs the following steps when called: + a. Let obj be OrdinaryObjectCreate(null). + b. For each integer i such that 0 ≤ i < iterCount, in ascending order, do + i. Perform ! CreateDataPropertyOrThrow(obj, keys[i], results[i]). + c. Return obj. + ... +includes: [compareArray.js, propertyHelper.js] +features: [joint-iteration] +---*/ + +// Assert |actual| is a plain object equal to |expected| with default property attributes. +function assertPlainObject(actual, expected) { + assert.sameValue( + Object.getPrototypeOf(actual), + null, + "[[Prototype]] of actual is null" + ); + + assert(Object.isExtensible(actual), "actual is extensible"); + + var actualKeys = Reflect.ownKeys(actual); + var expectedKeys = Reflect.ownKeys(expected); + + // All expected property keys are present. + assert.compareArray(actualKeys, expectedKeys); + + // All expected property values are equal. + for (var key of expectedKeys) { + assert.sameValue(actual[key], expected[key], "with key: " + String(key)); + } + + // Ensure all properties have the default property attributes. + for (var key of expectedKeys) { + verifyProperty(actual, key, { + writable: true, + enumerable: true, + configurable: true, + }); + } +} + +var iterables = Object.create(null, { + a: { + writable: true, + enumerable: true, + configurable: true, + value: ["A"], + }, + b: { + writable: false, + enumerable: true, + configurable: true, + value: ["B"], + }, + c: { + writable: true, + enumerable: true, + configurable: false, + value: ["C"], + }, + d: { + writable: false, + enumerable: true, + configurable: false, + value: ["D"], + }, + e: { + enumerable: true, + configurable: true, + get() { + return ["E"]; + } + }, + f: { + enumerable: true, + configurable: false, + get() { + return ["F"]; + } + }, +}); + +var it = Iterator.zipKeyed(iterables); + +var results = it.next().value; + +assertPlainObject(results, { + a: "A", + b: "B", + c: "C", + d: "D", + e: "E", + f: "F", +}); + +assert.sameValue(it.next().done, true, "iterator is exhausted"); diff --git a/test/built-ins/Iterator/zipKeyed/results-object-has-no-undefined-iterables-properties.js b/test/built-ins/Iterator/zipKeyed/results-object-has-no-undefined-iterables-properties.js new file mode 100644 index 00000000000..8889ff2baee --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/results-object-has-no-undefined-iterables-properties.js @@ -0,0 +1,33 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Undefined properties from the "iterables" object are not present in the results object +info: | + Iterator.zipKeyed ( iterables [ , options ] ) + ... + 12. For each element key of allKeys, do + a. Let desc be Completion(iterables.[[GetOwnProperty]](key)). + b. IfAbruptCloseIterators(desc, iters). + c. If desc is not undefined and desc.[[Enumerable]] is true, then + ... +features: [joint-iteration] +---*/ + +var iterables = { + a: ["A"], + b: undefined, + c: ["C"], +}; + +var it = Iterator.zipKeyed(iterables); + +var results = it.next().value; + +assert.sameValue("a" in results, true, "property 'a' is present"); +assert.sameValue("b" in results, false, "property 'b' is not present"); +assert.sameValue("c" in results, true, "property 'c' is present"); + +assert.sameValue(it.next().done, true, "iterator is exhausted"); diff --git a/test/built-ins/Iterator/zipKeyed/suspended-start-iterator-close-calls-next.js b/test/built-ins/Iterator/zipKeyed/suspended-start-iterator-close-calls-next.js new file mode 100644 index 00000000000..56032a50a9a --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/suspended-start-iterator-close-calls-next.js @@ -0,0 +1,78 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Generator is closed from suspended-start state and IteratorClose calls next. +info: | + %IteratorHelperPrototype%.return ( ) + ... + 4. If O.[[GeneratorState]] is suspended-start, then + a. Set O.[[GeneratorState]] to completed. + ... + c. Perform ? IteratorCloseAll(O.[[UnderlyingIterators]], ReturnCompletion(undefined)). + d. Return CreateIteratorResultObject(undefined, true). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + ... + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + ... + + %IteratorHelperPrototype%.next ( ) + 1. Return ? GeneratorResume(this value, undefined, "Iterator Helper"). + + GeneratorResume ( generator, value, generatorBrand ) + 1. Let state be ? GeneratorValidate(generator, generatorBrand). + 2. If state is completed, return CreateIteratorResultObject(undefined, true). + ... + + GeneratorValidate ( generator, generatorBrand ) + ... + 5. Let state be generator.[[GeneratorState]]. + 6. If state is executing, throw a TypeError exception. + 7. Return state. +features: [joint-iteration] +---*/ + +var returnCallCount = 0; + +var underlying = { + next() { + throw new Test262Error("Unexpected call to next"); + }, + return() { + returnCallCount += 1; + + // The generator state is already set to "completed". The generator state is + // not "executing", so `GeneratorValidate` succeeds and `GeneratorResume` + // returns with `CreateIteratorResultObject()`. + var result = it.next(); + assert.sameValue(result.value, undefined); + assert.sameValue(result.done, true); + + return {}; + }, +}; + +var it = Iterator.zipKeyed({a: underlying}); + +assert.sameValue(returnCallCount, 0); + +// This `return()` call sets the generator state to "completed" and then calls +// `IteratorClose()`. +var result = it.return(); +assert.sameValue(result.value, undefined); +assert.sameValue(result.done, true); + +assert.sameValue(returnCallCount, 1); diff --git a/test/built-ins/Iterator/zipKeyed/suspended-start-iterator-close-calls-return.js b/test/built-ins/Iterator/zipKeyed/suspended-start-iterator-close-calls-return.js new file mode 100644 index 00000000000..938786dc792 --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/suspended-start-iterator-close-calls-return.js @@ -0,0 +1,82 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Generator is closed from suspended-start state and IteratorClose calls return. +info: | + %IteratorHelperPrototype%.return ( ) + ... + 4. If O.[[GeneratorState]] is suspended-start, then + a. Set O.[[GeneratorState]] to completed. + ... + c. Perform ? IteratorCloseAll(O.[[UnderlyingIterators]], ReturnCompletion(undefined)). + d. Return CreateIteratorResultObject(undefined, true). + 5. Let C be ReturnCompletion(undefined). + 6. Return ? GeneratorResumeAbrupt(O, C, "Iterator Helper"). + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + ... + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + ... + 8. Return ? completion. + + GeneratorResumeAbrupt ( generator, abruptCompletion, generatorBrand ) + 1. Let state be ? GeneratorValidate(generator, generatorBrand). + 2. If state is suspended-start, then + ... + 3. If state is completed, then + a. If abruptCompletion is a return completion, then + i. Return CreateIteratorResultObject(abruptCompletion.[[Value]], true). + ... + + GeneratorValidate ( generator, generatorBrand ) + ... + 5. Let state be generator.[[GeneratorState]]. + 6. If state is executing, throw a TypeError exception. + 7. Return state. +features: [joint-iteration] +---*/ + +var returnCallCount = 0; + +var underlying = { + next() { + throw new Test262Error("Unexpected call to next"); + }, + return() { + returnCallCount += 1; + + // The generator state is already set to "completed", so this `return()` + // call proceeds to `GeneratorResumeAbrupt`. The generator state is not + // "executing", so `GeneratorValidate` succeeds and `GeneratorResumeAbrupt` + // returns with `CreateIteratorResultObject()`. + var result = it.return(); + assert.sameValue(result.value, undefined); + assert.sameValue(result.done, true); + + return {}; + }, +}; + +var it = Iterator.zipKeyed({a: underlying}); + +assert.sameValue(returnCallCount, 0); + +// This `return()` call sets the generator state to "completed" and then calls +// `IteratorClose()`. +var result = it.return(); +assert.sameValue(result.value, undefined); +assert.sameValue(result.done, true); + +assert.sameValue(returnCallCount, 1); diff --git a/test/built-ins/Iterator/zipKeyed/suspended-yield-iterator-close-calls-next.js b/test/built-ins/Iterator/zipKeyed/suspended-yield-iterator-close-calls-next.js new file mode 100644 index 00000000000..b825a2f3e0b --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/suspended-yield-iterator-close-calls-next.js @@ -0,0 +1,101 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Generator is closed from suspended-yield state and IteratorClose calls next. +info: | + %IteratorHelperPrototype%.return ( ) + ... + 5. Let C be ReturnCompletion(undefined). + 6. Return ? GeneratorResumeAbrupt(O, C, "Iterator Helper"). + + GeneratorResumeAbrupt ( generator, abruptCompletion, generatorBrand ) + 1. Let state be ? GeneratorValidate(generator, generatorBrand). + ... + 4. Assert: state is suspended-yield. + ... + 8. Set generator.[[GeneratorState]] to executing. + ... + 10. Resume the suspended evaluation of genContext using abruptCompletion as + the result of the operation that suspended it. Let result be the + Completion Record returned by the resumed computation. + ... + + GeneratorValidate ( generator, generatorBrand ) + ... + 5. Let state be generator.[[GeneratorState]]. + 6. If state is executing, throw a TypeError exception. + 7. Return state. + + IteratorZip ( iters, mode, padding, finishResults ) + ... + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + v. Let completion be Completion(Yield(results)). + vi. If completion is an abrupt completion, then + 1. Return ? IteratorCloseAll(openIters, completion). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + ... + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + ... + + %IteratorHelperPrototype%.next ( ) + 1. Return ? GeneratorResume(this value, undefined, "Iterator Helper"). + + GeneratorResume ( generator, value, generatorBrand ) + 1. Let state be ? GeneratorValidate(generator, generatorBrand). + ... +features: [joint-iteration] +---*/ + +var returnCallCount = 0; + +var underlying = { + next() { + return {value: 123, done: false}; + }, + return() { + returnCallCount += 1; + + // The generator state is set to "executing", so this `next()` call throws + // a TypeError when `GeneratorResume` performs `GeneratorValidate`. + assert.throws(TypeError, function() { + it.next(); + }); + + return {}; + }, +}; + +var it = Iterator.zipKeyed({a: underlying}); + +// Move generator into "suspended-yield" state. +var result = it.next(); +assert.sameValue(result.value.a, 123); +assert.sameValue(result.done, false); + +assert.sameValue(returnCallCount, 0); + +// This `return()` call continues execution in the suspended generator. +var result = it.return(); +assert.sameValue(result.value, undefined); +assert.sameValue(result.done, true); + +assert.sameValue(returnCallCount, 1); diff --git a/test/built-ins/Iterator/zipKeyed/suspended-yield-iterator-close-calls-return.js b/test/built-ins/Iterator/zipKeyed/suspended-yield-iterator-close-calls-return.js new file mode 100644 index 00000000000..e58f2cf0cfe --- /dev/null +++ b/test/built-ins/Iterator/zipKeyed/suspended-yield-iterator-close-calls-return.js @@ -0,0 +1,94 @@ +// Copyright (C) 2025 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-iterator.zipkeyed +description: > + Generator is closed from suspended-yield state and IteratorClose calls return. +info: | + %IteratorHelperPrototype%.return ( ) + ... + 5. Let C be ReturnCompletion(undefined). + 6. Return ? GeneratorResumeAbrupt(O, C, "Iterator Helper"). + + GeneratorResumeAbrupt ( generator, abruptCompletion, generatorBrand ) + 1. Let state be ? GeneratorValidate(generator, generatorBrand). + ... + 4. Assert: state is suspended-yield. + ... + 8. Set generator.[[GeneratorState]] to executing. + ... + 10. Resume the suspended evaluation of genContext using abruptCompletion as + the result of the operation that suspended it. Let result be the + Completion Record returned by the resumed computation. + ... + + GeneratorValidate ( generator, generatorBrand ) + ... + 5. Let state be generator.[[GeneratorState]]. + 6. If state is executing, throw a TypeError exception. + 7. Return state. + + IteratorZip ( iters, mode, padding, finishResults ) + ... + 3. Let closure be a new Abstract Closure with no parameters that captures + iters, iterCount, openIters, mode, padding, and finishResults, and + performs the following steps when called: + ... + b. Repeat, + ... + v. Let completion be Completion(Yield(results)). + vi. If completion is an abrupt completion, then + 1. Return ? IteratorCloseAll(openIters, completion). + ... + + IteratorCloseAll ( iters, completion ) + 1. For each element iter of iters, in reverse List order, do + a. Set completion to Completion(IteratorClose(iter, completion)). + 2. Return ? completion. + + IteratorClose ( iteratorRecord, completion ) + ... + 3. Let innerResult be Completion(GetMethod(iterator, "return")). + 4. If innerResult is a normal completion, then + a. Let return be innerResult.[[Value]]. + b. If return is undefined, return ? completion. + c. Set innerResult to Completion(Call(return, iterator)). + ... +features: [joint-iteration] +---*/ + +var returnCallCount = 0; + +var underlying = { + next() { + return {value: 123, done: false}; + }, + return() { + returnCallCount += 1; + + // The generator state is set to "executing", so this `return()` call throws + // a TypeError when `GeneratorResumeAbrupt` performs `GeneratorValidate`. + assert.throws(TypeError, function() { + it.return(); + }); + + return {}; + }, +}; + +var it = Iterator.zipKeyed({a: underlying}); + +// Move generator into "suspended-yield" state. +var result = it.next(); +assert.sameValue(result.value.a, 123); +assert.sameValue(result.done, false); + +assert.sameValue(returnCallCount, 0); + +// This `return()` call continues execution in the suspended generator. +var result = it.return(); +assert.sameValue(result.value, undefined); +assert.sameValue(result.done, true); + +assert.sameValue(returnCallCount, 1);