From 44a8208655d03b83935f7c08f088dea149623b6a Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 15 Jun 2026 20:48:33 -0700 Subject: [PATCH] Test262: fromCodePoint surrogates (#108), call arg-order (#106), Object.create proto+isPrototypeOf (#104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #108 — String.fromCodePoint rejected lone surrogates (0xD800-0xDFFF) because char.ConvertFromUtf32 throws on them. Implement ECMA-262 §11.1.3 UTF16EncodeCodePoint in both modes (cp<=0xFFFF -> single code unit incl. lone surrogates; else surrogate pair). Also enforce the §22.1.2.2 integral check in the interpreter. Clears all RuntimeErrors in RegExp/CharacterClassEscapes; the remaining \w/\s failures are the .NET ECMAScript-shorthand divergence (#693). #106 — Compiled `o.bar.gar(foo())` evaluated arguments before the member-access callee threw. Per §13.3.2.1/§13.3.6.1, evaluating `recv.method` runs RequireObjectCoercible(recv) before ArgumentListEvaluation, so an undefined receiver must throw TypeError before the args' side effects. Add EmitThrowIfReceiverUndefined on the method-call path (EmitMethodCall + EmitGetPropertyAndInvoke). Guards on $Undefined only — compiled sloppy `this` is null and must stay coercible (avoids regressing this.bar(foo()), 11.2.3-3_8). #104 — Interpreter Object.create lagged compiled. Two fixes: (1) validate the prototype argument (§20.1.2.2 step 1) and throw a real SharpTSTypeError for a non-Object/non-null proto, matching compiled; (2) Object.prototype.isPrototypeOf was a stub returning false — implement the §20.1.3.4 chain walk. The titled built-in property-bag accessor iteration remains (needs accessor storage on Math/JSON/Date/Error/wrappers) — scoped out and documented on #104. #105 already satisfied both modes; added a regression suite to lock .length + receiver guards (Test262 isn't part of `dotnet test`). Baselines updated only for the folders these changes deterministically affect (Object/create, RegExp/CharacterClassEscapes, expressions/call 11.2.3-3); the rest of the suite had drifted/was flaky under parallel regen and is left for a maintainer full regen. Full unit suite: 12,740 pass. Filed #693 (\w/\s ECMAScript shorthand class semantics) and #694 (interpreter built-ins throwing string-typed TypeError instead of an Error instance). --- .../ExpressionEmitterBase.CallHelpers.cs | 39 ++++++++ Compilation/ILEmitter.Calls.MethodDispatch.cs | 8 ++ Compilation/RuntimeEmitter.Strings.cs | 20 ++++- Runtime/BuiltIns/ObjectBuiltIns.cs | 16 +++- Runtime/BuiltIns/StringBuiltIns.cs | 31 ++++++- Runtime/Types/SharpTSObjectPrototype.cs | 23 ++++- SharpTS.Test262/baselines/compiled.txt | 26 +++--- SharpTS.Test262/baselines/interpreted.txt | 90 +++++++++---------- .../BuiltInMethodLengthAndReceiverTests.cs | 66 ++++++++++++++ .../SharedTests/NonCallableInvocationTests.cs | 60 +++++++++++++ .../SharedTests/ObjectCreateTests.cs | 25 ++++++ .../SharedTests/ObjectPrototypeTests.cs | 22 +++++ .../SharedTests/StringMethodTests.cs | 21 +++++ 13 files changed, 381 insertions(+), 66 deletions(-) create mode 100644 SharpTS.Tests/SharedTests/BuiltInMethodLengthAndReceiverTests.cs diff --git a/Compilation/ExpressionEmitterBase.CallHelpers.cs b/Compilation/ExpressionEmitterBase.CallHelpers.cs index a575b91e..f8d337f4 100644 --- a/Compilation/ExpressionEmitterBase.CallHelpers.cs +++ b/Compilation/ExpressionEmitterBase.CallHelpers.cs @@ -380,6 +380,16 @@ private void EmitDynamicMethodCallPreservingThis(Expr obj, string methodName, Li private void EmitGetPropertyAndInvoke(LocalBuilder objLocal, string methodName, List arguments, LocalBuilder[]? argLocals = null) { + // ECMA-262 §13.3.2.1: evaluating the member-access callee `recv.method` + // performs RequireObjectCoercible(recv), so an undefined receiver throws + // TypeError *before* ArgumentListEvaluation (§13.3.6.1 step 2 runs before + // the callability check in steps 3/4). $Runtime.GetProperty returns + // undefined for a nullish base (so optional chains can short-circuit), + // which would otherwise defer the throw past the arguments and run their + // side effects. Enforce coercibility here, on the non-optional method-call + // path. (test262 language/expressions/call/11.2.3-3_3) + EmitThrowIfReceiverUndefined(objLocal, methodName); + // Resolve the method first (property lookup precedes argument evaluation, per spec) and // store it, so the receiver and resolved fn aren't left on the IL stack while arguments are // evaluated — a later argument can suspend (await/yield), which requires a clear stack. @@ -400,6 +410,35 @@ private void EmitGetPropertyAndInvoke(LocalBuilder objLocal, string methodName, IL.Emit(OpCodes.Call, Ctx.Runtime!.InvokeMethodValue); } + /// + /// Emits a guard that throws TypeError when + /// holds $Undefined — the RequireObjectCoercible step a member-access + /// callee performs before its arguments are evaluated. Missing-property reads + /// (e.g. o.bar where bar is absent) yield $Undefined, so + /// this catches o.bar.gar(sideEffect()) before the side effect runs. + /// + /// Deliberately does NOT reject a bare CLR null: compiled sloppy-mode + /// this is represented as null (spec says it is globalThis, which + /// is coercible), so this.method(sideEffect()) must keep evaluating its + /// arguments. Symbols/primitives are coercible too. A genuine null.x() + /// still throws — just deferred to InvokeMethodValue, as before this guard. + /// + protected void EmitThrowIfReceiverUndefined(LocalBuilder objLocal, string methodName) + { + var okLabel = IL.DefineLabel(); + + IL.Emit(OpCodes.Ldloc, objLocal); + IL.Emit(OpCodes.Isinst, Ctx.Runtime!.UndefinedType); + IL.Emit(OpCodes.Brfalse, okLabel); // not $Undefined → ok + + IL.Emit(OpCodes.Ldstr, $"Cannot read properties of undefined (reading '{methodName}')"); + IL.Emit(OpCodes.Newobj, Ctx.Runtime!.TSTypeErrorCtor); + IL.Emit(OpCodes.Call, Ctx.Runtime!.CreateException); + IL.Emit(OpCodes.Throw); + + IL.MarkLabel(okLabel); + } + /// /// Builds an object[] of the boxed call arguments on the stack. Stack: [] -> [object[]]. /// When is non-null each element is loaded from those pre-spilled, diff --git a/Compilation/ILEmitter.Calls.MethodDispatch.cs b/Compilation/ILEmitter.Calls.MethodDispatch.cs index b6961a6f..3d1b24c1 100644 --- a/Compilation/ILEmitter.Calls.MethodDispatch.cs +++ b/Compilation/ILEmitter.Calls.MethodDispatch.cs @@ -182,6 +182,14 @@ private void EmitMethodCall(Expr.Get methodGet, List arguments) var receiverLocal = IL.DeclareLocal(_ctx.Types.Object); IL.Emit(OpCodes.Stloc, receiverLocal); + // ECMA-262 §13.3.2.1 / §13.3.6.1: the member-access callee `recv.method` + // runs RequireObjectCoercible(recv) before ArgumentListEvaluation, so an + // undefined receiver throws TypeError *before* the arguments' side effects + // fire. GetProperty/InvokeMethodValue would otherwise defer the throw until + // after the args are built. (test262 .../call/11.2.3-3_3) + if (!methodGet.Optional) + EmitThrowIfReceiverUndefined(receiverLocal, methodName); + // Load receiver for InvokeMethodValue's first argument IL.Emit(OpCodes.Ldloc, receiverLocal); diff --git a/Compilation/RuntimeEmitter.Strings.cs b/Compilation/RuntimeEmitter.Strings.cs index cc67b4ce..c0efb6b4 100644 --- a/Compilation/RuntimeEmitter.Strings.cs +++ b/Compilation/RuntimeEmitter.Strings.cs @@ -1719,10 +1719,28 @@ private void EmitStringFromCodePoint(TypeBuilder typeBuilder, EmittedRuntime run il.MarkLabel(validLabel); - // result = string.Concat(result, Char.ConvertFromUtf32(codePoint)) + // result = string.Concat(result, ) + // ECMA-262 §11.1.3: char.ConvertFromUtf32 rejects lone surrogates + // (0xD800–0xDFFF), but fromCodePoint must emit them as single UTF-16 + // code units. Code points <= 0xFFFF (incl. lone surrogates) become one + // char; supplementary code points (> 0xFFFF, never a surrogate) go + // through ConvertFromUtf32. il.Emit(OpCodes.Ldloc, resultLocal); + var supplementaryLabel = il.DefineLabel(); + var segmentReadyLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, codePointLocal); + il.Emit(OpCodes.Ldc_I4, 0xFFFF); + il.Emit(OpCodes.Bgt, supplementaryLabel); + // cp <= 0xFFFF → char.ToString((char)cp) + il.Emit(OpCodes.Ldloc, codePointLocal); + il.Emit(OpCodes.Conv_U2); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Char, "ToString", _types.Char)); + il.Emit(OpCodes.Br, segmentReadyLabel); + // cp > 0xFFFF → char.ConvertFromUtf32(cp) + il.MarkLabel(supplementaryLabel); il.Emit(OpCodes.Ldloc, codePointLocal); il.Emit(OpCodes.Call, _types.GetMethod(_types.Char, "ConvertFromUtf32", _types.Int32)); + il.MarkLabel(segmentReadyLabel); il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "Concat", _types.String, _types.String)); il.Emit(OpCodes.Stloc, resultLocal); diff --git a/Runtime/BuiltIns/ObjectBuiltIns.cs b/Runtime/BuiltIns/ObjectBuiltIns.cs index 7c50c646..b4cfe332 100644 --- a/Runtime/BuiltIns/ObjectBuiltIns.cs +++ b/Runtime/BuiltIns/ObjectBuiltIns.cs @@ -1438,6 +1438,17 @@ private static List GetPropertyNamesFromList(System.Collections.IList li var proto = args[0]; var propertiesObject = args.Count > 1 ? args[1] : null; + // ECMA-262 §20.1.2.2 step 1: if Type(O) is neither Object nor Null, + // throw TypeError. Object.create(undefined/number/string/bool/symbol/…) + // throws; only a real object or null is a valid prototype. (Mirrors the + // compiled-mode $Runtime.ObjectCreate guard.) Thrown as a real + // SharpTSTypeError so guest `assert.throws(TypeError, …)` / `instanceof` + // see a TypeError instance, not a bare string. + if (proto is SharpTSUndefined or double or int or long or bool or string + or System.Numerics.BigInteger or SharpTSSymbol) + throw new Runtime.Exceptions.ThrowException( + new SharpTSTypeError("Object prototype may only be an Object or null")); + // ECMA-262 §20.1.2.2 step 2: Let obj be OrdinaryObjectCreate(O). // OrdinaryObjectCreate creates a FRESH object whose [[Prototype]] is // O — it does NOT copy O's own properties. Inherited properties are @@ -1451,8 +1462,9 @@ private static List GetPropertyNamesFromList(System.Collections.IList li result.Prototype = proto; // Object.create(null) → a null-prototype object that inherits nothing // (not even Object.prototype's methods). Distinguishes it from an - // ordinary object, whose Prototype is also null by default. - if (proto is null or SharpTSUndefined) + // ordinary object, whose Prototype is also null by default. (undefined + // is rejected by the Object-or-Null guard above, so only null reaches here.) + if (proto is null) result.IsNullPrototype = true; // If propertiesObject is provided, define properties using defineProperty semantics diff --git a/Runtime/BuiltIns/StringBuiltIns.cs b/Runtime/BuiltIns/StringBuiltIns.cs index 02361f59..78ea243e 100644 --- a/Runtime/BuiltIns/StringBuiltIns.cs +++ b/Runtime/BuiltIns/StringBuiltIns.cs @@ -242,14 +242,37 @@ private static RuntimeValue FromCodePointV2(Interpreter _, RuntimeValue receiver var sb = new StringBuilder(); foreach (var arg in args) { - var codePoint = (int)arg.AsNumber(); - if (codePoint < 0 || codePoint > 0x10FFFF) - throw new Exception($"RangeError: Invalid code point {codePoint}"); - sb.Append(char.ConvertFromUtf32(codePoint)); + // ECMA-262 §22.1.2.2: each code point must be an integral Number in + // [0, 0x10FFFF]; NaN / Infinity / fractional values throw RangeError. + var num = arg.AsNumber(); + if (!double.IsInteger(num) || num < 0 || num > 0x10FFFF) + throw new Exception($"RangeError: Invalid code point {num}"); + AppendCodePoint(sb, (int)num); } return RuntimeValue.FromString(sb.ToString()); } + /// + /// ECMA-262 §11.1.3 UTF16EncodeCodePoint. Unlike , + /// this accepts lone surrogates (0xD800–0xDFFF): JS strings are sequences of + /// UTF-16 code units, so String.fromCodePoint(0xDC00) yields a one-unit + /// string holding that surrogate. .NET strings are likewise UTF-16, so a lone + /// surrogate is representable as a single . + /// + internal static void AppendCodePoint(StringBuilder sb, int cp) + { + if (cp <= 0xFFFF) + { + sb.Append((char)cp); + } + else + { + cp -= 0x10000; + sb.Append((char)((cp >> 10) + 0xD800)); + sb.Append((char)((cp & 0x3FF) + 0xDC00)); + } + } + #region V2 Implementations (RuntimeValue — no boxing) private static RuntimeValue CharAtV2(Interpreter _, string str, ReadOnlySpan args) diff --git a/Runtime/Types/SharpTSObjectPrototype.cs b/Runtime/Types/SharpTSObjectPrototype.cs index 0a549ff4..78fcf9b4 100644 --- a/Runtime/Types/SharpTSObjectPrototype.cs +++ b/Runtime/Types/SharpTSObjectPrototype.cs @@ -144,7 +144,28 @@ private SharpTSObjectUnboundMethod(string name, Func, obj private static object? ValueOfImpl(object? target, List args) => target; - private static object? IsPrototypeOfImpl(object? target, List args) => false; + private static object? IsPrototypeOfImpl(object? target, List args) + { + // ECMA-262 §20.1.3.4 Object.prototype.isPrototypeOf(V): walk V's + // prototype chain and return true if `this` (target) appears in it. + // (Was stubbed to always return false.) + if (args.Count == 0) return false; + var v = args[0]; + // Step 1: if Type(V) is not Object, return false — primitives have no chain. + if (v is null or SharpTSUndefined or double or int or long or bool or string + or System.Numerics.BigInteger or SharpTSSymbol) + return false; + + while (true) + { + object? proto; + try { proto = ObjectBuiltIns.RuntimeGetPrototypeOf(v); } + catch { return false; } + if (proto is null or SharpTSUndefined) return false; + if (ReferenceEquals(proto, target)) return true; + v = proto; + } + } private static object? PropertyIsEnumerableImpl(object? target, List args) { diff --git a/SharpTS.Test262/baselines/compiled.txt b/SharpTS.Test262/baselines/compiled.txt index 28e37813..b7515122 100644 --- a/SharpTS.Test262/baselines/compiled.txt +++ b/SharpTS.Test262/baselines/compiled.txt @@ -8115,17 +8115,17 @@ test/built-ins/RegExp/15.10.4.1-1.js Pass test/built-ins/RegExp/15.10.4.1-2.js Pass test/built-ins/RegExp/15.10.4.1-3.js Pass test/built-ins/RegExp/15.10.4.1-4.js Pass -test/built-ins/RegExp/CharacterClassEscapes/character-class-digit-class-escape-negative-cases.js RuntimeError +test/built-ins/RegExp/CharacterClassEscapes/character-class-digit-class-escape-negative-cases.js Pass test/built-ins/RegExp/CharacterClassEscapes/character-class-digit-class-escape-positive-cases.js Pass test/built-ins/RegExp/CharacterClassEscapes/character-class-non-digit-class-escape-negative-cases.js Pass -test/built-ins/RegExp/CharacterClassEscapes/character-class-non-digit-class-escape-positive-cases.js RuntimeError +test/built-ins/RegExp/CharacterClassEscapes/character-class-non-digit-class-escape-positive-cases.js Pass test/built-ins/RegExp/CharacterClassEscapes/character-class-non-whitespace-class-escape-negative-cases.js Fail -test/built-ins/RegExp/CharacterClassEscapes/character-class-non-whitespace-class-escape-positive-cases.js RuntimeError +test/built-ins/RegExp/CharacterClassEscapes/character-class-non-whitespace-class-escape-positive-cases.js Pass test/built-ins/RegExp/CharacterClassEscapes/character-class-non-word-class-escape-negative-cases.js Pass -test/built-ins/RegExp/CharacterClassEscapes/character-class-non-word-class-escape-positive-cases.js RuntimeError -test/built-ins/RegExp/CharacterClassEscapes/character-class-whitespace-class-escape-negative-cases.js RuntimeError +test/built-ins/RegExp/CharacterClassEscapes/character-class-non-word-class-escape-positive-cases.js Fail +test/built-ins/RegExp/CharacterClassEscapes/character-class-whitespace-class-escape-negative-cases.js Pass test/built-ins/RegExp/CharacterClassEscapes/character-class-whitespace-class-escape-positive-cases.js Fail -test/built-ins/RegExp/CharacterClassEscapes/character-class-word-class-escape-negative-cases.js RuntimeError +test/built-ins/RegExp/CharacterClassEscapes/character-class-word-class-escape-negative-cases.js Fail test/built-ins/RegExp/CharacterClassEscapes/character-class-word-class-escape-positive-cases.js Pass test/built-ins/RegExp/S15.10.1_A1_T1.js Pass test/built-ins/RegExp/S15.10.1_A1_T10.js Pass @@ -11211,14 +11211,14 @@ test/built-ins/String/raw/template-substitutions-are-appended-on-same-index.js P test/built-ins/String/raw/zero-literal-segments.js Pass test/built-ins/String/symbol-string-coercion.js RuntimeError test/built-ins/String/symbol-wrapping.js Pass -test/language/expressions/call/11.2.3-3_1.js Fail -test/language/expressions/call/11.2.3-3_2.js Fail -test/language/expressions/call/11.2.3-3_3.js Fail -test/language/expressions/call/11.2.3-3_4.js Fail +test/language/expressions/call/11.2.3-3_1.js Pass +test/language/expressions/call/11.2.3-3_2.js Pass +test/language/expressions/call/11.2.3-3_3.js Pass +test/language/expressions/call/11.2.3-3_4.js Pass test/language/expressions/call/11.2.3-3_5.js Fail -test/language/expressions/call/11.2.3-3_6.js Fail -test/language/expressions/call/11.2.3-3_7.js Fail -test/language/expressions/call/11.2.3-3_8.js Fail +test/language/expressions/call/11.2.3-3_6.js Pass +test/language/expressions/call/11.2.3-3_7.js Pass +test/language/expressions/call/11.2.3-3_8.js Pass test/language/expressions/call/S11.2.3_A1.js Pass test/language/expressions/call/S11.2.3_A2.js Pass test/language/expressions/call/S11.2.3_A3_T1.js Fail diff --git a/SharpTS.Test262/baselines/interpreted.txt b/SharpTS.Test262/baselines/interpreted.txt index de478e8f..323b9558 100644 --- a/SharpTS.Test262/baselines/interpreted.txt +++ b/SharpTS.Test262/baselines/interpreted.txt @@ -4116,15 +4116,15 @@ test/built-ins/Object/assign/target-set-user-error.js Fail test/built-ins/Object/bigint.js RuntimeError test/built-ins/Object/create/15.2.3.5-0-1.js Pass test/built-ins/Object/create/15.2.3.5-0-2.js Fail -test/built-ins/Object/create/15.2.3.5-1-1.js Fail +test/built-ins/Object/create/15.2.3.5-1-1.js Pass test/built-ins/Object/create/15.2.3.5-1-2.js Pass -test/built-ins/Object/create/15.2.3.5-1-3.js Fail -test/built-ins/Object/create/15.2.3.5-1-4.js Fail -test/built-ins/Object/create/15.2.3.5-1.js Fail +test/built-ins/Object/create/15.2.3.5-1-3.js Pass +test/built-ins/Object/create/15.2.3.5-1-4.js Pass +test/built-ins/Object/create/15.2.3.5-1.js Pass test/built-ins/Object/create/15.2.3.5-2-1.js Pass -test/built-ins/Object/create/15.2.3.5-2-2.js Fail -test/built-ins/Object/create/15.2.3.5-3-1.js Fail -test/built-ins/Object/create/15.2.3.5-4-1.js Fail +test/built-ins/Object/create/15.2.3.5-2-2.js Pass +test/built-ins/Object/create/15.2.3.5-3-1.js Pass +test/built-ins/Object/create/15.2.3.5-4-1.js Pass test/built-ins/Object/create/15.2.3.5-4-10.js RuntimeError test/built-ins/Object/create/15.2.3.5-4-100.js Pass test/built-ins/Object/create/15.2.3.5-4-101.js Pass @@ -4141,9 +4141,9 @@ test/built-ins/Object/create/15.2.3.5-4-110.js Fail test/built-ins/Object/create/15.2.3.5-4-111.js Pass test/built-ins/Object/create/15.2.3.5-4-112.js Pass test/built-ins/Object/create/15.2.3.5-4-113.js Pass -test/built-ins/Object/create/15.2.3.5-4-114.js RuntimeError -test/built-ins/Object/create/15.2.3.5-4-115.js RuntimeError -test/built-ins/Object/create/15.2.3.5-4-116.js RuntimeError +test/built-ins/Object/create/15.2.3.5-4-114.js Pass +test/built-ins/Object/create/15.2.3.5-4-115.js Pass +test/built-ins/Object/create/15.2.3.5-4-116.js Pass test/built-ins/Object/create/15.2.3.5-4-117.js Pass test/built-ins/Object/create/15.2.3.5-4-118.js RuntimeError test/built-ins/Object/create/15.2.3.5-4-119.js Pass @@ -4180,7 +4180,7 @@ test/built-ins/Object/create/15.2.3.5-4-147.js Pass test/built-ins/Object/create/15.2.3.5-4-149.js RuntimeError test/built-ins/Object/create/15.2.3.5-4-15.js Fail test/built-ins/Object/create/15.2.3.5-4-150.js Pass -test/built-ins/Object/create/15.2.3.5-4-151.js Fail +test/built-ins/Object/create/15.2.3.5-4-151.js Pass test/built-ins/Object/create/15.2.3.5-4-152.js Pass test/built-ins/Object/create/15.2.3.5-4-153.js Pass test/built-ins/Object/create/15.2.3.5-4-154.js Pass @@ -4195,11 +4195,11 @@ test/built-ins/Object/create/15.2.3.5-4-161.js Fail test/built-ins/Object/create/15.2.3.5-4-162.js Pass test/built-ins/Object/create/15.2.3.5-4-163.js Pass test/built-ins/Object/create/15.2.3.5-4-164.js Pass -test/built-ins/Object/create/15.2.3.5-4-165.js RuntimeError +test/built-ins/Object/create/15.2.3.5-4-165.js Pass test/built-ins/Object/create/15.2.3.5-4-166.js Fail -test/built-ins/Object/create/15.2.3.5-4-167.js RuntimeError -test/built-ins/Object/create/15.2.3.5-4-168.js RuntimeError -test/built-ins/Object/create/15.2.3.5-4-169.js RuntimeError +test/built-ins/Object/create/15.2.3.5-4-167.js Pass +test/built-ins/Object/create/15.2.3.5-4-168.js Pass +test/built-ins/Object/create/15.2.3.5-4-169.js Pass test/built-ins/Object/create/15.2.3.5-4-17.js Fail test/built-ins/Object/create/15.2.3.5-4-170.js Fail test/built-ins/Object/create/15.2.3.5-4-171.js RuntimeError @@ -4223,16 +4223,16 @@ test/built-ins/Object/create/15.2.3.5-4-188.js Pass test/built-ins/Object/create/15.2.3.5-4-189.js Fail test/built-ins/Object/create/15.2.3.5-4-19.js Fail test/built-ins/Object/create/15.2.3.5-4-190.js Fail -test/built-ins/Object/create/15.2.3.5-4-191.js RuntimeError +test/built-ins/Object/create/15.2.3.5-4-191.js Pass test/built-ins/Object/create/15.2.3.5-4-192.js Fail -test/built-ins/Object/create/15.2.3.5-4-193.js RuntimeError -test/built-ins/Object/create/15.2.3.5-4-194.js RuntimeError -test/built-ins/Object/create/15.2.3.5-4-195.js RuntimeError +test/built-ins/Object/create/15.2.3.5-4-193.js Pass +test/built-ins/Object/create/15.2.3.5-4-194.js Pass +test/built-ins/Object/create/15.2.3.5-4-195.js Pass test/built-ins/Object/create/15.2.3.5-4-196.js Fail test/built-ins/Object/create/15.2.3.5-4-197.js RuntimeError test/built-ins/Object/create/15.2.3.5-4-198.js Fail test/built-ins/Object/create/15.2.3.5-4-199.js RuntimeError -test/built-ins/Object/create/15.2.3.5-4-2.js Fail +test/built-ins/Object/create/15.2.3.5-4-2.js Pass test/built-ins/Object/create/15.2.3.5-4-20.js Pass test/built-ins/Object/create/15.2.3.5-4-200.js Fail test/built-ins/Object/create/15.2.3.5-4-201.js Fail @@ -4252,10 +4252,10 @@ test/built-ins/Object/create/15.2.3.5-4-214.js Pass test/built-ins/Object/create/15.2.3.5-4-215.js Pass test/built-ins/Object/create/15.2.3.5-4-216.js Pass test/built-ins/Object/create/15.2.3.5-4-217.js Pass -test/built-ins/Object/create/15.2.3.5-4-218.js Fail -test/built-ins/Object/create/15.2.3.5-4-219.js Fail +test/built-ins/Object/create/15.2.3.5-4-218.js Pass +test/built-ins/Object/create/15.2.3.5-4-219.js Pass test/built-ins/Object/create/15.2.3.5-4-22.js Pass -test/built-ins/Object/create/15.2.3.5-4-220.js Fail +test/built-ins/Object/create/15.2.3.5-4-220.js Pass test/built-ins/Object/create/15.2.3.5-4-221.js Pass test/built-ins/Object/create/15.2.3.5-4-222.js Pass test/built-ins/Object/create/15.2.3.5-4-223.js Pass @@ -4265,7 +4265,7 @@ test/built-ins/Object/create/15.2.3.5-4-226.js Pass test/built-ins/Object/create/15.2.3.5-4-228.js RuntimeError test/built-ins/Object/create/15.2.3.5-4-229.js Pass test/built-ins/Object/create/15.2.3.5-4-23.js Pass -test/built-ins/Object/create/15.2.3.5-4-230.js Fail +test/built-ins/Object/create/15.2.3.5-4-230.js Pass test/built-ins/Object/create/15.2.3.5-4-231.js Pass test/built-ins/Object/create/15.2.3.5-4-232.js Pass test/built-ins/Object/create/15.2.3.5-4-233.js Pass @@ -4282,9 +4282,9 @@ test/built-ins/Object/create/15.2.3.5-4-242.js Pass test/built-ins/Object/create/15.2.3.5-4-243.js Pass test/built-ins/Object/create/15.2.3.5-4-244.js Fail test/built-ins/Object/create/15.2.3.5-4-245.js Fail -test/built-ins/Object/create/15.2.3.5-4-246.js RuntimeError -test/built-ins/Object/create/15.2.3.5-4-247.js RuntimeError -test/built-ins/Object/create/15.2.3.5-4-248.js RuntimeError +test/built-ins/Object/create/15.2.3.5-4-246.js Pass +test/built-ins/Object/create/15.2.3.5-4-247.js Pass +test/built-ins/Object/create/15.2.3.5-4-248.js Pass test/built-ins/Object/create/15.2.3.5-4-249.js RuntimeError test/built-ins/Object/create/15.2.3.5-4-25.js Fail test/built-ins/Object/create/15.2.3.5-4-250.js Fail @@ -4318,9 +4318,9 @@ test/built-ins/Object/create/15.2.3.5-4-278.js Pass test/built-ins/Object/create/15.2.3.5-4-279.js Fail test/built-ins/Object/create/15.2.3.5-4-28.js Fail test/built-ins/Object/create/15.2.3.5-4-280.js Fail -test/built-ins/Object/create/15.2.3.5-4-281.js RuntimeError -test/built-ins/Object/create/15.2.3.5-4-282.js RuntimeError -test/built-ins/Object/create/15.2.3.5-4-283.js RuntimeError +test/built-ins/Object/create/15.2.3.5-4-281.js Fail +test/built-ins/Object/create/15.2.3.5-4-282.js Fail +test/built-ins/Object/create/15.2.3.5-4-283.js Fail test/built-ins/Object/create/15.2.3.5-4-284.js Fail test/built-ins/Object/create/15.2.3.5-4-285.js RuntimeError test/built-ins/Object/create/15.2.3.5-4-286.js Fail @@ -4337,7 +4337,7 @@ test/built-ins/Object/create/15.2.3.5-4-296.js Fail test/built-ins/Object/create/15.2.3.5-4-297.js Fail test/built-ins/Object/create/15.2.3.5-4-298.js Fail test/built-ins/Object/create/15.2.3.5-4-3.js Fail -test/built-ins/Object/create/15.2.3.5-4-30.js RuntimeError +test/built-ins/Object/create/15.2.3.5-4-30.js Pass test/built-ins/Object/create/15.2.3.5-4-300.js RuntimeError test/built-ins/Object/create/15.2.3.5-4-301.js Fail test/built-ins/Object/create/15.2.3.5-4-302.js Fail @@ -4348,7 +4348,7 @@ test/built-ins/Object/create/15.2.3.5-4-306.js Pass test/built-ins/Object/create/15.2.3.5-4-307.js Pass test/built-ins/Object/create/15.2.3.5-4-308.js Fail test/built-ins/Object/create/15.2.3.5-4-309.js Pass -test/built-ins/Object/create/15.2.3.5-4-31.js RuntimeError +test/built-ins/Object/create/15.2.3.5-4-31.js Pass test/built-ins/Object/create/15.2.3.5-4-310.js Fail test/built-ins/Object/create/15.2.3.5-4-311.js Fail test/built-ins/Object/create/15.2.3.5-4-312.js Pass @@ -4356,7 +4356,7 @@ test/built-ins/Object/create/15.2.3.5-4-313.js Pass test/built-ins/Object/create/15.2.3.5-4-314.js Pass test/built-ins/Object/create/15.2.3.5-4-315.js Fail test/built-ins/Object/create/15.2.3.5-4-316.js Fail -test/built-ins/Object/create/15.2.3.5-4-32.js RuntimeError +test/built-ins/Object/create/15.2.3.5-4-32.js Pass test/built-ins/Object/create/15.2.3.5-4-33.js Fail test/built-ins/Object/create/15.2.3.5-4-34.js RuntimeError test/built-ins/Object/create/15.2.3.5-4-35.js Fail @@ -4388,16 +4388,16 @@ test/built-ins/Object/create/15.2.3.5-4-58.js Fail test/built-ins/Object/create/15.2.3.5-4-59.js Pass test/built-ins/Object/create/15.2.3.5-4-6.js Fail test/built-ins/Object/create/15.2.3.5-4-60.js Pass -test/built-ins/Object/create/15.2.3.5-4-61.js RuntimeError -test/built-ins/Object/create/15.2.3.5-4-62.js RuntimeError -test/built-ins/Object/create/15.2.3.5-4-63.js RuntimeError +test/built-ins/Object/create/15.2.3.5-4-61.js Pass +test/built-ins/Object/create/15.2.3.5-4-62.js Pass +test/built-ins/Object/create/15.2.3.5-4-63.js Pass test/built-ins/Object/create/15.2.3.5-4-64.js Pass test/built-ins/Object/create/15.2.3.5-4-65.js RuntimeError test/built-ins/Object/create/15.2.3.5-4-66.js Pass test/built-ins/Object/create/15.2.3.5-4-67.js RuntimeError test/built-ins/Object/create/15.2.3.5-4-68.js Pass test/built-ins/Object/create/15.2.3.5-4-69.js Pass -test/built-ins/Object/create/15.2.3.5-4-7.js RuntimeError +test/built-ins/Object/create/15.2.3.5-4-7.js Fail test/built-ins/Object/create/15.2.3.5-4-71.js RuntimeError test/built-ins/Object/create/15.2.3.5-4-72.js Fail test/built-ins/Object/create/15.2.3.5-4-73.js Fail @@ -4407,7 +4407,7 @@ test/built-ins/Object/create/15.2.3.5-4-76.js Fail test/built-ins/Object/create/15.2.3.5-4-77.js Fail test/built-ins/Object/create/15.2.3.5-4-78.js Fail test/built-ins/Object/create/15.2.3.5-4-79.js Fail -test/built-ins/Object/create/15.2.3.5-4-8.js RuntimeError +test/built-ins/Object/create/15.2.3.5-4-8.js Fail test/built-ins/Object/create/15.2.3.5-4-80.js Pass test/built-ins/Object/create/15.2.3.5-4-81.js Pass test/built-ins/Object/create/15.2.3.5-4-82.js Fail @@ -4418,7 +4418,7 @@ test/built-ins/Object/create/15.2.3.5-4-86.js Pass test/built-ins/Object/create/15.2.3.5-4-87.js Pass test/built-ins/Object/create/15.2.3.5-4-88.js Pass test/built-ins/Object/create/15.2.3.5-4-89.js Pass -test/built-ins/Object/create/15.2.3.5-4-9.js RuntimeError +test/built-ins/Object/create/15.2.3.5-4-9.js Fail test/built-ins/Object/create/15.2.3.5-4-90.js Pass test/built-ins/Object/create/15.2.3.5-4-91.js Pass test/built-ins/Object/create/15.2.3.5-4-92.js Pass @@ -8115,17 +8115,17 @@ test/built-ins/RegExp/15.10.4.1-1.js Pass test/built-ins/RegExp/15.10.4.1-2.js Pass test/built-ins/RegExp/15.10.4.1-3.js Pass test/built-ins/RegExp/15.10.4.1-4.js Pass -test/built-ins/RegExp/CharacterClassEscapes/character-class-digit-class-escape-negative-cases.js RuntimeError +test/built-ins/RegExp/CharacterClassEscapes/character-class-digit-class-escape-negative-cases.js Pass test/built-ins/RegExp/CharacterClassEscapes/character-class-digit-class-escape-positive-cases.js Pass test/built-ins/RegExp/CharacterClassEscapes/character-class-non-digit-class-escape-negative-cases.js Pass -test/built-ins/RegExp/CharacterClassEscapes/character-class-non-digit-class-escape-positive-cases.js RuntimeError +test/built-ins/RegExp/CharacterClassEscapes/character-class-non-digit-class-escape-positive-cases.js Pass test/built-ins/RegExp/CharacterClassEscapes/character-class-non-whitespace-class-escape-negative-cases.js Fail -test/built-ins/RegExp/CharacterClassEscapes/character-class-non-whitespace-class-escape-positive-cases.js RuntimeError +test/built-ins/RegExp/CharacterClassEscapes/character-class-non-whitespace-class-escape-positive-cases.js Pass test/built-ins/RegExp/CharacterClassEscapes/character-class-non-word-class-escape-negative-cases.js Pass -test/built-ins/RegExp/CharacterClassEscapes/character-class-non-word-class-escape-positive-cases.js RuntimeError -test/built-ins/RegExp/CharacterClassEscapes/character-class-whitespace-class-escape-negative-cases.js RuntimeError +test/built-ins/RegExp/CharacterClassEscapes/character-class-non-word-class-escape-positive-cases.js Timeout +test/built-ins/RegExp/CharacterClassEscapes/character-class-whitespace-class-escape-negative-cases.js Pass test/built-ins/RegExp/CharacterClassEscapes/character-class-whitespace-class-escape-positive-cases.js Fail -test/built-ins/RegExp/CharacterClassEscapes/character-class-word-class-escape-negative-cases.js RuntimeError +test/built-ins/RegExp/CharacterClassEscapes/character-class-word-class-escape-negative-cases.js Timeout test/built-ins/RegExp/CharacterClassEscapes/character-class-word-class-escape-positive-cases.js Pass test/built-ins/RegExp/S15.10.1_A1_T1.js Pass test/built-ins/RegExp/S15.10.1_A1_T10.js Pass diff --git a/SharpTS.Tests/SharedTests/BuiltInMethodLengthAndReceiverTests.cs b/SharpTS.Tests/SharedTests/BuiltInMethodLengthAndReceiverTests.cs new file mode 100644 index 00000000..4149b265 --- /dev/null +++ b/SharpTS.Tests/SharedTests/BuiltInMethodLengthAndReceiverTests.cs @@ -0,0 +1,66 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +/// +/// Locks the behaviors tracked by issue #105 across both modes: +/// (1) built-in prototype methods expose ECMA-262 §17 length (the formal +/// parameter count), and (2) RegExp prototype methods validate their receiver's +/// internal slot and throw TypeError when called on a non-RegExp this. +/// These are pinned in the Test262 baseline (RegExp/prototype/exec/S15.10.6.2_A11, +/// _A2_T1, …) but Test262 is a separate project, so cover them here for fast signal. +/// +public class BuiltInMethodLengthAndReceiverTests +{ + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void RegExpPrototypeMethods_ExposeLength(ExecutionMode mode) + { + var source = """ + console.log(RegExp.prototype.exec.length); + console.log(RegExp.prototype.test.length); + console.log(RegExp.prototype.exec.hasOwnProperty("length")); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("1\n1\ntrue\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void CrossCuttingBuiltInMethods_ExposeLength(ExecutionMode mode) + { + // §17: each built-in function's `length` is its formal parameter count. + var source = """ + console.log(String.prototype.charAt.length); + console.log(String.prototype.slice.length); + console.log(Array.prototype.push.length); + console.log(Array.prototype.indexOf.length); + console.log(Function.prototype.call.length); + """; + + var output = TestHarness.Run(source, mode); + // charAt(pos)=1, slice(start,end)=2, push(...items)=1, indexOf(searchElement,fromIndex?)=1, call(thisArg,...)=1 + Assert.Equal("1\n2\n1\n1\n1\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void RegExpExec_OnNonRegExpReceiver_ThrowsTypeError(ExecutionMode mode) + { + // §22.2.6.x: exec/test require `this` to have the [[RegExpMatcher]] slot; + // a borrowed call on a plain object must throw TypeError. + var source = """ + function check(label: string, fn: () => void) { + try { fn(); console.log(label, "no throw"); } + catch (e: any) { console.log(label, e instanceof TypeError); } + } + check("exec", () => RegExp.prototype.exec.call({}, "x")); + check("test", () => RegExp.prototype.test.call({}, "x")); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("exec true\ntest true\n", output); + } +} diff --git a/SharpTS.Tests/SharedTests/NonCallableInvocationTests.cs b/SharpTS.Tests/SharedTests/NonCallableInvocationTests.cs index 28ad7985..cf165f77 100644 --- a/SharpTS.Tests/SharedTests/NonCallableInvocationTests.cs +++ b/SharpTS.Tests/SharedTests/NonCallableInvocationTests.cs @@ -103,6 +103,66 @@ public void OptionalChainMethodCall_ShortCircuitsWholeChain(ExecutionMode mode) Assert.Equal("undefined\n/x/\nundefined\n", output); } + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void NonCallableMember_EvaluatesArgsBeforeCallabilityCheck(ExecutionMode mode) + { + // ECMA-262 §13.3.6.1: ArgumentListEvaluation (step 2) runs before the + // IsCallable check (steps 3/4). `o.bar(se())` with undefined `o.bar` must + // still evaluate `se()` before throwing. (test262 .../call/11.2.3-3_1) + var source = """ + let n = 0; + function se() { n++; return 1; } + const o: any = {}; + try { o.bar(se()); } catch (e) {} + console.log(n); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("1\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void MemberAccessOnUndefinedCallee_ThrowsBeforeArgs(ExecutionMode mode) + { + // ECMA-262 §13.3.2.1: evaluating the callee `o.bar.gar` calls + // RequireObjectCoercible(o.bar). With `o.bar` undefined that throws during + // callee evaluation, BEFORE the arguments — so `se()` never runs. + // (test262 .../call/11.2.3-3_3; compiled mode formerly deferred the throw.) + var source = """ + let n = 0; + function se() { n++; return 1; } + const o: any = {}; + try { o.bar.gar(se()); } catch (e) {} + console.log(n); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("0\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void SloppyThisMethodCall_EvaluatesArgs(ExecutionMode mode) + { + // `this.bar(se())` in a function called without a receiver: sloppy-mode + // `this` is the (coercible) global object, so accessing `.bar` does not + // throw — `se()` runs, then the undefined `this.bar` fails the callability + // check. Guards against the coercibility check over-firing on the + // compiled null-`this` representation. (test262 .../call/11.2.3-3_8) + var source = """ + let n = 0; + function se() { n++; return 1; } + function f() { (this as any).bar(se()); } + try { f(); } catch (e) {} + console.log(n); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("1\n", output); + } + [Theory] [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] public void ForAwaitBreak_WithoutIteratorReturn_DoesNotThrow(ExecutionMode mode) diff --git a/SharpTS.Tests/SharedTests/ObjectCreateTests.cs b/SharpTS.Tests/SharedTests/ObjectCreateTests.cs index 2b12bffd..b546f2a0 100644 --- a/SharpTS.Tests/SharedTests/ObjectCreateTests.cs +++ b/SharpTS.Tests/SharedTests/ObjectCreateTests.cs @@ -363,4 +363,29 @@ public void Object_Create_MixedPropertyTypes(ExecutionMode mode) var output = TestHarness.Run(source, mode); Assert.Equal("42\nhello\ntrue\n2\n1\n", output); } + + // ECMA-262 §20.1.2.2 step 1: prototype must be Object or null (#104). + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void Object_Create_NonObjectPrototype_ThrowsTypeError(ExecutionMode mode) + { + var source = """ + function attempt(label: string, fn: () => void) { + try { fn(); console.log(label, "no throw"); } + catch (e: any) { console.log(label, e instanceof TypeError); } + } + attempt("undefined", () => Object.create(undefined as any)); + attempt("number", () => Object.create(5 as any)); + attempt("string", () => Object.create("x" as any)); + attempt("bool", () => Object.create(true as any)); + // null and objects are valid prototypes — must NOT throw. + console.log("null", typeof Object.create(null)); + console.log("obj", typeof Object.create({})); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal( + "undefined true\nnumber true\nstring true\nbool true\nnull object\nobj object\n", + output); + } } diff --git a/SharpTS.Tests/SharedTests/ObjectPrototypeTests.cs b/SharpTS.Tests/SharedTests/ObjectPrototypeTests.cs index c4ce77f7..c984b4d2 100644 --- a/SharpTS.Tests/SharedTests/ObjectPrototypeTests.cs +++ b/SharpTS.Tests/SharedTests/ObjectPrototypeTests.cs @@ -452,4 +452,26 @@ class MyClass { """; Assert.Equal("1\n", TestHarness.Run(source, mode)); } + + // ECMA-262 §20.1.3.4 Object.prototype.isPrototypeOf — was stubbed to + // always return false (#104). + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void IsPrototypeOf_WalksPrototypeChain(ExecutionMode mode) + { + var source = """ + function Base() {} + const b = new Base(); + const d = Object.create(b); + const g = Object.create(d); + console.log(b.isPrototypeOf(d)); // direct proto + console.log(b.isPrototypeOf(g)); // transitive + console.log(d.isPrototypeOf(b)); // reverse — false + console.log(({}).isPrototypeOf(d)); // unrelated — false + console.log(b.isPrototypeOf(5 as any)); // non-object arg — false + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("true\ntrue\nfalse\nfalse\nfalse\n", output); + } } diff --git a/SharpTS.Tests/SharedTests/StringMethodTests.cs b/SharpTS.Tests/SharedTests/StringMethodTests.cs index 9d59b7dc..053b823b 100644 --- a/SharpTS.Tests/SharedTests/StringMethodTests.cs +++ b/SharpTS.Tests/SharedTests/StringMethodTests.cs @@ -722,6 +722,27 @@ public void String_FromCodePoint_WithVariables(ExecutionMode mode) Assert.Equal("9731\n", output); } + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void String_FromCodePoint_LoneSurrogates(ExecutionMode mode) + { + // ECMA-262 §22.1.2.2 + §11.1.3 UTF16EncodeCodePoint: lone surrogates + // (0xD800–0xDFFF) are valid code points that encode to a single UTF-16 + // code unit. .NET's char.ConvertFromUtf32 rejects them, so this guards + // the surrogate-aware encoding path (regressed RegExp/CharacterClassEscapes). + var source = """ + const lo = String.fromCodePoint(0xDC00); + console.log(lo.length + " " + lo.charCodeAt(0)); + const hi = String.fromCodePoint(0xD800); + console.log(hi.length + " " + hi.charCodeAt(0)); + console.log(String.fromCodePoint(0x10FFFF).length); + """; + + var output = TestHarness.Run(source, mode); + // Lone low/high surrogates → length-1 strings; max code point → 2 units. + Assert.Equal("1 56320\n1 55296\n2\n", output); + } + #endregion #region String.prototype.codePointAt