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
39 changes: 39 additions & 0 deletions Compilation/ExpressionEmitterBase.CallHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,16 @@ private void EmitDynamicMethodCallPreservingThis(Expr obj, string methodName, Li

private void EmitGetPropertyAndInvoke(LocalBuilder objLocal, string methodName, List<Expr> 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.
Expand All @@ -400,6 +410,35 @@ private void EmitGetPropertyAndInvoke(LocalBuilder objLocal, string methodName,
IL.Emit(OpCodes.Call, Ctx.Runtime!.InvokeMethodValue);
}

/// <summary>
/// Emits a guard that throws <c>TypeError</c> when <paramref name="objLocal"/>
/// holds <c>$Undefined</c> — the RequireObjectCoercible step a member-access
/// callee performs before its arguments are evaluated. Missing-property reads
/// (e.g. <c>o.bar</c> where <c>bar</c> is absent) yield <c>$Undefined</c>, so
/// this catches <c>o.bar.gar(sideEffect())</c> before the side effect runs.
///
/// Deliberately does NOT reject a bare CLR <c>null</c>: compiled sloppy-mode
/// <c>this</c> is represented as <c>null</c> (spec says it is globalThis, which
/// is coercible), so <c>this.method(sideEffect())</c> must keep evaluating its
/// arguments. Symbols/primitives are coercible too. A genuine <c>null.x()</c>
/// still throws — just deferred to InvokeMethodValue, as before this guard.
/// </summary>
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);
}

/// <summary>
/// Builds an <c>object[]</c> of the boxed call arguments on the stack. Stack: [] -&gt; [object[]].
/// When <paramref name="argLocals"/> is non-null each element is loaded from those pre-spilled,
Expand Down
8 changes: 8 additions & 0 deletions Compilation/ILEmitter.Calls.MethodDispatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,14 @@ private void EmitMethodCall(Expr.Get methodGet, List<Expr> 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);

Expand Down
20 changes: 19 additions & 1 deletion Compilation/RuntimeEmitter.Strings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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, <UTF16-encoded codePoint>)
// 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);

Expand Down
16 changes: 14 additions & 2 deletions Runtime/BuiltIns/ObjectBuiltIns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1438,6 +1438,17 @@ private static List<string> 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
Expand All @@ -1451,8 +1462,9 @@ private static List<string> 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
Expand Down
31 changes: 27 additions & 4 deletions Runtime/BuiltIns/StringBuiltIns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

/// <summary>
/// ECMA-262 §11.1.3 UTF16EncodeCodePoint. Unlike <see cref="char.ConvertFromUtf32"/>,
/// this accepts lone surrogates (0xD800–0xDFFF): JS strings are sequences of
/// UTF-16 code units, so <c>String.fromCodePoint(0xDC00)</c> yields a one-unit
/// string holding that surrogate. .NET strings are likewise UTF-16, so a lone
/// surrogate is representable as a single <see cref="char"/>.
/// </summary>
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<RuntimeValue> args)
Expand Down
23 changes: 22 additions & 1 deletion Runtime/Types/SharpTSObjectPrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,28 @@ private SharpTSObjectUnboundMethod(string name, Func<object?, List<object?>, obj

private static object? ValueOfImpl(object? target, List<object?> args) => target;

private static object? IsPrototypeOfImpl(object? target, List<object?> args) => false;
private static object? IsPrototypeOfImpl(object? target, List<object?> 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<object?> args)
{
Expand Down
26 changes: 13 additions & 13 deletions SharpTS.Test262/baselines/compiled.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading