Skip to content

Commit 53339e2

Browse files
committed
feat: port sparse/hole-aware arrays into compile mode (#73 Stage E.2)
Lands the full plan in docs/plans/sparse-arrays-compile-mode.md (M1–M6). Emitted `$Array` now matches the interpreter's `SharpTSArray` end-to-end: - $Array inherits from List<object?> so legacy Isinst/Castclass dispatch sites continue to work; adds _sparse dictionary + _length long fields for true ECMA-262 uint32 semantics. New $ArrayHole singleton mirrors Runtime.Types.ArrayHole for the hole sentinel. - Creation helpers (CreateArray, ArrayConstructor, ArrayFrom, ArrayOf, ConcatArrays) return $Array; Stage-D 1M guard removed. new Array(N) now sparse-transitions past threshold 1024 — `new Array(10_000_000)` no longer OOMs. - Runtime dispatch (GetIndex/SetIndex/GetProperty/HasIn/DeleteIndex/ SetProperty) routes $Array through the long-indexed API. arr.length reads LongLength; arr.length = N truncates/extends via SetLength; a[2147483648] = 1 writes without truncation; delete arr[i] turns the slot into a hole. - Built-in emitters honor ECMA-262 hole semantics (Stage C audit table): skip-holes (forEach/map callback/filter/reduce/reduceRight/every/some/ flat/flatMap/indexOf), preserve-holes (map output/slice/concat/ reverse/splice removed portion), unhole-at-read (find/findLast/ includes/toReversed/with/toSpliced/at/join). - Ancillary dispatch updated: for-in and Object.getOwnPropertyNames skip holes; Object.values/entries gain a list path with hole-skipping; JSON.stringify renders holes as "null" per SerializeJSONArray. - Test262 compiled subset adds test/built-ins/Array (compiledExcludeFolders cleared). Baseline regenerated: 2,181 Pass compiled vs 850 interpreter; Array folder contributes 1,302 compiled passes. Standalone DLL constraint preserved throughout (no reflection back to SharpTS.dll). Zero unit-test regressions: 10,275/10,275 green.
1 parent dd35a86 commit 53339e2

23 files changed

Lines changed: 5241 additions & 646 deletions

Compilation/EmittedRuntime.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,11 @@ public class EmittedRuntime
812812
public MethodBuilder DynamicImportModule { get; set; } = null!;
813813
public MethodBuilder WrapTaskAsPromise { get; set; } = null!;
814814

815+
// $ArrayHole singleton — sentinel for ECMA-262 array holes (index in range but never written).
816+
// NOTE: Must stay in sync with SharpTS.Runtime.Types.ArrayHole
817+
public Type ArrayHoleType { get; set; } = null!;
818+
public FieldInfo ArrayHoleInstance { get; set; } = null!;
819+
815820
// $Array type - emitted for standalone assemblies
816821
// NOTE: Must stay in sync with SharpTS.Runtime.Types.SharpTSArray
817822
public Type TSArrayType { get; set; } = null!;
@@ -826,6 +831,20 @@ public class EmittedRuntime
826831
public MethodBuilder TSArraySetStrict { get; set; } = null!;
827832
public MethodBuilder TSArrayToString { get; set; } = null!;
828833

834+
// Stage E.2 additions (long-indexed sparse/hole-aware API).
835+
// Mirrors SharpTSArray public surface. Legacy int-indexed Get/Set above
836+
// continue to work (they widen to the long path internally).
837+
// Count is inherited from List<object?> — no custom getter.
838+
public MethodBuilder TSArrayLongLengthGetter { get; set; } = null!;
839+
public MethodBuilder TSArrayLengthGetter { get; set; } = null!;
840+
public MethodBuilder TSArrayHasIndex { get; set; } = null!;
841+
public MethodBuilder TSArrayGetRaw { get; set; } = null!;
842+
public MethodBuilder TSArrayGetLong { get; set; } = null!;
843+
public MethodBuilder TSArraySetLong { get; set; } = null!;
844+
public MethodBuilder TSArraySetStrictLong { get; set; } = null!;
845+
public MethodBuilder TSArraySetLength { get; set; } = null!;
846+
public MethodBuilder TSArrayDeleteAt { get; set; } = null!;
847+
829848
// $IHasFields interface - for unified property access on user classes and $Object
830849
// Note: These use MethodInfo instead of MethodBuilder because we need the actual
831850
// methods from the created interface type (after CreateType() is called)

Compilation/Emitters/ArrayEmitter.cs

Lines changed: 104 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,188 +22,234 @@ public bool TryEmitMethodCall(IEmitterContext emitter, Expr receiver, string met
2222
emitter.EmitBoxIfNeeded(receiver);
2323

2424
// Handle both List<object> and $Array types
25-
// For $Array, extract the Elements property
26-
EmitGetListFromArrayOrList(il, ctx);
25+
// For $Array, extract the Elements property.
26+
// The returned local holds the ORIGINAL receiver, used below for
27+
// identity-preserving methods (sort/reverse/fill/copyWithin) and to
28+
// wrap "new array" method results back into $Array so downstream
29+
// code sees a $Array whenever the input was one.
30+
var receiverLocal = EmitGetListFromArrayOrList(il, ctx);
31+
32+
// Methods whose spec says "return this" — the caller expects the same
33+
// reference the receiver started with. Since we unwrapped to a List,
34+
// the runtime helper returns the inner List, not the $Array wrapper;
35+
// to preserve `arr === arr.sort()` we stash the wrapper and push it
36+
// back at the end.
37+
bool returnsReceiver = methodName is "sort" or "reverse" or "fill" or "copyWithin";
38+
// Methods whose spec says "return a new Array" — we want callers to
39+
// continue seeing a $Array after them (not a bare List<object?>), so
40+
// downstream array methods / runtime dispatch still match.
41+
bool returnsNewArray = methodName is
42+
"slice" or "concat" or "map" or "filter" or "flat" or "flatMap"
43+
or "splice" or "toReversed" or "toSorted" or "toSpliced" or "with";
2744

2845
switch (methodName)
2946
{
3047
case "pop":
3148
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayPop);
32-
return true;
49+
break;
3350

3451
case "shift":
3552
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayShift);
36-
return true;
53+
break;
3754

3855
case "unshift":
3956
// JS `arr.unshift(a, b, c)` prepends all args in order: [a,b,c,...orig].
4057
// ArrayUnshift(list, el) inserts one element at the start, so iterate
4158
// in reverse to preserve the final order.
4259
EmitVariadicListMutation(emitter, arguments, ctx.Runtime!.ArrayUnshift, reverse: true);
43-
return true;
60+
break;
4461

4562
case "push":
4663
// JS `arr.push(a, b, c)` appends all args. Iterate forward.
4764
EmitVariadicListMutation(emitter, arguments, ctx.Runtime!.ArrayPush, reverse: false);
48-
return true;
65+
break;
4966

5067
case "slice":
5168
EmitArgsArray(emitter, arguments);
5269
il.Emit(OpCodes.Call, ctx.Runtime!.ArraySlice);
53-
return true;
70+
break;
5471

5572
case "map":
5673
EmitSingleArgOrNull(emitter, arguments);
5774
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayMap);
58-
return true;
75+
break;
5976

6077
case "filter":
6178
EmitSingleArgOrNull(emitter, arguments);
6279
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayFilter);
63-
return true;
80+
break;
6481

6582
case "forEach":
6683
EmitSingleArgOrNull(emitter, arguments);
6784
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayForEach);
6885
il.Emit(OpCodes.Ldnull); // forEach returns undefined
69-
return true;
86+
break;
7087

7188
case "find":
7289
EmitSingleArgOrNull(emitter, arguments);
7390
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayFind);
74-
return true;
91+
break;
7592

7693
case "findIndex":
7794
EmitSingleArgOrNull(emitter, arguments);
7895
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayFindIndex);
7996
il.Emit(OpCodes.Box, ctx.Types.Double);
80-
return true;
97+
break;
8198

8299
case "some":
83100
EmitSingleArgOrNull(emitter, arguments);
84101
il.Emit(OpCodes.Call, ctx.Runtime!.ArraySome);
85-
return true;
102+
break;
86103

87104
case "every":
88105
EmitSingleArgOrNull(emitter, arguments);
89106
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayEvery);
90-
return true;
107+
break;
91108

92109
case "reduce":
93110
EmitArgsArray(emitter, arguments);
94111
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayReduce);
95-
return true;
112+
break;
96113

97114
case "reduceRight":
98115
EmitArgsArray(emitter, arguments);
99116
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayReduceRight);
100-
return true;
117+
break;
101118

102119
case "join":
103120
EmitSingleArgOrNull(emitter, arguments);
104121
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayJoin);
105-
return true;
122+
break;
106123

107124
case "concat":
108125
EmitSingleArgOrNull(emitter, arguments);
109126
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayConcat);
110-
return true;
127+
break;
111128

112129
case "reverse":
113130
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayReverse);
114-
return true;
131+
break;
115132

116133
case "flat":
117134
EmitSingleArgOrNull(emitter, arguments);
118135
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayFlat);
119-
return true;
136+
break;
120137

121138
case "flatMap":
122139
EmitSingleArgOrNull(emitter, arguments);
123140
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayFlatMap);
124-
return true;
141+
break;
125142

126143
case "includes":
127144
EmitSingleArgOrNull(emitter, arguments);
128145
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayIncludes);
129-
return true;
146+
break;
130147

131148
case "indexOf":
132149
EmitSingleArgOrNull(emitter, arguments);
133150
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayIndexOf);
134151
il.Emit(OpCodes.Box, ctx.Types.Double);
135-
return true;
152+
break;
136153

137154
case "sort":
138155
EmitSingleArgOrNull(emitter, arguments);
139156
il.Emit(OpCodes.Call, ctx.Runtime!.ArraySort);
140-
return true;
157+
break;
141158

142159
case "toSorted":
143160
EmitSingleArgOrNull(emitter, arguments);
144161
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayToSorted);
145-
return true;
162+
break;
146163

147164
case "splice":
148165
EmitArgsArray(emitter, arguments);
149166
il.Emit(OpCodes.Call, ctx.Runtime!.ArraySplice);
150-
return true;
167+
break;
151168

152169
case "toSpliced":
153170
EmitArgsArray(emitter, arguments);
154171
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayToSpliced);
155-
return true;
172+
break;
156173

157174
case "findLast":
158175
EmitSingleArgOrNull(emitter, arguments);
159176
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayFindLast);
160-
return true;
177+
break;
161178

162179
case "findLastIndex":
163180
EmitSingleArgOrNull(emitter, arguments);
164181
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayFindLastIndex);
165182
il.Emit(OpCodes.Box, ctx.Types.Double);
166-
return true;
183+
break;
167184

168185
case "toReversed":
169186
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayToReversed);
170-
return true;
187+
break;
171188

172189
case "with":
173190
EmitArgsArray(emitter, arguments);
174191
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayWith);
175-
return true;
192+
break;
176193

177194
case "at":
178195
EmitSingleArgOrNull(emitter, arguments);
179196
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayAt);
180-
return true;
197+
break;
181198

182199
case "fill":
183200
EmitArgsArray(emitter, arguments);
184201
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayFill);
185-
return true;
202+
break;
186203

187204
case "copyWithin":
188205
EmitArgsArray(emitter, arguments);
189206
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayCopyWithin);
190-
return true;
207+
break;
191208

192209
case "entries":
193210
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayEntries);
194-
return true;
211+
break;
195212

196213
case "keys":
197214
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayKeys);
198-
return true;
215+
break;
199216

200217
case "values":
201218
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayValues);
202-
return true;
219+
break;
203220

204221
default:
205222
return false;
206223
}
224+
225+
EmitPostCallAdjust(il, ctx, receiverLocal, returnsReceiver, returnsNewArray);
226+
return true;
227+
}
228+
229+
/// <summary>
230+
/// After a call that leaves a List&lt;object?&gt; on the stack, adjust the
231+
/// top-of-stack so downstream code sees the expected JS value:
232+
/// - For "return this" methods: pop the list, push the saved <c>$Array</c>
233+
/// receiver (or the bare list if receiver wasn't a <c>$Array</c>).
234+
/// - For "return new array" methods: wrap the list in a fresh <c>$Array</c>.
235+
/// Called from per-case branches in <see cref="TryEmitMethodCall"/>.
236+
/// </summary>
237+
private static void EmitPostCallAdjust(ILGenerator il, CompilationContext ctx, LocalBuilder receiverLocal, bool returnsReceiver, bool returnsNewArray)
238+
{
239+
if (returnsReceiver)
240+
{
241+
// Stack: [list (the mutated inner List<object?>)]
242+
// We want: the ORIGINAL receiver (the $Array wrapper if it was one).
243+
il.Emit(OpCodes.Pop);
244+
il.Emit(OpCodes.Ldloc, receiverLocal);
245+
return;
246+
}
247+
248+
if (returnsNewArray)
249+
{
250+
// Stack: [list] → want: [new $Array(list)]
251+
il.Emit(OpCodes.Newobj, ctx.Runtime!.TSArrayCtor);
252+
}
207253
}
208254

209255
/// <summary>
@@ -265,7 +311,23 @@ public bool TryEmitPropertyGet(IEmitterContext emitter, Expr receiver, string pr
265311

266312
var fallbackLabelNH = il.DefineLabel();
267313
var endLabelNH = il.DefineLabel();
314+
// Stage E.2 M2/M3: $Array inherits List<object?>, so `isinst List<object?>`
315+
// below matches $Array instances — but base `Count` only sees the dense
316+
// prefix, missing any sparse tail. Check $Array first and use its
317+
// LongLength getter (int-clamped Length would truncate lengths past
318+
// int.MaxValue; M3 acceptance demands `a.length === 2147483649` works).
319+
var tsArrayCheckLabel = il.DefineLabel();
320+
il.Emit(OpCodes.Ldloc, objLocal);
321+
il.Emit(OpCodes.Isinst, ctx.Runtime!.TSArrayType);
322+
il.Emit(OpCodes.Brfalse, tsArrayCheckLabel);
323+
il.Emit(OpCodes.Ldloc, objLocal);
324+
il.Emit(OpCodes.Castclass, ctx.Runtime!.TSArrayType);
325+
il.Emit(OpCodes.Callvirt, ctx.Runtime!.TSArrayLongLengthGetter);
326+
il.Emit(OpCodes.Conv_R8);
327+
il.Emit(OpCodes.Box, ctx.Types.Double);
328+
il.Emit(OpCodes.Br, endLabelNH);
268329

330+
il.MarkLabel(tsArrayCheckLabel);
269331
il.Emit(OpCodes.Ldloc, objLocal);
270332
il.Emit(OpCodes.Isinst, listTypeNH);
271333
il.Emit(OpCodes.Brfalse, fallbackLabelNH);
@@ -301,8 +363,11 @@ public bool TryEmitPropertySet(IEmitterContext emitter, Expr receiver, string pr
301363
/// <summary>
302364
/// Emits code to convert an array value (either List&lt;object&gt; or $Array) to List&lt;object&gt;.
303365
/// The value is expected to be on the stack; leaves List&lt;object&gt; on the stack.
366+
/// Returns the local that stashes the ORIGINAL receiver — callers emitting
367+
/// identity-preserving methods (sort/reverse/fill/copyWithin) use this to
368+
/// push the receiver back after the runtime helper returns.
304369
/// </summary>
305-
private static void EmitGetListFromArrayOrList(ILGenerator il, CompilationContext ctx)
370+
private static LocalBuilder EmitGetListFromArrayOrList(ILGenerator il, CompilationContext ctx)
306371
{
307372
var objLocal = il.DeclareLocal(ctx.Types.Object);
308373
il.Emit(OpCodes.Stloc, objLocal);
@@ -384,6 +449,7 @@ private static void EmitGetListFromArrayOrList(ILGenerator il, CompilationContex
384449
il.Emit(OpCodes.Castclass, ctx.Types.ListOfObject);
385450

386451
il.MarkLabel(endLabel);
452+
return objLocal;
387453
}
388454

389455
/// <summary>

0 commit comments

Comments
 (0)