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/State.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static partial class State
{
// Canonical SHA-256 over every /harness and /test path and per-file SHA-256 at GitHubSha.
// A Test262 pin bump must update this digest from the reviewed corpus too.
private const string CorpusContentSha256 = "53f0415391f9b10732861cebc6f4d1229350c9367347e6d98102f59f76e19ff0";
private const string CorpusContentSha256 = "3a5186f786877400125b6ab4a73e4528821faa911527bd8e6ba69c92c000a23d";

private static readonly Lazy<Task<Test262Stream>> _test262Stream = new(LoadTest262Stream);

Expand Down
10 changes: 9 additions & 1 deletion Jint.Tests.Test262/Test262Harness.settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"SuiteGitSha": "35d566604512cba908054eec49f85e64a59f3091",
"SuiteGitSha": "7ab7fafa0003f73fc85c1b95d88094d33f7eb8bd",
//"SuiteDirectory": "//mnt/c/work/test262",
"TargetPath": "./Generated",
"Namespace": "Jint.Tests.Test262",
Expand Down Expand Up @@ -169,6 +169,14 @@
"intl402/Collator/prototype/compare/non-normative-phonebook.js",
"intl402/Collator/usage-de.js",

// DateTimeFormat - a component bag such as { weekday, day, month } is not resolved against CLDR's
// availableFormats skeletons by a format matcher (https://tc39.es/ecma402/#sec-createdatetimeformat);
// JsDateTimeFormat assembles the fields in ShortDatePattern order with fixed separators, and .NET has only
// the stand-alone abbreviated weekday names. German's MMMMEd pattern is "EEE, d. MMMM" with the
// format-context "Sa.", and this test's "Sa.," needs both; Jint writes "Sa 24 Dezember". Removed when
// component bags format through a matched locale pattern with format-context weekday names.
"intl402/DateTimeFormat/prototype/format/german-weekday-no-year.js",

// DurationFormat - precision formatting requires exact sub-second arithmetic
"intl402/DurationFormat/prototype/format/precision-exact-mathematical-values.js",
// Requires Spanish unit patterns (ICU4N doesn't include unit data, only core locale data)
Expand Down
164 changes: 164 additions & 0 deletions Jint.Tests/Runtime/TypedArrayWithShrinkTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
namespace Jint.Tests.Runtime;

/// <summary>
/// <see href="https://tc39.es/ecma262/#sec-%typedarray%.prototype.with">%TypedArray%.prototype.with</see>
/// reads the length before it coerces the index and the value, and either coercion can run script that shrinks
/// a resizable buffer. <c>IsValidIntegerIndex</c> then proves only that the replaced index survived, so every
/// element past the new length reads as <c>undefined</c> and <c>TypedArraySetElement</c> converts it: NaN in a
/// float array, zero in an integer one, and a <c>TypeError</c> in a BigInt one. Jint bulk-copied the original
/// length's worth of bytes out of the reallocated, shorter buffer, so a CLR <c>ArgumentException</c> escaped
/// into the host instead.
/// <para>
/// test262 covers this in <c>built-ins/TypedArray/prototype/with/*-coercion-shrinks.js</c> (tc39/test262#5136,
/// written for tc39/ecma262#3979); these pin it per element type and add the growing and still-in-bounds cases
/// the generated projection does not reach.
/// </para>
/// </summary>
public class TypedArrayWithShrinkTests
{
private readonly Engine _engine = new();

/// <summary>
/// Runs <paramref name="body"/> against a three-element <paramref name="constructor"/> array over a
/// resizable buffer, with <c>shrink()</c> cutting the buffer to one element. Written in script with its
/// own catch, so a CLR exception leaking out of the engine fails the test instead of passing as an error.
/// </summary>
private string Run(string constructor, string one, string body) => _engine.Evaluate($$"""
(function () {
var TA = {{constructor}};
var buffer = new ArrayBuffer(3 * TA.BYTES_PER_ELEMENT, { maxByteLength: 3 * TA.BYTES_PER_ELEMENT });
var sample = new TA(buffer);
sample[0] = {{one}};
sample[1] = {{one}} + {{one}};
sample[2] = {{one}} + {{one}} + {{one}};
var calls = 0;
function shrink() { calls++; buffer.resize(TA.BYTES_PER_ELEMENT); }
try {
{{body}}
} catch (e) {
return e.constructor.name + ' calls=' + calls + ' source=' + Array.prototype.join.call(sample, ',');
}
})()
""").AsString();

[TestCase("Int8Array", "0")]
[TestCase("Uint8Array", "0")]
[TestCase("Uint8ClampedArray", "0")]
[TestCase("Int16Array", "0")]
[TestCase("Uint16Array", "0")]
[TestCase("Int32Array", "0")]
[TestCase("Uint32Array", "0")]
[TestCase("Float32Array", "NaN")]
[TestCase("Float64Array", "NaN")]
public void IndexCoercionThatShrinksConvertsTheMissingElements(string constructor, string missing)
{
Run(constructor, "1", """
var result = sample.with({ valueOf: function () { shrink(); return 0; } }, 9);
return Array.prototype.join.call(result, ',') + ' calls=' + calls + ' source=' + Array.prototype.join.call(sample, ',');
""").Should().Be($"9,{missing},{missing} calls=1 source=1");
}

[TestCase("Int8Array", "0")]
[TestCase("Uint8Array", "0")]
[TestCase("Uint8ClampedArray", "0")]
[TestCase("Int16Array", "0")]
[TestCase("Uint16Array", "0")]
[TestCase("Int32Array", "0")]
[TestCase("Uint32Array", "0")]
[TestCase("Float32Array", "NaN")]
[TestCase("Float64Array", "NaN")]
public void ValueCoercionThatShrinksConvertsTheMissingElements(string constructor, string missing)
{
Run(constructor, "1", """
var result = sample.with(0, { valueOf: function () { shrink(); return 9; } });
return Array.prototype.join.call(result, ',') + ' calls=' + calls + ' source=' + Array.prototype.join.call(sample, ',');
""").Should().Be($"9,{missing},{missing} calls=1 source=1");
}

/// <summary>
/// Float16 needs <c>System.Half</c>, which the net472 build lacks, so these rows run where it exists.
/// </summary>
private static readonly bool SupportsHalf = Type.GetType("System.Half") is not null;

[TestCase("index")]
[TestCase("value")]
[IgnoreUnless(nameof(SupportsHalf), "Float16Array needs System.Half, which this target framework lacks")]
public void AFloat16ArrayThatShrinksConvertsTheMissingElementsToNaN(string coerced)
{
var call = coerced == "index"
? "sample.with({ valueOf: function () { shrink(); return 0; } }, 9)"
: "sample.with(0, { valueOf: function () { shrink(); return 9; } })";

Run("Float16Array", "1", "var result = " + call + """
;
return Array.prototype.join.call(result, ',') + ' calls=' + calls + ' source=' + Array.prototype.join.call(sample, ',');
""").Should().Be("9,NaN,NaN calls=1 source=1");
}

/// <summary>
/// <c>ToBigInt(undefined)</c> is a <c>TypeError</c>, and since tc39/ecma262#3979 step 12 propagates it.
/// </summary>
[TestCase("BigInt64Array", "index")]
[TestCase("BigInt64Array", "value")]
[TestCase("BigUint64Array", "index")]
[TestCase("BigUint64Array", "value")]
public void ABigIntArrayThatShrinksIsATypeError(string constructor, string coerced)
{
var call = coerced == "index"
? "sample.with({ valueOf: function () { shrink(); return 0; } }, 9n)"
: "sample.with(0, { valueOf: function () { shrink(); return 9n; } })";

Run(constructor, "1n", call + "; return 'did not throw';")
.Should().Be("TypeError calls=1 source=1");
}

/// <summary>
/// A negative index resolves against the length read before the coercion, so shrinking past it is the
/// step 9 <c>RangeError</c>, which comes before any element is copied.
/// </summary>
[TestCase("Int32Array", "1", "3")]
[TestCase("Float64Array", "1", "3")]
[TestCase("BigInt64Array", "1n", "3n")]
public void ANegativeIndexShrunkOutOfBoundsIsARangeError(string constructor, string one, string three)
{
Run(constructor, one, $"sample.with(-2, {{ valueOf: function () {{ shrink(); return {three}; }} }}); return 'did not throw';")
.Should().Be($"RangeError calls=1 source={one.TrimEnd('n')}");
}

/// <summary>
/// Growing the buffer behind a length-tracking view copies only the length read before the coercion.
/// </summary>
[TestCase("Int32Array")]
[TestCase("BigInt64Array")]
public void GrowingDuringCoercionCopiesTheOriginalLength(string constructor)
{
var suffix = constructor.StartsWith("Big", StringComparison.Ordinal) ? "n" : "";
_engine.Evaluate($$"""
(function () {
var TA = {{constructor}};
var buffer = new ArrayBuffer(2 * TA.BYTES_PER_ELEMENT, { maxByteLength: 4 * TA.BYTES_PER_ELEMENT });
var sample = new TA(buffer);
sample[0] = 1{{suffix}};
sample[1] = 2{{suffix}};
var result = sample.with(1, { valueOf: function () { buffer.resize(4 * TA.BYTES_PER_ELEMENT); sample[2] = 7{{suffix}}; return 5{{suffix}}; } });
return Array.prototype.join.call(result, ',') + ' source=' + Array.prototype.join.call(sample, ',');
})()
""").AsString().Should().Be("1,5 source=1,2,7,0");
}

/// <summary>
/// A fixed-length view over a buffer that keeps its full length copies every element as before.
/// </summary>
[Test]
public void AnUnshrunkBufferCopiesEveryElement()
{
_engine.Evaluate("""
(function () {
var buffer = new ArrayBuffer(16, { maxByteLength: 32 });
var sample = new Int32Array(buffer, 4, 3);
sample.set([1, 2, 3]);
return Array.prototype.join.call(sample.with(-1, 9), ',');
})()
""").AsString().Should().Be("1,2,9");
}
}
3 changes: 2 additions & 1 deletion Jint.Tests/SpecAnchors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# resolving here until somebody re-runs that.
#
verified: 2026-09-19
anchors: 1259
anchors: 1260
absent: 2
!proposal-decorators sec-autoaccessordefinitionevaluation # tc39.es still renders the 2018 descriptor design, which has no such clause
!proposal-decorators sec-createdecoratorcontextobject # tc39.es still renders the 2018 descriptor design, which has no such clause
Expand Down Expand Up @@ -68,6 +68,7 @@ ecma262 sec-%typedarray%.prototype.sort
ecma262 sec-%typedarray%.prototype.subarray
ecma262 sec-%typedarray%.prototype.tolocalestring
ecma262 sec-%typedarray%.prototype.values
ecma262 sec-%typedarray%.prototype.with
ecma262 sec-%wrapforvaliditeratorprototype%-object
ecma262 sec-%wrapforvaliditeratorprototype%.next
ecma262 sec-%wrapforvaliditeratorprototype%.return
Expand Down
28 changes: 26 additions & 2 deletions Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1639,6 +1639,15 @@ private JsValue ToSorted(JsValue thisObject, JsValue compareFnArg)
return a;
}

/// <summary>
/// https://tc39.es/ecma262/#sec-%typedarray%.prototype.with
/// </summary>
/// <remarks>
/// Step 12 propagates the <c>TypedArraySetElement</c> abrupt completion. The published prose still asserts
/// it cannot fail (<c>!</c>); tc39/ecma262#3979 corrects that to <c>?</c> and test262 already tests it:
/// coercing the index or the value can run script that shrinks the buffer, and every element past the new
/// length then reads as <c>undefined</c>, which a BigInt array rejects with a <c>TypeError</c>.
/// </remarks>
[JsFunction]
private ObjectInstance With(JsValue thisObject, JsValue indexArg, JsValue value)
{
Expand Down Expand Up @@ -1670,12 +1679,27 @@ private ObjectInstance With(JsValue thisObject, JsValue indexArg, JsValue value)
var a = TypedArrayCreateSameType(o, [JsNumber.Create(len)]);

// The result has the same element type, so bulk-copy the source bytes into the freshly created target
// and then overwrite the single replaced element (whose value is already coerced above).
// and then overwrite the single replaced element (whose value is already coerced above). Only the
// elements O still has are copied: the coercions above may have shrunk its buffer, and the valid-index
// check proved only that actualIndex survived.
var elementSize = o._arrayElementType.GetElementSize();
var present = System.Math.Min(len, o.GetLength());
_engine.Constraints.Check();
System.Array.Copy(o._viewedArrayBuffer._arrayBufferData!, o._byteOffset, a._viewedArrayBuffer._arrayBufferData!, a._byteOffset, len * elementSize);
System.Array.Copy(o._viewedArrayBuffer._arrayBufferData!, o._byteOffset, a._viewedArrayBuffer._arrayBufferData!, a._byteOffset, present * elementSize);
a[(int) actualIndex] = value;

// Step 12.c reads undefined past O's current length, and step 12.e converts it: NaN for a float element
// type, +0 for an integer one, and a TypeError for BigInt.
for (var k = present; k < len; k++)
{
if (k % ConstraintCheckInterval == 0)
{
_engine.Constraints.Check();
}

a[k] = Undefined;
}

return a;
}

Expand Down
Loading