Skip to content

Update test262 suite and implement Iterator.prototype.join - #2867

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:iterator-join
Jul 29, 2026
Merged

Update test262 suite and implement Iterator.prototype.join#2867
lahma merged 1 commit into
sebastienros:mainfrom
lahma:iterator-join

Conversation

@lahma

@lahma lahma commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Bumps the pinned test262 suite to defaaf1571cd13b183e3f505c6a06e8db316e593. Three things came in with it, and all three needed engine work to stay green — which is why they are one PR: the SHA bump cannot land without any of them.

Iterator.prototype.join (proposal-iterator-join) is the new feature, with 18 tests. Joining is the easy half; which failures close the receiver is the part worth reading the tests for. Coercing the separator closes it, and so does coercing a value the iterator produced. An abrupt next lookup, a throwing next(), and a protocol violation (a step result that is not an object) all propagate without closing, because IteratorStepValue marks the record done first. Exhaustion does not close either. The separator is coerced before next is read, so a receiver whose next getter throws never gets that far, and next is read exactly once however many steps follow. Nullish values format as the empty string, as they do in Array.prototype.join.

Intl.Locale.prototype.getCollations picked up a normative change (ecma402#1072): a tag matching no available Collator locale no longer falls back to the default locale — it reports the hardcoded root collations « "emoji", "eor" » — and the result is now sorted in lexicographic code unit order. Jint reported a single-element ["default"] for every locale, and "default" is Jint's internal placeholder for "no collation was requested", not an identifier a locale can report at all. It now reports the root collations plus whatever the language adds, sorted; an explicitly requested collation, however it was requested, is still the whole answer. The data comes from the table CollatorConstructor already kept, so nothing new was invented:

tag before after
und, und-Latn-US, qtz-CN, tr ["default"] ["emoji","eor"]
de ["default"] ["emoji","eor","phonebk"]
ko ["default"] ["emoji","eor","searchjl","unihan"]
de-u-co-phonebk ["phonebk"] ["phonebk"]

import-defer grew IsModuleSCCEvaluated, and Jint needed it. A module that finished its own body is only really done once the strongly connected component it belongs to is: a member of an async cycle reaches EVALUATED as soon as its body returns, while the cycle root sits in EVALUATING-ASYNC awaiting a top-level await. Reading the member's own status is what GatherAsynchronousTransitiveDependencies and ReadyForSyncExecution were doing, so a deferred subgraph reaching into such a cycle looked free of async dependencies — and the deferring module ran its body, and took its deferred namespace, before the cycle had finished. The new test (import-defer/evaluation-top-level-await/async-cycle-dependency-of-deferred-module) walks exactly that graph, and Jint produced

B, A-before-await, Middle-before-nsD.z, Middle-after-nsD.z, resolve-blocker, C, A-after-await, D

where the spec wants

B, A-before-await, resolve-blocker, A-after-await, Middle-before-nsD.z, D, Middle-after-nsD.z, C

ReadyForSyncExecution refuses an EVALUATED-but-not-SCC-evaluated module rather than asserting on it: the spec asserts LINKED at that point, having convinced itself the state is unreachable, and refusing is both the conservative answer and the one EvaluateSync already turns into the spec's TypeError.

Verification

All in Release, on the rebased branch:

suite result
Jint.Tests.Test262 99484 passed, 0 failed, 157 skipped
Jint.Tests (net10.0 / net472) 4636 / 4555 passed, 0 failed
Jint.Tests.PublicInterface (net10.0 / net472) 1240 / 1239 passed, 0 failed
both of the above with JINT_HOST_CONTRACT_VERIFICATION=1 0 failed

New in-repo tests cover join's results, its nullish formatting, the separator-before-next ordering, which failures close the receiver and which do not, and the non-object receiver rejection; plus getCollations for the root fallback, a matched locale, an explicit collation, and the "never reports standard/search, always sorted" invariant.

🤖 Generated with Claude Code

Bumps the pinned suite to defaaf1571cd13b183e3f505c6a06e8db316e593. Three
things came in with it, and all three needed engine work to stay green.

Iterator.prototype.join (https://github.com/tc39/proposal-iterator-join) is the
new feature, with 18 tests. Joining is the easy half; which failures close the
receiver is the part worth reading the tests for. Coercing the separator closes
it, and so does coercing a value the iterator produced. An abrupt `next` lookup,
a throwing `next()`, and a protocol violation (a step result that is not an
object) all propagate without closing, because IteratorStepValue marks the
record done first. Exhaustion does not close either. The separator is coerced
*before* `next` is read, so a receiver whose `next` getter throws never gets
that far, and `next` is read exactly once however many steps follow. Nullish
values format as the empty string, as they do in Array.prototype.join.

Intl.Locale.prototype.getCollations picked up a normative change
(tc39/ecma402#1072): a tag matching no available
Collator locale no longer falls back to the default locale — it reports the
hardcoded root collations « "emoji", "eor" » — and the result is now sorted in
lexicographic code unit order. Jint reported a single-element ["default"] for
every locale, and "default" is Jint's internal placeholder for "no collation was
requested", not an identifier a locale can report at all. It now reports the
root collations plus whatever the language adds, sorted; an explicitly requested
collation, however it was requested, is still the whole answer. The data comes
from the table CollatorConstructor already kept, so nothing new was invented:
"de" reports ["emoji","eor","phonebk"], "zh" its seven, "tr" and "und" and any
private-use tag the bare root pair.

import-defer grew IsModuleSCCEvaluated, and Jint needed it. A module that
finished its own body is only really done once the strongly connected component
it belongs to is: a member of an async cycle reaches EVALUATED as soon as its
body returns, while the cycle root sits in EVALUATING-ASYNC awaiting a top-level
await. Reading the member's own status is what
GatherAsynchronousTransitiveDependencies and ReadyForSyncExecution were doing,
so a deferred subgraph reaching into such a cycle looked free of async
dependencies — and the deferring module ran its body, and took its deferred
namespace, before the cycle had finished. The new test walks exactly that graph.
ReadyForSyncExecution refuses an EVALUATED-but-not-SCC-evaluated module rather
than asserting on it: the spec asserts LINKED at that point, having convinced
itself the state is unreachable, and refusing is both the conservative answer
and the one EvaluateSync already turns into the spec's TypeError.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lahma
lahma merged commit cf8dad6 into sebastienros:main Jul 29, 2026
5 checks passed
@lahma
lahma deleted the iterator-join branch July 29, 2026 11:32
lahma added a commit that referenced this pull request Aug 11, 2026
)

ECMA-402 gives a locale one collation list and reads it from two places.
https://tc39.es/ecma402/#sec-collationsoflocale (15.5.10) step 3.c takes
%Intl.Collator%.[[SortLocaleData]].[[<foundLocale>]].[[co]] and reports it,
and https://tc39.es/ecma402/#sec-resolvelocale (9.2.7) step 10 - reached from
https://tc39.es/ecma402/#sec-intl.collator (10.1.1) through ResolveOptions -
resolves a requested "co" by asking whether that same list contains it, from
the -u-co- keyword and from the options bag alike. getCollations() and
Intl.Collator are two views of one list, so they may not disagree.

Jint's disagreed on thirteen of fifteen tags probed. GetCollationsForLanguage
appends the root collations "emoji" and "eor" to every locale, which is right
- CLDR's root locale carries both, and 15.5.10 step 4 hardcodes exactly that
pair for a tag matching nothing - while IsCollationSupportedForLocale conceded
only "eor" from that pair and otherwise allowed just what the per-language
table lists. The table lists "emoji" for "en" alone, so

    new Intl.Locale('de').getCollations()
    // ["emoji", "eor", "phonebk"]
    new Intl.Collator('de', { collation: 'emoji' }).resolvedOptions().collation
    // "default"

and likewise for ar, da, es, hi, ja, ko, ln, si, sv, zh, tr and fr. Only "en",
whose table entry happens to list "emoji", and "und", which resolves through
the default locale, were consistent. V8 accepts "emoji" on all fifteen.

The acceptance predicate is the older half, but the disagreement is new: until
#2867 getCollations() answered ["default"] for every locale and so had nothing
to contradict.

Both views now read one derived list. LocaleCollations is built once from the
CLDR table plus the root collations, in the code unit order 15.5.10 returns;
GetCollationsForLanguage hands it out and IsCollationSupportedForLocale scans
it. Adding a collation to the table now reports it and accepts it by the same
act, which is the property that was missing.

Two identifiers stay out of that list rather than being special-cased where a
request is resolved. "standard" and "search" are excluded because
https://tc39.es/ecma402/#sec-intl-collator-internal-slots (10.2.3) forbids
either from being an element of any [[SortLocaleData]].[[<locale>]].[[co]] or
[[SearchLocaleData]].[[<locale>]].[[co]] List, and keeping them out of the list
makes them unreportable and unrequestable at once - the guard they used to have
in GetCollationOption would have gone on rejecting them even if a table edit
made getCollations() report them, which is this same bug in miniature.
"default" is excluded because no locale reports it, but stays acceptable as a
request: it is how Jint spells the null [[co]] that 10.1.1 turns into a
resolved collation of "default" anyway.

ValidCollationTypes goes with them. It was a second, wider whitelist every
request had to clear first, and because every entry of every per-language list
is already in it the check never changed an answer - but it was one more list
to keep in step, and a collation added to the table and not to it would have
been reported and refused exactly as "emoji" was.

The fifteen-tag probe now finds nothing: everything getCollations() reports
constructs and resolves back to itself, through the options bag and through
-u-co-. The resolved locale for a -u-co-emoji tag moves with it, from "de" to
"de-u-co-emoji", because 9.2.7 step 10 inserts a keyword back into the locale
once its value resolves. A collation supplied through the options bag still
does not appear there - the same step sets supportedKeyword to empty on the
options branch - which is what
intl402/Collator/prototype/resolvedOptions/resolved-collation-unicode-extensions-and-options.js
pins, and is one place V8 answers differently.

Left alone deliberately: whether the per-language table is accurate CLDR at
all. Its "ducet", "direct", "big5han" and "reformed"-rather-than-"trad" entries
are flagged as suspect and are a data question of their own. This change makes
the two views agree on whatever the table says, which is a prerequisite for
correcting it rather than a substitute.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
legrab added a commit to legrab/pocok that referenced this pull request Aug 18, 2026
Updated [Jint](https://github.com/sebastienros/jint) from 4.15.3 to
4.16.0.

<details>
<summary>Release notes</summary>

_Sourced from [Jint's
releases](https://github.com/sebastienros/jint/releases)._

## 4.16.0

Jint 4.16.0 is a **correctness- and reliability-focused release**:
alongside asynchronous module loading, proper tail calls and four new
iterator built-ins, a pre-tag review swept the whole engine and fixed
what it found — including long-standing defects that predate this cycle.
**No option defaults changed.** Behaviour changes to note up front:
`JSON.stringify` and other machine-readable output now format
invariantly under every host culture — under Swedish or Finnish locales
on .NET 8+ it used to emit a Unicode minus sign no JSON parser accepts;
`JSON.parse` now rejects trailing commas as the grammar requires; bare
identifiers at global scope resolve through the global's prototype chain
per spec; `IModuleLoader.Resolve` is consulted once per (referrer,
specifier) pair, so a loader using it as a per-import access-control
checkpoint should move the check to `LoadModule`; and an inconsistent
sort comparator now finishes with an implementation-defined order on
every target framework instead of hanging (net462/netstandard) or
throwing a CLR exception at script (net8+).

### Highlights

**Proper tail calls (#​2975).** Strict-mode calls in tail position reuse
their frame, so `"use strict"` tail recursion runs in constant stack —
the first ES2015 PTC implementation among the .NET engines.

**Asynchronous module loading (#​2872).** `IAsyncModuleLoader` and the
`AsyncModuleLoader` template let a host fetch module source over I/O
without blocking a thread; `Engine.Modules.StartImport` returns an
operation a game loop drives via `ProcessTasks()`, and `ImportAsync`
awaits without holding a thread. The spec's load phase now exists as
written, a warm-cache async loader keeps the blocking `Import` fully
synchronous, and the blocking drain wakes on a work-arrived signal
instead of polling. A module served over a transport keeps its whole url
as `Module.Location` so its own relative imports resolve, a deferred
namespace evaluates its module instead of exposing uninitialized
bindings, and an import abandoned by a global snapshot restore reports
itself faulted instead of polling forever.

**The process no longer dies for recoverable reasons.**
`Options.LimitRecursion` used to kill the host process for most useful
limits — the constraint fired, and the unwind itself overflowed the
stack; exception filters now let it unwind ~7× deeper. The new opt-in
`Options.Constraints.StackOverflowGuard` converts unbounded recursion —
reachable through eighteen distinct routes, `new`, accessors, coercions
and Proxy traps included — from a process kill into a catchable
`RangeError`, exempting strict tail calls, which grow no stack. And a
family of CLR exceptions that escaped `engine.Evaluate` past every
script `catch` are now proper JavaScript errors or correct results:
sorting with an inconsistent comparator, destructuring with a
function-valued default (`const { onChange = () => {} } = opts`),
`toLocaleString` outside `DateTime`'s range, typed-array
`defineProperty` without a value, `DataView` reads at 2³¹,
`String.replace` `$'` with a lying exec, and the first instant of year
10000.

**New built-ins.** `Iterator.prototype.join`, `chunks`, `windows` and
`includes`; `take`/`drop` now throw `RangeError` for a finite limit
above 2^53−1 per the updated proposals.
`Intl.Locale.prototype.getCollations` reports CLDR-cited collation data
that `Intl.Collator` accepts in full, a malformed `collation` option is
a `RangeError`, and `Intl.supportedValuesOf("collation")` derives from
the same lists so the three can never drift.

**Conformance, from a review that ran what the suite does not.** Two of
the fixed defects had test262 coverage only under the never-generated
`staging/` directory, and several had none at all: `parseInt` strips the
sign before testing for a hex prefix, so `parseInt("-0x10")` is −16; a
suspended `finally` no longer swallows a pending `break`/`continue`; a
Proxy (or exotic host object) as the global's prototype answers bare
identifiers through its `get` trap; `Date.prototype.toISOString` emits
the spec's six-digit expanded year and round-trips through `Date.parse`
in every spelling including year 0; iterator helpers close their
receiver exactly once and only when the spec says so, and carry their
own `@@​toStringTag`; `Map`/`Set` `size` is the prototype accessor the
spec defines rather than a phantom own property; a Proxy's
`defineProperty` trap receives the partial descriptor the caller wrote;
a string's `@@​iterator` is read once, with the primitive as receiver;
`Array.prototype.join` re-asks the array when a side effect fills a hole
mid-join; a direct eval reaches the enclosing function's `arguments` in
both modes; and `Temporal.Now` drops the methods the proposal removed.

**Embedder surface.** `OperationDeadlineConstraint` bounds a whole
multi-entry host operation; `ScriptPreparationOptions.StaticAnalysis`
trades prepare-time analysis for per-engine materialization on shared
graphs; `ModuleFactory.LocationOf` exposes the module-naming rule a host
must match; `Engine.Advanced.HostDefined` carries per-request state on a
pooled engine; the CLR exception behind an interop error is reachable
through `JintException.TryGetClrException` with opt-in
`ChainClrExceptions()`, and a host method's own `TargetException` is no
longer mistaken for a receiver mismatch; and a recursion-limit failure
propagates out of a module load instead of becoming a catchable
rejection.

**Performance, gated.** Against v4.15.3 on idle hardware, medians of
three paired runs: `controlflow-recursive` **−15.6% time and −40.4%
allocation** (proper tail calls), `bitops-3bit-bits-in-byte` −8.9%,
`math-spectral-norm` −7.3%, `crypto-sha1` −6.9%, `3d-raytrace` −5.9%,
`math-cordic` −5.8%, with a broad −1–4% tail across the call- and
string-heavy rows; no row moved outside its own measured cross-run
envelope in the other direction, and allocation is flat within ±0.2%
suite-wide. Warmed `parseInt` call sites take the frameless fast-call
lane (−13% on the parse loop), joined by the `Number` predicates,
`String.prototype.indexOf`/`startsWith`/`endsWith`/`includes`/`at`/`substr`,
global `isNaN`/`isFinite` and `Array.isArray` (−3% to −19%) and the
`Map`/`Set` method family (`map.get` hit loop −13%); existence questions
on a wrapped dictionary answer from `ContainsKey`, taking `in` −33% with
−98% allocation and `Object.keys` −37%; resolving an inherited global no
longer allocates per miss (−99.99% on the read loop) and a global
created through an inherited write keeps the in-place store; JSON
replacer/reviver eligibility is decided once per document, built-in
callback dispatch once per loop, a call site's arguments reach an
interpreted callee in registers, and function-local `let`/`const` live
in fixed slots.

**Breaking changes.**
`Int32Extensions`/`Int64Extensions`/`DoubleExtensions` — polyfill hosts
that leaked into the public API — are now internal; on
net462/netstandard2.0, code with `using Jint;` may have bound span
`Parse`/`TryParse` members through them. `JsonParser` rejects trailing
commas. `Number.parseInt.length`/`Number.parseFloat.length` report their
spec values. Post-construction mutation of an `Options` instance no
longer reaches an already-built engine, and `Options.Configure`
callbacks work again. `UnwrapIfPromise` reports a cancelled engine as
`ExecutionCanceledException` instead of a timeout. Time-zone matching is
ASCII-case-insensitive per ECMA-402.

On the [engine comparison
benchmarks](https://github.com/sebastienros/jint/blob/main/Jint.Benchmark/README.md),
Jint 4.16.0 is the fastest engine outright on 5 of 12 scripts — leading
`dromaeo-object-regexp-modern` over native V8 by 1.25× — in a
statistical tie for first on `interop-collection-traversal`, the fastest
managed engine on 10 of 12, the fastest interpreter on all 12, and
8.6×–11.2× ahead of ClearScript (native V8) on every interop row while
allocating 3.9×–12.4× less than the nearest managed competitor.

## What's Changed
* Run the repository's own host tests under Release-mode contract
verification by @​lahma in
sebastienros/jint#2866
* Update test262 suite and implement Iterator.prototype.join by @​lahma
in sebastienros/jint#2867
* Stop a closing iterator from swallowing the error that closed it by
@​lahma in sebastienros/jint#2868
* Give every benchmark row its own engine by @​lahma in
sebastienros/jint#2873
* Hand a call site's arguments to an interpreted callee in registers by
@​lahma in sebastienros/jint#2874
* Ask once per loop, not once per element, how to call a built-in's
callback by @​lahma in sebastienros/jint#2876
* Say which spec document to read for a feature by @​lahma in
sebastienros/jint#2882
* Run PR CI on every pull request, not only those targeting main by
@​lahma in sebastienros/jint#2883
* Update test262 suite and adopt the new take/drop RangeError by @​lahma
in sebastienros/jint#2877
* Implement Iterator Chunking by @​lahma in
sebastienros/jint#2878
* Say to write against the modern BCL and polyfill downwards by @​lahma
in sebastienros/jint#2884
* Implement Iterator Includes by @​lahma in
sebastienros/jint#2879
* Ask once per document, not once per key, how to call a JSON replacer
or reviver by @​lahma in sebastienros/jint#2885
* Use double.IsFinite instead of hand-rolled NaN and infinity pairs by
@​lahma in sebastienros/jint#2880
* Bump the testing group with 1 update by @​dependabot[bot] in
sebastienros/jint#2889
* Stop charging closures for per-engine lazy globals by @​lahma in
sebastienros/jint#2890
* Bump the analyzers group with 1 update by @​dependabot[bot] in
sebastienros/jint#2888
* Make the lazy-global re-arm on restore a contract instead of an
accident by @​lahma in sebastienros/jint#2892
* Let the amortized constraint cadence span top-level entries into the
engine by @​lahma in sebastienros/jint#2886
* Let a function's own let/const live in its fixed slots by @​lahma in
sebastienros/jint#2887
* Keep a host function in the engine's own realm after a second realm
exists by @​lahma in sebastienros/jint#2893
* Make an engine's principal realm [[HostDefined]] reachable by @​lahma
in sebastienros/jint#2891
* Carry a labelled break/continue target on the completion record by
@​lahma in sebastienros/jint#2894
* Make Array.prototype.sort stable on every target framework by @​lahma
in sebastienros/jint#2898
* Stop toSorted and %TypedArray%.sort hanging on an inconsistent
comparator by @​lahma in sebastienros/jint#2899
* Delete the evaluation context's dead completion channel by @​lahma in
sebastienros/jint#2895
* Stop shipping the numeric polyfill hosts as public API by @​lahma in
sebastienros/jint#2901
 ... (truncated)

Commits viewable in [compare
view](sebastienros/jint@v4.15.3...v4.16.0).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=Jint&package-manager=nuget&previous-version=4.15.3&new-version=4.16.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant