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
2 changes: 1 addition & 1 deletion Jint.Tests.Test262/Test262Harness.settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"SuiteGitSha": "5f1f06f0cacf5c15b9387760375e897d7ae3f6d5",
"SuiteGitSha": "defaaf1571cd13b183e3f505c6a06e8db316e593",
//"SuiteDirectory": "//mnt/c/work/test262",
"TargetPath": "./Generated",
"Namespace": "Jint.Tests.Test262",
Expand Down
33 changes: 33 additions & 0 deletions Jint.Tests/Runtime/IntlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -741,4 +741,37 @@ public void NumberFormat_FormatRange_PrefixCurrencyCollapse_TightSeparator()
".formatRange(2.9, 3.1)");
result.AsString().Should().Be("+$2.90–3.10");
}

[Theory]
// a tag matching no available locale falls back to the hardcoded root collations
[InlineData("und", """["emoji","eor"]""")]
[InlineData("und-Latn-US", """["emoji","eor"]""")]
[InlineData("qtz-CN", """["emoji","eor"]""")]
// a matched locale reports the root collations plus its own, in code unit order
[InlineData("tr", """["emoji","eor"]""")]
[InlineData("de", """["emoji","eor","phonebk"]""")]
[InlineData("ko", """["emoji","eor","searchjl","unihan"]""")]
// an explicitly requested collation is the whole answer, however it was requested
[InlineData("de-u-co-phonebk", """["phonebk"]""")]
[InlineData("und-u-co-pinyin", """["pinyin"]""")]
public void LocaleGetCollations(string tag, string expected)
{
var result = _engine.Evaluate($"JSON.stringify(new Intl.Locale('{tag}').getCollations())");
result.AsString().Should().Be(expected);
}

[Fact]
public void LocaleGetCollationsNeverReportsStandardOrSearch()
{
var result = _engine.Evaluate("""
['ar', 'de', 'en', 'ja', 'ko', 'sv', 'tr', 'zh', 'und'].every(tag => {
const collations = new Intl.Locale(tag).getCollations();
return collations.length > 0
&& !collations.includes('standard')
&& !collations.includes('search')
&& collations.join() === [...collations].sort().join();
});
""");
result.AsBoolean().Should().BeTrue();
}
}
97 changes: 97 additions & 0 deletions Jint.Tests/Runtime/IteratorHelpersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,101 @@ public void ToArrayReturnsPlainArray()

result.Should().BeTrue();
}

[Fact]
public void JoinConcatenatesUsingSeparator()
{
var engine = new Engine();
var result = engine.Evaluate("""
function* gen() { yield 'a'; yield 'b'; }
JSON.stringify([
[].values().join(),
['one'].values().join(),
['one', 'two', 'three'].values().join(),
['one', 'two', 'three'].values().join('&&'),
['one', 'two', 'three'].values().join(''),
gen().join('-'),
[1, 2, 3, 4, 5].values().drop(1).take(3).map(x => x * 10).join('/')
]);
""").AsString();

result.Should().Be("""["","one","one,two,three","one&&two&&three","onetwothree","a-b","20/30/40"]""");
}

[Fact]
public void JoinFormatsNullishValuesAsEmptyString()
{
var engine = new Engine();
var result = engine.Evaluate("['one', null, 'two', undefined, 'three'].values().join()").AsString();

result.Should().Be("one,,two,,three");
}

[Fact]
public void JoinCoercesSeparatorBeforeReadingNext()
{
var engine = new Engine();
var result = engine.Evaluate("""
const effects = [];
const separator = { toString() { effects.push('toString'); return '&&'; } };
let n = 0;
const it = {
get next() {
effects.push('get next');
return () => ++n <= 2 ? { done: false, value: n === 1 ? 'one' : 'two' } : { done: true };
}
};
Iterator.prototype.join.call(it, separator) + '|' + effects.join(',');
""").AsString();

result.Should().Be("one&&two|toString,get next");
}

[Fact]
public void JoinClosesIteratorWhenCoercionThrows()
{
var engine = new Engine();
var result = engine.Evaluate("""
const throwy = { toString() { throw new Error('nope'); } };
function makeIterator(value) {
return {
closed: false,
next() { return this.done ? { done: true } : (this.done = true, { done: false, value }); },
return() { this.closed = true; }
};
}

const onSeparator = makeIterator('x');
try { Iterator.prototype.join.call(onSeparator, throwy); } catch { }

const onContents = makeIterator(throwy);
try { Iterator.prototype.join.call(onContents); } catch { }

// an iterator that simply runs out must NOT be closed
const onExhaustion = makeIterator('x');
Iterator.prototype.join.call(onExhaustion);

JSON.stringify([onSeparator.closed, onContents.closed, onExhaustion.closed]);
""").AsString();

result.Should().Be("[true,true,false]");
}

[Fact]
public void JoinThrowsOnNonObjectReceiver()
{
var engine = new Engine();
var result = engine.Evaluate("""
[undefined, null, false, 0, 0n, '', Symbol()].every(receiver => {
try {
Iterator.prototype.join.call(receiver);
return false;
} catch (e) {
return e instanceof TypeError;
}
});
""").AsBoolean();

result.Should().BeTrue();
}
}
41 changes: 41 additions & 0 deletions Jint/Native/Intl/CollatorConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,47 @@ private static string GetCollationOption(ObjectInstance options, string? unicode
return "default";
}

// The collations CLDR's root locale contributes to every locale. "standard" and "search" are
// deliberately absent: ECMA-402 forbids either from appearing in a reported collation list.
private static readonly string[] RootCollations = ["emoji", "eor"];

/// <summary>
/// The collation identifiers reported for <paramref name="language"/> by
/// https://tc39.es/ecma402/#sec-collationsoflocale — the root collations plus whatever the
/// language adds, in lexicographic code unit order. A language carrying no data of its own —
/// including "und" and any tag matching no available Collator locale — gets exactly the root
/// list, which is what the spec hardcodes for the unmatched case.
/// </summary>
internal static string[] GetCollationsForLanguage(string? language)
{
if (language is null || !LocaleCollationSupport.TryGetValue(language, out var supported))
{
return RootCollations;
}

var list = new List<string>(supported.Count + RootCollations.Length);
foreach (var collation in supported)
{
// "default" is Jint's placeholder for "no explicit collation was requested", not an
// identifier a locale can report.
if (!string.Equals(collation, "default", StringComparison.Ordinal))
{
list.Add(collation);
}
}

foreach (var rootCollation in RootCollations)
{
if (!list.Contains(rootCollation))
{
list.Add(rootCollation);
}
}

list.Sort(StringComparer.Ordinal);
return list.ToArray();
}

private static bool IsCollationSupportedForLocale(string language, string collation)
{
if (string.Equals(collation, "default", StringComparison.Ordinal))
Expand Down
22 changes: 18 additions & 4 deletions Jint/Native/Intl/LocalePrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,24 @@ private JsArray GetCollations(JsValue thisObject)
{
var locale = ValidateLocale(thisObject);

// Return array of supported collations
var result = new JsArray(Engine, 1);
result.SetIndexValue(0, locale.Collation ?? "default", updateLength: true);
return result;
// 1. If loc.[[Collation]] is not undefined, return CreateArrayFromList(« loc.[[Collation]] »).
if (locale.Collation is not null)
{
var requested = new JsArray(Engine, 1);
requested.SetIndexValue(0, locale.Collation, updateLength: true);
return requested;
}

// 2-6. Otherwise report the matched locale's collations, or the hardcoded root list when the
// tag matches no available Collator locale, sorted in lexicographic code unit order.
var collations = CollatorConstructor.GetCollationsForLanguage(locale.Language);
var values = new JsValue[collations.Length];
for (var i = 0; i < collations.Length; i++)
{
values[i] = JsString.Create(collations[i]);
}

return new JsArray(Engine, values);
}

/// <summary>
Expand Down
87 changes: 87 additions & 0 deletions Jint/Native/Iterator/IteratorPrototype.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text;
using Jint.Native.Object;
using Jint.Native.Symbol;
using Jint.Pooling;
Expand Down Expand Up @@ -640,6 +641,92 @@ private JsValue Find(JsValue thisObject, JsValue predicate)
return Undefined;
}

/// <summary>
/// https://tc39.es/proposal-iterator-join/#sec-iterator.prototype.join
/// </summary>
[JsFunction]
private JsValue Join(JsValue thisObject, JsValue separator)
{
// 1. Let O be the this value.
// 2. If O is not an Object, throw a TypeError exception.
if (thisObject is not ObjectInstance o)
{
Throw.TypeError(_realm, "Iterator.prototype.join called on non-object");
return Undefined;
}

string sep;
if (separator.IsUndefined())
{
// 3. If separator is undefined, let sep be ",".
sep = ",";
}
else
{
// 4. Else, let sep be Completion(ToString(separator)), closing O if that is abrupt.
try
{
sep = TypeConverter.ToString(separator);
}
catch
{
IteratorClose(o, CompletionType.Throw);
throw;
}
}

// 5. Let iterated be ? GetIteratorDirect(O). Reading "next" deliberately happens only after
// the separator has been coerced, and an abrupt lookup here does NOT close the receiver.
var iterated = GetIteratorDirect(o);

// 6. Let R be the empty String. 7. Let first be true.
using var sb = new ValueStringBuilder();
var first = true;
var iterations = 0;

// 8. Repeat,
// a. Let next be ? IteratorStepValue(iterated). b. If next is DONE, return R.
// An abrupt step — a throwing next(), a non-object result, or a throwing "value" getter —
// propagates without closing, per IteratorStepValue marking the record done.
while (iterated.TryIteratorStep(out var iteratorResult))
{
var value = iteratorResult.Get(CommonProperties.Value);

// c. If first is false, set R to the string-concatenation of R and sep.
if (!first)
{
sb.Append(sep);
}

// d. Set first to false.
first = false;

try
{
// e. If next is neither undefined nor null, set R to the string-concatenation of R
// and ? ToString(next), closing the iterator if the coercion is abrupt.
if (!value.IsNullOrUndefined())
{
sb.Append(TypeConverter.ToString(value));
}

// Check constraints periodically so a huge (or native-backed) iterator cannot run
// uninterrupted; the catch closes the iterator.
if (++iterations % Engine.ConstraintCheckInterval == 0)
{
_engine.Constraints.Check();
}
}
catch
{
iterated.Close(CompletionType.Throw);
throw;
}
}

return sb.Length == 0 ? JsString.Empty : JsString.Create(sb.ToString());
}

[JsSymbolFunction("Iterator", Length = 0, Flags = PropertyFlag.NonEnumerable)]
private static JsValue ToIterator(JsValue thisObject) => thisObject;

Expand Down
28 changes: 25 additions & 3 deletions Jint/Runtime/Modules/CyclicModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,25 @@ m._evalError is not null ||
}
}

/// <summary>
/// https://tc39.es/proposal-defer-import-eval/#sec-IsModuleSCCEvaluated
/// 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 is still EVALUATING-ASYNC awaiting a top-level await. Reading the member's
/// own status alone would report that graph as settled and let a deferred dependency of it run —
/// or be declared synchronously runnable — before the cycle has actually finished.
/// </summary>
private static bool IsModuleSCCEvaluated(CyclicModule module)
{
var cycleRoot = module._cycleRoot;
if (cycleRoot is not null)
{
return cycleRoot.Status == ModuleStatus.Evaluated;
}

return module.Status == ModuleStatus.Evaluated;
}

/// <summary>
/// https://tc39.es/proposal-defer-import-eval/#sec-ReadyForSyncExecution
/// </summary>
Expand All @@ -692,12 +711,15 @@ internal static bool ReadyForSyncExecution(Module module, HashSet<Module> seen =
return true;
}

if (cyclicModule.Status == ModuleStatus.Evaluated)
if (IsModuleSCCEvaluated(cyclicModule))
{
return true;
}

if (cyclicModule.Status is ModuleStatus.Evaluating or ModuleStatus.EvaluatingAsync)
// The spec asserts LINKED here, having ruled out EVALUATING and EVALUATING-ASYNC. EVALUATED is
// reachable too — a member of an async cycle whose root has not settled — and such a module
// cannot be completed synchronously either, so it is refused rather than asserted on.
if (cyclicModule.Status is ModuleStatus.Evaluating or ModuleStatus.EvaluatingAsync or ModuleStatus.Evaluated)
{
return false;
}
Expand Down Expand Up @@ -766,7 +788,7 @@ internal static void GatherAsynchronousTransitiveDependencies(
return;
}

if (cyclicModule.Status is ModuleStatus.Evaluating or ModuleStatus.EvaluatingAsync or ModuleStatus.Evaluated)
if (cyclicModule.Status == ModuleStatus.Evaluating || IsModuleSCCEvaluated(cyclicModule))
{
return;
}
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ and many more.
- ✔ Explicit Resource Management (`using` and `await using`)
- ✔ Immutable Arraybuffers
- ✔ Import Bytes (`import x from './file' with { type: 'bytes' }`)
- ✔ Iterator Join (`Iterator.prototype.join`)
- ✔ Iterator Sequencing
- ✔ Joint Iteration
- ✔ JSON.parse source text access
Expand Down