Skip to content

Commit a0eee96

Browse files
lahmaclaude
andauthored
Array.from honours IsConstructor, and a typed array's length write throws (#3043)
Array.from and Array.fromAsync branch on IsConstructor (https://tc39.es/ecma262/#sec-isconstructor), a question about the object's [[Construct]] slot. Four sites asked `is IConstructor` instead, which is a CLR type test: ScriptFunction, BindFunction and JsProxy all implement the interface unconditionally and put the real predicate in an `IsConstructor` override. So `Array.from.call(() => ({}), [])` took the construct branch where 23.1.2.1 steps 5.a and 6.b require ArrayCreate, and `Array.from.call(Math.sin.bind(null), [1,2,3])` threw a TypeError instead of producing an array -- likewise for a generator, an async function and a method carrying a [[HomeObject]]. They now ask JsValue.IsConstructor and cast, which is what Array.of already did. The trailing `Set(A, "length", len, true)` of the same algorithm (steps 5.b.iii.7 and 6.g) had the mirror-image hole: JsTypedArrayOperations.SetLength was an empty method, so `Array.from.call(Uint8Array, [])` silently did nothing where %TypedArray%.prototype.length -- a getter-only accessor -- makes that Set fail and therefore throw. It now performs the real write, which also reaches the Array.prototype generics a typed array can be the receiver of (push, pop, shift, unshift, splice), every one of which is specified to make the same throwing Set. One caller wanted the old silence and had to be corrected rather than exempted: Array.prototype.filter has no length step at all, and its `operations.SetLength(to)` is Jint's own bookkeeping for the fast JsArray lane, which stores elements with updateLength: false. It is now confined to an array result, so a typed-array species (`a.constructor = { [Symbol.species]: Uint8Array }`) comes back untouched instead of tripping over the accessor -- and a Proxy species no longer sees a `set` trap the algorithm never performs. Frees staging/sm/Array/from_constructor.js, staging/sm/Array/from_errors.js and staging/sm/Function/bound-non-constructable.js. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent d92d810 commit a0eee96

5 files changed

Lines changed: 318 additions & 18 deletions

File tree

Jint.Tests.Test262/Test262Harness.settings.json

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,6 @@
166166
"staging/sm/expressions/destructuring-array-done.js",
167167
"staging/sm/statements/for-of-iterator-close.js",
168168

169-
// Array.from applied to an arbitrary constructor: the spec constructs `this` when it is a
170-
// constructor and falls back to ArrayCreate when it is not, and each element goes in through
171-
// CreateDataPropertyOrThrow, so a read-only element or a non-extensible target has to throw.
172-
"staging/sm/Array/from_constructor.js",
173-
"staging/sm/Array/from_errors.js",
174-
175169
// Proxy trap invariants and the exact trap sequence the array built-ins are specified to
176170
// perform. Jint's own invariant check on a defineProperty trap also fires where it should not.
177171
"staging/sm/Array/concat-proxy.js",
@@ -223,7 +217,6 @@
223217
// Argument validation raising no error, or the wrong one, on built-ins called with a bad this
224218
// or a bad index. Includes SpiderMonkey's own toSource extension, which Jint does not implement
225219
// at all -- that half is removed by the test moving, not by Jint changing.
226-
"staging/sm/Function/bound-non-constructable.js",
227220
"staging/sm/TypedArray/constructor-byteoffsets-bounds.js",
228221
"staging/sm/TypedArray/iterator-next-with-detached.js",
229222
"staging/sm/class/newTargetDVG.js",
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
using Jint.Runtime;
2+
3+
namespace Jint.Tests.Runtime;
4+
5+
/// <summary>
6+
/// <c>Array.from</c> / <c>Array.fromAsync</c> branch on the spec's IsConstructor
7+
/// (https://tc39.es/ecma262/#sec-isconstructor), which is a question about the object's [[Construct]] slot and
8+
/// not about which CLR interface its implementing type happens to declare. Arrow functions, generators, async
9+
/// functions, concise methods and a bound function whose target is not constructable are all callable objects
10+
/// with no [[Construct]], so §23.1.2.1 steps 5.a / 6.b make each of them fall back to ArrayCreate.
11+
/// <para>
12+
/// The other half of the same algorithm is the trailing <c>Perform ? Set(A, "length", len, true)</c> (steps
13+
/// 5.b.iii.7 and 6.g): <c>%TypedArray%.prototype.length</c> is a getter-only accessor, so a typed array as the
14+
/// receiver has to make that write throw a TypeError rather than silently do nothing.
15+
/// </para>
16+
/// </summary>
17+
public class ArrayFromConstructorTests
18+
{
19+
private const string ArrowFunction = "(() => ({}))";
20+
private const string GeneratorFunction = "(function* () {})";
21+
private const string AsyncFunction = "(async function () {})";
22+
private const string ConciseMethod = "({ m() {} }).m";
23+
private const string BoundNonConstructor = "Math.sin.bind(null)";
24+
25+
[Theory]
26+
[InlineData(ArrowFunction)]
27+
[InlineData(GeneratorFunction)]
28+
[InlineData(AsyncFunction)]
29+
[InlineData(ConciseMethod)]
30+
[InlineData(BoundNonConstructor)]
31+
public void ArrayFromFallsBackToArrayCreateForANonConstructorThisOnTheIteratorPath(string nonConstructor)
32+
{
33+
var engine = new Engine();
34+
35+
var result = engine.Evaluate($$"""
36+
var out = Array.from.call({{nonConstructor}}, [1, 2]);
37+
JSON.stringify([Array.isArray(out), out.length, out[0], out[1]]);
38+
""").AsString();
39+
40+
result.Should().Be("[true,2,1,2]");
41+
}
42+
43+
[Theory]
44+
[InlineData(ArrowFunction)]
45+
[InlineData(GeneratorFunction)]
46+
[InlineData(AsyncFunction)]
47+
[InlineData(ConciseMethod)]
48+
[InlineData(BoundNonConstructor)]
49+
public void ArrayFromFallsBackToArrayCreateForANonConstructorThisOnTheArrayLikePath(string nonConstructor)
50+
{
51+
var engine = new Engine();
52+
53+
// No @@iterator, so this is the ConstructArrayFromArrayLike branch (step 6) rather than step 5.
54+
var result = engine.Evaluate($$"""
55+
var out = Array.from.call({{nonConstructor}}, { length: 2, 0: 1, 1: 2 });
56+
JSON.stringify([Array.isArray(out), out.length, out[0], out[1]]);
57+
""").AsString();
58+
59+
result.Should().Be("[true,2,1,2]");
60+
}
61+
62+
[Theory]
63+
[InlineData(ArrowFunction)]
64+
[InlineData(GeneratorFunction)]
65+
[InlineData(AsyncFunction)]
66+
[InlineData(ConciseMethod)]
67+
[InlineData(BoundNonConstructor)]
68+
public void ArrayFromAsyncFallsBackToArrayCreateForANonConstructorThis(string nonConstructor)
69+
{
70+
var engine = new Engine();
71+
72+
var result = engine.Evaluate($$"""
73+
Array.fromAsync.call({{nonConstructor}}, [1, 2]).then(function (out) {
74+
return JSON.stringify([Array.isArray(out), out.length, out[0], out[1]]);
75+
});
76+
""").UnwrapIfPromise(TimeSpan.FromSeconds(5)).AsString();
77+
78+
result.Should().Be("[true,2,1,2]");
79+
}
80+
81+
[Theory]
82+
[InlineData(ArrowFunction)]
83+
[InlineData(GeneratorFunction)]
84+
[InlineData(AsyncFunction)]
85+
[InlineData(ConciseMethod)]
86+
[InlineData(BoundNonConstructor)]
87+
public void ArrayFromAsyncFallsBackToArrayCreateForANonConstructorThisOnTheArrayLikePath(string nonConstructor)
88+
{
89+
var engine = new Engine();
90+
91+
var result = engine.Evaluate($$"""
92+
Array.fromAsync.call({{nonConstructor}}, { length: 2, 0: 1, 1: 2 }).then(function (out) {
93+
return JSON.stringify([Array.isArray(out), out.length, out[0], out[1]]);
94+
});
95+
""").UnwrapIfPromise(TimeSpan.FromSeconds(5)).AsString();
96+
97+
result.Should().Be("[true,2,1,2]");
98+
}
99+
100+
[Fact]
101+
public void ArrayOfFallsBackToArrayCreateForANonConstructorThis()
102+
{
103+
// Array.of already asked the right question; pinned here so the two stay in step.
104+
var engine = new Engine();
105+
106+
var result = engine.Evaluate($$"""
107+
var out = Array.of.call({{BoundNonConstructor}}, 1, 2, 3);
108+
JSON.stringify([Array.isArray(out), out.length, out[0], out[2]]);
109+
""").AsString();
110+
111+
result.Should().Be("[true,3,1,3]");
112+
}
113+
114+
[Fact]
115+
public void ABoundNonConstructorIsStillCallableAndStillNotConstructable()
116+
{
117+
var engine = new Engine();
118+
119+
engine.Evaluate($"typeof ({BoundNonConstructor})(0)").AsString().Should().Be("number");
120+
engine.Evaluate($$"""
121+
try { new ({{BoundNonConstructor}}); 'no throw'; } catch (e) { e.constructor.name; }
122+
""").AsString().Should().Be("TypeError");
123+
}
124+
125+
[Fact]
126+
public void ArrayFromStillConstructsAGenuineConstructorThis()
127+
{
128+
var engine = new Engine();
129+
130+
var result = engine.Evaluate("""
131+
function C() { this.constructed = true; }
132+
var out = Array.from.call(C, [1, 2]);
133+
JSON.stringify([Array.isArray(out), out instanceof C, out.constructed, out.length, out[0], out[1]]);
134+
""").AsString();
135+
136+
result.Should().Be("[false,true,true,2,1,2]");
137+
}
138+
139+
[Fact]
140+
public void ArrayFromPassesTheLengthToAGenuineConstructorOnTheArrayLikePath()
141+
{
142+
var engine = new Engine();
143+
144+
// Step 6.b: Construct(C, « 𝔽(len) »).
145+
var result = engine.Evaluate("""
146+
function C(n) { this.argCount = arguments.length; this.arg = n; }
147+
var out = Array.from.call(C, { length: 2, 0: 'a', 1: 'b' });
148+
JSON.stringify([out instanceof C, out.argCount, out.arg, out.length, out[0], out[1]]);
149+
""").AsString();
150+
151+
result.Should().Be("[true,1,2,2,\"a\",\"b\"]");
152+
}
153+
154+
[Fact]
155+
public void ArrayFromConstructsAClassAndABoundConstructor()
156+
{
157+
var engine = new Engine();
158+
159+
engine.Evaluate("""
160+
class C {}
161+
var out = Array.from.call(C, [1]);
162+
JSON.stringify([out instanceof C, out.length, out[0]]);
163+
""").AsString().Should().Be("[true,1,1]");
164+
165+
engine.Evaluate("""
166+
function D() {}
167+
var Bound = D.bind(null);
168+
var out = Array.from.call(Bound, [1]);
169+
JSON.stringify([out instanceof D, out.length, out[0]]);
170+
""").AsString().Should().Be("[true,1,1]");
171+
}
172+
173+
[Fact]
174+
public void TypedArrayFromIsUnaffected()
175+
{
176+
var engine = new Engine();
177+
178+
// %TypedArray%.from is its own algorithm and never performs the Set(A, "length", ...).
179+
engine.Evaluate("""
180+
var out = Uint8Array.from([]);
181+
JSON.stringify([out instanceof Uint8Array, out.length]);
182+
""").AsString().Should().Be("[true,0]");
183+
184+
engine.Evaluate("""
185+
var out = Uint8Array.from([1, 2, 3]);
186+
JSON.stringify([out instanceof Uint8Array, out.length, out[0], out[2]]);
187+
""").AsString().Should().Be("[true,3,1,3]");
188+
}
189+
190+
[Fact]
191+
public void ArrayFromWithATypedArrayConstructorThrowsOnTheUnwritableLength()
192+
{
193+
var engine = new Engine();
194+
195+
// Iterator path (step 5.b.iii.7).
196+
engine.Evaluate("""
197+
try { Array.from.call(Uint8Array, []); 'no throw'; } catch (e) { e.constructor.name; }
198+
""").AsString().Should().Be("TypeError");
199+
200+
// Array-like path (step 6.g).
201+
engine.Evaluate("""
202+
try { Array.from.call(Uint8Array, { length: 0 }); 'no throw'; } catch (e) { e.constructor.name; }
203+
""").AsString().Should().Be("TypeError");
204+
205+
// The exact shape staging/sm/Array/from_errors.js uses.
206+
engine.Evaluate("""
207+
Uint8Array.from = Array.from;
208+
try { Uint8Array.from([]); 'no throw'; } catch (e) { e.constructor.name; }
209+
""").AsString().Should().Be("TypeError");
210+
}
211+
212+
[Fact]
213+
public void ArrayFromThrowsWhenTheConstructedObjectHasAnUnwritableLength()
214+
{
215+
var engine = new Engine();
216+
217+
// Same Set(A, "length", ...) obligation reached through an ordinary object; the typed-array lane
218+
// above is only a second way to own a length that refuses to be written.
219+
engine.Evaluate("""
220+
function C() { Object.defineProperty(this, 'length', { configurable: true, writable: false, value: 4 }); }
221+
try { Array.from.call(C, []); 'no throw'; } catch (e) { e.constructor.name; }
222+
""").AsString().Should().Be("TypeError");
223+
224+
engine.Evaluate("""
225+
function C() { Object.defineProperty(this, 'length', { configurable: true, get: function () { return 4; } }); }
226+
try { Array.from.call(C, [0, 10]); 'no throw'; } catch (e) { e.constructor.name; }
227+
""").AsString().Should().Be("TypeError");
228+
}
229+
230+
[Fact]
231+
public void ArrayPrototypeGenericsThrowOnATypedArraysUnwritableLength()
232+
{
233+
var engine = new Engine();
234+
235+
// Every one of these is specified to Set(O, "length", ...) with throw = true on the receiver.
236+
foreach (var script in new[]
237+
{
238+
"Array.prototype.push.call(new Uint8Array(3))",
239+
"Array.prototype.pop.call(new Uint8Array(0))",
240+
"Array.prototype.shift.call(new Uint8Array(0))",
241+
"Array.prototype.unshift.call(new Uint8Array(3))",
242+
"Array.prototype.splice.call(new Uint8Array(3), 0, 0)",
243+
})
244+
{
245+
engine.Evaluate($$"""
246+
try { {{script}}; 'no throw'; } catch (e) { e.constructor.name; }
247+
""").AsString().Should().Be("TypeError", because: script);
248+
}
249+
}
250+
251+
[Fact]
252+
public void FilterDoesNotWriteALengthTheSpecNeverWrites()
253+
{
254+
var engine = new Engine();
255+
256+
// Array.prototype.filter has no Set(A, "length", ...) step at all, so a typed-array species must come
257+
// back untouched rather than tripping over the length accessor the trailing bookkeeping write used to
258+
// aim at.
259+
var result = engine.Evaluate("""
260+
var a = [1, 2, 3];
261+
a.constructor = { [Symbol.species]: Uint8Array };
262+
var out = a.filter(function () { return false; });
263+
JSON.stringify([out instanceof Uint8Array, out.length]);
264+
""").AsString();
265+
266+
result.Should().Be("[true,0]");
267+
}
268+
269+
[Fact]
270+
public void FilterStillFixesUpTheLengthOfARealArrayResult()
271+
{
272+
var engine = new Engine();
273+
274+
engine.Evaluate("""
275+
var a = [1, 2, 3, 4];
276+
a.constructor = Array;
277+
var out = a.filter(function (x) { return x % 2 === 0; });
278+
JSON.stringify([Array.isArray(out), out.length, out[0], out[1]]);
279+
""").AsString().Should().Be("[true,2,2,4]");
280+
281+
engine.Evaluate("""
282+
class MyArray extends Array {}
283+
var a = new MyArray();
284+
a.push(1, 2, 3, 4, 5);
285+
var out = a.filter(function (x) { return x > 3; });
286+
JSON.stringify([out instanceof MyArray, out.length, out[0], out[1]]);
287+
""").AsString().Should().Be("[true,2,4,5]");
288+
}
289+
}

Jint/Native/Array/ArrayConstructor.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,13 @@ private JsValue From(JsValue thisObject, JsCallArguments arguments)
6161
var usingIterator = GetMethod(_realm, items, GlobalSymbolRegistry.Iterator);
6262
if (usingIterator is not null)
6363
{
64-
if (!ReferenceEquals(this, thisObject) && thisObject is IConstructor constructor)
64+
// Step 5.a: IsConstructor(C), which is a question about the [[Construct]] slot, not about the CLR
65+
// interface the implementing type declares -- an arrow function, a generator, an async function, a
66+
// method with a [[HomeObject]] and a bound function over a non-constructor all implement
67+
// IConstructor and are all rejected by IsConstructor. Fall back to ArrayCreate for those.
68+
if (!ReferenceEquals(this, thisObject) && thisObject.IsConstructor)
6569
{
66-
var instance = constructor.Construct([], thisObject);
70+
var instance = ((IConstructor) thisObject).Construct([], thisObject);
6771
var iterator = items.GetIterator(_realm, method: usingIterator);
6872
var protocol = new ArrayProtocol(_engine, thisArg, instance, iterator, callable);
6973
protocol.Execute();
@@ -247,9 +251,9 @@ private void FromAsyncWithIterator(
247251
// ii. Let iteratorRecord be ? GetIterator(asyncItems, async).
248252
// We'll handle both async and sync iterators
249253
ObjectInstance instance;
250-
if (!ReferenceEquals(this, c) && c is IConstructor constructor)
254+
if (!ReferenceEquals(this, c) && c.IsConstructor)
251255
{
252-
instance = constructor.Construct([], c);
256+
instance = ((IConstructor) c).Construct([], c);
253257
}
254258
else
255259
{
@@ -498,9 +502,9 @@ private void FromAsyncWithArrayLike(
498502
// v. Else,
499503
// 1. Let A be ? ArrayCreate(len).
500504
ObjectInstance a;
501-
if (!ReferenceEquals(c, this) && c is IConstructor constructor)
505+
if (!ReferenceEquals(c, this) && c.IsConstructor)
502506
{
503-
a = constructor.Construct([(JsNumber) longLen], c);
507+
a = ((IConstructor) c).Construct([(JsNumber) longLen], c);
504508
}
505509
else
506510
{
@@ -650,10 +654,11 @@ private ObjectInstance ConstructArrayFromArrayLike(
650654
var length = source.GetLength();
651655

652656
ObjectInstance a;
653-
if (!ReferenceEquals(thisObj, this) && thisObj is IConstructor constructor)
657+
// Step 6.b: IsConstructor(C) -- see the note on the iterator branch above.
658+
if (!ReferenceEquals(thisObj, this) && thisObj.IsConstructor)
654659
{
655660
var argumentsList = new JsValue[] { length };
656-
a = Construct(constructor, argumentsList);
661+
a = Construct((IConstructor) thisObj, argumentsList);
657662
}
658663
else
659664
{

Jint/Native/Array/ArrayOperations.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,9 +385,12 @@ public override uint GetLength()
385385

386386
public override ulong GetLongLength() => GetLength();
387387

388+
// Every caller of this is a spec step spelled "Perform ? Set(O, "length", len, true)" -- Array.from,
389+
// Array.fromAsync and the Array.prototype generics that a typed array can be the receiver of. A typed
390+
// array's "length" is a getter-only accessor on %TypedArray%.prototype, so that Set fails and, being a
391+
// throwing one, raises a TypeError. Doing nothing here silently swallowed all of them.
388392
public override void SetLength(ulong length)
389-
{
390-
}
393+
=> _target.Set(CommonProperties.Length, length, true);
391394

392395
public override void EnsureCapacity(ulong capacity)
393396
{

Jint/Native/Array/ArrayPrototype.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,17 @@ private JsValue Filter(JsValue thisObject, JsCallArguments arguments)
583583
}
584584
}
585585

586-
operations.SetLength(to);
586+
// https://tc39.es/ecma262/#sec-array.prototype.filter has no Set(A, "length", ...) step at all: A is
587+
// created with length 0 and each accepted element arrives through CreateDataPropertyOrThrow, which is
588+
// what grows an array's length. The write below is Jint's own bookkeeping, needed because the fast
589+
// JsArray lane stores elements with updateLength: false. Anything that is not an array -- a typed array
590+
// species, a plain object, a Proxy -- must be left alone, or the algorithm performs a Set it does not
591+
// have; on a typed array that Set now correctly throws, which would turn a legal filter into a TypeError.
592+
if (a is JsArray)
593+
{
594+
operations.SetLength(to);
595+
}
596+
587597
invoker.Return();
588598

589599
return a;

0 commit comments

Comments
 (0)