diff --git a/docs/built-ins.md b/docs/built-ins.md index d8920672..4336c895 100644 --- a/docs/built-ins.md +++ b/docs/built-ins.md @@ -389,7 +389,7 @@ ErrorName: message ### Iterator (`Goccia.Values.IteratorValue.pas`, `Iterator.Concrete.pas`, `Iterator.Lazy.pas`, `Iterator.Concat.pas`, `Iterator.Generic.pas`) -Implements the [ECMAScript Iterator Helpers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator), Iterator Sequencing (`Iterator.concat`), and the Stage 3 Joint Iteration proposal (`Iterator.zip`). +Implements the [ECMAScript Iterator Helpers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator), Iterator Sequencing (`Iterator.concat`), the Stage 3 Joint Iteration proposal (`Iterator.zip`), and the Stage 3 [Iterator Includes](https://github.com/tc39/proposal-iterator-includes) proposal. All built-in iterators (Array, String, Map, Set) share a common `Iterator.prototype` with helper methods per the ECMAScript Iterator Helpers proposal: @@ -408,8 +408,9 @@ All built-in iterators (Array, String, Map, Set) share a common `Iterator.protot | `iter.some(callback)` | Test if any value matches | | `iter.every(callback)` | Test if all values match | | `iter.find(callback)` | Find first matching value | +| `iter.includes(searchElement, skippedElements?)` | Consume the iterator until a value matches by SameValueZero, optionally ignoring an integral Number from zero through `Number.MAX_SAFE_INTEGER`, or positive `Infinity`, without coercion | -**Lazy helpers:** `map`, `filter`, `take`, `drop`, and `flatMap` return new lazy iterators that advance the source on-demand — they do not eagerly consume the underlying iterator. Consuming methods (`forEach`, `reduce`, `toArray`, `some`, `every`, `find`) do consume the iterator. +**Lazy helpers:** `map`, `filter`, `take`, `drop`, and `flatMap` return new lazy iterators that advance the source on-demand — they do not eagerly consume the underlying iterator. Consuming methods (`forEach`, `reduce`, `toArray`, `some`, `every`, `find`, `includes`) do consume the iterator. `includes` closes the source when it finds a match, but leaves a normally exhausted source closed by exhaustion alone. Iterators are consumed once — calling `next()` past the end returns `{value: undefined, done: true}` forever. Spread (`[...iter]`), destructuring, and `Array.from()` all consume iterators via the `[Symbol.iterator]` protocol. diff --git a/docs/language-tables.md b/docs/language-tables.md index d7027445..4a6417f2 100644 --- a/docs/language-tables.md +++ b/docs/language-tables.md @@ -108,6 +108,7 @@ APIs from WHATWG and W3C specifications — not part of ECMA-262, but widely exp | [Decorators](https://github.com/tc39/proposal-decorators) | 3 | Supported — class, method, field, getter/setter, auto-accessor decorators with `addInitializer` | | [Decorator Metadata](https://github.com/tc39/proposal-decorator-metadata) | 3 | Supported — `Symbol.metadata` for decorator-attached class metadata with inheritance | | [Import Defer](https://tc39.es/proposal-defer-import-eval/) | 3 | Supported — static `import defer * as ns` and dynamic `import.defer()` create deferred namespace objects | +| [Iterator Includes](https://github.com/tc39/proposal-iterator-includes) | 3 | Supported — `Iterator.prototype.includes(searchElement, skippedElements?)` with SameValueZero comparison and iterator closing | | [Joint Iteration](https://github.com/tc39/proposal-joint-iteration) | 3 | Supported — `Iterator.zip` and `Iterator.zipKeyed` | | [Import Bytes](https://github.com/tc39/proposal-import-bytes) | 2.7 | Supported — `import x from "./f" with { type: "bytes" }` (static, dynamic, `import.defer`) yields a default-only `Uint8Array` over an immutable `ArrayBuffer` | | [Immutable ArrayBuffers](https://github.com/tc39/proposal-immutable-arraybuffer) | 2.7 | Supported — `ArrayBuffer.prototype.transferToImmutable` plus the `immutable` getter; writes to immutable-backed views are rejected | diff --git a/docs/language.md b/docs/language.md index 66c3c178..b9a8894e 100644 --- a/docs/language.md +++ b/docs/language.md @@ -7,7 +7,7 @@ - **Recommended defaults** — Modern, explicit ECMAScript: `let`/`const`, arrow functions, classes with private fields, `for...of`, async/await, ES modules; this profile is product policy rather than the engine's language ceiling - **Conformance objective** — Core ECMAScript compatibility is a release-track objective, measured by generated test262 reports; Annex B's browser-only legacy surface is deferred until any future Web API/browser-compatibility profile -- **TC39 proposals** — Decorators, decorator metadata, pattern matching, types as comments, enums, `Math.clamp` +- **TC39 proposals** — Decorators, decorator metadata, Iterator Includes, pattern matching, types as comments, enums, `Math.clamp` - **Excluded by design** — Runtime `eval` outside private conformance host hooks - **Parser policy** — Parser-recognized excluded or disabled syntax (`var`, `function`, `==`/`!=`, labels, `while`/`do...while`, `with`, traditional `for(;;)`, `for...in`) is a `SyntaxError` by default. `--warning-unsupported-features` / `"warning-unsupported-features": true` restores warning plus recovery behavior for migration and diagnostics without enabling real semantics. - **Compatibility flags** — `--compat-*` flags primarily exist for ECMAScript conformance and legacy semantic requirements. Userland code should usually prefer default forms instead of enabling ASI (`--compat-asi`), `var` (`--compat-var`), `function` (`--compat-function`), implicit `arguments` (`--compat-arguments-object`), non-strict Script compatibility (`--compat-non-strict-mode`), loose equality (`--compat-loose-equality`), labels (`--compat-label`), traditional loops (`--compat-traditional-for-loop`), `for...in` (`--compat-for-in-loop`), or `while`/`do...while` (`--compat-while-loops`) preemptively. @@ -921,7 +921,7 @@ With `--compat-function` and `--compat-non-strict-mode`, sloppy labeled function ### Generators and Iterators -Generator method shorthand (`*method()` and `async *method()`) is supported by default. Generator function syntax (`function*` and `async function*`) is supported only when `--compat-function` is enabled. Iterator protocol and Iterator Helpers are also implemented. +Generator method shorthand (`*method()` and `async *method()`) is supported by default. Generator function syntax (`function*` and `async function*`) is supported only when `--compat-function` is enabled. Iterator protocol and Iterator Helpers are also implemented, including the Stage 3 [Iterator Includes](https://github.com/tc39/proposal-iterator-includes) consuming search method. `Iterator.prototype.includes(searchElement, skippedElements?)` compares with SameValueZero; `skippedElements` must be an integral Number from zero through `Number.MAX_SAFE_INTEGER`, or positive `Infinity`, and is not coerced. Finding a match closes the iterator, while normal exhaustion does not perform an additional close. ## Intentional Divergences from ECMAScript diff --git a/source/units/Goccia.Error.Messages.pas b/source/units/Goccia.Error.Messages.pas index a570eb04..ff4775ce 100644 --- a/source/units/Goccia.Error.Messages.pas +++ b/source/units/Goccia.Error.Messages.pas @@ -88,6 +88,7 @@ interface SErrorIteratorSomeNonIterator = 'Iterator.prototype.some called on non-iterator'; SErrorIteratorEveryNonIterator = 'Iterator.prototype.every called on non-iterator'; SErrorIteratorFindNonIterator = 'Iterator.prototype.find called on non-iterator'; + SErrorIteratorIncludesNonIterator = 'Iterator.prototype.includes called on non-iterator'; // Iterator helper errors — argument validation SErrorIteratorMapCallable = 'Iterator.prototype.map requires a callable argument'; @@ -98,6 +99,9 @@ interface SErrorIteratorSomeCallable = 'Iterator.prototype.some requires a callable argument'; SErrorIteratorEveryCallable = 'Iterator.prototype.every requires a callable argument'; SErrorIteratorFindCallable = 'Iterator.prototype.find requires a callable argument'; + SErrorIteratorIncludesSkippedElementsNumber = 'Iterator.prototype.includes skippedElements must be an integral Number'; + SErrorIteratorIncludesSkippedElementsNegative = 'Iterator.prototype.includes skippedElements must not be negative'; + SErrorIteratorIncludesSkippedElementsTooLarge = 'Iterator.prototype.includes skippedElements must not exceed Number.MAX_SAFE_INTEGER'; SErrorIteratorTakeRequiresArg = 'Iterator.prototype.take requires an argument'; SErrorIteratorDropRequiresArg = 'Iterator.prototype.drop requires an argument'; SErrorIteratorTakeNonNegative = 'Iterator.prototype.take argument must be non-negative'; diff --git a/source/units/Goccia.Error.Suggestions.pas b/source/units/Goccia.Error.Suggestions.pas index 55af1dda..5d1cec89 100644 --- a/source/units/Goccia.Error.Suggestions.pas +++ b/source/units/Goccia.Error.Suggestions.pas @@ -212,6 +212,7 @@ interface SSuggestIteratorCallable = 'pass a function as the first argument'; SSuggestIteratorFlatMapCallable = 'pass a function that returns an iterable'; SSuggestIteratorNonNegative = 'pass a non-negative integer as the argument'; + SSuggestIteratorIncludesSkippedElements = 'pass an integral Number from 0 through Number.MAX_SAFE_INTEGER, or Infinity'; SSuggestIteratorFromArg = 'pass an array, string, set, map, or object with Symbol.iterator or a next() method'; SSuggestIteratorZipOptions = 'pass an options object with mode and/or padding properties'; SSuggestIteratorZipKeyedArg = 'pass an object whose values are iterables'; diff --git a/source/units/Goccia.Spec.pas b/source/units/Goccia.Spec.pas index 367dae70..e17a5087 100644 --- a/source/units/Goccia.Spec.pas +++ b/source/units/Goccia.Spec.pas @@ -218,9 +218,10 @@ implementation // --------------------------------------------------------------------------- // TC39 Stage 3 // --------------------------------------------------------------------------- - STAGE3_PROPOSALS: array[0..2] of TGocciaFeatureEntry = ( + STAGE3_PROPOSALS: array[0..3] of TGocciaFeatureEntry = ( (Name: 'Decorators'; Link: 'https://github.com/tc39/proposal-decorators'), (Name: 'Decorator Metadata'; Link: 'https://github.com/tc39/proposal-decorator-metadata'), + (Name: 'Iterator Includes'; Link: 'https://github.com/tc39/proposal-iterator-includes'), (Name: 'Joint Iteration'; Link: 'https://github.com/tc39/proposal-joint-iteration') ); diff --git a/source/units/Goccia.Values.IteratorValue.pas b/source/units/Goccia.Values.IteratorValue.pas index d1c5f1b2..c978c08d 100644 --- a/source/units/Goccia.Values.IteratorValue.pas +++ b/source/units/Goccia.Values.IteratorValue.pas @@ -36,6 +36,7 @@ TGocciaIteratorValue = class(TGocciaObjectValue) function IteratorSome(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; function IteratorEvery(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; function IteratorFind(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; + function IteratorIncludes(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; function IteratorFlatMap(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; function IteratorFrom(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; function IteratorConcat(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; @@ -99,9 +100,12 @@ procedure PreserveCurrentExceptionAcrossNestedHandler; implementation uses + Math, SysUtils, + Goccia.Arithmetic, Goccia.Constants.ConstructorNames, + Goccia.Constants.NumericLimits, Goccia.Constants.PropertyNames, Goccia.Error.Messages, Goccia.Error.Suggestions, @@ -563,6 +567,53 @@ procedure RemoveTempRootIfNeeded( GC.RemoveTempRoot(AValue); end; +procedure CloseDirectIterator(const AIteratorObject: TGocciaValue); +var + CallArgs: TGocciaArgumentsCollection; + ReturnMethod, ReturnResult: TGocciaValue; + IteratorWasRooted, ReturnMethodWasRooted: Boolean; +begin + IteratorWasRooted := AddTempRootIfNeeded(AIteratorObject); + try + ReturnMethod := TGocciaObjectValue(AIteratorObject).GetProperty(PROP_RETURN); + if not Assigned(ReturnMethod) or + (ReturnMethod is TGocciaUndefinedLiteralValue) or + (ReturnMethod is TGocciaNullLiteralValue) then + Exit; + if not ReturnMethod.IsCallable then + ThrowTypeError(SErrorIteratorReturnMustBeCallable, + SSuggestIteratorProtocol); + + ReturnMethodWasRooted := AddTempRootIfNeeded(ReturnMethod); + try + CallArgs := TGocciaArgumentsCollection.Create; + try + ReturnResult := InvokeCallable(ReturnMethod, CallArgs, + AIteratorObject); + finally + CallArgs.Free; + end; + if not (ReturnResult is TGocciaObjectValue) then + ThrowTypeError(SErrorIteratorReturnObject, + SSuggestIteratorResultObject); + finally + RemoveTempRootIfNeeded(ReturnMethod, ReturnMethodWasRooted); + end; + finally + RemoveTempRootIfNeeded(AIteratorObject, IteratorWasRooted); + end; +end; + +procedure CloseDirectIteratorPreservingError( + const AIteratorObject: TGocciaValue); +begin + try + CloseDirectIterator(AIteratorObject); + except + // An existing abrupt completion takes precedence over close failures. + end; +end; + { TGocciaIteratorValue } constructor TGocciaIteratorValue.Create; @@ -733,6 +784,7 @@ procedure TGocciaIteratorValue.InitializePrototype; Members.AddNamedMethod('some', IteratorSome, 1, gmkPrototypeMethod, [gmfNoFunctionPrototype]); Members.AddNamedMethod('every', IteratorEvery, 1, gmkPrototypeMethod, [gmfNoFunctionPrototype]); Members.AddNamedMethod('find', IteratorFind, 1, gmkPrototypeMethod, [gmfNoFunctionPrototype]); + Members.AddNamedMethod('includes', IteratorIncludes, 1, gmkPrototypeMethod, [gmfNoFunctionPrototype]); Members.AddNamedMethod('flatMap', IteratorFlatMap, 1, gmkPrototypeMethod, [gmfNoFunctionPrototype]); PrototypeMembers := Members.ToDefinitions; finally @@ -1416,6 +1468,88 @@ function TGocciaIteratorValue.IteratorFind(const AArgs: TGocciaArgumentsCollecti end; end; +function TGocciaIteratorValue.IteratorIncludes( + const AArgs: TGocciaArgumentsCollection; + const AThisValue: TGocciaValue): TGocciaValue; +var + Iterator: TGocciaIteratorValue; + NumberArg: TGocciaNumberLiteralValue; + SearchElement, SkippedArg, Value: TGocciaValue; + SkippedElements, Skipped: Double; + Done: Boolean; + IteratorWasRooted, SearchWasRooted: Boolean; +begin + // Iterator Includes proposal §sec-iterator.prototype.includes. + if not (AThisValue is TGocciaObjectValue) then + ThrowTypeError(SErrorIteratorIncludesNonIterator, + SSuggestIteratorThisType); + + SearchElement := AArgs.GetElement(0); + SkippedArg := AArgs.GetElement(1); + SkippedElements := 0; + if not (SkippedArg is TGocciaUndefinedLiteralValue) then + begin + if not (SkippedArg is TGocciaNumberLiteralValue) then + begin + CloseDirectIteratorPreservingError(AThisValue); + ThrowTypeError(SErrorIteratorIncludesSkippedElementsNumber, + SSuggestIteratorIncludesSkippedElements); + end; + + NumberArg := TGocciaNumberLiteralValue(SkippedArg); + if NumberArg.IsNaN or + (not NumberArg.IsInfinite and (Frac(NumberArg.Value) <> 0)) then + begin + CloseDirectIteratorPreservingError(AThisValue); + ThrowTypeError(SErrorIteratorIncludesSkippedElementsNumber, + SSuggestIteratorIncludesSkippedElements); + end; + if NumberArg.Value < 0 then + begin + CloseDirectIteratorPreservingError(AThisValue); + ThrowRangeError(SErrorIteratorIncludesSkippedElementsNegative, + SSuggestIteratorIncludesSkippedElements); + end; + if not NumberArg.IsInfinite and + (NumberArg.Value > MAX_SAFE_INTEGER_F) then + begin + CloseDirectIteratorPreservingError(AThisValue); + ThrowRangeError(SErrorIteratorIncludesSkippedElementsTooLarge, + SSuggestIteratorIncludesSkippedElements); + end; + SkippedElements := NumberArg.Value; + end; + + SearchWasRooted := AddTempRootIfNeeded(SearchElement); + try + Iterator := IteratorThisToDirectIterator(AThisValue, 'includes'); + IteratorWasRooted := AddTempRootIfNeeded(Iterator); + try + Skipped := 0; + while True do + begin + Value := Iterator.DirectNext(Done); + if Done then + Exit(TGocciaBooleanLiteralValue.FalseValue); + if Skipped < SkippedElements then + begin + Skipped := Skipped + 1; + Continue; + end; + if IsSameValueZero(Value, SearchElement) then + begin + CloseDirectIterator(AThisValue); + Exit(TGocciaBooleanLiteralValue.TrueValue); + end; + end; + finally + RemoveTempRootIfNeeded(Iterator, IteratorWasRooted); + end; + finally + RemoveTempRootIfNeeded(SearchElement, SearchWasRooted); + end; +end; + { Iterator.from() } function TGocciaIteratorValue.IteratorFrom(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; diff --git a/tests/built-ins/Goccia/proposal.js b/tests/built-ins/Goccia/proposal.js index f2160880..e2ab2a7f 100644 --- a/tests/built-ins/Goccia/proposal.js +++ b/tests/built-ins/Goccia/proposal.js @@ -38,6 +38,16 @@ describe.runIf(hasGoccia)("Goccia.proposal", () => { }); }); + test("stage 3 includes Iterator Includes", () => { + const proposal = Goccia.proposal["stage-3"].find( + (entry) => entry.name === "Iterator Includes", + ); + + expect(proposal.link).toBe( + "https://github.com/tc39/proposal-iterator-includes", + ); + }); + test("proposal is read-only", () => { const original = Goccia.proposal; expect(() => { Goccia.proposal = "overwritten"; }).toThrow(TypeError); diff --git a/tests/built-ins/Iterator/prototype/includes.js b/tests/built-ins/Iterator/prototype/includes.js new file mode 100644 index 00000000..f1588d4a --- /dev/null +++ b/tests/built-ins/Iterator/prototype/includes.js @@ -0,0 +1,396 @@ +/*--- +description: Iterator.prototype.includes proposal behavior +features: [Iterator.prototype.includes] +---*/ + +describe("Iterator.prototype.includes()", () => { + test("has the proposal-defined function shape", () => { + const descriptor = Object.getOwnPropertyDescriptor( + Iterator.prototype, + "includes", + ); + + expect(typeof descriptor.value).toBe("function"); + expect(descriptor.value.name).toBe("includes"); + expect(descriptor.value.length).toBe(1); + expect(descriptor.enumerable).toBe(false); + expect(descriptor.configurable).toBe(true); + expect(descriptor.writable).toBe(true); + expect(() => new Iterator.prototype.includes()).toThrow(TypeError); + }); + + test("returns a boolean and consumes through the matching value", () => { + const iterator = [1, 2, 3].values(); + + expect(iterator.includes(2)).toBe(true); + expect(iterator.next().value).toBe(3); + expect([1, 2, 3].values().includes(4)).toBe(false); + }); + + test("does not internally close a native iterator without return", () => { + const matched = new Map([[1, "one"], [2, "two"]]).values(); + expect(matched.includes("one")).toBe(true); + expect(matched.next().value).toBe("two"); + + const validationError = new Set([1, 2]).values(); + expect(() => validationError.includes(1, "0")).toThrow(TypeError); + expect(validationError.next().value).toBe(1); + }); + + test("searches for undefined when searchElement is omitted", () => { + expect([1, undefined, 3].values().includes()).toBe(true); + }); + + test("accepts direct plain iterator objects", () => { + const iterator = { + count: 0, + next() { + this.count = this.count + 1; + return this.count <= 3 + ? { value: this.count, done: false } + : { value: undefined, done: true }; + }, + }; + + expect(Iterator.prototype.includes.call(iterator, 2)).toBe(true); + expect(iterator.count).toBe(2); + }); + + test("uses SameValueZero for numbers", () => { + expect([NaN].values().includes(NaN)).toBe(true); + expect([-0].values().includes(0)).toBe(true); + expect([0].values().includes(-0)).toBe(true); + }); + + test("uses identity for objects and symbols", () => { + const object = {}; + const sameDescription = Symbol("value"); + const symbol = Symbol("value"); + + expect([object].values().includes(object)).toBe(true); + expect([{}].values().includes(object)).toBe(false); + expect([symbol].values().includes(symbol)).toBe(true); + expect([sameDescription].values().includes(symbol)).toBe(false); + }); + + test("skips the requested number of leading elements", () => { + expect([1, 2, 1].values().includes(1, 1)).toBe(true); + expect([1, 2, 3].values().includes(1, 1)).toBe(false); + expect([1].values().includes(1, -0)).toBe(true); + expect([1].values().includes(1, undefined)).toBe(true); + expect([1].values().includes(1, Number.MAX_SAFE_INTEGER)).toBe(false); + }); + + test("allows Infinity and consumes the iterator to exhaustion", () => { + let returnAccessed = false; + const iterator = { + count: 0, + next() { + this.count = this.count + 1; + return this.count <= 2 + ? { value: 1, done: false } + : { value: undefined, done: true }; + }, + }; + Object.defineProperty(iterator, "return", { + get() { + returnAccessed = true; + return () => ({}); + }, + }); + + expect(Iterator.prototype.includes.call(iterator, 1, Infinity)).toBe(false); + expect(iterator.count).toBe(3); + expect(returnAccessed).toBe(false); + }); + + test("rejects non-integral and non-Number skippedElements without coercion", () => { + const invalidValues = [ + NaN, + -0.5, + 0.5, + "1", + 1n, + true, + null, + {}, + Symbol("1"), + ]; + + invalidValues.forEach((invalidValue) => { + expect(() => [1].values().includes(1, invalidValue)).toThrow(TypeError); + }); + + let coerced = 0; + let closed = 0; + const skippedElements = { + valueOf() { + coerced = coerced + 1; + return 0; + }, + }; + const iterator = { + next() { + return { value: 1, done: false }; + }, + return() { + closed = closed + 1; + return {}; + }, + }; + + expect(() => Iterator.prototype.includes.call( + iterator, + 1, + skippedElements, + )).toThrow(TypeError); + expect(coerced).toBe(0); + expect(closed).toBe(1); + }); + + test("rejects negative and too-large skippedElements", () => { + expect(() => [1].values().includes(1, -1)).toThrow(RangeError); + expect(() => [1].values().includes(1, -Infinity)).toThrow(RangeError); + expect(() => [1].values().includes( + 1, + Number.MAX_SAFE_INTEGER + 1, + )).toThrow(RangeError); + }); + + test("closes before reading next when skippedElements is invalid", () => { + let nextAccessed = false; + let closed = 0; + const iterator = { + return() { + closed = closed + 1; + return {}; + }, + }; + Object.defineProperty(iterator, "next", { + get() { + nextAccessed = true; + return () => ({ done: true }); + }, + }); + + expect(() => Iterator.prototype.includes.call(iterator, 1, 0.5)) + .toThrow(TypeError); + expect(nextAccessed).toBe(false); + expect(closed).toBe(1); + }); + + test("preserves validation errors when return access or calls fail", () => { + const getterError = {}; + Object.defineProperty(getterError, "return", { + get() { + throw new RangeError("close getter"); + }, + }); + + expect(() => Iterator.prototype.includes.call(getterError, 1, "0")) + .toThrow(TypeError); + + const methodError = { + return() { + throw new TypeError("close method"); + }, + }; + expect(() => Iterator.prototype.includes.call(methodError, 1, -1)) + .toThrow(RangeError); + }); + + test("rejects primitive receivers before skippedElements validation", () => { + expect(() => Iterator.prototype.includes.call(null, 1, 0)).toThrow(TypeError); + expect(() => Iterator.prototype.includes.call(1, 1, 0)).toThrow(TypeError); + }); + + test("gets next once after validating skippedElements", () => { + let nextGets = 0; + let calls = 0; + const iterator = {}; + Object.defineProperty(iterator, "next", { + get() { + nextGets = nextGets + 1; + return () => { + calls = calls + 1; + return calls === 1 + ? { value: 1, done: false } + : { value: undefined, done: true }; + }; + }, + }); + + expect(Iterator.prototype.includes.call(iterator, 2)).toBe(false); + expect(nextGets).toBe(1); + expect(calls).toBe(2); + }); + + test("does not close when getting next throws", () => { + let closed = 0; + const iterator = { + return() { + closed = closed + 1; + return {}; + }, + }; + Object.defineProperty(iterator, "next", { + get() { + throw new Error("next getter"); + }, + }); + + expect(() => Iterator.prototype.includes.call(iterator, 1)).toThrow(Error); + expect(closed).toBe(0); + }); + + test("does not close when next is invalid or throws", () => { + let closed = 0; + const invalidNext = { + next: 0, + return() { + closed = closed + 1; + return {}; + }, + }; + expect(() => Iterator.prototype.includes.call(invalidNext, 1)) + .toThrow(TypeError); + expect(closed).toBe(0); + + const throwingNext = { + next() { + throw new Error("next call"); + }, + return() { + closed = closed + 1; + return {}; + }, + }; + expect(() => Iterator.prototype.includes.call(throwingNext, 1)) + .toThrow(Error); + expect(closed).toBe(0); + }); + + test("does not close when next returns a primitive", () => { + let closed = 0; + const iterator = { + next() { + return 1; + }, + return() { + closed = closed + 1; + return {}; + }, + }; + + expect(() => Iterator.prototype.includes.call(iterator, 1)).toThrow(TypeError); + expect(closed).toBe(0); + }); + + test("does not close when done or value access throws", () => { + let closed = 0; + const doneError = { + next() { + const result = {}; + Object.defineProperty(result, "done", { + get() { + throw new Error("done"); + }, + }); + return result; + }, + return() { + closed = closed + 1; + return {}; + }, + }; + expect(() => Iterator.prototype.includes.call(doneError, 1)).toThrow(Error); + expect(closed).toBe(0); + + const valueError = { + next() { + const result = { done: false }; + Object.defineProperty(result, "value", { + get() { + throw new Error("value"); + }, + }); + return result; + }, + return() { + closed = closed + 1; + return {}; + }, + }; + expect(() => Iterator.prototype.includes.call(valueError, 1)).toThrow(Error); + expect(closed).toBe(0); + }); + + test("closes with the direct iterator as receiver after a match", () => { + let returnThis; + const iterator = { + next() { + return { value: 1, done: false }; + }, + return() { + returnThis = this; + return {}; + }, + }; + + expect(Iterator.prototype.includes.call(iterator, 1)).toBe(true); + expect(returnThis).toBe(iterator); + }); + + test("propagates return failures after a match", () => { + const getterError = { + next() { + return { value: 1, done: false }; + }, + }; + Object.defineProperty(getterError, "return", { + get() { + throw new Error("return getter"); + }, + }); + expect(() => Iterator.prototype.includes.call(getterError, 1)).toThrow(Error); + + const methodError = { + next() { + return { value: 1, done: false }; + }, + return() { + throw new Error("return call"); + }, + }; + expect(() => Iterator.prototype.includes.call(methodError, 1)).toThrow(Error); + + const invalidResult = { + next() { + return { value: 1, done: false }; + }, + return() { + return 0; + }, + }; + expect(() => Iterator.prototype.includes.call(invalidResult, 1)) + .toThrow(TypeError); + }); + + test("does not access return after normal exhaustion", () => { + let returnAccessed = false; + const iterator = { + next() { + return { value: undefined, done: true }; + }, + }; + Object.defineProperty(iterator, "return", { + get() { + returnAccessed = true; + return () => ({}); + }, + }); + + expect(Iterator.prototype.includes.call(iterator, 1)).toBe(false); + expect(returnAccessed).toBe(false); + }); +});