Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/built-ins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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.

Expand Down
1 change: 1 addition & 0 deletions docs/language-tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
4 changes: 2 additions & 2 deletions docs/language.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions source/units/Goccia.Error.Messages.pas
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down
1 change: 1 addition & 0 deletions source/units/Goccia.Error.Suggestions.pas
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 2 additions & 1 deletion source/units/Goccia.Spec.pas
Original file line number Diff line number Diff line change
Expand Up @@ -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')
);

Expand Down
134 changes: 134 additions & 0 deletions source/units/Goccia.Values.IteratorValue.pas
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions tests/built-ins/Goccia/proposal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading