Skip to content

Commit f3a0347

Browse files
committed
#101: spec-compliance batch — Promise/RegExp/Object/Array/dispatch (+254 test262 passes)
Bundles ~30 ECMA-262 fidelity fixes across the compiled-mode emitters. Net Test262 baseline movement: 7305 → 7559 passes (+254). All 10,321 unit tests still pass. Themes: Promise (§27.2.4, §27.2.5) • Promise.prototype.{then,catch,finally}.call(null|undefined) throws TypeError per ToObject step 1; new EmitThrowIfNullOrUndefined helper. • Promise.prototype.then validates IsPromise(promise) (only $TSPromise / Task<object>); throws TypeError otherwise per §27.2.5.4 step 2. • catch/finally dispatch shape: fast path for real promises, user-then invoke path for objects with custom `then` (spec §27.2.5.1 — catch is `Invoke(this, "then", «undefined, onRejected»)`). User-then PDS override on $TSPromise instances correctly diverts to user `then`. • Promise.all/any/race/allSettled non-iterable receivers reject with TypeError synchronously per §27.2.4.1 step 3. • Promise.then onRejected paths use $Runtime.WrapException to unwrap __tsValue / PromiseRejectedException.Reason / TargetInvocationException. RegExp (§22.2) • Sticky flag (`y`) honors lastIndex like global: requires match.Index == lastIndex, resets to 0 on failure (§22.2.5.2.2 step 15.c.i). Parity in both interpreter (SharpTSRegExp.Test) and compiler ($RegExp). • String.split distinguishes null (coerces to "null" per 7.1.17) from $Undefined.Instance (early-return per §22.1.3.21 step 4); callers now push UndefinedInstance instead of "" for the no-separator case. Object / Error prototypes (§10.1.6.3, §20.5.6.4) • Native-error subclass prototypes: distinct TypeError.prototype / RangeError.prototype / etc., each inheriting from Error.prototype, populated lazily with constructor/name/message via PDS. • Object.getPrototypeOf and the gOPD .prototype paths route every native-error subclass first, with base $Error last (subclass Type tokens are distinct from base; check order matters). • defineProperty validation: accessor-redefine spec rule 7.b/7.c (existing accessor + non-configurable + new Desc.[[Get]]/[[Set]] must SameValue match existing); data-redefine writable=false uses SameValue (Object.is) not Object.Equals so +0/-0 and NaN behave per §7.2.10. BigInt and Symbol primitive descriptors are explicitly rejected. Dispatch identity • Static-method bracket access (`Object["assign"]`, `Number["isNaN"]`, `Array["from"]`) routes through TSFunctionGetOrCreate so it returns the same $TSFunction wrapper as syntactic access. gOPD descriptors on Object/Number/Array statics now use LookupBuiltInStaticMember for value identity per test262 15.2.3.3-4-{14,15,…} probes. • Math.random's descriptor synthesis can wire the actual MethodBuilder (EmitRandom moved earlier than gOPD) for `desc.value === Math.random`. Array iteration (§23.1.3) • map/filter/forEach/find/findIndex/some/every cache len ONCE before the loop (§23.1.3.X step 2) so callback-driven mutations to list.Count don't shift iteration bounds. Per-iteration bounds re- check routes truncated indices to each method's hole-handling label (map preserves the hole; filter/some/every/forEach skip; find/ findIndex invoke predicate with undefined — fixed in prior commit). • Void-returning prototype methods (forEach, split-no-sep, Map/Set .forEach) emit UndefinedInstance instead of null so strict-equality probes (`arr.forEach(cb) === undefined`) pass. String methods • indexOf/lastIndexOf now use ToJsString (handles undefined/symbols/ objects) instead of Castclass-to-string. Default-arg push is "undefined" not "" per §22.1.3.8. • Array-creating method receivers: pre-check length ≤ 2^32-1, throw RangeError before any receiver-stack operation. Test262 infra • BatchedSubprocessRunner: each of the N worker slots now respawns its worker subprocess after the 1.5GB memory ceiling triggers a clean exit. Pre-fix, parallelism degraded 6→5→4→…→1 over an 11K- test regen. Post-fix, all slots stay busy until the shared queue is drained. • SmokeTest.cs: Diagnostic_ClusterFixes test enumerating the test262 paths this batch targets for regression watch. Tests: 10,321/10,321 passing. Test262 compiled baseline +254 net.
1 parent 13b9794 commit f3a0347

33 files changed

Lines changed: 1659 additions & 453 deletions

Compilation/EmittedRuntime.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,21 @@ public class EmittedRuntime
441441
public FieldBuilder ErrorPrototypeField { get; set; } = null!;
442442
/// <summary>Idempotent populate for <see cref="ErrorPrototypeField"/>.</summary>
443443
public MethodBuilder ErrorPrototypePopulateMethod { get; set; } = null!;
444+
/// <summary>TypeError.prototype singleton dict. Per ECMA-262 §20.5.6.4 each NativeError prototype is a distinct object whose [[Prototype]] is Error.prototype, with own `constructor` / `name` / `message` slots. Lazy-populated.</summary>
445+
public FieldBuilder TypeErrorPrototypeField { get; set; } = null!;
446+
public MethodBuilder TypeErrorPrototypePopulateMethod { get; set; } = null!;
447+
public FieldBuilder RangeErrorPrototypeField { get; set; } = null!;
448+
public MethodBuilder RangeErrorPrototypePopulateMethod { get; set; } = null!;
449+
public FieldBuilder ReferenceErrorPrototypeField { get; set; } = null!;
450+
public MethodBuilder ReferenceErrorPrototypePopulateMethod { get; set; } = null!;
451+
public FieldBuilder SyntaxErrorPrototypeField { get; set; } = null!;
452+
public MethodBuilder SyntaxErrorPrototypePopulateMethod { get; set; } = null!;
453+
public FieldBuilder URIErrorPrototypeField { get; set; } = null!;
454+
public MethodBuilder URIErrorPrototypePopulateMethod { get; set; } = null!;
455+
public FieldBuilder EvalErrorPrototypeField { get; set; } = null!;
456+
public MethodBuilder EvalErrorPrototypePopulateMethod { get; set; } = null!;
457+
public FieldBuilder AggregateErrorPrototypeField { get; set; } = null!;
458+
public MethodBuilder AggregateErrorPrototypePopulateMethod { get; set; } = null!;
444459
/// <summary>$Runtime.ErrorToStringSpec(this) — ECMA-262 20.5.3.4 Error.prototype.toString. Throws TypeError on non-object receiver; otherwise reads name/message via Get and returns the formatted string.</summary>
445460
public MethodBuilder ErrorToStringSpec { get; set; } = null!;
446461

@@ -736,6 +751,17 @@ public class EmittedRuntime
736751
// Promise support
737752
public MethodBuilder PromiseResolve { get; set; } = null!;
738753
public MethodBuilder PromiseReject { get; set; } = null!;
754+
// Value-form `Promise.resolve` / `Promise.reject` wrappers that validate
755+
// `this` is Object per ECMA-262 §27.2.5.1 step 2. Used by the $TSFunction
756+
// value-form path so `let r = Promise.resolve; r.call(undefined, x)` throws.
757+
public MethodBuilder PromiseResolveStatic { get; set; } = null!;
758+
public MethodBuilder PromiseRejectStatic { get; set; } = null!;
759+
// Same pattern for all/race/allSettled/any — value-form invocation must
760+
// validate `this` is Object before delegating to the iteration helper.
761+
public MethodBuilder PromiseAllStatic { get; set; } = null!;
762+
public MethodBuilder PromiseRaceStatic { get; set; } = null!;
763+
public MethodBuilder PromiseAllSettledStatic { get; set; } = null!;
764+
public MethodBuilder PromiseAnyStatic { get; set; } = null!;
739765
public MethodBuilder PromiseAll { get; set; } = null!;
740766
public MethodBuilder PromiseRace { get; set; } = null!;
741767
public MethodBuilder PromiseThen { get; set; } = null!;

Compilation/Emitters/ArrayEmitter.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,18 @@ public bool TryEmitMethodCall(IEmitterContext emitter, Expr receiver, string met
100100

101101
case "forEach":
102102
{
103+
// Spec: forEach returns undefined (not null). Push
104+
// $Undefined.Instance so `arr.forEach(...) === undefined`
105+
// holds — test262 callback-related tests rely on this.
103106
if (TryEmitDirectDelegateCall(emitter, arguments, ctx.Runtime!.ArrayForEachDirect))
104107
{
105-
il.Emit(OpCodes.Ldnull); // forEach returns undefined; helper is void
108+
il.Emit(OpCodes.Ldsfld, ctx.Runtime!.UndefinedInstance);
106109
break;
107110
}
108111
var saved = EmitCallbackAndStashThisArg(emitter, arguments);
109112
il.Emit(OpCodes.Call, ctx.Runtime!.ArrayForEach);
110113
EmitRestoreCallbackThisArg(emitter, saved);
111-
il.Emit(OpCodes.Ldnull); // forEach returns undefined
114+
il.Emit(OpCodes.Ldsfld, ctx.Runtime!.UndefinedInstance);
112115
break;
113116
}
114117

Compilation/Emitters/MapEmitter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public bool TryEmitMethodCall(IEmitterContext emitter, Expr receiver, string met
6666
case "forEach":
6767
EmitSingleArgOrNull(emitter, arguments);
6868
il.Emit(OpCodes.Call, ctx.Runtime!.MapForEach);
69-
il.Emit(OpCodes.Ldnull); // forEach returns undefined
69+
il.Emit(OpCodes.Ldsfld, ctx.Runtime!.UndefinedInstance);
7070
return true;
7171

7272
default:

Compilation/Emitters/PromiseStaticEmitter.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,18 @@ public bool TryEmitStaticPropertyGet(IEmitterContext emitter, string propertyNam
124124
var runtime = ctx.Runtime!;
125125
// Stage 4y: Promise.* statics as values for `let r = Promise.resolve;
126126
// r(42).then(...)` patterns + test262 isConstructor harness.
127+
// ECMA-262 §27.2.5.1: value-form Promise.resolve/reject route through
128+
// wrappers that validate `this` is Object before delegating. Direct
129+
// syntactic dispatch (`Promise.resolve(x)`) skips the check (since
130+
// `Promise` is always an Object).
127131
MethodInfo? method = propertyName switch
128132
{
129-
"resolve" => runtime.PromiseResolve,
130-
"reject" => runtime.PromiseReject,
131-
"all" => runtime.PromiseAll,
132-
"race" => runtime.PromiseRace,
133-
"allSettled" => runtime.PromiseAllSettled,
134-
"any" => runtime.PromiseAny,
133+
"resolve" => runtime.PromiseResolveStatic,
134+
"reject" => runtime.PromiseRejectStatic,
135+
"all" => runtime.PromiseAllStatic,
136+
"race" => runtime.PromiseRaceStatic,
137+
"allSettled" => runtime.PromiseAllSettledStatic,
138+
"any" => runtime.PromiseAnyStatic,
135139
"withResolvers" => runtime.PromiseWithResolvers,
136140
_ => null
137141
};

Compilation/Emitters/SetEmitter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public bool TryEmitMethodCall(IEmitterContext emitter, Expr receiver, string met
6060
case "forEach":
6161
EmitSingleArgOrNull(emitter, arguments);
6262
il.Emit(OpCodes.Call, ctx.Runtime!.SetForEach);
63-
il.Emit(OpCodes.Ldnull); // forEach returns undefined
63+
il.Emit(OpCodes.Ldsfld, ctx.Runtime!.UndefinedInstance);
6464
return true;
6565

6666
// ES2025 Set Operations

Compilation/Emitters/StringEmitter.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,10 @@ private static void EmitSplit(IEmitterContext emitter, List<Expr> arguments)
361361
}
362362
else
363363
{
364-
il.Emit(OpCodes.Ldstr, "");
364+
// ECMA-262 22.1.3.21 step 4: missing separator is `undefined`,
365+
// not `""`. The helper's undefined-arm returns [str], so push
366+
// the $Undefined singleton.
367+
il.Emit(OpCodes.Ldsfld, ctx.Runtime!.UndefinedInstance);
365368
}
366369
il.Emit(OpCodes.Call, ctx.Runtime!.StringSplitRegExp);
367370

Compilation/ExpressionEmitterBase.CallHelpers.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,14 +1224,15 @@ protected void EmitAmbiguousMethodCall(Expr obj, string methodName, List<Expr> a
12241224
IL.Emit(OpCodes.Box, typeof(bool));
12251225
break;
12261226
case "indexOf":
1227-
if (arguments.Count > 0) { EmitExpression(arguments[0]); EnsureBoxed(); IL.Emit(OpCodes.Castclass, typeof(string)); }
1228-
else { IL.Emit(OpCodes.Ldstr, ""); }
1227+
// ECMA-262 §22.1.3.8 step 3: searchString = ? ToString(searchString).
1228+
if (arguments.Count > 0) { EmitExpression(arguments[0]); EnsureBoxed(); IL.Emit(OpCodes.Call, Ctx.Runtime!.ToJsString); }
1229+
else { IL.Emit(OpCodes.Ldstr, "undefined"); }
12291230
IL.Emit(OpCodes.Call, Ctx.Runtime!.StringIndexOf);
12301231
IL.Emit(OpCodes.Box, typeof(double));
12311232
break;
12321233
case "lastIndexOf":
1233-
if (arguments.Count > 0) { EmitExpression(arguments[0]); EnsureBoxed(); IL.Emit(OpCodes.Castclass, typeof(string)); }
1234-
else { IL.Emit(OpCodes.Ldstr, ""); }
1234+
if (arguments.Count > 0) { EmitExpression(arguments[0]); EnsureBoxed(); IL.Emit(OpCodes.Call, Ctx.Runtime!.ToJsString); }
1235+
else { IL.Emit(OpCodes.Ldstr, "undefined"); }
12351236
IL.Emit(OpCodes.Call, Ctx.Runtime!.StringLastIndexOf);
12361237
IL.Emit(OpCodes.Box, typeof(double));
12371238
break;

Compilation/ILEmitter.Calls.AmbiguousDispatch.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,12 @@ public partial class ILEmitter
8888
{
8989
EmitExpression(arguments[0]);
9090
EmitBoxIfNeeded(arguments[0]);
91-
IL.Emit(OpCodes.Castclass, _ctx.Types.String);
91+
// ECMA-262 §22.1.3.8 step 3: searchString = ? ToString(searchString).
92+
// Castclass-to-string would either NRE on null (→ String.IndexOf
93+
// throws ArgumentNullException) or InvalidCastException on
94+
// $Undefined/other objects. ToJsString handles all primitives
95+
// and invokes the @@toPrimitive/toString protocol for objects.
96+
IL.Emit(OpCodes.Call, _ctx.Runtime!.ToJsString);
9297
}
9398
else
9499
{
@@ -118,7 +123,8 @@ public partial class ILEmitter
118123
{
119124
EmitExpression(arguments[0]);
120125
EmitBoxIfNeeded(arguments[0]);
121-
IL.Emit(OpCodes.Castclass, _ctx.Types.String);
126+
// ECMA-262 §22.1.3.9 step 3: searchString = ? ToString(searchString).
127+
IL.Emit(OpCodes.Call, _ctx.Runtime!.ToJsString);
122128
}
123129
else
124130
{

Compilation/ILEmitter.Calls.NumberMethods.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,14 +147,17 @@ private void EmitNumberMethodCall(Expr obj, string methodName, List<Expr> argume
147147
break;
148148

149149
case "toExponential":
150+
// 0-arg → JS `undefined` (shortest-form branch). Push
151+
// UndefinedInstance, not Ldnull (null coerces to 0). Same
152+
// distinction as the typed-double dispatch site above.
150153
if (arguments.Count > 0)
151154
{
152155
EmitExpression(arguments[0]);
153156
EmitBoxIfNeeded(arguments[0]);
154157
}
155158
else
156159
{
157-
IL.Emit(OpCodes.Ldnull);
160+
IL.Emit(OpCodes.Ldsfld, _ctx.Runtime!.UndefinedInstance);
158161
}
159162
IL.Emit(OpCodes.Call, _ctx.Runtime!.NumberToExponential);
160163
break;

Compilation/ILEmitter.Calls.StringMethods.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,10 @@ private void EmitStringOnlyMethodCall(Expr obj, string methodName, List<Expr> ar
151151
}
152152
else
153153
{
154-
IL.Emit(OpCodes.Ldstr, "");
154+
// ECMA-262 22.1.3.21 step 4: missing separator is
155+
// `undefined`, not `""`. Push the $Undefined singleton so
156+
// the helper's undefined-arm returns [str].
157+
IL.Emit(OpCodes.Ldsfld, _ctx.Runtime!.UndefinedInstance);
155158
}
156159
IL.Emit(OpCodes.Call, _ctx.Runtime!.StringSplitRegExp);
157160
// ECMA-262 22.1.3.21 step 6: optional `limit` argument truncates

0 commit comments

Comments
 (0)