diff --git a/Compilation/RuntimeEmitter.Coercion.cs b/Compilation/RuntimeEmitter.Coercion.cs new file mode 100644 index 00000000..65ca6f69 --- /dev/null +++ b/Compilation/RuntimeEmitter.Coercion.cs @@ -0,0 +1,2068 @@ +using System.Reflection; +using System.Reflection.Emit; +using System.Text; + +namespace SharpTS.Compilation; + +// Split out of RuntimeEmitter.CoreUtilities.cs (#1141). Emits the runtime +// coercion helpers: JS ToString/stringify, ToNumber/ToInt32, and IsTruthy. +public partial class RuntimeEmitter +{ + private void EmitStringify(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // Signature was forward-declared by DefineRuntimeClassPhase1 so + // helper types that emit before $Runtime (notably $RegExp's + // Symbol.* protocol methods) can call us. Just emit the body on + // the existing MethodBuilder. + var method = (MethodBuilder)runtime.Stringify; + + var il = method.GetILGenerator(); + var nullLabel = il.DefineLabel(); + var undefinedLabel = il.DefineLabel(); + var boolLabel = il.DefineLabel(); + var doubleLabel = il.DefineLabel(); + var listLabel = il.DefineLabel(); + var endLabel = il.DefineLabel(); + + // number[] unboxing: materialize a numeric-mode $Array before the `is List` branch reads it. + EmitDeoptArgIfNumericArray(il, runtime, 0); + + // if (value == null) return "null" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brfalse, nullLabel); + + // if (value is SharpTSUndefined) return "undefined" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, undefinedLabel); + + // if (value is bool b) return b ? "true" : "false" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Boolean); + il.Emit(OpCodes.Brtrue, boolLabel); + + // if (value is double d) return d.ToString() + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Double); + il.Emit(OpCodes.Brtrue, doubleLabel); + + // if (value is List) return array string + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.ListOfObject); + il.Emit(OpCodes.Brtrue, listLabel); + + // if (value is BigInteger) return value.ToString() + "n" + var bigintLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.BigInteger); + il.Emit(OpCodes.Brtrue, bigintLabel); + + // if (value is Dictionary) return "{ key: value, ... }" + var dictLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, dictLabel); + + // Default: return value.ToString() ?? "null" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.Emit(OpCodes.Dup); + var notNullLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, notNullLabel); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldstr, "null"); + il.MarkLabel(notNullLabel); + il.Emit(OpCodes.Br, endLabel); + + // null case + il.MarkLabel(nullLabel); + il.Emit(OpCodes.Ldstr, "null"); + il.Emit(OpCodes.Br, endLabel); + + // undefined case + il.MarkLabel(undefinedLabel); + il.Emit(OpCodes.Ldstr, "undefined"); + il.Emit(OpCodes.Br, endLabel); + + // bool case + il.MarkLabel(boolLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, _types.Boolean); + var trueLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, trueLabel); + il.Emit(OpCodes.Ldstr, "false"); + il.Emit(OpCodes.Br, endLabel); + il.MarkLabel(trueLabel); + il.Emit(OpCodes.Ldstr, "true"); + il.Emit(OpCodes.Br, endLabel); + + // double case - delegate to $Runtime.FormatNumber (ECMA-262 7.1.12.1 + // Number::toString: shortest round-trip + JS thresholds). Mirrors the + // interpreter's RuntimeTypes.FormatNumber so the two modes agree. + il.MarkLabel(doubleLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, _types.Double); + il.Emit(OpCodes.Call, runtime.FormatNumber); + il.Emit(OpCodes.Br, endLabel); + + // BigInteger case - format as value.ToString() + "n" + il.MarkLabel(bigintLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, _types.BigInteger); + var bigintLocal = il.DeclareLocal(_types.BigInteger); + il.Emit(OpCodes.Stloc, bigintLocal); + il.Emit(OpCodes.Ldloca, bigintLocal); + il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.BigInteger, "ToString")); + il.Emit(OpCodes.Ldstr, "n"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "Concat", _types.String, _types.String)); + il.Emit(OpCodes.Br, endLabel); + + // List case - format as "[elem1, elem2, ...]" + il.MarkLabel(listLabel); + // Use StringBuilder to build the result + var sbLocal = il.DeclareLocal(_types.StringBuilder); + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, _types.EmptyTypes)); + il.Emit(OpCodes.Stloc, sbLocal); + + // Append "[" + il.Emit(OpCodes.Ldloc, sbLocal); + il.Emit(OpCodes.Ldstr, "["); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + // Loop through list elements + var listLocal = il.DeclareLocal(_types.ListOfObject); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.ListOfObject); + il.Emit(OpCodes.Stloc, listLocal); + + var indexLocal = il.DeclareLocal(_types.Int32); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, indexLocal); + + var loopStart = il.DefineLabel(); + var loopEnd = il.DefineLabel(); + + il.MarkLabel(loopStart); + // if (index >= list.Count) break + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldloc, listLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count").GetGetMethod()!); + il.Emit(OpCodes.Bge, loopEnd); + + // if (index > 0) append ", " + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldc_I4_0); + var skipComma = il.DefineLabel(); + il.Emit(OpCodes.Ble, skipComma); + il.Emit(OpCodes.Ldloc, sbLocal); + il.Emit(OpCodes.Ldstr, ", "); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.MarkLabel(skipComma); + + // Append Stringify(list[index]) + il.Emit(OpCodes.Ldloc, sbLocal); + il.Emit(OpCodes.Ldloc, listLocal); + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Item").GetGetMethod()!); + il.Emit(OpCodes.Call, method); // Recursive call to Stringify + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + // index++ + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, indexLocal); + il.Emit(OpCodes.Br, loopStart); + + il.MarkLabel(loopEnd); + + // Append "]" and return + il.Emit(OpCodes.Ldloc, sbLocal); + il.Emit(OpCodes.Ldstr, "]"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + il.Emit(OpCodes.Ldloc, sbLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); + il.Emit(OpCodes.Br, endLabel); + + // Dictionary case - format as "{ key1: value1, key2: value2, ... }" + il.MarkLabel(dictLabel); + + // ECMA-262 §7.1.17 ToString of an object goes through ToPrimitive, + // which (hint "string") tries the object's own toString method first. + // If the user installed a callable `toString` on the dictionary + // (`{toString: () => 'foo'}`), invoke it and return the result. + // This is the path test262's coerce-string.js exercises. + var dictHasUserToString = il.DeclareLocal(_types.Object); + var skipUserToStringLabel = il.DefineLabel(); + var castDictLocal = il.DeclareLocal(_types.DictionaryStringObject); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); + il.Emit(OpCodes.Stloc, castDictLocal); + + // Try TryGetValue(d, "toString", out userToString). + var tryGetValueResult = il.DeclareLocal(_types.Boolean); + il.Emit(OpCodes.Ldloc, castDictLocal); + il.Emit(OpCodes.Ldstr, "toString"); + il.Emit(OpCodes.Ldloca, dictHasUserToString); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "TryGetValue", _types.String, _types.Object.MakeByRefType())); + il.Emit(OpCodes.Brfalse, skipUserToStringLabel); + + // If the value is a $TSFunction, invoke it with the dict as `this` + // and (if it returns a string-coercible value) return the result. + il.Emit(OpCodes.Ldloc, dictHasUserToString); + il.Emit(OpCodes.Isinst, runtime.TSFunctionType); + il.Emit(OpCodes.Brfalse, skipUserToStringLabel); + + // result = userToString.InvokeWithThis(dict, []); + var userToStringResult = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldloc, dictHasUserToString); + il.Emit(OpCodes.Castclass, runtime.TSFunctionType); + il.Emit(OpCodes.Ldloc, castDictLocal); + il.Emit(OpCodes.Call, _types.GetMethod(typeof(Array), "Empty").MakeGenericMethod(_types.Object)); + il.Emit(OpCodes.Callvirt, runtime.TSFunctionInvokeWithThis); + il.Emit(OpCodes.Stloc, userToStringResult); + + // Recursively Stringify the result so non-string returns coerce + // properly (e.g. number → "42"). The result is normally a string + // already, so this is a fast path through Stringify's string branch. + il.Emit(OpCodes.Ldloc, userToStringResult); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(skipUserToStringLabel); + + // Use StringBuilder to build the result + var dictSbLocal = il.DeclareLocal(_types.StringBuilder); + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, _types.EmptyTypes)); + il.Emit(OpCodes.Stloc, dictSbLocal); + + // Append "{ " + il.Emit(OpCodes.Ldloc, dictSbLocal); + il.Emit(OpCodes.Ldstr, "{ "); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + // Get the dictionary + var dictLocal = il.DeclareLocal(_types.DictionaryStringObject); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); + il.Emit(OpCodes.Stloc, dictLocal); + + // Get enumerator + var enumeratorLocal = il.DeclareLocal(_types.DictionaryStringObjectEnumerator); + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.DictionaryStringObject, "GetEnumerator")); + il.Emit(OpCodes.Stloc, enumeratorLocal); + + // Track if first element + var isFirstLocal = il.DeclareLocal(_types.Boolean); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Stloc, isFirstLocal); + + var dictLoopStart = il.DefineLabel(); + var dictLoopEnd = il.DefineLabel(); + + il.MarkLabel(dictLoopStart); + + // if (!enumerator.MoveNext()) break + il.Emit(OpCodes.Ldloca, enumeratorLocal); + il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.DictionaryStringObjectEnumerator, "MoveNext")); + il.Emit(OpCodes.Brfalse, dictLoopEnd); + + // if (!isFirst) append ", " + var dictSkipComma = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, isFirstLocal); + il.Emit(OpCodes.Brtrue, dictSkipComma); + il.Emit(OpCodes.Ldloc, dictSbLocal); + il.Emit(OpCodes.Ldstr, ", "); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.MarkLabel(dictSkipComma); + + // isFirst = false + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, isFirstLocal); + + // Get current KeyValuePair + var kvpLocal = il.DeclareLocal(_types.KeyValuePairStringObject); + il.Emit(OpCodes.Ldloca, enumeratorLocal); + il.Emit(OpCodes.Call, _types.GetProperty(_types.DictionaryStringObjectEnumerator, "Current").GetGetMethod()!); + il.Emit(OpCodes.Stloc, kvpLocal); + + // Append key + il.Emit(OpCodes.Ldloc, dictSbLocal); + il.Emit(OpCodes.Ldloca, kvpLocal); + il.Emit(OpCodes.Call, _types.GetProperty(_types.KeyValuePairStringObject, "Key").GetGetMethod()!); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + // Append ": " + il.Emit(OpCodes.Ldloc, dictSbLocal); + il.Emit(OpCodes.Ldstr, ": "); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + // Append Stringify(value) - recursive call to emitted method + il.Emit(OpCodes.Ldloc, dictSbLocal); + il.Emit(OpCodes.Ldloca, kvpLocal); + il.Emit(OpCodes.Call, _types.GetProperty(_types.KeyValuePairStringObject, "Value").GetGetMethod()!); + il.Emit(OpCodes.Call, method); // Recursive call to this Stringify method + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + il.Emit(OpCodes.Br, dictLoopStart); + + il.MarkLabel(dictLoopEnd); + + // Append " }" and return + il.Emit(OpCodes.Ldloc, dictSbLocal); + il.Emit(OpCodes.Ldstr, " }"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + il.Emit(OpCodes.Ldloc, dictSbLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(endLabel); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits: public static string StringRaw(object template, object[] substitutions). + /// Implements String.raw per ECMA-262 22.1.2.4. Accepts: + /// + /// string[] — the legacy tagged-template-literal calling convention + /// (used by ); used directly as the rawStrings array. + /// any object with a raw property — the spec form + /// (String.raw({raw: [...]}, ...subs)); reads raw via GetProperty, + /// reads its length, iterates indexed members. + /// + /// + private void EmitStringRaw(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // Second param is `List substitutions` (not object[]) so + // $TSFunction.AdjustArgs's rest-param recognition kicks in for direct + // `String.raw(template, ...subs)` calls — otherwise only the first + // substitution would land in the param. + var method = typeBuilder.DefineMethod( + "StringRaw", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.Object, _types.ListOfObject] + ); + runtime.StringRaw = method; + + var il = method.GetILGenerator(); + + // We unify both shapes by extracting `length` and an indexed-access + // closure into locals. For string[]: length = arr.Length, get(i) = arr[i]. + // For object: length = ToLength(template.length OR template.raw.length), + // get(i) = ToString(template.raw[i]). + var lengthLocal = il.DeclareLocal(_types.Int32); + var rawListLocal = il.DeclareLocal(_types.Object); // either string[] or List from raw + var isStringArrayLocal = il.DeclareLocal(_types.Boolean); + + // Detect string[] (legacy tagged-template path). + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.StringArray); + var notStringArrayLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notStringArrayLabel); + // string[] path: length = ((string[])arg0).Length, rawList = arg0 + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.StringArray); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Stloc, lengthLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Stloc, rawListLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Stloc, isStringArrayLocal); + var afterDispatchLabel = il.DefineLabel(); + il.Emit(OpCodes.Br, afterDispatchLabel); + + il.MarkLabel(notStringArrayLabel); + // Object path (spec form): raw = template.raw; length = ToLength(raw.length). + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, isStringArrayLocal); + + // ECMA-262 22.1.2.4 step 2: ? RequireObjectCoercible(template). null/ + // undefined throws TypeError. Catches String.raw(undefined) / .call(null). + il.Emit(OpCodes.Ldarg_0); + var notNullishLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, notNullishLabel); + var throwTypeErrorLabel = il.DefineLabel(); + il.MarkLabel(throwTypeErrorLabel); + il.Emit(OpCodes.Ldstr, "Cannot convert undefined or null to object"); + il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); + il.Emit(OpCodes.Call, runtime.CreateException); + il.Emit(OpCodes.Throw); + il.MarkLabel(notNullishLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, throwTypeErrorLabel); + + // raw = template.raw via $Runtime.GetProperty + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldstr, "raw"); + il.Emit(OpCodes.Call, runtime.GetProperty); + il.Emit(OpCodes.Stloc, rawListLocal); + + // ECMA-262 22.1.2.4 step 4: ? ToObject(raw). If raw is null/undefined, + // throw TypeError per spec. Required for `String.raw({raw: undefined})` + // and `String.raw({})` (raw absent → undefined). + il.Emit(OpCodes.Ldloc, rawListLocal); + il.Emit(OpCodes.Brfalse, throwTypeErrorLabel); + il.Emit(OpCodes.Ldloc, rawListLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, throwTypeErrorLabel); + + // ToLength(raw.length): use $Runtime.GetProperty(raw, "length") then + // $Runtime.ToNumber → clamp to non-negative int. + il.Emit(OpCodes.Ldloc, rawListLocal); + il.Emit(OpCodes.Ldstr, "length"); + il.Emit(OpCodes.Call, runtime.GetProperty); + il.Emit(OpCodes.Call, runtime.ToNumber); + var lenDouble = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Stloc, lenDouble); + // NaN / negative / -Infinity → 0 + il.Emit(OpCodes.Ldloc, lenDouble); + il.Emit(OpCodes.Ldloc, lenDouble); + var notNaNLabel = il.DefineLabel(); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Brtrue, notNaNLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, lengthLocal); + il.Emit(OpCodes.Br, afterDispatchLabel); + il.MarkLabel(notNaNLabel); + il.Emit(OpCodes.Ldloc, lenDouble); + il.Emit(OpCodes.Ldc_R8, 0.0); + var positiveLenLabel = il.DefineLabel(); + il.Emit(OpCodes.Bgt, positiveLenLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, lengthLocal); + il.Emit(OpCodes.Br, afterDispatchLabel); + il.MarkLabel(positiveLenLabel); + // length = (int)Math.Min(d, 1<<24) — guard against runaway alloc. + il.Emit(OpCodes.Ldloc, lenDouble); + il.Emit(OpCodes.Ldc_R8, (double)(1 << 24)); + il.Emit(OpCodes.Call, _types.Math.GetMethod("Min", [_types.Double, _types.Double])!); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Stloc, lengthLocal); + + il.MarkLabel(afterDispatchLabel); + + // ECMA-262 22.1.2.4 step 7: If literalSegments ≤ 0, return the empty string. + il.Emit(OpCodes.Ldloc, lengthLocal); + var hasSegmentsLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, hasSegmentsLabel); + il.Emit(OpCodes.Ldstr, ""); + il.Emit(OpCodes.Ret); + il.MarkLabel(hasSegmentsLabel); + + // var sb = new StringBuilder(); + var sbLocal = il.DeclareLocal(_types.StringBuilder); + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, Type.EmptyTypes)); + il.Emit(OpCodes.Stloc, sbLocal); + + // for (int i = 0; i < length; i++) { ... } + var iLocal = il.DeclareLocal(_types.Int32); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, iLocal); + var loopStart = il.DefineLabel(); + var loopCondition = il.DefineLabel(); + il.Emit(OpCodes.Br, loopCondition); + + il.MarkLabel(loopStart); + + // segment = isStringArray ? rawList[i] : ToJsString(GetProperty(rawList, i.ToString())) + var segmentLocal = il.DeclareLocal(_types.String); + il.Emit(OpCodes.Ldloc, isStringArrayLocal); + var segmentObjPathLabel = il.DefineLabel(); + var segmentDoneLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, segmentObjPathLabel); + // string[] path + il.Emit(OpCodes.Ldloc, rawListLocal); + il.Emit(OpCodes.Castclass, _types.StringArray); + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Stloc, segmentLocal); + il.Emit(OpCodes.Br, segmentDoneLabel); + il.MarkLabel(segmentObjPathLabel); + // object path: ToJsString(GetProperty(raw, i.ToString())) + il.Emit(OpCodes.Ldloc, rawListLocal); + il.Emit(OpCodes.Ldloca, iLocal); + il.Emit(OpCodes.Call, _types.Int32.GetMethod("ToString", Type.EmptyTypes)!); + il.Emit(OpCodes.Call, runtime.GetProperty); + il.Emit(OpCodes.Call, runtime.ToJsString); + il.Emit(OpCodes.Stloc, segmentLocal); + il.MarkLabel(segmentDoneLabel); + + // sb.Append(segment) + il.Emit(OpCodes.Ldloc, sbLocal); + il.Emit(OpCodes.Ldloc, segmentLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + // if (i + 1 < length) append substitution + var skipSubLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Ldloc, lengthLocal); + il.Emit(OpCodes.Bge, skipSubLabel); + // if i < substitutions.Count, append ToJsString(substitutions[i]) + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count").GetGetMethod()!); + il.Emit(OpCodes.Bge, skipSubLabel); + var subStrLocal = il.DeclareLocal(_types.String); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Item").GetGetMethod()!); + il.Emit(OpCodes.Call, runtime.ToJsString); + il.Emit(OpCodes.Stloc, subStrLocal); + il.Emit(OpCodes.Ldloc, sbLocal); + il.Emit(OpCodes.Ldloc, subStrLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.MarkLabel(skipSubLabel); + + // i++ + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, iLocal); + + il.MarkLabel(loopCondition); + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldloc, lengthLocal); + il.Emit(OpCodes.Blt, loopStart); + + il.Emit(OpCodes.Ldloc, sbLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); + il.Emit(OpCodes.Ret); + } + + // Emits an early-return guard for the language-level ToString coercion paths + // (String(), template-literal interpolation, `+` concat): a bigint coerces to + // its bare decimal form ("42"), NOT the "42n" debug form that console.log / + // util.inspect (Stringify) uses. Mirrors the interpreter's Interpreter.Stringify + // / SharpTSStringNamespace bigint handling so the two modes agree. + private void EmitBigIntToStringReturn(ILGenerator il) + { + var notBigInt = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.BigInteger); + il.Emit(OpCodes.Brfalse, notBigInt); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, _types.BigInteger); + var loc = il.DeclareLocal(_types.BigInteger); + il.Emit(OpCodes.Stloc, loc); + il.Emit(OpCodes.Ldloca, loc); + il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.BigInteger, "ToString")); + il.Emit(OpCodes.Ret); + il.MarkLabel(notBigInt); + } + + // StringFromValue — ECMA-262 §22.1.1.1 String(value) constructor called as a + // function. Identical to ToJsString except that Symbol arguments return + // SymbolDescriptiveString instead of throwing: the String() call form is the + // single coercion site the spec exempts from ToString's Symbol TypeError. + private void EmitStringFromValue(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "StringFromValue", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.Object]); + runtime.StringFromValueMethod = method; + + var il = method.GetILGenerator(); + + // if (value is $TSSymbol) return value.ToString(); // "Symbol(desc)" + var notSymbolLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSSymbolType); + il.Emit(OpCodes.Brfalse, notSymbolLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.Emit(OpCodes.Ret); + il.MarkLabel(notSymbolLabel); + + // return ToJsString(value); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.ToJsString); + il.Emit(OpCodes.Ret); + } + + // StringifyCoerce — Stringify behind the ECMA-262 §7.1.17 Symbol guard. + // Implicit ToString coercion sites (template-literal interpolation, string + // +/+= concatenation) must throw TypeError for Symbol operands; everything + // else keeps Stringify's display semantics. Signature forward-declared by + // DefineRuntimeClassPhase1 because $Runtime.Add's string-concat arm is + // emitted before this body is filled. + private void EmitStringifyCoerce(EmittedRuntime runtime) + { + var il = runtime.StringifyCoerce.GetILGenerator(); + + // if (value is $TSSymbol) throw TypeError + var notSymbolLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSSymbolType); + il.Emit(OpCodes.Brfalse, notSymbolLabel); + il.Emit(OpCodes.Ldstr, "Cannot convert a Symbol value to a string"); + il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); + il.Emit(OpCodes.Call, runtime.CreateException); + il.Emit(OpCodes.Throw); + il.MarkLabel(notSymbolLabel); + + // A bigint coerces to its bare decimal form ("42"), not Stringify's "42n". + EmitBigIntToStringReturn(il); + + // An object-like value ($Object / Dictionary / List / user class instance) + // coerces via ToJsString — the spec ToString → OrdinaryToPrimitive(string) + // protocol, which honors an own toString override and unwraps a boxed + // wrapper to its primitive (#574). Plain Stringify returns "[object Object]" + // for a wrapper and the bare " instance" CLR form for a user class + // instance, both wrong for template literals / string concat. A user class + // instance is identified by the $IHasFields marker — $Error/Map/Set/ + // $TSFunction don't implement it, so they keep Stringify's path (#931). + // Primitive values stay on the cheap Stringify path. + var objectLikeLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brtrue, objectLikeLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, objectLikeLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.ListOfObject); + il.Emit(OpCodes.Brtrue, objectLikeLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.IHasFieldsInterface); + il.Emit(OpCodes.Brtrue, objectLikeLabel); + + // return Stringify(value); (primitives) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Ret); + + // return ToJsString(value); (object-like) + il.MarkLabel(objectLikeLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.ToJsString); + il.Emit(OpCodes.Ret); + } + + // ToJsString — ECMA-262 ToString protocol. For Dictionary/$Object receivers + // with a user-defined "toString" function, invoke it and use the result. + // Falls back to Stringify for primitives. Used by String.prototype methods + // that take an object argument (search/indexOf/etc.). + private void EmitToJsString(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // Signature forward-declared by DefineRuntimeClassPhase1 so $RegExp's + // Symbol.* helpers (which emit before $Runtime's body) can bind to + // it. Just fill the body on the existing MethodBuilder. + var method = (MethodBuilder)runtime.ToJsString; + var il = method.GetILGenerator(); + var fallbackLabel = il.DefineLabel(); + + // number[] unboxing: the `is List` array branch below joins the base + // list directly, so a numeric-mode $Array (empty base list) must materialize first. + EmitDeoptArgIfNumericArray(il, runtime, 0); + + // null / undefined → handled by Stringify ("null" / "undefined") + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brfalse, fallbackLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, fallbackLabel); + + // ECMA-262 7.1.17 ToString — Symbol primitives throw TypeError. + var notSymbolLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSSymbolType); + il.Emit(OpCodes.Brfalse, notSymbolLabel); + il.Emit(OpCodes.Ldstr, "Cannot convert a Symbol value to a string"); + il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); + il.Emit(OpCodes.Call, runtime.CreateException); + il.Emit(OpCodes.Throw); + il.MarkLabel(notSymbolLabel); + + // A bigint coerces to its bare decimal form ("42"), not Stringify's "42n". + EmitBigIntToStringReturn(il); + + // Already a string → return as-is (avoid CLR ToString round-trip). + var alreadyStringLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, alreadyStringLabel); + + // ECMA-262 22.3.7 Arguments.prototype.toString inherits from + // Object.prototype.toString → "[object Arguments]". Without this + // check, $Arguments (which extends List) hits the List + // branch and gets comma-joined. Real-world code rarely relies on + // this brand string, but Test262's `String.prototype.trim.call(arguments)` + // test asserts on it. + var notArgumentsBrandLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.ArgumentsType); + il.Emit(OpCodes.Brfalse, notArgumentsBrandLabel); + il.Emit(OpCodes.Ldstr, "[object Arguments]"); + il.Emit(OpCodes.Ret); + il.MarkLabel(notArgumentsBrandLabel); + + // List → ECMA-262 Array.prototype.toString returns join(","), + // not Stringify's debug-style "[a, b]". Build the comma-joined form + // inline so `String([1,2,3]) === "1,2,3"`. Recursively Stringify each + // element (matches join's per-element ToString conversion). + var notListLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.ListOfObject); + il.Emit(OpCodes.Brfalse, notListLabel); + var joinedListLocal = il.DeclareLocal(_types.ListOfObject); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.ListOfObject); + il.Emit(OpCodes.Stloc, joinedListLocal); + var sbJoinLocal = il.DeclareLocal(_types.StringBuilder); + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, _types.EmptyTypes)); + il.Emit(OpCodes.Stloc, sbJoinLocal); + var idxJoinLocal = il.DeclareLocal(_types.Int32); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, idxJoinLocal); + var joinLoop = il.DefineLabel(); + var joinEnd = il.DefineLabel(); + il.MarkLabel(joinLoop); + il.Emit(OpCodes.Ldloc, idxJoinLocal); + il.Emit(OpCodes.Ldloc, joinedListLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count").GetGetMethod()!); + il.Emit(OpCodes.Bge, joinEnd); + // Append "," for index > 0 + var skipJoinComma = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, idxJoinLocal); + il.Emit(OpCodes.Brfalse, skipJoinComma); + il.Emit(OpCodes.Ldloc, sbJoinLocal); + il.Emit(OpCodes.Ldstr, ","); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.MarkLabel(skipJoinComma); + // val = list[index]; null/undefined → empty per spec join behavior; else recursive Stringify. + var valLocalJ = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldloc, joinedListLocal); + il.Emit(OpCodes.Ldloc, idxJoinLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Item").GetGetMethod()!); + il.Emit(OpCodes.Stloc, valLocalJ); + var skipAppend = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, valLocalJ); + il.Emit(OpCodes.Brfalse, skipAppend); + il.Emit(OpCodes.Ldloc, valLocalJ); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, skipAppend); + il.Emit(OpCodes.Ldloc, sbJoinLocal); + il.Emit(OpCodes.Ldloc, valLocalJ); + il.Emit(OpCodes.Call, runtime.ToJsString); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.MarkLabel(skipAppend); + il.Emit(OpCodes.Ldloc, idxJoinLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, idxJoinLocal); + il.Emit(OpCodes.Br, joinLoop); + il.MarkLabel(joinEnd); + il.Emit(OpCodes.Ldloc, sbJoinLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); + il.Emit(OpCodes.Ret); + il.MarkLabel(notListLabel); + + // KeyValuePair → treat as a 2-element [key, value] tuple for + // string coercion (ECMA-262 Array.prototype.toString ≡ join(",")). Map-spread + // tuples land here in compiled mode because IterateToList falls through to the + // IEnumerable path, yielding boxed KVP structs instead of List pairs. + var notKvpLabel = il.DefineLabel(); + var kvpType = _types.MakeGenericType(_types.KeyValuePairOpen, _types.Object, _types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, kvpType); + il.Emit(OpCodes.Brfalse, notKvpLabel); + var kvpLocal = il.DeclareLocal(kvpType); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, kvpType); + il.Emit(OpCodes.Stloc, kvpLocal); + var sbKvpLocal = il.DeclareLocal(_types.StringBuilder); + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, _types.EmptyTypes)); + il.Emit(OpCodes.Stloc, sbKvpLocal); + // Key (index 0): skip if null/undefined, else recursive ToJsString + var keyLocal = il.DeclareLocal(_types.Object); + var skipKeyAppend = il.DefineLabel(); + il.Emit(OpCodes.Ldloca, kvpLocal); + il.Emit(OpCodes.Call, _types.GetProperty(kvpType, "Key").GetGetMethod()!); + il.Emit(OpCodes.Stloc, keyLocal); + il.Emit(OpCodes.Ldloc, keyLocal); + il.Emit(OpCodes.Brfalse, skipKeyAppend); + il.Emit(OpCodes.Ldloc, keyLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, skipKeyAppend); + il.Emit(OpCodes.Ldloc, sbKvpLocal); + il.Emit(OpCodes.Ldloc, keyLocal); + il.Emit(OpCodes.Call, runtime.ToJsString); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.MarkLabel(skipKeyAppend); + il.Emit(OpCodes.Ldloc, sbKvpLocal); + il.Emit(OpCodes.Ldstr, ","); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + // Value (index 1): skip if null/undefined, else recursive ToJsString + var valueLocal = il.DeclareLocal(_types.Object); + var skipValueAppend = il.DefineLabel(); + il.Emit(OpCodes.Ldloca, kvpLocal); + il.Emit(OpCodes.Call, _types.GetProperty(kvpType, "Value").GetGetMethod()!); + il.Emit(OpCodes.Stloc, valueLocal); + il.Emit(OpCodes.Ldloc, valueLocal); + il.Emit(OpCodes.Brfalse, skipValueAppend); + il.Emit(OpCodes.Ldloc, valueLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, skipValueAppend); + il.Emit(OpCodes.Ldloc, sbKvpLocal); + il.Emit(OpCodes.Ldloc, valueLocal); + il.Emit(OpCodes.Call, runtime.ToJsString); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.MarkLabel(skipValueAppend); + il.Emit(OpCodes.Ldloc, sbKvpLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); + il.Emit(OpCodes.Ret); + il.MarkLabel(notKvpLabel); + + // Only attempt JS-toString invocation for Dictionary, $Object, or user + // class instances ($IHasFields marker — #931). $Error/Map/Set/$TSFunction + // don't implement $IHasFields, so they fall through to Stringify (Error + // keeps its overridden CLR ToString → "TypeError: x"). A user class + // instance resolves toString/valueOf through its generated GetProperty, + // yielding the user override or "[object Object]" when neither exists. + var isObjectLikeLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, isObjectLikeLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brtrue, isObjectLikeLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.IHasFieldsInterface); + il.Emit(OpCodes.Brtrue, isObjectLikeLabel); + il.Emit(OpCodes.Br, fallbackLabel); + + il.MarkLabel(isObjectLikeLabel); + + // Boxed primitive marker fast-path: if the receiver carries + // __primitiveType + __primitiveValue (Stage 4z19 wrappers), Stringify + // the underlying primitive directly. Without this, toString walks the + // prototype chain to the StringPrototypeGenericStub which doesn't read + // the marker — returns receiver-as-string instead of the primitive's + // natural string repr (`new Object(true).valueOf()` gives wrapper, not true). + var primValLocal = il.DeclareLocal(_types.Object); + var notBoxedLabel = il.DefineLabel(); + // #574: an own (instance) toString override must win over the boxed + // __primitiveValue fast-path — ECMA-262 OrdinaryToPrimitive(O, "string") + // calls the own toString first. When the wrapper carries an own toString, + // defer to the OrdinaryToPrimitive section below (which invokes it). An + // inherited prototype toString is NOT own, so un-overridden wrappers still + // take the fast-path. (HasOwnPropertyHelper does not walk the prototype.) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldstr, "toString"); + il.Emit(OpCodes.Call, runtime.HasOwnPropertyHelperMethod); + il.Emit(OpCodes.Brtrue, notBoxedLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldstr, "__primitiveValue"); + il.Emit(OpCodes.Call, runtime.GetProperty); + il.Emit(OpCodes.Stloc, primValLocal); + il.Emit(OpCodes.Ldloc, primValLocal); + il.Emit(OpCodes.Brfalse, notBoxedLabel); + il.Emit(OpCodes.Ldloc, primValLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, notBoxedLabel); + // ECMA-262 §7.1.17 step 2: throw TypeError if the unwrapped primitive + // is a Symbol. The entry-point check at line ~1856 only catches raw + // Symbol values — Object(Symbol("x")) wraps it as $Object with + // __primitiveValue=sym, and the unwrap below bypasses that guard, + // letting Stringify run on a Symbol (returns "Symbol(x)" rather than + // throwing). Required by indexOf/searchstring-tostring-errors et al. + var unwrapNotSymLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, primValLocal); + il.Emit(OpCodes.Isinst, runtime.TSSymbolType); + il.Emit(OpCodes.Brfalse, unwrapNotSymLabel); + il.Emit(OpCodes.Ldstr, "Cannot convert a Symbol value to a string"); + il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); + il.Emit(OpCodes.Call, runtime.CreateException); + il.Emit(OpCodes.Throw); + il.MarkLabel(unwrapNotSymLabel); + // Stringify the primitive — handles bool/double/string identically to + // the top-level fallback path. + il.Emit(OpCodes.Ldloc, primValLocal); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Ret); + il.MarkLabel(notBoxedLabel); + + // ECMA-262 7.1.1 ToPrimitive(input, "string"): GetMethod(input, @@toPrimitive) + // takes priority over OrdinaryToPrimitive. Look up Symbol.toPrimitive in the + // value's symbol-dict (compiled mode stores symbol-keyed properties separately + // from string-keyed ones). If found and callable, invoke with hint "string". + // Per spec, the result must be primitive — if it's an object, throw TypeError. + var afterToPrimSymLabel = il.DefineLabel(); + var symDictForToPrimLocal = il.DeclareLocal(_types.DictionaryObjectObject); + var toPrimFnLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.GetSymbolDictMethod); + il.Emit(OpCodes.Stloc, symDictForToPrimLocal); + il.Emit(OpCodes.Ldloc, symDictForToPrimLocal); + il.Emit(OpCodes.Brfalse, afterToPrimSymLabel); + il.Emit(OpCodes.Ldloc, symDictForToPrimLocal); + il.Emit(OpCodes.Ldsfld, runtime.SymbolToPrimitive); + il.Emit(OpCodes.Ldloca, toPrimFnLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "TryGetValue")); + il.Emit(OpCodes.Brfalse, afterToPrimSymLabel); + il.Emit(OpCodes.Ldloc, toPrimFnLocal); + il.Emit(OpCodes.Brfalse, afterToPrimSymLabel); + il.Emit(OpCodes.Ldloc, toPrimFnLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, afterToPrimSymLabel); + + // Accessor descriptor: $CompiledPropertyDescriptor with a Getter field. + // Object literals with `get [Symbol.toPrimitive]() {...}` store the descriptor + // here via $Runtime.DefineSymbolAccessor. Invoke the getter to materialize + // the actual @@toPrimitive function. If the descriptor's Getter is null, the + // accessor is set-only — fall through to OrdinaryToPrimitive (treat as if + // @@toPrimitive is undefined). + var notDescriptorLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, toPrimFnLocal); + il.Emit(OpCodes.Isinst, runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Brfalse, notDescriptorLabel); + var descGetterLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldloc, toPrimFnLocal); + il.Emit(OpCodes.Castclass, runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorGetter.GetGetMethod()!); + il.Emit(OpCodes.Stloc, descGetterLocal); + il.Emit(OpCodes.Ldloc, descGetterLocal); + il.Emit(OpCodes.Brfalse, afterToPrimSymLabel); + // result = InvokeMethodValue(receiver, getter, []) + var emptyArgsForGetterStr = il.DeclareLocal(_types.ObjectArray); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Stloc, emptyArgsForGetterStr); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, descGetterLocal); + il.Emit(OpCodes.Ldloc, emptyArgsForGetterStr); + il.Emit(OpCodes.Call, runtime.InvokeMethodValue); + il.Emit(OpCodes.Stloc, toPrimFnLocal); + // Re-check that the materialized value is non-null/non-undefined. + il.Emit(OpCodes.Ldloc, toPrimFnLocal); + il.Emit(OpCodes.Brfalse, afterToPrimSymLabel); + il.Emit(OpCodes.Ldloc, toPrimFnLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, afterToPrimSymLabel); + il.MarkLabel(notDescriptorLabel); + + // Build args array ["string"] and invoke. + var hintArgsStrLocal = il.DeclareLocal(_types.ObjectArray); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Stloc, hintArgsStrLocal); + il.Emit(OpCodes.Ldloc, hintArgsStrLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldstr, "string"); + il.Emit(OpCodes.Stelem_Ref); + var toPrimResultLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, toPrimFnLocal); + il.Emit(OpCodes.Ldloc, hintArgsStrLocal); + il.Emit(OpCodes.Call, runtime.InvokeMethodValue); + il.Emit(OpCodes.Stloc, toPrimResultLocal); + + // If primitive (null/undefined/string/number/bool/BigInt) → ToJsString and return. + var resIsPrimitiveLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, toPrimResultLocal); + il.Emit(OpCodes.Brfalse, resIsPrimitiveLabel); + il.Emit(OpCodes.Ldloc, toPrimResultLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, resIsPrimitiveLabel); + il.Emit(OpCodes.Ldloc, toPrimResultLocal); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, resIsPrimitiveLabel); + il.Emit(OpCodes.Ldloc, toPrimResultLocal); + il.Emit(OpCodes.Isinst, _types.Double); + il.Emit(OpCodes.Brtrue, resIsPrimitiveLabel); + il.Emit(OpCodes.Ldloc, toPrimResultLocal); + il.Emit(OpCodes.Isinst, _types.Boolean); + il.Emit(OpCodes.Brtrue, resIsPrimitiveLabel); + il.Emit(OpCodes.Ldloc, toPrimResultLocal); + il.Emit(OpCodes.Isinst, _types.BigInteger); + il.Emit(OpCodes.Brtrue, resIsPrimitiveLabel); + // Object result → TypeError per ECMA-262 7.1.1 step 1.b.iii. + il.Emit(OpCodes.Ldstr, "Cannot convert object to primitive value"); + il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); + il.Emit(OpCodes.Call, runtime.CreateException); + il.Emit(OpCodes.Throw); + il.MarkLabel(resIsPrimitiveLabel); + il.Emit(OpCodes.Ldloc, toPrimResultLocal); + il.Emit(OpCodes.Call, runtime.ToJsString); + il.Emit(OpCodes.Ret); + il.MarkLabel(afterToPrimSymLabel); + + // emptyArgs = new object[0] + var emptyArgsLocal = il.DeclareLocal(_types.ObjectArray); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Stloc, emptyArgsLocal); + + // Track whether either toString or valueOf was defined+callable but + // returned a non-primitive. ECMA-262 7.1.1.1 OrdinaryToPrimitive + // requires throwing TypeError in this case (both methods produced + // objects). The lenient "[object Object]" fallback is only correct + // when neither method exists. + var sawNonPrimitiveLocal = il.DeclareLocal(_types.Boolean); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, sawNonPrimitiveLocal); + + // ECMA-262 ToPrimitive(O, "string"): try toString, then valueOf. + void TryInvoke(string name, Label afterLabel) + { + var fnLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldstr, name); + il.Emit(OpCodes.Call, runtime.GetProperty); + il.Emit(OpCodes.Stloc, fnLocal); + il.Emit(OpCodes.Ldloc, fnLocal); + il.Emit(OpCodes.Brfalse, afterLabel); + il.Emit(OpCodes.Ldloc, fnLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, afterLabel); + + // result = $Runtime.InvokeMethodValue(receiver, fn, emptyArgs) + var resultLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, fnLocal); + il.Emit(OpCodes.Ldloc, emptyArgsLocal); + il.Emit(OpCodes.Call, runtime.InvokeMethodValue); + il.Emit(OpCodes.Stloc, resultLocal); + + // If primitive (string / number / bool / null / undefined / BigInt), + // ToJsString and return. Per ECMA-262 ToPrimitive, all primitive types + // — including undefined and null — are valid OrdinaryToPrimitive results. + var resultIsString = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Brfalse, resultIsString); // null primitive + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, resultIsString); // undefined primitive + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, resultIsString); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Isinst, _types.Double); + il.Emit(OpCodes.Brtrue, resultIsString); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Isinst, _types.Boolean); + il.Emit(OpCodes.Brtrue, resultIsString); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Isinst, _types.BigInteger); + il.Emit(OpCodes.Brtrue, resultIsString); + // Not primitive — set the flag and continue to next attempt. + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Stloc, sawNonPrimitiveLocal); + il.Emit(OpCodes.Br, afterLabel); + + il.MarkLabel(resultIsString); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Call, runtime.ToJsString); + il.Emit(OpCodes.Ret); + } + + var afterToString = il.DefineLabel(); + TryInvoke("toString", afterToString); + il.MarkLabel(afterToString); + var afterValueOf = il.DefineLabel(); + TryInvoke("valueOf", afterValueOf); + il.MarkLabel(afterValueOf); + + // If at least one of toString/valueOf was defined+callable but returned + // a non-primitive, ECMA-262 demands TypeError. Otherwise (neither method + // existed), fall back to "[object Object]" — see comment below. + var noThrowLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, sawNonPrimitiveLocal); + il.Emit(OpCodes.Brfalse, noThrowLabel); + il.Emit(OpCodes.Ldstr, "Cannot convert object to primitive value"); + il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); + il.Emit(OpCodes.Call, runtime.CreateException); + il.Emit(OpCodes.Throw); + il.MarkLabel(noThrowLabel); + + // No usable toString/valueOf on this object — fall back to "[object Object]" + // per ECMA-262 19.1.3.6 (Object.prototype.toString returns this for plain objects). + // Lenient: spec strictly throws TypeError when both are unusable, but the + // compiled-mode prototype-chain walk doesn't reliably surface + // Object.prototype's toString for user $TSObject receivers. Throwing + // here regresses charAt/etc. on borrowed prototypes. Tests that depend + // on the throw (`{toString: undefined, valueOf: undefined}`) are a + // smaller bucket than tests that depend on the "[object Object]" + // fallback for plain user objects. + il.Emit(OpCodes.Ldstr, "[object Object]"); + il.Emit(OpCodes.Ret); + + il.MarkLabel(alreadyStringLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Ret); + + il.MarkLabel(fallbackLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Ret); + } + + /// + /// Pre-declares the $Runtime.ToNumber MethodBuilder slot. Body is filled + /// in by later, after GetProperty/InvokeMethodValue + /// are available so the ToPrimitive(value, "number") chain can call them. + /// + internal void DeclareToNumber(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "ToNumber", + MethodAttributes.Public | MethodAttributes.Static, + _types.Double, + [_types.Object] + ); + runtime.ToNumber = method; + } + + private void EmitToNumber(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = (MethodBuilder)runtime.ToNumber; + + var il = method.GetILGenerator(); + var resultLocal = il.DeclareLocal(_types.Double); + + // ECMA-262 7.1.4 ToNumber on object: ToPrimitive(value, "number") which + // tries valueOf first, then toString. Without this, Math.hypot(obj-with- + // throwing-valueOf) silently returns NaN instead of propagating the + // throw. Apply for Dictionary or $Object only. + var argLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Stloc, argLocal); + + var skipToPrimLabelTop = il.DefineLabel(); + var doToPrimLabelTop = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, doToPrimLabelTop); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brfalse, skipToPrimLabelTop); + il.MarkLabel(doToPrimLabelTop); + + // ECMA-262 7.1.1 ToPrimitive(input, "number"): @@toPrimitive (if defined and + // callable) takes priority over OrdinaryToPrimitive. Look up the symbol-keyed + // method and invoke with hint "number". Result must be primitive or TypeError. + var afterToPrimSymN = il.DefineLabel(); + var doThrowN = il.DefineLabel(); + var symDictForToPrimN = il.DeclareLocal(_types.DictionaryObjectObject); + var toPrimFnLocalN = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Call, runtime.GetSymbolDictMethod); + il.Emit(OpCodes.Stloc, symDictForToPrimN); + il.Emit(OpCodes.Ldloc, symDictForToPrimN); + il.Emit(OpCodes.Brfalse, afterToPrimSymN); + il.Emit(OpCodes.Ldloc, symDictForToPrimN); + il.Emit(OpCodes.Ldsfld, runtime.SymbolToPrimitive); + il.Emit(OpCodes.Ldloca, toPrimFnLocalN); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "TryGetValue")); + il.Emit(OpCodes.Brfalse, afterToPrimSymN); + il.Emit(OpCodes.Ldloc, toPrimFnLocalN); + il.Emit(OpCodes.Brfalse, afterToPrimSymN); + il.Emit(OpCodes.Ldloc, toPrimFnLocalN); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, afterToPrimSymN); + + // Accessor descriptor unwrap (mirrors EmitToJsString — see notes there). + var notDescLabelN = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, toPrimFnLocalN); + il.Emit(OpCodes.Isinst, runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Brfalse, notDescLabelN); + var descGetterLocalN = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldloc, toPrimFnLocalN); + il.Emit(OpCodes.Castclass, runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorGetter.GetGetMethod()!); + il.Emit(OpCodes.Stloc, descGetterLocalN); + il.Emit(OpCodes.Ldloc, descGetterLocalN); + il.Emit(OpCodes.Brfalse, afterToPrimSymN); + var emptyArgsForGetterNum = il.DeclareLocal(_types.ObjectArray); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Stloc, emptyArgsForGetterNum); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Ldloc, descGetterLocalN); + il.Emit(OpCodes.Ldloc, emptyArgsForGetterNum); + il.Emit(OpCodes.Call, runtime.InvokeMethodValue); + il.Emit(OpCodes.Stloc, toPrimFnLocalN); + il.Emit(OpCodes.Ldloc, toPrimFnLocalN); + il.Emit(OpCodes.Brfalse, afterToPrimSymN); + il.Emit(OpCodes.Ldloc, toPrimFnLocalN); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, afterToPrimSymN); + il.MarkLabel(notDescLabelN); + + var hintArgsNumLocal = il.DeclareLocal(_types.ObjectArray); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Stloc, hintArgsNumLocal); + il.Emit(OpCodes.Ldloc, hintArgsNumLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldstr, "number"); + il.Emit(OpCodes.Stelem_Ref); + var toPrimResultLocalN = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Ldloc, toPrimFnLocalN); + il.Emit(OpCodes.Ldloc, hintArgsNumLocal); + il.Emit(OpCodes.Call, runtime.InvokeMethodValue); + il.Emit(OpCodes.Stloc, toPrimResultLocalN); + // Object result → TypeError per ECMA-262 7.1.1 step 1.b.iii. + il.Emit(OpCodes.Ldloc, toPrimResultLocalN); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, doThrowN); + il.Emit(OpCodes.Ldloc, toPrimResultLocalN); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brtrue, doThrowN); + il.Emit(OpCodes.Ldloc, toPrimResultLocalN); + il.Emit(OpCodes.Stloc, argLocal); + il.Emit(OpCodes.Br, afterToPrimSymN); + il.MarkLabel(doThrowN); + il.Emit(OpCodes.Ldstr, "Cannot convert object to primitive value"); + il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); + il.Emit(OpCodes.Call, runtime.CreateException); + il.Emit(OpCodes.Throw); + il.MarkLabel(afterToPrimSymN); + + var emptyArgsLocalT = il.DeclareLocal(_types.ObjectArray); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Stloc, emptyArgsLocalT); + + void TryToPrim2(string name, Label afterLabel) + { + var fnLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Ldstr, name); + il.Emit(OpCodes.Call, runtime.GetProperty); + il.Emit(OpCodes.Stloc, fnLocal); + il.Emit(OpCodes.Ldloc, fnLocal); + il.Emit(OpCodes.Brfalse, afterLabel); + il.Emit(OpCodes.Ldloc, fnLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, afterLabel); + + var resLoc = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Ldloc, fnLocal); + il.Emit(OpCodes.Ldloc, emptyArgsLocalT); + il.Emit(OpCodes.Call, runtime.InvokeMethodValue); + il.Emit(OpCodes.Stloc, resLoc); + + il.Emit(OpCodes.Ldloc, resLoc); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, afterLabel); + il.Emit(OpCodes.Ldloc, resLoc); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brtrue, afterLabel); + il.Emit(OpCodes.Ldloc, resLoc); + il.Emit(OpCodes.Stloc, argLocal); + } + + var afterValueOfT = il.DefineLabel(); + TryToPrim2("valueOf", afterValueOfT); + il.MarkLabel(afterValueOfT); + + var afterToStringT = il.DefineLabel(); + var stillObjT = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, stillObjT); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brfalse, afterToStringT); + il.MarkLabel(stillObjT); + TryToPrim2("toString", afterToStringT); + il.MarkLabel(afterToStringT); + + // ECMA-262 7.1.1.1: if both methods returned non-primitives, throw TypeError. + var afterTeT = il.DefineLabel(); + var stillObjTeT = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, stillObjTeT); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brfalse, afterTeT); + il.MarkLabel(stillObjTeT); + il.Emit(OpCodes.Ldstr, "Cannot convert object to primitive value"); + il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); + il.Emit(OpCodes.Call, runtime.CreateException); + il.Emit(OpCodes.Throw); + il.MarkLabel(afterTeT); + + il.MarkLabel(skipToPrimLabelTop); + + // ECMA-262 7.1.4 ToNumber on Symbol → throws TypeError. Without this, + // Convert.ToDouble would catch the InvalidCastException → NaN → 0, + // silently masking the spec-required throw (e.g. `(0).toFixed(Symbol())` + // must throw, not silently produce "0"). + var notSymbolLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, runtime.TSSymbolType); + il.Emit(OpCodes.Brfalse, notSymbolLabel); + il.Emit(OpCodes.Ldstr, "Cannot convert a Symbol value to a number"); + il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); + il.Emit(OpCodes.Call, runtime.CreateException); + il.Emit(OpCodes.Throw); + il.MarkLabel(notSymbolLabel); + + // ECMA-262 7.1.4 step 2: BigInt → TypeError. `(0).toFixed(0n)` must + // throw not silently coerce. Convert.ToDouble would otherwise narrow + // BigInteger to its double value (or throw OverflowException → NaN). + var notBigIntLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, _types.BigInteger); + il.Emit(OpCodes.Brfalse, notBigIntLabel); + il.Emit(OpCodes.Ldstr, "Cannot convert a BigInt to a number"); + il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); + il.Emit(OpCodes.Call, runtime.CreateException); + il.Emit(OpCodes.Throw); + il.MarkLabel(notBigIntLabel); + + // ECMA-262 ToNumber: strings with "0x"/"0X" prefix parse as hex. Convert.ToDouble + // throws on those, so special-case before the fallback. Without this, tests that + // set `length: "0x0002"` on array-likes surface as NaN → 0 → empty iteration. + var tryParseInt64 = _types.GetMethod(_types.Int64, "Parse", _types.String, typeof(System.Globalization.NumberStyles), typeof(System.IFormatProvider)); + var notHexLabel = il.DefineLabel(); + var strLocal = il.DeclareLocal(_types.String); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Stloc, strLocal); + il.Emit(OpCodes.Brfalse, notHexLabel); + + // if (strLocal.Length >= 2 && (strLocal[0] == '0') && (strLocal[1] == 'x' || strLocal[1] == 'X')) + il.Emit(OpCodes.Ldloc, strLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length").GetGetMethod()!); + il.Emit(OpCodes.Ldc_I4_2); + il.Emit(OpCodes.Blt, notHexLabel); + + il.Emit(OpCodes.Ldloc, strLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Callvirt, _types.String.GetMethod("get_Chars", [_types.Int32])!); + il.Emit(OpCodes.Ldc_I4, (int)'0'); + il.Emit(OpCodes.Bne_Un, notHexLabel); + + // second char == 'x' or 'X': compare with OR'd check. Use (ch | 0x20) == 'x'. + il.Emit(OpCodes.Ldloc, strLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Callvirt, _types.String.GetMethod("get_Chars", [_types.Int32])!); + il.Emit(OpCodes.Ldc_I4, 0x20); + il.Emit(OpCodes.Or); + il.Emit(OpCodes.Ldc_I4, (int)'x'); + il.Emit(OpCodes.Bne_Un, notHexLabel); + + // Hex-parse: strLocal.Substring(2), try Int64.Parse with HexNumber style. + il.BeginExceptionBlock(); + il.Emit(OpCodes.Ldloc, strLocal); + il.Emit(OpCodes.Ldc_I4_2); + il.Emit(OpCodes.Callvirt, _types.String.GetMethod("Substring", [_types.Int32])!); + il.Emit(OpCodes.Ldc_I4, (int)System.Globalization.NumberStyles.HexNumber); + // CultureInfo.InvariantCulture — property getter, not a static field. Use Call. + il.Emit(OpCodes.Call, typeof(System.Globalization.CultureInfo).GetProperty("InvariantCulture")!.GetGetMethod()!); + il.Emit(OpCodes.Call, tryParseInt64); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Stloc, resultLocal); + il.BeginCatchBlock(_types.Exception); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldc_R8, double.NaN); + il.Emit(OpCodes.Stloc, resultLocal); + il.EndExceptionBlock(); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notHexLabel); + + // ECMA-262: strings with "0b"/"0B" parse as binary literals, + // "0o"/"0O" as octal. Convert.ToDouble doesn't recognize these. + // Pattern: "0[bB][01]+" or "0[oO][0-7]+". + EmitParsePrefixedInt(il, strLocal, resultLocal, 'b', 2); + EmitParsePrefixedInt(il, strLocal, resultLocal, 'o', 8); + + // Handle "Infinity"/"+Infinity"/"-Infinity" strings before Convert.ToDouble + // (which throws FormatException on those — caught below as NaN, but + // ECMA-262 specifies +Infinity/-Infinity numeric values). + var notInfStrLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, strLocal); + il.Emit(OpCodes.Brfalse, notInfStrLabel); + + // Trim and check + var trimmedLocal = il.DeclareLocal(_types.String); + il.Emit(OpCodes.Ldloc, strLocal); + il.Emit(OpCodes.Callvirt, _types.String.GetMethod("Trim", Type.EmptyTypes)!); + il.Emit(OpCodes.Stloc, trimmedLocal); + + // "Infinity" → +Inf + var notPlainInf = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, trimmedLocal); + il.Emit(OpCodes.Ldstr, "Infinity"); + il.Emit(OpCodes.Call, _types.String.GetMethod("op_Equality", [_types.String, _types.String])!); + il.Emit(OpCodes.Brfalse, notPlainInf); + il.Emit(OpCodes.Ldc_R8, double.PositiveInfinity); + il.Emit(OpCodes.Ret); + il.MarkLabel(notPlainInf); + + // "+Infinity" → +Inf + var notPlusInf = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, trimmedLocal); + il.Emit(OpCodes.Ldstr, "+Infinity"); + il.Emit(OpCodes.Call, _types.String.GetMethod("op_Equality", [_types.String, _types.String])!); + il.Emit(OpCodes.Brfalse, notPlusInf); + il.Emit(OpCodes.Ldc_R8, double.PositiveInfinity); + il.Emit(OpCodes.Ret); + il.MarkLabel(notPlusInf); + + // "-Infinity" → -Inf + var notMinusInf = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, trimmedLocal); + il.Emit(OpCodes.Ldstr, "-Infinity"); + il.Emit(OpCodes.Call, _types.String.GetMethod("op_Equality", [_types.String, _types.String])!); + il.Emit(OpCodes.Brfalse, notMinusInf); + il.Emit(OpCodes.Ldc_R8, double.NegativeInfinity); + il.Emit(OpCodes.Ret); + il.MarkLabel(notMinusInf); + + il.MarkLabel(notInfStrLabel); + + // Use Convert.ToDouble with try-catch fallback to NaN + il.BeginExceptionBlock(); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Convert, "ToDouble", _types.Object)); + il.Emit(OpCodes.Stloc, resultLocal); + il.BeginCatchBlock(_types.Exception); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldc_R8, double.NaN); + il.Emit(OpCodes.Stloc, resultLocal); + il.EndExceptionBlock(); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits JsToInt32(object) → int that implements ECMA-262 ToInt32 + /// (7.1.6 in the spec). Unlike Convert.ToInt32, NaN / ±Infinity / out-of-range + /// doubles wrap modulo 2^32 instead of throwing, matching JavaScript's + /// bitwise-op and `x | 0` semantics. Required for packages like lodash + /// and debug that rely on hash |= 0 idioms. + /// + /// + /// Emits IL that parses "0[Pp]" prefixed integer-literal strings + /// to a double. If the string at doesn't + /// match the prefix shape, falls through. Used by ToNumber for binary + /// (0b/0B, radix 2) and octal (0o/0O, radix 8) literal support per + /// ECMA-262 7.1.4.1. Hex (0x/0X) has its own inline path since it + /// predates this helper. + /// + private void EmitParsePrefixedInt(ILGenerator il, LocalBuilder strLocal, + LocalBuilder resultLocal, char prefix, int radix) + { + var skipLabel = il.DefineLabel(); + + // strLocal == null? skip. + il.Emit(OpCodes.Ldloc, strLocal); + il.Emit(OpCodes.Brfalse, skipLabel); + + // Length >= 3? + il.Emit(OpCodes.Ldloc, strLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length").GetGetMethod()!); + il.Emit(OpCodes.Ldc_I4_3); + il.Emit(OpCodes.Blt, skipLabel); + + // strLocal[0] == '0'? + il.Emit(OpCodes.Ldloc, strLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Callvirt, _types.String.GetMethod("get_Chars", [_types.Int32])!); + il.Emit(OpCodes.Ldc_I4, (int)'0'); + il.Emit(OpCodes.Bne_Un, skipLabel); + + // (strLocal[1] | 0x20) == prefix? + il.Emit(OpCodes.Ldloc, strLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Callvirt, _types.String.GetMethod("get_Chars", [_types.Int32])!); + il.Emit(OpCodes.Ldc_I4, 0x20); + il.Emit(OpCodes.Or); + il.Emit(OpCodes.Ldc_I4, (int)prefix); + il.Emit(OpCodes.Bne_Un, skipLabel); + + // try { result = (double)Convert.ToInt64(strLocal.Substring(2), radix); } + il.BeginExceptionBlock(); + il.Emit(OpCodes.Ldloc, strLocal); + il.Emit(OpCodes.Ldc_I4_2); + il.Emit(OpCodes.Callvirt, _types.String.GetMethod("Substring", [_types.Int32])!); + il.Emit(OpCodes.Ldc_I4, radix); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Convert, "ToInt64", _types.String, _types.Int32)); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Stloc, resultLocal); + il.BeginCatchBlock(_types.Exception); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldc_R8, double.NaN); + il.Emit(OpCodes.Stloc, resultLocal); + il.EndExceptionBlock(); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ret); + + il.MarkLabel(skipLabel); + } + + private void EmitJsToInt32(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // Signature forward-declared by DefineRuntimeClassPhase1 so $RegExp + // (which emits before $Runtime's body) can call it; reuse that slot. + var method = (MethodBuilder)runtime.JsToInt32; + + var il = method.GetILGenerator(); + + const double TWO_32 = 4294967296.0; + const double TWO_31 = 2147483648.0; + + // n = ToNumber(arg0) + var nLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.ToNumber); + il.Emit(OpCodes.Stloc, nLocal); + + // if (!double.IsFinite(n)) return 0 + var finiteLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, nLocal); + il.Emit(OpCodes.Call, typeof(double).GetMethod("IsFinite", [typeof(double)])!); + il.Emit(OpCodes.Brtrue, finiteLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + il.MarkLabel(finiteLabel); + + // truncated = n >= 0 ? Math.Floor(n) : Math.Ceiling(n) + var truncLocal = il.DeclareLocal(_types.Double); + var negLabel = il.DefineLabel(); + var truncDoneLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, nLocal); + il.Emit(OpCodes.Ldc_R8, 0.0); + il.Emit(OpCodes.Blt, negLabel); + il.Emit(OpCodes.Ldloc, nLocal); + il.Emit(OpCodes.Call, typeof(Math).GetMethod("Floor", [typeof(double)])!); + il.Emit(OpCodes.Stloc, truncLocal); + il.Emit(OpCodes.Br, truncDoneLabel); + il.MarkLabel(negLabel); + il.Emit(OpCodes.Ldloc, nLocal); + il.Emit(OpCodes.Call, typeof(Math).GetMethod("Ceiling", [typeof(double)])!); + il.Emit(OpCodes.Stloc, truncLocal); + il.MarkLabel(truncDoneLabel); + + // int32bit = truncated - Math.Floor(truncated / 2^32) * 2^32 (mathematical mod) + var int32bitLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Ldloc, truncLocal); + il.Emit(OpCodes.Ldloc, truncLocal); + il.Emit(OpCodes.Ldc_R8, TWO_32); + il.Emit(OpCodes.Div); + il.Emit(OpCodes.Call, typeof(Math).GetMethod("Floor", [typeof(double)])!); + il.Emit(OpCodes.Ldc_R8, TWO_32); + il.Emit(OpCodes.Mul); + il.Emit(OpCodes.Sub); + il.Emit(OpCodes.Stloc, int32bitLocal); + + // return int32bit >= 2^31 ? (int)(int32bit - 2^32) : (int)int32bit + var smallLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, int32bitLocal); + il.Emit(OpCodes.Ldc_R8, TWO_31); + il.Emit(OpCodes.Blt, smallLabel); + il.Emit(OpCodes.Ldloc, int32bitLocal); + il.Emit(OpCodes.Ldc_R8, TWO_32); + il.Emit(OpCodes.Sub); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Ret); + il.MarkLabel(smallLabel); + il.Emit(OpCodes.Ldloc, int32bitLocal); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits ConvertToNumber — matches JS Number(value) semantics. + /// Differs from ToNumber in that empty/whitespace strings return 0 (not NaN). + /// + /// + /// Pre-declares the $Runtime.ConvertToNumber MethodBuilder so its slot is + /// assigned before any other emitter binds to it. Body is filled in by + /// later, after GetProperty/InvokeMethodValue + /// are available (the ToPrimitive(value, "number") chain on Dictionary/$Object + /// args calls those helpers). + /// + internal void DeclareConvertToNumber(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "ConvertToNumber", + MethodAttributes.Public | MethodAttributes.Static, + _types.Double, + [_types.Object] + ); + runtime.ConvertToNumber = method; + } + + private void EmitConvertToNumber(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = (MethodBuilder)runtime.ConvertToNumber; + + var il = method.GetILGenerator(); + + var nullLabel = il.DefineLabel(); + var undefinedLabel = il.DefineLabel(); + var doubleLabel = il.DefineLabel(); + var boolLabel = il.DefineLabel(); + var stringLabel = il.DefineLabel(); + var nanLabel = il.DefineLabel(); + + // ECMA-262 7.1.4 ToNumber on object: ToPrimitive(value, "number") which + // tries valueOf first, then toString. Without this, Number({valueOf: + // () => 1}) returns NaN (object falls through to nan-label). + // Apply for Dictionary or $Object only — those hold user-defined + // valueOf/toString. Boxed primitives (via $Object marker fields) are + // also $Object, which is correct: their valueOf returns the underlying + // primitive per ECMA-262 19.x.3.7. + var argLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Stloc, argLocal); + + var skipToPrimLabel = il.DefineLabel(); + var doToPrimLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, doToPrimLabel); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brfalse, skipToPrimLabel); + il.MarkLabel(doToPrimLabel); + + // ToPrimitive: try valueOf, then toString. Mirrors EmitLengthToPrimitive's + // logic but writes to argLocal so the existing branches below see the + // (possibly replaced) primitive value. + var emptyArgsLocal = il.DeclareLocal(_types.ObjectArray); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Stloc, emptyArgsLocal); + + void TryToPrim(string name, Label afterLabel) + { + var fnLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Ldstr, name); + il.Emit(OpCodes.Call, runtime.GetProperty); + il.Emit(OpCodes.Stloc, fnLocal); + il.Emit(OpCodes.Ldloc, fnLocal); + il.Emit(OpCodes.Brfalse, afterLabel); + il.Emit(OpCodes.Ldloc, fnLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, afterLabel); + + var resultLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Ldloc, fnLocal); + il.Emit(OpCodes.Ldloc, emptyArgsLocal); + il.Emit(OpCodes.Call, runtime.InvokeMethodValue); + il.Emit(OpCodes.Stloc, resultLocal); + + // Still object? Fall through to next attempt without committing. + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, afterLabel); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brtrue, afterLabel); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Stloc, argLocal); + } + + var afterValueOf = il.DefineLabel(); + TryToPrim("valueOf", afterValueOf); + il.MarkLabel(afterValueOf); + + var afterToString = il.DefineLabel(); + var stillObj = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, stillObj); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brfalse, afterToString); + il.MarkLabel(stillObj); + TryToPrim("toString", afterToString); + il.MarkLabel(afterToString); + + // ECMA-262 7.1.1.1 OrdinaryToPrimitive: if neither valueOf nor toString + // returned a primitive (both returned objects, or neither was callable), + // throw TypeError. Pre-fix: the value fell through to NaN, silently + // masking the spec-required throw. + var afterTypeErrorLabel = il.DefineLabel(); + var stillObjAfterToString = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, stillObjAfterToString); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brfalse, afterTypeErrorLabel); + il.MarkLabel(stillObjAfterToString); + il.Emit(OpCodes.Ldstr, "Cannot convert object to primitive value"); + il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); + il.Emit(OpCodes.Call, runtime.CreateException); + il.Emit(OpCodes.Throw); + il.MarkLabel(afterTypeErrorLabel); + + il.MarkLabel(skipToPrimLabel); + + // null => 0.0 + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Brfalse, nullLabel); + + // ECMA-262 21.1.1.1 → 7.1.4: Number(Symbol) throws TypeError. Without + // this branch the value falls through to the "everything else → NaN" + // tail, masking the spec-required throw. + var notSymbolConvLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, runtime.TSSymbolType); + il.Emit(OpCodes.Brfalse, notSymbolConvLabel); + il.Emit(OpCodes.Ldstr, "Cannot convert a Symbol value to a number"); + il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); + il.Emit(OpCodes.Call, runtime.CreateException); + il.Emit(OpCodes.Throw); + il.MarkLabel(notSymbolConvLabel); + + // ECMA-262 21.1.1.1 step 5: Number(BigInt) returns a Number with the + // same mathematical value (rounded per 21.1.1.1.1 NumberFromBigInt). + // System.Numerics.BigInteger has an explicit op_Explicit(BigInteger) + // → double — use it. Don't throw (ToNumber's spec wants throw, but + // ConvertToNumber backs the Number() constructor's spec which coerces). + var notBigIntConvLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, _types.BigInteger); + il.Emit(OpCodes.Brfalse, notBigIntConvLabel); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Unbox_Any, _types.BigInteger); + // System.Numerics.BigInteger has multiple op_Explicit overloads + // (one per primitive return type). Pick the BigInteger → double one + // by walking the candidate set explicitly. + System.Reflection.MethodInfo? explicitToDouble = null; + foreach (var m in _types.BigInteger.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)) + { + if (m.Name != "op_Explicit") continue; + if (m.ReturnType != _types.Double) continue; + var ps = m.GetParameters(); + if (ps.Length == 1 && ps[0].ParameterType == _types.BigInteger) + { + explicitToDouble = m; + break; + } + } + if (explicitToDouble != null) + { + il.Emit(OpCodes.Call, explicitToDouble); + il.Emit(OpCodes.Ret); + } + else + { + // Fallback: Convert.ToDouble(BigInteger) via boxed object. + il.Emit(OpCodes.Box, _types.BigInteger); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Convert, "ToDouble", _types.Object)); + il.Emit(OpCodes.Ret); + } + il.MarkLabel(notBigIntConvLabel); + + // undefined => NaN + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, undefinedLabel); + + // double => return as-is + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, _types.Double); + il.Emit(OpCodes.Brtrue, doubleLabel); + + // bool => true:1.0, false:0.0 + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, _types.Boolean); + il.Emit(OpCodes.Brtrue, boolLabel); + + // string => trim, empty→0, tryparse, else NaN + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, stringLabel); + + // .NET interop: boxed non-double numerics (float, int, long, decimal, …) + // arrive via @DotNetType calls. Convert them numerically rather than + // falling through to NaN (EnsureDouble routes through this method). + var notConvertibleLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Isinst, typeof(IConvertible)); + il.Emit(OpCodes.Brfalse, notConvertibleLabel); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Convert, "ToDouble", _types.Object)); + il.Emit(OpCodes.Ret); + il.MarkLabel(notConvertibleLabel); + + // everything else => NaN + il.Emit(OpCodes.Br, nanLabel); + + // null case: return 0.0 + il.MarkLabel(nullLabel); + il.Emit(OpCodes.Ldc_R8, 0.0); + il.Emit(OpCodes.Ret); + + // undefined case: return NaN + il.MarkLabel(undefinedLabel); + il.Emit(OpCodes.Ldc_R8, double.NaN); + il.Emit(OpCodes.Ret); + + // double case: unbox and return + il.MarkLabel(doubleLabel); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Unbox_Any, _types.Double); + il.Emit(OpCodes.Ret); + + // bool case: unbox, convert to float + il.MarkLabel(boolLabel); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Unbox_Any, _types.Boolean); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Ret); + + // string case: trim, check empty, try parse + il.MarkLabel(stringLabel); + var trimmedLocal = il.DeclareLocal(_types.String); + var resultLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Ldloc, argLocal); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.String, "Trim")); + il.Emit(OpCodes.Stloc, trimmedLocal); + + // if (trimmed.Length == 0) return 0.0 + var nonEmptyLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, trimmedLocal); + il.Emit(OpCodes.Callvirt, _types.String.GetProperty("Length")!.GetGetMethod()!); + il.Emit(OpCodes.Brtrue, nonEmptyLabel); + il.Emit(OpCodes.Ldc_R8, 0.0); + il.Emit(OpCodes.Ret); + + // ECMA-262 21.1.1.1 / 7.1.4: numeric string can be hex ("0x..."/"0X..."), + // Infinity (already short-circuited by ToNumber), or float. Try hex first. + il.MarkLabel(nonEmptyLabel); + var notHexInConvLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, trimmedLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length").GetGetMethod()!); + il.Emit(OpCodes.Ldc_I4_2); + il.Emit(OpCodes.Blt, notHexInConvLabel); + il.Emit(OpCodes.Ldloc, trimmedLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Callvirt, _types.String.GetMethod("get_Chars", [_types.Int32])!); + il.Emit(OpCodes.Ldc_I4, (int)'0'); + il.Emit(OpCodes.Bne_Un, notHexInConvLabel); + il.Emit(OpCodes.Ldloc, trimmedLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Callvirt, _types.String.GetMethod("get_Chars", [_types.Int32])!); + il.Emit(OpCodes.Ldc_I4, 0x20); + il.Emit(OpCodes.Or); + il.Emit(OpCodes.Ldc_I4, (int)'x'); + il.Emit(OpCodes.Bne_Un, notHexInConvLabel); + il.BeginExceptionBlock(); + il.Emit(OpCodes.Ldloc, trimmedLocal); + il.Emit(OpCodes.Ldc_I4_2); + il.Emit(OpCodes.Callvirt, _types.String.GetMethod("Substring", [_types.Int32])!); + il.Emit(OpCodes.Ldc_I4, (int)System.Globalization.NumberStyles.HexNumber); + il.Emit(OpCodes.Call, typeof(System.Globalization.CultureInfo).GetProperty("InvariantCulture")!.GetGetMethod()!); + il.Emit(OpCodes.Call, typeof(long).GetMethod("Parse", [typeof(string), typeof(System.Globalization.NumberStyles), typeof(IFormatProvider)])!); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Stloc, resultLocal); + il.BeginCatchBlock(_types.Exception); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldc_R8, double.NaN); + il.Emit(OpCodes.Stloc, resultLocal); + il.EndExceptionBlock(); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notHexInConvLabel); + // ECMA-262: "0b"/"0B" → binary, "0o"/"0O" → octal literal parsing. + EmitParsePrefixedInt(il, trimmedLocal, resultLocal, 'b', 2); + EmitParsePrefixedInt(il, trimmedLocal, resultLocal, 'o', 8); + + // ECMA-262 7.1.4: only the case-sensitive forms "Infinity", "+Infinity", + // "-Infinity" are valid Infinity literals. .NET's Double.TryParse + // (NumberStyles.Float) accepts "infinity"/"INFINITY"/etc case- + // insensitively — must reject those before TryParse runs. + var notCiInfLabel = il.DefineLabel(); + // Exact case-sensitive forms first → return ±Infinity. + var trimEqInfLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, trimmedLocal); + il.Emit(OpCodes.Ldstr, "Infinity"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, trimEqInfLabel); + il.Emit(OpCodes.Ldc_R8, double.PositiveInfinity); + il.Emit(OpCodes.Ret); + il.MarkLabel(trimEqInfLabel); + var trimEqPlusInfLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, trimmedLocal); + il.Emit(OpCodes.Ldstr, "+Infinity"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, trimEqPlusInfLabel); + il.Emit(OpCodes.Ldc_R8, double.PositiveInfinity); + il.Emit(OpCodes.Ret); + il.MarkLabel(trimEqPlusInfLabel); + var trimEqMinusInfLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, trimmedLocal); + il.Emit(OpCodes.Ldstr, "-Infinity"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, trimEqMinusInfLabel); + il.Emit(OpCodes.Ldc_R8, double.NegativeInfinity); + il.Emit(OpCodes.Ret); + il.MarkLabel(trimEqMinusInfLabel); + // Now reject any string that contains "infinity" case-insensitively + // (since the exact-case forms have already been short-circuited). + // Use String.Contains(string, StringComparison) — net8+ overload. + il.Emit(OpCodes.Ldloc, trimmedLocal); + il.Emit(OpCodes.Ldstr, "infinity"); + il.Emit(OpCodes.Ldc_I4_5); // StringComparison.OrdinalIgnoreCase + il.Emit(OpCodes.Callvirt, _types.String.GetMethod("Contains", [_types.String, typeof(StringComparison)])!); + il.Emit(OpCodes.Brfalse, notCiInfLabel); + // Case-insensitive but not exact → NaN. + il.Emit(OpCodes.Ldc_R8, double.NaN); + il.Emit(OpCodes.Ret); + il.MarkLabel(notCiInfLabel); + + // double.TryParse(trimmed, NumberStyles.Float, CultureInfo.InvariantCulture, out result) + il.Emit(OpCodes.Ldloc, trimmedLocal); + il.Emit(OpCodes.Ldc_I4, (int)System.Globalization.NumberStyles.Float); + il.Emit(OpCodes.Call, typeof(System.Globalization.CultureInfo).GetProperty("InvariantCulture")!.GetGetMethod()!); + il.Emit(OpCodes.Ldloca, resultLocal); + il.Emit(OpCodes.Call, typeof(double).GetMethod("TryParse", [typeof(string), typeof(System.Globalization.NumberStyles), typeof(IFormatProvider), typeof(double).MakeByRefType()])!); + var parseSuccessLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, parseSuccessLabel); + // parse failed => NaN + il.Emit(OpCodes.Ldc_R8, double.NaN); + il.Emit(OpCodes.Ret); + // parse succeeded => return result + il.MarkLabel(parseSuccessLabel); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ret); + + // NaN fallback + il.MarkLabel(nanLabel); + il.Emit(OpCodes.Ldc_R8, double.NaN); + il.Emit(OpCodes.Ret); + } + + private void EmitIsTruthy(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // Signature forward-declared by DefineRuntimeClassPhase1 so $RegExp + // (which emits before $Runtime's body) can call it; reuse that slot. + var method = (MethodBuilder)runtime.IsTruthy; + + var il = method.GetILGenerator(); + var falseLabel = il.DefineLabel(); + var checkBool = il.DefineLabel(); + var checkDouble = il.DefineLabel(); + var checkString = il.DefineLabel(); + var checkBigInt = il.DefineLabel(); + var trueLabel = il.DefineLabel(); + + // null => false + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brfalse, falseLabel); + + // undefined => false + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, falseLabel); + + // bool => return value + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Boolean); + il.Emit(OpCodes.Brtrue, checkBool); + + // double => check for 0 and NaN + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Double); + il.Emit(OpCodes.Brtrue, checkDouble); + + // string => check for empty + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, checkString); + + // bigint => 0n is falsy (ToBoolean(bigint): false iff zero) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.BigInteger); + il.Emit(OpCodes.Brtrue, checkBigInt); + + // everything else => true + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + + // Check bool value + il.MarkLabel(checkBool); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, _types.Boolean); + il.Emit(OpCodes.Ret); + + // Check double: 0 and NaN are falsy + il.MarkLabel(checkDouble); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, _types.Double); + var dLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Stloc, dLocal); + // Check if d == 0 + il.Emit(OpCodes.Ldloc, dLocal); + il.Emit(OpCodes.Ldc_R8, 0.0); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Brtrue, falseLabel); + // Check if d is NaN (NaN != NaN) + il.Emit(OpCodes.Ldloc, dLocal); + il.Emit(OpCodes.Ldloc, dLocal); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Brfalse, falseLabel); // If d != d, it's NaN + // Not 0 and not NaN => truthy + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + + // Check string: empty is falsy + il.MarkLabel(checkString); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Length")); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Cgt); + il.Emit(OpCodes.Ret); + + // Check bigint: truthy iff non-zero (value != BigInteger.Zero). + il.MarkLabel(checkBigInt); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, _types.BigInteger); + il.Emit(OpCodes.Call, _types.GetMethod(_types.BigInteger, "get_Zero")); + il.Emit(OpCodes.Call, _types.GetMethod(_types.BigInteger, "op_Inequality", _types.BigInteger, _types.BigInteger)); + il.Emit(OpCodes.Ret); + + il.MarkLabel(falseLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + } +} diff --git a/Compilation/RuntimeEmitter.ConsoleHelpers.cs b/Compilation/RuntimeEmitter.ConsoleHelpers.cs index 2e25c7fe..3dec2134 100644 --- a/Compilation/RuntimeEmitter.ConsoleHelpers.cs +++ b/Compilation/RuntimeEmitter.ConsoleHelpers.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Reflection.Emit; +using System.Text; namespace SharpTS.Compilation; @@ -1107,4 +1108,1180 @@ private void EmitConsoleTraceMultiple(TypeBuilder typeBuilder, EmittedRuntime ru il.Emit(OpCodes.Ret); } + + /// + /// Emits: public static string GetConsoleIndent() + /// Returns a string of spaces based on _consoleGroupLevel (2 spaces per level). + /// + private void EmitGetConsoleIndent(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "GetConsoleIndent", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + Type.EmptyTypes + ); + runtime.GetConsoleIndent = method; + + var il = method.GetILGenerator(); + + // if (_consoleGroupLevel <= 0) return "" + var hasIndentLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldsfld, runtime.ConsoleGroupLevelField); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Bgt, hasIndentLabel); + il.Emit(OpCodes.Ldstr, ""); + il.Emit(OpCodes.Ret); + + il.MarkLabel(hasIndentLabel); + // return new string(' ', _consoleGroupLevel * 2) + il.Emit(OpCodes.Ldc_I4_S, (sbyte)' '); + il.Emit(OpCodes.Ldsfld, runtime.ConsoleGroupLevelField); + il.Emit(OpCodes.Ldc_I4_2); + il.Emit(OpCodes.Mul); + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.String, [_types.Char, _types.Int32])); + il.Emit(OpCodes.Ret); + } + + private void EmitConsoleLog(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "ConsoleLog", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.Object] + ); + runtime.ConsoleLog = method; + + var il = method.GetILGenerator(); + var noFormatLabel = il.DefineLabel(); + + // number[] unboxing: materialize a numeric-mode $Array before any array-formatting reads its base list. + EmitDeoptArgIfNumericArray(il, runtime, 0); + + // Check if arg is a string with format specifiers + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brfalse, noFormatLabel); + + // Check HasFormatSpecifiers + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Call, runtime.HasFormatSpecifiers); + il.Emit(OpCodes.Brfalse, noFormatLabel); + + // Has format specifiers - process with FormatSingleArg, then prepend indent + // Console.WriteLine(GetConsoleIndent() + FormatSingleArg(value)) + il.Emit(OpCodes.Call, runtime.GetConsoleIndent); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Call, runtime.FormatSingleArg); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "Concat", _types.String, _types.String)); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Console, "WriteLine", _types.String)); + il.Emit(OpCodes.Ret); + + // No format specifiers - call Stringify then prepend indent + // Console.WriteLine(GetConsoleIndent() + Stringify(value)) + il.MarkLabel(noFormatLabel); + il.Emit(OpCodes.Call, runtime.GetConsoleIndent); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "Concat", _types.String, _types.String)); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Console, "WriteLine", _types.String)); + il.Emit(OpCodes.Ret); + } + + private void EmitFormatSingleArg(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // Process format specifiers in a single string (handles %% -> % and unsubstituted specifiers) + var method = typeBuilder.DefineMethod( + "FormatSingleArg", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.String] + ); + runtime.FormatSingleArg = method; + + var il = method.GetILGenerator(); + + // StringBuilder result = new StringBuilder() + var resultLocal = il.DeclareLocal(_types.StringBuilder); + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, _types.EmptyTypes)); + il.Emit(OpCodes.Stloc, resultLocal); + + // int i = 0 + var iLocal = il.DeclareLocal(_types.Int32); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, iLocal); + + var loopStart = il.DefineLabel(); + var loopEnd = il.DefineLabel(); + + // while (i < format.Length) + il.MarkLabel(loopStart); + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Length")); + il.Emit(OpCodes.Bge, loopEnd); + + // char c = format[i] + var charLocal = il.DeclareLocal(_types.Char); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Chars", _types.Int32)); + il.Emit(OpCodes.Stloc, charLocal); + + // if (c == '%' && i + 1 < format.Length && format[i+1] == '%') + var notPercent = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, charLocal); + il.Emit(OpCodes.Ldc_I4, (int)'%'); + il.Emit(OpCodes.Bne_Un, notPercent); + + // Check i + 1 < format.Length + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Length")); + il.Emit(OpCodes.Bge, notPercent); + + // Check format[i+1] == '%' + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Chars", _types.Int32)); + il.Emit(OpCodes.Ldc_I4, (int)'%'); + il.Emit(OpCodes.Bne_Un, notPercent); + + // Append '%' and skip 2 + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldc_I4, (int)'%'); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.Char)); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldc_I4_2); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, iLocal); + il.Emit(OpCodes.Br, loopStart); + + // Regular character - append as-is + il.MarkLabel(notPercent); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldloc, charLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.Char)); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, iLocal); + il.Emit(OpCodes.Br, loopStart); + + il.MarkLabel(loopEnd); + + // Return result.ToString() + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); + il.Emit(OpCodes.Ret); + } + + private void EmitConsoleLogMultiple(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "ConsoleLogMultiple", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.ObjectArray] + ); + runtime.ConsoleLogMultiple = method; + + var il = method.GetILGenerator(); + + // Check if first argument is a format string with specifiers + // if (args.Length > 0 && args[0] is string fmt && HasFormatSpecifiers(fmt)) + var noFormatLabel = il.DefineLabel(); + + // Check args.Length > 0 + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ble, noFormatLabel); + + // Check args[0] is string + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brfalse, noFormatLabel); + + // Check HasFormatSpecifiers(args[0] as string) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Call, runtime.HasFormatSpecifiers); + il.Emit(OpCodes.Brfalse, noFormatLabel); + + // Format string case: call FormatConsoleArgs, prepend indent + // Console.WriteLine(GetConsoleIndent() + FormatConsoleArgs(args)) + il.Emit(OpCodes.Call, runtime.GetConsoleIndent); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.FormatConsoleArgs); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "Concat", _types.String, _types.String)); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Console, "WriteLine", _types.String)); + il.Emit(OpCodes.Ret); + + // No format specifiers: join with spaces using Stringify for JS-compatible output + // Console.WriteLine(GetConsoleIndent() + JoinWithStringify(" ", args)) + il.MarkLabel(noFormatLabel); + il.Emit(OpCodes.Call, runtime.GetConsoleIndent); + il.Emit(OpCodes.Ldstr, " "); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.JoinWithStringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "Concat", _types.String, _types.String)); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Console, "WriteLine", _types.String)); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits JoinWithStringify: joins array elements with separator using Stringify for JS-compatible output. + /// Signature: string JoinWithStringify(string separator, object[] args) + /// + private void EmitJoinWithStringify(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "JoinWithStringify", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.String, _types.ObjectArray] + ); + runtime.JoinWithStringify = method; + + var il = method.GetILGenerator(); + + // StringBuilder result = new StringBuilder() + var resultLocal = il.DeclareLocal(_types.StringBuilder); + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, _types.EmptyTypes)); + il.Emit(OpCodes.Stloc, resultLocal); + + // int i = 0 + var indexLocal = il.DeclareLocal(_types.Int32); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, indexLocal); + + // Loop + var loopStart = il.DefineLabel(); + var loopEnd = il.DefineLabel(); + + il.MarkLabel(loopStart); + // if (i >= args.Length) goto loopEnd + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Bge, loopEnd); + + // if (i > 0) result.Append(separator) + var skipSeparator = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ble, skipSeparator); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldarg_0); // separator + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.MarkLabel(skipSeparator); + + // result.Append(Stringify(args[i])) + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + // i++ + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, indexLocal); + il.Emit(OpCodes.Br, loopStart); + + il.MarkLabel(loopEnd); + + // return result.ToString() + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); + il.Emit(OpCodes.Ret); + } + + private void EmitHasFormatSpecifiers(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "HasFormatSpecifiers", + MethodAttributes.Public | MethodAttributes.Static, + _types.Boolean, + [_types.String] + ); + runtime.HasFormatSpecifiers = method; + + var il = method.GetILGenerator(); + var indexLocal = il.DeclareLocal(_types.Int32); + var loopStart = il.DefineLabel(); + var loopEnd = il.DefineLabel(); + var returnFalse = il.DefineLabel(); + + // int i = 0 + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, indexLocal); + + // Loop: while (i < str.Length - 1) + il.MarkLabel(loopStart); + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Length")); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Sub); + il.Emit(OpCodes.Bge, returnFalse); + + // if (str[i] == '%') + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Chars", _types.Int32)); + il.Emit(OpCodes.Ldc_I4, (int)'%'); + var notPercentLabel = il.DefineLabel(); + il.Emit(OpCodes.Bne_Un, notPercentLabel); + + // Check next char is s, d, i, f, o, O, or j + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Chars", _types.Int32)); + var nextCharLocal = il.DeclareLocal(_types.Char); + il.Emit(OpCodes.Stloc, nextCharLocal); + + // Check for each specifier (including %% escape) + var checkNext = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, nextCharLocal); + il.Emit(OpCodes.Ldc_I4, (int)'%'); + var returnTrue = il.DefineLabel(); + il.Emit(OpCodes.Beq, returnTrue); + + il.Emit(OpCodes.Ldloc, nextCharLocal); + il.Emit(OpCodes.Ldc_I4, (int)'s'); + il.Emit(OpCodes.Beq, returnTrue); + + il.Emit(OpCodes.Ldloc, nextCharLocal); + il.Emit(OpCodes.Ldc_I4, (int)'d'); + il.Emit(OpCodes.Beq, returnTrue); + + il.Emit(OpCodes.Ldloc, nextCharLocal); + il.Emit(OpCodes.Ldc_I4, (int)'i'); + il.Emit(OpCodes.Beq, returnTrue); + + il.Emit(OpCodes.Ldloc, nextCharLocal); + il.Emit(OpCodes.Ldc_I4, (int)'f'); + il.Emit(OpCodes.Beq, returnTrue); + + il.Emit(OpCodes.Ldloc, nextCharLocal); + il.Emit(OpCodes.Ldc_I4, (int)'o'); + il.Emit(OpCodes.Beq, returnTrue); + + il.Emit(OpCodes.Ldloc, nextCharLocal); + il.Emit(OpCodes.Ldc_I4, (int)'O'); + il.Emit(OpCodes.Beq, returnTrue); + + il.Emit(OpCodes.Ldloc, nextCharLocal); + il.Emit(OpCodes.Ldc_I4, (int)'j'); + il.Emit(OpCodes.Beq, returnTrue); + + il.MarkLabel(notPercentLabel); + // i++ + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, indexLocal); + il.Emit(OpCodes.Br, loopStart); + + il.MarkLabel(returnTrue); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + + il.MarkLabel(returnFalse); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + } + + private void EmitFormatConsoleArgs(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "FormatConsoleArgs", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.ObjectArray] + ); + runtime.FormatConsoleArgs = method; + + var il = method.GetILGenerator(); + + // Get format string: string format = (string)args[0] + var formatLocal = il.DeclareLocal(_types.String); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Stloc, formatLocal); + + // StringBuilder result = new StringBuilder() + var resultLocal = il.DeclareLocal(_types.StringBuilder); + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, _types.EmptyTypes)); + il.Emit(OpCodes.Stloc, resultLocal); + + // int currentArg = 1 + var argIndexLocal = il.DeclareLocal(_types.Int32); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Stloc, argIndexLocal); + + // int i = 0 + var iLocal = il.DeclareLocal(_types.Int32); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, iLocal); + + var loopStart = il.DefineLabel(); + var loopEnd = il.DefineLabel(); + var appendRemaining = il.DefineLabel(); + + // while (i < format.Length) + il.MarkLabel(loopStart); + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldloc, formatLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Length")); + il.Emit(OpCodes.Bge, appendRemaining); + + // char c = format[i] + var charLocal = il.DeclareLocal(_types.Char); + il.Emit(OpCodes.Ldloc, formatLocal); + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Chars", _types.Int32)); + il.Emit(OpCodes.Stloc, charLocal); + + // if (c == '%' && i + 1 < format.Length) + var notPercent = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, charLocal); + il.Emit(OpCodes.Ldc_I4, (int)'%'); + il.Emit(OpCodes.Bne_Un, notPercent); + + // Check i + 1 < format.Length + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Ldloc, formatLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Length")); + il.Emit(OpCodes.Bge, notPercent); + + // char specifier = format[i + 1] + var specifierLocal = il.DeclareLocal(_types.Char); + il.Emit(OpCodes.Ldloc, formatLocal); + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Chars", _types.Int32)); + il.Emit(OpCodes.Stloc, specifierLocal); + + // Handle %% -> % + var notDoublePercent = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, specifierLocal); + il.Emit(OpCodes.Ldc_I4, (int)'%'); + il.Emit(OpCodes.Bne_Un, notDoublePercent); + + // Append '%' and skip 2 + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldc_I4, (int)'%'); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.Char)); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldc_I4_2); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, iLocal); + il.Emit(OpCodes.Br, loopStart); + + il.MarkLabel(notDoublePercent); + + // Check if we have args remaining: currentArg < args.Length + var noArgsLeft = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, argIndexLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Bge, noArgsLeft); + + // Handle specifiers s, d, i, f, o, O, j + var handleS = il.DefineLabel(); + var handleD = il.DefineLabel(); + var handleF = il.DefineLabel(); + var handleO = il.DefineLabel(); + var handleJ = il.DefineLabel(); + var unknownSpecifier = il.DefineLabel(); + + // Check 's' + il.Emit(OpCodes.Ldloc, specifierLocal); + il.Emit(OpCodes.Ldc_I4, (int)'s'); + il.Emit(OpCodes.Beq, handleS); + + // Check 'd' or 'i' + il.Emit(OpCodes.Ldloc, specifierLocal); + il.Emit(OpCodes.Ldc_I4, (int)'d'); + il.Emit(OpCodes.Beq, handleD); + + il.Emit(OpCodes.Ldloc, specifierLocal); + il.Emit(OpCodes.Ldc_I4, (int)'i'); + il.Emit(OpCodes.Beq, handleD); + + // Check 'f' + il.Emit(OpCodes.Ldloc, specifierLocal); + il.Emit(OpCodes.Ldc_I4, (int)'f'); + il.Emit(OpCodes.Beq, handleF); + + // Check 'o' or 'O' + il.Emit(OpCodes.Ldloc, specifierLocal); + il.Emit(OpCodes.Ldc_I4, (int)'o'); + il.Emit(OpCodes.Beq, handleO); + + il.Emit(OpCodes.Ldloc, specifierLocal); + il.Emit(OpCodes.Ldc_I4, (int)'O'); + il.Emit(OpCodes.Beq, handleO); + + // Check 'j' + il.Emit(OpCodes.Ldloc, specifierLocal); + il.Emit(OpCodes.Ldc_I4, (int)'j'); + il.Emit(OpCodes.Beq, handleJ); + + il.Emit(OpCodes.Br, unknownSpecifier); + + // Handle %s - string + il.MarkLabel(handleS); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, argIndexLocal); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + var afterS = il.DefineLabel(); + il.Emit(OpCodes.Br, afterS); + + // Handle %d/%i - integer + il.MarkLabel(handleD); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, argIndexLocal); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.FormatAsInteger); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Br, afterS); + + // Handle %f - float + il.MarkLabel(handleF); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, argIndexLocal); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.FormatAsFloat); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Br, afterS); + + // Handle %o/%O - object (same as Stringify) + il.MarkLabel(handleO); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, argIndexLocal); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Br, afterS); + + // Handle %j - JSON + il.MarkLabel(handleJ); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, argIndexLocal); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.FormatAsJson); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Br, afterS); + + il.MarkLabel(afterS); + // currentArg++, i += 2 + il.Emit(OpCodes.Ldloc, argIndexLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, argIndexLocal); + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldc_I4_2); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, iLocal); + il.Emit(OpCodes.Br, loopStart); + + // Unknown specifier or no args left - append char literally + il.MarkLabel(noArgsLeft); + il.MarkLabel(unknownSpecifier); + il.MarkLabel(notPercent); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldloc, charLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.Char)); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldloc, iLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, iLocal); + il.Emit(OpCodes.Br, loopStart); + + // Append remaining args + il.MarkLabel(appendRemaining); + var remainingLoop = il.DefineLabel(); + var remainingEnd = il.DefineLabel(); + + il.MarkLabel(remainingLoop); + il.Emit(OpCodes.Ldloc, argIndexLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Bge, remainingEnd); + + // Append " " + Stringify(args[currentArg]) + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldstr, " "); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, argIndexLocal); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + // currentArg++ + il.Emit(OpCodes.Ldloc, argIndexLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, argIndexLocal); + il.Emit(OpCodes.Br, remainingLoop); + + il.MarkLabel(remainingEnd); + + // Return result.ToString() + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); + il.Emit(OpCodes.Ret); + } + + private void EmitFormatAsInteger(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "FormatAsInteger", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.Object] + ); + runtime.FormatAsInteger = method; + + var il = method.GetILGenerator(); + var nullLabel = il.DefineLabel(); + var undefinedLabel = il.DefineLabel(); + var doubleLabel = il.DefineLabel(); + var boolLabel = il.DefineLabel(); + var stringLabel = il.DefineLabel(); + var nanLabel = il.DefineLabel(); + + // if (value == null) return "NaN" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brfalse, nanLabel); + + // if (value is SharpTSUndefined) return "NaN" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, nanLabel); + + // if (value is double) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Double); + il.Emit(OpCodes.Brtrue, doubleLabel); + + // if (value is bool) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Boolean); + il.Emit(OpCodes.Brtrue, boolLabel); + + // if (value is string) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, stringLabel); + + // default: "NaN" + il.Emit(OpCodes.Br, nanLabel); + + // double case + il.MarkLabel(doubleLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, _types.Double); + var dLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Stloc, dLocal); + // Check NaN + il.Emit(OpCodes.Ldloc, dLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsNaN", _types.Double)); + il.Emit(OpCodes.Brtrue, nanLabel); + // Check Infinity + il.Emit(OpCodes.Ldloc, dLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsInfinity", _types.Double)); + il.Emit(OpCodes.Brtrue, nanLabel); + // Return ((long)d).ToString() + il.Emit(OpCodes.Ldloc, dLocal); + il.Emit(OpCodes.Conv_I8); + var longLocal = il.DeclareLocal(_types.Int64); + il.Emit(OpCodes.Stloc, longLocal); + il.Emit(OpCodes.Ldloca, longLocal); + il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Int64, "ToString")); + il.Emit(OpCodes.Ret); + + // bool case + il.MarkLabel(boolLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, _types.Boolean); + var boolTrueLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, boolTrueLabel); + il.Emit(OpCodes.Ldstr, "0"); + il.Emit(OpCodes.Ret); + il.MarkLabel(boolTrueLabel); + il.Emit(OpCodes.Ldstr, "1"); + il.Emit(OpCodes.Ret); + + // string case - try parse + il.MarkLabel(stringLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.String); + var parsedLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Ldloca, parsedLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "TryParse", _types.String, _types.Double.MakeByRefType())); + var parseFailedLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, parseFailedLabel); + // Parse succeeded - check NaN/Infinity + il.Emit(OpCodes.Ldloc, parsedLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsNaN", _types.Double)); + il.Emit(OpCodes.Brtrue, nanLabel); + il.Emit(OpCodes.Ldloc, parsedLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsInfinity", _types.Double)); + il.Emit(OpCodes.Brtrue, nanLabel); + // Return ((long)parsed).ToString() + il.Emit(OpCodes.Ldloc, parsedLocal); + il.Emit(OpCodes.Conv_I8); + il.Emit(OpCodes.Stloc, longLocal); + il.Emit(OpCodes.Ldloca, longLocal); + il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Int64, "ToString")); + il.Emit(OpCodes.Ret); + + il.MarkLabel(parseFailedLabel); + il.MarkLabel(nanLabel); + il.Emit(OpCodes.Ldstr, "NaN"); + il.Emit(OpCodes.Ret); + } + + private void EmitFormatAsFloat(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "FormatAsFloat", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.Object] + ); + runtime.FormatAsFloat = method; + + var il = method.GetILGenerator(); + var nanLabel = il.DefineLabel(); + var doubleLabel = il.DefineLabel(); + var boolLabel = il.DefineLabel(); + var stringLabel = il.DefineLabel(); + + // if (value == null) return "NaN" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brfalse, nanLabel); + + // if (value is SharpTSUndefined) return "NaN" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, nanLabel); + + // if (value is double) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Double); + il.Emit(OpCodes.Brtrue, doubleLabel); + + // if (value is bool) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Boolean); + il.Emit(OpCodes.Brtrue, boolLabel); + + // if (value is string) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, stringLabel); + + // default: "NaN" + il.Emit(OpCodes.Br, nanLabel); + + // double case + il.MarkLabel(doubleLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, _types.Double); + var dLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Stloc, dLocal); + // Check NaN + il.Emit(OpCodes.Ldloc, dLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsNaN", _types.Double)); + var notNanLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notNanLabel); + il.Emit(OpCodes.Ldstr, "NaN"); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notNanLabel); + // Check Infinity + il.Emit(OpCodes.Ldloc, dLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsPositiveInfinity", _types.Double)); + var notPosInfLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notPosInfLabel); + il.Emit(OpCodes.Ldstr, "Infinity"); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notPosInfLabel); + il.Emit(OpCodes.Ldloc, dLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsNegativeInfinity", _types.Double)); + var notNegInfLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notNegInfLabel); + il.Emit(OpCodes.Ldstr, "-Infinity"); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notNegInfLabel); + // Return d.ToString() + il.Emit(OpCodes.Ldloca, dLocal); + il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Double, "ToString")); + il.Emit(OpCodes.Ret); + + // bool case + il.MarkLabel(boolLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, _types.Boolean); + var boolTrueLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, boolTrueLabel); + il.Emit(OpCodes.Ldstr, "0"); + il.Emit(OpCodes.Ret); + il.MarkLabel(boolTrueLabel); + il.Emit(OpCodes.Ldstr, "1"); + il.Emit(OpCodes.Ret); + + // string case - try parse + il.MarkLabel(stringLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.String); + var parsedLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Ldloca, parsedLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "TryParse", _types.String, _types.Double.MakeByRefType())); + var parseFailedLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, parseFailedLabel); + // Parse succeeded + il.Emit(OpCodes.Ldloc, parsedLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsNaN", _types.Double)); + var parsedNotNan = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, parsedNotNan); + il.Emit(OpCodes.Ldstr, "NaN"); + il.Emit(OpCodes.Ret); + + il.MarkLabel(parsedNotNan); + il.Emit(OpCodes.Ldloc, parsedLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsPositiveInfinity", _types.Double)); + var parsedNotPosInf = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, parsedNotPosInf); + il.Emit(OpCodes.Ldstr, "Infinity"); + il.Emit(OpCodes.Ret); + + il.MarkLabel(parsedNotPosInf); + il.Emit(OpCodes.Ldloc, parsedLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsNegativeInfinity", _types.Double)); + var parsedNotNegInf = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, parsedNotNegInf); + il.Emit(OpCodes.Ldstr, "-Infinity"); + il.Emit(OpCodes.Ret); + + il.MarkLabel(parsedNotNegInf); + il.Emit(OpCodes.Ldloca, parsedLocal); + il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Double, "ToString")); + il.Emit(OpCodes.Ret); + + il.MarkLabel(parseFailedLabel); + il.MarkLabel(nanLabel); + il.Emit(OpCodes.Ldstr, "NaN"); + il.Emit(OpCodes.Ret); + } + + private void EmitFormatAsJson(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "FormatAsJson", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.Object] + ); + runtime.FormatAsJson = method; + + var il = method.GetILGenerator(); + var endLabel = il.DefineLabel(); + + // if (value == null) return "null" + il.Emit(OpCodes.Ldarg_0); + var notNull = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, notNull); + il.Emit(OpCodes.Ldstr, "null"); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notNull); + + // if (value is SharpTSUndefined) return "undefined" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + var notUndefined = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notUndefined); + il.Emit(OpCodes.Ldstr, "undefined"); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notUndefined); + + // if (value is double d) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Double); + var notDouble = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notDouble); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, _types.Double); + var dLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Stloc, dLocal); + // NaN -> "null" + il.Emit(OpCodes.Ldloc, dLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsNaN", _types.Double)); + var dNotNan = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, dNotNan); + il.Emit(OpCodes.Ldstr, "null"); + il.Emit(OpCodes.Ret); + il.MarkLabel(dNotNan); + // Infinity -> "null" + il.Emit(OpCodes.Ldloc, dLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsInfinity", _types.Double)); + var dNotInf = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, dNotInf); + il.Emit(OpCodes.Ldstr, "null"); + il.Emit(OpCodes.Ret); + il.MarkLabel(dNotInf); + il.Emit(OpCodes.Ldloca, dLocal); + il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Double, "ToString")); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notDouble); + + // if (value is bool b) return b ? "true" : "false" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Boolean); + var notBool = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notBool); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, _types.Boolean); + var bTrue = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, bTrue); + il.Emit(OpCodes.Ldstr, "false"); + il.Emit(OpCodes.Ret); + il.MarkLabel(bTrue); + il.Emit(OpCodes.Ldstr, "true"); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notBool); + + // if (value is string s) return "\"" + escaped + "\"" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.String); + var notString = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notString); + // Escape backslash and quote: s.Replace("\\", "\\\\").Replace("\"", "\\\"") + il.Emit(OpCodes.Ldstr, "\""); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Ldstr, "\\"); + il.Emit(OpCodes.Ldstr, "\\\\"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "Replace", _types.String, _types.String)); + il.Emit(OpCodes.Ldstr, "\""); + il.Emit(OpCodes.Ldstr, "\\\""); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "Replace", _types.String, _types.String)); + il.Emit(OpCodes.Ldstr, "\""); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "Concat", _types.String, _types.String, _types.String)); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notString); + + // if (value is List list) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.ListOfObject); + var notList = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notList); + + // Build "[" + elements.join(",") + "]" + var listSbLocal = il.DeclareLocal(_types.StringBuilder); + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, Type.EmptyTypes)); + il.Emit(OpCodes.Stloc, listSbLocal); + il.Emit(OpCodes.Ldloc, listSbLocal); + il.Emit(OpCodes.Ldstr, "["); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + // Iterate list + var listIdxLocal = il.DeclareLocal(_types.Int32); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, listIdxLocal); + var listLoopStart = il.DefineLabel(); + var listLoopEnd = il.DefineLabel(); + il.Emit(OpCodes.Br, listLoopEnd); + + il.MarkLabel(listLoopStart); + // if (i > 0) sb.Append(",") + il.Emit(OpCodes.Ldloc, listIdxLocal); + var skipCommaLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, skipCommaLabel); + il.Emit(OpCodes.Ldloc, listSbLocal); + il.Emit(OpCodes.Ldstr, ","); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.MarkLabel(skipCommaLabel); + + // sb.Append(FormatAsJson(list[i])) + il.Emit(OpCodes.Ldloc, listSbLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.ListOfObject); + il.Emit(OpCodes.Ldloc, listIdxLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ListOfObject, "get_Item", _types.Int32)); + il.Emit(OpCodes.Call, method); // Recursive call to FormatAsJson + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + // i++ + il.Emit(OpCodes.Ldloc, listIdxLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, listIdxLocal); + + il.MarkLabel(listLoopEnd); + il.Emit(OpCodes.Ldloc, listIdxLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.ListOfObject); + il.Emit(OpCodes.Callvirt, _types.GetPropertyGetter(_types.ListOfObject, "Count")); + il.Emit(OpCodes.Blt, listLoopStart); + + // sb.Append("]") + il.Emit(OpCodes.Ldloc, listSbLocal); + il.Emit(OpCodes.Ldstr, "]"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldloc, listSbLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notList); + + // if (value is Dictionary dict) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + var notDict = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notDict); + + // Build "{" + pairs.join(",") + "}" + var dictSbLocal = il.DeclareLocal(_types.StringBuilder); + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, Type.EmptyTypes)); + il.Emit(OpCodes.Stloc, dictSbLocal); + il.Emit(OpCodes.Ldloc, dictSbLocal); + il.Emit(OpCodes.Ldstr, "{"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + // Get enumerator for key-value pairs + var dictEnumLocal = il.DeclareLocal(_types.DictionaryStringObjectEnumerator); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.DictionaryStringObject, "GetEnumerator")); + il.Emit(OpCodes.Stloc, dictEnumLocal); + + var dictFirstLocal = il.DeclareLocal(_types.Boolean); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Stloc, dictFirstLocal); + + var dictLoopStart = il.DefineLabel(); + var dictLoopEnd = il.DefineLabel(); + il.MarkLabel(dictLoopStart); + il.Emit(OpCodes.Ldloca, dictEnumLocal); + il.Emit(OpCodes.Call, _types.DictionaryStringObjectEnumerator.GetMethod("MoveNext")!); + il.Emit(OpCodes.Brfalse, dictLoopEnd); + + // if (!first) sb.Append(",") + il.Emit(OpCodes.Ldloc, dictFirstLocal); + var skipDictComma = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, skipDictComma); + il.Emit(OpCodes.Ldloc, dictSbLocal); + il.Emit(OpCodes.Ldstr, ","); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.MarkLabel(skipDictComma); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, dictFirstLocal); + + // Get current key-value pair + var kvpLocal = il.DeclareLocal(_types.KeyValuePairStringObject); + il.Emit(OpCodes.Ldloca, dictEnumLocal); + il.Emit(OpCodes.Call, _types.DictionaryStringObjectEnumerator.GetProperty("Current")!.GetGetMethod()!); + il.Emit(OpCodes.Stloc, kvpLocal); + + // sb.Append("\"").Append(key).Append("\":").Append(FormatAsJson(value)) + il.Emit(OpCodes.Ldloc, dictSbLocal); + il.Emit(OpCodes.Ldstr, "\""); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Ldloca, kvpLocal); + il.Emit(OpCodes.Call, _types.KeyValuePairStringObject.GetProperty("Key")!.GetGetMethod()!); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Ldstr, "\":"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + // Append FormatAsJson(value) + il.Emit(OpCodes.Ldloca, kvpLocal); + il.Emit(OpCodes.Call, _types.KeyValuePairStringObject.GetProperty("Value")!.GetGetMethod()!); + il.Emit(OpCodes.Call, method); // Recursive call + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + il.Emit(OpCodes.Br, dictLoopStart); + + il.MarkLabel(dictLoopEnd); + il.Emit(OpCodes.Ldloc, dictSbLocal); + il.Emit(OpCodes.Ldstr, "}"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldloc, dictSbLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notDict); + + // Fallback: use Stringify (already emitted in $Runtime) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Ret); + } } diff --git a/Compilation/RuntimeEmitter.CoreUtilities.cs b/Compilation/RuntimeEmitter.CoreUtilities.cs deleted file mode 100644 index 5409c860..00000000 --- a/Compilation/RuntimeEmitter.CoreUtilities.cs +++ /dev/null @@ -1,4454 +0,0 @@ -using System.Reflection; -using System.Reflection.Emit; -using System.Text; - -namespace SharpTS.Compilation; - -public partial class RuntimeEmitter -{ - private void EmitStringify(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - // Signature was forward-declared by DefineRuntimeClassPhase1 so - // helper types that emit before $Runtime (notably $RegExp's - // Symbol.* protocol methods) can call us. Just emit the body on - // the existing MethodBuilder. - var method = (MethodBuilder)runtime.Stringify; - - var il = method.GetILGenerator(); - var nullLabel = il.DefineLabel(); - var undefinedLabel = il.DefineLabel(); - var boolLabel = il.DefineLabel(); - var doubleLabel = il.DefineLabel(); - var listLabel = il.DefineLabel(); - var endLabel = il.DefineLabel(); - - // number[] unboxing: materialize a numeric-mode $Array before the `is List` branch reads it. - EmitDeoptArgIfNumericArray(il, runtime, 0); - - // if (value == null) return "null" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Brfalse, nullLabel); - - // if (value is SharpTSUndefined) return "undefined" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, undefinedLabel); - - // if (value is bool b) return b ? "true" : "false" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Boolean); - il.Emit(OpCodes.Brtrue, boolLabel); - - // if (value is double d) return d.ToString() - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Double); - il.Emit(OpCodes.Brtrue, doubleLabel); - - // if (value is List) return array string - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.ListOfObject); - il.Emit(OpCodes.Brtrue, listLabel); - - // if (value is BigInteger) return value.ToString() + "n" - var bigintLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.BigInteger); - il.Emit(OpCodes.Brtrue, bigintLabel); - - // if (value is Dictionary) return "{ key: value, ... }" - var dictLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, dictLabel); - - // Default: return value.ToString() ?? "null" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); - il.Emit(OpCodes.Dup); - var notNullLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, notNullLabel); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldstr, "null"); - il.MarkLabel(notNullLabel); - il.Emit(OpCodes.Br, endLabel); - - // null case - il.MarkLabel(nullLabel); - il.Emit(OpCodes.Ldstr, "null"); - il.Emit(OpCodes.Br, endLabel); - - // undefined case - il.MarkLabel(undefinedLabel); - il.Emit(OpCodes.Ldstr, "undefined"); - il.Emit(OpCodes.Br, endLabel); - - // bool case - il.MarkLabel(boolLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, _types.Boolean); - var trueLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, trueLabel); - il.Emit(OpCodes.Ldstr, "false"); - il.Emit(OpCodes.Br, endLabel); - il.MarkLabel(trueLabel); - il.Emit(OpCodes.Ldstr, "true"); - il.Emit(OpCodes.Br, endLabel); - - // double case - delegate to $Runtime.FormatNumber (ECMA-262 7.1.12.1 - // Number::toString: shortest round-trip + JS thresholds). Mirrors the - // interpreter's RuntimeTypes.FormatNumber so the two modes agree. - il.MarkLabel(doubleLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, _types.Double); - il.Emit(OpCodes.Call, runtime.FormatNumber); - il.Emit(OpCodes.Br, endLabel); - - // BigInteger case - format as value.ToString() + "n" - il.MarkLabel(bigintLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, _types.BigInteger); - var bigintLocal = il.DeclareLocal(_types.BigInteger); - il.Emit(OpCodes.Stloc, bigintLocal); - il.Emit(OpCodes.Ldloca, bigintLocal); - il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.BigInteger, "ToString")); - il.Emit(OpCodes.Ldstr, "n"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "Concat", _types.String, _types.String)); - il.Emit(OpCodes.Br, endLabel); - - // List case - format as "[elem1, elem2, ...]" - il.MarkLabel(listLabel); - // Use StringBuilder to build the result - var sbLocal = il.DeclareLocal(_types.StringBuilder); - il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, _types.EmptyTypes)); - il.Emit(OpCodes.Stloc, sbLocal); - - // Append "[" - il.Emit(OpCodes.Ldloc, sbLocal); - il.Emit(OpCodes.Ldstr, "["); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - // Loop through list elements - var listLocal = il.DeclareLocal(_types.ListOfObject); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.ListOfObject); - il.Emit(OpCodes.Stloc, listLocal); - - var indexLocal = il.DeclareLocal(_types.Int32); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc, indexLocal); - - var loopStart = il.DefineLabel(); - var loopEnd = il.DefineLabel(); - - il.MarkLabel(loopStart); - // if (index >= list.Count) break - il.Emit(OpCodes.Ldloc, indexLocal); - il.Emit(OpCodes.Ldloc, listLocal); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count").GetGetMethod()!); - il.Emit(OpCodes.Bge, loopEnd); - - // if (index > 0) append ", " - il.Emit(OpCodes.Ldloc, indexLocal); - il.Emit(OpCodes.Ldc_I4_0); - var skipComma = il.DefineLabel(); - il.Emit(OpCodes.Ble, skipComma); - il.Emit(OpCodes.Ldloc, sbLocal); - il.Emit(OpCodes.Ldstr, ", "); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.MarkLabel(skipComma); - - // Append Stringify(list[index]) - il.Emit(OpCodes.Ldloc, sbLocal); - il.Emit(OpCodes.Ldloc, listLocal); - il.Emit(OpCodes.Ldloc, indexLocal); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Item").GetGetMethod()!); - il.Emit(OpCodes.Call, method); // Recursive call to Stringify - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - // index++ - il.Emit(OpCodes.Ldloc, indexLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Stloc, indexLocal); - il.Emit(OpCodes.Br, loopStart); - - il.MarkLabel(loopEnd); - - // Append "]" and return - il.Emit(OpCodes.Ldloc, sbLocal); - il.Emit(OpCodes.Ldstr, "]"); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - il.Emit(OpCodes.Ldloc, sbLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); - il.Emit(OpCodes.Br, endLabel); - - // Dictionary case - format as "{ key1: value1, key2: value2, ... }" - il.MarkLabel(dictLabel); - - // ECMA-262 §7.1.17 ToString of an object goes through ToPrimitive, - // which (hint "string") tries the object's own toString method first. - // If the user installed a callable `toString` on the dictionary - // (`{toString: () => 'foo'}`), invoke it and return the result. - // This is the path test262's coerce-string.js exercises. - var dictHasUserToString = il.DeclareLocal(_types.Object); - var skipUserToStringLabel = il.DefineLabel(); - var castDictLocal = il.DeclareLocal(_types.DictionaryStringObject); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); - il.Emit(OpCodes.Stloc, castDictLocal); - - // Try TryGetValue(d, "toString", out userToString). - var tryGetValueResult = il.DeclareLocal(_types.Boolean); - il.Emit(OpCodes.Ldloc, castDictLocal); - il.Emit(OpCodes.Ldstr, "toString"); - il.Emit(OpCodes.Ldloca, dictHasUserToString); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "TryGetValue", _types.String, _types.Object.MakeByRefType())); - il.Emit(OpCodes.Brfalse, skipUserToStringLabel); - - // If the value is a $TSFunction, invoke it with the dict as `this` - // and (if it returns a string-coercible value) return the result. - il.Emit(OpCodes.Ldloc, dictHasUserToString); - il.Emit(OpCodes.Isinst, runtime.TSFunctionType); - il.Emit(OpCodes.Brfalse, skipUserToStringLabel); - - // result = userToString.InvokeWithThis(dict, []); - var userToStringResult = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldloc, dictHasUserToString); - il.Emit(OpCodes.Castclass, runtime.TSFunctionType); - il.Emit(OpCodes.Ldloc, castDictLocal); - il.Emit(OpCodes.Call, _types.GetMethod(typeof(Array), "Empty").MakeGenericMethod(_types.Object)); - il.Emit(OpCodes.Callvirt, runtime.TSFunctionInvokeWithThis); - il.Emit(OpCodes.Stloc, userToStringResult); - - // Recursively Stringify the result so non-string returns coerce - // properly (e.g. number → "42"). The result is normally a string - // already, so this is a fast path through Stringify's string branch. - il.Emit(OpCodes.Ldloc, userToStringResult); - il.Emit(OpCodes.Call, runtime.Stringify); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(skipUserToStringLabel); - - // Use StringBuilder to build the result - var dictSbLocal = il.DeclareLocal(_types.StringBuilder); - il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, _types.EmptyTypes)); - il.Emit(OpCodes.Stloc, dictSbLocal); - - // Append "{ " - il.Emit(OpCodes.Ldloc, dictSbLocal); - il.Emit(OpCodes.Ldstr, "{ "); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - // Get the dictionary - var dictLocal = il.DeclareLocal(_types.DictionaryStringObject); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); - il.Emit(OpCodes.Stloc, dictLocal); - - // Get enumerator - var enumeratorLocal = il.DeclareLocal(_types.DictionaryStringObjectEnumerator); - il.Emit(OpCodes.Ldloc, dictLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.DictionaryStringObject, "GetEnumerator")); - il.Emit(OpCodes.Stloc, enumeratorLocal); - - // Track if first element - var isFirstLocal = il.DeclareLocal(_types.Boolean); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Stloc, isFirstLocal); - - var dictLoopStart = il.DefineLabel(); - var dictLoopEnd = il.DefineLabel(); - - il.MarkLabel(dictLoopStart); - - // if (!enumerator.MoveNext()) break - il.Emit(OpCodes.Ldloca, enumeratorLocal); - il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.DictionaryStringObjectEnumerator, "MoveNext")); - il.Emit(OpCodes.Brfalse, dictLoopEnd); - - // if (!isFirst) append ", " - var dictSkipComma = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, isFirstLocal); - il.Emit(OpCodes.Brtrue, dictSkipComma); - il.Emit(OpCodes.Ldloc, dictSbLocal); - il.Emit(OpCodes.Ldstr, ", "); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.MarkLabel(dictSkipComma); - - // isFirst = false - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc, isFirstLocal); - - // Get current KeyValuePair - var kvpLocal = il.DeclareLocal(_types.KeyValuePairStringObject); - il.Emit(OpCodes.Ldloca, enumeratorLocal); - il.Emit(OpCodes.Call, _types.GetProperty(_types.DictionaryStringObjectEnumerator, "Current").GetGetMethod()!); - il.Emit(OpCodes.Stloc, kvpLocal); - - // Append key - il.Emit(OpCodes.Ldloc, dictSbLocal); - il.Emit(OpCodes.Ldloca, kvpLocal); - il.Emit(OpCodes.Call, _types.GetProperty(_types.KeyValuePairStringObject, "Key").GetGetMethod()!); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - // Append ": " - il.Emit(OpCodes.Ldloc, dictSbLocal); - il.Emit(OpCodes.Ldstr, ": "); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - // Append Stringify(value) - recursive call to emitted method - il.Emit(OpCodes.Ldloc, dictSbLocal); - il.Emit(OpCodes.Ldloca, kvpLocal); - il.Emit(OpCodes.Call, _types.GetProperty(_types.KeyValuePairStringObject, "Value").GetGetMethod()!); - il.Emit(OpCodes.Call, method); // Recursive call to this Stringify method - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - il.Emit(OpCodes.Br, dictLoopStart); - - il.MarkLabel(dictLoopEnd); - - // Append " }" and return - il.Emit(OpCodes.Ldloc, dictSbLocal); - il.Emit(OpCodes.Ldstr, " }"); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - il.Emit(OpCodes.Ldloc, dictSbLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(endLabel); - il.Emit(OpCodes.Ret); - } - - /// - /// Emits: public static string StringRaw(object template, object[] substitutions). - /// Implements String.raw per ECMA-262 22.1.2.4. Accepts: - /// - /// string[] — the legacy tagged-template-literal calling convention - /// (used by ); used directly as the rawStrings array. - /// any object with a raw property — the spec form - /// (String.raw({raw: [...]}, ...subs)); reads raw via GetProperty, - /// reads its length, iterates indexed members. - /// - /// - private void EmitStringRaw(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - // Second param is `List substitutions` (not object[]) so - // $TSFunction.AdjustArgs's rest-param recognition kicks in for direct - // `String.raw(template, ...subs)` calls — otherwise only the first - // substitution would land in the param. - var method = typeBuilder.DefineMethod( - "StringRaw", - MethodAttributes.Public | MethodAttributes.Static, - _types.String, - [_types.Object, _types.ListOfObject] - ); - runtime.StringRaw = method; - - var il = method.GetILGenerator(); - - // We unify both shapes by extracting `length` and an indexed-access - // closure into locals. For string[]: length = arr.Length, get(i) = arr[i]. - // For object: length = ToLength(template.length OR template.raw.length), - // get(i) = ToString(template.raw[i]). - var lengthLocal = il.DeclareLocal(_types.Int32); - var rawListLocal = il.DeclareLocal(_types.Object); // either string[] or List from raw - var isStringArrayLocal = il.DeclareLocal(_types.Boolean); - - // Detect string[] (legacy tagged-template path). - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.StringArray); - var notStringArrayLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notStringArrayLabel); - // string[] path: length = ((string[])arg0).Length, rawList = arg0 - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.StringArray); - il.Emit(OpCodes.Ldlen); - il.Emit(OpCodes.Conv_I4); - il.Emit(OpCodes.Stloc, lengthLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Stloc, rawListLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Stloc, isStringArrayLocal); - var afterDispatchLabel = il.DefineLabel(); - il.Emit(OpCodes.Br, afterDispatchLabel); - - il.MarkLabel(notStringArrayLabel); - // Object path (spec form): raw = template.raw; length = ToLength(raw.length). - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc, isStringArrayLocal); - - // ECMA-262 22.1.2.4 step 2: ? RequireObjectCoercible(template). null/ - // undefined throws TypeError. Catches String.raw(undefined) / .call(null). - il.Emit(OpCodes.Ldarg_0); - var notNullishLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, notNullishLabel); - var throwTypeErrorLabel = il.DefineLabel(); - il.MarkLabel(throwTypeErrorLabel); - il.Emit(OpCodes.Ldstr, "Cannot convert undefined or null to object"); - il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); - il.Emit(OpCodes.Call, runtime.CreateException); - il.Emit(OpCodes.Throw); - il.MarkLabel(notNullishLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, throwTypeErrorLabel); - - // raw = template.raw via $Runtime.GetProperty - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldstr, "raw"); - il.Emit(OpCodes.Call, runtime.GetProperty); - il.Emit(OpCodes.Stloc, rawListLocal); - - // ECMA-262 22.1.2.4 step 4: ? ToObject(raw). If raw is null/undefined, - // throw TypeError per spec. Required for `String.raw({raw: undefined})` - // and `String.raw({})` (raw absent → undefined). - il.Emit(OpCodes.Ldloc, rawListLocal); - il.Emit(OpCodes.Brfalse, throwTypeErrorLabel); - il.Emit(OpCodes.Ldloc, rawListLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, throwTypeErrorLabel); - - // ToLength(raw.length): use $Runtime.GetProperty(raw, "length") then - // $Runtime.ToNumber → clamp to non-negative int. - il.Emit(OpCodes.Ldloc, rawListLocal); - il.Emit(OpCodes.Ldstr, "length"); - il.Emit(OpCodes.Call, runtime.GetProperty); - il.Emit(OpCodes.Call, runtime.ToNumber); - var lenDouble = il.DeclareLocal(_types.Double); - il.Emit(OpCodes.Stloc, lenDouble); - // NaN / negative / -Infinity → 0 - il.Emit(OpCodes.Ldloc, lenDouble); - il.Emit(OpCodes.Ldloc, lenDouble); - var notNaNLabel = il.DefineLabel(); - il.Emit(OpCodes.Ceq); - il.Emit(OpCodes.Brtrue, notNaNLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc, lengthLocal); - il.Emit(OpCodes.Br, afterDispatchLabel); - il.MarkLabel(notNaNLabel); - il.Emit(OpCodes.Ldloc, lenDouble); - il.Emit(OpCodes.Ldc_R8, 0.0); - var positiveLenLabel = il.DefineLabel(); - il.Emit(OpCodes.Bgt, positiveLenLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc, lengthLocal); - il.Emit(OpCodes.Br, afterDispatchLabel); - il.MarkLabel(positiveLenLabel); - // length = (int)Math.Min(d, 1<<24) — guard against runaway alloc. - il.Emit(OpCodes.Ldloc, lenDouble); - il.Emit(OpCodes.Ldc_R8, (double)(1 << 24)); - il.Emit(OpCodes.Call, _types.Math.GetMethod("Min", [_types.Double, _types.Double])!); - il.Emit(OpCodes.Conv_I4); - il.Emit(OpCodes.Stloc, lengthLocal); - - il.MarkLabel(afterDispatchLabel); - - // ECMA-262 22.1.2.4 step 7: If literalSegments ≤ 0, return the empty string. - il.Emit(OpCodes.Ldloc, lengthLocal); - var hasSegmentsLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, hasSegmentsLabel); - il.Emit(OpCodes.Ldstr, ""); - il.Emit(OpCodes.Ret); - il.MarkLabel(hasSegmentsLabel); - - // var sb = new StringBuilder(); - var sbLocal = il.DeclareLocal(_types.StringBuilder); - il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, Type.EmptyTypes)); - il.Emit(OpCodes.Stloc, sbLocal); - - // for (int i = 0; i < length; i++) { ... } - var iLocal = il.DeclareLocal(_types.Int32); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc, iLocal); - var loopStart = il.DefineLabel(); - var loopCondition = il.DefineLabel(); - il.Emit(OpCodes.Br, loopCondition); - - il.MarkLabel(loopStart); - - // segment = isStringArray ? rawList[i] : ToJsString(GetProperty(rawList, i.ToString())) - var segmentLocal = il.DeclareLocal(_types.String); - il.Emit(OpCodes.Ldloc, isStringArrayLocal); - var segmentObjPathLabel = il.DefineLabel(); - var segmentDoneLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, segmentObjPathLabel); - // string[] path - il.Emit(OpCodes.Ldloc, rawListLocal); - il.Emit(OpCodes.Castclass, _types.StringArray); - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Stloc, segmentLocal); - il.Emit(OpCodes.Br, segmentDoneLabel); - il.MarkLabel(segmentObjPathLabel); - // object path: ToJsString(GetProperty(raw, i.ToString())) - il.Emit(OpCodes.Ldloc, rawListLocal); - il.Emit(OpCodes.Ldloca, iLocal); - il.Emit(OpCodes.Call, _types.Int32.GetMethod("ToString", Type.EmptyTypes)!); - il.Emit(OpCodes.Call, runtime.GetProperty); - il.Emit(OpCodes.Call, runtime.ToJsString); - il.Emit(OpCodes.Stloc, segmentLocal); - il.MarkLabel(segmentDoneLabel); - - // sb.Append(segment) - il.Emit(OpCodes.Ldloc, sbLocal); - il.Emit(OpCodes.Ldloc, segmentLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - // if (i + 1 < length) append substitution - var skipSubLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Ldloc, lengthLocal); - il.Emit(OpCodes.Bge, skipSubLabel); - // if i < substitutions.Count, append ToJsString(substitutions[i]) - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count").GetGetMethod()!); - il.Emit(OpCodes.Bge, skipSubLabel); - var subStrLocal = il.DeclareLocal(_types.String); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Item").GetGetMethod()!); - il.Emit(OpCodes.Call, runtime.ToJsString); - il.Emit(OpCodes.Stloc, subStrLocal); - il.Emit(OpCodes.Ldloc, sbLocal); - il.Emit(OpCodes.Ldloc, subStrLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.MarkLabel(skipSubLabel); - - // i++ - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Stloc, iLocal); - - il.MarkLabel(loopCondition); - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldloc, lengthLocal); - il.Emit(OpCodes.Blt, loopStart); - - il.Emit(OpCodes.Ldloc, sbLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); - il.Emit(OpCodes.Ret); - } - - /// - /// Emits: public static string GetConsoleIndent() - /// Returns a string of spaces based on _consoleGroupLevel (2 spaces per level). - /// - private void EmitGetConsoleIndent(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "GetConsoleIndent", - MethodAttributes.Public | MethodAttributes.Static, - _types.String, - Type.EmptyTypes - ); - runtime.GetConsoleIndent = method; - - var il = method.GetILGenerator(); - - // if (_consoleGroupLevel <= 0) return "" - var hasIndentLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldsfld, runtime.ConsoleGroupLevelField); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Bgt, hasIndentLabel); - il.Emit(OpCodes.Ldstr, ""); - il.Emit(OpCodes.Ret); - - il.MarkLabel(hasIndentLabel); - // return new string(' ', _consoleGroupLevel * 2) - il.Emit(OpCodes.Ldc_I4_S, (sbyte)' '); - il.Emit(OpCodes.Ldsfld, runtime.ConsoleGroupLevelField); - il.Emit(OpCodes.Ldc_I4_2); - il.Emit(OpCodes.Mul); - il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.String, [_types.Char, _types.Int32])); - il.Emit(OpCodes.Ret); - } - - private void EmitConsoleLog(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "ConsoleLog", - MethodAttributes.Public | MethodAttributes.Static, - _types.Void, - [_types.Object] - ); - runtime.ConsoleLog = method; - - var il = method.GetILGenerator(); - var noFormatLabel = il.DefineLabel(); - - // number[] unboxing: materialize a numeric-mode $Array before any array-formatting reads its base list. - EmitDeoptArgIfNumericArray(il, runtime, 0); - - // Check if arg is a string with format specifiers - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brfalse, noFormatLabel); - - // Check HasFormatSpecifiers - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Call, runtime.HasFormatSpecifiers); - il.Emit(OpCodes.Brfalse, noFormatLabel); - - // Has format specifiers - process with FormatSingleArg, then prepend indent - // Console.WriteLine(GetConsoleIndent() + FormatSingleArg(value)) - il.Emit(OpCodes.Call, runtime.GetConsoleIndent); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Call, runtime.FormatSingleArg); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "Concat", _types.String, _types.String)); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Console, "WriteLine", _types.String)); - il.Emit(OpCodes.Ret); - - // No format specifiers - call Stringify then prepend indent - // Console.WriteLine(GetConsoleIndent() + Stringify(value)) - il.MarkLabel(noFormatLabel); - il.Emit(OpCodes.Call, runtime.GetConsoleIndent); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.Stringify); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "Concat", _types.String, _types.String)); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Console, "WriteLine", _types.String)); - il.Emit(OpCodes.Ret); - } - - private void EmitFormatSingleArg(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - // Process format specifiers in a single string (handles %% -> % and unsubstituted specifiers) - var method = typeBuilder.DefineMethod( - "FormatSingleArg", - MethodAttributes.Public | MethodAttributes.Static, - _types.String, - [_types.String] - ); - runtime.FormatSingleArg = method; - - var il = method.GetILGenerator(); - - // StringBuilder result = new StringBuilder() - var resultLocal = il.DeclareLocal(_types.StringBuilder); - il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, _types.EmptyTypes)); - il.Emit(OpCodes.Stloc, resultLocal); - - // int i = 0 - var iLocal = il.DeclareLocal(_types.Int32); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc, iLocal); - - var loopStart = il.DefineLabel(); - var loopEnd = il.DefineLabel(); - - // while (i < format.Length) - il.MarkLabel(loopStart); - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Length")); - il.Emit(OpCodes.Bge, loopEnd); - - // char c = format[i] - var charLocal = il.DeclareLocal(_types.Char); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Chars", _types.Int32)); - il.Emit(OpCodes.Stloc, charLocal); - - // if (c == '%' && i + 1 < format.Length && format[i+1] == '%') - var notPercent = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, charLocal); - il.Emit(OpCodes.Ldc_I4, (int)'%'); - il.Emit(OpCodes.Bne_Un, notPercent); - - // Check i + 1 < format.Length - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Length")); - il.Emit(OpCodes.Bge, notPercent); - - // Check format[i+1] == '%' - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Chars", _types.Int32)); - il.Emit(OpCodes.Ldc_I4, (int)'%'); - il.Emit(OpCodes.Bne_Un, notPercent); - - // Append '%' and skip 2 - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ldc_I4, (int)'%'); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.Char)); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldc_I4_2); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Stloc, iLocal); - il.Emit(OpCodes.Br, loopStart); - - // Regular character - append as-is - il.MarkLabel(notPercent); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ldloc, charLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.Char)); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Stloc, iLocal); - il.Emit(OpCodes.Br, loopStart); - - il.MarkLabel(loopEnd); - - // Return result.ToString() - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); - il.Emit(OpCodes.Ret); - } - - private void EmitConsoleLogMultiple(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "ConsoleLogMultiple", - MethodAttributes.Public | MethodAttributes.Static, - _types.Void, - [_types.ObjectArray] - ); - runtime.ConsoleLogMultiple = method; - - var il = method.GetILGenerator(); - - // Check if first argument is a format string with specifiers - // if (args.Length > 0 && args[0] is string fmt && HasFormatSpecifiers(fmt)) - var noFormatLabel = il.DefineLabel(); - - // Check args.Length > 0 - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldlen); - il.Emit(OpCodes.Conv_I4); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ble, noFormatLabel); - - // Check args[0] is string - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brfalse, noFormatLabel); - - // Check HasFormatSpecifiers(args[0] as string) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Call, runtime.HasFormatSpecifiers); - il.Emit(OpCodes.Brfalse, noFormatLabel); - - // Format string case: call FormatConsoleArgs, prepend indent - // Console.WriteLine(GetConsoleIndent() + FormatConsoleArgs(args)) - il.Emit(OpCodes.Call, runtime.GetConsoleIndent); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.FormatConsoleArgs); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "Concat", _types.String, _types.String)); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Console, "WriteLine", _types.String)); - il.Emit(OpCodes.Ret); - - // No format specifiers: join with spaces using Stringify for JS-compatible output - // Console.WriteLine(GetConsoleIndent() + JoinWithStringify(" ", args)) - il.MarkLabel(noFormatLabel); - il.Emit(OpCodes.Call, runtime.GetConsoleIndent); - il.Emit(OpCodes.Ldstr, " "); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.JoinWithStringify); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "Concat", _types.String, _types.String)); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Console, "WriteLine", _types.String)); - il.Emit(OpCodes.Ret); - } - - /// - /// Emits JoinWithStringify: joins array elements with separator using Stringify for JS-compatible output. - /// Signature: string JoinWithStringify(string separator, object[] args) - /// - private void EmitJoinWithStringify(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "JoinWithStringify", - MethodAttributes.Public | MethodAttributes.Static, - _types.String, - [_types.String, _types.ObjectArray] - ); - runtime.JoinWithStringify = method; - - var il = method.GetILGenerator(); - - // StringBuilder result = new StringBuilder() - var resultLocal = il.DeclareLocal(_types.StringBuilder); - il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, _types.EmptyTypes)); - il.Emit(OpCodes.Stloc, resultLocal); - - // int i = 0 - var indexLocal = il.DeclareLocal(_types.Int32); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc, indexLocal); - - // Loop - var loopStart = il.DefineLabel(); - var loopEnd = il.DefineLabel(); - - il.MarkLabel(loopStart); - // if (i >= args.Length) goto loopEnd - il.Emit(OpCodes.Ldloc, indexLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldlen); - il.Emit(OpCodes.Conv_I4); - il.Emit(OpCodes.Bge, loopEnd); - - // if (i > 0) result.Append(separator) - var skipSeparator = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, indexLocal); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ble, skipSeparator); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ldarg_0); // separator - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.MarkLabel(skipSeparator); - - // result.Append(Stringify(args[i])) - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloc, indexLocal); - il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Call, runtime.Stringify); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - // i++ - il.Emit(OpCodes.Ldloc, indexLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Stloc, indexLocal); - il.Emit(OpCodes.Br, loopStart); - - il.MarkLabel(loopEnd); - - // return result.ToString() - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); - il.Emit(OpCodes.Ret); - } - - private void EmitHasFormatSpecifiers(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "HasFormatSpecifiers", - MethodAttributes.Public | MethodAttributes.Static, - _types.Boolean, - [_types.String] - ); - runtime.HasFormatSpecifiers = method; - - var il = method.GetILGenerator(); - var indexLocal = il.DeclareLocal(_types.Int32); - var loopStart = il.DefineLabel(); - var loopEnd = il.DefineLabel(); - var returnFalse = il.DefineLabel(); - - // int i = 0 - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc, indexLocal); - - // Loop: while (i < str.Length - 1) - il.MarkLabel(loopStart); - il.Emit(OpCodes.Ldloc, indexLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Length")); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Sub); - il.Emit(OpCodes.Bge, returnFalse); - - // if (str[i] == '%') - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloc, indexLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Chars", _types.Int32)); - il.Emit(OpCodes.Ldc_I4, (int)'%'); - var notPercentLabel = il.DefineLabel(); - il.Emit(OpCodes.Bne_Un, notPercentLabel); - - // Check next char is s, d, i, f, o, O, or j - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloc, indexLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Chars", _types.Int32)); - var nextCharLocal = il.DeclareLocal(_types.Char); - il.Emit(OpCodes.Stloc, nextCharLocal); - - // Check for each specifier (including %% escape) - var checkNext = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, nextCharLocal); - il.Emit(OpCodes.Ldc_I4, (int)'%'); - var returnTrue = il.DefineLabel(); - il.Emit(OpCodes.Beq, returnTrue); - - il.Emit(OpCodes.Ldloc, nextCharLocal); - il.Emit(OpCodes.Ldc_I4, (int)'s'); - il.Emit(OpCodes.Beq, returnTrue); - - il.Emit(OpCodes.Ldloc, nextCharLocal); - il.Emit(OpCodes.Ldc_I4, (int)'d'); - il.Emit(OpCodes.Beq, returnTrue); - - il.Emit(OpCodes.Ldloc, nextCharLocal); - il.Emit(OpCodes.Ldc_I4, (int)'i'); - il.Emit(OpCodes.Beq, returnTrue); - - il.Emit(OpCodes.Ldloc, nextCharLocal); - il.Emit(OpCodes.Ldc_I4, (int)'f'); - il.Emit(OpCodes.Beq, returnTrue); - - il.Emit(OpCodes.Ldloc, nextCharLocal); - il.Emit(OpCodes.Ldc_I4, (int)'o'); - il.Emit(OpCodes.Beq, returnTrue); - - il.Emit(OpCodes.Ldloc, nextCharLocal); - il.Emit(OpCodes.Ldc_I4, (int)'O'); - il.Emit(OpCodes.Beq, returnTrue); - - il.Emit(OpCodes.Ldloc, nextCharLocal); - il.Emit(OpCodes.Ldc_I4, (int)'j'); - il.Emit(OpCodes.Beq, returnTrue); - - il.MarkLabel(notPercentLabel); - // i++ - il.Emit(OpCodes.Ldloc, indexLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Stloc, indexLocal); - il.Emit(OpCodes.Br, loopStart); - - il.MarkLabel(returnTrue); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ret); - - il.MarkLabel(returnFalse); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ret); - } - - private void EmitFormatConsoleArgs(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "FormatConsoleArgs", - MethodAttributes.Public | MethodAttributes.Static, - _types.String, - [_types.ObjectArray] - ); - runtime.FormatConsoleArgs = method; - - var il = method.GetILGenerator(); - - // Get format string: string format = (string)args[0] - var formatLocal = il.DeclareLocal(_types.String); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Stloc, formatLocal); - - // StringBuilder result = new StringBuilder() - var resultLocal = il.DeclareLocal(_types.StringBuilder); - il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, _types.EmptyTypes)); - il.Emit(OpCodes.Stloc, resultLocal); - - // int currentArg = 1 - var argIndexLocal = il.DeclareLocal(_types.Int32); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Stloc, argIndexLocal); - - // int i = 0 - var iLocal = il.DeclareLocal(_types.Int32); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc, iLocal); - - var loopStart = il.DefineLabel(); - var loopEnd = il.DefineLabel(); - var appendRemaining = il.DefineLabel(); - - // while (i < format.Length) - il.MarkLabel(loopStart); - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldloc, formatLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Length")); - il.Emit(OpCodes.Bge, appendRemaining); - - // char c = format[i] - var charLocal = il.DeclareLocal(_types.Char); - il.Emit(OpCodes.Ldloc, formatLocal); - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Chars", _types.Int32)); - il.Emit(OpCodes.Stloc, charLocal); - - // if (c == '%' && i + 1 < format.Length) - var notPercent = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, charLocal); - il.Emit(OpCodes.Ldc_I4, (int)'%'); - il.Emit(OpCodes.Bne_Un, notPercent); - - // Check i + 1 < format.Length - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Ldloc, formatLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Length")); - il.Emit(OpCodes.Bge, notPercent); - - // char specifier = format[i + 1] - var specifierLocal = il.DeclareLocal(_types.Char); - il.Emit(OpCodes.Ldloc, formatLocal); - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Chars", _types.Int32)); - il.Emit(OpCodes.Stloc, specifierLocal); - - // Handle %% -> % - var notDoublePercent = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, specifierLocal); - il.Emit(OpCodes.Ldc_I4, (int)'%'); - il.Emit(OpCodes.Bne_Un, notDoublePercent); - - // Append '%' and skip 2 - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ldc_I4, (int)'%'); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.Char)); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldc_I4_2); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Stloc, iLocal); - il.Emit(OpCodes.Br, loopStart); - - il.MarkLabel(notDoublePercent); - - // Check if we have args remaining: currentArg < args.Length - var noArgsLeft = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, argIndexLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldlen); - il.Emit(OpCodes.Conv_I4); - il.Emit(OpCodes.Bge, noArgsLeft); - - // Handle specifiers s, d, i, f, o, O, j - var handleS = il.DefineLabel(); - var handleD = il.DefineLabel(); - var handleF = il.DefineLabel(); - var handleO = il.DefineLabel(); - var handleJ = il.DefineLabel(); - var unknownSpecifier = il.DefineLabel(); - - // Check 's' - il.Emit(OpCodes.Ldloc, specifierLocal); - il.Emit(OpCodes.Ldc_I4, (int)'s'); - il.Emit(OpCodes.Beq, handleS); - - // Check 'd' or 'i' - il.Emit(OpCodes.Ldloc, specifierLocal); - il.Emit(OpCodes.Ldc_I4, (int)'d'); - il.Emit(OpCodes.Beq, handleD); - - il.Emit(OpCodes.Ldloc, specifierLocal); - il.Emit(OpCodes.Ldc_I4, (int)'i'); - il.Emit(OpCodes.Beq, handleD); - - // Check 'f' - il.Emit(OpCodes.Ldloc, specifierLocal); - il.Emit(OpCodes.Ldc_I4, (int)'f'); - il.Emit(OpCodes.Beq, handleF); - - // Check 'o' or 'O' - il.Emit(OpCodes.Ldloc, specifierLocal); - il.Emit(OpCodes.Ldc_I4, (int)'o'); - il.Emit(OpCodes.Beq, handleO); - - il.Emit(OpCodes.Ldloc, specifierLocal); - il.Emit(OpCodes.Ldc_I4, (int)'O'); - il.Emit(OpCodes.Beq, handleO); - - // Check 'j' - il.Emit(OpCodes.Ldloc, specifierLocal); - il.Emit(OpCodes.Ldc_I4, (int)'j'); - il.Emit(OpCodes.Beq, handleJ); - - il.Emit(OpCodes.Br, unknownSpecifier); - - // Handle %s - string - il.MarkLabel(handleS); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloc, argIndexLocal); - il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Call, runtime.Stringify); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - var afterS = il.DefineLabel(); - il.Emit(OpCodes.Br, afterS); - - // Handle %d/%i - integer - il.MarkLabel(handleD); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloc, argIndexLocal); - il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Call, runtime.FormatAsInteger); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Br, afterS); - - // Handle %f - float - il.MarkLabel(handleF); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloc, argIndexLocal); - il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Call, runtime.FormatAsFloat); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Br, afterS); - - // Handle %o/%O - object (same as Stringify) - il.MarkLabel(handleO); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloc, argIndexLocal); - il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Call, runtime.Stringify); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Br, afterS); - - // Handle %j - JSON - il.MarkLabel(handleJ); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloc, argIndexLocal); - il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Call, runtime.FormatAsJson); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Br, afterS); - - il.MarkLabel(afterS); - // currentArg++, i += 2 - il.Emit(OpCodes.Ldloc, argIndexLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Stloc, argIndexLocal); - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldc_I4_2); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Stloc, iLocal); - il.Emit(OpCodes.Br, loopStart); - - // Unknown specifier or no args left - append char literally - il.MarkLabel(noArgsLeft); - il.MarkLabel(unknownSpecifier); - il.MarkLabel(notPercent); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ldloc, charLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.Char)); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldloc, iLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Stloc, iLocal); - il.Emit(OpCodes.Br, loopStart); - - // Append remaining args - il.MarkLabel(appendRemaining); - var remainingLoop = il.DefineLabel(); - var remainingEnd = il.DefineLabel(); - - il.MarkLabel(remainingLoop); - il.Emit(OpCodes.Ldloc, argIndexLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldlen); - il.Emit(OpCodes.Conv_I4); - il.Emit(OpCodes.Bge, remainingEnd); - - // Append " " + Stringify(args[currentArg]) - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ldstr, " "); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloc, argIndexLocal); - il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Call, runtime.Stringify); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - // currentArg++ - il.Emit(OpCodes.Ldloc, argIndexLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Stloc, argIndexLocal); - il.Emit(OpCodes.Br, remainingLoop); - - il.MarkLabel(remainingEnd); - - // Return result.ToString() - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); - il.Emit(OpCodes.Ret); - } - - private void EmitFormatAsInteger(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "FormatAsInteger", - MethodAttributes.Public | MethodAttributes.Static, - _types.String, - [_types.Object] - ); - runtime.FormatAsInteger = method; - - var il = method.GetILGenerator(); - var nullLabel = il.DefineLabel(); - var undefinedLabel = il.DefineLabel(); - var doubleLabel = il.DefineLabel(); - var boolLabel = il.DefineLabel(); - var stringLabel = il.DefineLabel(); - var nanLabel = il.DefineLabel(); - - // if (value == null) return "NaN" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Brfalse, nanLabel); - - // if (value is SharpTSUndefined) return "NaN" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, nanLabel); - - // if (value is double) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Double); - il.Emit(OpCodes.Brtrue, doubleLabel); - - // if (value is bool) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Boolean); - il.Emit(OpCodes.Brtrue, boolLabel); - - // if (value is string) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brtrue, stringLabel); - - // default: "NaN" - il.Emit(OpCodes.Br, nanLabel); - - // double case - il.MarkLabel(doubleLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, _types.Double); - var dLocal = il.DeclareLocal(_types.Double); - il.Emit(OpCodes.Stloc, dLocal); - // Check NaN - il.Emit(OpCodes.Ldloc, dLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsNaN", _types.Double)); - il.Emit(OpCodes.Brtrue, nanLabel); - // Check Infinity - il.Emit(OpCodes.Ldloc, dLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsInfinity", _types.Double)); - il.Emit(OpCodes.Brtrue, nanLabel); - // Return ((long)d).ToString() - il.Emit(OpCodes.Ldloc, dLocal); - il.Emit(OpCodes.Conv_I8); - var longLocal = il.DeclareLocal(_types.Int64); - il.Emit(OpCodes.Stloc, longLocal); - il.Emit(OpCodes.Ldloca, longLocal); - il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Int64, "ToString")); - il.Emit(OpCodes.Ret); - - // bool case - il.MarkLabel(boolLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, _types.Boolean); - var boolTrueLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, boolTrueLabel); - il.Emit(OpCodes.Ldstr, "0"); - il.Emit(OpCodes.Ret); - il.MarkLabel(boolTrueLabel); - il.Emit(OpCodes.Ldstr, "1"); - il.Emit(OpCodes.Ret); - - // string case - try parse - il.MarkLabel(stringLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.String); - var parsedLocal = il.DeclareLocal(_types.Double); - il.Emit(OpCodes.Ldloca, parsedLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "TryParse", _types.String, _types.Double.MakeByRefType())); - var parseFailedLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, parseFailedLabel); - // Parse succeeded - check NaN/Infinity - il.Emit(OpCodes.Ldloc, parsedLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsNaN", _types.Double)); - il.Emit(OpCodes.Brtrue, nanLabel); - il.Emit(OpCodes.Ldloc, parsedLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsInfinity", _types.Double)); - il.Emit(OpCodes.Brtrue, nanLabel); - // Return ((long)parsed).ToString() - il.Emit(OpCodes.Ldloc, parsedLocal); - il.Emit(OpCodes.Conv_I8); - il.Emit(OpCodes.Stloc, longLocal); - il.Emit(OpCodes.Ldloca, longLocal); - il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Int64, "ToString")); - il.Emit(OpCodes.Ret); - - il.MarkLabel(parseFailedLabel); - il.MarkLabel(nanLabel); - il.Emit(OpCodes.Ldstr, "NaN"); - il.Emit(OpCodes.Ret); - } - - private void EmitFormatAsFloat(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "FormatAsFloat", - MethodAttributes.Public | MethodAttributes.Static, - _types.String, - [_types.Object] - ); - runtime.FormatAsFloat = method; - - var il = method.GetILGenerator(); - var nanLabel = il.DefineLabel(); - var doubleLabel = il.DefineLabel(); - var boolLabel = il.DefineLabel(); - var stringLabel = il.DefineLabel(); - - // if (value == null) return "NaN" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Brfalse, nanLabel); - - // if (value is SharpTSUndefined) return "NaN" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, nanLabel); - - // if (value is double) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Double); - il.Emit(OpCodes.Brtrue, doubleLabel); - - // if (value is bool) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Boolean); - il.Emit(OpCodes.Brtrue, boolLabel); - - // if (value is string) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brtrue, stringLabel); - - // default: "NaN" - il.Emit(OpCodes.Br, nanLabel); - - // double case - il.MarkLabel(doubleLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, _types.Double); - var dLocal = il.DeclareLocal(_types.Double); - il.Emit(OpCodes.Stloc, dLocal); - // Check NaN - il.Emit(OpCodes.Ldloc, dLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsNaN", _types.Double)); - var notNanLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notNanLabel); - il.Emit(OpCodes.Ldstr, "NaN"); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notNanLabel); - // Check Infinity - il.Emit(OpCodes.Ldloc, dLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsPositiveInfinity", _types.Double)); - var notPosInfLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notPosInfLabel); - il.Emit(OpCodes.Ldstr, "Infinity"); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notPosInfLabel); - il.Emit(OpCodes.Ldloc, dLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsNegativeInfinity", _types.Double)); - var notNegInfLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notNegInfLabel); - il.Emit(OpCodes.Ldstr, "-Infinity"); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notNegInfLabel); - // Return d.ToString() - il.Emit(OpCodes.Ldloca, dLocal); - il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Double, "ToString")); - il.Emit(OpCodes.Ret); - - // bool case - il.MarkLabel(boolLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, _types.Boolean); - var boolTrueLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, boolTrueLabel); - il.Emit(OpCodes.Ldstr, "0"); - il.Emit(OpCodes.Ret); - il.MarkLabel(boolTrueLabel); - il.Emit(OpCodes.Ldstr, "1"); - il.Emit(OpCodes.Ret); - - // string case - try parse - il.MarkLabel(stringLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.String); - var parsedLocal = il.DeclareLocal(_types.Double); - il.Emit(OpCodes.Ldloca, parsedLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "TryParse", _types.String, _types.Double.MakeByRefType())); - var parseFailedLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, parseFailedLabel); - // Parse succeeded - il.Emit(OpCodes.Ldloc, parsedLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsNaN", _types.Double)); - var parsedNotNan = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, parsedNotNan); - il.Emit(OpCodes.Ldstr, "NaN"); - il.Emit(OpCodes.Ret); - - il.MarkLabel(parsedNotNan); - il.Emit(OpCodes.Ldloc, parsedLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsPositiveInfinity", _types.Double)); - var parsedNotPosInf = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, parsedNotPosInf); - il.Emit(OpCodes.Ldstr, "Infinity"); - il.Emit(OpCodes.Ret); - - il.MarkLabel(parsedNotPosInf); - il.Emit(OpCodes.Ldloc, parsedLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsNegativeInfinity", _types.Double)); - var parsedNotNegInf = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, parsedNotNegInf); - il.Emit(OpCodes.Ldstr, "-Infinity"); - il.Emit(OpCodes.Ret); - - il.MarkLabel(parsedNotNegInf); - il.Emit(OpCodes.Ldloca, parsedLocal); - il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Double, "ToString")); - il.Emit(OpCodes.Ret); - - il.MarkLabel(parseFailedLabel); - il.MarkLabel(nanLabel); - il.Emit(OpCodes.Ldstr, "NaN"); - il.Emit(OpCodes.Ret); - } - - private void EmitFormatAsJson(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "FormatAsJson", - MethodAttributes.Public | MethodAttributes.Static, - _types.String, - [_types.Object] - ); - runtime.FormatAsJson = method; - - var il = method.GetILGenerator(); - var endLabel = il.DefineLabel(); - - // if (value == null) return "null" - il.Emit(OpCodes.Ldarg_0); - var notNull = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, notNull); - il.Emit(OpCodes.Ldstr, "null"); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notNull); - - // if (value is SharpTSUndefined) return "undefined" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - var notUndefined = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notUndefined); - il.Emit(OpCodes.Ldstr, "undefined"); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notUndefined); - - // if (value is double d) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Double); - var notDouble = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notDouble); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, _types.Double); - var dLocal = il.DeclareLocal(_types.Double); - il.Emit(OpCodes.Stloc, dLocal); - // NaN -> "null" - il.Emit(OpCodes.Ldloc, dLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsNaN", _types.Double)); - var dNotNan = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, dNotNan); - il.Emit(OpCodes.Ldstr, "null"); - il.Emit(OpCodes.Ret); - il.MarkLabel(dNotNan); - // Infinity -> "null" - il.Emit(OpCodes.Ldloc, dLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Double, "IsInfinity", _types.Double)); - var dNotInf = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, dNotInf); - il.Emit(OpCodes.Ldstr, "null"); - il.Emit(OpCodes.Ret); - il.MarkLabel(dNotInf); - il.Emit(OpCodes.Ldloca, dLocal); - il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Double, "ToString")); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notDouble); - - // if (value is bool b) return b ? "true" : "false" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Boolean); - var notBool = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notBool); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, _types.Boolean); - var bTrue = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, bTrue); - il.Emit(OpCodes.Ldstr, "false"); - il.Emit(OpCodes.Ret); - il.MarkLabel(bTrue); - il.Emit(OpCodes.Ldstr, "true"); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notBool); - - // if (value is string s) return "\"" + escaped + "\"" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.String); - var notString = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notString); - // Escape backslash and quote: s.Replace("\\", "\\\\").Replace("\"", "\\\"") - il.Emit(OpCodes.Ldstr, "\""); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Ldstr, "\\"); - il.Emit(OpCodes.Ldstr, "\\\\"); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "Replace", _types.String, _types.String)); - il.Emit(OpCodes.Ldstr, "\""); - il.Emit(OpCodes.Ldstr, "\\\""); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "Replace", _types.String, _types.String)); - il.Emit(OpCodes.Ldstr, "\""); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "Concat", _types.String, _types.String, _types.String)); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notString); - - // if (value is List list) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.ListOfObject); - var notList = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notList); - - // Build "[" + elements.join(",") + "]" - var listSbLocal = il.DeclareLocal(_types.StringBuilder); - il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, Type.EmptyTypes)); - il.Emit(OpCodes.Stloc, listSbLocal); - il.Emit(OpCodes.Ldloc, listSbLocal); - il.Emit(OpCodes.Ldstr, "["); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - // Iterate list - var listIdxLocal = il.DeclareLocal(_types.Int32); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc, listIdxLocal); - var listLoopStart = il.DefineLabel(); - var listLoopEnd = il.DefineLabel(); - il.Emit(OpCodes.Br, listLoopEnd); - - il.MarkLabel(listLoopStart); - // if (i > 0) sb.Append(",") - il.Emit(OpCodes.Ldloc, listIdxLocal); - var skipCommaLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, skipCommaLabel); - il.Emit(OpCodes.Ldloc, listSbLocal); - il.Emit(OpCodes.Ldstr, ","); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.MarkLabel(skipCommaLabel); - - // sb.Append(FormatAsJson(list[i])) - il.Emit(OpCodes.Ldloc, listSbLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.ListOfObject); - il.Emit(OpCodes.Ldloc, listIdxLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ListOfObject, "get_Item", _types.Int32)); - il.Emit(OpCodes.Call, method); // Recursive call to FormatAsJson - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - // i++ - il.Emit(OpCodes.Ldloc, listIdxLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Stloc, listIdxLocal); - - il.MarkLabel(listLoopEnd); - il.Emit(OpCodes.Ldloc, listIdxLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.ListOfObject); - il.Emit(OpCodes.Callvirt, _types.GetPropertyGetter(_types.ListOfObject, "Count")); - il.Emit(OpCodes.Blt, listLoopStart); - - // sb.Append("]") - il.Emit(OpCodes.Ldloc, listSbLocal); - il.Emit(OpCodes.Ldstr, "]"); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldloc, listSbLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notList); - - // if (value is Dictionary dict) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - var notDict = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notDict); - - // Build "{" + pairs.join(",") + "}" - var dictSbLocal = il.DeclareLocal(_types.StringBuilder); - il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, Type.EmptyTypes)); - il.Emit(OpCodes.Stloc, dictSbLocal); - il.Emit(OpCodes.Ldloc, dictSbLocal); - il.Emit(OpCodes.Ldstr, "{"); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - // Get enumerator for key-value pairs - var dictEnumLocal = il.DeclareLocal(_types.DictionaryStringObjectEnumerator); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.DictionaryStringObject, "GetEnumerator")); - il.Emit(OpCodes.Stloc, dictEnumLocal); - - var dictFirstLocal = il.DeclareLocal(_types.Boolean); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Stloc, dictFirstLocal); - - var dictLoopStart = il.DefineLabel(); - var dictLoopEnd = il.DefineLabel(); - il.MarkLabel(dictLoopStart); - il.Emit(OpCodes.Ldloca, dictEnumLocal); - il.Emit(OpCodes.Call, _types.DictionaryStringObjectEnumerator.GetMethod("MoveNext")!); - il.Emit(OpCodes.Brfalse, dictLoopEnd); - - // if (!first) sb.Append(",") - il.Emit(OpCodes.Ldloc, dictFirstLocal); - var skipDictComma = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, skipDictComma); - il.Emit(OpCodes.Ldloc, dictSbLocal); - il.Emit(OpCodes.Ldstr, ","); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.MarkLabel(skipDictComma); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc, dictFirstLocal); - - // Get current key-value pair - var kvpLocal = il.DeclareLocal(_types.KeyValuePairStringObject); - il.Emit(OpCodes.Ldloca, dictEnumLocal); - il.Emit(OpCodes.Call, _types.DictionaryStringObjectEnumerator.GetProperty("Current")!.GetGetMethod()!); - il.Emit(OpCodes.Stloc, kvpLocal); - - // sb.Append("\"").Append(key).Append("\":").Append(FormatAsJson(value)) - il.Emit(OpCodes.Ldloc, dictSbLocal); - il.Emit(OpCodes.Ldstr, "\""); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Ldloca, kvpLocal); - il.Emit(OpCodes.Call, _types.KeyValuePairStringObject.GetProperty("Key")!.GetGetMethod()!); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Ldstr, "\":"); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - // Append FormatAsJson(value) - il.Emit(OpCodes.Ldloca, kvpLocal); - il.Emit(OpCodes.Call, _types.KeyValuePairStringObject.GetProperty("Value")!.GetGetMethod()!); - il.Emit(OpCodes.Call, method); // Recursive call - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - - il.Emit(OpCodes.Br, dictLoopStart); - - il.MarkLabel(dictLoopEnd); - il.Emit(OpCodes.Ldloc, dictSbLocal); - il.Emit(OpCodes.Ldstr, "}"); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldloc, dictSbLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notDict); - - // Fallback: use Stringify (already emitted in $Runtime) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.Stringify); - il.Emit(OpCodes.Ret); - } - - // Emits an early-return guard for the language-level ToString coercion paths - // (String(), template-literal interpolation, `+` concat): a bigint coerces to - // its bare decimal form ("42"), NOT the "42n" debug form that console.log / - // util.inspect (Stringify) uses. Mirrors the interpreter's Interpreter.Stringify - // / SharpTSStringNamespace bigint handling so the two modes agree. - private void EmitBigIntToStringReturn(ILGenerator il) - { - var notBigInt = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.BigInteger); - il.Emit(OpCodes.Brfalse, notBigInt); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, _types.BigInteger); - var loc = il.DeclareLocal(_types.BigInteger); - il.Emit(OpCodes.Stloc, loc); - il.Emit(OpCodes.Ldloca, loc); - il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.BigInteger, "ToString")); - il.Emit(OpCodes.Ret); - il.MarkLabel(notBigInt); - } - - // StringFromValue — ECMA-262 §22.1.1.1 String(value) constructor called as a - // function. Identical to ToJsString except that Symbol arguments return - // SymbolDescriptiveString instead of throwing: the String() call form is the - // single coercion site the spec exempts from ToString's Symbol TypeError. - private void EmitStringFromValue(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "StringFromValue", - MethodAttributes.Public | MethodAttributes.Static, - _types.String, - [_types.Object]); - runtime.StringFromValueMethod = method; - - var il = method.GetILGenerator(); - - // if (value is $TSSymbol) return value.ToString(); // "Symbol(desc)" - var notSymbolLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSSymbolType); - il.Emit(OpCodes.Brfalse, notSymbolLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); - il.Emit(OpCodes.Ret); - il.MarkLabel(notSymbolLabel); - - // return ToJsString(value); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.ToJsString); - il.Emit(OpCodes.Ret); - } - - // StringifyCoerce — Stringify behind the ECMA-262 §7.1.17 Symbol guard. - // Implicit ToString coercion sites (template-literal interpolation, string - // +/+= concatenation) must throw TypeError for Symbol operands; everything - // else keeps Stringify's display semantics. Signature forward-declared by - // DefineRuntimeClassPhase1 because $Runtime.Add's string-concat arm is - // emitted before this body is filled. - private void EmitStringifyCoerce(EmittedRuntime runtime) - { - var il = runtime.StringifyCoerce.GetILGenerator(); - - // if (value is $TSSymbol) throw TypeError - var notSymbolLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSSymbolType); - il.Emit(OpCodes.Brfalse, notSymbolLabel); - il.Emit(OpCodes.Ldstr, "Cannot convert a Symbol value to a string"); - il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); - il.Emit(OpCodes.Call, runtime.CreateException); - il.Emit(OpCodes.Throw); - il.MarkLabel(notSymbolLabel); - - // A bigint coerces to its bare decimal form ("42"), not Stringify's "42n". - EmitBigIntToStringReturn(il); - - // An object-like value ($Object / Dictionary / List / user class instance) - // coerces via ToJsString — the spec ToString → OrdinaryToPrimitive(string) - // protocol, which honors an own toString override and unwraps a boxed - // wrapper to its primitive (#574). Plain Stringify returns "[object Object]" - // for a wrapper and the bare " instance" CLR form for a user class - // instance, both wrong for template literals / string concat. A user class - // instance is identified by the $IHasFields marker — $Error/Map/Set/ - // $TSFunction don't implement it, so they keep Stringify's path (#931). - // Primitive values stay on the cheap Stringify path. - var objectLikeLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brtrue, objectLikeLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, objectLikeLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.ListOfObject); - il.Emit(OpCodes.Brtrue, objectLikeLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.IHasFieldsInterface); - il.Emit(OpCodes.Brtrue, objectLikeLabel); - - // return Stringify(value); (primitives) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.Stringify); - il.Emit(OpCodes.Ret); - - // return ToJsString(value); (object-like) - il.MarkLabel(objectLikeLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.ToJsString); - il.Emit(OpCodes.Ret); - } - - // ToJsString — ECMA-262 ToString protocol. For Dictionary/$Object receivers - // with a user-defined "toString" function, invoke it and use the result. - // Falls back to Stringify for primitives. Used by String.prototype methods - // that take an object argument (search/indexOf/etc.). - private void EmitToJsString(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - // Signature forward-declared by DefineRuntimeClassPhase1 so $RegExp's - // Symbol.* helpers (which emit before $Runtime's body) can bind to - // it. Just fill the body on the existing MethodBuilder. - var method = (MethodBuilder)runtime.ToJsString; - var il = method.GetILGenerator(); - var fallbackLabel = il.DefineLabel(); - - // number[] unboxing: the `is List` array branch below joins the base - // list directly, so a numeric-mode $Array (empty base list) must materialize first. - EmitDeoptArgIfNumericArray(il, runtime, 0); - - // null / undefined → handled by Stringify ("null" / "undefined") - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Brfalse, fallbackLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, fallbackLabel); - - // ECMA-262 7.1.17 ToString — Symbol primitives throw TypeError. - var notSymbolLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSSymbolType); - il.Emit(OpCodes.Brfalse, notSymbolLabel); - il.Emit(OpCodes.Ldstr, "Cannot convert a Symbol value to a string"); - il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); - il.Emit(OpCodes.Call, runtime.CreateException); - il.Emit(OpCodes.Throw); - il.MarkLabel(notSymbolLabel); - - // A bigint coerces to its bare decimal form ("42"), not Stringify's "42n". - EmitBigIntToStringReturn(il); - - // Already a string → return as-is (avoid CLR ToString round-trip). - var alreadyStringLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brtrue, alreadyStringLabel); - - // ECMA-262 22.3.7 Arguments.prototype.toString inherits from - // Object.prototype.toString → "[object Arguments]". Without this - // check, $Arguments (which extends List) hits the List - // branch and gets comma-joined. Real-world code rarely relies on - // this brand string, but Test262's `String.prototype.trim.call(arguments)` - // test asserts on it. - var notArgumentsBrandLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.ArgumentsType); - il.Emit(OpCodes.Brfalse, notArgumentsBrandLabel); - il.Emit(OpCodes.Ldstr, "[object Arguments]"); - il.Emit(OpCodes.Ret); - il.MarkLabel(notArgumentsBrandLabel); - - // List → ECMA-262 Array.prototype.toString returns join(","), - // not Stringify's debug-style "[a, b]". Build the comma-joined form - // inline so `String([1,2,3]) === "1,2,3"`. Recursively Stringify each - // element (matches join's per-element ToString conversion). - var notListLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.ListOfObject); - il.Emit(OpCodes.Brfalse, notListLabel); - var joinedListLocal = il.DeclareLocal(_types.ListOfObject); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.ListOfObject); - il.Emit(OpCodes.Stloc, joinedListLocal); - var sbJoinLocal = il.DeclareLocal(_types.StringBuilder); - il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, _types.EmptyTypes)); - il.Emit(OpCodes.Stloc, sbJoinLocal); - var idxJoinLocal = il.DeclareLocal(_types.Int32); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc, idxJoinLocal); - var joinLoop = il.DefineLabel(); - var joinEnd = il.DefineLabel(); - il.MarkLabel(joinLoop); - il.Emit(OpCodes.Ldloc, idxJoinLocal); - il.Emit(OpCodes.Ldloc, joinedListLocal); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count").GetGetMethod()!); - il.Emit(OpCodes.Bge, joinEnd); - // Append "," for index > 0 - var skipJoinComma = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, idxJoinLocal); - il.Emit(OpCodes.Brfalse, skipJoinComma); - il.Emit(OpCodes.Ldloc, sbJoinLocal); - il.Emit(OpCodes.Ldstr, ","); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.MarkLabel(skipJoinComma); - // val = list[index]; null/undefined → empty per spec join behavior; else recursive Stringify. - var valLocalJ = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldloc, joinedListLocal); - il.Emit(OpCodes.Ldloc, idxJoinLocal); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Item").GetGetMethod()!); - il.Emit(OpCodes.Stloc, valLocalJ); - var skipAppend = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, valLocalJ); - il.Emit(OpCodes.Brfalse, skipAppend); - il.Emit(OpCodes.Ldloc, valLocalJ); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, skipAppend); - il.Emit(OpCodes.Ldloc, sbJoinLocal); - il.Emit(OpCodes.Ldloc, valLocalJ); - il.Emit(OpCodes.Call, runtime.ToJsString); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.MarkLabel(skipAppend); - il.Emit(OpCodes.Ldloc, idxJoinLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Stloc, idxJoinLocal); - il.Emit(OpCodes.Br, joinLoop); - il.MarkLabel(joinEnd); - il.Emit(OpCodes.Ldloc, sbJoinLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); - il.Emit(OpCodes.Ret); - il.MarkLabel(notListLabel); - - // KeyValuePair → treat as a 2-element [key, value] tuple for - // string coercion (ECMA-262 Array.prototype.toString ≡ join(",")). Map-spread - // tuples land here in compiled mode because IterateToList falls through to the - // IEnumerable path, yielding boxed KVP structs instead of List pairs. - var notKvpLabel = il.DefineLabel(); - var kvpType = _types.MakeGenericType(_types.KeyValuePairOpen, _types.Object, _types.Object); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, kvpType); - il.Emit(OpCodes.Brfalse, notKvpLabel); - var kvpLocal = il.DeclareLocal(kvpType); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, kvpType); - il.Emit(OpCodes.Stloc, kvpLocal); - var sbKvpLocal = il.DeclareLocal(_types.StringBuilder); - il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.StringBuilder, _types.EmptyTypes)); - il.Emit(OpCodes.Stloc, sbKvpLocal); - // Key (index 0): skip if null/undefined, else recursive ToJsString - var keyLocal = il.DeclareLocal(_types.Object); - var skipKeyAppend = il.DefineLabel(); - il.Emit(OpCodes.Ldloca, kvpLocal); - il.Emit(OpCodes.Call, _types.GetProperty(kvpType, "Key").GetGetMethod()!); - il.Emit(OpCodes.Stloc, keyLocal); - il.Emit(OpCodes.Ldloc, keyLocal); - il.Emit(OpCodes.Brfalse, skipKeyAppend); - il.Emit(OpCodes.Ldloc, keyLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, skipKeyAppend); - il.Emit(OpCodes.Ldloc, sbKvpLocal); - il.Emit(OpCodes.Ldloc, keyLocal); - il.Emit(OpCodes.Call, runtime.ToJsString); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.MarkLabel(skipKeyAppend); - il.Emit(OpCodes.Ldloc, sbKvpLocal); - il.Emit(OpCodes.Ldstr, ","); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - // Value (index 1): skip if null/undefined, else recursive ToJsString - var valueLocal = il.DeclareLocal(_types.Object); - var skipValueAppend = il.DefineLabel(); - il.Emit(OpCodes.Ldloca, kvpLocal); - il.Emit(OpCodes.Call, _types.GetProperty(kvpType, "Value").GetGetMethod()!); - il.Emit(OpCodes.Stloc, valueLocal); - il.Emit(OpCodes.Ldloc, valueLocal); - il.Emit(OpCodes.Brfalse, skipValueAppend); - il.Emit(OpCodes.Ldloc, valueLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, skipValueAppend); - il.Emit(OpCodes.Ldloc, sbKvpLocal); - il.Emit(OpCodes.Ldloc, valueLocal); - il.Emit(OpCodes.Call, runtime.ToJsString); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); - il.Emit(OpCodes.Pop); - il.MarkLabel(skipValueAppend); - il.Emit(OpCodes.Ldloc, sbKvpLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); - il.Emit(OpCodes.Ret); - il.MarkLabel(notKvpLabel); - - // Only attempt JS-toString invocation for Dictionary, $Object, or user - // class instances ($IHasFields marker — #931). $Error/Map/Set/$TSFunction - // don't implement $IHasFields, so they fall through to Stringify (Error - // keeps its overridden CLR ToString → "TypeError: x"). A user class - // instance resolves toString/valueOf through its generated GetProperty, - // yielding the user override or "[object Object]" when neither exists. - var isObjectLikeLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, isObjectLikeLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brtrue, isObjectLikeLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.IHasFieldsInterface); - il.Emit(OpCodes.Brtrue, isObjectLikeLabel); - il.Emit(OpCodes.Br, fallbackLabel); - - il.MarkLabel(isObjectLikeLabel); - - // Boxed primitive marker fast-path: if the receiver carries - // __primitiveType + __primitiveValue (Stage 4z19 wrappers), Stringify - // the underlying primitive directly. Without this, toString walks the - // prototype chain to the StringPrototypeGenericStub which doesn't read - // the marker — returns receiver-as-string instead of the primitive's - // natural string repr (`new Object(true).valueOf()` gives wrapper, not true). - var primValLocal = il.DeclareLocal(_types.Object); - var notBoxedLabel = il.DefineLabel(); - // #574: an own (instance) toString override must win over the boxed - // __primitiveValue fast-path — ECMA-262 OrdinaryToPrimitive(O, "string") - // calls the own toString first. When the wrapper carries an own toString, - // defer to the OrdinaryToPrimitive section below (which invokes it). An - // inherited prototype toString is NOT own, so un-overridden wrappers still - // take the fast-path. (HasOwnPropertyHelper does not walk the prototype.) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldstr, "toString"); - il.Emit(OpCodes.Call, runtime.HasOwnPropertyHelperMethod); - il.Emit(OpCodes.Brtrue, notBoxedLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldstr, "__primitiveValue"); - il.Emit(OpCodes.Call, runtime.GetProperty); - il.Emit(OpCodes.Stloc, primValLocal); - il.Emit(OpCodes.Ldloc, primValLocal); - il.Emit(OpCodes.Brfalse, notBoxedLabel); - il.Emit(OpCodes.Ldloc, primValLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, notBoxedLabel); - // ECMA-262 §7.1.17 step 2: throw TypeError if the unwrapped primitive - // is a Symbol. The entry-point check at line ~1856 only catches raw - // Symbol values — Object(Symbol("x")) wraps it as $Object with - // __primitiveValue=sym, and the unwrap below bypasses that guard, - // letting Stringify run on a Symbol (returns "Symbol(x)" rather than - // throwing). Required by indexOf/searchstring-tostring-errors et al. - var unwrapNotSymLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, primValLocal); - il.Emit(OpCodes.Isinst, runtime.TSSymbolType); - il.Emit(OpCodes.Brfalse, unwrapNotSymLabel); - il.Emit(OpCodes.Ldstr, "Cannot convert a Symbol value to a string"); - il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); - il.Emit(OpCodes.Call, runtime.CreateException); - il.Emit(OpCodes.Throw); - il.MarkLabel(unwrapNotSymLabel); - // Stringify the primitive — handles bool/double/string identically to - // the top-level fallback path. - il.Emit(OpCodes.Ldloc, primValLocal); - il.Emit(OpCodes.Call, runtime.Stringify); - il.Emit(OpCodes.Ret); - il.MarkLabel(notBoxedLabel); - - // ECMA-262 7.1.1 ToPrimitive(input, "string"): GetMethod(input, @@toPrimitive) - // takes priority over OrdinaryToPrimitive. Look up Symbol.toPrimitive in the - // value's symbol-dict (compiled mode stores symbol-keyed properties separately - // from string-keyed ones). If found and callable, invoke with hint "string". - // Per spec, the result must be primitive — if it's an object, throw TypeError. - var afterToPrimSymLabel = il.DefineLabel(); - var symDictForToPrimLocal = il.DeclareLocal(_types.DictionaryObjectObject); - var toPrimFnLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.GetSymbolDictMethod); - il.Emit(OpCodes.Stloc, symDictForToPrimLocal); - il.Emit(OpCodes.Ldloc, symDictForToPrimLocal); - il.Emit(OpCodes.Brfalse, afterToPrimSymLabel); - il.Emit(OpCodes.Ldloc, symDictForToPrimLocal); - il.Emit(OpCodes.Ldsfld, runtime.SymbolToPrimitive); - il.Emit(OpCodes.Ldloca, toPrimFnLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "TryGetValue")); - il.Emit(OpCodes.Brfalse, afterToPrimSymLabel); - il.Emit(OpCodes.Ldloc, toPrimFnLocal); - il.Emit(OpCodes.Brfalse, afterToPrimSymLabel); - il.Emit(OpCodes.Ldloc, toPrimFnLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, afterToPrimSymLabel); - - // Accessor descriptor: $CompiledPropertyDescriptor with a Getter field. - // Object literals with `get [Symbol.toPrimitive]() {...}` store the descriptor - // here via $Runtime.DefineSymbolAccessor. Invoke the getter to materialize - // the actual @@toPrimitive function. If the descriptor's Getter is null, the - // accessor is set-only — fall through to OrdinaryToPrimitive (treat as if - // @@toPrimitive is undefined). - var notDescriptorLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, toPrimFnLocal); - il.Emit(OpCodes.Isinst, runtime.CompiledPropertyDescriptorType); - il.Emit(OpCodes.Brfalse, notDescriptorLabel); - var descGetterLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldloc, toPrimFnLocal); - il.Emit(OpCodes.Castclass, runtime.CompiledPropertyDescriptorType); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorGetter.GetGetMethod()!); - il.Emit(OpCodes.Stloc, descGetterLocal); - il.Emit(OpCodes.Ldloc, descGetterLocal); - il.Emit(OpCodes.Brfalse, afterToPrimSymLabel); - // result = InvokeMethodValue(receiver, getter, []) - var emptyArgsForGetterStr = il.DeclareLocal(_types.ObjectArray); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Newarr, _types.Object); - il.Emit(OpCodes.Stloc, emptyArgsForGetterStr); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloc, descGetterLocal); - il.Emit(OpCodes.Ldloc, emptyArgsForGetterStr); - il.Emit(OpCodes.Call, runtime.InvokeMethodValue); - il.Emit(OpCodes.Stloc, toPrimFnLocal); - // Re-check that the materialized value is non-null/non-undefined. - il.Emit(OpCodes.Ldloc, toPrimFnLocal); - il.Emit(OpCodes.Brfalse, afterToPrimSymLabel); - il.Emit(OpCodes.Ldloc, toPrimFnLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, afterToPrimSymLabel); - il.MarkLabel(notDescriptorLabel); - - // Build args array ["string"] and invoke. - var hintArgsStrLocal = il.DeclareLocal(_types.ObjectArray); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Newarr, _types.Object); - il.Emit(OpCodes.Stloc, hintArgsStrLocal); - il.Emit(OpCodes.Ldloc, hintArgsStrLocal); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ldstr, "string"); - il.Emit(OpCodes.Stelem_Ref); - var toPrimResultLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloc, toPrimFnLocal); - il.Emit(OpCodes.Ldloc, hintArgsStrLocal); - il.Emit(OpCodes.Call, runtime.InvokeMethodValue); - il.Emit(OpCodes.Stloc, toPrimResultLocal); - - // If primitive (null/undefined/string/number/bool/BigInt) → ToJsString and return. - var resIsPrimitiveLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, toPrimResultLocal); - il.Emit(OpCodes.Brfalse, resIsPrimitiveLabel); - il.Emit(OpCodes.Ldloc, toPrimResultLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, resIsPrimitiveLabel); - il.Emit(OpCodes.Ldloc, toPrimResultLocal); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brtrue, resIsPrimitiveLabel); - il.Emit(OpCodes.Ldloc, toPrimResultLocal); - il.Emit(OpCodes.Isinst, _types.Double); - il.Emit(OpCodes.Brtrue, resIsPrimitiveLabel); - il.Emit(OpCodes.Ldloc, toPrimResultLocal); - il.Emit(OpCodes.Isinst, _types.Boolean); - il.Emit(OpCodes.Brtrue, resIsPrimitiveLabel); - il.Emit(OpCodes.Ldloc, toPrimResultLocal); - il.Emit(OpCodes.Isinst, _types.BigInteger); - il.Emit(OpCodes.Brtrue, resIsPrimitiveLabel); - // Object result → TypeError per ECMA-262 7.1.1 step 1.b.iii. - il.Emit(OpCodes.Ldstr, "Cannot convert object to primitive value"); - il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); - il.Emit(OpCodes.Call, runtime.CreateException); - il.Emit(OpCodes.Throw); - il.MarkLabel(resIsPrimitiveLabel); - il.Emit(OpCodes.Ldloc, toPrimResultLocal); - il.Emit(OpCodes.Call, runtime.ToJsString); - il.Emit(OpCodes.Ret); - il.MarkLabel(afterToPrimSymLabel); - - // emptyArgs = new object[0] - var emptyArgsLocal = il.DeclareLocal(_types.ObjectArray); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Newarr, _types.Object); - il.Emit(OpCodes.Stloc, emptyArgsLocal); - - // Track whether either toString or valueOf was defined+callable but - // returned a non-primitive. ECMA-262 7.1.1.1 OrdinaryToPrimitive - // requires throwing TypeError in this case (both methods produced - // objects). The lenient "[object Object]" fallback is only correct - // when neither method exists. - var sawNonPrimitiveLocal = il.DeclareLocal(_types.Boolean); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc, sawNonPrimitiveLocal); - - // ECMA-262 ToPrimitive(O, "string"): try toString, then valueOf. - void TryInvoke(string name, Label afterLabel) - { - var fnLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldstr, name); - il.Emit(OpCodes.Call, runtime.GetProperty); - il.Emit(OpCodes.Stloc, fnLocal); - il.Emit(OpCodes.Ldloc, fnLocal); - il.Emit(OpCodes.Brfalse, afterLabel); - il.Emit(OpCodes.Ldloc, fnLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, afterLabel); - - // result = $Runtime.InvokeMethodValue(receiver, fn, emptyArgs) - var resultLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloc, fnLocal); - il.Emit(OpCodes.Ldloc, emptyArgsLocal); - il.Emit(OpCodes.Call, runtime.InvokeMethodValue); - il.Emit(OpCodes.Stloc, resultLocal); - - // If primitive (string / number / bool / null / undefined / BigInt), - // ToJsString and return. Per ECMA-262 ToPrimitive, all primitive types - // — including undefined and null — are valid OrdinaryToPrimitive results. - var resultIsString = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Brfalse, resultIsString); // null primitive - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, resultIsString); // undefined primitive - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brtrue, resultIsString); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Isinst, _types.Double); - il.Emit(OpCodes.Brtrue, resultIsString); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Isinst, _types.Boolean); - il.Emit(OpCodes.Brtrue, resultIsString); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Isinst, _types.BigInteger); - il.Emit(OpCodes.Brtrue, resultIsString); - // Not primitive — set the flag and continue to next attempt. - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Stloc, sawNonPrimitiveLocal); - il.Emit(OpCodes.Br, afterLabel); - - il.MarkLabel(resultIsString); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Call, runtime.ToJsString); - il.Emit(OpCodes.Ret); - } - - var afterToString = il.DefineLabel(); - TryInvoke("toString", afterToString); - il.MarkLabel(afterToString); - var afterValueOf = il.DefineLabel(); - TryInvoke("valueOf", afterValueOf); - il.MarkLabel(afterValueOf); - - // If at least one of toString/valueOf was defined+callable but returned - // a non-primitive, ECMA-262 demands TypeError. Otherwise (neither method - // existed), fall back to "[object Object]" — see comment below. - var noThrowLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, sawNonPrimitiveLocal); - il.Emit(OpCodes.Brfalse, noThrowLabel); - il.Emit(OpCodes.Ldstr, "Cannot convert object to primitive value"); - il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); - il.Emit(OpCodes.Call, runtime.CreateException); - il.Emit(OpCodes.Throw); - il.MarkLabel(noThrowLabel); - - // No usable toString/valueOf on this object — fall back to "[object Object]" - // per ECMA-262 19.1.3.6 (Object.prototype.toString returns this for plain objects). - // Lenient: spec strictly throws TypeError when both are unusable, but the - // compiled-mode prototype-chain walk doesn't reliably surface - // Object.prototype's toString for user $TSObject receivers. Throwing - // here regresses charAt/etc. on borrowed prototypes. Tests that depend - // on the throw (`{toString: undefined, valueOf: undefined}`) are a - // smaller bucket than tests that depend on the "[object Object]" - // fallback for plain user objects. - il.Emit(OpCodes.Ldstr, "[object Object]"); - il.Emit(OpCodes.Ret); - - il.MarkLabel(alreadyStringLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Ret); - - il.MarkLabel(fallbackLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.Stringify); - il.Emit(OpCodes.Ret); - } - - /// - /// Pre-declares the $Runtime.ToNumber MethodBuilder slot. Body is filled - /// in by later, after GetProperty/InvokeMethodValue - /// are available so the ToPrimitive(value, "number") chain can call them. - /// - internal void DeclareToNumber(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "ToNumber", - MethodAttributes.Public | MethodAttributes.Static, - _types.Double, - [_types.Object] - ); - runtime.ToNumber = method; - } - - private void EmitToNumber(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = (MethodBuilder)runtime.ToNumber; - - var il = method.GetILGenerator(); - var resultLocal = il.DeclareLocal(_types.Double); - - // ECMA-262 7.1.4 ToNumber on object: ToPrimitive(value, "number") which - // tries valueOf first, then toString. Without this, Math.hypot(obj-with- - // throwing-valueOf) silently returns NaN instead of propagating the - // throw. Apply for Dictionary or $Object only. - var argLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Stloc, argLocal); - - var skipToPrimLabelTop = il.DefineLabel(); - var doToPrimLabelTop = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, doToPrimLabelTop); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brfalse, skipToPrimLabelTop); - il.MarkLabel(doToPrimLabelTop); - - // ECMA-262 7.1.1 ToPrimitive(input, "number"): @@toPrimitive (if defined and - // callable) takes priority over OrdinaryToPrimitive. Look up the symbol-keyed - // method and invoke with hint "number". Result must be primitive or TypeError. - var afterToPrimSymN = il.DefineLabel(); - var doThrowN = il.DefineLabel(); - var symDictForToPrimN = il.DeclareLocal(_types.DictionaryObjectObject); - var toPrimFnLocalN = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Call, runtime.GetSymbolDictMethod); - il.Emit(OpCodes.Stloc, symDictForToPrimN); - il.Emit(OpCodes.Ldloc, symDictForToPrimN); - il.Emit(OpCodes.Brfalse, afterToPrimSymN); - il.Emit(OpCodes.Ldloc, symDictForToPrimN); - il.Emit(OpCodes.Ldsfld, runtime.SymbolToPrimitive); - il.Emit(OpCodes.Ldloca, toPrimFnLocalN); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "TryGetValue")); - il.Emit(OpCodes.Brfalse, afterToPrimSymN); - il.Emit(OpCodes.Ldloc, toPrimFnLocalN); - il.Emit(OpCodes.Brfalse, afterToPrimSymN); - il.Emit(OpCodes.Ldloc, toPrimFnLocalN); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, afterToPrimSymN); - - // Accessor descriptor unwrap (mirrors EmitToJsString — see notes there). - var notDescLabelN = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, toPrimFnLocalN); - il.Emit(OpCodes.Isinst, runtime.CompiledPropertyDescriptorType); - il.Emit(OpCodes.Brfalse, notDescLabelN); - var descGetterLocalN = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldloc, toPrimFnLocalN); - il.Emit(OpCodes.Castclass, runtime.CompiledPropertyDescriptorType); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorGetter.GetGetMethod()!); - il.Emit(OpCodes.Stloc, descGetterLocalN); - il.Emit(OpCodes.Ldloc, descGetterLocalN); - il.Emit(OpCodes.Brfalse, afterToPrimSymN); - var emptyArgsForGetterNum = il.DeclareLocal(_types.ObjectArray); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Newarr, _types.Object); - il.Emit(OpCodes.Stloc, emptyArgsForGetterNum); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Ldloc, descGetterLocalN); - il.Emit(OpCodes.Ldloc, emptyArgsForGetterNum); - il.Emit(OpCodes.Call, runtime.InvokeMethodValue); - il.Emit(OpCodes.Stloc, toPrimFnLocalN); - il.Emit(OpCodes.Ldloc, toPrimFnLocalN); - il.Emit(OpCodes.Brfalse, afterToPrimSymN); - il.Emit(OpCodes.Ldloc, toPrimFnLocalN); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, afterToPrimSymN); - il.MarkLabel(notDescLabelN); - - var hintArgsNumLocal = il.DeclareLocal(_types.ObjectArray); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Newarr, _types.Object); - il.Emit(OpCodes.Stloc, hintArgsNumLocal); - il.Emit(OpCodes.Ldloc, hintArgsNumLocal); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ldstr, "number"); - il.Emit(OpCodes.Stelem_Ref); - var toPrimResultLocalN = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Ldloc, toPrimFnLocalN); - il.Emit(OpCodes.Ldloc, hintArgsNumLocal); - il.Emit(OpCodes.Call, runtime.InvokeMethodValue); - il.Emit(OpCodes.Stloc, toPrimResultLocalN); - // Object result → TypeError per ECMA-262 7.1.1 step 1.b.iii. - il.Emit(OpCodes.Ldloc, toPrimResultLocalN); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, doThrowN); - il.Emit(OpCodes.Ldloc, toPrimResultLocalN); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brtrue, doThrowN); - il.Emit(OpCodes.Ldloc, toPrimResultLocalN); - il.Emit(OpCodes.Stloc, argLocal); - il.Emit(OpCodes.Br, afterToPrimSymN); - il.MarkLabel(doThrowN); - il.Emit(OpCodes.Ldstr, "Cannot convert object to primitive value"); - il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); - il.Emit(OpCodes.Call, runtime.CreateException); - il.Emit(OpCodes.Throw); - il.MarkLabel(afterToPrimSymN); - - var emptyArgsLocalT = il.DeclareLocal(_types.ObjectArray); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Newarr, _types.Object); - il.Emit(OpCodes.Stloc, emptyArgsLocalT); - - void TryToPrim2(string name, Label afterLabel) - { - var fnLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Ldstr, name); - il.Emit(OpCodes.Call, runtime.GetProperty); - il.Emit(OpCodes.Stloc, fnLocal); - il.Emit(OpCodes.Ldloc, fnLocal); - il.Emit(OpCodes.Brfalse, afterLabel); - il.Emit(OpCodes.Ldloc, fnLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, afterLabel); - - var resLoc = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Ldloc, fnLocal); - il.Emit(OpCodes.Ldloc, emptyArgsLocalT); - il.Emit(OpCodes.Call, runtime.InvokeMethodValue); - il.Emit(OpCodes.Stloc, resLoc); - - il.Emit(OpCodes.Ldloc, resLoc); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, afterLabel); - il.Emit(OpCodes.Ldloc, resLoc); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brtrue, afterLabel); - il.Emit(OpCodes.Ldloc, resLoc); - il.Emit(OpCodes.Stloc, argLocal); - } - - var afterValueOfT = il.DefineLabel(); - TryToPrim2("valueOf", afterValueOfT); - il.MarkLabel(afterValueOfT); - - var afterToStringT = il.DefineLabel(); - var stillObjT = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, stillObjT); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brfalse, afterToStringT); - il.MarkLabel(stillObjT); - TryToPrim2("toString", afterToStringT); - il.MarkLabel(afterToStringT); - - // ECMA-262 7.1.1.1: if both methods returned non-primitives, throw TypeError. - var afterTeT = il.DefineLabel(); - var stillObjTeT = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, stillObjTeT); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brfalse, afterTeT); - il.MarkLabel(stillObjTeT); - il.Emit(OpCodes.Ldstr, "Cannot convert object to primitive value"); - il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); - il.Emit(OpCodes.Call, runtime.CreateException); - il.Emit(OpCodes.Throw); - il.MarkLabel(afterTeT); - - il.MarkLabel(skipToPrimLabelTop); - - // ECMA-262 7.1.4 ToNumber on Symbol → throws TypeError. Without this, - // Convert.ToDouble would catch the InvalidCastException → NaN → 0, - // silently masking the spec-required throw (e.g. `(0).toFixed(Symbol())` - // must throw, not silently produce "0"). - var notSymbolLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, runtime.TSSymbolType); - il.Emit(OpCodes.Brfalse, notSymbolLabel); - il.Emit(OpCodes.Ldstr, "Cannot convert a Symbol value to a number"); - il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); - il.Emit(OpCodes.Call, runtime.CreateException); - il.Emit(OpCodes.Throw); - il.MarkLabel(notSymbolLabel); - - // ECMA-262 7.1.4 step 2: BigInt → TypeError. `(0).toFixed(0n)` must - // throw not silently coerce. Convert.ToDouble would otherwise narrow - // BigInteger to its double value (or throw OverflowException → NaN). - var notBigIntLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.BigInteger); - il.Emit(OpCodes.Brfalse, notBigIntLabel); - il.Emit(OpCodes.Ldstr, "Cannot convert a BigInt to a number"); - il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); - il.Emit(OpCodes.Call, runtime.CreateException); - il.Emit(OpCodes.Throw); - il.MarkLabel(notBigIntLabel); - - // ECMA-262 ToNumber: strings with "0x"/"0X" prefix parse as hex. Convert.ToDouble - // throws on those, so special-case before the fallback. Without this, tests that - // set `length: "0x0002"` on array-likes surface as NaN → 0 → empty iteration. - var tryParseInt64 = _types.GetMethod(_types.Int64, "Parse", _types.String, typeof(System.Globalization.NumberStyles), typeof(System.IFormatProvider)); - var notHexLabel = il.DefineLabel(); - var strLocal = il.DeclareLocal(_types.String); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Stloc, strLocal); - il.Emit(OpCodes.Brfalse, notHexLabel); - - // if (strLocal.Length >= 2 && (strLocal[0] == '0') && (strLocal[1] == 'x' || strLocal[1] == 'X')) - il.Emit(OpCodes.Ldloc, strLocal); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length").GetGetMethod()!); - il.Emit(OpCodes.Ldc_I4_2); - il.Emit(OpCodes.Blt, notHexLabel); - - il.Emit(OpCodes.Ldloc, strLocal); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Callvirt, _types.String.GetMethod("get_Chars", [_types.Int32])!); - il.Emit(OpCodes.Ldc_I4, (int)'0'); - il.Emit(OpCodes.Bne_Un, notHexLabel); - - // second char == 'x' or 'X': compare with OR'd check. Use (ch | 0x20) == 'x'. - il.Emit(OpCodes.Ldloc, strLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Callvirt, _types.String.GetMethod("get_Chars", [_types.Int32])!); - il.Emit(OpCodes.Ldc_I4, 0x20); - il.Emit(OpCodes.Or); - il.Emit(OpCodes.Ldc_I4, (int)'x'); - il.Emit(OpCodes.Bne_Un, notHexLabel); - - // Hex-parse: strLocal.Substring(2), try Int64.Parse with HexNumber style. - il.BeginExceptionBlock(); - il.Emit(OpCodes.Ldloc, strLocal); - il.Emit(OpCodes.Ldc_I4_2); - il.Emit(OpCodes.Callvirt, _types.String.GetMethod("Substring", [_types.Int32])!); - il.Emit(OpCodes.Ldc_I4, (int)System.Globalization.NumberStyles.HexNumber); - // CultureInfo.InvariantCulture — property getter, not a static field. Use Call. - il.Emit(OpCodes.Call, typeof(System.Globalization.CultureInfo).GetProperty("InvariantCulture")!.GetGetMethod()!); - il.Emit(OpCodes.Call, tryParseInt64); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Stloc, resultLocal); - il.BeginCatchBlock(_types.Exception); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldc_R8, double.NaN); - il.Emit(OpCodes.Stloc, resultLocal); - il.EndExceptionBlock(); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notHexLabel); - - // ECMA-262: strings with "0b"/"0B" parse as binary literals, - // "0o"/"0O" as octal. Convert.ToDouble doesn't recognize these. - // Pattern: "0[bB][01]+" or "0[oO][0-7]+". - EmitParsePrefixedInt(il, strLocal, resultLocal, 'b', 2); - EmitParsePrefixedInt(il, strLocal, resultLocal, 'o', 8); - - // Handle "Infinity"/"+Infinity"/"-Infinity" strings before Convert.ToDouble - // (which throws FormatException on those — caught below as NaN, but - // ECMA-262 specifies +Infinity/-Infinity numeric values). - var notInfStrLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, strLocal); - il.Emit(OpCodes.Brfalse, notInfStrLabel); - - // Trim and check - var trimmedLocal = il.DeclareLocal(_types.String); - il.Emit(OpCodes.Ldloc, strLocal); - il.Emit(OpCodes.Callvirt, _types.String.GetMethod("Trim", Type.EmptyTypes)!); - il.Emit(OpCodes.Stloc, trimmedLocal); - - // "Infinity" → +Inf - var notPlainInf = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, trimmedLocal); - il.Emit(OpCodes.Ldstr, "Infinity"); - il.Emit(OpCodes.Call, _types.String.GetMethod("op_Equality", [_types.String, _types.String])!); - il.Emit(OpCodes.Brfalse, notPlainInf); - il.Emit(OpCodes.Ldc_R8, double.PositiveInfinity); - il.Emit(OpCodes.Ret); - il.MarkLabel(notPlainInf); - - // "+Infinity" → +Inf - var notPlusInf = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, trimmedLocal); - il.Emit(OpCodes.Ldstr, "+Infinity"); - il.Emit(OpCodes.Call, _types.String.GetMethod("op_Equality", [_types.String, _types.String])!); - il.Emit(OpCodes.Brfalse, notPlusInf); - il.Emit(OpCodes.Ldc_R8, double.PositiveInfinity); - il.Emit(OpCodes.Ret); - il.MarkLabel(notPlusInf); - - // "-Infinity" → -Inf - var notMinusInf = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, trimmedLocal); - il.Emit(OpCodes.Ldstr, "-Infinity"); - il.Emit(OpCodes.Call, _types.String.GetMethod("op_Equality", [_types.String, _types.String])!); - il.Emit(OpCodes.Brfalse, notMinusInf); - il.Emit(OpCodes.Ldc_R8, double.NegativeInfinity); - il.Emit(OpCodes.Ret); - il.MarkLabel(notMinusInf); - - il.MarkLabel(notInfStrLabel); - - // Use Convert.ToDouble with try-catch fallback to NaN - il.BeginExceptionBlock(); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Convert, "ToDouble", _types.Object)); - il.Emit(OpCodes.Stloc, resultLocal); - il.BeginCatchBlock(_types.Exception); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldc_R8, double.NaN); - il.Emit(OpCodes.Stloc, resultLocal); - il.EndExceptionBlock(); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ret); - } - - /// - /// Emits JsToInt32(object) → int that implements ECMA-262 ToInt32 - /// (7.1.6 in the spec). Unlike Convert.ToInt32, NaN / ±Infinity / out-of-range - /// doubles wrap modulo 2^32 instead of throwing, matching JavaScript's - /// bitwise-op and `x | 0` semantics. Required for packages like lodash - /// and debug that rely on hash |= 0 idioms. - /// - /// - /// Emits IL that parses "0[Pp]" prefixed integer-literal strings - /// to a double. If the string at doesn't - /// match the prefix shape, falls through. Used by ToNumber for binary - /// (0b/0B, radix 2) and octal (0o/0O, radix 8) literal support per - /// ECMA-262 7.1.4.1. Hex (0x/0X) has its own inline path since it - /// predates this helper. - /// - private void EmitParsePrefixedInt(ILGenerator il, LocalBuilder strLocal, - LocalBuilder resultLocal, char prefix, int radix) - { - var skipLabel = il.DefineLabel(); - - // strLocal == null? skip. - il.Emit(OpCodes.Ldloc, strLocal); - il.Emit(OpCodes.Brfalse, skipLabel); - - // Length >= 3? - il.Emit(OpCodes.Ldloc, strLocal); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length").GetGetMethod()!); - il.Emit(OpCodes.Ldc_I4_3); - il.Emit(OpCodes.Blt, skipLabel); - - // strLocal[0] == '0'? - il.Emit(OpCodes.Ldloc, strLocal); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Callvirt, _types.String.GetMethod("get_Chars", [_types.Int32])!); - il.Emit(OpCodes.Ldc_I4, (int)'0'); - il.Emit(OpCodes.Bne_Un, skipLabel); - - // (strLocal[1] | 0x20) == prefix? - il.Emit(OpCodes.Ldloc, strLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Callvirt, _types.String.GetMethod("get_Chars", [_types.Int32])!); - il.Emit(OpCodes.Ldc_I4, 0x20); - il.Emit(OpCodes.Or); - il.Emit(OpCodes.Ldc_I4, (int)prefix); - il.Emit(OpCodes.Bne_Un, skipLabel); - - // try { result = (double)Convert.ToInt64(strLocal.Substring(2), radix); } - il.BeginExceptionBlock(); - il.Emit(OpCodes.Ldloc, strLocal); - il.Emit(OpCodes.Ldc_I4_2); - il.Emit(OpCodes.Callvirt, _types.String.GetMethod("Substring", [_types.Int32])!); - il.Emit(OpCodes.Ldc_I4, radix); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Convert, "ToInt64", _types.String, _types.Int32)); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Stloc, resultLocal); - il.BeginCatchBlock(_types.Exception); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldc_R8, double.NaN); - il.Emit(OpCodes.Stloc, resultLocal); - il.EndExceptionBlock(); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ret); - - il.MarkLabel(skipLabel); - } - - private void EmitJsToInt32(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - // Signature forward-declared by DefineRuntimeClassPhase1 so $RegExp - // (which emits before $Runtime's body) can call it; reuse that slot. - var method = (MethodBuilder)runtime.JsToInt32; - - var il = method.GetILGenerator(); - - const double TWO_32 = 4294967296.0; - const double TWO_31 = 2147483648.0; - - // n = ToNumber(arg0) - var nLocal = il.DeclareLocal(_types.Double); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.ToNumber); - il.Emit(OpCodes.Stloc, nLocal); - - // if (!double.IsFinite(n)) return 0 - var finiteLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, nLocal); - il.Emit(OpCodes.Call, typeof(double).GetMethod("IsFinite", [typeof(double)])!); - il.Emit(OpCodes.Brtrue, finiteLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ret); - il.MarkLabel(finiteLabel); - - // truncated = n >= 0 ? Math.Floor(n) : Math.Ceiling(n) - var truncLocal = il.DeclareLocal(_types.Double); - var negLabel = il.DefineLabel(); - var truncDoneLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, nLocal); - il.Emit(OpCodes.Ldc_R8, 0.0); - il.Emit(OpCodes.Blt, negLabel); - il.Emit(OpCodes.Ldloc, nLocal); - il.Emit(OpCodes.Call, typeof(Math).GetMethod("Floor", [typeof(double)])!); - il.Emit(OpCodes.Stloc, truncLocal); - il.Emit(OpCodes.Br, truncDoneLabel); - il.MarkLabel(negLabel); - il.Emit(OpCodes.Ldloc, nLocal); - il.Emit(OpCodes.Call, typeof(Math).GetMethod("Ceiling", [typeof(double)])!); - il.Emit(OpCodes.Stloc, truncLocal); - il.MarkLabel(truncDoneLabel); - - // int32bit = truncated - Math.Floor(truncated / 2^32) * 2^32 (mathematical mod) - var int32bitLocal = il.DeclareLocal(_types.Double); - il.Emit(OpCodes.Ldloc, truncLocal); - il.Emit(OpCodes.Ldloc, truncLocal); - il.Emit(OpCodes.Ldc_R8, TWO_32); - il.Emit(OpCodes.Div); - il.Emit(OpCodes.Call, typeof(Math).GetMethod("Floor", [typeof(double)])!); - il.Emit(OpCodes.Ldc_R8, TWO_32); - il.Emit(OpCodes.Mul); - il.Emit(OpCodes.Sub); - il.Emit(OpCodes.Stloc, int32bitLocal); - - // return int32bit >= 2^31 ? (int)(int32bit - 2^32) : (int)int32bit - var smallLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, int32bitLocal); - il.Emit(OpCodes.Ldc_R8, TWO_31); - il.Emit(OpCodes.Blt, smallLabel); - il.Emit(OpCodes.Ldloc, int32bitLocal); - il.Emit(OpCodes.Ldc_R8, TWO_32); - il.Emit(OpCodes.Sub); - il.Emit(OpCodes.Conv_I4); - il.Emit(OpCodes.Ret); - il.MarkLabel(smallLabel); - il.Emit(OpCodes.Ldloc, int32bitLocal); - il.Emit(OpCodes.Conv_I4); - il.Emit(OpCodes.Ret); - } - - /// - /// Emits $Runtime.JsLessThan(object x, object y) -> bool: - /// ECMA-262 7.2.13 IsLessThan abstract algorithm (LeftFirst=true). - /// If both operands are strings, lexicographic comparison. - /// Otherwise both are coerced via ToNumber and numerically compared - /// (NaN on either side yields false). - /// - private void EmitJsLessThan(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "JsLessThan", - MethodAttributes.Public | MethodAttributes.Static, - _types.Boolean, - [_types.Object, _types.Object]); - runtime.JsLessThan = method; - - var il = method.GetILGenerator(); - - // If both args are strings, do lexicographic comparison (CompareOrdinal < 0). - var notBothStrings = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brfalse, notBothStrings); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brfalse, notBothStrings); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Castclass, _types.String); - var compareOrdinal = _types.GetMethod(_types.String, "CompareOrdinal", _types.String, _types.String); - il.Emit(OpCodes.Call, compareOrdinal); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Clt); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notBothStrings); - // Numeric path: a = ToNumber(arg0); b = ToNumber(arg1); a < b ? true : false (NaN → false). - var aLocal = il.DeclareLocal(_types.Double); - var bLocal = il.DeclareLocal(_types.Double); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.ToNumber); - il.Emit(OpCodes.Stloc, aLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.ToNumber); - il.Emit(OpCodes.Stloc, bLocal); - // NaN check: a == a, b == b - var notNaN = il.DefineLabel(); - var falseLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, aLocal); - il.Emit(OpCodes.Ldloc, aLocal); - il.Emit(OpCodes.Ceq); - il.Emit(OpCodes.Brfalse, falseLabel); - il.Emit(OpCodes.Ldloc, bLocal); - il.Emit(OpCodes.Ldloc, bLocal); - il.Emit(OpCodes.Ceq); - il.Emit(OpCodes.Brfalse, falseLabel); - il.MarkLabel(notNaN); - il.Emit(OpCodes.Ldloc, aLocal); - il.Emit(OpCodes.Ldloc, bLocal); - il.Emit(OpCodes.Clt); - il.Emit(OpCodes.Ret); - il.MarkLabel(falseLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ret); - } - - /// - /// Emits $Runtime.JsLessOrEqual(object x, object y) -> bool: - /// ECMA-262 abstract relational comparison: x <= y is "y < x is false - /// AND neither operand is NaN". Implemented as !JsLessThan(y, x) provided - /// neither is NaN; we replicate the helper inline to avoid double ToNumber. - /// - private void EmitJsLessOrEqual(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "JsLessOrEqual", - MethodAttributes.Public | MethodAttributes.Static, - _types.Boolean, - [_types.Object, _types.Object]); - runtime.JsLessOrEqual = method; - - var il = method.GetILGenerator(); - - // If both strings: CompareOrdinal <= 0 - var notBothStrings = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brfalse, notBothStrings); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brfalse, notBothStrings); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Castclass, _types.String); - var compareOrdinal = _types.GetMethod(_types.String, "CompareOrdinal", _types.String, _types.String); - il.Emit(OpCodes.Call, compareOrdinal); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Cgt); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ceq); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notBothStrings); - var aLocal = il.DeclareLocal(_types.Double); - var bLocal = il.DeclareLocal(_types.Double); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.ToNumber); - il.Emit(OpCodes.Stloc, aLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.ToNumber); - il.Emit(OpCodes.Stloc, bLocal); - var falseLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, aLocal); - il.Emit(OpCodes.Ldloc, aLocal); - il.Emit(OpCodes.Ceq); - il.Emit(OpCodes.Brfalse, falseLabel); - il.Emit(OpCodes.Ldloc, bLocal); - il.Emit(OpCodes.Ldloc, bLocal); - il.Emit(OpCodes.Ceq); - il.Emit(OpCodes.Brfalse, falseLabel); - // a <= b → !(a > b) → !(b < a) - il.Emit(OpCodes.Ldloc, bLocal); - il.Emit(OpCodes.Ldloc, aLocal); - il.Emit(OpCodes.Clt); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ceq); - il.Emit(OpCodes.Ret); - il.MarkLabel(falseLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ret); - } - - /// - /// Emits ConvertToNumber — matches JS Number(value) semantics. - /// Differs from ToNumber in that empty/whitespace strings return 0 (not NaN). - /// - /// - /// Pre-declares the $Runtime.ConvertToNumber MethodBuilder so its slot is - /// assigned before any other emitter binds to it. Body is filled in by - /// later, after GetProperty/InvokeMethodValue - /// are available (the ToPrimitive(value, "number") chain on Dictionary/$Object - /// args calls those helpers). - /// - internal void DeclareConvertToNumber(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "ConvertToNumber", - MethodAttributes.Public | MethodAttributes.Static, - _types.Double, - [_types.Object] - ); - runtime.ConvertToNumber = method; - } - - private void EmitConvertToNumber(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = (MethodBuilder)runtime.ConvertToNumber; - - var il = method.GetILGenerator(); - - var nullLabel = il.DefineLabel(); - var undefinedLabel = il.DefineLabel(); - var doubleLabel = il.DefineLabel(); - var boolLabel = il.DefineLabel(); - var stringLabel = il.DefineLabel(); - var nanLabel = il.DefineLabel(); - - // ECMA-262 7.1.4 ToNumber on object: ToPrimitive(value, "number") which - // tries valueOf first, then toString. Without this, Number({valueOf: - // () => 1}) returns NaN (object falls through to nan-label). - // Apply for Dictionary or $Object only — those hold user-defined - // valueOf/toString. Boxed primitives (via $Object marker fields) are - // also $Object, which is correct: their valueOf returns the underlying - // primitive per ECMA-262 19.x.3.7. - var argLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Stloc, argLocal); - - var skipToPrimLabel = il.DefineLabel(); - var doToPrimLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, doToPrimLabel); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brfalse, skipToPrimLabel); - il.MarkLabel(doToPrimLabel); - - // ToPrimitive: try valueOf, then toString. Mirrors EmitLengthToPrimitive's - // logic but writes to argLocal so the existing branches below see the - // (possibly replaced) primitive value. - var emptyArgsLocal = il.DeclareLocal(_types.ObjectArray); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Newarr, _types.Object); - il.Emit(OpCodes.Stloc, emptyArgsLocal); - - void TryToPrim(string name, Label afterLabel) - { - var fnLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Ldstr, name); - il.Emit(OpCodes.Call, runtime.GetProperty); - il.Emit(OpCodes.Stloc, fnLocal); - il.Emit(OpCodes.Ldloc, fnLocal); - il.Emit(OpCodes.Brfalse, afterLabel); - il.Emit(OpCodes.Ldloc, fnLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, afterLabel); - - var resultLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Ldloc, fnLocal); - il.Emit(OpCodes.Ldloc, emptyArgsLocal); - il.Emit(OpCodes.Call, runtime.InvokeMethodValue); - il.Emit(OpCodes.Stloc, resultLocal); - - // Still object? Fall through to next attempt without committing. - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, afterLabel); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brtrue, afterLabel); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Stloc, argLocal); - } - - var afterValueOf = il.DefineLabel(); - TryToPrim("valueOf", afterValueOf); - il.MarkLabel(afterValueOf); - - var afterToString = il.DefineLabel(); - var stillObj = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, stillObj); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brfalse, afterToString); - il.MarkLabel(stillObj); - TryToPrim("toString", afterToString); - il.MarkLabel(afterToString); - - // ECMA-262 7.1.1.1 OrdinaryToPrimitive: if neither valueOf nor toString - // returned a primitive (both returned objects, or neither was callable), - // throw TypeError. Pre-fix: the value fell through to NaN, silently - // masking the spec-required throw. - var afterTypeErrorLabel = il.DefineLabel(); - var stillObjAfterToString = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, stillObjAfterToString); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brfalse, afterTypeErrorLabel); - il.MarkLabel(stillObjAfterToString); - il.Emit(OpCodes.Ldstr, "Cannot convert object to primitive value"); - il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); - il.Emit(OpCodes.Call, runtime.CreateException); - il.Emit(OpCodes.Throw); - il.MarkLabel(afterTypeErrorLabel); - - il.MarkLabel(skipToPrimLabel); - - // null => 0.0 - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Brfalse, nullLabel); - - // ECMA-262 21.1.1.1 → 7.1.4: Number(Symbol) throws TypeError. Without - // this branch the value falls through to the "everything else → NaN" - // tail, masking the spec-required throw. - var notSymbolConvLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, runtime.TSSymbolType); - il.Emit(OpCodes.Brfalse, notSymbolConvLabel); - il.Emit(OpCodes.Ldstr, "Cannot convert a Symbol value to a number"); - il.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); - il.Emit(OpCodes.Call, runtime.CreateException); - il.Emit(OpCodes.Throw); - il.MarkLabel(notSymbolConvLabel); - - // ECMA-262 21.1.1.1 step 5: Number(BigInt) returns a Number with the - // same mathematical value (rounded per 21.1.1.1.1 NumberFromBigInt). - // System.Numerics.BigInteger has an explicit op_Explicit(BigInteger) - // → double — use it. Don't throw (ToNumber's spec wants throw, but - // ConvertToNumber backs the Number() constructor's spec which coerces). - var notBigIntConvLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.BigInteger); - il.Emit(OpCodes.Brfalse, notBigIntConvLabel); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Unbox_Any, _types.BigInteger); - // System.Numerics.BigInteger has multiple op_Explicit overloads - // (one per primitive return type). Pick the BigInteger → double one - // by walking the candidate set explicitly. - System.Reflection.MethodInfo? explicitToDouble = null; - foreach (var m in _types.BigInteger.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)) - { - if (m.Name != "op_Explicit") continue; - if (m.ReturnType != _types.Double) continue; - var ps = m.GetParameters(); - if (ps.Length == 1 && ps[0].ParameterType == _types.BigInteger) - { - explicitToDouble = m; - break; - } - } - if (explicitToDouble != null) - { - il.Emit(OpCodes.Call, explicitToDouble); - il.Emit(OpCodes.Ret); - } - else - { - // Fallback: Convert.ToDouble(BigInteger) via boxed object. - il.Emit(OpCodes.Box, _types.BigInteger); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Convert, "ToDouble", _types.Object)); - il.Emit(OpCodes.Ret); - } - il.MarkLabel(notBigIntConvLabel); - - // undefined => NaN - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, undefinedLabel); - - // double => return as-is - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.Double); - il.Emit(OpCodes.Brtrue, doubleLabel); - - // bool => true:1.0, false:0.0 - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.Boolean); - il.Emit(OpCodes.Brtrue, boolLabel); - - // string => trim, empty→0, tryparse, else NaN - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brtrue, stringLabel); - - // .NET interop: boxed non-double numerics (float, int, long, decimal, …) - // arrive via @DotNetType calls. Convert them numerically rather than - // falling through to NaN (EnsureDouble routes through this method). - var notConvertibleLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, typeof(IConvertible)); - il.Emit(OpCodes.Brfalse, notConvertibleLabel); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Convert, "ToDouble", _types.Object)); - il.Emit(OpCodes.Ret); - il.MarkLabel(notConvertibleLabel); - - // everything else => NaN - il.Emit(OpCodes.Br, nanLabel); - - // null case: return 0.0 - il.MarkLabel(nullLabel); - il.Emit(OpCodes.Ldc_R8, 0.0); - il.Emit(OpCodes.Ret); - - // undefined case: return NaN - il.MarkLabel(undefinedLabel); - il.Emit(OpCodes.Ldc_R8, double.NaN); - il.Emit(OpCodes.Ret); - - // double case: unbox and return - il.MarkLabel(doubleLabel); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Unbox_Any, _types.Double); - il.Emit(OpCodes.Ret); - - // bool case: unbox, convert to float - il.MarkLabel(boolLabel); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Unbox_Any, _types.Boolean); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Ret); - - // string case: trim, check empty, try parse - il.MarkLabel(stringLabel); - var trimmedLocal = il.DeclareLocal(_types.String); - var resultLocal = il.DeclareLocal(_types.Double); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.String, "Trim")); - il.Emit(OpCodes.Stloc, trimmedLocal); - - // if (trimmed.Length == 0) return 0.0 - var nonEmptyLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, trimmedLocal); - il.Emit(OpCodes.Callvirt, _types.String.GetProperty("Length")!.GetGetMethod()!); - il.Emit(OpCodes.Brtrue, nonEmptyLabel); - il.Emit(OpCodes.Ldc_R8, 0.0); - il.Emit(OpCodes.Ret); - - // ECMA-262 21.1.1.1 / 7.1.4: numeric string can be hex ("0x..."/"0X..."), - // Infinity (already short-circuited by ToNumber), or float. Try hex first. - il.MarkLabel(nonEmptyLabel); - var notHexInConvLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, trimmedLocal); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length").GetGetMethod()!); - il.Emit(OpCodes.Ldc_I4_2); - il.Emit(OpCodes.Blt, notHexInConvLabel); - il.Emit(OpCodes.Ldloc, trimmedLocal); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Callvirt, _types.String.GetMethod("get_Chars", [_types.Int32])!); - il.Emit(OpCodes.Ldc_I4, (int)'0'); - il.Emit(OpCodes.Bne_Un, notHexInConvLabel); - il.Emit(OpCodes.Ldloc, trimmedLocal); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Callvirt, _types.String.GetMethod("get_Chars", [_types.Int32])!); - il.Emit(OpCodes.Ldc_I4, 0x20); - il.Emit(OpCodes.Or); - il.Emit(OpCodes.Ldc_I4, (int)'x'); - il.Emit(OpCodes.Bne_Un, notHexInConvLabel); - il.BeginExceptionBlock(); - il.Emit(OpCodes.Ldloc, trimmedLocal); - il.Emit(OpCodes.Ldc_I4_2); - il.Emit(OpCodes.Callvirt, _types.String.GetMethod("Substring", [_types.Int32])!); - il.Emit(OpCodes.Ldc_I4, (int)System.Globalization.NumberStyles.HexNumber); - il.Emit(OpCodes.Call, typeof(System.Globalization.CultureInfo).GetProperty("InvariantCulture")!.GetGetMethod()!); - il.Emit(OpCodes.Call, typeof(long).GetMethod("Parse", [typeof(string), typeof(System.Globalization.NumberStyles), typeof(IFormatProvider)])!); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Stloc, resultLocal); - il.BeginCatchBlock(_types.Exception); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldc_R8, double.NaN); - il.Emit(OpCodes.Stloc, resultLocal); - il.EndExceptionBlock(); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notHexInConvLabel); - // ECMA-262: "0b"/"0B" → binary, "0o"/"0O" → octal literal parsing. - EmitParsePrefixedInt(il, trimmedLocal, resultLocal, 'b', 2); - EmitParsePrefixedInt(il, trimmedLocal, resultLocal, 'o', 8); - - // ECMA-262 7.1.4: only the case-sensitive forms "Infinity", "+Infinity", - // "-Infinity" are valid Infinity literals. .NET's Double.TryParse - // (NumberStyles.Float) accepts "infinity"/"INFINITY"/etc case- - // insensitively — must reject those before TryParse runs. - var notCiInfLabel = il.DefineLabel(); - // Exact case-sensitive forms first → return ±Infinity. - var trimEqInfLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, trimmedLocal); - il.Emit(OpCodes.Ldstr, "Infinity"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, trimEqInfLabel); - il.Emit(OpCodes.Ldc_R8, double.PositiveInfinity); - il.Emit(OpCodes.Ret); - il.MarkLabel(trimEqInfLabel); - var trimEqPlusInfLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, trimmedLocal); - il.Emit(OpCodes.Ldstr, "+Infinity"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, trimEqPlusInfLabel); - il.Emit(OpCodes.Ldc_R8, double.PositiveInfinity); - il.Emit(OpCodes.Ret); - il.MarkLabel(trimEqPlusInfLabel); - var trimEqMinusInfLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, trimmedLocal); - il.Emit(OpCodes.Ldstr, "-Infinity"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, trimEqMinusInfLabel); - il.Emit(OpCodes.Ldc_R8, double.NegativeInfinity); - il.Emit(OpCodes.Ret); - il.MarkLabel(trimEqMinusInfLabel); - // Now reject any string that contains "infinity" case-insensitively - // (since the exact-case forms have already been short-circuited). - // Use String.Contains(string, StringComparison) — net8+ overload. - il.Emit(OpCodes.Ldloc, trimmedLocal); - il.Emit(OpCodes.Ldstr, "infinity"); - il.Emit(OpCodes.Ldc_I4_5); // StringComparison.OrdinalIgnoreCase - il.Emit(OpCodes.Callvirt, _types.String.GetMethod("Contains", [_types.String, typeof(StringComparison)])!); - il.Emit(OpCodes.Brfalse, notCiInfLabel); - // Case-insensitive but not exact → NaN. - il.Emit(OpCodes.Ldc_R8, double.NaN); - il.Emit(OpCodes.Ret); - il.MarkLabel(notCiInfLabel); - - // double.TryParse(trimmed, NumberStyles.Float, CultureInfo.InvariantCulture, out result) - il.Emit(OpCodes.Ldloc, trimmedLocal); - il.Emit(OpCodes.Ldc_I4, (int)System.Globalization.NumberStyles.Float); - il.Emit(OpCodes.Call, typeof(System.Globalization.CultureInfo).GetProperty("InvariantCulture")!.GetGetMethod()!); - il.Emit(OpCodes.Ldloca, resultLocal); - il.Emit(OpCodes.Call, typeof(double).GetMethod("TryParse", [typeof(string), typeof(System.Globalization.NumberStyles), typeof(IFormatProvider), typeof(double).MakeByRefType()])!); - var parseSuccessLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, parseSuccessLabel); - // parse failed => NaN - il.Emit(OpCodes.Ldc_R8, double.NaN); - il.Emit(OpCodes.Ret); - // parse succeeded => return result - il.MarkLabel(parseSuccessLabel); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Ret); - - // NaN fallback - il.MarkLabel(nanLabel); - il.Emit(OpCodes.Ldc_R8, double.NaN); - il.Emit(OpCodes.Ret); - } - - private void EmitIsTruthy(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - // Signature forward-declared by DefineRuntimeClassPhase1 so $RegExp - // (which emits before $Runtime's body) can call it; reuse that slot. - var method = (MethodBuilder)runtime.IsTruthy; - - var il = method.GetILGenerator(); - var falseLabel = il.DefineLabel(); - var checkBool = il.DefineLabel(); - var checkDouble = il.DefineLabel(); - var checkString = il.DefineLabel(); - var checkBigInt = il.DefineLabel(); - var trueLabel = il.DefineLabel(); - - // null => false - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Brfalse, falseLabel); - - // undefined => false - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, falseLabel); - - // bool => return value - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Boolean); - il.Emit(OpCodes.Brtrue, checkBool); - - // double => check for 0 and NaN - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Double); - il.Emit(OpCodes.Brtrue, checkDouble); - - // string => check for empty - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brtrue, checkString); - - // bigint => 0n is falsy (ToBoolean(bigint): false iff zero) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.BigInteger); - il.Emit(OpCodes.Brtrue, checkBigInt); - - // everything else => true - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ret); - - // Check bool value - il.MarkLabel(checkBool); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, _types.Boolean); - il.Emit(OpCodes.Ret); - - // Check double: 0 and NaN are falsy - il.MarkLabel(checkDouble); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, _types.Double); - var dLocal = il.DeclareLocal(_types.Double); - il.Emit(OpCodes.Stloc, dLocal); - // Check if d == 0 - il.Emit(OpCodes.Ldloc, dLocal); - il.Emit(OpCodes.Ldc_R8, 0.0); - il.Emit(OpCodes.Ceq); - il.Emit(OpCodes.Brtrue, falseLabel); - // Check if d is NaN (NaN != NaN) - il.Emit(OpCodes.Ldloc, dLocal); - il.Emit(OpCodes.Ldloc, dLocal); - il.Emit(OpCodes.Ceq); - il.Emit(OpCodes.Brfalse, falseLabel); // If d != d, it's NaN - // Not 0 and not NaN => truthy - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ret); - - // Check string: empty is falsy - il.MarkLabel(checkString); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "get_Length")); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Cgt); - il.Emit(OpCodes.Ret); - - // Check bigint: truthy iff non-zero (value != BigInteger.Zero). - il.MarkLabel(checkBigInt); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, _types.BigInteger); - il.Emit(OpCodes.Call, _types.GetMethod(_types.BigInteger, "get_Zero")); - il.Emit(OpCodes.Call, _types.GetMethod(_types.BigInteger, "op_Inequality", _types.BigInteger, _types.BigInteger)); - il.Emit(OpCodes.Ret); - - il.MarkLabel(falseLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ret); - } - - private void EmitTypeOf(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "TypeOf", - MethodAttributes.Public | MethodAttributes.Static, - _types.String, - [_types.Object] - ); - runtime.TypeOf = method; - - var il = method.GetILGenerator(); - var nullLabel = il.DefineLabel(); - var undefinedLabel = il.DefineLabel(); - var boolLabel = il.DefineLabel(); - var numberLabel = il.DefineLabel(); - var stringLabel = il.DefineLabel(); - var symbolLabel = il.DefineLabel(); - var functionLabel = il.DefineLabel(); - var endLabel = il.DefineLabel(); - - // null => "object" (JS typeof null === "object") - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Brfalse, nullLabel); - - // undefined => "undefined" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, undefinedLabel); - - // Check for union types using $IUnionType marker interface - // If value implements $IUnionType, unwrap via Value property and recurse - var notUnionLabel = il.DefineLabel(); - var unionLocal = il.DeclareLocal(runtime.IUnionTypeInterface); - - // Check: if (value is $IUnionType union) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.IUnionTypeInterface); - il.Emit(OpCodes.Stloc, unionLocal); - il.Emit(OpCodes.Ldloc, unionLocal); - il.Emit(OpCodes.Brfalse, notUnionLabel); - - // Get underlying value via interface: union.Value - il.Emit(OpCodes.Ldloc, unionLocal); - il.Emit(OpCodes.Callvirt, runtime.IUnionTypeValueGetter); - - // return TypeOf(underlyingValue) - recursive call - il.Emit(OpCodes.Call, method); // Recursive call to self - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(notUnionLabel); - - // bool => "boolean" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Boolean); - il.Emit(OpCodes.Brtrue, boolLabel); - - // double => "number" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Double); - il.Emit(OpCodes.Brtrue, numberLabel); - - // string => "string" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brtrue, stringLabel); - - // TSSymbol => "symbol" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSSymbolType); - il.Emit(OpCodes.Brtrue, symbolLabel); - - // TSFunction => "function" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSFunctionType); - il.Emit(OpCodes.Brtrue, functionLabel); - - // $BoundTSFunction => "function" (returned by Function.prototype.bind - // and similar paths; without this, `typeof fn.bind(x) === "object"`). - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.BoundTSFunctionType); - il.Emit(OpCodes.Brtrue, functionLabel); - - // $FunctionBindWrapper / $FunctionCallWrapper / $FunctionApplyWrapper - // => "function". These wrap non-$TSFunction targets for late-bound - // dispatch and need to be callable from JS land. - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.FunctionBindWrapperType); - il.Emit(OpCodes.Brtrue, functionLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.FunctionCallWrapperType); - il.Emit(OpCodes.Brtrue, functionLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.FunctionApplyWrapperType); - il.Emit(OpCodes.Brtrue, functionLabel); - - // Delegate => "function" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Delegate); - il.Emit(OpCodes.Brtrue, functionLabel); - - // $PromisifiedFunction / $DeprecatedFunction => "function" (from util.promisify/deprecate). - // These types only exist when UsesUtilPromisify is set; without the feature - // the runtime fields are null and Isinst would NRE at IL emission time. - if (_features.UsesUtilPromisify) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSPromisifiedFunctionType); - il.Emit(OpCodes.Brtrue, functionLabel); - - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSDeprecatedFunctionType); - il.Emit(OpCodes.Brtrue, functionLabel); - } - - // $BoundArrayMethod / $BoundMapMethod / $BoundSetMethod / $BoundAnyFunction => "function" - // These are callable wrappers returned by GetListProperty/GetMapProperty/GetSetProperty - // for dynamic property access on arrays/maps/sets (duck typing across module boundaries) - // and by `.bind` on non-$TSFunction targets. - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.BoundArrayMethodType); - il.Emit(OpCodes.Brtrue, functionLabel); - - if (_features.UsesMap) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.BoundMapMethodType); - il.Emit(OpCodes.Brtrue, functionLabel); - } - - if (_features.UsesSet) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.BoundSetMethodType); - il.Emit(OpCodes.Brtrue, functionLabel); - } - - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.BoundAnyFunctionType); - il.Emit(OpCodes.Brtrue, functionLabel); - - // System.Type => "function" - // Compiled class references (e.g. `const f = Foo` where Foo is a class) are - // emitted as Ldtoken + GetTypeFromHandle, which yields a System.Type. Node/JS - // spec says classes are functions, so `typeof Foo === 'function'` must hold. - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Type); - il.Emit(OpCodes.Brtrue, functionLabel); - - // $CJSModule => "object" (falls through naturally, but explicit null-isinst checks - // above might have short-circuited — this branch ensures consistent routing). - // No early return needed; falls through to the generic "object" default at the end. - - // BigInteger => "bigint" - var bigintLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.BigInteger); - il.Emit(OpCodes.Brtrue, bigintLabel); - - // Proxy => check IsCallable: "function" if callable, "object" otherwise - // Uses FullName comparison to avoid SharpTS.dll dependency - var notProxyLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "GetType")); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.Type, "FullName").GetGetMethod()!); - il.Emit(OpCodes.Ldstr, ProxyTypeName); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notProxyLabel); - - // It's a proxy - check IsCallable property getter via reflection - EmitProxyMethodCall(il, () => il.Emit(OpCodes.Ldarg_0), "get_IsCallable", () => - { - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Newarr, _types.Object); - }); - il.Emit(OpCodes.Unbox_Any, _types.Boolean); - il.Emit(OpCodes.Brtrue, functionLabel); - il.Emit(OpCodes.Ldstr, "object"); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(notProxyLabel); - - // Default => "object" - il.Emit(OpCodes.Ldstr, "object"); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(nullLabel); - il.Emit(OpCodes.Ldstr, "object"); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(undefinedLabel); - il.Emit(OpCodes.Ldstr, "undefined"); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(boolLabel); - il.Emit(OpCodes.Ldstr, "boolean"); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(numberLabel); - il.Emit(OpCodes.Ldstr, "number"); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(stringLabel); - il.Emit(OpCodes.Ldstr, "string"); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(symbolLabel); - il.Emit(OpCodes.Ldstr, "symbol"); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(functionLabel); - il.Emit(OpCodes.Ldstr, "function"); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(bigintLabel); - il.Emit(OpCodes.Ldstr, "bigint"); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(endLabel); - il.Emit(OpCodes.Ret); - } - - private void EmitInstanceOf(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "InstanceOf", - MethodAttributes.Public | MethodAttributes.Static, - _types.Boolean, - [_types.Object, _types.Object] - ); - runtime.InstanceOf = method; - - var il = method.GetILGenerator(); - var falseLabel = il.DefineLabel(); - var trueLabel = il.DefineLabel(); - - // if (instance == null || classType == null) return false - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Brfalse, falseLabel); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Brfalse, falseLabel); - - // A plain Dictionary RHS is never a constructor — letting it reach the - // IsAssignableFrom fallback matched EVERY dict-shaped value (object - // literals, namespace singletons, module namespaces), so - // `{} instanceof AbortSignal` was true (#246). The AbortSignal - // namespace singleton brand-checks signal dicts via their - // "_reasonSet" slot; any other dict RHS is false. - var notDictRhsLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brfalse, notDictRhsLabel); - - if (runtime.AbortSignalNamespaceField != null) - { - var lhsDictLocal = il.DeclareLocal(_types.DictionaryStringObject); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldsfld, runtime.AbortSignalNamespaceField); - il.Emit(OpCodes.Bne_Un, falseLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Stloc, lhsDictLocal); - il.Emit(OpCodes.Ldloc, lhsDictLocal); - il.Emit(OpCodes.Brfalse, falseLabel); - il.Emit(OpCodes.Ldloc, lhsDictLocal); - il.Emit(OpCodes.Ldstr, "_reasonSet"); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); - il.Emit(OpCodes.Ret); - } - else - { - il.Emit(OpCodes.Br, falseLabel); - } - - il.MarkLabel(notDictRhsLabel); - - // Per JS spec, `instance instanceof F` where F is a user function walks - // instance's prototype chain looking for F.prototype. Compiled mode's - // legacy InstanceOf used .NET IsAssignableFrom, which is type-system - // semantics — wrong for $TSFunction callees (every $TSFunction has the - // same .NET type, so the check was meaningless). With Stage 0b/0c - // landed, F.prototype is a real $Object and `new F()` links newObj's - // prototype to it; walking the chain via PDSGetPrototype now produces - // the correct answer. - var notTSFuncLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Isinst, runtime.TSFunctionType); - il.Emit(OpCodes.Brfalse, notTSFuncLabel); - - // F.prototype = $Runtime.GetFunctionMethod(F, "prototype") - var targetProtoLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "prototype"); - il.Emit(OpCodes.Call, runtime.GetFunctionMethod); - il.Emit(OpCodes.Stloc, targetProtoLocal); - - // If F has no .prototype somehow (e.g., bound function), fall back to - // false rather than walking — JS spec actually throws TypeError, but - // returning false matches what the previous implementation produced. - il.Emit(OpCodes.Ldloc, targetProtoLocal); - il.Emit(OpCodes.Brfalse, falseLabel); - - // Walk: current = PDSGetPrototype(instance); while (current != null) { - // if (current === F.prototype) return true; - // current = PDSGetPrototype(current); } - // return false - var currentLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.PDSGetPrototype); - il.Emit(OpCodes.Stloc, currentLocal); - - var loopLabel = il.DefineLabel(); - il.MarkLabel(loopLabel); - il.Emit(OpCodes.Ldloc, currentLocal); - il.Emit(OpCodes.Brfalse, falseLabel); - - // current === F.prototype ? - il.Emit(OpCodes.Ldloc, currentLocal); - il.Emit(OpCodes.Ldloc, targetProtoLocal); - il.Emit(OpCodes.Beq, trueLabel); - - // current = PDSGetPrototype(current) - il.Emit(OpCodes.Ldloc, currentLocal); - il.Emit(OpCodes.Call, runtime.PDSGetPrototype); - il.Emit(OpCodes.Stloc, currentLocal); - il.Emit(OpCodes.Br, loopLabel); - - il.MarkLabel(notTSFuncLabel); - - // Get type of instance and check IsAssignableFrom (legacy path for - // .NET-typed class-reference instanceof checks). - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Isinst, _types.Type); - var notTypeLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notTypeLabel); - - // Stage 4z19 boxed-primitive detection: when classType is one of the - // primitive types (Boolean/Double/String) and instance is a $Object - // wrapper with __primitiveType matching, return true. Only applies - // when the legacy IsAssignableFrom would say false (since .NET - // System.Boolean/Double/String are sealed value types, IsAssignableFrom - // for a $Object always returns false; checking the marker comes first - // to short-circuit). - var classTypeLocal = il.DeclareLocal(_types.Type); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Castclass, _types.Type); - il.Emit(OpCodes.Stloc, classTypeLocal); - - // `x instanceof Object` — the bare `Object` identifier resolves to the - // System.Object Type token (see RuntimeEmitter.GlobalThis.cs), so apply - // JS Object semantics rather than the IsAssignableFrom fallback below: - // every .NET type is assignable to System.Object, so that fallback would - // wrongly say true for boxed primitives (double/bool/string), BigInteger, - // $TSSymbol, and the undefined sentinel. Per ECMA-262 OrdinaryHasInstance - // a primitive O short-circuits to false — so a guest value is an Object - // iff it is non-primitive (mirrors interp's IsJsObject, #342). Boxed - // primitive wrappers (`new Number(5)`) are $Object instances, not boxed - // doubles, so they correctly fall through to true. - var notObjectClassLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, classTypeLocal); - il.Emit(OpCodes.Ldtoken, _types.Object); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Bne_Un, notObjectClassLabel); - void PrimitiveToFalse(Type primType) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, primType); - il.Emit(OpCodes.Brtrue, falseLabel); - } - PrimitiveToFalse(_types.Boolean); - PrimitiveToFalse(_types.Double); - PrimitiveToFalse(_types.String); - PrimitiveToFalse(_types.BigInteger); - PrimitiveToFalse(runtime.TSSymbolType); - PrimitiveToFalse(runtime.UndefinedType); - il.Emit(OpCodes.Br, trueLabel); - il.MarkLabel(notObjectClassLabel); - - // When classType is one of the primitive wrapper types (Number/String/ - // Boolean, which lower to the System.Double/String/Boolean Type tokens), - // the decision is TERMINAL: per ECMA-262 OrdinaryHasInstance only a boxed - // wrapper object (`new Number(5)`) is an instance — a bare primitive is - // NOT. Branch to true iff `instance` carries the matching __primitiveType - // marker, else to false. Falling through to the IsAssignableFrom fallback - // below would wrongly match a bare boxed double/string/bool, since e.g. - // IsAssignableFrom(double, double) is true (#375). Mirrors the terminal - // `Object` branch above. - void CheckBoxed(Type primType, string typeTag) - { - var skip = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, classTypeLocal); - il.Emit(OpCodes.Ldtoken, primType); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Bne_Un, skip); - // classType is the primitive wrapper type — true iff boxed marker matches. - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldstr, typeTag); - il.Emit(OpCodes.Call, runtime.IsBoxedPrimitiveOfTypeMethod); - il.Emit(OpCodes.Brtrue, trueLabel); - il.Emit(OpCodes.Br, falseLabel); - il.MarkLabel(skip); - } - CheckBoxed(_types.Boolean, "Boolean"); - CheckBoxed(_types.Double, "Number"); - CheckBoxed(_types.String, "String"); - // Symbol lowers to the $TSSymbol Type token, so a bare $TSSymbol would - // otherwise reach the IsAssignableFrom($TSSymbol, $TSSymbol) fallback and - // wrongly match. Terminal like the wrappers above: true iff `instance` is - // a boxed Symbol wrapper (`Object(sym)`), false for a bare symbol (#449). - CheckBoxed(runtime.TSSymbolType, "Symbol"); - - // `x instanceof Promise`: the Promise identifier resolves to - // typeof(Task), but $Promise instances (and #242 Promise - // subclasses, which derive from $Promise) wrap their task instead of - // being one — accept them here; raw tasks fall through to the - // IsAssignableFrom below. - var notTaskTargetLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, classTypeLocal); - il.Emit(OpCodes.Ldtoken, _types.TaskOfObject); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Bne_Un, notTaskTargetLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSPromiseType); - il.Emit(OpCodes.Brtrue, trueLabel); - il.MarkLabel(notTaskTargetLabel); - - // Generic class target: `b instanceof Box` emits the OPEN generic - // definition (Box`1) while instances carry constructed types - // (Box) — IsAssignableFrom never matches across that gap. - // Walk the instance's base-type chain comparing generic definitions. - var notGenericDefLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, classTypeLocal); - il.Emit(OpCodes.Callvirt, _types.Type.GetProperty("IsGenericTypeDefinition")!.GetGetMethod()!); - il.Emit(OpCodes.Brfalse, notGenericDefLabel); - var walkTypeLocal = il.DeclareLocal(_types.Type); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "GetType")); - il.Emit(OpCodes.Stloc, walkTypeLocal); - var genericWalkLoop = il.DefineLabel(); - var genericWalkNext = il.DefineLabel(); - il.MarkLabel(genericWalkLoop); - il.Emit(OpCodes.Ldloc, walkTypeLocal); - il.Emit(OpCodes.Brfalse, falseLabel); - il.Emit(OpCodes.Ldloc, walkTypeLocal); - il.Emit(OpCodes.Callvirt, _types.Type.GetProperty("IsGenericType")!.GetGetMethod()!); - il.Emit(OpCodes.Brfalse, genericWalkNext); - il.Emit(OpCodes.Ldloc, walkTypeLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Type, "GetGenericTypeDefinition")); - il.Emit(OpCodes.Ldloc, classTypeLocal); - il.Emit(OpCodes.Beq, trueLabel); - il.MarkLabel(genericWalkNext); - il.Emit(OpCodes.Ldloc, walkTypeLocal); - il.Emit(OpCodes.Callvirt, _types.Type.GetProperty("BaseType")!.GetGetMethod()!); - il.Emit(OpCodes.Stloc, walkTypeLocal); - il.Emit(OpCodes.Br, genericWalkLoop); - il.MarkLabel(notGenericDefLabel); - - // classType is Type, use it directly - il.Emit(OpCodes.Ldloc, classTypeLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "GetType")); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "IsAssignableFrom", _types.Type)); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notTypeLabel); - // classType is not Type, get its type - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "GetType")); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "GetType")); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "IsAssignableFrom", _types.Type)); - il.Emit(OpCodes.Ret); - - il.MarkLabel(trueLabel); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ret); - - il.MarkLabel(falseLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ret); - } - - /// - /// Emits HasIn(object key, object obj) -> bool - /// Implements the JavaScript 'in' operator: checks if a property exists in an object. - /// Handles both symbol keys and string keys. - /// - private void EmitHasIn(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "HasIn", - MethodAttributes.Public | MethodAttributes.Static, - _types.Boolean, - [_types.Object, _types.Object] - ); - runtime.HasIn = method; - - var il = method.GetILGenerator(); - var falseLabel = il.DefineLabel(); - var symbolKeyLabel = il.DefineLabel(); - var dictLabel = il.DefineLabel(); - var listLabel = il.DefineLabel(); - - // if (obj == null) return false - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Brfalse, falseLabel); - - // Proxy check: uses obj.GetType().FullName comparison (no SharpTS.dll dependency) - // Note: HasIn signature is (key, obj) so obj is arg_1 - var notProxyLabel = il.DefineLabel(); - EmitProxyHasCheck(il, () => il.Emit(OpCodes.Ldarg_1), () => il.Emit(OpCodes.Ldarg_0), notProxyLabel, runtime); - - il.MarkLabel(notProxyLabel); - - // Check if key is a symbol - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.IsSymbolMethod); - il.Emit(OpCodes.Brtrue, symbolKeyLabel); - - // String key path - // Check if obj is $TSObject - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - var notTSObjectLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notTSObjectLabel); - - // $TSObject - call HasProperty(string) - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Castclass, runtime.TSObjectType); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); - il.Emit(OpCodes.Callvirt, runtime.TSObjectHasProperty); - il.Emit(OpCodes.Ret); - - // Check if obj is Dictionary - il.MarkLabel(notTSObjectLabel); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, dictLabel); - - // $Array (check BEFORE the plain List check — $Array inherits - // List; the List branch below reads base.Count and misses - // sparse holes, and returns true for a hole index where it should - // return false). - var tsArrayHasLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Isinst, runtime.TSArrayType); - il.Emit(OpCodes.Brtrue, tsArrayHasLabel); - - // Check if obj is List (array) - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Isinst, _types.ListOfObject); - il.Emit(OpCodes.Brtrue, listLabel); - - // Other types (e.g., emitted class instances) — check via $IHasFields + reflection - var classKeyStrLocal = il.DeclareLocal(_types.String); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); - il.Emit(OpCodes.Stloc, classKeyStrLocal); - - var classTrueLabel = il.DefineLabel(); - - // Check $IHasFields interface: call HasProperty(key) for typed backing fields + _fields dict - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Isinst, runtime.IHasFieldsInterface); - var notHasFieldsLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notHasFieldsLabel); - - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Castclass, runtime.IHasFieldsInterface); - il.Emit(OpCodes.Ldloc, classKeyStrLocal); - il.Emit(OpCodes.Callvirt, runtime.IHasFieldsHasProperty); - il.Emit(OpCodes.Brtrue, classTrueLabel); - - il.MarkLabel(notHasFieldsLabel); - - // Also check for methods via reflection (e.g., inherited methods) - // Convert camelCase key to PascalCase for .NET method lookup - var classPascalNameLocal = il.DeclareLocal(_types.String); - il.Emit(OpCodes.Ldloc, classKeyStrLocal); - il.Emit(OpCodes.Call, runtime.ToPascalCase); - il.Emit(OpCodes.Stloc, classPascalNameLocal); - - // obj.GetType().GetProperty(pascalName, Instance | Public | IgnoreCase) - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "GetType")); - il.Emit(OpCodes.Ldloc, classPascalNameLocal); - il.Emit(OpCodes.Ldc_I4, (int)(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.IgnoreCase)); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetProperty", _types.String, typeof(System.Reflection.BindingFlags))); - il.Emit(OpCodes.Brtrue, classTrueLabel); - - // obj.GetType().GetMethod(pascalName, Instance | Public | IgnoreCase) - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "GetType")); - il.Emit(OpCodes.Ldloc, classPascalNameLocal); - il.Emit(OpCodes.Ldc_I4, (int)(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.IgnoreCase)); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetMethod", _types.String, typeof(System.Reflection.BindingFlags))); - il.Emit(OpCodes.Brtrue, classTrueLabel); - - il.Emit(OpCodes.Br, falseLabel); - - il.MarkLabel(classTrueLabel); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ret); - - // Dictionary - use ContainsKey - il.MarkLabel(dictLabel); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); - il.Emit(OpCodes.Ret); - - // List (array) - check "length" property or numeric index - il.MarkLabel(listLabel); - var indexLocal = il.DeclareLocal(_types.Int32); - var listKeyStrLocal = il.DeclareLocal(_types.String); - - // Convert key to string - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); - il.Emit(OpCodes.Stloc, listKeyStrLocal); - - // Check if key == "length" → return true - var notLengthLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, listKeyStrLocal); - il.Emit(OpCodes.Ldstr, "length"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notLengthLabel); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notLengthLabel); - // Try int.TryParse(key, out index) → if fails, return false - il.Emit(OpCodes.Ldloc, listKeyStrLocal); - il.Emit(OpCodes.Ldloca, indexLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Int32, "TryParse", _types.String, _types.Int32.MakeByRefType())); - il.Emit(OpCodes.Brfalse, falseLabel); - - // index >= 0 && index < list.Count - il.Emit(OpCodes.Ldloc, indexLocal); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Blt, falseLabel); - il.Emit(OpCodes.Ldloc, indexLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Castclass, _types.ListOfObject); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count").GetGetMethod()!); - il.Emit(OpCodes.Bge, falseLabel); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ret); - - // $Array — "length" is always present; numeric keys use TSArrayHasIndex - // (which returns false for holes, unlike the List branch's index-in- - // range check). Non-numeric named keys aren't stored on arrays, so - // fall back to false. - il.MarkLabel(tsArrayHasLabel); - { - var tsArrKeyStrLocal = il.DeclareLocal(_types.String); - var tsArrIndexLocal = il.DeclareLocal(_types.Int64); - - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); - il.Emit(OpCodes.Stloc, tsArrKeyStrLocal); - - // if (key == "length") return true - var tsArrNotLength = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, tsArrKeyStrLocal); - il.Emit(OpCodes.Ldstr, "length"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, tsArrNotLength); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ret); - - il.MarkLabel(tsArrNotLength); - // long.TryParse(key, out idx) — if fails, key isn't numeric → false. - il.Emit(OpCodes.Ldloc, tsArrKeyStrLocal); - il.Emit(OpCodes.Ldloca, tsArrIndexLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Int64, "TryParse", _types.String, _types.Int64.MakeByRefType())); - il.Emit(OpCodes.Brfalse, falseLabel); - - // arr.HasIndex(idx) — handles sparse + hole semantics. - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Castclass, runtime.TSArrayType); - il.Emit(OpCodes.Ldloc, tsArrIndexLocal); - il.Emit(OpCodes.Callvirt, runtime.TSArrayHasIndex); - il.Emit(OpCodes.Ret); - } - - // Symbol key path - il.MarkLabel(symbolKeyLabel); - // Get symbol dict and check if key exists - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.GetSymbolDictMethod); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "ContainsKey", _types.Object)); - il.Emit(OpCodes.Ret); - - // Return false - il.MarkLabel(falseLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ret); - } - - // NOTE: EmitConvertArgsForUnionTypes was removed - it was dead code. - // The actual conversion is done by the private ConvertArgsForUnionTypes method on $TSFunction. - - private void EmitAdd(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "Add", - MethodAttributes.Public | MethodAttributes.Static, - _types.Object, - [_types.Object, _types.Object] - ); - runtime.Add = method; - - var il = method.GetILGenerator(); - var stringConcatLabel = il.DefineLabel(); - var undefinedNanLabel = il.DefineLabel(); - - // ECMA-262 §13.10.1 step 1-2: ToPrimitive both operands (default hint) - // before the string-vs-numeric branch. UnwrapIfBoxed handles the boxed- - // primitive case (`new String("x") + "y"` → "xy" instead of - // "[object Object]y"); plain $Object operands pass through unchanged - // and continue to the existing Stringify path which calls .ToString(). - var leftLocal = il.DeclareLocal(_types.Object); - var rightLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.UnwrapIfBoxedMethod); - il.Emit(OpCodes.Stloc, leftLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.UnwrapIfBoxedMethod); - il.Emit(OpCodes.Stloc, rightLocal); - - // if (left is string || right is string) string concat - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brtrue, stringConcatLabel); - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brtrue, stringConcatLabel); - - // Either operand $Undefined → NaN (ECMA-262 12.8.3: ToNumber(undefined) = NaN, - // and any arithmetic with NaN yields NaN). Convert.ToDouble($Undefined) throws - // because $Undefined isn't IConvertible; short-circuit here. - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, undefinedNanLabel); - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, undefinedNanLabel); - - // Numeric addition - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Convert, "ToDouble", _types.Object)); - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Convert, "ToDouble", _types.Object)); - il.Emit(OpCodes.Add); - il.Emit(OpCodes.Box, _types.Double); - il.Emit(OpCodes.Ret); - - il.MarkLabel(undefinedNanLabel); - il.Emit(OpCodes.Ldc_R8, double.NaN); - il.Emit(OpCodes.Box, _types.Double); - il.Emit(OpCodes.Ret); - - // String concat - StringifyCoerce: JS-compatible conversion (null->"null", - // bool->"true"/"false") that throws TypeError for Symbol operands (§7.1.17). - il.MarkLabel(stringConcatLabel); - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Call, runtime.StringifyCoerce); - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Call, runtime.StringifyCoerce); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "Concat", _types.String, _types.String)); - il.Emit(OpCodes.Ret); - } - - /// - /// Declares the Equals MethodBuilder shell. Body fills in via - /// , which must run AFTER EmitToJsString so the - /// Object-vs-String spec branch can reference runtime.ToJsString. - /// - internal void DeclareEquals(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - runtime.Equals = typeBuilder.DefineMethod( - "Equals", - MethodAttributes.Public | MethodAttributes.Static, - _types.Boolean, - [_types.Object, _types.Object] - ); - } - - private void EmitEquals(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = runtime.Equals; - - var il = method.GetILGenerator(); - var trueLabel = il.DefineLabel(); - var falseLabel = il.DefineLabel(); - var checkRightNullish = il.DefineLabel(); - var notBothNullish = il.DefineLabel(); - var objectEqualsLabel = il.DefineLabel(); - var endLabel = il.DefineLabel(); - - // ECMA-262 §7.2.14 step 11/12: when one operand is an Object and the - // other a primitive, IsLooselyEqual delegates to ToPrimitive on the - // Object then re-runs. For boxed-primitive wrappers the spec'd - // OrdinaryToPrimitive lands at __primitiveValue via valueOf, so unwrap - // upfront and let the existing primitive-vs-primitive logic do the - // rest. Plain $Object operands without a __primitiveType marker pass - // through unchanged (UnwrapIfBoxed is a no-op there) and continue to - // the existing Dict/$Object-vs-primitive ToNumber path below, which - // handles `Number.prototype == 0` and similar. - var leftLocal = il.DeclareLocal(_types.Object); - var rightLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.UnwrapIfBoxedMethod); - il.Emit(OpCodes.Stloc, leftLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.UnwrapIfBoxedMethod); - il.Emit(OpCodes.Stloc, rightLocal); - - // Local to track if left is nullish - var leftNullish = il.DeclareLocal(_types.Boolean); - var rightNullish = il.DeclareLocal(_types.Boolean); - - // Check if left is nullish (null or undefined) - // leftNullish = (left == null || left is SharpTSUndefined) - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Brfalse_S, checkRightNullish); // left is null - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Cgt_Un); // true if left is SharpTSUndefined - il.Emit(OpCodes.Stloc, leftNullish); - il.Emit(OpCodes.Br_S, notBothNullish); - - il.MarkLabel(checkRightNullish); - // Left is null - mark as nullish - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Stloc, leftNullish); - - il.MarkLabel(notBothNullish); - - // Check if right is nullish (null or undefined) - // rightNullish = (right == null || right is SharpTSUndefined) - var rightNotNull = il.DefineLabel(); - var afterRightCheck = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Brtrue_S, rightNotNull); - // Right is null - mark as nullish - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Stloc, rightNullish); - il.Emit(OpCodes.Br_S, afterRightCheck); - - il.MarkLabel(rightNotNull); - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Cgt_Un); // true if right is SharpTSUndefined - il.Emit(OpCodes.Stloc, rightNullish); - - il.MarkLabel(afterRightCheck); - - // If both are nullish, return true (null == undefined) - il.Emit(OpCodes.Ldloc, leftNullish); - il.Emit(OpCodes.Ldloc, rightNullish); - il.Emit(OpCodes.And); - il.Emit(OpCodes.Brtrue, trueLabel); - - // If only one is nullish, return false - il.Emit(OpCodes.Ldloc, leftNullish); - il.Emit(OpCodes.Ldloc, rightNullish); - il.Emit(OpCodes.Or); - il.Emit(OpCodes.Brtrue, falseLabel); - - // ECMA-262 7.2.14 IsLooselyEqual: when one side is a Dictionary/$Object - // and the other is a String, Number, or Boolean, the spec calls - // ToPrimitive(object) then recursively compares. Two cases for the - // primitive type matter: - // - String: compare as strings (ToJsString fires the same ToPrimitive - // valueOf/toString chain ToNumber would, but yields a string — - // `new String("one") == "one"` returns true because - // ToPrimitive(wrapper) is "one", then "one" === "one"). - // - Number/Boolean: compare as numbers (ToNumber on both sides; - // ToNumber(boolean)=0/1 per spec; ToNumber on the object does - // ToPrimitive(hint number) then ToNumber). - // Without the String split, `wrapper == "non-numeric"` was always - // false because ToNumber("non-numeric")=NaN and NaN!==NaN. - - // If LEFT is Dict/$Object and RIGHT is double/string/bool → coerce LEFT. - var notLeftCoercibleLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - var leftIsDictLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, leftIsDictLabel); - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brfalse, notLeftCoercibleLabel); - il.MarkLabel(leftIsDictLabel); - // Right is String → ToJsString(LEFT) and string-compare. - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Isinst, _types.String); - var leftObjVsStringLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, leftObjVsStringLabel); - // Right is double/bool → ToNumber both and Ceq. - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Isinst, _types.Double); - var leftObjVsNumLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, leftObjVsNumLabel); - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Isinst, _types.Boolean); - il.Emit(OpCodes.Brfalse, notLeftCoercibleLabel); - il.MarkLabel(leftObjVsNumLabel); - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Call, runtime.ToNumber); - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Call, runtime.ToNumber); - il.Emit(OpCodes.Ceq); - il.Emit(OpCodes.Br, endLabel); - // Object-vs-String: ToJsString(LEFT) and string-compare via op_Equality. - il.MarkLabel(leftObjVsStringLabel); - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Call, runtime.ToJsString); - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Call, _types.StringOpEquality); - il.Emit(OpCodes.Br, endLabel); - il.MarkLabel(notLeftCoercibleLabel); - - // Symmetric: RIGHT is Dict/$Object and LEFT is primitive. - var notRightCoercibleLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - var rightIsDictLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, rightIsDictLabel); - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brfalse, notRightCoercibleLabel); - il.MarkLabel(rightIsDictLabel); - // Left is String → ToJsString(RIGHT) and string-compare. - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Isinst, _types.String); - var rightObjVsStringLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, rightObjVsStringLabel); - // Left is double/bool → ToNumber both and Ceq. - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Isinst, _types.Double); - var rightObjVsNumLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, rightObjVsNumLabel); - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Isinst, _types.Boolean); - il.Emit(OpCodes.Brfalse, notRightCoercibleLabel); - il.MarkLabel(rightObjVsNumLabel); - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Call, runtime.ToNumber); - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Call, runtime.ToNumber); - il.Emit(OpCodes.Ceq); - il.Emit(OpCodes.Br, endLabel); - // String-vs-Object: ToJsString(RIGHT) and string-compare. - il.MarkLabel(rightObjVsStringLabel); - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Call, runtime.ToJsString); - il.Emit(OpCodes.Call, _types.StringOpEquality); - il.Emit(OpCodes.Br, endLabel); - il.MarkLabel(notRightCoercibleLabel); - - // Neither is nullish - use object.Equals - il.Emit(OpCodes.Ldloc, leftLocal); - il.Emit(OpCodes.Ldloc, rightLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Object, "Equals", _types.Object, _types.Object)); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(trueLabel); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(falseLabel); - il.Emit(OpCodes.Ldc_I4_0); - - il.MarkLabel(endLabel); - il.Emit(OpCodes.Ret); - } - - private void EmitStrictEquals(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - // ECMA-262 IsStrictlyEqual semantics: null/undefined are distinct values - // (unlike loose ==). Used by Array.prototype.indexOf/lastIndexOf/includes, - // which all forbid null/undefined unification per spec. - var method = typeBuilder.DefineMethod( - "StrictEquals", - MethodAttributes.Public | MethodAttributes.Static, - _types.Boolean, - [_types.Object, _types.Object] - ); - runtime.StrictEquals = method; - - var il = method.GetILGenerator(); - var trueLabel = il.DefineLabel(); - var falseLabel = il.DefineLabel(); - var endLabel = il.DefineLabel(); - - var leftIsNull = il.DefineLabel(); - var leftNotUndef = il.DefineLabel(); - - // If left is CLR null → match iff right is CLR null. - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Brfalse, leftIsNull); - - // If left is $Undefined → match iff right is $Undefined. - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brfalse, leftNotUndef); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Cgt_Un); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(leftNotUndef); - // Left is non-null, non-undefined. If right is null or undefined → false. - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Brfalse, falseLabel); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, falseLabel); - - // ECMA-262 IsStrictlyEqual: NaN !== NaN. Object.Equals(NaN, NaN) is true - // in .NET (Double.Equals special-cases NaN as equal to itself), so test - // upfront via double.IsNaN. Pre-fix `[NaN].indexOf(NaN)` returned 0 - // instead of -1. - var notDoubleSEqLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Double); - il.Emit(OpCodes.Brfalse, notDoubleSEqLabel); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Isinst, _types.Double); - il.Emit(OpCodes.Brfalse, notDoubleSEqLabel); - // Both are double — if either is NaN, return false. - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, _types.Double); - il.Emit(OpCodes.Call, _types.Double.GetMethod("IsNaN", [_types.Double])!); - il.Emit(OpCodes.Brtrue, falseLabel); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Unbox_Any, _types.Double); - il.Emit(OpCodes.Call, _types.Double.GetMethod("IsNaN", [_types.Double])!); - il.Emit(OpCodes.Brtrue, falseLabel); - il.MarkLabel(notDoubleSEqLabel); - - // Both are concrete values — defer to Object.Equals (handles double, - // string, reference equality for objects). - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Object, "Equals", _types.Object, _types.Object)); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(leftIsNull); - // Left is CLR null. Match iff right is also CLR null (NOT $Undefined). - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Ceq); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(trueLabel); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Br, endLabel); - - il.MarkLabel(falseLabel); - il.Emit(OpCodes.Ldc_I4_0); - - il.MarkLabel(endLabel); - il.Emit(OpCodes.Ret); - } -} - diff --git a/Compilation/RuntimeEmitter.Objects.DeleteProperty.cs b/Compilation/RuntimeEmitter.Objects.DeleteProperty.cs new file mode 100644 index 00000000..cfd93292 --- /dev/null +++ b/Compilation/RuntimeEmitter.Objects.DeleteProperty.cs @@ -0,0 +1,487 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Runtime.BuiltIns; +using SharpTS.Runtime.Types; + +namespace SharpTS.Compilation; + +// Split out of RuntimeEmitter.Objects.Properties.cs (#1141): the delete-property emitters (sloppy + strict). +public partial class RuntimeEmitter +{ + /// + /// Emits DeleteProperty(object obj, string name) -> bool + /// Removes a property from an object and returns true if successful. + /// Returns false for frozen/sealed objects or if the object doesn't support deletion. + /// + private void EmitDeleteProperty(TypeBuilder typeBuilder, EmittedRuntime runtime) + => EmitDeletePropertyCore(typeBuilder, runtime, strict: false); + /// + /// Emits DeleteProperty(object obj, string name) -> bool (non-strict) or + /// DeletePropertyStrict(object obj, string name, bool strictMode) -> bool. + /// On a failed delete (frozen/sealed object, non-configurable property) + /// the non-strict variant returns false; the strict variant throws a + /// TypeError when strictMode is set, else returns false. + /// The strict variant intentionally has NO $Array or System.Type receiver + /// branches, and its $TSFunction handler skips the frozen/sealed/PDS + /// configurability checks — preserved as-is from before the #1131 merge + /// (behavior-preserving refactor; see the epic notes for the drift list). + /// + private void EmitDeletePropertyCore(TypeBuilder typeBuilder, EmittedRuntime runtime, bool strict) + { + var method = typeBuilder.DefineMethod( + strict ? "DeletePropertyStrict" : "DeleteProperty", + MethodAttributes.Public | MethodAttributes.Static, + _types.Boolean, + strict ? [_types.Object, _types.String, _types.Boolean] : [_types.Object, _types.String] + ); + if (strict) + runtime.DeletePropertyStrict = method; + else + runtime.DeleteProperty = method; + + var il = method.GetILGenerator(); + var nullLabel = il.DefineLabel(); + var dictLabel = il.DefineLabel(); + var trueLabel = il.DefineLabel(); + + // Emits the failed-delete path: strict mode (arg 2 set) throws + // TypeError("Cannot delete property ''"), otherwise + // (non-strict variant, or strictMode == false) returns false. + void EmitDeleteFail(string suffix) + { + if (strict) + { + var sloppyLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Brfalse, sloppyLabel); + EmitThrowTypeErrorWithName(il, runtime, "Cannot delete property '", suffix); + il.MarkLabel(sloppyLabel); + } + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + } + + // null check - return true (deleting from null is allowed in JS) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brfalse, trueLabel); + + // Proxy check: uses obj.GetType().FullName comparison (no SharpTS.dll dependency) + var notProxyLabel = il.DefineLabel(); + EmitProxyDeleteCheck(il, () => il.Emit(OpCodes.Ldarg_0), () => il.Emit(OpCodes.Ldarg_1), notProxyLabel); + + il.MarkLabel(notProxyLabel); + + // Check if $TSObject + var sharpTSObjectLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brtrue, sharpTSObjectLabel); + + // $TSFunction — `delete fn.name` records in the per-instance set so + // the synthetic name/length descriptors stop reporting (ECMA-262 §17 + // configurable). Pre-fix this fell through to trueLabel without + // recording, so verifyProperty's isConfigurable failed. + var tsFunctionDelLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSFunctionType); + il.Emit(OpCodes.Brtrue, tsFunctionDelLabel); + + // $Array — `delete arr[i]` turns the slot into a hole. Must come + // BEFORE the Dictionary check (not relevant here, just ordering) + // and BEFORE the trueLabel fallthrough so actual deletions happen. + // (Non-strict only — the strict variant never had an $Array branch.) + var tsArrayDelLabel = il.DefineLabel(); + if (!strict) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSArrayType); + il.Emit(OpCodes.Brtrue, tsArrayDelLabel); + } + + // Dictionary + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, dictLabel); + + if (!strict) + { + // System.Type — `delete String.prototype` / `delete Number.MAX_VALUE`. + // Per ECMA-262 §17 + §22.x: built-in constructor's "prototype" data + // property is { writable:false, enumerable:false, configurable:false }; + // static constants likewise non-configurable. [[Delete]] returns false + // on non-configurable. Test262 S15.5.3.1_A3 verifies. PDS check first + // for user-installed override-descriptors with configurable=true. + // (Non-strict only — the strict variant never had a Type branch.) + var notTypeForDelLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Type); + il.Emit(OpCodes.Brfalse, notTypeForDelLabel); + var typeDelDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); + il.Emit(OpCodes.Stloc, typeDelDescLocal); + var typeNoPdsDescLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, typeDelDescLocal); + il.Emit(OpCodes.Brfalse, typeNoPdsDescLabel); + il.Emit(OpCodes.Ldloc, typeDelDescLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorConfigurable.GetGetMethod()!); + var typeConfigurableLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, typeConfigurableLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + il.MarkLabel(typeConfigurableLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSDeleteProperty); + il.Emit(OpCodes.Pop); + // Also mark in the per-Type deletion tracker so the static-names list + // check in HasOwnPropertyHelper / gOPD doesn't resurrect this name. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.MarkBuiltinDeletedMethod); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + il.MarkLabel(typeNoPdsDescLabel); + // "prototype"/"name"/"length" are non-configurable on every built-in. + var typeBuiltinNameTrueLabel = il.DefineLabel(); + void EmitTypeBuiltinNameCheck(string n) + { + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, n); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brtrue, typeBuiltinNameTrueLabel); + } + EmitTypeBuiltinNameCheck("prototype"); + EmitTypeBuiltinNameCheck("name"); + EmitTypeBuiltinNameCheck("length"); + + // Number Type-specific non-configurable constants. Reflection + // probe below would miss these because JS names (UPPER_SNAKE_CASE) + // differ from .NET names (PascalCase): MAX_VALUE → double.MaxValue + // etc. Without this, `delete Number.MAX_VALUE` returned true + // (Test262 S15.7.3.2_A3). + var notNumberTypeForDelLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldtoken, _types.Double); + il.Emit(OpCodes.Call, _types.Type.GetMethod("GetTypeFromHandle")!); + il.Emit(OpCodes.Bne_Un, notNumberTypeForDelLabel); + void EmitNumberConstNameCheck(string n) + { + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, n); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brtrue, typeBuiltinNameTrueLabel); + } + EmitNumberConstNameCheck("MAX_VALUE"); + EmitNumberConstNameCheck("MIN_VALUE"); + EmitNumberConstNameCheck("NaN"); + EmitNumberConstNameCheck("POSITIVE_INFINITY"); + EmitNumberConstNameCheck("NEGATIVE_INFINITY"); + EmitNumberConstNameCheck("MAX_SAFE_INTEGER"); + EmitNumberConstNameCheck("MIN_SAFE_INTEGER"); + EmitNumberConstNameCheck("EPSILON"); + il.MarkLabel(notNumberTypeForDelLabel); + // Object/Array/String constructor static method names: per ECMA-262 + // §17, every other data property has configurable:true. Mark the + // deletion in the per-Type tracker so subsequent gOPD/hasOwn report + // the property as absent, then return true. (prototype/name/length + // and Number constants caught above are non-configurable.) + var objTypeDelLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldtoken, _types.Object); + il.Emit(OpCodes.Call, _types.Type.GetMethod("GetTypeFromHandle")!); + il.Emit(OpCodes.Bne_Un, objTypeDelLabel); + void EmitObjectMethodDelCheck(string n) + { + var skipLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, n); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, skipLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.MarkBuiltinDeletedMethod); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + il.MarkLabel(skipLabel); + } + EmitObjectMethodDelCheck("assign"); EmitObjectMethodDelCheck("create"); + EmitObjectMethodDelCheck("defineProperties"); EmitObjectMethodDelCheck("defineProperty"); + EmitObjectMethodDelCheck("entries"); EmitObjectMethodDelCheck("freeze"); + EmitObjectMethodDelCheck("fromEntries"); EmitObjectMethodDelCheck("getOwnPropertyDescriptor"); + EmitObjectMethodDelCheck("getOwnPropertyDescriptors"); EmitObjectMethodDelCheck("getOwnPropertyNames"); + EmitObjectMethodDelCheck("getOwnPropertySymbols"); EmitObjectMethodDelCheck("getPrototypeOf"); + EmitObjectMethodDelCheck("groupBy"); EmitObjectMethodDelCheck("hasOwn"); EmitObjectMethodDelCheck("is"); + EmitObjectMethodDelCheck("isExtensible"); EmitObjectMethodDelCheck("isFrozen"); + EmitObjectMethodDelCheck("isSealed"); EmitObjectMethodDelCheck("keys"); + EmitObjectMethodDelCheck("preventExtensions"); EmitObjectMethodDelCheck("seal"); + EmitObjectMethodDelCheck("setPrototypeOf"); EmitObjectMethodDelCheck("values"); + il.MarkLabel(objTypeDelLabel); + + // Reflection: any static field/property on the Type → built-in own. + const System.Reflection.BindingFlags typeDelStaticPub = + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static; + var typeDelLocal = il.DeclareLocal(_types.Type); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.Type); + il.Emit(OpCodes.Stloc, typeDelLocal); + il.Emit(OpCodes.Ldloc, typeDelLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldc_I4, (int)typeDelStaticPub); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetField", _types.String, typeof(System.Reflection.BindingFlags))); + il.Emit(OpCodes.Brtrue, typeBuiltinNameTrueLabel); + il.Emit(OpCodes.Ldloc, typeDelLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldc_I4, (int)typeDelStaticPub); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetProperty", _types.String, typeof(System.Reflection.BindingFlags))); + il.Emit(OpCodes.Brtrue, typeBuiltinNameTrueLabel); + // Not a built-in own property — return true (delete-missing = success). + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + il.MarkLabel(typeBuiltinNameTrueLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + il.MarkLabel(notTypeForDelLabel); + } + + // Other types - cannot delete properties, return true (JS behavior for non-configurable) + il.Emit(OpCodes.Br, trueLabel); + + // $TSFunction delete handler. + il.MarkLabel(tsFunctionDelLabel); + if (strict) + { + // Strict variant: record the deletion only — it intentionally skips + // the frozen/sealed/PDS configurability checks the non-strict + // handler performs (preserved pre-#1131 behavior). + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.MarkBuiltinDeletedMethod); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + } + else + { + // Honor configurability: + // 1. If frozen or sealed (via CWT), return false (silent no-op). + // 2. If a PDS descriptor exists with configurable=false, return false + // without removing. + // 3. Otherwise: clean up PDS, then mark as deleted in the per-instance + // tracker so the synthetic descriptor stops reporting and direct + // property lookups return undefined. + // Frozen check. + var tsFnDelTmp = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloca, tsFnDelTmp); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); + var tsFnNotFrozenLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, tsFnNotFrozenLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + + // Sealed check. + il.MarkLabel(tsFnNotFrozenLabel); + il.Emit(OpCodes.Ldsfld, runtime.SealedObjectsField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloca, tsFnDelTmp); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); + var tsFnNotSealedLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, tsFnNotSealedLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + + // PDS configurable check. + il.MarkLabel(tsFnNotSealedLabel); + var tsFnDelDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); + il.Emit(OpCodes.Stloc, tsFnDelDescLocal); + var tsFnNoPdsLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, tsFnDelDescLocal); + il.Emit(OpCodes.Brfalse, tsFnNoPdsLabel); + il.Emit(OpCodes.Ldloc, tsFnDelDescLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorConfigurable.GetGetMethod()!); + var tsFnDelConfigurableLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, tsFnDelConfigurableLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + il.MarkLabel(tsFnDelConfigurableLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSDeleteProperty); + il.Emit(OpCodes.Pop); + il.MarkLabel(tsFnNoPdsLabel); + + // Mark as deleted in per-instance tracker (covers synthetic + // name/length and any other descriptor-less data entry). + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.MarkBuiltinDeletedMethod); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + } + + // $Array delete handler — if the key is a numeric index call + // DeleteAt; otherwise return true (JS non-configurable behavior). + if (!strict) + { + il.MarkLabel(tsArrayDelLabel); + var tsArrDelIndexLocal = il.DeclareLocal(_types.Int64); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloca, tsArrDelIndexLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Int64, "TryParse", _types.String, _types.Int64.MakeByRefType())); + var tsArrDelNonNumericLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, tsArrDelNonNumericLabel); + + // arr.DeleteAt(idx); return true; + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSArrayType); + il.Emit(OpCodes.Ldloc, tsArrDelIndexLocal); + il.Emit(OpCodes.Callvirt, runtime.TSArrayDeleteAt); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + + il.MarkLabel(tsArrDelNonNumericLabel); + // Non-numeric key. PDS-installed named property: honor frozen + + // descriptor.configurable (mirrors the Dict path's behavior). + // Pre-fix returned true unconditionally, allowing `delete arr.foo` + // to silently succeed even when `Object.freeze(arr)` made the + // property non-configurable. + var tsArrDelFrozenLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.PDSIsFrozen); + il.Emit(OpCodes.Brfalse, tsArrDelFrozenLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + il.MarkLabel(tsArrDelFrozenLabel); + var tsArrDelSealedLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.PDSIsSealed); + il.Emit(OpCodes.Brfalse, tsArrDelSealedLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + il.MarkLabel(tsArrDelSealedLabel); + // Check PDS descriptor configurable. + var tsArrDelDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); + il.Emit(OpCodes.Stloc, tsArrDelDescLocal); + var tsArrDelNoDescLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, tsArrDelDescLocal); + il.Emit(OpCodes.Brfalse, tsArrDelNoDescLabel); + il.Emit(OpCodes.Ldloc, tsArrDelDescLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorConfigurable.GetGetMethod()!); + var tsArrDelConfigurableLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, tsArrDelConfigurableLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + il.MarkLabel(tsArrDelConfigurableLabel); + // Configurable — PDS remove + return true. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSDeleteProperty); + il.Emit(OpCodes.Pop); + il.MarkLabel(tsArrDelNoDescLabel); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + } + + // $TSObject - call the DeleteProperty / DeletePropertyStrict instance method + il.MarkLabel(sharpTSObjectLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSObjectType); + il.Emit(OpCodes.Ldarg_1); + if (strict) + { + il.Emit(OpCodes.Ldarg_2); // strictMode + il.Emit(OpCodes.Callvirt, runtime.TSObjectDeletePropertyStrict); + } + else + { + il.Emit(OpCodes.Callvirt, runtime.TSObjectDeleteProperty); + } + il.Emit(OpCodes.Ret); + + // Dictionary - use Remove, honoring frozen/sealed and PDS configurability. + il.MarkLabel(dictLabel); + var valueLocal = il.DeclareLocal(_types.Object); + + // Check if frozen + il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloca, valueLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); + var notFrozenLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notFrozenLabel); + + // Frozen - fail (strict throws / sloppy returns false) + EmitDeleteFail("' of a frozen object"); + + // Check if sealed + il.MarkLabel(notFrozenLabel); + il.Emit(OpCodes.Ldsfld, runtime.SealedObjectsField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloca, valueLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); + var notSealedLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notSealedLabel); + + // Sealed - fail (strict throws / sloppy returns false) + EmitDeleteFail("' of a sealed object"); + + // Not frozen/sealed — remove from BOTH the dict (default data entries) + // AND the PDS descriptor store (Object.defineProperty installs). When + // a PDS descriptor is present and non-configurable, delete fails per + // ECMA-262 §10.1.10 without removing: strict throws TypeError + // (§13.5.1.2), sloppy returns false. + il.MarkLabel(notSealedLabel); + var descLocalDel = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); + il.Emit(OpCodes.Stloc, descLocalDel); + var noPdsForDelLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, descLocalDel); + il.Emit(OpCodes.Brfalse, noPdsForDelLabel); + // Descriptor present — check Configurable. + il.Emit(OpCodes.Ldloc, descLocalDel); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorConfigurable.GetGetMethod()!); + var configurableLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, configurableLabel); + // Non-configurable — fail without removing. + EmitDeleteFail("' of object"); + il.MarkLabel(configurableLabel); + // Configurable — remove PDS entry. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSDeleteProperty); + il.Emit(OpCodes.Pop); + il.MarkLabel(noPdsForDelLabel); + + // Always also try to remove from the dict (the property may be a + // plain data entry without a PDS descriptor). Dictionary.Remove + // returns false when the key isn't present, which is fine. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "Remove", _types.String)); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + + // Return true (default for null and other types) + il.MarkLabel(trueLabel); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + + il.MarkLabel(nullLabel); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + } + private void EmitDeletePropertyStrict(TypeBuilder typeBuilder, EmittedRuntime runtime) + => EmitDeletePropertyCore(typeBuilder, runtime, strict: true); +} diff --git a/Compilation/RuntimeEmitter.Objects.Properties.cs b/Compilation/RuntimeEmitter.Objects.Properties.cs index 7b55e828..f4940180 100644 --- a/Compilation/RuntimeEmitter.Objects.Properties.cs +++ b/Compilation/RuntimeEmitter.Objects.Properties.cs @@ -660,1103 +660,616 @@ private void EmitGetFieldsProperty(TypeBuilder typeBuilder, EmittedRuntime runti il.Emit(OpCodes.Ret); } - private void EmitSetFieldsProperty(TypeBuilder typeBuilder, EmittedRuntime runtime) + private void EmitGetListProperty(TypeBuilder typeBuilder, EmittedRuntime runtime) { - // SetFieldsProperty(object obj, string name, object value) -> void - // Updates class-instance state through emitted runtime state only: - // descriptor store checks and emitted $Object fields. + // GetListProperty(list: List, name: string) -> object? + // Returns length as double, or a $BoundArrayMethod for array methods var method = typeBuilder.DefineMethod( - "SetFieldsProperty", + "GetListProperty", MethodAttributes.Public | MethodAttributes.Static, - _types.Void, - [_types.Object, _types.String, _types.Object] + _types.Object, + [_types.ListOfObject, _types.String] ); - runtime.SetFieldsProperty = method; + runtime.GetListProperty = method; var il = method.GetILGenerator(); - var endLabel = il.DefineLabel(); - var tryFieldsLabel = il.DefineLabel(); - var trySetterLabel = il.DefineLabel(); - - // Declare locals upfront - var dictLocal = il.DeclareLocal(_types.DictionaryStringObject); - var frozenCheckLocal = il.DeclareLocal(_types.Object); - - // if (obj == null) return; - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Brfalse, endLabel); - // Check if frozen: _frozenObjects.TryGetValue(obj, out _) - // If frozen, silently return (non-strict mode behavior) - il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloca, frozenCheckLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); - il.Emit(OpCodes.Brtrue, endLabel); // Frozen - silently return + var lengthLabel = il.DefineLabel(); + var returnNullLabel = il.DefineLabel(); + var createBoundMethodLabel = il.DefineLabel(); - // No additional setter fallback in standalone mode. - il.MarkLabel(trySetterLabel); + // if (name == "length") goto lengthLabel + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "length"); + il.Emit(OpCodes.Call, _types.StringOpEquality); + il.Emit(OpCodes.Brtrue, lengthLabel); - // Try _fields dictionary - walk up type hierarchy to find non-null _fields - il.MarkLabel(tryFieldsLabel); - var notTSObjectLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brfalse, notTSObjectLabel); + // if (name == "raw") - check for TemplateStringsList.raw property + var rawLabel = il.DefineLabel(); + var skipRawLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "raw"); + il.Emit(OpCodes.Call, _types.StringOpEquality); + il.Emit(OpCodes.Brtrue, rawLabel); + il.Emit(OpCodes.Br, skipRawLabel); + il.MarkLabel(rawLabel); + // For tagged template arrays, call emitted TemplateStringsList.raw getter. il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSObjectType); - il.Emit(OpCodes.Callvirt, runtime.TSObjectFieldsGetter); - il.Emit(OpCodes.Stloc, dictLocal); - il.Emit(OpCodes.Ldloc, dictLocal); - il.Emit(OpCodes.Brfalse, endLabel); - - // Found a non-null _fields dictionary - // Check if sealed: _sealedObjects.TryGetValue(obj, out _) - var doSetFieldLabel = il.DefineLabel(); - var checkExtensibilityLabel = il.DefineLabel(); - var sealedCheckLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldsfld, runtime.SealedObjectsField); + il.Emit(OpCodes.Isinst, runtime.TemplateStringsListType); + il.Emit(OpCodes.Brfalse, returnNullLabel); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloca, sealedCheckLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); - il.Emit(OpCodes.Brfalse, checkExtensibilityLabel); // Not sealed, check extensibility + il.Emit(OpCodes.Castclass, runtime.TemplateStringsListType); + il.Emit(OpCodes.Callvirt, runtime.TemplateStringsListRawGetter); + il.Emit(OpCodes.Ret); - // Object is sealed - check if property exists in dictionary - il.Emit(OpCodes.Ldloc, dictLocal); - il.Emit(OpCodes.Ldarg_1); // name - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); - il.Emit(OpCodes.Brfalse, endLabel); // Property doesn't exist on sealed object, silently return - il.Emit(OpCodes.Br, doSetFieldLabel); // Property exists, proceed to set + il.MarkLabel(skipRawLabel); - // Not sealed - check extensibility via $PropertyDescriptorStore - fully standalone, no reflection - il.MarkLabel(checkExtensibilityLabel); - il.Emit(OpCodes.Ldarg_0); // obj - il.Emit(OpCodes.Ldarg_1); // name - il.Emit(OpCodes.Call, runtime.PDSCanAddProperty); - il.Emit(OpCodes.Brfalse, endLabel); // Cannot add property, silently return + // Check for known array method names + // For each method name, if match, create $BoundArrayMethod + // Must stay in sync with EmitBoundArrayMethodFinalize's dispatch switch + // (RuntimeEmitter.Arrays.cs) and ArrayEmitter.cs static dispatch. + string[] methodNames = [ + "join", "push", "pop", "shift", "unshift", "slice", "splice", + "indexOf", "lastIndexOf", "includes", "concat", "reverse", "sort", "map", "filter", "forEach", + "find", "findIndex", "findLast", "findLastIndex", "some", "every", + "reduce", "reduceRight", + "flat", "flatMap", "at", + "toSorted", "toSpliced", "toReversed", "with", + "fill", "copyWithin", + "entries", "keys", "values", + "toString", "toLocaleString" + ]; - // Set the value: dict[name] = value; - il.MarkLabel(doSetFieldLabel); - il.Emit(OpCodes.Ldloc, dictLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "set_Item")); - il.Emit(OpCodes.Ret); + foreach (var methodName in methodNames) + { + var skipLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, methodName); + il.Emit(OpCodes.Call, _types.StringOpEquality); + il.Emit(OpCodes.Brfalse, skipLabel); - il.MarkLabel(notTSObjectLabel); + // Create $BoundArrayMethod(list, name) and return + il.Emit(OpCodes.Ldarg_0); // list + il.Emit(OpCodes.Ldarg_1); // name + il.Emit(OpCodes.Newobj, runtime.BoundArrayMethodCtor); + il.Emit(OpCodes.Ret); - // Check $IHasFields interface (covers user-defined classes) - var notHasFieldsLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.IHasFieldsInterface); - il.Emit(OpCodes.Brfalse, notHasFieldsLabel); + il.MarkLabel(skipLabel); + } - // Check extensibility before setting (handles sealed/non-extensible objects) - il.Emit(OpCodes.Ldarg_0); // obj + // No known array method match — check PropertyDescriptorStore for custom defined properties + il.MarkLabel(returnNullLabel); + var reallyNullLabel = il.DefineLabel(); + var pdsDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Ldarg_0); // list (as object) il.Emit(OpCodes.Ldarg_1); // name - il.Emit(OpCodes.Call, runtime.PDSCanAddProperty); - il.Emit(OpCodes.Brfalse, endLabel); // Cannot add property, silently return - - // Call interface method: ((IHasFields)obj).SetProperty(name, value) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.IHasFieldsInterface); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Callvirt, runtime.IHasFieldsSetProperty); + il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); + il.Emit(OpCodes.Stloc, pdsDescLocal); + il.Emit(OpCodes.Ldloc, pdsDescLocal); + il.Emit(OpCodes.Brfalse, reallyNullLabel); + // Return descriptor.Value + il.Emit(OpCodes.Ldloc, pdsDescLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorValue.GetGetMethod()!); il.Emit(OpCodes.Ret); - il.MarkLabel(notHasFieldsLabel); - - // Check $Error - handle name, message, stack properties - var notErrorLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSErrorType); - il.Emit(OpCodes.Brfalse, notErrorLabel); - - // Check "name" - var notErrorNameLabel = il.DefineLabel(); + // Final fallback: walk Array.prototype, then Object.prototype singleton + // dicts. ECMA-262 says a List receiver inherits from %Array.prototype% + // which inherits from %Object.prototype%, so user-added entries + // (`Array.prototype.foo = 1`) reach indexed-access reads as + // `arr.foo === 1`, and Object.prototype methods like hasOwnProperty, + // toString, valueOf flow through too. Populate both prototype dicts + // if not yet populated. + il.MarkLabel(reallyNullLabel); + var arrayProtoFallbackLabel = il.DefineLabel(); + var objectProtoFallbackLabel = il.DefineLabel(); + il.Emit(OpCodes.Call, runtime.ArrayPrototypePopulateMethod); + var arrayProtoValLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldsfld, runtime.ArrayPrototypeField); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "name"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notErrorNameLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSErrorType); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Callvirt, runtime.TSErrorNameSetter); + il.Emit(OpCodes.Ldloca, arrayProtoValLocal); + il.Emit(OpCodes.Callvirt, _types.DictionaryStringObject.GetMethod("TryGetValue", + [_types.String, _types.Object.MakeByRefType()])!); + il.Emit(OpCodes.Brfalse, arrayProtoFallbackLabel); + il.Emit(OpCodes.Ldloc, arrayProtoValLocal); il.Emit(OpCodes.Ret); - il.MarkLabel(notErrorNameLabel); - - // Check "message" - var notErrorMessageLabel = il.DefineLabel(); + il.MarkLabel(arrayProtoFallbackLabel); + // Walk to Object.prototype for shared methods like hasOwnProperty. + il.Emit(OpCodes.Call, runtime.ObjectPrototypePopulateMethod); + il.Emit(OpCodes.Ldsfld, runtime.ObjectPrototypeField); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "message"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notErrorMessageLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSErrorType); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Callvirt, runtime.TSErrorMessageSetter); + il.Emit(OpCodes.Ldloca, arrayProtoValLocal); + il.Emit(OpCodes.Callvirt, _types.DictionaryStringObject.GetMethod("TryGetValue", + [_types.String, _types.Object.MakeByRefType()])!); + il.Emit(OpCodes.Brfalse, objectProtoFallbackLabel); + il.Emit(OpCodes.Ldloc, arrayProtoValLocal); il.Emit(OpCodes.Ret); - il.MarkLabel(notErrorMessageLabel); - - // Check "stack" - var notErrorStackLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "stack"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notErrorStackLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSErrorType); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Callvirt, runtime.TSErrorStackSetter); + il.MarkLabel(objectProtoFallbackLabel); + // Missing-property reads should return JS undefined, not C# null — + // tests assert `arr.foo === undefined` not `arr.foo === null`. + il.Emit(OpCodes.Ldsfld, runtime.UndefinedInstance); il.Emit(OpCodes.Ret); - il.MarkLabel(notErrorStackLabel); - // Check "code" - var notErrorCodeSetLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "code"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notErrorCodeSetLabel); + // length case: return (double)list.Count — except for $Arguments, + // which exposes its own _length field (sloppy arguments objects + // don't auto-update length on out-of-range indexed writes per + // ECMA-262 10.4.4). + il.MarkLabel(lengthLabel); + var notArgumentsLengthLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSErrorType); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Callvirt, runtime.TSErrorCodeSetter); + il.Emit(OpCodes.Isinst, runtime.ArgumentsType); + il.Emit(OpCodes.Brfalse, notArgumentsLengthLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.ArgumentsType); + il.Emit(OpCodes.Ldfld, runtime.ArgumentsLengthField); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Box, _types.Double); il.Emit(OpCodes.Ret); - il.MarkLabel(notErrorCodeSetLabel); - - // Check "syscall" - var notErrorSyscallSetLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "syscall"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notErrorSyscallSetLabel); + il.MarkLabel(notArgumentsLengthLabel); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSErrorType); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Callvirt, runtime.TSErrorSyscallSetter); + il.Emit(OpCodes.Callvirt, _types.GetPropertyGetter(_types.ListOfObject, "Count")); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Box, _types.Double); il.Emit(OpCodes.Ret); - il.MarkLabel(notErrorSyscallSetLabel); - - il.MarkLabel(notErrorLabel); + } - // Scoped PDS-data-store fallback for ECMA built-ins: $TSDate, $TSRegExp, - // $TSPromise, $TSError. JS allows ad-hoc property assignment on these - // instances (`d = new Date(); d.foo = 1; d.foo === 1`); the value lands - // in PDS so GetFieldsProperty's PDS-data-descriptor arm reads it back. - // Limited to these types so user-defined class instances and runtime- - // side types (which may rely on silent-no-op semantics for unknown - // writes — e.g., the Debug npm package) are not affected. - // Note: $TSError already has explicit name/message/stack/code/syscall - // handlers above and only reaches the PDS path for OTHER property - // names (like `obj.length`, `obj[0]`). - var pdsStoreLabel = il.DefineLabel(); - var afterPdsStoreLabel = il.DefineLabel(); - if (_features.UsesDate) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSDateType); - il.Emit(OpCodes.Brtrue, pdsStoreLabel); - } - if (_features.UsesRegExp) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSRegExpType); - il.Emit(OpCodes.Brtrue, pdsStoreLabel); - } - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSPromiseType); - il.Emit(OpCodes.Brtrue, pdsStoreLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSErrorType); - il.Emit(OpCodes.Brtrue, pdsStoreLabel); - il.Emit(OpCodes.Br, afterPdsStoreLabel); + private void EmitGetMapProperty(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // GetMapProperty(map: Dictionary, name: string) -> object? + // Returns size as double, or a $BoundMapMethod wrapper for known Map methods. + // Mirrors GetListProperty — ensures duck typing works across module boundaries: + // typeof map.get === 'function' and map.get.call(map, k) both work on a Map + // received from another module. + var method = typeBuilder.DefineMethod( + "GetMapProperty", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.DictionaryObjectObject, _types.String] + ); + runtime.GetMapProperty = method; - il.MarkLabel(pdsStoreLabel); - { - var fbDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); - il.Emit(OpCodes.Newobj, runtime.CompiledPropertyDescriptorCtor); - il.Emit(OpCodes.Stloc, fbDescLocal); - il.Emit(OpCodes.Ldloc, fbDescLocal); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorValue.GetSetMethod()!); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloc, fbDescLocal); - il.Emit(OpCodes.Call, runtime.PDSDefineProperty); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ret); - } + var il = method.GetILGenerator(); - il.MarkLabel(afterPdsStoreLabel); + var sizeLabel = il.DefineLabel(); - // Fallback: Try SetMember(string, object) method for types like $HttpResponse - // that expose property setters through their SetMember dispatch method. - var setMemberLocal = il.DeclareLocal(_types.MethodInfo); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "GetType")); - il.Emit(OpCodes.Ldstr, "SetMember"); - il.Emit(OpCodes.Ldc_I4, (int)(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)); - il.Emit(OpCodes.Call, runtime.SafeGetMethod); - il.Emit(OpCodes.Stloc, setMemberLocal); + // if (name == "size") goto sizeLabel + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "size"); + il.Emit(OpCodes.Call, _types.StringOpEquality); + il.Emit(OpCodes.Brtrue, sizeLabel); - il.Emit(OpCodes.Ldloc, setMemberLocal); - il.Emit(OpCodes.Brfalse, endLabel); + // For each known Map method name, return a $BoundMapMethod wrapper. + string[] methodNames = ["get", "set", "has", "delete", "clear", + "keys", "values", "entries", "forEach"]; - // Call SetMember(name, value): methodInfo.Invoke(obj, new object[] { name, value }) - il.Emit(OpCodes.Ldloc, setMemberLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldc_I4_2); - il.Emit(OpCodes.Newarr, _types.Object); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ldarg_1); // name - il.Emit(OpCodes.Stelem_Ref); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ldarg_2); // value - il.Emit(OpCodes.Stelem_Ref); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.MethodInfo, "Invoke", _types.Object, _types.ObjectArray)); - il.Emit(OpCodes.Pop); + foreach (var methodName in methodNames) + { + var skipLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, methodName); + il.Emit(OpCodes.Call, _types.StringOpEquality); + il.Emit(OpCodes.Brfalse, skipLabel); + + il.Emit(OpCodes.Ldarg_0); // map + il.Emit(OpCodes.Ldarg_1); // name + il.Emit(OpCodes.Newobj, runtime.BoundMapMethodCtor); + il.Emit(OpCodes.Ret); + + il.MarkLabel(skipLabel); + } + + // Unknown property: return null + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ret); - il.MarkLabel(endLabel); + // size case: return (double)map.Count + il.MarkLabel(sizeLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.DictionaryObjectObject, "Count").GetGetMethod()!); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Box, _types.Double); il.Emit(OpCodes.Ret); } - /// - /// Emits SetFieldsPropertyStrict(object obj, string name, object value, bool strictMode) -> void - /// In strict mode, throws TypeError for modifications to frozen objects. - /// - private void EmitSetFieldsPropertyStrict(TypeBuilder typeBuilder, EmittedRuntime runtime) + private void EmitGetSetProperty(TypeBuilder typeBuilder, EmittedRuntime runtime) { + // GetSetProperty(set: HashSet, name: string) -> object? + // Returns size as double, or a $BoundSetMethod wrapper for known Set methods + // (including ES2025 set operations). Mirrors GetListProperty / GetMapProperty. var method = typeBuilder.DefineMethod( - "SetFieldsPropertyStrict", + "GetSetProperty", MethodAttributes.Public | MethodAttributes.Static, - _types.Void, - [_types.Object, _types.String, _types.Object, _types.Boolean] + _types.Object, + [_types.HashSetOfObject, _types.String] ); - runtime.SetFieldsPropertyStrict = method; + runtime.GetSetProperty = method; var il = method.GetILGenerator(); - var endLabel = il.DefineLabel(); - var notFrozenLabel = il.DefineLabel(); - var tryFieldsLabel = il.DefineLabel(); - - // Declare locals upfront - var dictLocal = il.DeclareLocal(_types.DictionaryStringObject); - var frozenCheckLocal = il.DeclareLocal(_types.Object); - // if (obj == null) return; - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Brfalse, endLabel); + var sizeLabel = il.DefineLabel(); - // Check if frozen: _frozenObjects.TryGetValue(obj, out _) - il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloca, frozenCheckLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); - il.Emit(OpCodes.Brfalse, notFrozenLabel); + // if (name == "size") goto sizeLabel + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "size"); + il.Emit(OpCodes.Call, _types.StringOpEquality); + il.Emit(OpCodes.Brtrue, sizeLabel); - // Frozen - check strictMode and throw if true - il.Emit(OpCodes.Ldarg_3); // strictMode - var frozenSilentLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, frozenSilentLabel); - EmitThrowTypeErrorWithName(il, runtime, "Cannot assign to read only property '", "' of object"); - il.MarkLabel(frozenSilentLabel); - il.Emit(OpCodes.Ret); // Silently return in non-strict mode + // For each known Set method name, return a $BoundSetMethod wrapper. + string[] methodNames = ["add", "has", "delete", "clear", + "keys", "values", "entries", "forEach", + "union", "intersection", "difference", "symmetricDifference", + "isSubsetOf", "isSupersetOf", "isDisjointFrom"]; - il.MarkLabel(notFrozenLabel); + foreach (var methodName in methodNames) + { + var skipLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, methodName); + il.Emit(OpCodes.Call, _types.StringOpEquality); + il.Emit(OpCodes.Brfalse, skipLabel); - // No reflection setter fallback in standalone mode. + il.Emit(OpCodes.Ldarg_0); // set + il.Emit(OpCodes.Ldarg_1); // name + il.Emit(OpCodes.Newobj, runtime.BoundSetMethodCtor); + il.Emit(OpCodes.Ret); - // Try _fields dictionary - walk up type hierarchy to find non-null _fields - il.MarkLabel(tryFieldsLabel); + il.MarkLabel(skipLabel); + } - var notTSObjectLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brfalse, notTSObjectLabel); + // Unknown property: return null + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ret); + // size case: return (double)set.Count + il.MarkLabel(sizeLabel); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSObjectType); - il.Emit(OpCodes.Callvirt, runtime.TSObjectFieldsGetter); - il.Emit(OpCodes.Stloc, dictLocal); - il.Emit(OpCodes.Ldloc, dictLocal); - il.Emit(OpCodes.Brfalse, endLabel); - - // Found a non-null _fields dictionary - set the value and return - // dict[name] = value; - il.Emit(OpCodes.Ldloc, dictLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "set_Item")); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.HashSetOfObject, "Count").GetGetMethod()!); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Box, _types.Double); il.Emit(OpCodes.Ret); - il.MarkLabel(notTSObjectLabel); + } - // Check $IHasFields interface (covers user-defined classes) - var notHasFieldsLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.IHasFieldsInterface); - il.Emit(OpCodes.Brfalse, notHasFieldsLabel); + private void EmitGetProperty(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // Signature forward-declared by DefineRuntimeClassPhase1. + var method = (MethodBuilder)runtime.GetProperty; - // Check extensibility before setting (handles sealed/non-extensible objects) - il.Emit(OpCodes.Ldarg_0); // obj - il.Emit(OpCodes.Ldarg_1); // name - il.Emit(OpCodes.Call, runtime.PDSCanAddProperty); - il.Emit(OpCodes.Brfalse, endLabel); // Cannot add property, silently return + var il = method.GetILGenerator(); + var nullLabel = il.DefineLabel(); + var namespaceLabel = il.DefineLabel(); + var dictLabel = il.DefineLabel(); + var listLabel = il.DefineLabel(); + var stringLabel = il.DefineLabel(); + // null check il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.IHasFieldsInterface); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Callvirt, runtime.IHasFieldsSetProperty); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notHasFieldsLabel); + il.Emit(OpCodes.Brfalse, nullLabel); - // Check $Error - handle name, message, stack properties - var notErrorLabel = il.DefineLabel(); + // globalThis/global sentinel (#271): a value-position globalThis reads + // properties through GlobalThisGetProperty (user props → built-in + // constructors/singletons), so `root.Object`/`root.Math` resolve to real + // values. Checked first so the bare-object sentinel never falls through to + // the class-instance handler (which would report every member undefined). + var notGlobalThisLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSErrorType); - il.Emit(OpCodes.Brfalse, notErrorLabel); - - var notErrorNameLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldsfld, runtime.GlobalThisSingletonField); + il.Emit(OpCodes.Bne_Un, notGlobalThisLabel); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "name"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notErrorNameLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSErrorType); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Callvirt, runtime.TSErrorNameSetter); + il.Emit(OpCodes.Call, runtime.GlobalThisGetProperty); il.Emit(OpCodes.Ret); - il.MarkLabel(notErrorNameLabel); + il.MarkLabel(notGlobalThisLabel); - var notErrorMessageLabel = il.DefineLabel(); + // __proto__ accessor (ECMA-262 Annex B.2.2.1): obj.__proto__ delegates + // to Object.getPrototypeOf(obj). All object types support this — the + // accessor lives on Object.prototype, but intercepting here avoids + // replicating the dispatch in every object-specific branch. Without + // this, `{}.__proto__` returns undefined and breaks spec idioms. + // + // CAVEAT: ECMA-262 also allows defining an own "__proto__" data + // property that shadows the inherited accessor. JSON.parse creates + // such own data properties (CreateDataProperty). For Dict receivers, + // check ContainsKey first and fall through to the regular dict path + // when the key is present — preserves JSON.parse semantics for the + // `{"__proto__":...}` corner. + var notProtoNameTopLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "message"); + il.Emit(OpCodes.Ldstr, "__proto__"); il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notErrorMessageLabel); + il.Emit(OpCodes.Brfalse, notProtoNameTopLabel); + // Dict + ContainsKey("__proto__") → skip intercept. + var protoDictLocal = il.DeclareLocal(_types.DictionaryStringObject); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSErrorType); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Callvirt, runtime.TSErrorMessageSetter); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Stloc, protoDictLocal); + var noOwnProtoLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, protoDictLocal); + il.Emit(OpCodes.Brfalse, noOwnProtoLabel); + il.Emit(OpCodes.Ldloc, protoDictLocal); + il.Emit(OpCodes.Ldstr, "__proto__"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); + il.Emit(OpCodes.Brtrue, notProtoNameTopLabel); + il.MarkLabel(noOwnProtoLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.ObjectGetPrototypeOf); il.Emit(OpCodes.Ret); - il.MarkLabel(notErrorMessageLabel); + il.MarkLabel(notProtoNameTopLabel); - var notErrorStackLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "stack"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notErrorStackLabel); + // Proxy check: uses obj.GetType().FullName comparison (no SharpTS.dll dependency) + var notProxyLabel = il.DefineLabel(); + EmitProxyGetPropertyCheck(il, () => il.Emit(OpCodes.Ldarg_0), () => il.Emit(OpCodes.Ldarg_1), notProxyLabel); + + il.MarkLabel(notProxyLabel); + + // $TSNamespace - call ns.Get(name) il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSErrorType); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Callvirt, runtime.TSErrorStackSetter); - il.Emit(OpCodes.Ret); - il.MarkLabel(notErrorStackLabel); + il.Emit(OpCodes.Isinst, runtime.TSNamespaceType); + il.Emit(OpCodes.Brtrue, namespaceLabel); - il.MarkLabel(notErrorLabel); + // $Object (with getter/setter support) - call obj.GetProperty(name) + var tsObjectLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brtrue, tsObjectLabel); - // Built-in exotic objects (Date / RegExp / Promise / Error) accept arbitrary - // named property assignment per ECMA-262 [[Set]], stored as PDS data - // descriptors so GetProperty / for-in / gOPD round-trip. Mirrors the - // non-strict SetFieldsProperty PDS-store arm — without it these writes were - // silently dropped under "use strict", so e.g. `var d = new Date(0); - // d.enumerable = true;` (a defineProperty attributes object — Test262 - // Object/defineProperty 15.2.3.6-3-39 et al.) lost the field. Frozen / - // extensibility throws were already handled at the top of this method. - var fieldsPdsStoreLabel = il.DefineLabel(); - var afterFieldsPdsStoreLabel = il.DefineLabel(); - if (_features.UsesDate) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSDateType); - il.Emit(OpCodes.Brtrue, fieldsPdsStoreLabel); - } - if (_features.UsesRegExp) + // Map (Dictionary) - check for "size" property. + // Gated together with the handler body below. + var mapLabel = il.DefineLabel(); + if (_features.UsesMap) { il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSRegExpType); - il.Emit(OpCodes.Brtrue, fieldsPdsStoreLabel); + il.Emit(OpCodes.Isinst, _types.DictionaryObjectObject); + il.Emit(OpCodes.Brtrue, mapLabel); } - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSPromiseType); - il.Emit(OpCodes.Brtrue, fieldsPdsStoreLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSErrorType); - il.Emit(OpCodes.Brtrue, fieldsPdsStoreLabel); - il.Emit(OpCodes.Br, afterFieldsPdsStoreLabel); - il.MarkLabel(fieldsPdsStoreLabel); + // Set (HashSet) - duck-typed access via GetSetProperty. + var setLabel = il.DefineLabel(); + if (_features.UsesSet) { - var fbDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); - il.Emit(OpCodes.Newobj, runtime.CompiledPropertyDescriptorCtor); - il.Emit(OpCodes.Stloc, fbDescLocal); - il.Emit(OpCodes.Ldloc, fbDescLocal); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorValue.GetSetMethod()!); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloc, fbDescLocal); - il.Emit(OpCodes.Call, runtime.PDSDefineProperty); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ret); + il.Emit(OpCodes.Isinst, _types.HashSetOfObject); + il.Emit(OpCodes.Brtrue, setLabel); } - il.MarkLabel(afterFieldsPdsStoreLabel); - - // Fallback: Try SetMember(string, object) method for types like $HttpResponse - var setMemberLocal = il.DeclareLocal(_types.MethodInfo); + // Dictionary (regular object) il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "GetType")); - il.Emit(OpCodes.Ldstr, "SetMember"); - il.Emit(OpCodes.Ldc_I4, (int)(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)); - il.Emit(OpCodes.Call, runtime.SafeGetMethod); - il.Emit(OpCodes.Stloc, setMemberLocal); - - il.Emit(OpCodes.Ldloc, setMemberLocal); - il.Emit(OpCodes.Brfalse, endLabel); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, dictLabel); - // Call SetMember(name, value): methodInfo.Invoke(obj, new object[] { name, value }) - il.Emit(OpCodes.Ldloc, setMemberLocal); + // User Array subclass (#233): a guest class extending Array derives + // from $Array AND implements $IHasFields. Its class members (declared + // fields, getters, methods) take precedence over the built-in array + // surface; the per-class GetProperty returns $Undefined on miss, in + // which case we fall through to the ordinary $Array dispatch below. + var notArraySubclassLabel = il.DefineLabel(); + var arraySubclassMissLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldc_I4_2); - il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Isinst, runtime.TSArrayType); + il.Emit(OpCodes.Brfalse, notArraySubclassLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.IHasFieldsInterface); + il.Emit(OpCodes.Brfalse, notArraySubclassLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.IHasFieldsInterface); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, runtime.IHasFieldsGetProperty); il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ldarg_1); // name - il.Emit(OpCodes.Stelem_Ref); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ldarg_2); // value - il.Emit(OpCodes.Stelem_Ref); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.MethodInfo, "Invoke", _types.Object, _types.ObjectArray)); + il.Emit(OpCodes.Ldsfld, runtime.UndefinedInstance); + il.Emit(OpCodes.Beq, arraySubclassMissLabel); + il.Emit(OpCodes.Ret); + il.MarkLabel(arraySubclassMissLabel); il.Emit(OpCodes.Pop); + il.MarkLabel(notArraySubclassLabel); - il.MarkLabel(endLabel); - il.Emit(OpCodes.Ret); - } + // $Array - check for "length" (inherits List; MUST come + // BEFORE the plain List check so sparse-aware length is used — + // otherwise `new Array(10_000_000).length` returns 0). + var sharpTSArrayLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSArrayType); + il.Emit(OpCodes.Brtrue, sharpTSArrayLabel); - private void EmitGetListProperty(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - // GetListProperty(list: List, name: string) -> object? - // Returns length as double, or a $BoundArrayMethod for array methods - var method = typeBuilder.DefineMethod( - "GetListProperty", - MethodAttributes.Public | MethodAttributes.Static, - _types.Object, - [_types.ListOfObject, _types.String] - ); - runtime.GetListProperty = method; + // List - check for "length" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.ListOfObject); + il.Emit(OpCodes.Brtrue, listLabel); - var il = method.GetILGenerator(); + // object[] (compiled `arguments` representation) - check for "length" + var objectArrayLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.ObjectArray); + il.Emit(OpCodes.Brtrue, objectArrayLabel); - var lengthLabel = il.DefineLabel(); - var returnNullLabel = il.DefineLabel(); - var createBoundMethodLabel = il.DefineLabel(); + // String - check for "length" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, stringLabel); - // if (name == "length") goto lengthLabel - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "length"); - il.Emit(OpCodes.Call, _types.StringOpEquality); - il.Emit(OpCodes.Brtrue, lengthLabel); + // $Buffer - check for "length" and "toString". Only meaningful when + // some feature emitted $Buffer (crypto/fs/zlib/http/fetch/dgram/net). + var tsBufferLabel = il.DefineLabel(); + if (_features.UsesBuffer) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSBufferType); + il.Emit(OpCodes.Brtrue, tsBufferLabel); + } - // if (name == "raw") - check for TemplateStringsList.raw property - var rawLabel = il.DefineLabel(); - var skipRawLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "raw"); - il.Emit(OpCodes.Call, _types.StringOpEquality); - il.Emit(OpCodes.Brtrue, rawLabel); - il.Emit(OpCodes.Br, skipRawLabel); + // $Stats - check for isFile, isDirectory, size, etc. Only when fs is on. + var tsStatsLabel = il.DefineLabel(); + if (_features.UsesFs) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.StatsType); + il.Emit(OpCodes.Brtrue, tsStatsLabel); + } - il.MarkLabel(rawLabel); - // For tagged template arrays, call emitted TemplateStringsList.raw getter. - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TemplateStringsListType); - il.Emit(OpCodes.Brfalse, returnNullLabel); + // $TSFunction - check for bind/call/apply + var tsFunctionLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TemplateStringsListType); - il.Emit(OpCodes.Callvirt, runtime.TemplateStringsListRawGetter); - il.Emit(OpCodes.Ret); - - il.MarkLabel(skipRawLabel); + il.Emit(OpCodes.Isinst, runtime.TSFunctionType); + il.Emit(OpCodes.Brtrue, tsFunctionLabel); - // Check for known array method names - // For each method name, if match, create $BoundArrayMethod - // Must stay in sync with EmitBoundArrayMethodFinalize's dispatch switch - // (RuntimeEmitter.Arrays.cs) and ArrayEmitter.cs static dispatch. - string[] methodNames = [ - "join", "push", "pop", "shift", "unshift", "slice", "splice", - "indexOf", "lastIndexOf", "includes", "concat", "reverse", "sort", "map", "filter", "forEach", - "find", "findIndex", "findLast", "findLastIndex", "some", "every", - "reduce", "reduceRight", - "flat", "flatMap", "at", - "toSorted", "toSpliced", "toReversed", "with", - "fill", "copyWithin", - "entries", "keys", "values", - "toString", "toLocaleString" - ]; + // $BoundTSFunction - also check for bind/call/apply + var boundFunctionLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.BoundTSFunctionType); + il.Emit(OpCodes.Brtrue, boundFunctionLabel); - foreach (var methodName in methodNames) + // $CJSModule - route to the module's GetMember(name) for exports/id/filename/etc. + // Only emitted when the program uses CommonJS (require/module/exports). + var cjsModuleGetLabel = il.DefineLabel(); + if (_features.UsesCjsRequire) { - var skipLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, methodName); - il.Emit(OpCodes.Call, _types.StringOpEquality); - il.Emit(OpCodes.Brfalse, skipLabel); - - // Create $BoundArrayMethod(list, name) and return - il.Emit(OpCodes.Ldarg_0); // list - il.Emit(OpCodes.Ldarg_1); // name - il.Emit(OpCodes.Newobj, runtime.BoundArrayMethodCtor); - il.Emit(OpCodes.Ret); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.CjsModuleType); + il.Emit(OpCodes.Brtrue, cjsModuleGetLabel); + } - il.MarkLabel(skipLabel); + // $RegExp — surface the built-in slots (`lastIndex`, `source`, `flags`, + // `global`, `ignoreCase`, `multiline`, `sticky`, `unicode`, `hasIndices`, + // `dotAll`, `unicodeSets`) via the typed getters / parsed-from-flags + // expressions. Without this branch the read falls through to + // GetFieldsProperty whose reflection lookup is case-sensitive ("lastIndex" + // vs "LastIndex") and silently returns undefined. Test262's + // builtin-coerce-lastindex.js + many coerce/builtin-* tests require the + // internal slot value to round-trip through `r.lastIndex` reads/writes. + var tsRegExpGetLabel = il.DefineLabel(); + if (_features.UsesRegExp) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSRegExpType); + il.Emit(OpCodes.Brtrue, tsRegExpGetLabel); } - // No known array method match — check PropertyDescriptorStore for custom defined properties - il.MarkLabel(returnNullLabel); - var reallyNullLabel = il.DefineLabel(); - var pdsDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); - il.Emit(OpCodes.Ldarg_0); // list (as object) - il.Emit(OpCodes.Ldarg_1); // name - il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); - il.Emit(OpCodes.Stloc, pdsDescLocal); - il.Emit(OpCodes.Ldloc, pdsDescLocal); - il.Emit(OpCodes.Brfalse, reallyNullLabel); - // Return descriptor.Value - il.Emit(OpCodes.Ldloc, pdsDescLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorValue.GetGetMethod()!); - il.Emit(OpCodes.Ret); + // Task (Promise) - check for then/catch/finally + var promiseLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.TaskOfObject); + il.Emit(OpCodes.Brtrue, promiseLabel); - // Final fallback: walk Array.prototype, then Object.prototype singleton - // dicts. ECMA-262 says a List receiver inherits from %Array.prototype% - // which inherits from %Object.prototype%, so user-added entries - // (`Array.prototype.foo = 1`) reach indexed-access reads as - // `arr.foo === 1`, and Object.prototype methods like hasOwnProperty, - // toString, valueOf flow through too. Populate both prototype dicts - // if not yet populated. - il.MarkLabel(reallyNullLabel); - var arrayProtoFallbackLabel = il.DefineLabel(); - var objectProtoFallbackLabel = il.DefineLabel(); - il.Emit(OpCodes.Call, runtime.ArrayPrototypePopulateMethod); - var arrayProtoValLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldsfld, runtime.ArrayPrototypeField); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloca, arrayProtoValLocal); - il.Emit(OpCodes.Callvirt, _types.DictionaryStringObject.GetMethod("TryGetValue", - [_types.String, _types.Object.MakeByRefType()])!); - il.Emit(OpCodes.Brfalse, arrayProtoFallbackLabel); - il.Emit(OpCodes.Ldloc, arrayProtoValLocal); - il.Emit(OpCodes.Ret); - il.MarkLabel(arrayProtoFallbackLabel); - // Walk to Object.prototype for shared methods like hasOwnProperty. - il.Emit(OpCodes.Call, runtime.ObjectPrototypePopulateMethod); - il.Emit(OpCodes.Ldsfld, runtime.ObjectPrototypeField); + // User Promise subclass (#242): a guest class extending Promise + // derives from $Promise AND implements $IHasFields. Its class members + // (declared fields, getters, methods) take precedence over the + // built-in promise surface; the per-class GetProperty returns + // $Undefined on miss, in which case we fall through to the ordinary + // $Promise dispatch below. Mirrors the $Array subclass arm above. + var notPromiseSubclassLabel = il.DefineLabel(); + var promiseSubclassMissLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSPromiseType); + il.Emit(OpCodes.Brfalse, notPromiseSubclassLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.IHasFieldsInterface); + il.Emit(OpCodes.Brfalse, notPromiseSubclassLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.IHasFieldsInterface); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloca, arrayProtoValLocal); - il.Emit(OpCodes.Callvirt, _types.DictionaryStringObject.GetMethod("TryGetValue", - [_types.String, _types.Object.MakeByRefType()])!); - il.Emit(OpCodes.Brfalse, objectProtoFallbackLabel); - il.Emit(OpCodes.Ldloc, arrayProtoValLocal); - il.Emit(OpCodes.Ret); - il.MarkLabel(objectProtoFallbackLabel); - // Missing-property reads should return JS undefined, not C# null — - // tests assert `arr.foo === undefined` not `arr.foo === null`. + il.Emit(OpCodes.Callvirt, runtime.IHasFieldsGetProperty); + il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldsfld, runtime.UndefinedInstance); + il.Emit(OpCodes.Beq, promiseSubclassMissLabel); il.Emit(OpCodes.Ret); + il.MarkLabel(promiseSubclassMissLabel); + il.Emit(OpCodes.Pop); + il.MarkLabel(notPromiseSubclassLabel); - // length case: return (double)list.Count — except for $Arguments, - // which exposes its own _length field (sloppy arguments objects - // don't auto-update length on out-of-range indexed writes per - // ECMA-262 10.4.4). - il.MarkLabel(lengthLabel); - var notArgumentsLengthLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.ArgumentsType); - il.Emit(OpCodes.Brfalse, notArgumentsLengthLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.ArgumentsType); - il.Emit(OpCodes.Ldfld, runtime.ArgumentsLengthField); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Box, _types.Double); - il.Emit(OpCodes.Ret); - il.MarkLabel(notArgumentsLengthLabel); + // $Promise type (used by fetch, etc.) - check for then/catch/finally il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetPropertyGetter(_types.ListOfObject, "Count")); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Box, _types.Double); - il.Emit(OpCodes.Ret); - } + il.Emit(OpCodes.Isinst, runtime.TSPromiseType); + il.Emit(OpCodes.Brtrue, promiseLabel); - private void EmitGetMapProperty(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - // GetMapProperty(map: Dictionary, name: string) -> object? - // Returns size as double, or a $BoundMapMethod wrapper for known Map methods. - // Mirrors GetListProperty — ensures duck typing works across module boundaries: - // typeof map.get === 'function' and map.get.call(map, k) both work on a Map - // received from another module. - var method = typeBuilder.DefineMethod( - "GetMapProperty", - MethodAttributes.Public | MethodAttributes.Static, - _types.Object, - [_types.DictionaryObjectObject, _types.String] - ); - runtime.GetMapProperty = method; - - var il = method.GetILGenerator(); - - var sizeLabel = il.DefineLabel(); - - // if (name == "size") goto sizeLabel - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "size"); - il.Emit(OpCodes.Call, _types.StringOpEquality); - il.Emit(OpCodes.Brtrue, sizeLabel); - - // For each known Map method name, return a $BoundMapMethod wrapper. - string[] methodNames = ["get", "set", "has", "delete", "clear", - "keys", "values", "entries", "forEach"]; - - foreach (var methodName in methodNames) + // ArrayBuffer / SharedArrayBuffer / DataView / TypedArray dispatch arms — + // skipped when no typed-array kind is referenced. The handler bodies + // (MarkLabel'd at lines ~1834+) are gated on the same flag. + var arrayBufferLabel = il.DefineLabel(); + var sharedArrayBufferLabel = il.DefineLabel(); + var dataViewLabel = il.DefineLabel(); + var typedArrayLabel = il.DefineLabel(); + if (_features.HasAnyTypedArray) { - var skipLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, methodName); - il.Emit(OpCodes.Call, _types.StringOpEquality); - il.Emit(OpCodes.Brfalse, skipLabel); - - il.Emit(OpCodes.Ldarg_0); // map - il.Emit(OpCodes.Ldarg_1); // name - il.Emit(OpCodes.Newobj, runtime.BoundMapMethodCtor); - il.Emit(OpCodes.Ret); - - il.MarkLabel(skipLabel); - } - - // Unknown property: return null - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Ret); - - // size case: return (double)map.Count - il.MarkLabel(sizeLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.DictionaryObjectObject, "Count").GetGetMethod()!); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Box, _types.Double); - il.Emit(OpCodes.Ret); - } - - private void EmitGetSetProperty(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - // GetSetProperty(set: HashSet, name: string) -> object? - // Returns size as double, or a $BoundSetMethod wrapper for known Set methods - // (including ES2025 set operations). Mirrors GetListProperty / GetMapProperty. - var method = typeBuilder.DefineMethod( - "GetSetProperty", - MethodAttributes.Public | MethodAttributes.Static, - _types.Object, - [_types.HashSetOfObject, _types.String] - ); - runtime.GetSetProperty = method; - - var il = method.GetILGenerator(); - - var sizeLabel = il.DefineLabel(); - - // if (name == "size") goto sizeLabel - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "size"); - il.Emit(OpCodes.Call, _types.StringOpEquality); - il.Emit(OpCodes.Brtrue, sizeLabel); - - // For each known Set method name, return a $BoundSetMethod wrapper. - string[] methodNames = ["add", "has", "delete", "clear", - "keys", "values", "entries", "forEach", - "union", "intersection", "difference", "symmetricDifference", - "isSubsetOf", "isSupersetOf", "isDisjointFrom"]; + // $ArrayBuffer - check for "byteLength" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.ArrayBufferType); + il.Emit(OpCodes.Brtrue, arrayBufferLabel); - foreach (var methodName in methodNames) - { - var skipLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, methodName); - il.Emit(OpCodes.Call, _types.StringOpEquality); - il.Emit(OpCodes.Brfalse, skipLabel); + // $SharedArrayBuffer - check for "byteLength" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.SharedArrayBufferType); + il.Emit(OpCodes.Brtrue, sharedArrayBufferLabel); - il.Emit(OpCodes.Ldarg_0); // set - il.Emit(OpCodes.Ldarg_1); // name - il.Emit(OpCodes.Newobj, runtime.BoundSetMethodCtor); - il.Emit(OpCodes.Ret); + // $DataView - check for "byteLength", "byteOffset", "buffer" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.DataViewType); + il.Emit(OpCodes.Brtrue, dataViewLabel); - il.MarkLabel(skipLabel); + // TypedArray - use emitted helper dispatch for standalone behavior + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.IsTypedArrayMethod); + il.Emit(OpCodes.Brtrue, typedArrayLabel); } - // Unknown property: return null - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Ret); - - // size case: return (double)set.Count - il.MarkLabel(sizeLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.HashSetOfObject, "Count").GetGetMethod()!); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Box, _types.Double); - il.Emit(OpCodes.Ret); - } - - private void EmitGetProperty(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - // Signature forward-declared by DefineRuntimeClassPhase1. - var method = (MethodBuilder)runtime.GetProperty; - - var il = method.GetILGenerator(); - var nullLabel = il.DefineLabel(); - var namespaceLabel = il.DefineLabel(); - var dictLabel = il.DefineLabel(); - var listLabel = il.DefineLabel(); - var stringLabel = il.DefineLabel(); - - // null check - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Brfalse, nullLabel); - - // globalThis/global sentinel (#271): a value-position globalThis reads - // properties through GlobalThisGetProperty (user props → built-in - // constructors/singletons), so `root.Object`/`root.Math` resolve to real - // values. Checked first so the bare-object sentinel never falls through to - // the class-instance handler (which would report every member undefined). - var notGlobalThisLabel = il.DefineLabel(); + // Primitive bool/double receivers — look up the named property in the + // matching prototype singleton (Boolean.prototype / Number.prototype). + // ECMA-262 7.3.2 OrdinaryGetPrototypeOf treats every primitive as if + // wrapped via ToObject, so `(true).valueOf` walks Boolean.prototype. + // Without this branch, `b.valueOf` returned undefined for any-typed + // bools because the routing fell through to classInstanceLabel which + // can't resolve methods on a CLR `bool` value-type box. + var notBoolPrimLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldsfld, runtime.GlobalThisSingletonField); - il.Emit(OpCodes.Bne_Un, notGlobalThisLabel); + il.Emit(OpCodes.Isinst, _types.Boolean); + il.Emit(OpCodes.Brfalse, notBoolPrimLabel); + il.Emit(OpCodes.Call, runtime.BooleanPrototypePopulateMethod); + il.Emit(OpCodes.Ldsfld, runtime.BooleanPrototypeField); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.GlobalThisGetProperty); + il.Emit(OpCodes.Call, method); // recursive GetProperty lookup on the dict il.Emit(OpCodes.Ret); - il.MarkLabel(notGlobalThisLabel); + il.MarkLabel(notBoolPrimLabel); - // __proto__ accessor (ECMA-262 Annex B.2.2.1): obj.__proto__ delegates - // to Object.getPrototypeOf(obj). All object types support this — the - // accessor lives on Object.prototype, but intercepting here avoids - // replicating the dispatch in every object-specific branch. Without - // this, `{}.__proto__` returns undefined and breaks spec idioms. - // - // CAVEAT: ECMA-262 also allows defining an own "__proto__" data - // property that shadows the inherited accessor. JSON.parse creates - // such own data properties (CreateDataProperty). For Dict receivers, - // check ContainsKey first and fall through to the regular dict path - // when the key is present — preserves JSON.parse semantics for the - // `{"__proto__":...}` corner. - var notProtoNameTopLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "__proto__"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notProtoNameTopLabel); - // Dict + ContainsKey("__proto__") → skip intercept. - var protoDictLocal = il.DeclareLocal(_types.DictionaryStringObject); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Stloc, protoDictLocal); - var noOwnProtoLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, protoDictLocal); - il.Emit(OpCodes.Brfalse, noOwnProtoLabel); - il.Emit(OpCodes.Ldloc, protoDictLocal); - il.Emit(OpCodes.Ldstr, "__proto__"); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); - il.Emit(OpCodes.Brtrue, notProtoNameTopLabel); - il.MarkLabel(noOwnProtoLabel); + var notDoublePrimLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.ObjectGetPrototypeOf); + il.Emit(OpCodes.Isinst, _types.Double); + il.Emit(OpCodes.Brfalse, notDoublePrimLabel); + il.Emit(OpCodes.Call, runtime.NumberPrototypePopulateMethod); + il.Emit(OpCodes.Ldsfld, runtime.NumberPrototypeField); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, method); il.Emit(OpCodes.Ret); - il.MarkLabel(notProtoNameTopLabel); - - // Proxy check: uses obj.GetType().FullName comparison (no SharpTS.dll dependency) - var notProxyLabel = il.DefineLabel(); - EmitProxyGetPropertyCheck(il, () => il.Emit(OpCodes.Ldarg_0), () => il.Emit(OpCodes.Ldarg_1), notProxyLabel); - - il.MarkLabel(notProxyLabel); - - // $TSNamespace - call ns.Get(name) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSNamespaceType); - il.Emit(OpCodes.Brtrue, namespaceLabel); + il.MarkLabel(notDoublePrimLabel); - // $Object (with getter/setter support) - call obj.GetProperty(name) - var tsObjectLabel = il.DefineLabel(); + // $Bound*Method and $BoundAnyFunction - callable wrappers that need .call/.apply/.bind + // support. Route through GetFunctionMethod which handles bind/call/apply/length/name. + // Bound methods already capture their receiver, so thisArg passed to .call/.apply is + // ignored per JS bound-callable semantics — the CallWrapper/ApplyWrapper Invoke bodies + // implement that via EmitDispatchToTarget. + var callableWrapperLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brtrue, tsObjectLabel); - - // Map (Dictionary) - check for "size" property. - // Gated together with the handler body below. - var mapLabel = il.DefineLabel(); + il.Emit(OpCodes.Isinst, runtime.BoundArrayMethodType); + il.Emit(OpCodes.Brtrue, callableWrapperLabel); if (_features.UsesMap) { il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.DictionaryObjectObject); - il.Emit(OpCodes.Brtrue, mapLabel); + il.Emit(OpCodes.Isinst, runtime.BoundMapMethodType); + il.Emit(OpCodes.Brtrue, callableWrapperLabel); } - - // Set (HashSet) - duck-typed access via GetSetProperty. - var setLabel = il.DefineLabel(); if (_features.UsesSet) { il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.HashSetOfObject); - il.Emit(OpCodes.Brtrue, setLabel); + il.Emit(OpCodes.Isinst, runtime.BoundSetMethodType); + il.Emit(OpCodes.Brtrue, callableWrapperLabel); } + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.BoundAnyFunctionType); + il.Emit(OpCodes.Brtrue, callableWrapperLabel); - // Dictionary (regular object) + // System.Type (a class reference used as a value, e.g. `Scalar.PLAIN = 'x'` then + // reading `Scalar.PLAIN`). JS allows arbitrary static property assignment on classes; + // we store them in $PropertyDescriptorStore. Check PDS first; if no descriptor, fall + // through to class-instance resolver (which on a Type will read its .NET members). + var typeGetLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, dictLabel); - - // User Array subclass (#233): a guest class extending Array derives - // from $Array AND implements $IHasFields. Its class members (declared - // fields, getters, methods) take precedence over the built-in array - // surface; the per-class GetProperty returns $Undefined on miss, in - // which case we fall through to the ordinary $Array dispatch below. - var notArraySubclassLabel = il.DefineLabel(); - var arraySubclassMissLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSArrayType); - il.Emit(OpCodes.Brfalse, notArraySubclassLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.IHasFieldsInterface); - il.Emit(OpCodes.Brfalse, notArraySubclassLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.IHasFieldsInterface); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, runtime.IHasFieldsGetProperty); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Ldsfld, runtime.UndefinedInstance); - il.Emit(OpCodes.Beq, arraySubclassMissLabel); - il.Emit(OpCodes.Ret); - il.MarkLabel(arraySubclassMissLabel); - il.Emit(OpCodes.Pop); - il.MarkLabel(notArraySubclassLabel); - - // $Array - check for "length" (inherits List; MUST come - // BEFORE the plain List check so sparse-aware length is used — - // otherwise `new Array(10_000_000).length` returns 0). - var sharpTSArrayLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSArrayType); - il.Emit(OpCodes.Brtrue, sharpTSArrayLabel); - - // List - check for "length" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.ListOfObject); - il.Emit(OpCodes.Brtrue, listLabel); - - // object[] (compiled `arguments` representation) - check for "length" - var objectArrayLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.ObjectArray); - il.Emit(OpCodes.Brtrue, objectArrayLabel); - - // String - check for "length" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brtrue, stringLabel); - - // $Buffer - check for "length" and "toString". Only meaningful when - // some feature emitted $Buffer (crypto/fs/zlib/http/fetch/dgram/net). - var tsBufferLabel = il.DefineLabel(); - if (_features.UsesBuffer) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSBufferType); - il.Emit(OpCodes.Brtrue, tsBufferLabel); - } - - // $Stats - check for isFile, isDirectory, size, etc. Only when fs is on. - var tsStatsLabel = il.DefineLabel(); - if (_features.UsesFs) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.StatsType); - il.Emit(OpCodes.Brtrue, tsStatsLabel); - } - - // $TSFunction - check for bind/call/apply - var tsFunctionLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSFunctionType); - il.Emit(OpCodes.Brtrue, tsFunctionLabel); - - // $BoundTSFunction - also check for bind/call/apply - var boundFunctionLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.BoundTSFunctionType); - il.Emit(OpCodes.Brtrue, boundFunctionLabel); - - // $CJSModule - route to the module's GetMember(name) for exports/id/filename/etc. - // Only emitted when the program uses CommonJS (require/module/exports). - var cjsModuleGetLabel = il.DefineLabel(); - if (_features.UsesCjsRequire) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.CjsModuleType); - il.Emit(OpCodes.Brtrue, cjsModuleGetLabel); - } - - // $RegExp — surface the built-in slots (`lastIndex`, `source`, `flags`, - // `global`, `ignoreCase`, `multiline`, `sticky`, `unicode`, `hasIndices`, - // `dotAll`, `unicodeSets`) via the typed getters / parsed-from-flags - // expressions. Without this branch the read falls through to - // GetFieldsProperty whose reflection lookup is case-sensitive ("lastIndex" - // vs "LastIndex") and silently returns undefined. Test262's - // builtin-coerce-lastindex.js + many coerce/builtin-* tests require the - // internal slot value to round-trip through `r.lastIndex` reads/writes. - var tsRegExpGetLabel = il.DefineLabel(); - if (_features.UsesRegExp) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSRegExpType); - il.Emit(OpCodes.Brtrue, tsRegExpGetLabel); - } - - // Task (Promise) - check for then/catch/finally - var promiseLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.TaskOfObject); - il.Emit(OpCodes.Brtrue, promiseLabel); - - // User Promise subclass (#242): a guest class extending Promise - // derives from $Promise AND implements $IHasFields. Its class members - // (declared fields, getters, methods) take precedence over the - // built-in promise surface; the per-class GetProperty returns - // $Undefined on miss, in which case we fall through to the ordinary - // $Promise dispatch below. Mirrors the $Array subclass arm above. - var notPromiseSubclassLabel = il.DefineLabel(); - var promiseSubclassMissLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSPromiseType); - il.Emit(OpCodes.Brfalse, notPromiseSubclassLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.IHasFieldsInterface); - il.Emit(OpCodes.Brfalse, notPromiseSubclassLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.IHasFieldsInterface); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, runtime.IHasFieldsGetProperty); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Ldsfld, runtime.UndefinedInstance); - il.Emit(OpCodes.Beq, promiseSubclassMissLabel); - il.Emit(OpCodes.Ret); - il.MarkLabel(promiseSubclassMissLabel); - il.Emit(OpCodes.Pop); - il.MarkLabel(notPromiseSubclassLabel); - - // $Promise type (used by fetch, etc.) - check for then/catch/finally - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSPromiseType); - il.Emit(OpCodes.Brtrue, promiseLabel); - - // ArrayBuffer / SharedArrayBuffer / DataView / TypedArray dispatch arms — - // skipped when no typed-array kind is referenced. The handler bodies - // (MarkLabel'd at lines ~1834+) are gated on the same flag. - var arrayBufferLabel = il.DefineLabel(); - var sharedArrayBufferLabel = il.DefineLabel(); - var dataViewLabel = il.DefineLabel(); - var typedArrayLabel = il.DefineLabel(); - if (_features.HasAnyTypedArray) - { - // $ArrayBuffer - check for "byteLength" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.ArrayBufferType); - il.Emit(OpCodes.Brtrue, arrayBufferLabel); - - // $SharedArrayBuffer - check for "byteLength" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.SharedArrayBufferType); - il.Emit(OpCodes.Brtrue, sharedArrayBufferLabel); - - // $DataView - check for "byteLength", "byteOffset", "buffer" - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.DataViewType); - il.Emit(OpCodes.Brtrue, dataViewLabel); - - // TypedArray - use emitted helper dispatch for standalone behavior - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.IsTypedArrayMethod); - il.Emit(OpCodes.Brtrue, typedArrayLabel); - } - - // Primitive bool/double receivers — look up the named property in the - // matching prototype singleton (Boolean.prototype / Number.prototype). - // ECMA-262 7.3.2 OrdinaryGetPrototypeOf treats every primitive as if - // wrapped via ToObject, so `(true).valueOf` walks Boolean.prototype. - // Without this branch, `b.valueOf` returned undefined for any-typed - // bools because the routing fell through to classInstanceLabel which - // can't resolve methods on a CLR `bool` value-type box. - var notBoolPrimLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Boolean); - il.Emit(OpCodes.Brfalse, notBoolPrimLabel); - il.Emit(OpCodes.Call, runtime.BooleanPrototypePopulateMethod); - il.Emit(OpCodes.Ldsfld, runtime.BooleanPrototypeField); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, method); // recursive GetProperty lookup on the dict - il.Emit(OpCodes.Ret); - il.MarkLabel(notBoolPrimLabel); - - var notDoublePrimLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Double); - il.Emit(OpCodes.Brfalse, notDoublePrimLabel); - il.Emit(OpCodes.Call, runtime.NumberPrototypePopulateMethod); - il.Emit(OpCodes.Ldsfld, runtime.NumberPrototypeField); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, method); - il.Emit(OpCodes.Ret); - il.MarkLabel(notDoublePrimLabel); - - // $Bound*Method and $BoundAnyFunction - callable wrappers that need .call/.apply/.bind - // support. Route through GetFunctionMethod which handles bind/call/apply/length/name. - // Bound methods already capture their receiver, so thisArg passed to .call/.apply is - // ignored per JS bound-callable semantics — the CallWrapper/ApplyWrapper Invoke bodies - // implement that via EmitDispatchToTarget. - var callableWrapperLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.BoundArrayMethodType); - il.Emit(OpCodes.Brtrue, callableWrapperLabel); - if (_features.UsesMap) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.BoundMapMethodType); - il.Emit(OpCodes.Brtrue, callableWrapperLabel); - } - if (_features.UsesSet) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.BoundSetMethodType); - il.Emit(OpCodes.Brtrue, callableWrapperLabel); - } - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.BoundAnyFunctionType); - il.Emit(OpCodes.Brtrue, callableWrapperLabel); - - // System.Type (a class reference used as a value, e.g. `Scalar.PLAIN = 'x'` then - // reading `Scalar.PLAIN`). JS allows arbitrary static property assignment on classes; - // we store them in $PropertyDescriptorStore. Check PDS first; if no descriptor, fall - // through to class-instance resolver (which on a Type will read its .NET members). - var typeGetLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Type); - il.Emit(OpCodes.Brtrue, typeGetLabel); + il.Emit(OpCodes.Isinst, _types.Type); + il.Emit(OpCodes.Brtrue, typeGetLabel); // Default - try class-instance fields/property resolution helper var classInstanceLabel = il.DefineLabel(); @@ -2215,2880 +1728,1235 @@ void EmitErrorTypeBranch(Type ctorType, MethodBuilder populate, FieldBuilder pro il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Castclass, runtime.SharedArrayBufferType); il.Emit(OpCodes.Callvirt, runtime.SharedArrayBufferByteLengthGetter); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Box, _types.Double); - il.Emit(OpCodes.Ret); - il.MarkLabel(notSharedArrayBufferByteLengthLabel); - // Return null for other properties - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Ret); - - EmitDataViewHandler(); - } - - // $DataView handler local helper — extracted so the enclosing - // `if (_features.HasAnyTypedArray)` block can call it once. - void EmitDataViewHandler() - { - // $DataView handler - check for "byteLength", "byteOffset", "buffer" - il.MarkLabel(dataViewLabel); - // Check "byteLength" - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "byteLength"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notDataViewByteLengthLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notDataViewByteLengthLabel); - // Return ByteLength as double - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.DataViewType); - il.Emit(OpCodes.Callvirt, runtime.DataViewByteLengthGetter); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Box, _types.Double); - il.Emit(OpCodes.Ret); - il.MarkLabel(notDataViewByteLengthLabel); - // Check "byteOffset" - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "byteOffset"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notDataViewByteOffsetLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notDataViewByteOffsetLabel); - // Return ByteOffset as double - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.DataViewType); - il.Emit(OpCodes.Callvirt, runtime.DataViewByteOffsetGetter); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Box, _types.Double); - il.Emit(OpCodes.Ret); - il.MarkLabel(notDataViewByteOffsetLabel); - // Check "buffer" - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "buffer"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notDataViewBufferLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notDataViewBufferLabel); - // Return Buffer - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.DataViewType); - il.Emit(OpCodes.Callvirt, runtime.DataViewBufferGetter); - il.Emit(OpCodes.Ret); - il.MarkLabel(notDataViewBufferLabel); - // Return null for other properties - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Ret); - } - - // Namespace handler - call ns.Get(name) - il.MarkLabel(namespaceLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSNamespaceType); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, runtime.TSNamespaceGet); - il.Emit(OpCodes.Ret); - - // $Object handler - call obj.GetProperty(name) which handles getters. - // If the property is missing (HasProperty=false) and there's no own PDS - // descriptor either, walk the prototype chain via PDSGetPrototype. - // Required for Test262 `Con.prototype = proto; new Con(); obj.length` - // patterns where length lives on the prototype, and conversely - // `Object.defineProperty(child, "length", {value:2})` (PDS-only) must - // override an inherited accessor on proto. - il.MarkLabel(tsObjectLabel); - // Object.prototype method short-circuits (Stage 4z15 follow-on): - // expose hasOwnProperty + isPrototypeOf as $TSFunction wrappers - // bound to this $Object. - void EmitTSObjProtoCheck(string jsName, MethodBuilder helper) - { - var skip = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, jsName); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, skip); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldtoken, helper); - il.Emit(OpCodes.Ldtoken, helper.DeclaringType!); - il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", - _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Castclass, _types.MethodInfo); - il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); - il.Emit(OpCodes.Ret); - il.MarkLabel(skip); - } - EmitTSObjProtoCheck("hasOwnProperty", runtime.HasOwnPropertyHelperMethod); - EmitTSObjProtoCheck("isPrototypeOf", runtime.IsPrototypeOfHelperMethod); - - var tsObjectInstanceLocal = il.DeclareLocal(runtime.TSObjectType); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSObjectType); - il.Emit(OpCodes.Stloc, tsObjectInstanceLocal); - // if (obj.HasProperty(name)) return obj.GetProperty(name) - il.Emit(OpCodes.Ldloc, tsObjectInstanceLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, runtime.TSObjectHasProperty); - var tsObjectCheckPDS = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, tsObjectCheckPDS); - il.Emit(OpCodes.Ldloc, tsObjectInstanceLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, runtime.TSObjectGetProperty); - il.Emit(OpCodes.Ret); - // Check PDS for own data descriptor / accessor before walking chain - il.MarkLabel(tsObjectCheckPDS); - var tsObjectPDSDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); - il.Emit(OpCodes.Ldloc, tsObjectInstanceLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); - il.Emit(OpCodes.Stloc, tsObjectPDSDescLocal); - var tsObjectWalkProto = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, tsObjectPDSDescLocal); - il.Emit(OpCodes.Brfalse, tsObjectWalkProto); - // Has own descriptor: getter wins, else return value - var tsObjectPDSValue = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, tsObjectPDSDescLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorGetter.GetGetMethod()!); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Brfalse, tsObjectPDSValue); - // Invoke getter via InvokeMethodValue(obj, getter, []) - var tsObjectGetterLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Stloc, tsObjectGetterLocal); - il.Emit(OpCodes.Ldloc, tsObjectInstanceLocal); - il.Emit(OpCodes.Ldloc, tsObjectGetterLocal); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Newarr, _types.Object); - il.Emit(OpCodes.Call, runtime.InvokeMethodValue); - il.Emit(OpCodes.Ret); - il.MarkLabel(tsObjectPDSValue); - il.Emit(OpCodes.Pop); // discard the null getter - il.Emit(OpCodes.Ldloc, tsObjectPDSDescLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorValue.GetGetMethod()!); - il.Emit(OpCodes.Ret); - // Walk prototype chain - il.MarkLabel(tsObjectWalkProto); - var tsObjectProtoLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldloc, tsObjectInstanceLocal); - il.Emit(OpCodes.Call, runtime.PDSGetPrototype); - il.Emit(OpCodes.Stloc, tsObjectProtoLocal); - var tsObjectNoProto = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, tsObjectProtoLocal); - il.Emit(OpCodes.Brfalse, tsObjectNoProto); - // Recursively call GetProperty(prototype, name) - il.Emit(OpCodes.Ldloc, tsObjectProtoLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, method); - il.Emit(OpCodes.Ret); - il.MarkLabel(tsObjectNoProto); - // No own prototype — fall back to Object.prototype singleton (mirrors - // the dict-branch fallback). Catches `({}.toString)` style accesses on - // $Object instances created without an explicit prototype link. - var tsObjProtoFallbackMissLabel = il.DefineLabel(); - il.Emit(OpCodes.Call, runtime.ObjectPrototypePopulateMethod); - var tsObjProtoFallbackLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldsfld, runtime.ObjectPrototypeField); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloca, tsObjProtoFallbackLocal); - il.Emit(OpCodes.Callvirt, _types.DictionaryStringObject.GetMethod("TryGetValue", - [_types.String, _types.Object.MakeByRefType()])!); - il.Emit(OpCodes.Brfalse, tsObjProtoFallbackMissLabel); - il.Emit(OpCodes.Ldloc, tsObjProtoFallbackLocal); - il.Emit(OpCodes.Ret); - il.MarkLabel(tsObjProtoFallbackMissLabel); - // Property absent on object and Object.prototype — return undefined. - il.Emit(OpCodes.Ldsfld, runtime.UndefinedInstance); - il.Emit(OpCodes.Ret); - - il.MarkLabel(nullLabel); - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Ret); - - // $TSFunction handler - call GetFunctionMethod(func, name) - il.MarkLabel(tsFunctionLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.GetFunctionMethod); - il.Emit(OpCodes.Ret); - - // $BoundTSFunction handler - call GetFunctionMethod(func, name) - il.MarkLabel(boundFunctionLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.GetFunctionMethod); - il.Emit(OpCodes.Ret); - - // $CJSModule handler - call module.GetMember(name) — only emitted when - // UsesCjsRequire is on (matching the dispatch arm above). - if (_features.UsesCjsRequire) - { - il.MarkLabel(cjsModuleGetLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.CjsModuleType); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, runtime.CjsModuleType.GetMethod("GetMember", [_types.String])!); - il.Emit(OpCodes.Ret); - } - - // $RegExp handler — surface built-in slots via typed getters, plus - // flag-string-parsed accessors (sticky/unicode/hasIndices/dotAll/ - // unicodeSets) for the JS-spec flags that don't have dedicated - // .NET-side fields. Unknown property names fall through to - // GetFieldsProperty so user-set data (`r.foo = 1`, descriptor- - // installed properties) still resolves correctly. - if (_features.UsesRegExp) - { - il.MarkLabel(tsRegExpGetLabel); - var rxLocal = il.DeclareLocal(runtime.TSRegExpType); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSRegExpType); - il.Emit(OpCodes.Stloc, rxLocal); - - // ECMA-262 §22.2.6.* read paths go through ordinary Get, so - // user-installed Object.defineProperty(r, 'flags', {get}) etc. - // must win over the internal slot. Check PDS first for any name; - // when a descriptor is present, surface its value (data) or - // invoke its getter (accessor) before reaching the typed slot - // fast-paths below. Symbol.match's get-flags-err.js, - // builtin-coerce-lastindex.js and friends rely on the override - // path running before the internal-slot read. - var pdsDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); - il.Emit(OpCodes.Stloc, pdsDescLocal); - var noPdsDescLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, pdsDescLocal); - il.Emit(OpCodes.Brfalse, noPdsDescLabel); - // Accessor descriptor? Getter != null → invoke fn(thisArg=rx). - var dataDescLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, pdsDescLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorGetter.GetGetMethod()!); - var regexpGetterLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Stloc, regexpGetterLocal); - il.Emit(OpCodes.Ldloc, regexpGetterLocal); - il.Emit(OpCodes.Brfalse, dataDescLabel); - // Cast getter to $TSFunction and InvokeWithThis(rx). If the - // descriptor's getter slot isn't a $TSFunction (shouldn't - // happen normally), fall through to the data path. - var regexpFnLocal = il.DeclareLocal(runtime.TSFunctionType); - il.Emit(OpCodes.Ldloc, regexpGetterLocal); - il.Emit(OpCodes.Isinst, runtime.TSFunctionType); - il.Emit(OpCodes.Stloc, regexpFnLocal); - il.Emit(OpCodes.Ldloc, regexpFnLocal); - il.Emit(OpCodes.Brfalse, dataDescLabel); - il.Emit(OpCodes.Ldloc, regexpFnLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, _types.GetMethod(typeof(System.Array), "Empty").MakeGenericMethod(_types.Object)); - il.Emit(OpCodes.Callvirt, runtime.TSFunctionInvokeWithThis); - il.Emit(OpCodes.Ret); - il.MarkLabel(dataDescLabel); - // Data descriptor — return descriptor.Value. - il.Emit(OpCodes.Ldloc, pdsDescLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorValue.GetGetMethod()!); - il.Emit(OpCodes.Ret); - il.MarkLabel(noPdsDescLabel); - - // Helper closure to emit a name-equality test + branch to a - // labelled body. Keeps the dispatch table readable. - void NameMatchBranch(string propName, System.Action emitBody) - { - var notThisName = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, propName); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notThisName); - emitBody(); - il.Emit(OpCodes.Ret); - il.MarkLabel(notThisName); - } - - // "lastIndex" — return the raw boxed value when a non-numeric value - // was assigned (object identity preserved per spec); otherwise the - // typed int as a boxed double. - NameMatchBranch("lastIndex", () => - { - var numericLabel = il.DefineLabel(); - var doneLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, rxLocal); - il.Emit(OpCodes.Ldfld, _tsRegExpLastIndexBoxedField); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Brfalse, numericLabel); - il.Emit(OpCodes.Br, doneLabel); // boxed non-null → return it - il.MarkLabel(numericLabel); - il.Emit(OpCodes.Pop); // drop the null - il.Emit(OpCodes.Ldloc, rxLocal); - il.Emit(OpCodes.Callvirt, runtime.TSRegExpLastIndexGetter); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Box, _types.Double); - il.MarkLabel(doneLabel); - }); - // "constructor" — inherited from RegExp.prototype.constructor, which - // is the RegExp constructor (the $RegExp Type token, == what `RegExp` - // evaluates to as a value). Without this an instance read returns - // undefined and `re.constructor === RegExp` is false, blocking the - // §22.2.4.1 call-form same-object check. PDS is checked above, so a - // user `Object.defineProperty(re,'constructor',…)` / `re.constructor=x` - // still wins. - NameMatchBranch("constructor", () => - { - il.Emit(OpCodes.Ldtoken, runtime.TSRegExpType); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); - }); - // "source" / "flags" — string fields. - NameMatchBranch("source", () => - { - il.Emit(OpCodes.Ldloc, rxLocal); - il.Emit(OpCodes.Callvirt, runtime.TSRegExpSourceGetter); - }); - // Spec-aligned ECMA-262 §22.2.6.4 — assemble the flags string from - // individual property reads so user-installed `Object.defineProperty - // (r, 'global', {get})` overrides participate in the chain. Each - // Get(rx, propName) goes through this very GetProperty recursively, - // so PDS-first lookup on the per-flag property fires first; the - // typed slot fallback returns the same boxed bool we'd have read - // directly, keeping the assembled string identical to _flags for - // ordinary $RegExp without overrides. Unlocks Symbol.match/replace/ - // split/search's get-global-err / coerce-global / get-unicode-error - // test262 family. - NameMatchBranch("flags", () => - { - var sbLocal = il.DeclareLocal(typeof(System.Text.StringBuilder)); - il.Emit(OpCodes.Newobj, typeof(System.Text.StringBuilder).GetConstructor(Type.EmptyTypes)!); - il.Emit(OpCodes.Stloc, sbLocal); - - var sbAppendChar = typeof(System.Text.StringBuilder).GetMethod("Append", [typeof(char)])!; - void AppendIfTruthy(string propName, char ch) - { - var skipLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldstr, propName); - il.Emit(OpCodes.Call, method); - il.Emit(OpCodes.Call, runtime.IsTruthy); - il.Emit(OpCodes.Brfalse, skipLabel); - il.Emit(OpCodes.Ldloc, sbLocal); - il.Emit(OpCodes.Ldc_I4, (int)ch); - il.Emit(OpCodes.Callvirt, sbAppendChar); - il.Emit(OpCodes.Pop); - il.MarkLabel(skipLabel); - } - - // Order per ECMA-262 §22.2.6.4. - AppendIfTruthy("hasIndices", 'd'); - AppendIfTruthy("global", 'g'); - AppendIfTruthy("ignoreCase", 'i'); - AppendIfTruthy("multiline", 'm'); - AppendIfTruthy("dotAll", 's'); - AppendIfTruthy("unicode", 'u'); - AppendIfTruthy("unicodeSets", 'v'); - AppendIfTruthy("sticky", 'y'); - - il.Emit(OpCodes.Ldloc, sbLocal); - il.Emit(OpCodes.Callvirt, typeof(System.Text.StringBuilder).GetMethod("ToString", Type.EmptyTypes)!); - }); - // "global" / "ignoreCase" / "multiline" — boolean fields. - NameMatchBranch("global", () => - { - il.Emit(OpCodes.Ldloc, rxLocal); - il.Emit(OpCodes.Callvirt, runtime.TSRegExpGlobalGetter); - il.Emit(OpCodes.Box, _types.Boolean); - }); - NameMatchBranch("ignoreCase", () => - { - il.Emit(OpCodes.Ldloc, rxLocal); - il.Emit(OpCodes.Callvirt, runtime.TSRegExpIgnoreCaseGetter); - il.Emit(OpCodes.Box, _types.Boolean); - }); - NameMatchBranch("multiline", () => - { - il.Emit(OpCodes.Ldloc, rxLocal); - il.Emit(OpCodes.Callvirt, runtime.TSRegExpMultilineGetter); - il.Emit(OpCodes.Box, _types.Boolean); - }); - - // "sticky" / "unicode" / "hasIndices" / "dotAll" / "unicodeSets" - // — parsed from the flags string. There's no dedicated field for - // these, so we Contains-check the appropriate char (per ECMA-262 - // §22.2.5.3 flags-string assembly). - void FlagCharBranch(string propName, char ch) - { - NameMatchBranch(propName, () => - { - il.Emit(OpCodes.Ldloc, rxLocal); - il.Emit(OpCodes.Callvirt, runtime.TSRegExpFlagsGetter); - // s.Contains(ch) – use Contains(char) overload to dodge - // string-literal allocation for the single-char arg. - il.Emit(OpCodes.Ldc_I4, (int)ch); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "Contains", _types.Char)); - il.Emit(OpCodes.Box, _types.Boolean); - }); - } - FlagCharBranch("sticky", 'y'); - FlagCharBranch("unicode", 'u'); - FlagCharBranch("hasIndices", 'd'); - FlagCharBranch("dotAll", 's'); - FlagCharBranch("unicodeSets", 'v'); - - // Other property names fall through to GetFieldsProperty so - // user-set data (`r.foo = 1`) and prototype walks still resolve. - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.GetFieldsProperty); - il.Emit(OpCodes.Ret); - } - - // Promise (Task or $Promise) handler - return TSFunction wrappers for then/catch/finally - il.MarkLabel(promiseLabel); - // First, extract the underlying Task if this is a $Promise object - // Store the task in a local variable for use in creating TSFunction wrappers - var taskLocal = il.DeclareLocal(_types.TaskOfObject); - var isTSPromiseLabel = il.DefineLabel(); - var haveTaskLabel = il.DefineLabel(); - - // Check if obj is $Promise - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSPromiseType); - il.Emit(OpCodes.Brtrue, isTSPromiseLabel); - - // It's a raw Task, use directly - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.TaskOfObject); - il.Emit(OpCodes.Stloc, taskLocal); - il.Emit(OpCodes.Br, haveTaskLabel); - - // It's a $Promise, extract the Task property - il.MarkLabel(isTSPromiseLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSPromiseType); - il.Emit(OpCodes.Callvirt, runtime.TSPromiseTaskGetter); - il.Emit(OpCodes.Stloc, taskLocal); - - il.MarkLabel(haveTaskLabel); - - // then/catch/finally: walk Promise.prototype dict so the wrappers - // returned are identical to those on Promise.prototype itself - // (`p.then === Promise.prototype.then`, spec-correct .length/.name - // derived from the __this-aware helper signatures). The previous - // implementation constructed bound $TSFunction wrappers per access - // — which broke identity AND reported length=3 / name="PromiseThen" - // for `p.then`. The PromiseThenHelper/PromiseCatchHelper/ - // PromiseFinallyHelper installed in PromisePrototypePopulate accept - // `__this` so chaining via `.call(p, fn)` works without needing - // task pre-binding. - var taskLocal2 = taskLocal; // silence unused-warning; kept for legacy - void EmitPromiseProtoLookup(string jsName) - { - var notThisLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, jsName); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notThisLabel); - il.Emit(OpCodes.Call, runtime.PromisePrototypePopulateMethod); - var protoValLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldsfld, runtime.PromisePrototypeField); - il.Emit(OpCodes.Ldstr, jsName); - il.Emit(OpCodes.Ldloca, protoValLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "TryGetValue", - _types.String, _types.Object.MakeByRefType())); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldloc, protoValLocal); - il.Emit(OpCodes.Ret); - il.MarkLabel(notThisLabel); - } - EmitPromiseProtoLookup("then"); - EmitPromiseProtoLookup("catch"); - EmitPromiseProtoLookup("finally"); - - // ECMA-262 §27.2.5.1: Promise.prototype.constructor is %Promise%. - // Bare `Promise` resolves to typeof(Task) in compiled mode - // (TryEmitBuiltInClassType / GlobalThisGetProperty), so return the - // same Type token here for identity: - // `Promise.resolve(1).constructor === Promise` (#221 increment). - // #242 subclass instances ($Promise-derived emitted classes) return - // their own class token instead, so `MyP.resolve(1).constructor === MyP`. - // Generic subclasses carry constructed types (MyP) while bare - // `MyP` emits the open definition — return the definition (same gap - // InstanceOf's generic-definition walk handles). - var notPromiseCtorLabel = il.DefineLabel(); - var defaultPromiseCtorLabel = il.DefineLabel(); - var nonGenericCtorLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "constructor"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notPromiseCtorLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSPromiseType); - il.Emit(OpCodes.Brfalse, defaultPromiseCtorLabel); - var ctorTypeLocal = il.DeclareLocal(_types.Type); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "GetType")); - il.Emit(OpCodes.Stloc, ctorTypeLocal); - il.Emit(OpCodes.Ldloc, ctorTypeLocal); - il.Emit(OpCodes.Ldtoken, runtime.TSPromiseType); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Beq, defaultPromiseCtorLabel); - il.Emit(OpCodes.Ldloc, ctorTypeLocal); - il.Emit(OpCodes.Callvirt, _types.Type.GetProperty("IsGenericType")!.GetGetMethod()!); - il.Emit(OpCodes.Brfalse, nonGenericCtorLabel); - il.Emit(OpCodes.Ldloc, ctorTypeLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Type, "GetGenericTypeDefinition")); - il.Emit(OpCodes.Ret); - il.MarkLabel(nonGenericCtorLabel); - il.Emit(OpCodes.Ldloc, ctorTypeLocal); - il.Emit(OpCodes.Ret); - il.MarkLabel(defaultPromiseCtorLabel); - il.Emit(OpCodes.Ldtoken, _types.TaskOfObject); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Ret); - il.MarkLabel(notPromiseCtorLabel); - - // Unknown promise property - return null - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Ret); - - il.MarkLabel(dictLabel); - // Object.prototype methods short-circuit: return $TSFunction wrappers - // for the helper. target=null + cached name+length means the wrapper - // dispatches via InvokeWithThis (the helper's first param is "__this" - // so _expectsThis=true), letting .call(receiver, ...) inject the right - // receiver instead of being shadowed by a target-bound prepending - // that double-applies and trims the wrong tail. Direct dispatch - // (`obj.method(args)`) still works because compiled-mode method calls - // route through InvokeMethodValue → InvokeWithThis with the receiver - // as thisArg. JS-spec name + length surface to user code via fn.name - // / fn.length introspection. - void EmitObjProtoMethodCheck(string jsName, MethodBuilder helper, int jsLength) - { - var skip = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, jsName); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, skip); - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Ldtoken, helper); - il.Emit(OpCodes.Ldtoken, helper.DeclaringType!); - il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", - _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Castclass, _types.MethodInfo); - il.Emit(OpCodes.Ldstr, jsName); - il.Emit(OpCodes.Ldc_I4, jsLength); - il.Emit(OpCodes.Newobj, runtime.TSFunctionCtorWithCache); - il.Emit(OpCodes.Ret); - il.MarkLabel(skip); - } - EmitObjProtoMethodCheck("hasOwnProperty", runtime.HasOwnPropertyHelperMethod, 1); - EmitObjProtoMethodCheck("isPrototypeOf", runtime.IsPrototypeOfHelperMethod, 1); - - // Check for getter accessor via $PropertyDescriptorStore - fully standalone, no reflection - var getterLocal = il.DeclareLocal(_types.Object); - var noGetterLabel = il.DefineLabel(); - - // Call PDSTryGetGetter(obj, name, out getter) - il.Emit(OpCodes.Ldarg_0); // obj - il.Emit(OpCodes.Ldarg_1); // name - il.Emit(OpCodes.Ldloca, getterLocal); // out getter - il.Emit(OpCodes.Call, runtime.PDSTryGetGetter); - il.Emit(OpCodes.Brfalse, noGetterLabel); - - // Getter was found - invoke it via InvokeMethodValue(obj, getter, emptyArgs) - il.Emit(OpCodes.Ldarg_0); // receiver (obj) - il.Emit(OpCodes.Ldloc, getterLocal); // function (getter) - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Newarr, _types.Object); // empty args array - il.Emit(OpCodes.Call, runtime.InvokeMethodValue); - il.Emit(OpCodes.Ret); - - il.MarkLabel(noGetterLabel); - - // dict.TryGetValue(name, out value) ? value : check prototype chain - var valueLocal = il.DeclareLocal(_types.Object); - var dictLocal = il.DeclareLocal(_types.DictionaryStringObject); - var protoLocal = il.DeclareLocal(_types.Object); - - // Store the dictionary in a local for later use with BindThis - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); - il.Emit(OpCodes.Stloc, dictLocal); - - il.Emit(OpCodes.Ldloc, dictLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloca, valueLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "TryGetValue")); - var foundLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, foundLabel); - - // $AbortSignal dict surface (#224, #985): the properties "aborted"/"reason"/ - // "onabort" AND the methods "addEventListener"/"removeEventListener"/ - // "throwIfAborted" on a dynamically-typed (`any`) signal receiver. The typed - // path intercepts at compile time (AbortSignalEmitter); an `any` receiver lands - // here. The methods are returned as $TSFunction wrappers (target=null, - // _expectsThis=true via the "__this" first parameter) so a subsequent - // InvokeMethodValue injects the signal as the receiver — i.e. - // `signal.addEventListener('abort', cb)` works from inside a helper, not only at - // a statically-typed call site. Signals are identified by their "_reasonSet" - // internal slot — the public keys are computed from the CancellationToken, so - // they are never own dict entries and always reach this miss path. Name screen - // runs first to keep ordinary dict misses cheap. - if (_features.UsesAbortController) - { - var notSignalPropLabel = il.DefineLabel(); - var signalNameMatchLabel = il.DefineLabel(); - var strEq = _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String); - - // Returns a $TSFunction wrapping `helper`, bound to no target, so the - // wrapper's _expectsThis is set (helper's first param is "__this") and - // InvokeMethodValue passes the signal receiver through. Same shape as - // EmitObjProtoMethodCheck's hasOwnProperty/isPrototypeOf wrappers. - void EmitSignalMethodWrapper(MethodBuilder helper, string jsName, int jsLength) - { - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Ldtoken, helper); - il.Emit(OpCodes.Ldtoken, helper.DeclaringType!); - il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", - _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Castclass, _types.MethodInfo); - il.Emit(OpCodes.Ldstr, jsName); - il.Emit(OpCodes.Ldc_I4, jsLength); - il.Emit(OpCodes.Newobj, runtime.TSFunctionCtorWithCache); - il.Emit(OpCodes.Ret); - } - - foreach (var signalProp in new[] - { "aborted", "reason", "onabort", "addEventListener", "removeEventListener", "throwIfAborted" }) - { - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, signalProp); - il.Emit(OpCodes.Call, strEq); - il.Emit(OpCodes.Brtrue, signalNameMatchLabel); - } - il.Emit(OpCodes.Br, notSignalPropLabel); - - il.MarkLabel(signalNameMatchLabel); - il.Emit(OpCodes.Ldloc, dictLocal); - il.Emit(OpCodes.Ldstr, "_reasonSet"); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); - il.Emit(OpCodes.Brfalse, notSignalPropLabel); - - var notSignalAbortedLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "aborted"); - il.Emit(OpCodes.Call, strEq); - il.Emit(OpCodes.Brfalse, notSignalAbortedLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.AbortSignalGetAborted); - il.Emit(OpCodes.Box, _types.Boolean); - il.Emit(OpCodes.Ret); - il.MarkLabel(notSignalAbortedLabel); - - var notSignalReasonLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "reason"); - il.Emit(OpCodes.Call, strEq); - il.Emit(OpCodes.Brfalse, notSignalReasonLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.AbortSignalGetReason); - il.Emit(OpCodes.Ret); - il.MarkLabel(notSignalReasonLabel); - - var notSignalOnAbortLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "onabort"); - il.Emit(OpCodes.Call, strEq); - il.Emit(OpCodes.Brfalse, notSignalOnAbortLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.AbortSignalGetOnAbort); - il.Emit(OpCodes.Ret); - il.MarkLabel(notSignalOnAbortLabel); - - var notSignalAelLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "addEventListener"); - il.Emit(OpCodes.Call, strEq); - il.Emit(OpCodes.Brfalse, notSignalAelLabel); - EmitSignalMethodWrapper(runtime.AbortSignalAddEventListenerThis, "addEventListener", 2); - il.MarkLabel(notSignalAelLabel); - - var notSignalRelLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "removeEventListener"); - il.Emit(OpCodes.Call, strEq); - il.Emit(OpCodes.Brfalse, notSignalRelLabel); - EmitSignalMethodWrapper(runtime.AbortSignalRemoveEventListenerThis, "removeEventListener", 2); - il.MarkLabel(notSignalRelLabel); - - // Only "throwIfAborted" remains among the screened names. - EmitSignalMethodWrapper(runtime.AbortSignalThrowIfAbortedThis, "throwIfAborted", 0); - - il.MarkLabel(notSignalPropLabel); - } - - // Property not found on object - check prototype chain - // Get prototype: $PropertyDescriptorStore.GetPrototype(obj) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.PDSGetPrototype); - il.Emit(OpCodes.Stloc, protoLocal); - - // If prototype is null, return undefined - var returnUndefinedLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, protoLocal); - il.Emit(OpCodes.Brfalse, returnUndefinedLabel); - - // Recursively call GetProperty(prototype, name) to check prototype chain - il.Emit(OpCodes.Ldloc, protoLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, method); // Recursive call to GetProperty - il.Emit(OpCodes.Ret); - - il.MarkLabel(returnUndefinedLabel); - // ECMA-262: `({}).constructor === Object`. If user hasn't set a custom - // constructor and no prototype overrides it, return typeof(object) which - // matches what compiled-mode `Object` resolves to via globalThis. - var notDictCtorLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "constructor"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notDictCtorLabel); - il.Emit(OpCodes.Ldtoken, _types.Object); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Ret); - il.MarkLabel(notDictCtorLabel); - // ECMA-262 19.1.3: every plain object inherits from Object.prototype. - // For Dictionary literals (`{}` etc.) without an explicit prototype, - // fall back to the ObjectPrototypeField singleton — that's where - // `valueOf`, `toString`, `propertyIsEnumerable`, and the toLocaleString - // wrapper live. Required for Test262 patterns that do - // `({}).toString.call(receiver)` or for ToPrimitive coercion to find - // the inherited methods on plain dicts. Lazy-populates on first read. - var protoFallbackMissLabel = il.DefineLabel(); - il.Emit(OpCodes.Call, runtime.ObjectPrototypePopulateMethod); - var objProtoFallbackLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldsfld, runtime.ObjectPrototypeField); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloca, objProtoFallbackLocal); - il.Emit(OpCodes.Callvirt, _types.DictionaryStringObject.GetMethod("TryGetValue", - [_types.String, _types.Object.MakeByRefType()])!); - il.Emit(OpCodes.Brfalse, protoFallbackMissLabel); - il.Emit(OpCodes.Ldloc, objProtoFallbackLocal); - il.Emit(OpCodes.Ret); - il.MarkLabel(protoFallbackMissLabel); - // Return $Undefined.Instance for non-existent properties (JavaScript semantics) - il.Emit(OpCodes.Ldsfld, runtime.UndefinedInstance); - il.Emit(OpCodes.Ret); - il.MarkLabel(foundLabel); - - // If value is a TSFunction, call BindThis(dict) on it - // to bind 'this' for object method shorthand - var notTSFunction = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, valueLocal); - il.Emit(OpCodes.Isinst, runtime.TSFunctionType); - il.Emit(OpCodes.Brfalse, notTSFunction); - - // Call func.BindThis(dict) - il.Emit(OpCodes.Ldloc, valueLocal); - il.Emit(OpCodes.Castclass, runtime.TSFunctionType); - il.Emit(OpCodes.Ldloc, dictLocal); - il.Emit(OpCodes.Callvirt, runtime.TSFunctionBindThis); - - il.MarkLabel(notTSFunction); - il.Emit(OpCodes.Ldloc, valueLocal); - il.Emit(OpCodes.Ret); - - // Map (Dictionary) handler - check for "size" property. - // Gated together with the dispatch arm above. - if (_features.UsesMap) - { - il.MarkLabel(mapLabel); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "size"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notMapSizeLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notMapSizeLabel); - // Return map.Count as double - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.DictionaryObjectObject); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.DictionaryObjectObject, "Count").GetGetMethod()!); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Box, _types.Double); - il.Emit(OpCodes.Ret); - il.MarkLabel(notMapSizeLabel); - // For other Map properties, dispatch via GetMapProperty — returns a $BoundMapMethod - // wrapper for known methods (get/set/has/...) so that `typeof m.get === 'function'` - // and `m.get.call(m, k)` work on a Map received from another module. - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.DictionaryObjectObject); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.GetMapProperty); - il.Emit(OpCodes.Ret); - } - - // Set (HashSet) handler - dispatch via GetSetProperty for size + - // $BoundSetMethod wrappers on known methods (add/has/delete/... plus ES2025 - // set ops). Mirrors the Map handler above. - if (_features.UsesSet) - { - il.MarkLabel(setLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.HashSetOfObject); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.GetSetProperty); - il.Emit(OpCodes.Ret); - } - - il.MarkLabel(listLabel); - // Check for "length" - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "length"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notLengthLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notLengthLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.ListOfObject); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count").GetGetMethod()!); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Box, _types.Double); - il.Emit(OpCodes.Ret); - il.MarkLabel(notLengthLabel); - // ECMA-262: `[].constructor === Array`. Compiled `Array` resolves to - // typeof(IList) — return that here. - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "constructor"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notListCtorLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notListCtorLabel); - il.Emit(OpCodes.Ldtoken, _types.IListOfObject); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Ret); - il.MarkLabel(notListCtorLabel); - // Numeric-string index — `GetProperty(list, "0")` must return list[0] so - // that `f[0]` for `f.__proto__ === [1,2,3]` walks the prototype chain - // and finds the array element. Without this branch the proto-chain walk - // bottoms out in GetListProperty's null fallback. - var listIdxLocal = il.DeclareLocal(_types.Int32); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloca, listIdxLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Int32, "TryParse", _types.String, _types.Int32.MakeByRefType())); - var listNotIndexLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, listNotIndexLabel); - il.Emit(OpCodes.Ldloc, listIdxLocal); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Blt, listNotIndexLabel); - il.Emit(OpCodes.Ldloc, listIdxLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.ListOfObject); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count").GetGetMethod()!); - il.Emit(OpCodes.Bge, listNotIndexLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.ListOfObject); - il.Emit(OpCodes.Ldloc, listIdxLocal); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Item").GetGetMethod()!); - il.Emit(OpCodes.Ret); - il.MarkLabel(listNotIndexLabel); - // PDS-stored own descriptor (e.g., RegExp.exec result has `index` / - // `input` / `groups` attached via PropertyDescriptorStore so the - // returned value can be a real Array exotic — `instanceof Array` true - // — while still answering `result.index` / `result.input` correctly). - // Without this, those metadata properties are invisible. - var listPdsLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); - il.Emit(OpCodes.Stloc, listPdsLocal); - il.Emit(OpCodes.Ldloc, listPdsLocal); - var listSkipPdsLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, listSkipPdsLabel); - il.Emit(OpCodes.Ldloc, listPdsLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorValue.GetGetMethod()!); - il.Emit(OpCodes.Ret); - il.MarkLabel(listSkipPdsLabel); - // For other properties on List (like methods push, pop, etc.), use GetListProperty - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.ListOfObject); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.GetListProperty); - il.Emit(OpCodes.Ret); - - // object[] handler — `arguments`-shape receiver. "length" → array.Length; - // numeric-string indexes return the element; anything else → undefined. - il.MarkLabel(objectArrayLabel); - var objArrNotLengthLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "length"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, objArrNotLengthLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.ObjectArray); - il.Emit(OpCodes.Ldlen); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Box, _types.Double); - il.Emit(OpCodes.Ret); - il.MarkLabel(objArrNotLengthLabel); - // Try numeric-string index: int.TryParse(name, out i) - var objArrIdxLocal = il.DeclareLocal(_types.Int32); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloca, objArrIdxLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Int32, "TryParse", _types.String, _types.Int32.MakeByRefType())); - var objArrNotIndexLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, objArrNotIndexLabel); - // Bounds check: i >= 0 && i < arr.Length - il.Emit(OpCodes.Ldloc, objArrIdxLocal); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Blt, objArrNotIndexLabel); - il.Emit(OpCodes.Ldloc, objArrIdxLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.ObjectArray); - il.Emit(OpCodes.Ldlen); - il.Emit(OpCodes.Bge, objArrNotIndexLabel); - // Read element at index - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.ObjectArray); - il.Emit(OpCodes.Ldloc, objArrIdxLocal); - il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Ret); - il.MarkLabel(objArrNotIndexLabel); - // Other property → undefined - il.Emit(OpCodes.Ldsfld, runtime.UndefinedInstance); - il.Emit(OpCodes.Ret); - - // $Array handler - access Elements.Count for "length" - il.MarkLabel(sharpTSArrayLabel); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "length"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notSharpTSArrayLengthLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notSharpTSArrayLengthLabel); - // Use the LongLength getter — not the int-clamped Length — so `.length` - // reads up to 2^32 - 1 survive (M3 acceptance: `a.length === 2147483649`). - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSArrayType); - il.Emit(OpCodes.Callvirt, runtime.TSArrayLongLengthGetter); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Box, _types.Double); - il.Emit(OpCodes.Ret); - il.MarkLabel(notSharpTSArrayLengthLabel); - // ECMA-262: `[].constructor === Array`. Mirror the listLabel branch. - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "constructor"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notTSArrayCtorLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notTSArrayCtorLabel); - il.Emit(OpCodes.Ldtoken, _types.IListOfObject); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Ret); - il.MarkLabel(notTSArrayCtorLabel); - // Numeric-string index — same purpose as the listLabel branch above: - // proto-chain walks (`f.__proto__ === [1,2,3]; f[0]`) bottom out here - // when the prototype is a $Array. Without this, GetListProperty returns - // null for any digit-string name and the array element is invisible. - var tsArrIdxLocal = il.DeclareLocal(_types.Int32); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloca, tsArrIdxLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Int32, "TryParse", _types.String, _types.Int32.MakeByRefType())); - var tsArrNotIndexLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, tsArrNotIndexLabel); - il.Emit(OpCodes.Ldloc, tsArrIdxLocal); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Blt, tsArrNotIndexLabel); - il.Emit(OpCodes.Ldloc, tsArrIdxLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.ListOfObject); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count").GetGetMethod()!); - il.Emit(OpCodes.Bge, tsArrNotIndexLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.ListOfObject); - il.Emit(OpCodes.Ldloc, tsArrIdxLocal); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Item").GetGetMethod()!); - il.Emit(OpCodes.Ret); - il.MarkLabel(tsArrNotIndexLabel); - // For other properties on $Array (method names like push/pop/sort/etc.), - // reuse GetListProperty — it returns the $BoundArrayMethod wrapper, and - // $Array IS a List by inheritance, so the cast works. - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.ListOfObject); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.GetListProperty); - il.Emit(OpCodes.Ret); - - // $Buffer handler - "length" and "toString". Gated together with the - // dispatch arm above. - if (_features.UsesBuffer) - { - il.MarkLabel(tsBufferLabel); - // Check for "length" - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "length"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notBufferLenLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notBufferLenLabel); - // Get buf.Length - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSBufferType); - il.Emit(OpCodes.Call, runtime.TSBufferLengthGetter); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Box, _types.Double); - il.Emit(OpCodes.Ret); - il.MarkLabel(notBufferLenLabel); - // Check for "toString" - return a wrapper that calls ToEncodedString - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "toString"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notBufferToStringLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notBufferToStringLabel); - // Create a TSFunction wrapper for ToEncodedString - // For dynamically generated types, we need both method and type tokens - il.Emit(OpCodes.Ldarg_0); // target (the buffer) - il.Emit(OpCodes.Ldtoken, runtime.TSBufferToString); - il.Emit(OpCodes.Ldtoken, runtime.TSBufferType); - il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Castclass, _types.MethodInfo); - il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); - il.Emit(OpCodes.Ret); - il.MarkLabel(notBufferToStringLabel); - // Unknown buffer property - return null - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Ret); - } - - // $Stats handler - return method wrappers or property values. - // Whole block is gated; without UsesFs there's no Stats type to dispatch on. - if (_features.UsesFs) - { - il.MarkLabel(tsStatsLabel); - // Check for "size" property - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "size"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notStatsSizeLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notStatsSizeLabel); - // Return stats.size - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.StatsType); - il.Emit(OpCodes.Call, runtime.StatsSizeGetter); - il.Emit(OpCodes.Box, _types.Double); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notStatsSizeLabel); - // Check for "isFile" method - return TSFunction wrapper - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "isFile"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notStatsIsFileLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notStatsIsFileLabel); - // Create TSFunction wrapper for isFile - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldtoken, runtime.StatsIsFile); - il.Emit(OpCodes.Ldtoken, runtime.StatsType); - il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Castclass, _types.MethodInfo); - il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notStatsIsFileLabel); - // Check for "isDirectory" method - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "isDirectory"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notStatsIsDirLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notStatsIsDirLabel); - // Create TSFunction wrapper for isDirectory - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldtoken, runtime.StatsIsDirectory); - il.Emit(OpCodes.Ldtoken, runtime.StatsType); - il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Castclass, _types.MethodInfo); - il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notStatsIsDirLabel); - // Check for "isSymbolicLink" method - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "isSymbolicLink"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notStatsIsSymlinkLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notStatsIsSymlinkLabel); - // Create TSFunction wrapper for isSymbolicLink - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldtoken, runtime.StatsIsSymbolicLink); - il.Emit(OpCodes.Ldtoken, runtime.StatsType); - il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Castclass, _types.MethodInfo); - il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notStatsIsSymlinkLabel); - // Check for "isBlockDevice" method - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "isBlockDevice"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notStatsIsBlockLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notStatsIsBlockLabel); - // Create TSFunction wrapper for isBlockDevice - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldtoken, runtime.StatsIsBlockDevice); - il.Emit(OpCodes.Ldtoken, runtime.StatsType); - il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Castclass, _types.MethodInfo); - il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notStatsIsBlockLabel); - // Check for "isCharacterDevice" method - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "isCharacterDevice"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notStatsIsCharLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notStatsIsCharLabel); - // Create TSFunction wrapper for isCharacterDevice - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldtoken, runtime.StatsIsCharacterDevice); - il.Emit(OpCodes.Ldtoken, runtime.StatsType); - il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Castclass, _types.MethodInfo); - il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notStatsIsCharLabel); - // Check for "isFIFO" method - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "isFIFO"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notStatsIsFIFOLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notStatsIsFIFOLabel); - // Create TSFunction wrapper for isFIFO - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldtoken, runtime.StatsIsFIFO); - il.Emit(OpCodes.Ldtoken, runtime.StatsType); - il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Castclass, _types.MethodInfo); - il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); - il.Emit(OpCodes.Ret); - - il.MarkLabel(notStatsIsFIFOLabel); - // Check for "isSocket" method - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "isSocket"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notStatsIsSocketLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notStatsIsSocketLabel); - // Create TSFunction wrapper for isSocket - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldtoken, runtime.StatsIsSocket); - il.Emit(OpCodes.Ldtoken, runtime.StatsType); - il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); - il.Emit(OpCodes.Castclass, _types.MethodInfo); - il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); - il.Emit(OpCodes.Ret); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Box, _types.Double); + il.Emit(OpCodes.Ret); + il.MarkLabel(notSharedArrayBufferByteLengthLabel); + // Return null for other properties + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ret); - il.MarkLabel(notStatsIsSocketLabel); - // Unknown stats property - return null - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Ret); - } // end if (_features.UsesFs) — $Stats handler block + EmitDataViewHandler(); + } - il.MarkLabel(stringLabel); - // Check for "length" + // $DataView handler local helper — extracted so the enclosing + // `if (_features.HasAnyTypedArray)` block can call it once. + void EmitDataViewHandler() + { + // $DataView handler - check for "byteLength", "byteOffset", "buffer" + il.MarkLabel(dataViewLabel); + // Check "byteLength" il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "length"); + il.Emit(OpCodes.Ldstr, "byteLength"); il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notStrLenLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notStrLenLabel); + var notDataViewByteLengthLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notDataViewByteLengthLabel); + // Return ByteLength as double il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length").GetGetMethod()!); + il.Emit(OpCodes.Castclass, runtime.DataViewType); + il.Emit(OpCodes.Callvirt, runtime.DataViewByteLengthGetter); il.Emit(OpCodes.Conv_R8); il.Emit(OpCodes.Box, _types.Double); il.Emit(OpCodes.Ret); - il.MarkLabel(notStrLenLabel); - // ECMA-262: `"hello".constructor === String`. Compiled mode resolves - // bare `String` to `typeof(string)` — returning that here makes the - // strict-equality check hold. + il.MarkLabel(notDataViewByteLengthLabel); + // Check "byteOffset" il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "constructor"); + il.Emit(OpCodes.Ldstr, "byteOffset"); il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - var notStrCtorLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notStrCtorLabel); - il.Emit(OpCodes.Ldtoken, _types.String); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); + var notDataViewByteOffsetLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notDataViewByteOffsetLabel); + // Return ByteOffset as double + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.DataViewType); + il.Emit(OpCodes.Callvirt, runtime.DataViewByteOffsetGetter); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Box, _types.Double); il.Emit(OpCodes.Ret); - il.MarkLabel(notStrCtorLabel); - - // Numeric index: `"hello"[0]` returns "h". Pre-fix returned null - // because the string fallback didn't honor numeric-string keys — - // only the typed-string dispatch did. - var notNumericKeyLabel = il.DefineLabel(); - var strIdxLocal = il.DeclareLocal(_types.Int32); + il.MarkLabel(notDataViewByteOffsetLabel); + // Check "buffer" il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloca, strIdxLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Int32, "TryParse", _types.String, _types.Int32.MakeByRefType())); - il.Emit(OpCodes.Brfalse, notNumericKeyLabel); - il.Emit(OpCodes.Ldloc, strIdxLocal); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Blt, notNumericKeyLabel); - il.Emit(OpCodes.Ldloc, strIdxLocal); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length").GetGetMethod()!); - il.Emit(OpCodes.Bge, notNumericKeyLabel); - // Return str[idx].ToString() + il.Emit(OpCodes.Ldstr, "buffer"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notDataViewBufferLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notDataViewBufferLabel); + // Return Buffer il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Ldloc, strIdxLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "get_Chars", _types.Int32)); - var charLocalStr = il.DeclareLocal(_types.Char); - il.Emit(OpCodes.Stloc, charLocalStr); - il.Emit(OpCodes.Ldloca, charLocalStr); - il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Char, "ToString")); + il.Emit(OpCodes.Castclass, runtime.DataViewType); + il.Emit(OpCodes.Callvirt, runtime.DataViewBufferGetter); il.Emit(OpCodes.Ret); - il.MarkLabel(notNumericKeyLabel); + il.MarkLabel(notDataViewBufferLabel); + // Return null for other properties + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ret); + } - // ECMA-262 7.3.2: walk String.prototype for borrowed-method patterns - // (`s.valueOf`, `s.toString`, `s.toLowerCase`, etc.). Pre-fix returned - // null for any property other than length/constructor. - il.Emit(OpCodes.Call, runtime.StringPrototypePopulateMethod); - il.Emit(OpCodes.Ldsfld, runtime.StringPrototypeField); + // Namespace handler - call ns.Get(name) + il.MarkLabel(namespaceLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSNamespaceType); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, method); + il.Emit(OpCodes.Callvirt, runtime.TSNamespaceGet); il.Emit(OpCodes.Ret); - } - - private void EmitSetProperty(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - // Signature forward-declared by DefineRuntimeClassPhase1. - var method = (MethodBuilder)runtime.SetProperty; - - var il = method.GetILGenerator(); - var nullLabel = il.DefineLabel(); - var dictLabel = il.DefineLabel(); - var tsObjectLabel = il.DefineLabel(); - - // null check - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Brfalse, nullLabel); - - EmitGlobalThisSetRedirect(il, runtime); - - // Proxy check: uses obj.GetType().FullName comparison (no SharpTS.dll dependency) - var notProxyLabel = il.DefineLabel(); - EmitProxySetPropertyCheck(il, () => il.Emit(OpCodes.Ldarg_0), () => il.Emit(OpCodes.Ldarg_1), () => il.Emit(OpCodes.Ldarg_2), notProxyLabel); - - il.MarkLabel(notProxyLabel); - - // $Object (with setter support) - call obj.SetProperty(name, value) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brtrue, tsObjectLabel); - - // $Array — special-case `arr.length = N` (route through $Array.SetLength - // so truncation/extension uses the sparse-aware path). Other named- - // property writes on arrays are silently ignored; ECMA-262 §22.1.5 - // permits them but most emitters don't preserve them and the spec - // compiler tests don't exercise that corner. Must come BEFORE the - // Dictionary check (since $Array inherits List which is - // neither), and BEFORE SetFieldsProperty fallthrough. - var tsArraySetPropLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSArrayType); - il.Emit(OpCodes.Brtrue, tsArraySetPropLabel); - - // Dictionary - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, dictLabel); - - // $TSFunction — JS functions are objects and support arbitrary property assignment - // (`fn.x = 42`, `lodash.chunk = function(...){}`). Store as a data descriptor in - // $PropertyDescriptorStore; GetFunctionMethod's fallback path reads it back. Without - // this, the assignment would fall through to SetFieldsProperty which is a - // class-instance path that doesn't match $TSFunction. - var tsFunctionSetLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSFunctionType); - il.Emit(OpCodes.Brtrue, tsFunctionSetLabel); - - // $CJSModule — `module.exports = X` (or any aliased write) goes through here. - // Only emit when UsesCjsRequire is on (matching the type emission gate). - var cjsModuleSetLabel = il.DefineLabel(); - if (_features.UsesCjsRequire) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.CjsModuleType); - il.Emit(OpCodes.Brtrue, cjsModuleSetLabel); - } - // $RegExp — `r.lastIndex = value` must coerce `value` via JS - // ToLength (string "1.9" → 1, NaN → 0, etc.) and write the internal - // int32 slot. Without the special case the assignment falls through - // to SetFieldsProperty which stores the boxed value on a generic - // PDS bag — subsequent built-in matchers ignore it and use the - // stale internal slot. Test262 builtin-coerce-lastindex.js etc. - // require coerce+round-trip. - var tsRegExpSetLabel = il.DefineLabel(); - if (_features.UsesRegExp) + // $Object handler - call obj.GetProperty(name) which handles getters. + // If the property is missing (HasProperty=false) and there's no own PDS + // descriptor either, walk the prototype chain via PDSGetPrototype. + // Required for Test262 `Con.prototype = proto; new Con(); obj.length` + // patterns where length lives on the prototype, and conversely + // `Object.defineProperty(child, "length", {value:2})` (PDS-only) must + // override an inherited accessor on proto. + il.MarkLabel(tsObjectLabel); + // Object.prototype method short-circuits (Stage 4z15 follow-on): + // expose hasOwnProperty + isPrototypeOf as $TSFunction wrappers + // bound to this $Object. + void EmitTSObjProtoCheck(string jsName, MethodBuilder helper) { + var skip = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, jsName); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, skip); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSRegExpType); - il.Emit(OpCodes.Brtrue, tsRegExpSetLabel); + il.Emit(OpCodes.Ldtoken, helper); + il.Emit(OpCodes.Ldtoken, helper.DeclaringType!); + il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", + _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); + il.Emit(OpCodes.Ret); + il.MarkLabel(skip); } + EmitTSObjProtoCheck("hasOwnProperty", runtime.HasOwnPropertyHelperMethod); + EmitTSObjProtoCheck("isPrototypeOf", runtime.IsPrototypeOfHelperMethod); - // System.Type (class reference used as value, e.g. `Scalar.PLAIN = 'x'`). JS allows - // arbitrary static property assignment on classes; we store them in PropertyDescriptorStore. - var typeSetLabel = il.DefineLabel(); + var tsObjectInstanceLocal = il.DeclareLocal(runtime.TSObjectType); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Type); - il.Emit(OpCodes.Brtrue, typeSetLabel); - - // List — raw arrays from `[...]` literal / Array.prototype.concat - // / etc. accept arbitrary named property assignment per ECMA-262 §23.1.5 - // [[DefineOwnProperty]]. Numeric indices are handled via SetIndex; only - // string keys land here. Store named-non-numeric writes in PDS as data - // descriptors so GetProperty / hasOwn / gOPD round-trip. Pre-fix these - // fell to SetFieldsProperty which silently dropped them. - var listSetPropLabel = il.DefineLabel(); + il.Emit(OpCodes.Castclass, runtime.TSObjectType); + il.Emit(OpCodes.Stloc, tsObjectInstanceLocal); + // if (obj.HasProperty(name)) return obj.GetProperty(name) + il.Emit(OpCodes.Ldloc, tsObjectInstanceLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, runtime.TSObjectHasProperty); + var tsObjectCheckPDS = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, tsObjectCheckPDS); + il.Emit(OpCodes.Ldloc, tsObjectInstanceLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, runtime.TSObjectGetProperty); + il.Emit(OpCodes.Ret); + // Check PDS for own data descriptor / accessor before walking chain + il.MarkLabel(tsObjectCheckPDS); + var tsObjectPDSDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Ldloc, tsObjectInstanceLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); + il.Emit(OpCodes.Stloc, tsObjectPDSDescLocal); + var tsObjectWalkProto = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, tsObjectPDSDescLocal); + il.Emit(OpCodes.Brfalse, tsObjectWalkProto); + // Has own descriptor: getter wins, else return value + var tsObjectPDSValue = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, tsObjectPDSDescLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorGetter.GetGetMethod()!); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, tsObjectPDSValue); + // Invoke getter via InvokeMethodValue(obj, getter, []) + var tsObjectGetterLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Stloc, tsObjectGetterLocal); + il.Emit(OpCodes.Ldloc, tsObjectInstanceLocal); + il.Emit(OpCodes.Ldloc, tsObjectGetterLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Call, runtime.InvokeMethodValue); + il.Emit(OpCodes.Ret); + il.MarkLabel(tsObjectPDSValue); + il.Emit(OpCodes.Pop); // discard the null getter + il.Emit(OpCodes.Ldloc, tsObjectPDSDescLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorValue.GetGetMethod()!); + il.Emit(OpCodes.Ret); + // Walk prototype chain + il.MarkLabel(tsObjectWalkProto); + var tsObjectProtoLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldloc, tsObjectInstanceLocal); + il.Emit(OpCodes.Call, runtime.PDSGetPrototype); + il.Emit(OpCodes.Stloc, tsObjectProtoLocal); + var tsObjectNoProto = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, tsObjectProtoLocal); + il.Emit(OpCodes.Brfalse, tsObjectNoProto); + // Recursively call GetProperty(prototype, name) + il.Emit(OpCodes.Ldloc, tsObjectProtoLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, method); + il.Emit(OpCodes.Ret); + il.MarkLabel(tsObjectNoProto); + // No own prototype — fall back to Object.prototype singleton (mirrors + // the dict-branch fallback). Catches `({}.toString)` style accesses on + // $Object instances created without an explicit prototype link. + var tsObjProtoFallbackMissLabel = il.DefineLabel(); + il.Emit(OpCodes.Call, runtime.ObjectPrototypePopulateMethod); + var tsObjProtoFallbackLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldsfld, runtime.ObjectPrototypeField); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloca, tsObjProtoFallbackLocal); + il.Emit(OpCodes.Callvirt, _types.DictionaryStringObject.GetMethod("TryGetValue", + [_types.String, _types.Object.MakeByRefType()])!); + il.Emit(OpCodes.Brfalse, tsObjProtoFallbackMissLabel); + il.Emit(OpCodes.Ldloc, tsObjProtoFallbackLocal); + il.Emit(OpCodes.Ret); + il.MarkLabel(tsObjProtoFallbackMissLabel); + // Property absent on object and Object.prototype — return undefined. + il.Emit(OpCodes.Ldsfld, runtime.UndefinedInstance); + il.Emit(OpCodes.Ret); + + il.MarkLabel(nullLabel); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ret); + + // $TSFunction handler - call GetFunctionMethod(func, name) + il.MarkLabel(tsFunctionLabel); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.ListOfObject); - il.Emit(OpCodes.Brtrue, listSetPropLabel); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.GetFunctionMethod); + il.Emit(OpCodes.Ret); - // Not a dict or $Object or $TSFunction or $CJSModule or Type - try SetFieldsProperty for class instances + // $BoundTSFunction handler - call GetFunctionMethod(func, name) + il.MarkLabel(boundFunctionLabel); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Call, runtime.SetFieldsProperty); + il.Emit(OpCodes.Call, runtime.GetFunctionMethod); il.Emit(OpCodes.Ret); - // List handler: same shape as $TSArray's non-length path. - il.MarkLabel(listSetPropLabel); + // $CJSModule handler - call module.GetMember(name) — only emitted when + // UsesCjsRequire is on (matching the dispatch arm above). + if (_features.UsesCjsRequire) { - // Skip if key is "length" or a numeric index — those write paths - // belong to SetIndex (numeric) / a dedicated length path. Silent - // no-op for "length" matches current behavior; named numeric - // string falls through to PDS (close enough, integer-key writes - // through SetProperty are rare). - var listSetIsLengthLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "length"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brtrue, listSetIsLengthLabel); - // Frozen guard. - var listSetNotFrozenLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.PDSIsFrozen); - il.Emit(OpCodes.Brfalse, listSetNotFrozenLabel); - il.Emit(OpCodes.Ret); - il.MarkLabel(listSetNotFrozenLabel); - // Existing-descriptor writable=false guard. - var listSetExistingDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); + il.MarkLabel(cjsModuleGetLabel); il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.CjsModuleType); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); - il.Emit(OpCodes.Stloc, listSetExistingDescLocal); - var listSetWritableLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, listSetExistingDescLocal); - il.Emit(OpCodes.Brfalse, listSetWritableLabel); - il.Emit(OpCodes.Ldloc, listSetExistingDescLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorWritable.GetGetMethod()!); - il.Emit(OpCodes.Brtrue, listSetWritableLabel); - il.Emit(OpCodes.Ret); - il.MarkLabel(listSetWritableLabel); - // Install fresh data descriptor with the value. - EmitDefineDataDescriptorFromValue(il, runtime); - il.MarkLabel(listSetIsLengthLabel); + il.Emit(OpCodes.Callvirt, runtime.CjsModuleType.GetMethod("GetMember", [_types.String])!); il.Emit(OpCodes.Ret); } - // $RegExp handler: route `r.lastIndex = value` to the typed setter - // with JS ToLength coercion. Other property writes fall through to - // SetFieldsProperty so user data-property assignments - // (`Object.defineProperty(r, 'foo', {writable:true}); r.foo = ...`) - // still hit the user-property bag. + // $RegExp handler — surface built-in slots via typed getters, plus + // flag-string-parsed accessors (sticky/unicode/hasIndices/dotAll/ + // unicodeSets) for the JS-spec flags that don't have dedicated + // .NET-side fields. Unknown property names fall through to + // GetFieldsProperty so user-set data (`r.foo = 1`, descriptor- + // installed properties) still resolves correctly. if (_features.UsesRegExp) { - il.MarkLabel(tsRegExpSetLabel); - - // PDS-first: if user installed an accessor descriptor with a - // setter, invoke it. If a data descriptor with writable=false - // is present, silently swallow (non-strict; strict-mode is - // handled by the strict variant). Mirrors the GET-side fix - // for spec-aligned override semantics on $RegExp instances. - var setNoPdsLabel = il.DefineLabel(); - var setPdsDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); + il.MarkLabel(tsRegExpGetLabel); + var rxLocal = il.DeclareLocal(runtime.TSRegExpType); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSRegExpType); + il.Emit(OpCodes.Stloc, rxLocal); + + // ECMA-262 §22.2.6.* read paths go through ordinary Get, so + // user-installed Object.defineProperty(r, 'flags', {get}) etc. + // must win over the internal slot. Check PDS first for any name; + // when a descriptor is present, surface its value (data) or + // invoke its getter (accessor) before reaching the typed slot + // fast-paths below. Symbol.match's get-flags-err.js, + // builtin-coerce-lastindex.js and friends rely on the override + // path running before the internal-slot read. + var pdsDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); - il.Emit(OpCodes.Stloc, setPdsDescLocal); - il.Emit(OpCodes.Ldloc, setPdsDescLocal); - il.Emit(OpCodes.Brfalse, setNoPdsLabel); - - // Accessor setter? Setter slot non-null → InvokeWithThis(rx, value). - var setNoAccessorLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, setPdsDescLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorSetter.GetGetMethod()!); - var setterValueLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Stloc, setterValueLocal); - il.Emit(OpCodes.Ldloc, setterValueLocal); - il.Emit(OpCodes.Brfalse, setNoAccessorLabel); - var setterFnLocal = il.DeclareLocal(runtime.TSFunctionType); - il.Emit(OpCodes.Ldloc, setterValueLocal); + il.Emit(OpCodes.Stloc, pdsDescLocal); + var noPdsDescLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, pdsDescLocal); + il.Emit(OpCodes.Brfalse, noPdsDescLabel); + // Accessor descriptor? Getter != null → invoke fn(thisArg=rx). + var dataDescLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, pdsDescLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorGetter.GetGetMethod()!); + var regexpGetterLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Stloc, regexpGetterLocal); + il.Emit(OpCodes.Ldloc, regexpGetterLocal); + il.Emit(OpCodes.Brfalse, dataDescLabel); + // Cast getter to $TSFunction and InvokeWithThis(rx). If the + // descriptor's getter slot isn't a $TSFunction (shouldn't + // happen normally), fall through to the data path. + var regexpFnLocal = il.DeclareLocal(runtime.TSFunctionType); + il.Emit(OpCodes.Ldloc, regexpGetterLocal); il.Emit(OpCodes.Isinst, runtime.TSFunctionType); - il.Emit(OpCodes.Stloc, setterFnLocal); - il.Emit(OpCodes.Ldloc, setterFnLocal); - il.Emit(OpCodes.Brfalse, setNoAccessorLabel); - il.Emit(OpCodes.Ldloc, setterFnLocal); + il.Emit(OpCodes.Stloc, regexpFnLocal); + il.Emit(OpCodes.Ldloc, regexpFnLocal); + il.Emit(OpCodes.Brfalse, dataDescLabel); + il.Emit(OpCodes.Ldloc, regexpFnLocal); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Newarr, _types.Object); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Call, _types.GetMethod(typeof(System.Array), "Empty").MakeGenericMethod(_types.Object)); il.Emit(OpCodes.Callvirt, runtime.TSFunctionInvokeWithThis); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ret); - il.MarkLabel(setNoAccessorLabel); - - // No setter slot — check getter. If getter present (accessor - // descriptor), this is getter-only → silently no-op (non-strict). - var setSilentlyIgnoreLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, setPdsDescLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorGetter.GetGetMethod()!); - il.Emit(OpCodes.Brtrue, setSilentlyIgnoreLabel); - // No getter and no setter → data descriptor. Honor writable bit. - il.Emit(OpCodes.Ldloc, setPdsDescLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorWritable.GetGetMethod()!); - il.Emit(OpCodes.Brfalse, setSilentlyIgnoreLabel); - il.Emit(OpCodes.Ldloc, setPdsDescLocal); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorValue.GetSetMethod()!); - il.Emit(OpCodes.Ret); - il.MarkLabel(setSilentlyIgnoreLabel); - il.Emit(OpCodes.Ret); - - il.MarkLabel(setNoPdsLabel); - - var notLastIndexLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "lastIndex"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notLastIndexLabel); - - // ECMA-262: lastIndex is an ordinary writable data property — store - // the assigned value as-is (ToLength is deferred to RegExpBuiltinExec). - // Only an OBJECT (whose ToLength would run user `valueOf`) needs the - // boxed slot to defer that call + preserve identity; primitives - // (null/undefined/number/string/bool) ToLength with no user code, so - // they fold straight into the typed int slot (and clear the box). - // NB: JS `null` ToLengths to 0 — it must take the primitive path, not - // the box (a boxed C# null is indistinguishable from "no box"). - var numericSetLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Brfalse, numericSetLabel); // null - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Isinst, _types.Double); - il.Emit(OpCodes.Brtrue, numericSetLabel); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brtrue, numericSetLabel); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Isinst, _types.Boolean); - il.Emit(OpCodes.Brtrue, numericSetLabel); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, numericSetLabel); - // object → rx._lastIndexBoxed = value (defer ToLength/valueOf to exec) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSRegExpType); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Stfld, _tsRegExpLastIndexBoxedField); - il.Emit(OpCodes.Ret); - il.MarkLabel(numericSetLabel); - // primitive: rx._lastIndex = ToLength(value); rx._lastIndexBoxed = null - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSRegExpType); - il.Emit(OpCodes.Ldarg_2); - EmitToLengthBoxed(il, runtime); - il.Emit(OpCodes.Callvirt, runtime.TSRegExpLastIndexSetter); // also clears boxed il.Emit(OpCodes.Ret); - - il.MarkLabel(notLastIndexLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Call, runtime.SetFieldsProperty); + il.MarkLabel(dataDescLabel); + // Data descriptor — return descriptor.Value. + il.Emit(OpCodes.Ldloc, pdsDescLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorValue.GetGetMethod()!); il.Emit(OpCodes.Ret); - } + il.MarkLabel(noPdsDescLabel); - // System.Type handler: store as data descriptor in PropertyDescriptorStore. - // Read path (EmitGetProperty) looks it up before falling through to .NET member - // resolution, so writes become visible as reads. - il.MarkLabel(typeSetLabel); - { - // Per ECMA-262 §17, constructor static "prototype"/"name"/"length" - // are non-writable; non-strict writes silently no-op. Same for - // Number constants (MAX_VALUE etc.) which are W:F,E:F,C:F. - var typeSetSkipLabel = il.DefineLabel(); - void EmitTypeSetSkipName(string n) + // Helper closure to emit a name-equality test + branch to a + // labelled body. Keeps the dispatch table readable. + void NameMatchBranch(string propName, System.Action emitBody) { - var notNameLabel = il.DefineLabel(); + var notThisName = il.DefineLabel(); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, n); + il.Emit(OpCodes.Ldstr, propName); il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notNameLabel); + il.Emit(OpCodes.Brfalse, notThisName); + emitBody(); il.Emit(OpCodes.Ret); - il.MarkLabel(notNameLabel); + il.MarkLabel(notThisName); } - EmitTypeSetSkipName("prototype"); - EmitTypeSetSkipName("name"); - EmitTypeSetSkipName("length"); - // Number constants — non-writable on the Number constructor. - var notNumberTypeForSetLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldtoken, _types.Double); - il.Emit(OpCodes.Call, _types.Type.GetMethod("GetTypeFromHandle")!); - il.Emit(OpCodes.Bne_Un, notNumberTypeForSetLabel); - EmitTypeSetSkipName("MAX_VALUE"); - EmitTypeSetSkipName("MIN_VALUE"); - EmitTypeSetSkipName("NaN"); - EmitTypeSetSkipName("POSITIVE_INFINITY"); - EmitTypeSetSkipName("NEGATIVE_INFINITY"); - EmitTypeSetSkipName("MAX_SAFE_INTEGER"); - EmitTypeSetSkipName("MIN_SAFE_INTEGER"); - EmitTypeSetSkipName("EPSILON"); - il.MarkLabel(notNumberTypeForSetLabel); - - EmitDefineDataDescriptorFromValue(il, runtime); - il.Emit(OpCodes.Ret); - } - // $CJSModule handler — only "exports" is writable; others are no-ops (spec behavior). - if (_features.UsesCjsRequire) - { - il.MarkLabel(cjsModuleSetLabel); - EmitCjsModuleExportsSetBranch(il, runtime); - } + // "lastIndex" — return the raw boxed value when a non-numeric value + // was assigned (object identity preserved per spec); otherwise the + // typed int as a boxed double. + NameMatchBranch("lastIndex", () => + { + var numericLabel = il.DefineLabel(); + var doneLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, rxLocal); + il.Emit(OpCodes.Ldfld, _tsRegExpLastIndexBoxedField); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, numericLabel); + il.Emit(OpCodes.Br, doneLabel); // boxed non-null → return it + il.MarkLabel(numericLabel); + il.Emit(OpCodes.Pop); // drop the null + il.Emit(OpCodes.Ldloc, rxLocal); + il.Emit(OpCodes.Callvirt, runtime.TSRegExpLastIndexGetter); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Box, _types.Double); + il.MarkLabel(doneLabel); + }); + // "constructor" — inherited from RegExp.prototype.constructor, which + // is the RegExp constructor (the $RegExp Type token, == what `RegExp` + // evaluates to as a value). Without this an instance read returns + // undefined and `re.constructor === RegExp` is false, blocking the + // §22.2.4.1 call-form same-object check. PDS is checked above, so a + // user `Object.defineProperty(re,'constructor',…)` / `re.constructor=x` + // still wins. + NameMatchBranch("constructor", () => + { + il.Emit(OpCodes.Ldtoken, runtime.TSRegExpType); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); + }); + // "source" / "flags" — string fields. + NameMatchBranch("source", () => + { + il.Emit(OpCodes.Ldloc, rxLocal); + il.Emit(OpCodes.Callvirt, runtime.TSRegExpSourceGetter); + }); + // Spec-aligned ECMA-262 §22.2.6.4 — assemble the flags string from + // individual property reads so user-installed `Object.defineProperty + // (r, 'global', {get})` overrides participate in the chain. Each + // Get(rx, propName) goes through this very GetProperty recursively, + // so PDS-first lookup on the per-flag property fires first; the + // typed slot fallback returns the same boxed bool we'd have read + // directly, keeping the assembled string identical to _flags for + // ordinary $RegExp without overrides. Unlocks Symbol.match/replace/ + // split/search's get-global-err / coerce-global / get-unicode-error + // test262 family. + NameMatchBranch("flags", () => + { + var sbLocal = il.DeclareLocal(typeof(System.Text.StringBuilder)); + il.Emit(OpCodes.Newobj, typeof(System.Text.StringBuilder).GetConstructor(Type.EmptyTypes)!); + il.Emit(OpCodes.Stloc, sbLocal); - // $Array handler — `arr.length = N` routes through SetLength. Any - // other name falls off into the normal silent-ignore (JS permits - // arbitrary named writes on arrays but we don't persist them). - // ECMA-262 23.1.4.1 [[DefineOwnProperty]] for "length": if - // ToNumber(value) !== ToUint32(value) - // (i.e. value is not a non-negative integer ≤ 2^32 - 1), throw - // RangeError. Pre-fix Convert.ToInt64 rounded `1.5 → 2` silently. - il.MarkLabel(tsArraySetPropLabel); - { - var notLengthLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "length"); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notLengthLabel); - - // Coerce value through ToNumber (handles strings, booleans, etc.) - // then enforce ToUint32 round-trip. The .NET `Convert.ToInt64` path - // truncates fractional parts; we need to flag fractional / NaN / - // out-of-range as a RangeError instead. - var doubleValLocal = il.DeclareLocal(_types.Double); - var u32Local = il.DeclareLocal(_types.Int64); - var validLengthLabel = il.DefineLabel(); - var rangeErrorLabel = il.DefineLabel(); - - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Call, runtime.ToNumber); - il.Emit(OpCodes.Stloc, doubleValLocal); - - // Reject NaN / +Infinity / -Infinity via IsFinite. - il.Emit(OpCodes.Ldloc, doubleValLocal); - il.Emit(OpCodes.Call, _types.Double.GetMethod("IsFinite", [_types.Double])!); - il.Emit(OpCodes.Brfalse, rangeErrorLabel); - - // ToUint32 reciprocity: cast to long, ensure 0..2^32-1 inclusive, - // and that the round-trip back to double matches the original. - il.Emit(OpCodes.Ldloc, doubleValLocal); - il.Emit(OpCodes.Conv_I8); - il.Emit(OpCodes.Stloc, u32Local); - // negative → throw - il.Emit(OpCodes.Ldloc, u32Local); - il.Emit(OpCodes.Ldc_I8, 0L); - il.Emit(OpCodes.Blt, rangeErrorLabel); - // > uint.MaxValue → throw - il.Emit(OpCodes.Ldloc, u32Local); - il.Emit(OpCodes.Ldc_I8, (long)uint.MaxValue); - il.Emit(OpCodes.Bgt, rangeErrorLabel); - // round-trip mismatch (fractional component) → throw - il.Emit(OpCodes.Ldloc, u32Local); - il.Emit(OpCodes.Conv_R8); - il.Emit(OpCodes.Ldloc, doubleValLocal); - il.Emit(OpCodes.Bne_Un, rangeErrorLabel); - il.Emit(OpCodes.Br, validLengthLabel); + var sbAppendChar = typeof(System.Text.StringBuilder).GetMethod("Append", [typeof(char)])!; + void AppendIfTruthy(string propName, char ch) + { + var skipLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldstr, propName); + il.Emit(OpCodes.Call, method); + il.Emit(OpCodes.Call, runtime.IsTruthy); + il.Emit(OpCodes.Brfalse, skipLabel); + il.Emit(OpCodes.Ldloc, sbLocal); + il.Emit(OpCodes.Ldc_I4, (int)ch); + il.Emit(OpCodes.Callvirt, sbAppendChar); + il.Emit(OpCodes.Pop); + il.MarkLabel(skipLabel); + } + + // Order per ECMA-262 §22.2.6.4. + AppendIfTruthy("hasIndices", 'd'); + AppendIfTruthy("global", 'g'); + AppendIfTruthy("ignoreCase", 'i'); + AppendIfTruthy("multiline", 'm'); + AppendIfTruthy("dotAll", 's'); + AppendIfTruthy("unicode", 'u'); + AppendIfTruthy("unicodeSets", 'v'); + AppendIfTruthy("sticky", 'y'); + + il.Emit(OpCodes.Ldloc, sbLocal); + il.Emit(OpCodes.Callvirt, typeof(System.Text.StringBuilder).GetMethod("ToString", Type.EmptyTypes)!); + }); + // "global" / "ignoreCase" / "multiline" — boolean fields. + NameMatchBranch("global", () => + { + il.Emit(OpCodes.Ldloc, rxLocal); + il.Emit(OpCodes.Callvirt, runtime.TSRegExpGlobalGetter); + il.Emit(OpCodes.Box, _types.Boolean); + }); + NameMatchBranch("ignoreCase", () => + { + il.Emit(OpCodes.Ldloc, rxLocal); + il.Emit(OpCodes.Callvirt, runtime.TSRegExpIgnoreCaseGetter); + il.Emit(OpCodes.Box, _types.Boolean); + }); + NameMatchBranch("multiline", () => + { + il.Emit(OpCodes.Ldloc, rxLocal); + il.Emit(OpCodes.Callvirt, runtime.TSRegExpMultilineGetter); + il.Emit(OpCodes.Box, _types.Boolean); + }); - il.MarkLabel(rangeErrorLabel); - il.Emit(OpCodes.Ldstr, "Invalid array length"); - il.Emit(OpCodes.Newobj, runtime.TSRangeErrorCtor); - il.Emit(OpCodes.Call, runtime.CreateException); - il.Emit(OpCodes.Throw); + // "sticky" / "unicode" / "hasIndices" / "dotAll" / "unicodeSets" + // — parsed from the flags string. There's no dedicated field for + // these, so we Contains-check the appropriate char (per ECMA-262 + // §22.2.5.3 flags-string assembly). + void FlagCharBranch(string propName, char ch) + { + NameMatchBranch(propName, () => + { + il.Emit(OpCodes.Ldloc, rxLocal); + il.Emit(OpCodes.Callvirt, runtime.TSRegExpFlagsGetter); + // s.Contains(ch) – use Contains(char) overload to dodge + // string-literal allocation for the single-char arg. + il.Emit(OpCodes.Ldc_I4, (int)ch); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "Contains", _types.Char)); + il.Emit(OpCodes.Box, _types.Boolean); + }); + } + FlagCharBranch("sticky", 'y'); + FlagCharBranch("unicode", 'u'); + FlagCharBranch("hasIndices", 'd'); + FlagCharBranch("dotAll", 's'); + FlagCharBranch("unicodeSets", 'v'); - il.MarkLabel(validLengthLabel); + // Other property names fall through to GetFieldsProperty so + // user-set data (`r.foo = 1`) and prototype walks still resolve. il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSArrayType); - il.Emit(OpCodes.Ldloc, u32Local); - il.Emit(OpCodes.Callvirt, runtime.TSArraySetLength); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.GetFieldsProperty); il.Emit(OpCodes.Ret); + } + + // Promise (Task or $Promise) handler - return TSFunction wrappers for then/catch/finally + il.MarkLabel(promiseLabel); + // First, extract the underlying Task if this is a $Promise object + // Store the task in a local variable for use in creating TSFunction wrappers + var taskLocal = il.DeclareLocal(_types.TaskOfObject); + var isTSPromiseLabel = il.DefineLabel(); + var haveTaskLabel = il.DefineLabel(); + + // Check if obj is $Promise + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSPromiseType); + il.Emit(OpCodes.Brtrue, isTSPromiseLabel); + + // It's a raw Task, use directly + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.TaskOfObject); + il.Emit(OpCodes.Stloc, taskLocal); + il.Emit(OpCodes.Br, haveTaskLabel); - il.MarkLabel(notLengthLabel); - // Other named writes on arrays go to PDS as data descriptors. - // ECMA-262 23.1.5: arrays are exotic objects but accept arbitrary - // named property assignment via [[DefineOwnProperty]]. Test262 - // patterns like `var arr = []; arr.foo = ...; arr.foo()` rely on - // this. GetFieldsProperty's PDS-data-descriptor arm reads it back. - // Pre-fix unconditionally overwrote — Object.freeze(arr) didn't - // block `arr.foo = "x"` because the existing PDS descriptor's - // writable bit (false post-freeze via AND-mask) was ignored. - { - // Honor frozen state: if Object.isFrozen(arr), silently no-op - // (non-strict). Strict callers use SetPropertyStrict which - // can throw. - var arrFrozenLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.PDSIsFrozen); - il.Emit(OpCodes.Brfalse, arrFrozenLabel); - il.Emit(OpCodes.Ret); - il.MarkLabel(arrFrozenLabel); - - // Honor existing-descriptor writable=false: if there's a PDS - // data descriptor for this key with writable=false, silently - // no-op. Accessor descriptors fall through (defining a value - // over an accessor is handled by PDSDefineProperty). - var arrNotWritableLabel = il.DefineLabel(); - var arrExistingDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); - il.Emit(OpCodes.Stloc, arrExistingDescLocal); - il.Emit(OpCodes.Ldloc, arrExistingDescLocal); - il.Emit(OpCodes.Brfalse, arrNotWritableLabel); - // Has descriptor; check writable. - il.Emit(OpCodes.Ldloc, arrExistingDescLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorWritable.GetGetMethod()!); - il.Emit(OpCodes.Brtrue, arrNotWritableLabel); - // Not writable — silent no-op. - il.Emit(OpCodes.Ret); - il.MarkLabel(arrNotWritableLabel); + // It's a $Promise, extract the Task property + il.MarkLabel(isTSPromiseLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSPromiseType); + il.Emit(OpCodes.Callvirt, runtime.TSPromiseTaskGetter); + il.Emit(OpCodes.Stloc, taskLocal); - EmitDefineDataDescriptorFromValue(il, runtime); - } - il.Emit(OpCodes.Ret); - } + il.MarkLabel(haveTaskLabel); - // $TSFunction handler: ECMA-262 §10.1.9 [[Set]] honors non-extensibility for new - // properties. Gate via PDSCanAddProperty so `Object.preventExtensions(fn); fn.x = v` - // silently no-ops (non-strict). Existing PDS entries still update. - il.MarkLabel(tsFunctionSetLabel); + // then/catch/finally: walk Promise.prototype dict so the wrappers + // returned are identical to those on Promise.prototype itself + // (`p.then === Promise.prototype.then`, spec-correct .length/.name + // derived from the __this-aware helper signatures). The previous + // implementation constructed bound $TSFunction wrappers per access + // — which broke identity AND reported length=3 / name="PromiseThen" + // for `p.then`. The PromiseThenHelper/PromiseCatchHelper/ + // PromiseFinallyHelper installed in PromisePrototypePopulate accept + // `__this` so chaining via `.call(p, fn)` works without needing + // task pre-binding. + var taskLocal2 = taskLocal; // silence unused-warning; kept for legacy + void EmitPromiseProtoLookup(string jsName) { - var tsFnDoSetLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); + var notThisLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSCanAddProperty); - il.Emit(OpCodes.Brtrue, tsFnDoSetLabel); - il.Emit(OpCodes.Ret); - il.MarkLabel(tsFnDoSetLabel); - - EmitDefineDataDescriptorFromValue(il, runtime); + il.Emit(OpCodes.Ldstr, jsName); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notThisLabel); + il.Emit(OpCodes.Call, runtime.PromisePrototypePopulateMethod); + var protoValLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldsfld, runtime.PromisePrototypeField); + il.Emit(OpCodes.Ldstr, jsName); + il.Emit(OpCodes.Ldloca, protoValLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "TryGetValue", + _types.String, _types.Object.MakeByRefType())); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldloc, protoValLocal); il.Emit(OpCodes.Ret); + il.MarkLabel(notThisLabel); } + EmitPromiseProtoLookup("then"); + EmitPromiseProtoLookup("catch"); + EmitPromiseProtoLookup("finally"); - // $Object handler. First check PDS for a setter accessor descriptor - // (defineProperty-installed). If present, invoke it (passing $TSObject - // as `this`) — TSObject.SetProperty doesn't know about PDS-stored - // setters. Otherwise delegate to TSObject.SetProperty for the dict / - // _getters / _setters fast path. - il.MarkLabel(tsObjectLabel); - { - var tsObjPdsSetterLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloca, tsObjPdsSetterLocal); - il.Emit(OpCodes.Call, runtime.PDSTryGetSetter); - var tsObjNoPdsSetterLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, tsObjNoPdsSetterLabel); - // Invoke PDS setter. - EmitInvokePdsSetterWithValueAndReturn(il, runtime, tsObjPdsSetterLocal); - il.MarkLabel(tsObjNoPdsSetterLabel); - } - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSObjectType); + // ECMA-262 §27.2.5.1: Promise.prototype.constructor is %Promise%. + // Bare `Promise` resolves to typeof(Task) in compiled mode + // (TryEmitBuiltInClassType / GlobalThisGetProperty), so return the + // same Type token here for identity: + // `Promise.resolve(1).constructor === Promise` (#221 increment). + // #242 subclass instances ($Promise-derived emitted classes) return + // their own class token instead, so `MyP.resolve(1).constructor === MyP`. + // Generic subclasses carry constructed types (MyP) while bare + // `MyP` emits the open definition — return the definition (same gap + // InstanceOf's generic-definition walk handles). + var notPromiseCtorLabel = il.DefineLabel(); + var defaultPromiseCtorLabel = il.DefineLabel(); + var nonGenericCtorLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Callvirt, runtime.TSObjectSetProperty); + il.Emit(OpCodes.Ldstr, "constructor"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notPromiseCtorLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSPromiseType); + il.Emit(OpCodes.Brfalse, defaultPromiseCtorLabel); + var ctorTypeLocal = il.DeclareLocal(_types.Type); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "GetType")); + il.Emit(OpCodes.Stloc, ctorTypeLocal); + il.Emit(OpCodes.Ldloc, ctorTypeLocal); + il.Emit(OpCodes.Ldtoken, runtime.TSPromiseType); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Beq, defaultPromiseCtorLabel); + il.Emit(OpCodes.Ldloc, ctorTypeLocal); + il.Emit(OpCodes.Callvirt, _types.Type.GetProperty("IsGenericType")!.GetGetMethod()!); + il.Emit(OpCodes.Brfalse, nonGenericCtorLabel); + il.Emit(OpCodes.Ldloc, ctorTypeLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Type, "GetGenericTypeDefinition")); + il.Emit(OpCodes.Ret); + il.MarkLabel(nonGenericCtorLabel); + il.Emit(OpCodes.Ldloc, ctorTypeLocal); + il.Emit(OpCodes.Ret); + il.MarkLabel(defaultPromiseCtorLabel); + il.Emit(OpCodes.Ldtoken, _types.TaskOfObject); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); il.Emit(OpCodes.Ret); + il.MarkLabel(notPromiseCtorLabel); - il.MarkLabel(nullLabel); + // Unknown promise property - return null + il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Ret); il.MarkLabel(dictLabel); - - // $AbortSignal dict surface (#985): `signal.onabort = cb` on a dynamically- - // typed (`any`) signal receiver. The typed path intercepts at compile time - // (AbortSignalEmitter.TryEmitPropertySet); an `any` receiver lands here and - // would otherwise store a plain "onabort" dict key that FireAbortEvent (which - // reads the internal "_onabort" slot) never sees — so the handler never fired. - // Signals are identified by their "_reasonSet" internal slot. Gated on - // UsesAbortController so non-signal programs pay nothing; mirrors the GetProperty - // signal branch (#224). - if (_features.UsesAbortController) + // Object.prototype methods short-circuit: return $TSFunction wrappers + // for the helper. target=null + cached name+length means the wrapper + // dispatches via InvokeWithThis (the helper's first param is "__this" + // so _expectsThis=true), letting .call(receiver, ...) inject the right + // receiver instead of being shadowed by a target-bound prepending + // that double-applies and trims the wrong tail. Direct dispatch + // (`obj.method(args)`) still works because compiled-mode method calls + // route through InvokeMethodValue → InvokeWithThis with the receiver + // as thisArg. JS-spec name + length surface to user code via fn.name + // / fn.length introspection. + void EmitObjProtoMethodCheck(string jsName, MethodBuilder helper, int jsLength) { - var notSignalOnAbortSet = il.DefineLabel(); + var skip = il.DefineLabel(); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, "onabort"); + il.Emit(OpCodes.Ldstr, jsName); il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, notSignalOnAbortSet); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); - il.Emit(OpCodes.Ldstr, "_reasonSet"); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); - il.Emit(OpCodes.Brfalse, notSignalOnAbortSet); - il.Emit(OpCodes.Ldarg_0); // signal - il.Emit(OpCodes.Ldarg_2); // handler - il.Emit(OpCodes.Call, runtime.AbortSignalSetOnAbort); + il.Emit(OpCodes.Brfalse, skip); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ldtoken, helper); + il.Emit(OpCodes.Ldtoken, helper.DeclaringType!); + il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", + _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + il.Emit(OpCodes.Ldstr, jsName); + il.Emit(OpCodes.Ldc_I4, jsLength); + il.Emit(OpCodes.Newobj, runtime.TSFunctionCtorWithCache); il.Emit(OpCodes.Ret); - il.MarkLabel(notSignalOnAbortSet); + il.MarkLabel(skip); } + EmitObjProtoMethodCheck("hasOwnProperty", runtime.HasOwnPropertyHelperMethod, 1); + EmitObjProtoMethodCheck("isPrototypeOf", runtime.IsPrototypeOfHelperMethod, 1); - // For dictionaries, check frozen/sealed tables and silently ignore if frozen/sealed - var sealedCheckLabel = il.DefineLabel(); - var doSetLabel = il.DefineLabel(); - var valueLocal = il.DeclareLocal(_types.Object); - - // Check if frozen: _frozenObjects.TryGetValue(obj, out _). - // Per ECMA-262, freeze only forbids writes to DATA properties — accessor - // setters still fire because the descriptor's writable bit doesn't apply - // to accessors. So when there's a PDS setter for this key, fall through - // to the doSetLabel path (which invokes it). Pre-fix dropped frozen - // accessor writes silently — broke test262 15.2.3.9-2-c-{2,3,4}. - il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloca, valueLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); - var frozenNotFoundLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, frozenNotFoundLabel); - // Frozen — only allow if there's a PDS accessor setter for this key. - var frozenAccessorSetterLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloca, frozenAccessorSetterLocal); - il.Emit(OpCodes.Call, runtime.PDSTryGetSetter); - il.Emit(OpCodes.Brfalse, nullLabel); // No setter, frozen data — silent return - il.Emit(OpCodes.Br, doSetLabel); // Has setter — proceed (doSetLabel re-fetches via PDSTryGetSetter) - il.MarkLabel(frozenNotFoundLabel); - - // Check if sealed and property doesn't exist - il.Emit(OpCodes.Ldsfld, runtime.SealedObjectsField); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloca, valueLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); - var extensibleCheckLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, extensibleCheckLabel); // Not sealed, check extensibility - - // Object is sealed - check if property exists (dict OR PDS). Pre-fix - // only checked the backing dict, so an accessor-only own property - // (defineProperty with get/set + configurable:false) was treated as - // missing and the write silently dropped — including its setter. - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); - il.Emit(OpCodes.Brtrue, doSetLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); - il.Emit(OpCodes.Brfalse, nullLabel); // Property doesn't exist, silently return - il.Emit(OpCodes.Br, doSetLabel); // Property exists on sealed object, proceed to set - - // Check extensibility via $PropertyDescriptorStore.CanAddProperty - fully standalone, no reflection - il.MarkLabel(extensibleCheckLabel); - il.Emit(OpCodes.Ldarg_0); // obj - il.Emit(OpCodes.Ldarg_1); // name - il.Emit(OpCodes.Call, runtime.PDSCanAddProperty); - il.Emit(OpCodes.Brfalse, nullLabel); // Cannot add property, silently return - - // Actually set the property - il.MarkLabel(doSetLabel); - - // Check for setter accessor via $PropertyDescriptorStore - fully standalone, no reflection - var setterLocal = il.DeclareLocal(_types.Object); - var noSetterLabel = il.DefineLabel(); + // Check for getter accessor via $PropertyDescriptorStore - fully standalone, no reflection + var getterLocal = il.DeclareLocal(_types.Object); + var noGetterLabel = il.DefineLabel(); - // Call PDSTryGetSetter(obj, name, out setter) + // Call PDSTryGetGetter(obj, name, out getter) il.Emit(OpCodes.Ldarg_0); // obj il.Emit(OpCodes.Ldarg_1); // name - il.Emit(OpCodes.Ldloca, setterLocal); // out setter - il.Emit(OpCodes.Call, runtime.PDSTryGetSetter); - il.Emit(OpCodes.Brfalse, noSetterLabel); + il.Emit(OpCodes.Ldloca, getterLocal); // out getter + il.Emit(OpCodes.Call, runtime.PDSTryGetGetter); + il.Emit(OpCodes.Brfalse, noGetterLabel); - // Setter was found - invoke it via InvokeMethodValue(obj, setter, [value]) - EmitInvokePdsSetterWithValueAndReturn(il, runtime, setterLocal); + // Getter was found - invoke it via InvokeMethodValue(obj, getter, emptyArgs) + il.Emit(OpCodes.Ldarg_0); // receiver (obj) + il.Emit(OpCodes.Ldloc, getterLocal); // function (getter) + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, _types.Object); // empty args array + il.Emit(OpCodes.Call, runtime.InvokeMethodValue); + il.Emit(OpCodes.Ret); - il.MarkLabel(noSetterLabel); + il.MarkLabel(noGetterLabel); - // Check if property is writable via $PropertyDescriptorStore - fully standalone, no reflection - il.Emit(OpCodes.Ldarg_0); // obj - il.Emit(OpCodes.Ldarg_1); // name - il.Emit(OpCodes.Call, runtime.PDSIsWritable); - il.Emit(OpCodes.Brfalse, nullLabel); // Not writable, silently return + // dict.TryGetValue(name, out value) ? value : check prototype chain + var valueLocal = il.DeclareLocal(_types.Object); + var dictLocal = il.DeclareLocal(_types.DictionaryStringObject); + var protoLocal = il.DeclareLocal(_types.Object); + // Store the dictionary in a local for later use with BindThis il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "set_Item")); - il.Emit(OpCodes.Ret); - } - - /// - /// Emits inline IL that coerces the boxed-object value at the top of the - /// stack to int32 via the JS ToLength algorithm: null/undefined/NaN → 0, - /// false → 0, true → 1, double via truncate (clamped to int32), int via - /// pass-through, string via TryParse → double-path (or 0 on parse - /// failure), other types → 0. Used by SetProperty's $RegExp arm so - /// `r.lastIndex = '1.9'` stores 1. - /// - private void EmitToLengthBoxed(ILGenerator il, EmittedRuntime runtime) - { - var localVal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Stloc, localVal); - - var nullLabel = il.DefineLabel(); - var undefinedLabel = il.DefineLabel(); - var doubleLabel = il.DefineLabel(); - var intLabel = il.DefineLabel(); - var boolLabel = il.DefineLabel(); - var stringLabel = il.DefineLabel(); - var doneLabel = il.DefineLabel(); - - il.Emit(OpCodes.Ldloc, localVal); - il.Emit(OpCodes.Brfalse, nullLabel); - - il.Emit(OpCodes.Ldloc, localVal); - il.Emit(OpCodes.Isinst, runtime.UndefinedType); - il.Emit(OpCodes.Brtrue, undefinedLabel); - - il.Emit(OpCodes.Ldloc, localVal); - il.Emit(OpCodes.Isinst, _types.Double); - il.Emit(OpCodes.Brtrue, doubleLabel); - - il.Emit(OpCodes.Ldloc, localVal); - il.Emit(OpCodes.Isinst, _types.Int32); - il.Emit(OpCodes.Brtrue, intLabel); - - il.Emit(OpCodes.Ldloc, localVal); - il.Emit(OpCodes.Isinst, _types.Boolean); - il.Emit(OpCodes.Brtrue, boolLabel); - - il.Emit(OpCodes.Ldloc, localVal); - il.Emit(OpCodes.Isinst, _types.String); - il.Emit(OpCodes.Brtrue, stringLabel); - - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Br, doneLabel); - - il.MarkLabel(nullLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Br, doneLabel); + il.Emit(OpCodes.Stloc, dictLocal); - il.MarkLabel(undefinedLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Br, doneLabel); - - il.MarkLabel(doubleLabel); - var dTmp = il.DeclareLocal(_types.Double); - il.Emit(OpCodes.Ldloc, localVal); - il.Emit(OpCodes.Unbox_Any, _types.Double); - il.Emit(OpCodes.Stloc, dTmp); - - // NaN check: (d == d) is false iff d is NaN. - var dNonNanLabel = il.DefineLabel(); - var dPositiveLabel = il.DefineLabel(); - var dClampLabel = il.DefineLabel(); - var dInRangeLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, dTmp); - il.Emit(OpCodes.Ldloc, dTmp); - il.Emit(OpCodes.Ceq); - il.Emit(OpCodes.Brtrue, dNonNanLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Br, doneLabel); + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloca, valueLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "TryGetValue")); + var foundLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, foundLabel); - il.MarkLabel(dNonNanLabel); - il.Emit(OpCodes.Ldloc, dTmp); - il.Emit(OpCodes.Ldc_R8, 0.0); - il.Emit(OpCodes.Bgt, dPositiveLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Br, doneLabel); + // $AbortSignal dict surface (#224, #985): the properties "aborted"/"reason"/ + // "onabort" AND the methods "addEventListener"/"removeEventListener"/ + // "throwIfAborted" on a dynamically-typed (`any`) signal receiver. The typed + // path intercepts at compile time (AbortSignalEmitter); an `any` receiver lands + // here. The methods are returned as $TSFunction wrappers (target=null, + // _expectsThis=true via the "__this" first parameter) so a subsequent + // InvokeMethodValue injects the signal as the receiver — i.e. + // `signal.addEventListener('abort', cb)` works from inside a helper, not only at + // a statically-typed call site. Signals are identified by their "_reasonSet" + // internal slot — the public keys are computed from the CancellationToken, so + // they are never own dict entries and always reach this miss path. Name screen + // runs first to keep ordinary dict misses cheap. + if (_features.UsesAbortController) + { + var notSignalPropLabel = il.DefineLabel(); + var signalNameMatchLabel = il.DefineLabel(); + var strEq = _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String); - il.MarkLabel(dPositiveLabel); - il.Emit(OpCodes.Ldloc, dTmp); - il.Emit(OpCodes.Ldc_R8, (double)int.MaxValue); - il.Emit(OpCodes.Blt, dInRangeLabel); - il.Emit(OpCodes.Ldc_I4, int.MaxValue); - il.Emit(OpCodes.Br, doneLabel); + // Returns a $TSFunction wrapping `helper`, bound to no target, so the + // wrapper's _expectsThis is set (helper's first param is "__this") and + // InvokeMethodValue passes the signal receiver through. Same shape as + // EmitObjProtoMethodCheck's hasOwnProperty/isPrototypeOf wrappers. + void EmitSignalMethodWrapper(MethodBuilder helper, string jsName, int jsLength) + { + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ldtoken, helper); + il.Emit(OpCodes.Ldtoken, helper.DeclaringType!); + il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", + _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + il.Emit(OpCodes.Ldstr, jsName); + il.Emit(OpCodes.Ldc_I4, jsLength); + il.Emit(OpCodes.Newobj, runtime.TSFunctionCtorWithCache); + il.Emit(OpCodes.Ret); + } - il.MarkLabel(dInRangeLabel); - il.Emit(OpCodes.Ldloc, dTmp); - il.Emit(OpCodes.Conv_I4); - il.Emit(OpCodes.Br, doneLabel); + foreach (var signalProp in new[] + { "aborted", "reason", "onabort", "addEventListener", "removeEventListener", "throwIfAborted" }) + { + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, signalProp); + il.Emit(OpCodes.Call, strEq); + il.Emit(OpCodes.Brtrue, signalNameMatchLabel); + } + il.Emit(OpCodes.Br, notSignalPropLabel); - il.MarkLabel(intLabel); - il.Emit(OpCodes.Ldloc, localVal); - il.Emit(OpCodes.Unbox_Any, _types.Int32); - il.Emit(OpCodes.Br, doneLabel); + il.MarkLabel(signalNameMatchLabel); + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Ldstr, "_reasonSet"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); + il.Emit(OpCodes.Brfalse, notSignalPropLabel); - il.MarkLabel(boolLabel); - il.Emit(OpCodes.Ldloc, localVal); - il.Emit(OpCodes.Unbox_Any, _types.Boolean); - il.Emit(OpCodes.Br, doneLabel); + var notSignalAbortedLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "aborted"); + il.Emit(OpCodes.Call, strEq); + il.Emit(OpCodes.Brfalse, notSignalAbortedLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.AbortSignalGetAborted); + il.Emit(OpCodes.Box, _types.Boolean); + il.Emit(OpCodes.Ret); + il.MarkLabel(notSignalAbortedLabel); - il.MarkLabel(stringLabel); - var sTmp = il.DeclareLocal(_types.Double); - var parseFailLabel = il.DefineLabel(); - var sPosLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, localVal); - il.Emit(OpCodes.Castclass, _types.String); - il.Emit(OpCodes.Ldc_I4, (int)System.Globalization.NumberStyles.Float); - il.Emit(OpCodes.Call, _types.GetProperty(typeof(System.Globalization.CultureInfo), "InvariantCulture").GetGetMethod()!); - il.Emit(OpCodes.Ldloca, sTmp); - il.Emit(OpCodes.Call, typeof(double).GetMethod("TryParse", - [_types.String, typeof(System.Globalization.NumberStyles), typeof(IFormatProvider), typeof(double).MakeByRefType()])!); - il.Emit(OpCodes.Brfalse, parseFailLabel); - il.Emit(OpCodes.Ldloc, sTmp); - il.Emit(OpCodes.Ldloc, sTmp); - il.Emit(OpCodes.Ceq); - il.Emit(OpCodes.Brfalse, parseFailLabel); - il.Emit(OpCodes.Ldloc, sTmp); - il.Emit(OpCodes.Ldc_R8, 0.0); - il.Emit(OpCodes.Bgt, sPosLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Br, doneLabel); + var notSignalReasonLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "reason"); + il.Emit(OpCodes.Call, strEq); + il.Emit(OpCodes.Brfalse, notSignalReasonLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.AbortSignalGetReason); + il.Emit(OpCodes.Ret); + il.MarkLabel(notSignalReasonLabel); - il.MarkLabel(sPosLabel); - il.Emit(OpCodes.Ldloc, sTmp); - il.Emit(OpCodes.Conv_I4); - il.Emit(OpCodes.Br, doneLabel); + var notSignalOnAbortLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "onabort"); + il.Emit(OpCodes.Call, strEq); + il.Emit(OpCodes.Brfalse, notSignalOnAbortLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.AbortSignalGetOnAbort); + il.Emit(OpCodes.Ret); + il.MarkLabel(notSignalOnAbortLabel); - il.MarkLabel(parseFailLabel); - il.Emit(OpCodes.Ldc_I4_0); + var notSignalAelLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "addEventListener"); + il.Emit(OpCodes.Call, strEq); + il.Emit(OpCodes.Brfalse, notSignalAelLabel); + EmitSignalMethodWrapper(runtime.AbortSignalAddEventListenerThis, "addEventListener", 2); + il.MarkLabel(notSignalAelLabel); - il.MarkLabel(doneLabel); - } + var notSignalRelLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "removeEventListener"); + il.Emit(OpCodes.Call, strEq); + il.Emit(OpCodes.Brfalse, notSignalRelLabel); + EmitSignalMethodWrapper(runtime.AbortSignalRemoveEventListenerThis, "removeEventListener", 2); + il.MarkLabel(notSignalRelLabel); - /// - /// Emits SetPropertyStrict(object obj, string name, object value, bool strictMode) -> void - /// In strict mode, throws TypeError for modifications to frozen objects or new properties on sealed objects. - /// - private void EmitSetPropertyStrict(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "SetPropertyStrict", - MethodAttributes.Public | MethodAttributes.Static, - _types.Void, - [_types.Object, _types.String, _types.Object, _types.Boolean] - ); - runtime.SetPropertyStrict = method; + // Only "throwIfAborted" remains among the screened names. + EmitSignalMethodWrapper(runtime.AbortSignalThrowIfAbortedThis, "throwIfAborted", 0); - var il = method.GetILGenerator(); - var nullLabel = il.DefineLabel(); - var dictLabel = il.DefineLabel(); + il.MarkLabel(notSignalPropLabel); + } - // null check + // Property not found on object - check prototype chain + // Get prototype: $PropertyDescriptorStore.GetPrototype(obj) il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Brfalse, nullLabel); + il.Emit(OpCodes.Call, runtime.PDSGetPrototype); + il.Emit(OpCodes.Stloc, protoLocal); - // Without this redirect, a strict-mode top-level `this.foo = v` - // (compiled `this` resolves to the globalThis sentinel) fell through - // to SetFieldsPropertyStrict and was dropped — e.g. Test262 - // Object/create 15.2.3.5-4-177 / defineProperty 15.2.3.6-3-230 set - // `this.value` / `this.get` and reuse `this` as a descriptor. - EmitGlobalThisSetRedirect(il, runtime); + // If prototype is null, return undefined + var returnUndefinedLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, protoLocal); + il.Emit(OpCodes.Brfalse, returnUndefinedLabel); - // Check if $Object - var sharpTSObjectLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brtrue, sharpTSObjectLabel); + // Recursively call GetProperty(prototype, name) to check prototype chain + il.Emit(OpCodes.Ldloc, protoLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, method); // Recursive call to GetProperty + il.Emit(OpCodes.Ret); - // Dictionary - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, dictLabel); + il.MarkLabel(returnUndefinedLabel); + // ECMA-262: `({}).constructor === Object`. If user hasn't set a custom + // constructor and no prototype overrides it, return typeof(object) which + // matches what compiled-mode `Object` resolves to via globalThis. + var notDictCtorLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "constructor"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notDictCtorLabel); + il.Emit(OpCodes.Ldtoken, _types.Object); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Ret); + il.MarkLabel(notDictCtorLabel); + // ECMA-262 19.1.3: every plain object inherits from Object.prototype. + // For Dictionary literals (`{}` etc.) without an explicit prototype, + // fall back to the ObjectPrototypeField singleton — that's where + // `valueOf`, `toString`, `propertyIsEnumerable`, and the toLocaleString + // wrapper live. Required for Test262 patterns that do + // `({}).toString.call(receiver)` or for ToPrimitive coercion to find + // the inherited methods on plain dicts. Lazy-populates on first read. + var protoFallbackMissLabel = il.DefineLabel(); + il.Emit(OpCodes.Call, runtime.ObjectPrototypePopulateMethod); + var objProtoFallbackLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldsfld, runtime.ObjectPrototypeField); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloca, objProtoFallbackLocal); + il.Emit(OpCodes.Callvirt, _types.DictionaryStringObject.GetMethod("TryGetValue", + [_types.String, _types.Object.MakeByRefType()])!); + il.Emit(OpCodes.Brfalse, protoFallbackMissLabel); + il.Emit(OpCodes.Ldloc, objProtoFallbackLocal); + il.Emit(OpCodes.Ret); + il.MarkLabel(protoFallbackMissLabel); + // Return $Undefined.Instance for non-existent properties (JavaScript semantics) + il.Emit(OpCodes.Ldsfld, runtime.UndefinedInstance); + il.Emit(OpCodes.Ret); + il.MarkLabel(foundLabel); - // $TSFunction — mirror the non-strict SetProperty branch (functions as objects carry - // user-assigned properties through PDSDefineProperty). - var tsFunctionSetStrictLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); + // If value is a TSFunction, call BindThis(dict) on it + // to bind 'this' for object method shorthand + var notTSFunction = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, valueLocal); il.Emit(OpCodes.Isinst, runtime.TSFunctionType); - il.Emit(OpCodes.Brtrue, tsFunctionSetStrictLabel); - - // $CJSModule — mirror the non-strict branch. Gated on UsesCjsRequire. - var cjsModuleSetStrictLabel = il.DefineLabel(); - if (_features.UsesCjsRequire) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.CjsModuleType); - il.Emit(OpCodes.Brtrue, cjsModuleSetStrictLabel); - } + il.Emit(OpCodes.Brfalse, notTSFunction); - // $Array / List — named property write on an array in strict - // mode. The non-strict SetProperty has dedicated array branches (length - // coercion, numeric, PDS data descriptors); without a strict equivalent - // these writes fell to SetFieldsPropertyStrict, which silently dropped - // them, so `arr.foo = fn; arr.foo()` saw `foo === undefined` under "use - // strict" (Test262 Array slice/splice S15.4.4.*_A*_T*, the common - // `arr.getClass = Object.prototype.toString` pattern). Detect arrays here - // and route to arraySetStrictLabel, which enforces strict frozen / - // non-writable throws then reuses the non-strict store logic. - var arraySetStrictLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSArrayType); - il.Emit(OpCodes.Brtrue, arraySetStrictLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.ListOfObject); - il.Emit(OpCodes.Brtrue, arraySetStrictLabel); + // Call func.BindThis(dict) + il.Emit(OpCodes.Ldloc, valueLocal); + il.Emit(OpCodes.Castclass, runtime.TSFunctionType); + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Callvirt, runtime.TSFunctionBindThis); - // Not a dict or $Object or $TSFunction or $CJSModule or array - fall back to SetFieldsPropertyStrict. - // NOTE (#1131): unlike the non-strict SetProperty, this variant has no - // dedicated Proxy / $RegExp / System.Type receiver branches — those - // receivers fall through here. Preserved as-is by the strict/non-strict - // dedup (behavior-preserving); tracked as drift in the epic notes. - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Ldarg_3); // strictMode - il.Emit(OpCodes.Call, runtime.SetFieldsPropertyStrict); + il.MarkLabel(notTSFunction); + il.Emit(OpCodes.Ldloc, valueLocal); il.Emit(OpCodes.Ret); - // $Array / List strict handler: ECMA-262 §10.4.2.1 / OrdinarySet - // step 5 — a frozen array or an own non-writable data property makes the - // assignment fail; in strict mode PutValue then throws a TypeError. We - // honor those two cases here, then delegate the actual store to the - // non-strict SetProperty (shared length/numeric/PDS-data-descriptor path). - il.MarkLabel(arraySetStrictLabel); + // Map (Dictionary) handler - check for "size" property. + // Gated together with the dispatch arm above. + if (_features.UsesMap) { - var arrayDoStoreLabel = il.DefineLabel(); - - // Frozen array → throw "Cannot assign to read only property 'name'". - var arrayNotFrozenLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.PDSIsFrozen); - il.Emit(OpCodes.Brfalse, arrayNotFrozenLabel); - EmitThrowTypeErrorWithName(il, runtime, "Cannot assign to read only property '", "' of object '[object Array]'"); - il.MarkLabel(arrayNotFrozenLabel); - - // Own non-writable DATA descriptor → throw. Accessor descriptors - // (setter present) and writable/absent descriptors fall through to - // the store, where SetProperty invokes the setter or overwrites. - var arrayDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); - il.Emit(OpCodes.Ldarg_0); + il.MarkLabel(mapLabel); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); - il.Emit(OpCodes.Stloc, arrayDescLocal); - il.Emit(OpCodes.Ldloc, arrayDescLocal); - il.Emit(OpCodes.Brfalse, arrayDoStoreLabel); // no descriptor → store - il.Emit(OpCodes.Ldloc, arrayDescLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorSetter.GetGetMethod()!); - il.Emit(OpCodes.Brtrue, arrayDoStoreLabel); // accessor → delegate (invokes setter) - il.Emit(OpCodes.Ldloc, arrayDescLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorWritable.GetGetMethod()!); - il.Emit(OpCodes.Brtrue, arrayDoStoreLabel); // writable → store - EmitThrowTypeErrorWithName(il, runtime, "Cannot assign to read only property '", "' of object '[object Array]'"); - - il.MarkLabel(arrayDoStoreLabel); + il.Emit(OpCodes.Ldstr, "size"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notMapSizeLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notMapSizeLabel); + // Return map.Count as double + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.DictionaryObjectObject); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.DictionaryObjectObject, "Count").GetGetMethod()!); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Box, _types.Double); + il.Emit(OpCodes.Ret); + il.MarkLabel(notMapSizeLabel); + // For other Map properties, dispatch via GetMapProperty — returns a $BoundMapMethod + // wrapper for known methods (get/set/has/...) so that `typeof m.get === 'function'` + // and `m.get.call(m, k)` work on a Map received from another module. il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.DictionaryObjectObject); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Call, runtime.SetProperty); + il.Emit(OpCodes.Call, runtime.GetMapProperty); il.Emit(OpCodes.Ret); } - // $CJSModule strict handler — same as non-strict for now. Gated. - if (_features.UsesCjsRequire) + // Set (HashSet) handler - dispatch via GetSetProperty for size + + // $BoundSetMethod wrappers on known methods (add/has/delete/... plus ES2025 + // set ops). Mirrors the Map handler above. + if (_features.UsesSet) { - il.MarkLabel(cjsModuleSetStrictLabel); - EmitCjsModuleExportsSetBranch(il, runtime); + il.MarkLabel(setLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.HashSetOfObject); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.GetSetProperty); + il.Emit(OpCodes.Ret); } - // $TSFunction handler: create data descriptor with the value, store via PDSDefineProperty - il.MarkLabel(tsFunctionSetStrictLabel); - EmitDefineDataDescriptorFromValue(il, runtime); - il.Emit(OpCodes.Ret); - - // $Object - call SetPropertyStrict - il.MarkLabel(sharpTSObjectLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSObjectType); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Ldarg_3); // strictMode - il.Emit(OpCodes.Callvirt, runtime.TSObjectSetPropertyStrict); - il.Emit(OpCodes.Ret); - - il.MarkLabel(nullLabel); - il.Emit(OpCodes.Ret); - - il.MarkLabel(dictLabel); - // For dictionaries, check frozen/sealed tables - var frozenCheckLabel = il.DefineLabel(); - var sealedCheckLabel = il.DefineLabel(); - var doSetLabel = il.DefineLabel(); - var valueLocal = il.DeclareLocal(_types.Object); - - // Check if frozen: _frozenObjects.TryGetValue(obj, out _) - il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloca, valueLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); - il.Emit(OpCodes.Brfalse, sealedCheckLabel); // Not frozen, check sealed - - // Object is frozen - check strict mode - il.Emit(OpCodes.Ldarg_3); // strictMode - il.Emit(OpCodes.Brfalse, nullLabel); // Not strict, silently return - - // Strict mode and frozen - throw TypeError - EmitThrowTypeErrorWithName(il, runtime, "Cannot assign to read only property '", "' of object"); - - // Check if sealed and property doesn't exist - il.MarkLabel(sealedCheckLabel); - il.Emit(OpCodes.Ldsfld, runtime.SealedObjectsField); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloca, valueLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); - var extensibleCheckLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, extensibleCheckLabel); // Not sealed, check extensibility - - // Object is sealed - check if property exists - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); - il.Emit(OpCodes.Brtrue, doSetLabel); // Property exists, can modify - - // Property doesn't exist and object is sealed - check strict mode - il.Emit(OpCodes.Ldarg_3); // strictMode - il.Emit(OpCodes.Brfalse, nullLabel); // Not strict, silently return - - // Strict mode and sealed with new property - throw TypeError - EmitThrowTypeErrorWithName(il, runtime, "Cannot add property '", "' to a sealed object"); - - // Check extensibility via $PropertyDescriptorStore.CanAddProperty - fully standalone, no reflection - il.MarkLabel(extensibleCheckLabel); - il.Emit(OpCodes.Ldarg_0); // obj - il.Emit(OpCodes.Ldarg_1); // name - il.Emit(OpCodes.Call, runtime.PDSCanAddProperty); - il.Emit(OpCodes.Brtrue, doSetLabel); // Can add property, proceed to set - - // Cannot add property (non-extensible) - check strict mode - il.Emit(OpCodes.Ldarg_3); // strictMode - il.Emit(OpCodes.Brfalse, nullLabel); // Not strict, silently return - - // Strict mode and non-extensible with new property - throw TypeError - EmitThrowTypeErrorWithName(il, runtime, "Cannot add property '", "' to a non-extensible object"); - - // Actually set the property. Mirrors the non-strict SetProperty doSet - // arm: honor a PDS accessor setter, and an existing non-writable data - // descriptor (or getter-only accessor). Pre-fix the strict path skipped - // straight to dict.set_Item, so under "use strict": (1) an accessor - // setter was bypassed and overwritten with a data value, and (2) writes - // to a writable:false property neither threw nor were suppressed — - // `verifyProperty`'s isWritable probe then saw the write succeed (Test262 - // Object/defineProperty 15.2.3.6-3-181 et al.). - il.MarkLabel(doSetLabel); - - // PDS accessor setter present → invoke it (ECMA-262 OrdinarySet: a setter - // fires regardless of strictness). - var strictSetterLocal = il.DeclareLocal(_types.Object); - var strictNoSetterLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloca, strictSetterLocal); - il.Emit(OpCodes.Call, runtime.PDSTryGetSetter); - il.Emit(OpCodes.Brfalse, strictNoSetterLabel); - EmitInvokePdsSetterWithValueAndReturn(il, runtime, strictSetterLocal); - il.MarkLabel(strictNoSetterLabel); - - // Non-writable (data writable:false, or getter-only accessor) → strict - // throws TypeError (ECMA-262 §6.2.5.6 / PutValue), sloppy silently - // returns. PDSIsWritable returns true when no descriptor exists. - il.Emit(OpCodes.Ldarg_0); + il.MarkLabel(listLabel); + // Check for "length" il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSIsWritable); - var strictWritableLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, strictWritableLabel); - il.Emit(OpCodes.Ldarg_3); // strictMode - il.Emit(OpCodes.Brfalse, nullLabel); // sloppy → silent return - EmitThrowTypeErrorWithName(il, runtime, "Cannot assign to read only property '", "' of object"); - il.MarkLabel(strictWritableLabel); - + il.Emit(OpCodes.Ldstr, "length"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notLengthLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notLengthLabel); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "set_Item")); + il.Emit(OpCodes.Castclass, _types.ListOfObject); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count").GetGetMethod()!); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Box, _types.Double); il.Emit(OpCodes.Ret); - } - - /// - /// Emits SetIndexStrict(object obj, object index, object value, bool strictMode) -> void - /// In strict mode, throws TypeError for modifications to frozen/sealed arrays. - /// - private void EmitSetIndexStrict(TypeBuilder typeBuilder, EmittedRuntime runtime) - { - var method = typeBuilder.DefineMethod( - "SetIndexStrict", - MethodAttributes.Public | MethodAttributes.Static, - _types.Void, - [_types.Object, _types.Object, _types.Object, _types.Boolean] - ); - runtime.SetIndexStrict = method; - - var il = method.GetILGenerator(); - var nullLabel = il.DefineLabel(); - var sharpTSArrayLabel = il.DefineLabel(); - var listLabel = il.DefineLabel(); - var dictLabel = il.DefineLabel(); - - // null check - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Brfalse, nullLabel); - - // Symbol-keyed write (symbols work on any receiver type) — route to the - // non-strict SetIndex, which has the symbol-key handler (registered - // accessor setter, else symbol-dict store with extensibility checks). - // SetIndexStrict otherwise dispatches only by RECEIVER type ($Array / - // List / Dictionary) and would ToString a symbol key into a bogus string - // key, dropping the write — so under "use strict" `obj[sym] = v` and - // `obj[Symbol.iterator] = fn` were lost (Test262 Object/ - // getOwnPropertySymbols object-contains-symbol-property-without-description, - // Array/from iter-get-iter-val-err). - // The store itself (registered setter / symbol-dict write) is delegated - // to the non-strict SetIndex, but its symbol handler silently no-ops on a - // frozen/sealed/non-extensible receiver. In strict mode those must throw - // a TypeError (ECMA-262 PutValue), so enforce that here first: - // frozen → always throw (props non-writable + non-extensible); - // sealed/non-ext → throw only when the symbol key is NEW (adding it); - // otherwise → delegate to SetIndex (store / update / invoke setter). - // (Test262 Object/freeze frozen-object-contains-symbol-properties-strict.) - var notSymbolKeyLabel = il.DefineLabel(); + il.MarkLabel(notLengthLabel); + // ECMA-262: `[].constructor === Array`. Compiled `Array` resolves to + // typeof(IList) — return that here. + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "constructor"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notListCtorLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notListCtorLabel); + il.Emit(OpCodes.Ldtoken, _types.IListOfObject); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Ret); + il.MarkLabel(notListCtorLabel); + // Numeric-string index — `GetProperty(list, "0")` must return list[0] so + // that `f[0]` for `f.__proto__ === [1,2,3]` walks the prototype chain + // and finds the array element. Without this branch the proto-chain walk + // bottoms out in GetListProperty's null fallback. + var listIdxLocal = il.DeclareLocal(_types.Int32); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.IsSymbolMethod); - il.Emit(OpCodes.Brfalse, notSymbolKeyLabel); - - var symThrowLabel = il.DefineLabel(); - var symRouteLabel = il.DefineLabel(); - var symStateTmp = il.DeclareLocal(_types.Object); - var cwtTryGetValue = _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType()); - - // frozen → throw. - il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloca, symStateTmp); - il.Emit(OpCodes.Callvirt, cwtTryGetValue); - il.Emit(OpCodes.Brtrue, symThrowLabel); - - // sealed OR non-extensible → throw only if the symbol key is not already present. - var symSealedOrNonExtLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldsfld, runtime.SealedObjectsField); + il.Emit(OpCodes.Ldloca, listIdxLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Int32, "TryParse", _types.String, _types.Int32.MakeByRefType())); + var listNotIndexLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, listNotIndexLabel); + il.Emit(OpCodes.Ldloc, listIdxLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Blt, listNotIndexLabel); + il.Emit(OpCodes.Ldloc, listIdxLocal); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloca, symStateTmp); - il.Emit(OpCodes.Callvirt, cwtTryGetValue); - il.Emit(OpCodes.Brtrue, symSealedOrNonExtLabel); - il.Emit(OpCodes.Ldsfld, runtime.NonExtensibleObjectsField); + il.Emit(OpCodes.Castclass, _types.ListOfObject); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count").GetGetMethod()!); + il.Emit(OpCodes.Bge, listNotIndexLabel); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloca, symStateTmp); - il.Emit(OpCodes.Callvirt, cwtTryGetValue); - il.Emit(OpCodes.Brfalse, symRouteLabel); // extensible → store - il.MarkLabel(symSealedOrNonExtLabel); - // sealed/non-ext: present symbol → allow update (route); absent → throw. - var symDictLocal = il.DeclareLocal(_types.DictionaryObjectObject); + il.Emit(OpCodes.Castclass, _types.ListOfObject); + il.Emit(OpCodes.Ldloc, listIdxLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Item").GetGetMethod()!); + il.Emit(OpCodes.Ret); + il.MarkLabel(listNotIndexLabel); + // PDS-stored own descriptor (e.g., RegExp.exec result has `index` / + // `input` / `groups` attached via PropertyDescriptorStore so the + // returned value can be a real Array exotic — `instanceof Array` true + // — while still answering `result.index` / `result.input` correctly). + // Without this, those metadata properties are invisible. + var listPdsLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.GetSymbolDictMethod); - il.Emit(OpCodes.Stloc, symDictLocal); - il.Emit(OpCodes.Ldloc, symDictLocal); - il.Emit(OpCodes.Brfalse, symThrowLabel); // no symbol dict → key absent → throw - il.Emit(OpCodes.Ldloc, symDictLocal); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "ContainsKey", _types.Object)); - il.Emit(OpCodes.Brfalse, symThrowLabel); // key absent → throw - - il.MarkLabel(symRouteLabel); + il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); + il.Emit(OpCodes.Stloc, listPdsLocal); + il.Emit(OpCodes.Ldloc, listPdsLocal); + var listSkipPdsLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, listSkipPdsLabel); + il.Emit(OpCodes.Ldloc, listPdsLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorValue.GetGetMethod()!); + il.Emit(OpCodes.Ret); + il.MarkLabel(listSkipPdsLabel); + // For other properties on List (like methods push, pop, etc.), use GetListProperty il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.ListOfObject); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Call, runtime.SetIndex); + il.Emit(OpCodes.Call, runtime.GetListProperty); il.Emit(OpCodes.Ret); - il.MarkLabel(symThrowLabel); - EmitThrowTypeError(il, runtime, "Cannot assign to a read-only or non-extensible property"); - - il.MarkLabel(notSymbolKeyLabel); - - // Check if $Array (for strict mode support) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSArrayType); - il.Emit(OpCodes.Brtrue, sharpTSArrayLabel); - - // List + // object[] handler — `arguments`-shape receiver. "length" → array.Length; + // numeric-string indexes return the element; anything else → undefined. + il.MarkLabel(objectArrayLabel); + var objArrNotLengthLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "length"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, objArrNotLengthLabel); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.ListOfObjectNullable); - il.Emit(OpCodes.Brtrue, listLabel); - - // Dictionary + il.Emit(OpCodes.Castclass, _types.ObjectArray); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Box, _types.Double); + il.Emit(OpCodes.Ret); + il.MarkLabel(objArrNotLengthLabel); + // Try numeric-string index: int.TryParse(name, out i) + var objArrIdxLocal = il.DeclareLocal(_types.Int32); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloca, objArrIdxLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Int32, "TryParse", _types.String, _types.Int32.MakeByRefType())); + var objArrNotIndexLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, objArrNotIndexLabel); + // Bounds check: i >= 0 && i < arr.Length + il.Emit(OpCodes.Ldloc, objArrIdxLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Blt, objArrNotIndexLabel); + il.Emit(OpCodes.Ldloc, objArrIdxLocal); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, dictLabel); - - // $Object / $TSFunction / System.Type / other receivers: route indexed - // writes to SetPropertyStrict(obj, ToString(index), value, strictMode), - // mirroring how the non-strict SetIndex routes them to SetProperty. Pre-fix - // these fell through to a silent no-op under "use strict", so `child[0] = v` - // on a class instance / $Object dropped the element — which broke - // Array.prototype.{reduce,every,filter,indexOf}.call(nonArrayObj, …) on - // a `new Con()`-style receiver (Test262 Array/prototype/*/15.4.4.*-2-*). + il.Emit(OpCodes.Castclass, _types.ObjectArray); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Bge, objArrNotIndexLabel); + // Read element at index il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "ToString")); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Ldarg_3); // strictMode - il.Emit(OpCodes.Call, runtime.SetPropertyStrict); + il.Emit(OpCodes.Castclass, _types.ObjectArray); + il.Emit(OpCodes.Ldloc, objArrIdxLocal); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Ret); + il.MarkLabel(objArrNotIndexLabel); + // Other property → undefined + il.Emit(OpCodes.Ldsfld, runtime.UndefinedInstance); il.Emit(OpCodes.Ret); - // $Array - call SetStrict with index and strictMode via the long API. - // Stage E.2 M6: widened from TSArraySetStrict (int) to TSArraySetStrictLong - // so `"use strict"; arr[2147483648] = v` doesn't truncate to int.MinValue. - // Parallel to the M3 GetIndex/SetIndex widening. + // $Array handler - access Elements.Count for "length" il.MarkLabel(sharpTSArrayLabel); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "length"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notSharpTSArrayLengthLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notSharpTSArrayLengthLabel); + // Use the LongLength getter — not the int-clamped Length — so `.length` + // reads up to 2^32 - 1 survive (M3 acceptance: `a.length === 2147483649`). il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Castclass, runtime.TSArrayType); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Convert, "ToInt64", _types.Object)); - il.Emit(OpCodes.Ldarg_2); // value - il.Emit(OpCodes.Ldarg_3); // strictMode - il.Emit(OpCodes.Callvirt, runtime.TSArraySetStrictLong); - il.Emit(OpCodes.Ret); - - il.MarkLabel(nullLabel); + il.Emit(OpCodes.Callvirt, runtime.TSArrayLongLengthGetter); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Box, _types.Double); il.Emit(OpCodes.Ret); - - il.MarkLabel(listLabel); - // Check if frozen - in strict mode, throw TypeError - var listSetLabel = il.DefineLabel(); - var listFrozenCheckLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloca, listFrozenCheckLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); - var listNotFrozenLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, listNotFrozenLabel); - // Frozen - check strictMode and throw if true - il.Emit(OpCodes.Ldarg_3); // strictMode - var listFrozenSilentLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, listFrozenSilentLabel); - EmitThrowTypeError(il, runtime, "Cannot assign to read only property of frozen array"); - il.MarkLabel(listFrozenSilentLabel); - il.Emit(OpCodes.Ret); // Silently return in non-strict mode - il.MarkLabel(listNotFrozenLabel); - // Not frozen - set normally - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.ListOfObjectNullable); + il.MarkLabel(notSharpTSArrayLengthLabel); + // ECMA-262: `[].constructor === Array`. Mirror the listLabel branch. il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Unbox_Any, _types.Double); - il.Emit(OpCodes.Conv_I4); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ListOfObjectNullable, "set_Item", _types.Int32, _types.Object)); + il.Emit(OpCodes.Ldstr, "constructor"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notTSArrayCtorLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notTSArrayCtorLabel); + il.Emit(OpCodes.Ldtoken, _types.IListOfObject); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); il.Emit(OpCodes.Ret); - - il.MarkLabel(dictLabel); - // Dictionary string-key write: route to SetPropertyStrict(obj, ToString(idx), - // value, strictMode) so PDS accessor setters, non-writable data descriptors, - // and strict TypeErrors are honored identically to named-property writes. - // Pre-fix this did a raw dict.set_Item, bypassing the writable/setter checks — - // so under "use strict" `obj[name] = v` on a writable:false property neither - // threw nor was suppressed, which propertyHelper.js's isWritable probe (it - // writes via `obj[name] = …`) read back as "writable". (Test262 Object/ - // defineProperty 15.2.3.6-3-181, defineProperties 15.2.3.7-* et al.) - il.Emit(OpCodes.Ldarg_0); + il.MarkLabel(notTSArrayCtorLabel); + // Numeric-string index — same purpose as the listLabel branch above: + // proto-chain walks (`f.__proto__ === [1,2,3]; f[0]`) bottom out here + // when the prototype is a $Array. Without this, GetListProperty returns + // null for any digit-string name and the array element is invisible. + var tsArrIdxLocal = il.DeclareLocal(_types.Int32); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "ToString")); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Ldarg_3); // strictMode - il.Emit(OpCodes.Call, runtime.SetPropertyStrict); - il.Emit(OpCodes.Ret); - } - - /// - /// Emits DeleteProperty(object obj, string name) -> bool - /// Removes a property from an object and returns true if successful. - /// Returns false for frozen/sealed objects or if the object doesn't support deletion. - /// - private void EmitDeleteProperty(TypeBuilder typeBuilder, EmittedRuntime runtime) - => EmitDeletePropertyCore(typeBuilder, runtime, strict: false); - - /// - /// Emits DeleteProperty(object obj, string name) -> bool (non-strict) or - /// DeletePropertyStrict(object obj, string name, bool strictMode) -> bool. - /// On a failed delete (frozen/sealed object, non-configurable property) - /// the non-strict variant returns false; the strict variant throws a - /// TypeError when strictMode is set, else returns false. - /// The strict variant intentionally has NO $Array or System.Type receiver - /// branches, and its $TSFunction handler skips the frozen/sealed/PDS - /// configurability checks — preserved as-is from before the #1131 merge - /// (behavior-preserving refactor; see the epic notes for the drift list). - /// - private void EmitDeletePropertyCore(TypeBuilder typeBuilder, EmittedRuntime runtime, bool strict) - { - var method = typeBuilder.DefineMethod( - strict ? "DeletePropertyStrict" : "DeleteProperty", - MethodAttributes.Public | MethodAttributes.Static, - _types.Boolean, - strict ? [_types.Object, _types.String, _types.Boolean] : [_types.Object, _types.String] - ); - if (strict) - runtime.DeletePropertyStrict = method; - else - runtime.DeleteProperty = method; - - var il = method.GetILGenerator(); - var nullLabel = il.DefineLabel(); - var dictLabel = il.DefineLabel(); - var trueLabel = il.DefineLabel(); - - // Emits the failed-delete path: strict mode (arg 2 set) throws - // TypeError("Cannot delete property ''"), otherwise - // (non-strict variant, or strictMode == false) returns false. - void EmitDeleteFail(string suffix) - { - if (strict) - { - var sloppyLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Brfalse, sloppyLabel); - EmitThrowTypeErrorWithName(il, runtime, "Cannot delete property '", suffix); - il.MarkLabel(sloppyLabel); - } - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ret); - } - - // null check - return true (deleting from null is allowed in JS) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Brfalse, trueLabel); - - // Proxy check: uses obj.GetType().FullName comparison (no SharpTS.dll dependency) - var notProxyLabel = il.DefineLabel(); - EmitProxyDeleteCheck(il, () => il.Emit(OpCodes.Ldarg_0), () => il.Emit(OpCodes.Ldarg_1), notProxyLabel); - - il.MarkLabel(notProxyLabel); - - // Check if $TSObject - var sharpTSObjectLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSObjectType); - il.Emit(OpCodes.Brtrue, sharpTSObjectLabel); - - // $TSFunction — `delete fn.name` records in the per-instance set so - // the synthetic name/length descriptors stop reporting (ECMA-262 §17 - // configurable). Pre-fix this fell through to trueLabel without - // recording, so verifyProperty's isConfigurable failed. - var tsFunctionDelLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSFunctionType); - il.Emit(OpCodes.Brtrue, tsFunctionDelLabel); - - // $Array — `delete arr[i]` turns the slot into a hole. Must come - // BEFORE the Dictionary check (not relevant here, just ordering) - // and BEFORE the trueLabel fallthrough so actual deletions happen. - // (Non-strict only — the strict variant never had an $Array branch.) - var tsArrayDelLabel = il.DefineLabel(); - if (!strict) - { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, runtime.TSArrayType); - il.Emit(OpCodes.Brtrue, tsArrayDelLabel); - } - - // Dictionary + il.Emit(OpCodes.Ldloca, tsArrIdxLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Int32, "TryParse", _types.String, _types.Int32.MakeByRefType())); + var tsArrNotIndexLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, tsArrNotIndexLabel); + il.Emit(OpCodes.Ldloc, tsArrIdxLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Blt, tsArrNotIndexLabel); + il.Emit(OpCodes.Ldloc, tsArrIdxLocal); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, dictLabel); - - if (!strict) - { - // System.Type — `delete String.prototype` / `delete Number.MAX_VALUE`. - // Per ECMA-262 §17 + §22.x: built-in constructor's "prototype" data - // property is { writable:false, enumerable:false, configurable:false }; - // static constants likewise non-configurable. [[Delete]] returns false - // on non-configurable. Test262 S15.5.3.1_A3 verifies. PDS check first - // for user-installed override-descriptors with configurable=true. - // (Non-strict only — the strict variant never had a Type branch.) - var notTypeForDelLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, _types.Type); - il.Emit(OpCodes.Brfalse, notTypeForDelLabel); - var typeDelDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); - il.Emit(OpCodes.Stloc, typeDelDescLocal); - var typeNoPdsDescLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, typeDelDescLocal); - il.Emit(OpCodes.Brfalse, typeNoPdsDescLabel); - il.Emit(OpCodes.Ldloc, typeDelDescLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorConfigurable.GetGetMethod()!); - var typeConfigurableLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, typeConfigurableLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ret); - il.MarkLabel(typeConfigurableLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSDeleteProperty); - il.Emit(OpCodes.Pop); - // Also mark in the per-Type deletion tracker so the static-names list - // check in HasOwnPropertyHelper / gOPD doesn't resurrect this name. - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.MarkBuiltinDeletedMethod); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ret); - il.MarkLabel(typeNoPdsDescLabel); - // "prototype"/"name"/"length" are non-configurable on every built-in. - var typeBuiltinNameTrueLabel = il.DefineLabel(); - void EmitTypeBuiltinNameCheck(string n) - { - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, n); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brtrue, typeBuiltinNameTrueLabel); - } - EmitTypeBuiltinNameCheck("prototype"); - EmitTypeBuiltinNameCheck("name"); - EmitTypeBuiltinNameCheck("length"); - - // Number Type-specific non-configurable constants. Reflection - // probe below would miss these because JS names (UPPER_SNAKE_CASE) - // differ from .NET names (PascalCase): MAX_VALUE → double.MaxValue - // etc. Without this, `delete Number.MAX_VALUE` returned true - // (Test262 S15.7.3.2_A3). - var notNumberTypeForDelLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldtoken, _types.Double); - il.Emit(OpCodes.Call, _types.Type.GetMethod("GetTypeFromHandle")!); - il.Emit(OpCodes.Bne_Un, notNumberTypeForDelLabel); - void EmitNumberConstNameCheck(string n) - { - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, n); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brtrue, typeBuiltinNameTrueLabel); - } - EmitNumberConstNameCheck("MAX_VALUE"); - EmitNumberConstNameCheck("MIN_VALUE"); - EmitNumberConstNameCheck("NaN"); - EmitNumberConstNameCheck("POSITIVE_INFINITY"); - EmitNumberConstNameCheck("NEGATIVE_INFINITY"); - EmitNumberConstNameCheck("MAX_SAFE_INTEGER"); - EmitNumberConstNameCheck("MIN_SAFE_INTEGER"); - EmitNumberConstNameCheck("EPSILON"); - il.MarkLabel(notNumberTypeForDelLabel); - // Object/Array/String constructor static method names: per ECMA-262 - // §17, every other data property has configurable:true. Mark the - // deletion in the per-Type tracker so subsequent gOPD/hasOwn report - // the property as absent, then return true. (prototype/name/length - // and Number constants caught above are non-configurable.) - var objTypeDelLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldtoken, _types.Object); - il.Emit(OpCodes.Call, _types.Type.GetMethod("GetTypeFromHandle")!); - il.Emit(OpCodes.Bne_Un, objTypeDelLabel); - void EmitObjectMethodDelCheck(string n) - { - var skipLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldstr, n); - il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); - il.Emit(OpCodes.Brfalse, skipLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.MarkBuiltinDeletedMethod); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ret); - il.MarkLabel(skipLabel); - } - EmitObjectMethodDelCheck("assign"); EmitObjectMethodDelCheck("create"); - EmitObjectMethodDelCheck("defineProperties"); EmitObjectMethodDelCheck("defineProperty"); - EmitObjectMethodDelCheck("entries"); EmitObjectMethodDelCheck("freeze"); - EmitObjectMethodDelCheck("fromEntries"); EmitObjectMethodDelCheck("getOwnPropertyDescriptor"); - EmitObjectMethodDelCheck("getOwnPropertyDescriptors"); EmitObjectMethodDelCheck("getOwnPropertyNames"); - EmitObjectMethodDelCheck("getOwnPropertySymbols"); EmitObjectMethodDelCheck("getPrototypeOf"); - EmitObjectMethodDelCheck("groupBy"); EmitObjectMethodDelCheck("hasOwn"); EmitObjectMethodDelCheck("is"); - EmitObjectMethodDelCheck("isExtensible"); EmitObjectMethodDelCheck("isFrozen"); - EmitObjectMethodDelCheck("isSealed"); EmitObjectMethodDelCheck("keys"); - EmitObjectMethodDelCheck("preventExtensions"); EmitObjectMethodDelCheck("seal"); - EmitObjectMethodDelCheck("setPrototypeOf"); EmitObjectMethodDelCheck("values"); - il.MarkLabel(objTypeDelLabel); - - // Reflection: any static field/property on the Type → built-in own. - const System.Reflection.BindingFlags typeDelStaticPub = - System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static; - var typeDelLocal = il.DeclareLocal(_types.Type); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.Type); - il.Emit(OpCodes.Stloc, typeDelLocal); - il.Emit(OpCodes.Ldloc, typeDelLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldc_I4, (int)typeDelStaticPub); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetField", _types.String, typeof(System.Reflection.BindingFlags))); - il.Emit(OpCodes.Brtrue, typeBuiltinNameTrueLabel); - il.Emit(OpCodes.Ldloc, typeDelLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldc_I4, (int)typeDelStaticPub); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetProperty", _types.String, typeof(System.Reflection.BindingFlags))); - il.Emit(OpCodes.Brtrue, typeBuiltinNameTrueLabel); - // Not a built-in own property — return true (delete-missing = success). - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ret); - il.MarkLabel(typeBuiltinNameTrueLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ret); - il.MarkLabel(notTypeForDelLabel); - } - - // Other types - cannot delete properties, return true (JS behavior for non-configurable) - il.Emit(OpCodes.Br, trueLabel); - - // $TSFunction delete handler. - il.MarkLabel(tsFunctionDelLabel); - if (strict) - { - // Strict variant: record the deletion only — it intentionally skips - // the frozen/sealed/PDS configurability checks the non-strict - // handler performs (preserved pre-#1131 behavior). - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.MarkBuiltinDeletedMethod); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ret); - } - else - { - // Honor configurability: - // 1. If frozen or sealed (via CWT), return false (silent no-op). - // 2. If a PDS descriptor exists with configurable=false, return false - // without removing. - // 3. Otherwise: clean up PDS, then mark as deleted in the per-instance - // tracker so the synthetic descriptor stops reporting and direct - // property lookups return undefined. - // Frozen check. - var tsFnDelTmp = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloca, tsFnDelTmp); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); - var tsFnNotFrozenLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, tsFnNotFrozenLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ret); - - // Sealed check. - il.MarkLabel(tsFnNotFrozenLabel); - il.Emit(OpCodes.Ldsfld, runtime.SealedObjectsField); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloca, tsFnDelTmp); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); - var tsFnNotSealedLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, tsFnNotSealedLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ret); - - // PDS configurable check. - il.MarkLabel(tsFnNotSealedLabel); - var tsFnDelDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); - il.Emit(OpCodes.Stloc, tsFnDelDescLocal); - var tsFnNoPdsLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, tsFnDelDescLocal); - il.Emit(OpCodes.Brfalse, tsFnNoPdsLabel); - il.Emit(OpCodes.Ldloc, tsFnDelDescLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorConfigurable.GetGetMethod()!); - var tsFnDelConfigurableLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, tsFnDelConfigurableLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ret); - il.MarkLabel(tsFnDelConfigurableLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSDeleteProperty); - il.Emit(OpCodes.Pop); - il.MarkLabel(tsFnNoPdsLabel); - - // Mark as deleted in per-instance tracker (covers synthetic - // name/length and any other descriptor-less data entry). - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.MarkBuiltinDeletedMethod); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ret); - } + il.Emit(OpCodes.Castclass, _types.ListOfObject); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count").GetGetMethod()!); + il.Emit(OpCodes.Bge, tsArrNotIndexLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.ListOfObject); + il.Emit(OpCodes.Ldloc, tsArrIdxLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Item").GetGetMethod()!); + il.Emit(OpCodes.Ret); + il.MarkLabel(tsArrNotIndexLabel); + // For other properties on $Array (method names like push/pop/sort/etc.), + // reuse GetListProperty — it returns the $BoundArrayMethod wrapper, and + // $Array IS a List by inheritance, so the cast works. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.ListOfObject); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.GetListProperty); + il.Emit(OpCodes.Ret); - // $Array delete handler — if the key is a numeric index call - // DeleteAt; otherwise return true (JS non-configurable behavior). - if (!strict) + // $Buffer handler - "length" and "toString". Gated together with the + // dispatch arm above. + if (_features.UsesBuffer) { - il.MarkLabel(tsArrayDelLabel); - var tsArrDelIndexLocal = il.DeclareLocal(_types.Int64); + il.MarkLabel(tsBufferLabel); + // Check for "length" il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloca, tsArrDelIndexLocal); - il.Emit(OpCodes.Call, _types.GetMethod(_types.Int64, "TryParse", _types.String, _types.Int64.MakeByRefType())); - var tsArrDelNonNumericLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, tsArrDelNonNumericLabel); - - // arr.DeleteAt(idx); return true; - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSArrayType); - il.Emit(OpCodes.Ldloc, tsArrDelIndexLocal); - il.Emit(OpCodes.Callvirt, runtime.TSArrayDeleteAt); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Ret); - - il.MarkLabel(tsArrDelNonNumericLabel); - // Non-numeric key. PDS-installed named property: honor frozen + - // descriptor.configurable (mirrors the Dict path's behavior). - // Pre-fix returned true unconditionally, allowing `delete arr.foo` - // to silently succeed even when `Object.freeze(arr)` made the - // property non-configurable. - var tsArrDelFrozenLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.PDSIsFrozen); - il.Emit(OpCodes.Brfalse, tsArrDelFrozenLabel); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ret); - il.MarkLabel(tsArrDelFrozenLabel); - var tsArrDelSealedLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldstr, "length"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notBufferLenLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notBufferLenLabel); + // Get buf.Length il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, runtime.PDSIsSealed); - il.Emit(OpCodes.Brfalse, tsArrDelSealedLabel); - il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Castclass, runtime.TSBufferType); + il.Emit(OpCodes.Call, runtime.TSBufferLengthGetter); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Box, _types.Double); il.Emit(OpCodes.Ret); - il.MarkLabel(tsArrDelSealedLabel); - // Check PDS descriptor configurable. - var tsArrDelDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); - il.Emit(OpCodes.Ldarg_0); + il.MarkLabel(notBufferLenLabel); + // Check for "toString" - return a wrapper that calls ToEncodedString il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); - il.Emit(OpCodes.Stloc, tsArrDelDescLocal); - var tsArrDelNoDescLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, tsArrDelDescLocal); - il.Emit(OpCodes.Brfalse, tsArrDelNoDescLabel); - il.Emit(OpCodes.Ldloc, tsArrDelDescLocal); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorConfigurable.GetGetMethod()!); - var tsArrDelConfigurableLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, tsArrDelConfigurableLabel); - il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldstr, "toString"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notBufferToStringLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notBufferToStringLabel); + // Create a TSFunction wrapper for ToEncodedString + // For dynamically generated types, we need both method and type tokens + il.Emit(OpCodes.Ldarg_0); // target (the buffer) + il.Emit(OpCodes.Ldtoken, runtime.TSBufferToString); + il.Emit(OpCodes.Ldtoken, runtime.TSBufferType); + il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); il.Emit(OpCodes.Ret); - il.MarkLabel(tsArrDelConfigurableLabel); - // Configurable — PDS remove + return true. - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSDeleteProperty); - il.Emit(OpCodes.Pop); - il.MarkLabel(tsArrDelNoDescLabel); - il.Emit(OpCodes.Ldc_I4_1); + il.MarkLabel(notBufferToStringLabel); + // Unknown buffer property - return null + il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Ret); } - // $TSObject - call the DeleteProperty / DeletePropertyStrict instance method - il.MarkLabel(sharpTSObjectLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, runtime.TSObjectType); - il.Emit(OpCodes.Ldarg_1); - if (strict) - { - il.Emit(OpCodes.Ldarg_2); // strictMode - il.Emit(OpCodes.Callvirt, runtime.TSObjectDeletePropertyStrict); - } - else + // $Stats handler - return method wrappers or property values. + // Whole block is gated; without UsesFs there's no Stats type to dispatch on. + if (_features.UsesFs) { - il.Emit(OpCodes.Callvirt, runtime.TSObjectDeleteProperty); - } + il.MarkLabel(tsStatsLabel); + // Check for "size" property + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "size"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notStatsSizeLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notStatsSizeLabel); + // Return stats.size + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.StatsType); + il.Emit(OpCodes.Call, runtime.StatsSizeGetter); + il.Emit(OpCodes.Box, _types.Double); il.Emit(OpCodes.Ret); - // Dictionary - use Remove, honoring frozen/sealed and PDS configurability. - il.MarkLabel(dictLabel); - var valueLocal = il.DeclareLocal(_types.Object); + il.MarkLabel(notStatsSizeLabel); + // Check for "isFile" method - return TSFunction wrapper + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "isFile"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notStatsIsFileLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notStatsIsFileLabel); + // Create TSFunction wrapper for isFile + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldtoken, runtime.StatsIsFile); + il.Emit(OpCodes.Ldtoken, runtime.StatsType); + il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); + il.Emit(OpCodes.Ret); - // Check if frozen - il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); + il.MarkLabel(notStatsIsFileLabel); + // Check for "isDirectory" method + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "isDirectory"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notStatsIsDirLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notStatsIsDirLabel); + // Create TSFunction wrapper for isDirectory il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloca, valueLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); - var notFrozenLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notFrozenLabel); + il.Emit(OpCodes.Ldtoken, runtime.StatsIsDirectory); + il.Emit(OpCodes.Ldtoken, runtime.StatsType); + il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); + il.Emit(OpCodes.Ret); - // Frozen - fail (strict throws / sloppy returns false) - EmitDeleteFail("' of a frozen object"); + il.MarkLabel(notStatsIsDirLabel); + // Check for "isSymbolicLink" method + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "isSymbolicLink"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notStatsIsSymlinkLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notStatsIsSymlinkLabel); + // Create TSFunction wrapper for isSymbolicLink + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldtoken, runtime.StatsIsSymbolicLink); + il.Emit(OpCodes.Ldtoken, runtime.StatsType); + il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); + il.Emit(OpCodes.Ret); - // Check if sealed - il.MarkLabel(notFrozenLabel); - il.Emit(OpCodes.Ldsfld, runtime.SealedObjectsField); + il.MarkLabel(notStatsIsSymlinkLabel); + // Check for "isBlockDevice" method + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "isBlockDevice"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notStatsIsBlockLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notStatsIsBlockLabel); + // Create TSFunction wrapper for isBlockDevice il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloca, valueLocal); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); - var notSealedLabel = il.DefineLabel(); - il.Emit(OpCodes.Brfalse, notSealedLabel); - - // Sealed - fail (strict throws / sloppy returns false) - EmitDeleteFail("' of a sealed object"); - - // Not frozen/sealed — remove from BOTH the dict (default data entries) - // AND the PDS descriptor store (Object.defineProperty installs). When - // a PDS descriptor is present and non-configurable, delete fails per - // ECMA-262 §10.1.10 without removing: strict throws TypeError - // (§13.5.1.2), sloppy returns false. - il.MarkLabel(notSealedLabel); - var descLocalDel = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Ldtoken, runtime.StatsIsBlockDevice); + il.Emit(OpCodes.Ldtoken, runtime.StatsType); + il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notStatsIsBlockLabel); + // Check for "isCharacterDevice" method + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "isCharacterDevice"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notStatsIsCharLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notStatsIsCharLabel); + // Create TSFunction wrapper for isCharacterDevice il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldtoken, runtime.StatsIsCharacterDevice); + il.Emit(OpCodes.Ldtoken, runtime.StatsType); + il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notStatsIsCharLabel); + // Check for "isFIFO" method il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); - il.Emit(OpCodes.Stloc, descLocalDel); - var noPdsForDelLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, descLocalDel); - il.Emit(OpCodes.Brfalse, noPdsForDelLabel); - // Descriptor present — check Configurable. - il.Emit(OpCodes.Ldloc, descLocalDel); - il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorConfigurable.GetGetMethod()!); - var configurableLabel = il.DefineLabel(); - il.Emit(OpCodes.Brtrue, configurableLabel); - // Non-configurable — fail without removing. - EmitDeleteFail("' of object"); - il.MarkLabel(configurableLabel); - // Configurable — remove PDS entry. + il.Emit(OpCodes.Ldstr, "isFIFO"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notStatsIsFIFOLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notStatsIsFIFOLabel); + // Create TSFunction wrapper for isFIFO il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldtoken, runtime.StatsIsFIFO); + il.Emit(OpCodes.Ldtoken, runtime.StatsType); + il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notStatsIsFIFOLabel); + // Check for "isSocket" method il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.PDSDeleteProperty); - il.Emit(OpCodes.Pop); - il.MarkLabel(noPdsForDelLabel); + il.Emit(OpCodes.Ldstr, "isSocket"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notStatsIsSocketLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notStatsIsSocketLabel); + // Create TSFunction wrapper for isSocket + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldtoken, runtime.StatsIsSocket); + il.Emit(OpCodes.Ldtoken, runtime.StatsType); + il.Emit(OpCodes.Call, _types.GetMethod(_types.MethodBase, "GetMethodFromHandle", _types.RuntimeMethodHandle, _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notStatsIsSocketLabel); + // Unknown stats property - return null + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ret); + } // end if (_features.UsesFs) — $Stats handler block - // Always also try to remove from the dict (the property may be a - // plain data entry without a PDS descriptor). Dictionary.Remove - // returns false when the key isn't present, which is fine. + il.MarkLabel(stringLabel); + // Check for "length" + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "length"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notStrLenLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notStrLenLabel); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length").GetGetMethod()!); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Box, _types.Double); + il.Emit(OpCodes.Ret); + il.MarkLabel(notStrLenLabel); + // ECMA-262: `"hello".constructor === String`. Compiled mode resolves + // bare `String` to `typeof(string)` — returning that here makes the + // strict-equality check hold. il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "Remove", _types.String)); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ldstr, "constructor"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + var notStrCtorLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notStrCtorLabel); + il.Emit(OpCodes.Ldtoken, _types.String); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); il.Emit(OpCodes.Ret); + il.MarkLabel(notStrCtorLabel); - // Return true (default for null and other types) - il.MarkLabel(trueLabel); - il.Emit(OpCodes.Ldc_I4_1); + // Numeric index: `"hello"[0]` returns "h". Pre-fix returned null + // because the string fallback didn't honor numeric-string keys — + // only the typed-string dispatch did. + var notNumericKeyLabel = il.DefineLabel(); + var strIdxLocal = il.DeclareLocal(_types.Int32); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloca, strIdxLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Int32, "TryParse", _types.String, _types.Int32.MakeByRefType())); + il.Emit(OpCodes.Brfalse, notNumericKeyLabel); + il.Emit(OpCodes.Ldloc, strIdxLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Blt, notNumericKeyLabel); + il.Emit(OpCodes.Ldloc, strIdxLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length").GetGetMethod()!); + il.Emit(OpCodes.Bge, notNumericKeyLabel); + // Return str[idx].ToString() + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Ldloc, strIdxLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "get_Chars", _types.Int32)); + var charLocalStr = il.DeclareLocal(_types.Char); + il.Emit(OpCodes.Stloc, charLocalStr); + il.Emit(OpCodes.Ldloca, charLocalStr); + il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Char, "ToString")); il.Emit(OpCodes.Ret); + il.MarkLabel(notNumericKeyLabel); - il.MarkLabel(nullLabel); - il.Emit(OpCodes.Ldc_I4_1); + // ECMA-262 7.3.2: walk String.prototype for borrowed-method patterns + // (`s.valueOf`, `s.toString`, `s.toLowerCase`, etc.). Pre-fix returned + // null for any property other than length/constructor. + il.Emit(OpCodes.Call, runtime.StringPrototypePopulateMethod); + il.Emit(OpCodes.Ldsfld, runtime.StringPrototypeField); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, method); il.Emit(OpCodes.Ret); } - private void EmitDeletePropertyStrict(TypeBuilder typeBuilder, EmittedRuntime runtime) - => EmitDeletePropertyCore(typeBuilder, runtime, strict: true); - /// /// Phase 1: Define $MethodCallable type (wraps BuiltInMethod or other callable objects /// returned by GetMember so they can be dispatched through InvokeMethodValue/InvokeValue). diff --git a/Compilation/RuntimeEmitter.Objects.SetProperty.cs b/Compilation/RuntimeEmitter.Objects.SetProperty.cs new file mode 100644 index 00000000..7d94e9fc --- /dev/null +++ b/Compilation/RuntimeEmitter.Objects.SetProperty.cs @@ -0,0 +1,1658 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Runtime.BuiltIns; +using SharpTS.Runtime.Types; + +namespace SharpTS.Compilation; + +// Split out of RuntimeEmitter.Objects.Properties.cs (#1141): the property/index assignment emitters (sloppy + strict). +public partial class RuntimeEmitter +{ + private void EmitSetFieldsProperty(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // SetFieldsProperty(object obj, string name, object value) -> void + // Updates class-instance state through emitted runtime state only: + // descriptor store checks and emitted $Object fields. + var method = typeBuilder.DefineMethod( + "SetFieldsProperty", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.Object, _types.String, _types.Object] + ); + runtime.SetFieldsProperty = method; + + var il = method.GetILGenerator(); + var endLabel = il.DefineLabel(); + var tryFieldsLabel = il.DefineLabel(); + var trySetterLabel = il.DefineLabel(); + + // Declare locals upfront + var dictLocal = il.DeclareLocal(_types.DictionaryStringObject); + var frozenCheckLocal = il.DeclareLocal(_types.Object); + + // if (obj == null) return; + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brfalse, endLabel); + + // Check if frozen: _frozenObjects.TryGetValue(obj, out _) + // If frozen, silently return (non-strict mode behavior) + il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloca, frozenCheckLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); + il.Emit(OpCodes.Brtrue, endLabel); // Frozen - silently return + + // No additional setter fallback in standalone mode. + il.MarkLabel(trySetterLabel); + + // Try _fields dictionary - walk up type hierarchy to find non-null _fields + il.MarkLabel(tryFieldsLabel); + var notTSObjectLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brfalse, notTSObjectLabel); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSObjectType); + il.Emit(OpCodes.Callvirt, runtime.TSObjectFieldsGetter); + il.Emit(OpCodes.Stloc, dictLocal); + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Brfalse, endLabel); + + // Found a non-null _fields dictionary + // Check if sealed: _sealedObjects.TryGetValue(obj, out _) + var doSetFieldLabel = il.DefineLabel(); + var checkExtensibilityLabel = il.DefineLabel(); + var sealedCheckLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldsfld, runtime.SealedObjectsField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloca, sealedCheckLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); + il.Emit(OpCodes.Brfalse, checkExtensibilityLabel); // Not sealed, check extensibility + + // Object is sealed - check if property exists in dictionary + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Ldarg_1); // name + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); + il.Emit(OpCodes.Brfalse, endLabel); // Property doesn't exist on sealed object, silently return + il.Emit(OpCodes.Br, doSetFieldLabel); // Property exists, proceed to set + + // Not sealed - check extensibility via $PropertyDescriptorStore - fully standalone, no reflection + il.MarkLabel(checkExtensibilityLabel); + il.Emit(OpCodes.Ldarg_0); // obj + il.Emit(OpCodes.Ldarg_1); // name + il.Emit(OpCodes.Call, runtime.PDSCanAddProperty); + il.Emit(OpCodes.Brfalse, endLabel); // Cannot add property, silently return + + // Set the value: dict[name] = value; + il.MarkLabel(doSetFieldLabel); + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "set_Item")); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notTSObjectLabel); + + // Check $IHasFields interface (covers user-defined classes) + var notHasFieldsLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.IHasFieldsInterface); + il.Emit(OpCodes.Brfalse, notHasFieldsLabel); + + // Check extensibility before setting (handles sealed/non-extensible objects) + il.Emit(OpCodes.Ldarg_0); // obj + il.Emit(OpCodes.Ldarg_1); // name + il.Emit(OpCodes.Call, runtime.PDSCanAddProperty); + il.Emit(OpCodes.Brfalse, endLabel); // Cannot add property, silently return + + // Call interface method: ((IHasFields)obj).SetProperty(name, value) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.IHasFieldsInterface); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Callvirt, runtime.IHasFieldsSetProperty); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notHasFieldsLabel); + + // Check $Error - handle name, message, stack properties + var notErrorLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSErrorType); + il.Emit(OpCodes.Brfalse, notErrorLabel); + + // Check "name" + var notErrorNameLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "name"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notErrorNameLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSErrorType); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Callvirt, runtime.TSErrorNameSetter); + il.Emit(OpCodes.Ret); + il.MarkLabel(notErrorNameLabel); + + // Check "message" + var notErrorMessageLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "message"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notErrorMessageLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSErrorType); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Callvirt, runtime.TSErrorMessageSetter); + il.Emit(OpCodes.Ret); + il.MarkLabel(notErrorMessageLabel); + + // Check "stack" + var notErrorStackLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "stack"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notErrorStackLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSErrorType); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Callvirt, runtime.TSErrorStackSetter); + il.Emit(OpCodes.Ret); + il.MarkLabel(notErrorStackLabel); + + // Check "code" + var notErrorCodeSetLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "code"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notErrorCodeSetLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSErrorType); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Callvirt, runtime.TSErrorCodeSetter); + il.Emit(OpCodes.Ret); + il.MarkLabel(notErrorCodeSetLabel); + + // Check "syscall" + var notErrorSyscallSetLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "syscall"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notErrorSyscallSetLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSErrorType); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Callvirt, runtime.TSErrorSyscallSetter); + il.Emit(OpCodes.Ret); + il.MarkLabel(notErrorSyscallSetLabel); + + il.MarkLabel(notErrorLabel); + + // Scoped PDS-data-store fallback for ECMA built-ins: $TSDate, $TSRegExp, + // $TSPromise, $TSError. JS allows ad-hoc property assignment on these + // instances (`d = new Date(); d.foo = 1; d.foo === 1`); the value lands + // in PDS so GetFieldsProperty's PDS-data-descriptor arm reads it back. + // Limited to these types so user-defined class instances and runtime- + // side types (which may rely on silent-no-op semantics for unknown + // writes — e.g., the Debug npm package) are not affected. + // Note: $TSError already has explicit name/message/stack/code/syscall + // handlers above and only reaches the PDS path for OTHER property + // names (like `obj.length`, `obj[0]`). + var pdsStoreLabel = il.DefineLabel(); + var afterPdsStoreLabel = il.DefineLabel(); + if (_features.UsesDate) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSDateType); + il.Emit(OpCodes.Brtrue, pdsStoreLabel); + } + if (_features.UsesRegExp) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSRegExpType); + il.Emit(OpCodes.Brtrue, pdsStoreLabel); + } + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSPromiseType); + il.Emit(OpCodes.Brtrue, pdsStoreLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSErrorType); + il.Emit(OpCodes.Brtrue, pdsStoreLabel); + il.Emit(OpCodes.Br, afterPdsStoreLabel); + + il.MarkLabel(pdsStoreLabel); + { + var fbDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Newobj, runtime.CompiledPropertyDescriptorCtor); + il.Emit(OpCodes.Stloc, fbDescLocal); + il.Emit(OpCodes.Ldloc, fbDescLocal); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorValue.GetSetMethod()!); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloc, fbDescLocal); + il.Emit(OpCodes.Call, runtime.PDSDefineProperty); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ret); + } + + il.MarkLabel(afterPdsStoreLabel); + + // Fallback: Try SetMember(string, object) method for types like $HttpResponse + // that expose property setters through their SetMember dispatch method. + var setMemberLocal = il.DeclareLocal(_types.MethodInfo); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "GetType")); + il.Emit(OpCodes.Ldstr, "SetMember"); + il.Emit(OpCodes.Ldc_I4, (int)(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)); + il.Emit(OpCodes.Call, runtime.SafeGetMethod); + il.Emit(OpCodes.Stloc, setMemberLocal); + + il.Emit(OpCodes.Ldloc, setMemberLocal); + il.Emit(OpCodes.Brfalse, endLabel); + + // Call SetMember(name, value): methodInfo.Invoke(obj, new object[] { name, value }) + il.Emit(OpCodes.Ldloc, setMemberLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_2); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldarg_1); // name + il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ldarg_2); // value + il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.MethodInfo, "Invoke", _types.Object, _types.ObjectArray)); + il.Emit(OpCodes.Pop); + + il.MarkLabel(endLabel); + il.Emit(OpCodes.Ret); + } + /// + /// Emits SetFieldsPropertyStrict(object obj, string name, object value, bool strictMode) -> void + /// In strict mode, throws TypeError for modifications to frozen objects. + /// + private void EmitSetFieldsPropertyStrict(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "SetFieldsPropertyStrict", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.Object, _types.String, _types.Object, _types.Boolean] + ); + runtime.SetFieldsPropertyStrict = method; + + var il = method.GetILGenerator(); + var endLabel = il.DefineLabel(); + var notFrozenLabel = il.DefineLabel(); + var tryFieldsLabel = il.DefineLabel(); + + // Declare locals upfront + var dictLocal = il.DeclareLocal(_types.DictionaryStringObject); + var frozenCheckLocal = il.DeclareLocal(_types.Object); + + // if (obj == null) return; + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brfalse, endLabel); + + // Check if frozen: _frozenObjects.TryGetValue(obj, out _) + il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloca, frozenCheckLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); + il.Emit(OpCodes.Brfalse, notFrozenLabel); + + // Frozen - check strictMode and throw if true + il.Emit(OpCodes.Ldarg_3); // strictMode + var frozenSilentLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, frozenSilentLabel); + EmitThrowTypeErrorWithName(il, runtime, "Cannot assign to read only property '", "' of object"); + il.MarkLabel(frozenSilentLabel); + il.Emit(OpCodes.Ret); // Silently return in non-strict mode + + il.MarkLabel(notFrozenLabel); + + // No reflection setter fallback in standalone mode. + + // Try _fields dictionary - walk up type hierarchy to find non-null _fields + il.MarkLabel(tryFieldsLabel); + + var notTSObjectLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brfalse, notTSObjectLabel); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSObjectType); + il.Emit(OpCodes.Callvirt, runtime.TSObjectFieldsGetter); + il.Emit(OpCodes.Stloc, dictLocal); + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Brfalse, endLabel); + + // Found a non-null _fields dictionary - set the value and return + // dict[name] = value; + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "set_Item")); + il.Emit(OpCodes.Ret); + il.MarkLabel(notTSObjectLabel); + + // Check $IHasFields interface (covers user-defined classes) + var notHasFieldsLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.IHasFieldsInterface); + il.Emit(OpCodes.Brfalse, notHasFieldsLabel); + + // Check extensibility before setting (handles sealed/non-extensible objects) + il.Emit(OpCodes.Ldarg_0); // obj + il.Emit(OpCodes.Ldarg_1); // name + il.Emit(OpCodes.Call, runtime.PDSCanAddProperty); + il.Emit(OpCodes.Brfalse, endLabel); // Cannot add property, silently return + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.IHasFieldsInterface); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Callvirt, runtime.IHasFieldsSetProperty); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notHasFieldsLabel); + + // Check $Error - handle name, message, stack properties + var notErrorLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSErrorType); + il.Emit(OpCodes.Brfalse, notErrorLabel); + + var notErrorNameLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "name"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notErrorNameLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSErrorType); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Callvirt, runtime.TSErrorNameSetter); + il.Emit(OpCodes.Ret); + il.MarkLabel(notErrorNameLabel); + + var notErrorMessageLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "message"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notErrorMessageLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSErrorType); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Callvirt, runtime.TSErrorMessageSetter); + il.Emit(OpCodes.Ret); + il.MarkLabel(notErrorMessageLabel); + + var notErrorStackLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "stack"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notErrorStackLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSErrorType); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Callvirt, runtime.TSErrorStackSetter); + il.Emit(OpCodes.Ret); + il.MarkLabel(notErrorStackLabel); + + il.MarkLabel(notErrorLabel); + + // Built-in exotic objects (Date / RegExp / Promise / Error) accept arbitrary + // named property assignment per ECMA-262 [[Set]], stored as PDS data + // descriptors so GetProperty / for-in / gOPD round-trip. Mirrors the + // non-strict SetFieldsProperty PDS-store arm — without it these writes were + // silently dropped under "use strict", so e.g. `var d = new Date(0); + // d.enumerable = true;` (a defineProperty attributes object — Test262 + // Object/defineProperty 15.2.3.6-3-39 et al.) lost the field. Frozen / + // extensibility throws were already handled at the top of this method. + var fieldsPdsStoreLabel = il.DefineLabel(); + var afterFieldsPdsStoreLabel = il.DefineLabel(); + if (_features.UsesDate) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSDateType); + il.Emit(OpCodes.Brtrue, fieldsPdsStoreLabel); + } + if (_features.UsesRegExp) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSRegExpType); + il.Emit(OpCodes.Brtrue, fieldsPdsStoreLabel); + } + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSPromiseType); + il.Emit(OpCodes.Brtrue, fieldsPdsStoreLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSErrorType); + il.Emit(OpCodes.Brtrue, fieldsPdsStoreLabel); + il.Emit(OpCodes.Br, afterFieldsPdsStoreLabel); + + il.MarkLabel(fieldsPdsStoreLabel); + { + var fbDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Newobj, runtime.CompiledPropertyDescriptorCtor); + il.Emit(OpCodes.Stloc, fbDescLocal); + il.Emit(OpCodes.Ldloc, fbDescLocal); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorValue.GetSetMethod()!); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloc, fbDescLocal); + il.Emit(OpCodes.Call, runtime.PDSDefineProperty); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ret); + } + + il.MarkLabel(afterFieldsPdsStoreLabel); + + // Fallback: Try SetMember(string, object) method for types like $HttpResponse + var setMemberLocal = il.DeclareLocal(_types.MethodInfo); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "GetType")); + il.Emit(OpCodes.Ldstr, "SetMember"); + il.Emit(OpCodes.Ldc_I4, (int)(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)); + il.Emit(OpCodes.Call, runtime.SafeGetMethod); + il.Emit(OpCodes.Stloc, setMemberLocal); + + il.Emit(OpCodes.Ldloc, setMemberLocal); + il.Emit(OpCodes.Brfalse, endLabel); + + // Call SetMember(name, value): methodInfo.Invoke(obj, new object[] { name, value }) + il.Emit(OpCodes.Ldloc, setMemberLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_2); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldarg_1); // name + il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ldarg_2); // value + il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.MethodInfo, "Invoke", _types.Object, _types.ObjectArray)); + il.Emit(OpCodes.Pop); + + il.MarkLabel(endLabel); + il.Emit(OpCodes.Ret); + } + private void EmitSetProperty(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // Signature forward-declared by DefineRuntimeClassPhase1. + var method = (MethodBuilder)runtime.SetProperty; + + var il = method.GetILGenerator(); + var nullLabel = il.DefineLabel(); + var dictLabel = il.DefineLabel(); + var tsObjectLabel = il.DefineLabel(); + + // null check + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brfalse, nullLabel); + + EmitGlobalThisSetRedirect(il, runtime); + + // Proxy check: uses obj.GetType().FullName comparison (no SharpTS.dll dependency) + var notProxyLabel = il.DefineLabel(); + EmitProxySetPropertyCheck(il, () => il.Emit(OpCodes.Ldarg_0), () => il.Emit(OpCodes.Ldarg_1), () => il.Emit(OpCodes.Ldarg_2), notProxyLabel); + + il.MarkLabel(notProxyLabel); + + // $Object (with setter support) - call obj.SetProperty(name, value) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brtrue, tsObjectLabel); + + // $Array — special-case `arr.length = N` (route through $Array.SetLength + // so truncation/extension uses the sparse-aware path). Other named- + // property writes on arrays are silently ignored; ECMA-262 §22.1.5 + // permits them but most emitters don't preserve them and the spec + // compiler tests don't exercise that corner. Must come BEFORE the + // Dictionary check (since $Array inherits List which is + // neither), and BEFORE SetFieldsProperty fallthrough. + var tsArraySetPropLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSArrayType); + il.Emit(OpCodes.Brtrue, tsArraySetPropLabel); + + // Dictionary + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, dictLabel); + + // $TSFunction — JS functions are objects and support arbitrary property assignment + // (`fn.x = 42`, `lodash.chunk = function(...){}`). Store as a data descriptor in + // $PropertyDescriptorStore; GetFunctionMethod's fallback path reads it back. Without + // this, the assignment would fall through to SetFieldsProperty which is a + // class-instance path that doesn't match $TSFunction. + var tsFunctionSetLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSFunctionType); + il.Emit(OpCodes.Brtrue, tsFunctionSetLabel); + + // $CJSModule — `module.exports = X` (or any aliased write) goes through here. + // Only emit when UsesCjsRequire is on (matching the type emission gate). + var cjsModuleSetLabel = il.DefineLabel(); + if (_features.UsesCjsRequire) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.CjsModuleType); + il.Emit(OpCodes.Brtrue, cjsModuleSetLabel); + } + + // $RegExp — `r.lastIndex = value` must coerce `value` via JS + // ToLength (string "1.9" → 1, NaN → 0, etc.) and write the internal + // int32 slot. Without the special case the assignment falls through + // to SetFieldsProperty which stores the boxed value on a generic + // PDS bag — subsequent built-in matchers ignore it and use the + // stale internal slot. Test262 builtin-coerce-lastindex.js etc. + // require coerce+round-trip. + var tsRegExpSetLabel = il.DefineLabel(); + if (_features.UsesRegExp) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSRegExpType); + il.Emit(OpCodes.Brtrue, tsRegExpSetLabel); + } + + // System.Type (class reference used as value, e.g. `Scalar.PLAIN = 'x'`). JS allows + // arbitrary static property assignment on classes; we store them in PropertyDescriptorStore. + var typeSetLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Type); + il.Emit(OpCodes.Brtrue, typeSetLabel); + + // List — raw arrays from `[...]` literal / Array.prototype.concat + // / etc. accept arbitrary named property assignment per ECMA-262 §23.1.5 + // [[DefineOwnProperty]]. Numeric indices are handled via SetIndex; only + // string keys land here. Store named-non-numeric writes in PDS as data + // descriptors so GetProperty / hasOwn / gOPD round-trip. Pre-fix these + // fell to SetFieldsProperty which silently dropped them. + var listSetPropLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.ListOfObject); + il.Emit(OpCodes.Brtrue, listSetPropLabel); + + // Not a dict or $Object or $TSFunction or $CJSModule or Type - try SetFieldsProperty for class instances + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.SetFieldsProperty); + il.Emit(OpCodes.Ret); + + // List handler: same shape as $TSArray's non-length path. + il.MarkLabel(listSetPropLabel); + { + // Skip if key is "length" or a numeric index — those write paths + // belong to SetIndex (numeric) / a dedicated length path. Silent + // no-op for "length" matches current behavior; named numeric + // string falls through to PDS (close enough, integer-key writes + // through SetProperty are rare). + var listSetIsLengthLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "length"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brtrue, listSetIsLengthLabel); + // Frozen guard. + var listSetNotFrozenLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.PDSIsFrozen); + il.Emit(OpCodes.Brfalse, listSetNotFrozenLabel); + il.Emit(OpCodes.Ret); + il.MarkLabel(listSetNotFrozenLabel); + // Existing-descriptor writable=false guard. + var listSetExistingDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); + il.Emit(OpCodes.Stloc, listSetExistingDescLocal); + var listSetWritableLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, listSetExistingDescLocal); + il.Emit(OpCodes.Brfalse, listSetWritableLabel); + il.Emit(OpCodes.Ldloc, listSetExistingDescLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorWritable.GetGetMethod()!); + il.Emit(OpCodes.Brtrue, listSetWritableLabel); + il.Emit(OpCodes.Ret); + il.MarkLabel(listSetWritableLabel); + // Install fresh data descriptor with the value. + EmitDefineDataDescriptorFromValue(il, runtime); + il.MarkLabel(listSetIsLengthLabel); + il.Emit(OpCodes.Ret); + } + + // $RegExp handler: route `r.lastIndex = value` to the typed setter + // with JS ToLength coercion. Other property writes fall through to + // SetFieldsProperty so user data-property assignments + // (`Object.defineProperty(r, 'foo', {writable:true}); r.foo = ...`) + // still hit the user-property bag. + if (_features.UsesRegExp) + { + il.MarkLabel(tsRegExpSetLabel); + + // PDS-first: if user installed an accessor descriptor with a + // setter, invoke it. If a data descriptor with writable=false + // is present, silently swallow (non-strict; strict-mode is + // handled by the strict variant). Mirrors the GET-side fix + // for spec-aligned override semantics on $RegExp instances. + var setNoPdsLabel = il.DefineLabel(); + var setPdsDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); + il.Emit(OpCodes.Stloc, setPdsDescLocal); + il.Emit(OpCodes.Ldloc, setPdsDescLocal); + il.Emit(OpCodes.Brfalse, setNoPdsLabel); + + // Accessor setter? Setter slot non-null → InvokeWithThis(rx, value). + var setNoAccessorLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, setPdsDescLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorSetter.GetGetMethod()!); + var setterValueLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Stloc, setterValueLocal); + il.Emit(OpCodes.Ldloc, setterValueLocal); + il.Emit(OpCodes.Brfalse, setNoAccessorLabel); + var setterFnLocal = il.DeclareLocal(runtime.TSFunctionType); + il.Emit(OpCodes.Ldloc, setterValueLocal); + il.Emit(OpCodes.Isinst, runtime.TSFunctionType); + il.Emit(OpCodes.Stloc, setterFnLocal); + il.Emit(OpCodes.Ldloc, setterFnLocal); + il.Emit(OpCodes.Brfalse, setNoAccessorLabel); + il.Emit(OpCodes.Ldloc, setterFnLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Callvirt, runtime.TSFunctionInvokeWithThis); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ret); + il.MarkLabel(setNoAccessorLabel); + + // No setter slot — check getter. If getter present (accessor + // descriptor), this is getter-only → silently no-op (non-strict). + var setSilentlyIgnoreLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, setPdsDescLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorGetter.GetGetMethod()!); + il.Emit(OpCodes.Brtrue, setSilentlyIgnoreLabel); + // No getter and no setter → data descriptor. Honor writable bit. + il.Emit(OpCodes.Ldloc, setPdsDescLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorWritable.GetGetMethod()!); + il.Emit(OpCodes.Brfalse, setSilentlyIgnoreLabel); + il.Emit(OpCodes.Ldloc, setPdsDescLocal); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorValue.GetSetMethod()!); + il.Emit(OpCodes.Ret); + il.MarkLabel(setSilentlyIgnoreLabel); + il.Emit(OpCodes.Ret); + + il.MarkLabel(setNoPdsLabel); + + var notLastIndexLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "lastIndex"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notLastIndexLabel); + + // ECMA-262: lastIndex is an ordinary writable data property — store + // the assigned value as-is (ToLength is deferred to RegExpBuiltinExec). + // Only an OBJECT (whose ToLength would run user `valueOf`) needs the + // boxed slot to defer that call + preserve identity; primitives + // (null/undefined/number/string/bool) ToLength with no user code, so + // they fold straight into the typed int slot (and clear the box). + // NB: JS `null` ToLengths to 0 — it must take the primitive path, not + // the box (a boxed C# null is indistinguishable from "no box"). + var numericSetLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Brfalse, numericSetLabel); // null + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Isinst, _types.Double); + il.Emit(OpCodes.Brtrue, numericSetLabel); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, numericSetLabel); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Isinst, _types.Boolean); + il.Emit(OpCodes.Brtrue, numericSetLabel); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, numericSetLabel); + // object → rx._lastIndexBoxed = value (defer ToLength/valueOf to exec) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSRegExpType); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Stfld, _tsRegExpLastIndexBoxedField); + il.Emit(OpCodes.Ret); + il.MarkLabel(numericSetLabel); + // primitive: rx._lastIndex = ToLength(value); rx._lastIndexBoxed = null + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSRegExpType); + il.Emit(OpCodes.Ldarg_2); + EmitToLengthBoxed(il, runtime); + il.Emit(OpCodes.Callvirt, runtime.TSRegExpLastIndexSetter); // also clears boxed + il.Emit(OpCodes.Ret); + + il.MarkLabel(notLastIndexLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.SetFieldsProperty); + il.Emit(OpCodes.Ret); + } + + // System.Type handler: store as data descriptor in PropertyDescriptorStore. + // Read path (EmitGetProperty) looks it up before falling through to .NET member + // resolution, so writes become visible as reads. + il.MarkLabel(typeSetLabel); + { + // Per ECMA-262 §17, constructor static "prototype"/"name"/"length" + // are non-writable; non-strict writes silently no-op. Same for + // Number constants (MAX_VALUE etc.) which are W:F,E:F,C:F. + var typeSetSkipLabel = il.DefineLabel(); + void EmitTypeSetSkipName(string n) + { + var notNameLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, n); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notNameLabel); + il.Emit(OpCodes.Ret); + il.MarkLabel(notNameLabel); + } + EmitTypeSetSkipName("prototype"); + EmitTypeSetSkipName("name"); + EmitTypeSetSkipName("length"); + // Number constants — non-writable on the Number constructor. + var notNumberTypeForSetLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldtoken, _types.Double); + il.Emit(OpCodes.Call, _types.Type.GetMethod("GetTypeFromHandle")!); + il.Emit(OpCodes.Bne_Un, notNumberTypeForSetLabel); + EmitTypeSetSkipName("MAX_VALUE"); + EmitTypeSetSkipName("MIN_VALUE"); + EmitTypeSetSkipName("NaN"); + EmitTypeSetSkipName("POSITIVE_INFINITY"); + EmitTypeSetSkipName("NEGATIVE_INFINITY"); + EmitTypeSetSkipName("MAX_SAFE_INTEGER"); + EmitTypeSetSkipName("MIN_SAFE_INTEGER"); + EmitTypeSetSkipName("EPSILON"); + il.MarkLabel(notNumberTypeForSetLabel); + + EmitDefineDataDescriptorFromValue(il, runtime); + il.Emit(OpCodes.Ret); + } + + // $CJSModule handler — only "exports" is writable; others are no-ops (spec behavior). + if (_features.UsesCjsRequire) + { + il.MarkLabel(cjsModuleSetLabel); + EmitCjsModuleExportsSetBranch(il, runtime); + } + + // $Array handler — `arr.length = N` routes through SetLength. Any + // other name falls off into the normal silent-ignore (JS permits + // arbitrary named writes on arrays but we don't persist them). + // ECMA-262 23.1.4.1 [[DefineOwnProperty]] for "length": if + // ToNumber(value) !== ToUint32(value) + // (i.e. value is not a non-negative integer ≤ 2^32 - 1), throw + // RangeError. Pre-fix Convert.ToInt64 rounded `1.5 → 2` silently. + il.MarkLabel(tsArraySetPropLabel); + { + var notLengthLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "length"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notLengthLabel); + + // Coerce value through ToNumber (handles strings, booleans, etc.) + // then enforce ToUint32 round-trip. The .NET `Convert.ToInt64` path + // truncates fractional parts; we need to flag fractional / NaN / + // out-of-range as a RangeError instead. + var doubleValLocal = il.DeclareLocal(_types.Double); + var u32Local = il.DeclareLocal(_types.Int64); + var validLengthLabel = il.DefineLabel(); + var rangeErrorLabel = il.DefineLabel(); + + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.ToNumber); + il.Emit(OpCodes.Stloc, doubleValLocal); + + // Reject NaN / +Infinity / -Infinity via IsFinite. + il.Emit(OpCodes.Ldloc, doubleValLocal); + il.Emit(OpCodes.Call, _types.Double.GetMethod("IsFinite", [_types.Double])!); + il.Emit(OpCodes.Brfalse, rangeErrorLabel); + + // ToUint32 reciprocity: cast to long, ensure 0..2^32-1 inclusive, + // and that the round-trip back to double matches the original. + il.Emit(OpCodes.Ldloc, doubleValLocal); + il.Emit(OpCodes.Conv_I8); + il.Emit(OpCodes.Stloc, u32Local); + // negative → throw + il.Emit(OpCodes.Ldloc, u32Local); + il.Emit(OpCodes.Ldc_I8, 0L); + il.Emit(OpCodes.Blt, rangeErrorLabel); + // > uint.MaxValue → throw + il.Emit(OpCodes.Ldloc, u32Local); + il.Emit(OpCodes.Ldc_I8, (long)uint.MaxValue); + il.Emit(OpCodes.Bgt, rangeErrorLabel); + // round-trip mismatch (fractional component) → throw + il.Emit(OpCodes.Ldloc, u32Local); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Ldloc, doubleValLocal); + il.Emit(OpCodes.Bne_Un, rangeErrorLabel); + il.Emit(OpCodes.Br, validLengthLabel); + + il.MarkLabel(rangeErrorLabel); + il.Emit(OpCodes.Ldstr, "Invalid array length"); + il.Emit(OpCodes.Newobj, runtime.TSRangeErrorCtor); + il.Emit(OpCodes.Call, runtime.CreateException); + il.Emit(OpCodes.Throw); + + il.MarkLabel(validLengthLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSArrayType); + il.Emit(OpCodes.Ldloc, u32Local); + il.Emit(OpCodes.Callvirt, runtime.TSArraySetLength); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notLengthLabel); + // Other named writes on arrays go to PDS as data descriptors. + // ECMA-262 23.1.5: arrays are exotic objects but accept arbitrary + // named property assignment via [[DefineOwnProperty]]. Test262 + // patterns like `var arr = []; arr.foo = ...; arr.foo()` rely on + // this. GetFieldsProperty's PDS-data-descriptor arm reads it back. + // Pre-fix unconditionally overwrote — Object.freeze(arr) didn't + // block `arr.foo = "x"` because the existing PDS descriptor's + // writable bit (false post-freeze via AND-mask) was ignored. + { + // Honor frozen state: if Object.isFrozen(arr), silently no-op + // (non-strict). Strict callers use SetPropertyStrict which + // can throw. + var arrFrozenLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.PDSIsFrozen); + il.Emit(OpCodes.Brfalse, arrFrozenLabel); + il.Emit(OpCodes.Ret); + il.MarkLabel(arrFrozenLabel); + + // Honor existing-descriptor writable=false: if there's a PDS + // data descriptor for this key with writable=false, silently + // no-op. Accessor descriptors fall through (defining a value + // over an accessor is handled by PDSDefineProperty). + var arrNotWritableLabel = il.DefineLabel(); + var arrExistingDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); + il.Emit(OpCodes.Stloc, arrExistingDescLocal); + il.Emit(OpCodes.Ldloc, arrExistingDescLocal); + il.Emit(OpCodes.Brfalse, arrNotWritableLabel); + // Has descriptor; check writable. + il.Emit(OpCodes.Ldloc, arrExistingDescLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorWritable.GetGetMethod()!); + il.Emit(OpCodes.Brtrue, arrNotWritableLabel); + // Not writable — silent no-op. + il.Emit(OpCodes.Ret); + il.MarkLabel(arrNotWritableLabel); + + EmitDefineDataDescriptorFromValue(il, runtime); + } + il.Emit(OpCodes.Ret); + } + + // $TSFunction handler: ECMA-262 §10.1.9 [[Set]] honors non-extensibility for new + // properties. Gate via PDSCanAddProperty so `Object.preventExtensions(fn); fn.x = v` + // silently no-ops (non-strict). Existing PDS entries still update. + il.MarkLabel(tsFunctionSetLabel); + { + var tsFnDoSetLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSCanAddProperty); + il.Emit(OpCodes.Brtrue, tsFnDoSetLabel); + il.Emit(OpCodes.Ret); + il.MarkLabel(tsFnDoSetLabel); + + EmitDefineDataDescriptorFromValue(il, runtime); + il.Emit(OpCodes.Ret); + } + + // $Object handler. First check PDS for a setter accessor descriptor + // (defineProperty-installed). If present, invoke it (passing $TSObject + // as `this`) — TSObject.SetProperty doesn't know about PDS-stored + // setters. Otherwise delegate to TSObject.SetProperty for the dict / + // _getters / _setters fast path. + il.MarkLabel(tsObjectLabel); + { + var tsObjPdsSetterLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloca, tsObjPdsSetterLocal); + il.Emit(OpCodes.Call, runtime.PDSTryGetSetter); + var tsObjNoPdsSetterLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, tsObjNoPdsSetterLabel); + // Invoke PDS setter. + EmitInvokePdsSetterWithValueAndReturn(il, runtime, tsObjPdsSetterLocal); + il.MarkLabel(tsObjNoPdsSetterLabel); + } + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSObjectType); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Callvirt, runtime.TSObjectSetProperty); + il.Emit(OpCodes.Ret); + + il.MarkLabel(nullLabel); + il.Emit(OpCodes.Ret); + + il.MarkLabel(dictLabel); + + // $AbortSignal dict surface (#985): `signal.onabort = cb` on a dynamically- + // typed (`any`) signal receiver. The typed path intercepts at compile time + // (AbortSignalEmitter.TryEmitPropertySet); an `any` receiver lands here and + // would otherwise store a plain "onabort" dict key that FireAbortEvent (which + // reads the internal "_onabort" slot) never sees — so the handler never fired. + // Signals are identified by their "_reasonSet" internal slot. Gated on + // UsesAbortController so non-signal programs pay nothing; mirrors the GetProperty + // signal branch (#224). + if (_features.UsesAbortController) + { + var notSignalOnAbortSet = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "onabort"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notSignalOnAbortSet); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); + il.Emit(OpCodes.Ldstr, "_reasonSet"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); + il.Emit(OpCodes.Brfalse, notSignalOnAbortSet); + il.Emit(OpCodes.Ldarg_0); // signal + il.Emit(OpCodes.Ldarg_2); // handler + il.Emit(OpCodes.Call, runtime.AbortSignalSetOnAbort); + il.Emit(OpCodes.Ret); + il.MarkLabel(notSignalOnAbortSet); + } + + // For dictionaries, check frozen/sealed tables and silently ignore if frozen/sealed + var sealedCheckLabel = il.DefineLabel(); + var doSetLabel = il.DefineLabel(); + var valueLocal = il.DeclareLocal(_types.Object); + + // Check if frozen: _frozenObjects.TryGetValue(obj, out _). + // Per ECMA-262, freeze only forbids writes to DATA properties — accessor + // setters still fire because the descriptor's writable bit doesn't apply + // to accessors. So when there's a PDS setter for this key, fall through + // to the doSetLabel path (which invokes it). Pre-fix dropped frozen + // accessor writes silently — broke test262 15.2.3.9-2-c-{2,3,4}. + il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloca, valueLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); + var frozenNotFoundLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, frozenNotFoundLabel); + // Frozen — only allow if there's a PDS accessor setter for this key. + var frozenAccessorSetterLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloca, frozenAccessorSetterLocal); + il.Emit(OpCodes.Call, runtime.PDSTryGetSetter); + il.Emit(OpCodes.Brfalse, nullLabel); // No setter, frozen data — silent return + il.Emit(OpCodes.Br, doSetLabel); // Has setter — proceed (doSetLabel re-fetches via PDSTryGetSetter) + il.MarkLabel(frozenNotFoundLabel); + + // Check if sealed and property doesn't exist + il.Emit(OpCodes.Ldsfld, runtime.SealedObjectsField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloca, valueLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); + var extensibleCheckLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, extensibleCheckLabel); // Not sealed, check extensibility + + // Object is sealed - check if property exists (dict OR PDS). Pre-fix + // only checked the backing dict, so an accessor-only own property + // (defineProperty with get/set + configurable:false) was treated as + // missing and the write silently dropped — including its setter. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); + il.Emit(OpCodes.Brtrue, doSetLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); + il.Emit(OpCodes.Brfalse, nullLabel); // Property doesn't exist, silently return + il.Emit(OpCodes.Br, doSetLabel); // Property exists on sealed object, proceed to set + + // Check extensibility via $PropertyDescriptorStore.CanAddProperty - fully standalone, no reflection + il.MarkLabel(extensibleCheckLabel); + il.Emit(OpCodes.Ldarg_0); // obj + il.Emit(OpCodes.Ldarg_1); // name + il.Emit(OpCodes.Call, runtime.PDSCanAddProperty); + il.Emit(OpCodes.Brfalse, nullLabel); // Cannot add property, silently return + + // Actually set the property + il.MarkLabel(doSetLabel); + + // Check for setter accessor via $PropertyDescriptorStore - fully standalone, no reflection + var setterLocal = il.DeclareLocal(_types.Object); + var noSetterLabel = il.DefineLabel(); + + // Call PDSTryGetSetter(obj, name, out setter) + il.Emit(OpCodes.Ldarg_0); // obj + il.Emit(OpCodes.Ldarg_1); // name + il.Emit(OpCodes.Ldloca, setterLocal); // out setter + il.Emit(OpCodes.Call, runtime.PDSTryGetSetter); + il.Emit(OpCodes.Brfalse, noSetterLabel); + + // Setter was found - invoke it via InvokeMethodValue(obj, setter, [value]) + EmitInvokePdsSetterWithValueAndReturn(il, runtime, setterLocal); + + il.MarkLabel(noSetterLabel); + + // Check if property is writable via $PropertyDescriptorStore - fully standalone, no reflection + il.Emit(OpCodes.Ldarg_0); // obj + il.Emit(OpCodes.Ldarg_1); // name + il.Emit(OpCodes.Call, runtime.PDSIsWritable); + il.Emit(OpCodes.Brfalse, nullLabel); // Not writable, silently return + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "set_Item")); + il.Emit(OpCodes.Ret); + } + /// + /// Emits inline IL that coerces the boxed-object value at the top of the + /// stack to int32 via the JS ToLength algorithm: null/undefined/NaN → 0, + /// false → 0, true → 1, double via truncate (clamped to int32), int via + /// pass-through, string via TryParse → double-path (or 0 on parse + /// failure), other types → 0. Used by SetProperty's $RegExp arm so + /// `r.lastIndex = '1.9'` stores 1. + /// + private void EmitToLengthBoxed(ILGenerator il, EmittedRuntime runtime) + { + var localVal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Stloc, localVal); + + var nullLabel = il.DefineLabel(); + var undefinedLabel = il.DefineLabel(); + var doubleLabel = il.DefineLabel(); + var intLabel = il.DefineLabel(); + var boolLabel = il.DefineLabel(); + var stringLabel = il.DefineLabel(); + var doneLabel = il.DefineLabel(); + + il.Emit(OpCodes.Ldloc, localVal); + il.Emit(OpCodes.Brfalse, nullLabel); + + il.Emit(OpCodes.Ldloc, localVal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, undefinedLabel); + + il.Emit(OpCodes.Ldloc, localVal); + il.Emit(OpCodes.Isinst, _types.Double); + il.Emit(OpCodes.Brtrue, doubleLabel); + + il.Emit(OpCodes.Ldloc, localVal); + il.Emit(OpCodes.Isinst, _types.Int32); + il.Emit(OpCodes.Brtrue, intLabel); + + il.Emit(OpCodes.Ldloc, localVal); + il.Emit(OpCodes.Isinst, _types.Boolean); + il.Emit(OpCodes.Brtrue, boolLabel); + + il.Emit(OpCodes.Ldloc, localVal); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, stringLabel); + + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Br, doneLabel); + + il.MarkLabel(nullLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Br, doneLabel); + + il.MarkLabel(undefinedLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Br, doneLabel); + + il.MarkLabel(doubleLabel); + var dTmp = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Ldloc, localVal); + il.Emit(OpCodes.Unbox_Any, _types.Double); + il.Emit(OpCodes.Stloc, dTmp); + + // NaN check: (d == d) is false iff d is NaN. + var dNonNanLabel = il.DefineLabel(); + var dPositiveLabel = il.DefineLabel(); + var dClampLabel = il.DefineLabel(); + var dInRangeLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, dTmp); + il.Emit(OpCodes.Ldloc, dTmp); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Brtrue, dNonNanLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Br, doneLabel); + + il.MarkLabel(dNonNanLabel); + il.Emit(OpCodes.Ldloc, dTmp); + il.Emit(OpCodes.Ldc_R8, 0.0); + il.Emit(OpCodes.Bgt, dPositiveLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Br, doneLabel); + + il.MarkLabel(dPositiveLabel); + il.Emit(OpCodes.Ldloc, dTmp); + il.Emit(OpCodes.Ldc_R8, (double)int.MaxValue); + il.Emit(OpCodes.Blt, dInRangeLabel); + il.Emit(OpCodes.Ldc_I4, int.MaxValue); + il.Emit(OpCodes.Br, doneLabel); + + il.MarkLabel(dInRangeLabel); + il.Emit(OpCodes.Ldloc, dTmp); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Br, doneLabel); + + il.MarkLabel(intLabel); + il.Emit(OpCodes.Ldloc, localVal); + il.Emit(OpCodes.Unbox_Any, _types.Int32); + il.Emit(OpCodes.Br, doneLabel); + + il.MarkLabel(boolLabel); + il.Emit(OpCodes.Ldloc, localVal); + il.Emit(OpCodes.Unbox_Any, _types.Boolean); + il.Emit(OpCodes.Br, doneLabel); + + il.MarkLabel(stringLabel); + var sTmp = il.DeclareLocal(_types.Double); + var parseFailLabel = il.DefineLabel(); + var sPosLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, localVal); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Ldc_I4, (int)System.Globalization.NumberStyles.Float); + il.Emit(OpCodes.Call, _types.GetProperty(typeof(System.Globalization.CultureInfo), "InvariantCulture").GetGetMethod()!); + il.Emit(OpCodes.Ldloca, sTmp); + il.Emit(OpCodes.Call, typeof(double).GetMethod("TryParse", + [_types.String, typeof(System.Globalization.NumberStyles), typeof(IFormatProvider), typeof(double).MakeByRefType()])!); + il.Emit(OpCodes.Brfalse, parseFailLabel); + il.Emit(OpCodes.Ldloc, sTmp); + il.Emit(OpCodes.Ldloc, sTmp); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Brfalse, parseFailLabel); + il.Emit(OpCodes.Ldloc, sTmp); + il.Emit(OpCodes.Ldc_R8, 0.0); + il.Emit(OpCodes.Bgt, sPosLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Br, doneLabel); + + il.MarkLabel(sPosLabel); + il.Emit(OpCodes.Ldloc, sTmp); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Br, doneLabel); + + il.MarkLabel(parseFailLabel); + il.Emit(OpCodes.Ldc_I4_0); + + il.MarkLabel(doneLabel); + } + /// + /// Emits SetPropertyStrict(object obj, string name, object value, bool strictMode) -> void + /// In strict mode, throws TypeError for modifications to frozen objects or new properties on sealed objects. + /// + private void EmitSetPropertyStrict(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "SetPropertyStrict", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.Object, _types.String, _types.Object, _types.Boolean] + ); + runtime.SetPropertyStrict = method; + + var il = method.GetILGenerator(); + var nullLabel = il.DefineLabel(); + var dictLabel = il.DefineLabel(); + + // null check + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brfalse, nullLabel); + + // Without this redirect, a strict-mode top-level `this.foo = v` + // (compiled `this` resolves to the globalThis sentinel) fell through + // to SetFieldsPropertyStrict and was dropped — e.g. Test262 + // Object/create 15.2.3.5-4-177 / defineProperty 15.2.3.6-3-230 set + // `this.value` / `this.get` and reuse `this` as a descriptor. + EmitGlobalThisSetRedirect(il, runtime); + + // Check if $Object + var sharpTSObjectLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brtrue, sharpTSObjectLabel); + + // Dictionary + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, dictLabel); + + // $TSFunction — mirror the non-strict SetProperty branch (functions as objects carry + // user-assigned properties through PDSDefineProperty). + var tsFunctionSetStrictLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSFunctionType); + il.Emit(OpCodes.Brtrue, tsFunctionSetStrictLabel); + + // $CJSModule — mirror the non-strict branch. Gated on UsesCjsRequire. + var cjsModuleSetStrictLabel = il.DefineLabel(); + if (_features.UsesCjsRequire) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.CjsModuleType); + il.Emit(OpCodes.Brtrue, cjsModuleSetStrictLabel); + } + + // $Array / List — named property write on an array in strict + // mode. The non-strict SetProperty has dedicated array branches (length + // coercion, numeric, PDS data descriptors); without a strict equivalent + // these writes fell to SetFieldsPropertyStrict, which silently dropped + // them, so `arr.foo = fn; arr.foo()` saw `foo === undefined` under "use + // strict" (Test262 Array slice/splice S15.4.4.*_A*_T*, the common + // `arr.getClass = Object.prototype.toString` pattern). Detect arrays here + // and route to arraySetStrictLabel, which enforces strict frozen / + // non-writable throws then reuses the non-strict store logic. + var arraySetStrictLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSArrayType); + il.Emit(OpCodes.Brtrue, arraySetStrictLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.ListOfObject); + il.Emit(OpCodes.Brtrue, arraySetStrictLabel); + + // Not a dict or $Object or $TSFunction or $CJSModule or array - fall back to SetFieldsPropertyStrict. + // NOTE (#1131): unlike the non-strict SetProperty, this variant has no + // dedicated Proxy / $RegExp / System.Type receiver branches — those + // receivers fall through here. Preserved as-is by the strict/non-strict + // dedup (behavior-preserving); tracked as drift in the epic notes. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Ldarg_3); // strictMode + il.Emit(OpCodes.Call, runtime.SetFieldsPropertyStrict); + il.Emit(OpCodes.Ret); + + // $Array / List strict handler: ECMA-262 §10.4.2.1 / OrdinarySet + // step 5 — a frozen array or an own non-writable data property makes the + // assignment fail; in strict mode PutValue then throws a TypeError. We + // honor those two cases here, then delegate the actual store to the + // non-strict SetProperty (shared length/numeric/PDS-data-descriptor path). + il.MarkLabel(arraySetStrictLabel); + { + var arrayDoStoreLabel = il.DefineLabel(); + + // Frozen array → throw "Cannot assign to read only property 'name'". + var arrayNotFrozenLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.PDSIsFrozen); + il.Emit(OpCodes.Brfalse, arrayNotFrozenLabel); + EmitThrowTypeErrorWithName(il, runtime, "Cannot assign to read only property '", "' of object '[object Array]'"); + il.MarkLabel(arrayNotFrozenLabel); + + // Own non-writable DATA descriptor → throw. Accessor descriptors + // (setter present) and writable/absent descriptors fall through to + // the store, where SetProperty invokes the setter or overwrites. + var arrayDescLocal = il.DeclareLocal(runtime.CompiledPropertyDescriptorType); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSGetPropertyDescriptor); + il.Emit(OpCodes.Stloc, arrayDescLocal); + il.Emit(OpCodes.Ldloc, arrayDescLocal); + il.Emit(OpCodes.Brfalse, arrayDoStoreLabel); // no descriptor → store + il.Emit(OpCodes.Ldloc, arrayDescLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorSetter.GetGetMethod()!); + il.Emit(OpCodes.Brtrue, arrayDoStoreLabel); // accessor → delegate (invokes setter) + il.Emit(OpCodes.Ldloc, arrayDescLocal); + il.Emit(OpCodes.Callvirt, runtime.CompiledPropertyDescriptorWritable.GetGetMethod()!); + il.Emit(OpCodes.Brtrue, arrayDoStoreLabel); // writable → store + EmitThrowTypeErrorWithName(il, runtime, "Cannot assign to read only property '", "' of object '[object Array]'"); + + il.MarkLabel(arrayDoStoreLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.SetProperty); + il.Emit(OpCodes.Ret); + } + + // $CJSModule strict handler — same as non-strict for now. Gated. + if (_features.UsesCjsRequire) + { + il.MarkLabel(cjsModuleSetStrictLabel); + EmitCjsModuleExportsSetBranch(il, runtime); + } + + // $TSFunction handler: create data descriptor with the value, store via PDSDefineProperty + il.MarkLabel(tsFunctionSetStrictLabel); + EmitDefineDataDescriptorFromValue(il, runtime); + il.Emit(OpCodes.Ret); + + // $Object - call SetPropertyStrict + il.MarkLabel(sharpTSObjectLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSObjectType); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Ldarg_3); // strictMode + il.Emit(OpCodes.Callvirt, runtime.TSObjectSetPropertyStrict); + il.Emit(OpCodes.Ret); + + il.MarkLabel(nullLabel); + il.Emit(OpCodes.Ret); + + il.MarkLabel(dictLabel); + // For dictionaries, check frozen/sealed tables + var frozenCheckLabel = il.DefineLabel(); + var sealedCheckLabel = il.DefineLabel(); + var doSetLabel = il.DefineLabel(); + var valueLocal = il.DeclareLocal(_types.Object); + + // Check if frozen: _frozenObjects.TryGetValue(obj, out _) + il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloca, valueLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); + il.Emit(OpCodes.Brfalse, sealedCheckLabel); // Not frozen, check sealed + + // Object is frozen - check strict mode + il.Emit(OpCodes.Ldarg_3); // strictMode + il.Emit(OpCodes.Brfalse, nullLabel); // Not strict, silently return + + // Strict mode and frozen - throw TypeError + EmitThrowTypeErrorWithName(il, runtime, "Cannot assign to read only property '", "' of object"); + + // Check if sealed and property doesn't exist + il.MarkLabel(sealedCheckLabel); + il.Emit(OpCodes.Ldsfld, runtime.SealedObjectsField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloca, valueLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); + var extensibleCheckLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, extensibleCheckLabel); // Not sealed, check extensibility + + // Object is sealed - check if property exists + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); + il.Emit(OpCodes.Brtrue, doSetLabel); // Property exists, can modify + + // Property doesn't exist and object is sealed - check strict mode + il.Emit(OpCodes.Ldarg_3); // strictMode + il.Emit(OpCodes.Brfalse, nullLabel); // Not strict, silently return + + // Strict mode and sealed with new property - throw TypeError + EmitThrowTypeErrorWithName(il, runtime, "Cannot add property '", "' to a sealed object"); + + // Check extensibility via $PropertyDescriptorStore.CanAddProperty - fully standalone, no reflection + il.MarkLabel(extensibleCheckLabel); + il.Emit(OpCodes.Ldarg_0); // obj + il.Emit(OpCodes.Ldarg_1); // name + il.Emit(OpCodes.Call, runtime.PDSCanAddProperty); + il.Emit(OpCodes.Brtrue, doSetLabel); // Can add property, proceed to set + + // Cannot add property (non-extensible) - check strict mode + il.Emit(OpCodes.Ldarg_3); // strictMode + il.Emit(OpCodes.Brfalse, nullLabel); // Not strict, silently return + + // Strict mode and non-extensible with new property - throw TypeError + EmitThrowTypeErrorWithName(il, runtime, "Cannot add property '", "' to a non-extensible object"); + + // Actually set the property. Mirrors the non-strict SetProperty doSet + // arm: honor a PDS accessor setter, and an existing non-writable data + // descriptor (or getter-only accessor). Pre-fix the strict path skipped + // straight to dict.set_Item, so under "use strict": (1) an accessor + // setter was bypassed and overwritten with a data value, and (2) writes + // to a writable:false property neither threw nor were suppressed — + // `verifyProperty`'s isWritable probe then saw the write succeed (Test262 + // Object/defineProperty 15.2.3.6-3-181 et al.). + il.MarkLabel(doSetLabel); + + // PDS accessor setter present → invoke it (ECMA-262 OrdinarySet: a setter + // fires regardless of strictness). + var strictSetterLocal = il.DeclareLocal(_types.Object); + var strictNoSetterLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloca, strictSetterLocal); + il.Emit(OpCodes.Call, runtime.PDSTryGetSetter); + il.Emit(OpCodes.Brfalse, strictNoSetterLabel); + EmitInvokePdsSetterWithValueAndReturn(il, runtime, strictSetterLocal); + il.MarkLabel(strictNoSetterLabel); + + // Non-writable (data writable:false, or getter-only accessor) → strict + // throws TypeError (ECMA-262 §6.2.5.6 / PutValue), sloppy silently + // returns. PDSIsWritable returns true when no descriptor exists. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.PDSIsWritable); + var strictWritableLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, strictWritableLabel); + il.Emit(OpCodes.Ldarg_3); // strictMode + il.Emit(OpCodes.Brfalse, nullLabel); // sloppy → silent return + EmitThrowTypeErrorWithName(il, runtime, "Cannot assign to read only property '", "' of object"); + il.MarkLabel(strictWritableLabel); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "set_Item")); + il.Emit(OpCodes.Ret); + } + /// + /// Emits SetIndexStrict(object obj, object index, object value, bool strictMode) -> void + /// In strict mode, throws TypeError for modifications to frozen/sealed arrays. + /// + private void EmitSetIndexStrict(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "SetIndexStrict", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.Object, _types.Object, _types.Object, _types.Boolean] + ); + runtime.SetIndexStrict = method; + + var il = method.GetILGenerator(); + var nullLabel = il.DefineLabel(); + var sharpTSArrayLabel = il.DefineLabel(); + var listLabel = il.DefineLabel(); + var dictLabel = il.DefineLabel(); + + // null check + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brfalse, nullLabel); + + // Symbol-keyed write (symbols work on any receiver type) — route to the + // non-strict SetIndex, which has the symbol-key handler (registered + // accessor setter, else symbol-dict store with extensibility checks). + // SetIndexStrict otherwise dispatches only by RECEIVER type ($Array / + // List / Dictionary) and would ToString a symbol key into a bogus string + // key, dropping the write — so under "use strict" `obj[sym] = v` and + // `obj[Symbol.iterator] = fn` were lost (Test262 Object/ + // getOwnPropertySymbols object-contains-symbol-property-without-description, + // Array/from iter-get-iter-val-err). + // The store itself (registered setter / symbol-dict write) is delegated + // to the non-strict SetIndex, but its symbol handler silently no-ops on a + // frozen/sealed/non-extensible receiver. In strict mode those must throw + // a TypeError (ECMA-262 PutValue), so enforce that here first: + // frozen → always throw (props non-writable + non-extensible); + // sealed/non-ext → throw only when the symbol key is NEW (adding it); + // otherwise → delegate to SetIndex (store / update / invoke setter). + // (Test262 Object/freeze frozen-object-contains-symbol-properties-strict.) + var notSymbolKeyLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.IsSymbolMethod); + il.Emit(OpCodes.Brfalse, notSymbolKeyLabel); + + var symThrowLabel = il.DefineLabel(); + var symRouteLabel = il.DefineLabel(); + var symStateTmp = il.DeclareLocal(_types.Object); + var cwtTryGetValue = _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType()); + + // frozen → throw. + il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloca, symStateTmp); + il.Emit(OpCodes.Callvirt, cwtTryGetValue); + il.Emit(OpCodes.Brtrue, symThrowLabel); + + // sealed OR non-extensible → throw only if the symbol key is not already present. + var symSealedOrNonExtLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldsfld, runtime.SealedObjectsField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloca, symStateTmp); + il.Emit(OpCodes.Callvirt, cwtTryGetValue); + il.Emit(OpCodes.Brtrue, symSealedOrNonExtLabel); + il.Emit(OpCodes.Ldsfld, runtime.NonExtensibleObjectsField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloca, symStateTmp); + il.Emit(OpCodes.Callvirt, cwtTryGetValue); + il.Emit(OpCodes.Brfalse, symRouteLabel); // extensible → store + il.MarkLabel(symSealedOrNonExtLabel); + // sealed/non-ext: present symbol → allow update (route); absent → throw. + var symDictLocal = il.DeclareLocal(_types.DictionaryObjectObject); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.GetSymbolDictMethod); + il.Emit(OpCodes.Stloc, symDictLocal); + il.Emit(OpCodes.Ldloc, symDictLocal); + il.Emit(OpCodes.Brfalse, symThrowLabel); // no symbol dict → key absent → throw + il.Emit(OpCodes.Ldloc, symDictLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "ContainsKey", _types.Object)); + il.Emit(OpCodes.Brfalse, symThrowLabel); // key absent → throw + + il.MarkLabel(symRouteLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.SetIndex); + il.Emit(OpCodes.Ret); + + il.MarkLabel(symThrowLabel); + EmitThrowTypeError(il, runtime, "Cannot assign to a read-only or non-extensible property"); + + il.MarkLabel(notSymbolKeyLabel); + + // Check if $Array (for strict mode support) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSArrayType); + il.Emit(OpCodes.Brtrue, sharpTSArrayLabel); + + // List + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.ListOfObjectNullable); + il.Emit(OpCodes.Brtrue, listLabel); + + // Dictionary + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, dictLabel); + + // $Object / $TSFunction / System.Type / other receivers: route indexed + // writes to SetPropertyStrict(obj, ToString(index), value, strictMode), + // mirroring how the non-strict SetIndex routes them to SetProperty. Pre-fix + // these fell through to a silent no-op under "use strict", so `child[0] = v` + // on a class instance / $Object dropped the element — which broke + // Array.prototype.{reduce,every,filter,indexOf}.call(nonArrayObj, …) on + // a `new Con()`-style receiver (Test262 Array/prototype/*/15.4.4.*-2-*). + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "ToString")); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Ldarg_3); // strictMode + il.Emit(OpCodes.Call, runtime.SetPropertyStrict); + il.Emit(OpCodes.Ret); + + // $Array - call SetStrict with index and strictMode via the long API. + // Stage E.2 M6: widened from TSArraySetStrict (int) to TSArraySetStrictLong + // so `"use strict"; arr[2147483648] = v` doesn't truncate to int.MinValue. + // Parallel to the M3 GetIndex/SetIndex widening. + il.MarkLabel(sharpTSArrayLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, runtime.TSArrayType); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Convert, "ToInt64", _types.Object)); + il.Emit(OpCodes.Ldarg_2); // value + il.Emit(OpCodes.Ldarg_3); // strictMode + il.Emit(OpCodes.Callvirt, runtime.TSArraySetStrictLong); + il.Emit(OpCodes.Ret); + + il.MarkLabel(nullLabel); + il.Emit(OpCodes.Ret); + + il.MarkLabel(listLabel); + // Check if frozen - in strict mode, throw TypeError + var listSetLabel = il.DefineLabel(); + var listFrozenCheckLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldsfld, runtime.FrozenObjectsField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloca, listFrozenCheckLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ConditionalWeakTable, "TryGetValue", _types.Object, _types.Object.MakeByRefType())); + var listNotFrozenLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, listNotFrozenLabel); + // Frozen - check strictMode and throw if true + il.Emit(OpCodes.Ldarg_3); // strictMode + var listFrozenSilentLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, listFrozenSilentLabel); + EmitThrowTypeError(il, runtime, "Cannot assign to read only property of frozen array"); + il.MarkLabel(listFrozenSilentLabel); + il.Emit(OpCodes.Ret); // Silently return in non-strict mode + il.MarkLabel(listNotFrozenLabel); + // Not frozen - set normally + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.ListOfObjectNullable); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Unbox_Any, _types.Double); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ListOfObjectNullable, "set_Item", _types.Int32, _types.Object)); + il.Emit(OpCodes.Ret); + + il.MarkLabel(dictLabel); + // Dictionary string-key write: route to SetPropertyStrict(obj, ToString(idx), + // value, strictMode) so PDS accessor setters, non-writable data descriptors, + // and strict TypeErrors are honored identically to named-property writes. + // Pre-fix this did a raw dict.set_Item, bypassing the writable/setter checks — + // so under "use strict" `obj[name] = v` on a writable:false property neither + // threw nor was suppressed, which propertyHelper.js's isWritable probe (it + // writes via `obj[name] = …`) read back as "writable". (Test262 Object/ + // defineProperty 15.2.3.6-3-181, defineProperties 15.2.3.7-* et al.) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "ToString")); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Ldarg_3); // strictMode + il.Emit(OpCodes.Call, runtime.SetPropertyStrict); + il.Emit(OpCodes.Ret); + } +} diff --git a/Compilation/RuntimeEmitter.Operators.cs b/Compilation/RuntimeEmitter.Operators.cs new file mode 100644 index 00000000..2d420824 --- /dev/null +++ b/Compilation/RuntimeEmitter.Operators.cs @@ -0,0 +1,1218 @@ +using System.Reflection; +using System.Reflection.Emit; +using System.Text; + +namespace SharpTS.Compilation; + +// Split out of RuntimeEmitter.CoreUtilities.cs (#1141). Emits the runtime +// operator helpers: relational (<, <=), typeof, instanceof, in, +, ==, ===. +public partial class RuntimeEmitter +{ + /// + /// Emits $Runtime.JsLessThan(object x, object y) -> bool: + /// ECMA-262 7.2.13 IsLessThan abstract algorithm (LeftFirst=true). + /// If both operands are strings, lexicographic comparison. + /// Otherwise both are coerced via ToNumber and numerically compared + /// (NaN on either side yields false). + /// + private void EmitJsLessThan(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "JsLessThan", + MethodAttributes.Public | MethodAttributes.Static, + _types.Boolean, + [_types.Object, _types.Object]); + runtime.JsLessThan = method; + + var il = method.GetILGenerator(); + + // If both args are strings, do lexicographic comparison (CompareOrdinal < 0). + var notBothStrings = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brfalse, notBothStrings); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brfalse, notBothStrings); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Castclass, _types.String); + var compareOrdinal = _types.GetMethod(_types.String, "CompareOrdinal", _types.String, _types.String); + il.Emit(OpCodes.Call, compareOrdinal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Clt); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notBothStrings); + // Numeric path: a = ToNumber(arg0); b = ToNumber(arg1); a < b ? true : false (NaN → false). + var aLocal = il.DeclareLocal(_types.Double); + var bLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.ToNumber); + il.Emit(OpCodes.Stloc, aLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.ToNumber); + il.Emit(OpCodes.Stloc, bLocal); + // NaN check: a == a, b == b + var notNaN = il.DefineLabel(); + var falseLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, aLocal); + il.Emit(OpCodes.Ldloc, aLocal); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Brfalse, falseLabel); + il.Emit(OpCodes.Ldloc, bLocal); + il.Emit(OpCodes.Ldloc, bLocal); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Brfalse, falseLabel); + il.MarkLabel(notNaN); + il.Emit(OpCodes.Ldloc, aLocal); + il.Emit(OpCodes.Ldloc, bLocal); + il.Emit(OpCodes.Clt); + il.Emit(OpCodes.Ret); + il.MarkLabel(falseLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits $Runtime.JsLessOrEqual(object x, object y) -> bool: + /// ECMA-262 abstract relational comparison: x <= y is "y < x is false + /// AND neither operand is NaN". Implemented as !JsLessThan(y, x) provided + /// neither is NaN; we replicate the helper inline to avoid double ToNumber. + /// + private void EmitJsLessOrEqual(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "JsLessOrEqual", + MethodAttributes.Public | MethodAttributes.Static, + _types.Boolean, + [_types.Object, _types.Object]); + runtime.JsLessOrEqual = method; + + var il = method.GetILGenerator(); + + // If both strings: CompareOrdinal <= 0 + var notBothStrings = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brfalse, notBothStrings); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brfalse, notBothStrings); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Castclass, _types.String); + var compareOrdinal = _types.GetMethod(_types.String, "CompareOrdinal", _types.String, _types.String); + il.Emit(OpCodes.Call, compareOrdinal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Cgt); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notBothStrings); + var aLocal = il.DeclareLocal(_types.Double); + var bLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.ToNumber); + il.Emit(OpCodes.Stloc, aLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.ToNumber); + il.Emit(OpCodes.Stloc, bLocal); + var falseLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, aLocal); + il.Emit(OpCodes.Ldloc, aLocal); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Brfalse, falseLabel); + il.Emit(OpCodes.Ldloc, bLocal); + il.Emit(OpCodes.Ldloc, bLocal); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Brfalse, falseLabel); + // a <= b → !(a > b) → !(b < a) + il.Emit(OpCodes.Ldloc, bLocal); + il.Emit(OpCodes.Ldloc, aLocal); + il.Emit(OpCodes.Clt); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Ret); + il.MarkLabel(falseLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + } + + private void EmitTypeOf(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "TypeOf", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.Object] + ); + runtime.TypeOf = method; + + var il = method.GetILGenerator(); + var nullLabel = il.DefineLabel(); + var undefinedLabel = il.DefineLabel(); + var boolLabel = il.DefineLabel(); + var numberLabel = il.DefineLabel(); + var stringLabel = il.DefineLabel(); + var symbolLabel = il.DefineLabel(); + var functionLabel = il.DefineLabel(); + var endLabel = il.DefineLabel(); + + // null => "object" (JS typeof null === "object") + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brfalse, nullLabel); + + // undefined => "undefined" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, undefinedLabel); + + // Check for union types using $IUnionType marker interface + // If value implements $IUnionType, unwrap via Value property and recurse + var notUnionLabel = il.DefineLabel(); + var unionLocal = il.DeclareLocal(runtime.IUnionTypeInterface); + + // Check: if (value is $IUnionType union) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.IUnionTypeInterface); + il.Emit(OpCodes.Stloc, unionLocal); + il.Emit(OpCodes.Ldloc, unionLocal); + il.Emit(OpCodes.Brfalse, notUnionLabel); + + // Get underlying value via interface: union.Value + il.Emit(OpCodes.Ldloc, unionLocal); + il.Emit(OpCodes.Callvirt, runtime.IUnionTypeValueGetter); + + // return TypeOf(underlyingValue) - recursive call + il.Emit(OpCodes.Call, method); // Recursive call to self + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(notUnionLabel); + + // bool => "boolean" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Boolean); + il.Emit(OpCodes.Brtrue, boolLabel); + + // double => "number" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Double); + il.Emit(OpCodes.Brtrue, numberLabel); + + // string => "string" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, stringLabel); + + // TSSymbol => "symbol" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSSymbolType); + il.Emit(OpCodes.Brtrue, symbolLabel); + + // TSFunction => "function" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSFunctionType); + il.Emit(OpCodes.Brtrue, functionLabel); + + // $BoundTSFunction => "function" (returned by Function.prototype.bind + // and similar paths; without this, `typeof fn.bind(x) === "object"`). + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.BoundTSFunctionType); + il.Emit(OpCodes.Brtrue, functionLabel); + + // $FunctionBindWrapper / $FunctionCallWrapper / $FunctionApplyWrapper + // => "function". These wrap non-$TSFunction targets for late-bound + // dispatch and need to be callable from JS land. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.FunctionBindWrapperType); + il.Emit(OpCodes.Brtrue, functionLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.FunctionCallWrapperType); + il.Emit(OpCodes.Brtrue, functionLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.FunctionApplyWrapperType); + il.Emit(OpCodes.Brtrue, functionLabel); + + // Delegate => "function" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Delegate); + il.Emit(OpCodes.Brtrue, functionLabel); + + // $PromisifiedFunction / $DeprecatedFunction => "function" (from util.promisify/deprecate). + // These types only exist when UsesUtilPromisify is set; without the feature + // the runtime fields are null and Isinst would NRE at IL emission time. + if (_features.UsesUtilPromisify) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSPromisifiedFunctionType); + il.Emit(OpCodes.Brtrue, functionLabel); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSDeprecatedFunctionType); + il.Emit(OpCodes.Brtrue, functionLabel); + } + + // $BoundArrayMethod / $BoundMapMethod / $BoundSetMethod / $BoundAnyFunction => "function" + // These are callable wrappers returned by GetListProperty/GetMapProperty/GetSetProperty + // for dynamic property access on arrays/maps/sets (duck typing across module boundaries) + // and by `.bind` on non-$TSFunction targets. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.BoundArrayMethodType); + il.Emit(OpCodes.Brtrue, functionLabel); + + if (_features.UsesMap) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.BoundMapMethodType); + il.Emit(OpCodes.Brtrue, functionLabel); + } + + if (_features.UsesSet) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.BoundSetMethodType); + il.Emit(OpCodes.Brtrue, functionLabel); + } + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.BoundAnyFunctionType); + il.Emit(OpCodes.Brtrue, functionLabel); + + // System.Type => "function" + // Compiled class references (e.g. `const f = Foo` where Foo is a class) are + // emitted as Ldtoken + GetTypeFromHandle, which yields a System.Type. Node/JS + // spec says classes are functions, so `typeof Foo === 'function'` must hold. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Type); + il.Emit(OpCodes.Brtrue, functionLabel); + + // $CJSModule => "object" (falls through naturally, but explicit null-isinst checks + // above might have short-circuited — this branch ensures consistent routing). + // No early return needed; falls through to the generic "object" default at the end. + + // BigInteger => "bigint" + var bigintLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.BigInteger); + il.Emit(OpCodes.Brtrue, bigintLabel); + + // Proxy => check IsCallable: "function" if callable, "object" otherwise + // Uses FullName comparison to avoid SharpTS.dll dependency + var notProxyLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "GetType")); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.Type, "FullName").GetGetMethod()!); + il.Emit(OpCodes.Ldstr, ProxyTypeName); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notProxyLabel); + + // It's a proxy - check IsCallable property getter via reflection + EmitProxyMethodCall(il, () => il.Emit(OpCodes.Ldarg_0), "get_IsCallable", () => + { + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, _types.Object); + }); + il.Emit(OpCodes.Unbox_Any, _types.Boolean); + il.Emit(OpCodes.Brtrue, functionLabel); + il.Emit(OpCodes.Ldstr, "object"); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(notProxyLabel); + + // Default => "object" + il.Emit(OpCodes.Ldstr, "object"); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(nullLabel); + il.Emit(OpCodes.Ldstr, "object"); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(undefinedLabel); + il.Emit(OpCodes.Ldstr, "undefined"); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(boolLabel); + il.Emit(OpCodes.Ldstr, "boolean"); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(numberLabel); + il.Emit(OpCodes.Ldstr, "number"); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(stringLabel); + il.Emit(OpCodes.Ldstr, "string"); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(symbolLabel); + il.Emit(OpCodes.Ldstr, "symbol"); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(functionLabel); + il.Emit(OpCodes.Ldstr, "function"); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(bigintLabel); + il.Emit(OpCodes.Ldstr, "bigint"); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(endLabel); + il.Emit(OpCodes.Ret); + } + + private void EmitInstanceOf(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "InstanceOf", + MethodAttributes.Public | MethodAttributes.Static, + _types.Boolean, + [_types.Object, _types.Object] + ); + runtime.InstanceOf = method; + + var il = method.GetILGenerator(); + var falseLabel = il.DefineLabel(); + var trueLabel = il.DefineLabel(); + + // if (instance == null || classType == null) return false + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brfalse, falseLabel); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Brfalse, falseLabel); + + // A plain Dictionary RHS is never a constructor — letting it reach the + // IsAssignableFrom fallback matched EVERY dict-shaped value (object + // literals, namespace singletons, module namespaces), so + // `{} instanceof AbortSignal` was true (#246). The AbortSignal + // namespace singleton brand-checks signal dicts via their + // "_reasonSet" slot; any other dict RHS is false. + var notDictRhsLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brfalse, notDictRhsLabel); + + if (runtime.AbortSignalNamespaceField != null) + { + var lhsDictLocal = il.DeclareLocal(_types.DictionaryStringObject); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldsfld, runtime.AbortSignalNamespaceField); + il.Emit(OpCodes.Bne_Un, falseLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Stloc, lhsDictLocal); + il.Emit(OpCodes.Ldloc, lhsDictLocal); + il.Emit(OpCodes.Brfalse, falseLabel); + il.Emit(OpCodes.Ldloc, lhsDictLocal); + il.Emit(OpCodes.Ldstr, "_reasonSet"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); + il.Emit(OpCodes.Ret); + } + else + { + il.Emit(OpCodes.Br, falseLabel); + } + + il.MarkLabel(notDictRhsLabel); + + // Per JS spec, `instance instanceof F` where F is a user function walks + // instance's prototype chain looking for F.prototype. Compiled mode's + // legacy InstanceOf used .NET IsAssignableFrom, which is type-system + // semantics — wrong for $TSFunction callees (every $TSFunction has the + // same .NET type, so the check was meaningless). With Stage 0b/0c + // landed, F.prototype is a real $Object and `new F()` links newObj's + // prototype to it; walking the chain via PDSGetPrototype now produces + // the correct answer. + var notTSFuncLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, runtime.TSFunctionType); + il.Emit(OpCodes.Brfalse, notTSFuncLabel); + + // F.prototype = $Runtime.GetFunctionMethod(F, "prototype") + var targetProtoLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "prototype"); + il.Emit(OpCodes.Call, runtime.GetFunctionMethod); + il.Emit(OpCodes.Stloc, targetProtoLocal); + + // If F has no .prototype somehow (e.g., bound function), fall back to + // false rather than walking — JS spec actually throws TypeError, but + // returning false matches what the previous implementation produced. + il.Emit(OpCodes.Ldloc, targetProtoLocal); + il.Emit(OpCodes.Brfalse, falseLabel); + + // Walk: current = PDSGetPrototype(instance); while (current != null) { + // if (current === F.prototype) return true; + // current = PDSGetPrototype(current); } + // return false + var currentLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.PDSGetPrototype); + il.Emit(OpCodes.Stloc, currentLocal); + + var loopLabel = il.DefineLabel(); + il.MarkLabel(loopLabel); + il.Emit(OpCodes.Ldloc, currentLocal); + il.Emit(OpCodes.Brfalse, falseLabel); + + // current === F.prototype ? + il.Emit(OpCodes.Ldloc, currentLocal); + il.Emit(OpCodes.Ldloc, targetProtoLocal); + il.Emit(OpCodes.Beq, trueLabel); + + // current = PDSGetPrototype(current) + il.Emit(OpCodes.Ldloc, currentLocal); + il.Emit(OpCodes.Call, runtime.PDSGetPrototype); + il.Emit(OpCodes.Stloc, currentLocal); + il.Emit(OpCodes.Br, loopLabel); + + il.MarkLabel(notTSFuncLabel); + + // Get type of instance and check IsAssignableFrom (legacy path for + // .NET-typed class-reference instanceof checks). + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, _types.Type); + var notTypeLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notTypeLabel); + + // Stage 4z19 boxed-primitive detection: when classType is one of the + // primitive types (Boolean/Double/String) and instance is a $Object + // wrapper with __primitiveType matching, return true. Only applies + // when the legacy IsAssignableFrom would say false (since .NET + // System.Boolean/Double/String are sealed value types, IsAssignableFrom + // for a $Object always returns false; checking the marker comes first + // to short-circuit). + var classTypeLocal = il.DeclareLocal(_types.Type); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Castclass, _types.Type); + il.Emit(OpCodes.Stloc, classTypeLocal); + + // `x instanceof Object` — the bare `Object` identifier resolves to the + // System.Object Type token (see RuntimeEmitter.GlobalThis.cs), so apply + // JS Object semantics rather than the IsAssignableFrom fallback below: + // every .NET type is assignable to System.Object, so that fallback would + // wrongly say true for boxed primitives (double/bool/string), BigInteger, + // $TSSymbol, and the undefined sentinel. Per ECMA-262 OrdinaryHasInstance + // a primitive O short-circuits to false — so a guest value is an Object + // iff it is non-primitive (mirrors interp's IsJsObject, #342). Boxed + // primitive wrappers (`new Number(5)`) are $Object instances, not boxed + // doubles, so they correctly fall through to true. + var notObjectClassLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, classTypeLocal); + il.Emit(OpCodes.Ldtoken, _types.Object); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Bne_Un, notObjectClassLabel); + void PrimitiveToFalse(Type primType) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, primType); + il.Emit(OpCodes.Brtrue, falseLabel); + } + PrimitiveToFalse(_types.Boolean); + PrimitiveToFalse(_types.Double); + PrimitiveToFalse(_types.String); + PrimitiveToFalse(_types.BigInteger); + PrimitiveToFalse(runtime.TSSymbolType); + PrimitiveToFalse(runtime.UndefinedType); + il.Emit(OpCodes.Br, trueLabel); + il.MarkLabel(notObjectClassLabel); + + // When classType is one of the primitive wrapper types (Number/String/ + // Boolean, which lower to the System.Double/String/Boolean Type tokens), + // the decision is TERMINAL: per ECMA-262 OrdinaryHasInstance only a boxed + // wrapper object (`new Number(5)`) is an instance — a bare primitive is + // NOT. Branch to true iff `instance` carries the matching __primitiveType + // marker, else to false. Falling through to the IsAssignableFrom fallback + // below would wrongly match a bare boxed double/string/bool, since e.g. + // IsAssignableFrom(double, double) is true (#375). Mirrors the terminal + // `Object` branch above. + void CheckBoxed(Type primType, string typeTag) + { + var skip = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, classTypeLocal); + il.Emit(OpCodes.Ldtoken, primType); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Bne_Un, skip); + // classType is the primitive wrapper type — true iff boxed marker matches. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldstr, typeTag); + il.Emit(OpCodes.Call, runtime.IsBoxedPrimitiveOfTypeMethod); + il.Emit(OpCodes.Brtrue, trueLabel); + il.Emit(OpCodes.Br, falseLabel); + il.MarkLabel(skip); + } + CheckBoxed(_types.Boolean, "Boolean"); + CheckBoxed(_types.Double, "Number"); + CheckBoxed(_types.String, "String"); + // Symbol lowers to the $TSSymbol Type token, so a bare $TSSymbol would + // otherwise reach the IsAssignableFrom($TSSymbol, $TSSymbol) fallback and + // wrongly match. Terminal like the wrappers above: true iff `instance` is + // a boxed Symbol wrapper (`Object(sym)`), false for a bare symbol (#449). + CheckBoxed(runtime.TSSymbolType, "Symbol"); + + // `x instanceof Promise`: the Promise identifier resolves to + // typeof(Task), but $Promise instances (and #242 Promise + // subclasses, which derive from $Promise) wrap their task instead of + // being one — accept them here; raw tasks fall through to the + // IsAssignableFrom below. + var notTaskTargetLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, classTypeLocal); + il.Emit(OpCodes.Ldtoken, _types.TaskOfObject); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Bne_Un, notTaskTargetLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.TSPromiseType); + il.Emit(OpCodes.Brtrue, trueLabel); + il.MarkLabel(notTaskTargetLabel); + + // Generic class target: `b instanceof Box` emits the OPEN generic + // definition (Box`1) while instances carry constructed types + // (Box) — IsAssignableFrom never matches across that gap. + // Walk the instance's base-type chain comparing generic definitions. + var notGenericDefLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, classTypeLocal); + il.Emit(OpCodes.Callvirt, _types.Type.GetProperty("IsGenericTypeDefinition")!.GetGetMethod()!); + il.Emit(OpCodes.Brfalse, notGenericDefLabel); + var walkTypeLocal = il.DeclareLocal(_types.Type); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "GetType")); + il.Emit(OpCodes.Stloc, walkTypeLocal); + var genericWalkLoop = il.DefineLabel(); + var genericWalkNext = il.DefineLabel(); + il.MarkLabel(genericWalkLoop); + il.Emit(OpCodes.Ldloc, walkTypeLocal); + il.Emit(OpCodes.Brfalse, falseLabel); + il.Emit(OpCodes.Ldloc, walkTypeLocal); + il.Emit(OpCodes.Callvirt, _types.Type.GetProperty("IsGenericType")!.GetGetMethod()!); + il.Emit(OpCodes.Brfalse, genericWalkNext); + il.Emit(OpCodes.Ldloc, walkTypeLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Type, "GetGenericTypeDefinition")); + il.Emit(OpCodes.Ldloc, classTypeLocal); + il.Emit(OpCodes.Beq, trueLabel); + il.MarkLabel(genericWalkNext); + il.Emit(OpCodes.Ldloc, walkTypeLocal); + il.Emit(OpCodes.Callvirt, _types.Type.GetProperty("BaseType")!.GetGetMethod()!); + il.Emit(OpCodes.Stloc, walkTypeLocal); + il.Emit(OpCodes.Br, genericWalkLoop); + il.MarkLabel(notGenericDefLabel); + + // classType is Type, use it directly + il.Emit(OpCodes.Ldloc, classTypeLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "GetType")); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "IsAssignableFrom", _types.Type)); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notTypeLabel); + // classType is not Type, get its type + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "GetType")); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "GetType")); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "IsAssignableFrom", _types.Type)); + il.Emit(OpCodes.Ret); + + il.MarkLabel(trueLabel); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + + il.MarkLabel(falseLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits HasIn(object key, object obj) -> bool + /// Implements the JavaScript 'in' operator: checks if a property exists in an object. + /// Handles both symbol keys and string keys. + /// + private void EmitHasIn(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "HasIn", + MethodAttributes.Public | MethodAttributes.Static, + _types.Boolean, + [_types.Object, _types.Object] + ); + runtime.HasIn = method; + + var il = method.GetILGenerator(); + var falseLabel = il.DefineLabel(); + var symbolKeyLabel = il.DefineLabel(); + var dictLabel = il.DefineLabel(); + var listLabel = il.DefineLabel(); + + // if (obj == null) return false + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Brfalse, falseLabel); + + // Proxy check: uses obj.GetType().FullName comparison (no SharpTS.dll dependency) + // Note: HasIn signature is (key, obj) so obj is arg_1 + var notProxyLabel = il.DefineLabel(); + EmitProxyHasCheck(il, () => il.Emit(OpCodes.Ldarg_1), () => il.Emit(OpCodes.Ldarg_0), notProxyLabel, runtime); + + il.MarkLabel(notProxyLabel); + + // Check if key is a symbol + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.IsSymbolMethod); + il.Emit(OpCodes.Brtrue, symbolKeyLabel); + + // String key path + // Check if obj is $TSObject + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + var notTSObjectLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notTSObjectLabel); + + // $TSObject - call HasProperty(string) + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Castclass, runtime.TSObjectType); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.Emit(OpCodes.Callvirt, runtime.TSObjectHasProperty); + il.Emit(OpCodes.Ret); + + // Check if obj is Dictionary + il.MarkLabel(notTSObjectLabel); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + il.Emit(OpCodes.Brtrue, dictLabel); + + // $Array (check BEFORE the plain List check — $Array inherits + // List; the List branch below reads base.Count and misses + // sparse holes, and returns true for a hole index where it should + // return false). + var tsArrayHasLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, runtime.TSArrayType); + il.Emit(OpCodes.Brtrue, tsArrayHasLabel); + + // Check if obj is List (array) + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, _types.ListOfObject); + il.Emit(OpCodes.Brtrue, listLabel); + + // Other types (e.g., emitted class instances) — check via $IHasFields + reflection + var classKeyStrLocal = il.DeclareLocal(_types.String); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.Emit(OpCodes.Stloc, classKeyStrLocal); + + var classTrueLabel = il.DefineLabel(); + + // Check $IHasFields interface: call HasProperty(key) for typed backing fields + _fields dict + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, runtime.IHasFieldsInterface); + var notHasFieldsLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notHasFieldsLabel); + + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Castclass, runtime.IHasFieldsInterface); + il.Emit(OpCodes.Ldloc, classKeyStrLocal); + il.Emit(OpCodes.Callvirt, runtime.IHasFieldsHasProperty); + il.Emit(OpCodes.Brtrue, classTrueLabel); + + il.MarkLabel(notHasFieldsLabel); + + // Also check for methods via reflection (e.g., inherited methods) + // Convert camelCase key to PascalCase for .NET method lookup + var classPascalNameLocal = il.DeclareLocal(_types.String); + il.Emit(OpCodes.Ldloc, classKeyStrLocal); + il.Emit(OpCodes.Call, runtime.ToPascalCase); + il.Emit(OpCodes.Stloc, classPascalNameLocal); + + // obj.GetType().GetProperty(pascalName, Instance | Public | IgnoreCase) + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "GetType")); + il.Emit(OpCodes.Ldloc, classPascalNameLocal); + il.Emit(OpCodes.Ldc_I4, (int)(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.IgnoreCase)); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetProperty", _types.String, typeof(System.Reflection.BindingFlags))); + il.Emit(OpCodes.Brtrue, classTrueLabel); + + // obj.GetType().GetMethod(pascalName, Instance | Public | IgnoreCase) + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "GetType")); + il.Emit(OpCodes.Ldloc, classPascalNameLocal); + il.Emit(OpCodes.Ldc_I4, (int)(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.IgnoreCase)); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetMethod", _types.String, typeof(System.Reflection.BindingFlags))); + il.Emit(OpCodes.Brtrue, classTrueLabel); + + il.Emit(OpCodes.Br, falseLabel); + + il.MarkLabel(classTrueLabel); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + + // Dictionary - use ContainsKey + il.MarkLabel(dictLabel); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "ContainsKey", _types.String)); + il.Emit(OpCodes.Ret); + + // List (array) - check "length" property or numeric index + il.MarkLabel(listLabel); + var indexLocal = il.DeclareLocal(_types.Int32); + var listKeyStrLocal = il.DeclareLocal(_types.String); + + // Convert key to string + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.Emit(OpCodes.Stloc, listKeyStrLocal); + + // Check if key == "length" → return true + var notLengthLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, listKeyStrLocal); + il.Emit(OpCodes.Ldstr, "length"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notLengthLabel); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notLengthLabel); + // Try int.TryParse(key, out index) → if fails, return false + il.Emit(OpCodes.Ldloc, listKeyStrLocal); + il.Emit(OpCodes.Ldloca, indexLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Int32, "TryParse", _types.String, _types.Int32.MakeByRefType())); + il.Emit(OpCodes.Brfalse, falseLabel); + + // index >= 0 && index < list.Count + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Blt, falseLabel); + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Castclass, _types.ListOfObject); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count").GetGetMethod()!); + il.Emit(OpCodes.Bge, falseLabel); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + + // $Array — "length" is always present; numeric keys use TSArrayHasIndex + // (which returns false for holes, unlike the List branch's index-in- + // range check). Non-numeric named keys aren't stored on arrays, so + // fall back to false. + il.MarkLabel(tsArrayHasLabel); + { + var tsArrKeyStrLocal = il.DeclareLocal(_types.String); + var tsArrIndexLocal = il.DeclareLocal(_types.Int64); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.Emit(OpCodes.Stloc, tsArrKeyStrLocal); + + // if (key == "length") return true + var tsArrNotLength = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, tsArrKeyStrLocal); + il.Emit(OpCodes.Ldstr, "length"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, tsArrNotLength); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + + il.MarkLabel(tsArrNotLength); + // long.TryParse(key, out idx) — if fails, key isn't numeric → false. + il.Emit(OpCodes.Ldloc, tsArrKeyStrLocal); + il.Emit(OpCodes.Ldloca, tsArrIndexLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Int64, "TryParse", _types.String, _types.Int64.MakeByRefType())); + il.Emit(OpCodes.Brfalse, falseLabel); + + // arr.HasIndex(idx) — handles sparse + hole semantics. + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Castclass, runtime.TSArrayType); + il.Emit(OpCodes.Ldloc, tsArrIndexLocal); + il.Emit(OpCodes.Callvirt, runtime.TSArrayHasIndex); + il.Emit(OpCodes.Ret); + } + + // Symbol key path + il.MarkLabel(symbolKeyLabel); + // Get symbol dict and check if key exists + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.GetSymbolDictMethod); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "ContainsKey", _types.Object)); + il.Emit(OpCodes.Ret); + + // Return false + il.MarkLabel(falseLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + } + + private void EmitAdd(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "Add", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.Object, _types.Object] + ); + runtime.Add = method; + + var il = method.GetILGenerator(); + var stringConcatLabel = il.DefineLabel(); + var undefinedNanLabel = il.DefineLabel(); + + // ECMA-262 §13.10.1 step 1-2: ToPrimitive both operands (default hint) + // before the string-vs-numeric branch. UnwrapIfBoxed handles the boxed- + // primitive case (`new String("x") + "y"` → "xy" instead of + // "[object Object]y"); plain $Object operands pass through unchanged + // and continue to the existing Stringify path which calls .ToString(). + var leftLocal = il.DeclareLocal(_types.Object); + var rightLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.UnwrapIfBoxedMethod); + il.Emit(OpCodes.Stloc, leftLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.UnwrapIfBoxedMethod); + il.Emit(OpCodes.Stloc, rightLocal); + + // if (left is string || right is string) string concat + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, stringConcatLabel); + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, stringConcatLabel); + + // Either operand $Undefined → NaN (ECMA-262 12.8.3: ToNumber(undefined) = NaN, + // and any arithmetic with NaN yields NaN). Convert.ToDouble($Undefined) throws + // because $Undefined isn't IConvertible; short-circuit here. + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, undefinedNanLabel); + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, undefinedNanLabel); + + // Numeric addition + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Convert, "ToDouble", _types.Object)); + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Convert, "ToDouble", _types.Object)); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Box, _types.Double); + il.Emit(OpCodes.Ret); + + il.MarkLabel(undefinedNanLabel); + il.Emit(OpCodes.Ldc_R8, double.NaN); + il.Emit(OpCodes.Box, _types.Double); + il.Emit(OpCodes.Ret); + + // String concat - StringifyCoerce: JS-compatible conversion (null->"null", + // bool->"true"/"false") that throws TypeError for Symbol operands (§7.1.17). + il.MarkLabel(stringConcatLabel); + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Call, runtime.StringifyCoerce); + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Call, runtime.StringifyCoerce); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "Concat", _types.String, _types.String)); + il.Emit(OpCodes.Ret); + } + + /// + /// Declares the Equals MethodBuilder shell. Body fills in via + /// , which must run AFTER EmitToJsString so the + /// Object-vs-String spec branch can reference runtime.ToJsString. + /// + internal void DeclareEquals(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + runtime.Equals = typeBuilder.DefineMethod( + "Equals", + MethodAttributes.Public | MethodAttributes.Static, + _types.Boolean, + [_types.Object, _types.Object] + ); + } + + private void EmitEquals(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = runtime.Equals; + + var il = method.GetILGenerator(); + var trueLabel = il.DefineLabel(); + var falseLabel = il.DefineLabel(); + var checkRightNullish = il.DefineLabel(); + var notBothNullish = il.DefineLabel(); + var objectEqualsLabel = il.DefineLabel(); + var endLabel = il.DefineLabel(); + + // ECMA-262 §7.2.14 step 11/12: when one operand is an Object and the + // other a primitive, IsLooselyEqual delegates to ToPrimitive on the + // Object then re-runs. For boxed-primitive wrappers the spec'd + // OrdinaryToPrimitive lands at __primitiveValue via valueOf, so unwrap + // upfront and let the existing primitive-vs-primitive logic do the + // rest. Plain $Object operands without a __primitiveType marker pass + // through unchanged (UnwrapIfBoxed is a no-op there) and continue to + // the existing Dict/$Object-vs-primitive ToNumber path below, which + // handles `Number.prototype == 0` and similar. + var leftLocal = il.DeclareLocal(_types.Object); + var rightLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.UnwrapIfBoxedMethod); + il.Emit(OpCodes.Stloc, leftLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.UnwrapIfBoxedMethod); + il.Emit(OpCodes.Stloc, rightLocal); + + // Local to track if left is nullish + var leftNullish = il.DeclareLocal(_types.Boolean); + var rightNullish = il.DeclareLocal(_types.Boolean); + + // Check if left is nullish (null or undefined) + // leftNullish = (left == null || left is SharpTSUndefined) + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Brfalse_S, checkRightNullish); // left is null + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Cgt_Un); // true if left is SharpTSUndefined + il.Emit(OpCodes.Stloc, leftNullish); + il.Emit(OpCodes.Br_S, notBothNullish); + + il.MarkLabel(checkRightNullish); + // Left is null - mark as nullish + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Stloc, leftNullish); + + il.MarkLabel(notBothNullish); + + // Check if right is nullish (null or undefined) + // rightNullish = (right == null || right is SharpTSUndefined) + var rightNotNull = il.DefineLabel(); + var afterRightCheck = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Brtrue_S, rightNotNull); + // Right is null - mark as nullish + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Stloc, rightNullish); + il.Emit(OpCodes.Br_S, afterRightCheck); + + il.MarkLabel(rightNotNull); + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Cgt_Un); // true if right is SharpTSUndefined + il.Emit(OpCodes.Stloc, rightNullish); + + il.MarkLabel(afterRightCheck); + + // If both are nullish, return true (null == undefined) + il.Emit(OpCodes.Ldloc, leftNullish); + il.Emit(OpCodes.Ldloc, rightNullish); + il.Emit(OpCodes.And); + il.Emit(OpCodes.Brtrue, trueLabel); + + // If only one is nullish, return false + il.Emit(OpCodes.Ldloc, leftNullish); + il.Emit(OpCodes.Ldloc, rightNullish); + il.Emit(OpCodes.Or); + il.Emit(OpCodes.Brtrue, falseLabel); + + // ECMA-262 7.2.14 IsLooselyEqual: when one side is a Dictionary/$Object + // and the other is a String, Number, or Boolean, the spec calls + // ToPrimitive(object) then recursively compares. Two cases for the + // primitive type matter: + // - String: compare as strings (ToJsString fires the same ToPrimitive + // valueOf/toString chain ToNumber would, but yields a string — + // `new String("one") == "one"` returns true because + // ToPrimitive(wrapper) is "one", then "one" === "one"). + // - Number/Boolean: compare as numbers (ToNumber on both sides; + // ToNumber(boolean)=0/1 per spec; ToNumber on the object does + // ToPrimitive(hint number) then ToNumber). + // Without the String split, `wrapper == "non-numeric"` was always + // false because ToNumber("non-numeric")=NaN and NaN!==NaN. + + // If LEFT is Dict/$Object and RIGHT is double/string/bool → coerce LEFT. + var notLeftCoercibleLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + var leftIsDictLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, leftIsDictLabel); + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brfalse, notLeftCoercibleLabel); + il.MarkLabel(leftIsDictLabel); + // Right is String → ToJsString(LEFT) and string-compare. + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Isinst, _types.String); + var leftObjVsStringLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, leftObjVsStringLabel); + // Right is double/bool → ToNumber both and Ceq. + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Isinst, _types.Double); + var leftObjVsNumLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, leftObjVsNumLabel); + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Isinst, _types.Boolean); + il.Emit(OpCodes.Brfalse, notLeftCoercibleLabel); + il.MarkLabel(leftObjVsNumLabel); + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Call, runtime.ToNumber); + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Call, runtime.ToNumber); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Br, endLabel); + // Object-vs-String: ToJsString(LEFT) and string-compare via op_Equality. + il.MarkLabel(leftObjVsStringLabel); + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Call, runtime.ToJsString); + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Call, _types.StringOpEquality); + il.Emit(OpCodes.Br, endLabel); + il.MarkLabel(notLeftCoercibleLabel); + + // Symmetric: RIGHT is Dict/$Object and LEFT is primitive. + var notRightCoercibleLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + var rightIsDictLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, rightIsDictLabel); + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Isinst, runtime.TSObjectType); + il.Emit(OpCodes.Brfalse, notRightCoercibleLabel); + il.MarkLabel(rightIsDictLabel); + // Left is String → ToJsString(RIGHT) and string-compare. + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Isinst, _types.String); + var rightObjVsStringLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, rightObjVsStringLabel); + // Left is double/bool → ToNumber both and Ceq. + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Isinst, _types.Double); + var rightObjVsNumLabel = il.DefineLabel(); + il.Emit(OpCodes.Brtrue, rightObjVsNumLabel); + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Isinst, _types.Boolean); + il.Emit(OpCodes.Brfalse, notRightCoercibleLabel); + il.MarkLabel(rightObjVsNumLabel); + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Call, runtime.ToNumber); + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Call, runtime.ToNumber); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Br, endLabel); + // String-vs-Object: ToJsString(RIGHT) and string-compare. + il.MarkLabel(rightObjVsStringLabel); + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Call, runtime.ToJsString); + il.Emit(OpCodes.Call, _types.StringOpEquality); + il.Emit(OpCodes.Br, endLabel); + il.MarkLabel(notRightCoercibleLabel); + + // Neither is nullish - use object.Equals + il.Emit(OpCodes.Ldloc, leftLocal); + il.Emit(OpCodes.Ldloc, rightLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Object, "Equals", _types.Object, _types.Object)); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(trueLabel); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(falseLabel); + il.Emit(OpCodes.Ldc_I4_0); + + il.MarkLabel(endLabel); + il.Emit(OpCodes.Ret); + } + + private void EmitStrictEquals(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // ECMA-262 IsStrictlyEqual semantics: null/undefined are distinct values + // (unlike loose ==). Used by Array.prototype.indexOf/lastIndexOf/includes, + // which all forbid null/undefined unification per spec. + var method = typeBuilder.DefineMethod( + "StrictEquals", + MethodAttributes.Public | MethodAttributes.Static, + _types.Boolean, + [_types.Object, _types.Object] + ); + runtime.StrictEquals = method; + + var il = method.GetILGenerator(); + var trueLabel = il.DefineLabel(); + var falseLabel = il.DefineLabel(); + var endLabel = il.DefineLabel(); + + var leftIsNull = il.DefineLabel(); + var leftNotUndef = il.DefineLabel(); + + // If left is CLR null → match iff right is CLR null. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brfalse, leftIsNull); + + // If left is $Undefined → match iff right is $Undefined. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brfalse, leftNotUndef); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Cgt_Un); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(leftNotUndef); + // Left is non-null, non-undefined. If right is null or undefined → false. + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Brfalse, falseLabel); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, falseLabel); + + // ECMA-262 IsStrictlyEqual: NaN !== NaN. Object.Equals(NaN, NaN) is true + // in .NET (Double.Equals special-cases NaN as equal to itself), so test + // upfront via double.IsNaN. Pre-fix `[NaN].indexOf(NaN)` returned 0 + // instead of -1. + var notDoubleSEqLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Double); + il.Emit(OpCodes.Brfalse, notDoubleSEqLabel); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, _types.Double); + il.Emit(OpCodes.Brfalse, notDoubleSEqLabel); + // Both are double — if either is NaN, return false. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, _types.Double); + il.Emit(OpCodes.Call, _types.Double.GetMethod("IsNaN", [_types.Double])!); + il.Emit(OpCodes.Brtrue, falseLabel); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Unbox_Any, _types.Double); + il.Emit(OpCodes.Call, _types.Double.GetMethod("IsNaN", [_types.Double])!); + il.Emit(OpCodes.Brtrue, falseLabel); + il.MarkLabel(notDoubleSEqLabel); + + // Both are concrete values — defer to Object.Equals (handles double, + // string, reference equality for objects). + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Object, "Equals", _types.Object, _types.Object)); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(leftIsNull); + // Left is CLR null. Match iff right is also CLR null (NOT $Undefined). + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(trueLabel); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(falseLabel); + il.Emit(OpCodes.Ldc_I4_0); + + il.MarkLabel(endLabel); + il.Emit(OpCodes.Ret); + } +} diff --git a/Execution/Interpreter.Realm.cs b/Execution/Interpreter.Realm.cs new file mode 100644 index 00000000..567393dc --- /dev/null +++ b/Execution/Interpreter.Realm.cs @@ -0,0 +1,373 @@ +using SharpTS.Modules; +using SharpTS.Parsing; +using SharpTS.Runtime; +using SharpTS.Runtime.BuiltIns; +using SharpTS.Runtime.Types; +using System.Collections.Frozen; + +namespace SharpTS.Execution; + +/// +/// Realm / global-state partial of : the process-wide +/// globals table (CreateGlobalsLookup) and the per-realm mutable +/// intrinsics that must not leak across realms or race across worker threads — +/// the Symbol.for registry, Math, the primitive prototypes, +/// RegExp.prototype, and globalThis routing. Extracted verbatim +/// from Interpreter.cs (#1142); no behaviour change. +/// +/// +/// The three ordering-sensitive static fields +/// (, +/// _globalConstants, ) +/// stay together and in textual order here: static field initializers run in +/// textual order within a single file, and CreateGlobalsLookup reads +/// the sentinel while RegExpConstructorObject reads the globals table. +/// +public partial class Interpreter +{ + /// + /// The %Promise% constructor sentinel registered when no Promise singleton + /// claimed the global first. MUST be declared before + /// — static initializers run in textual + /// order and CreateGlobalsLookup reads this field. + /// + internal static readonly SharpTSBuiltInConstructor PromiseConstructorSentinel = new( + BuiltInNames.Promise, + _ => throw new Exception("Runtime Error: Use 'new Promise(executor)' syntax.")); + + /// + /// Frozen dictionary of global constants and built-in singletons for fast lookup. + /// Combines global constants (NaN, Infinity, undefined) with built-in namespaces + /// (Math, JSON, Object, etc.) into a single lookup to minimize dictionary operations. + /// + private static readonly FrozenDictionary _globalConstants = CreateGlobalsLookup(); + + // The process-wide RegExp constructor singleton (a SharpTSBuiltInConstructor), + // resolved once from the static globals table. ECMA-262 §22.2.6.1 requires + // `RegExp.prototype.constructor === RegExp` and, by inheritance, + // `(/x/).constructor === RegExp` — both must reference this exact instance + // for strict-equality identity to hold. Cached so the regex property hot + // path returns it without a dictionary probe. Mirrors the compiled side, + // where the `$RegExp` Type token plays the same role. + internal static readonly object? RegExpConstructorObject = + _globalConstants.TryGetValue(BuiltInNames.RegExp, out var rxCtor) ? rxCtor : null; + + private static FrozenDictionary CreateGlobalsLookup() + { + var globals = new Dictionary + { + [BuiltInNames.NaN] = double.NaN, + [BuiltInNames.Infinity] = double.PositiveInfinity, + [BuiltInNames.Undefined] = Runtime.Types.SharpTSUndefined.Instance, + [BuiltInNames.Fetch] = Runtime.Types.SharpTSFetchGlobal.Instance, + + // SharedArrayBuffer constructor + [BuiltInNames.SharedArrayBuffer] = WorkerBuiltIns.SharedArrayBufferConstructor, + + // ArrayBuffer constructor + [BuiltInNames.ArrayBuffer] = WorkerBuiltIns.ArrayBufferConstructor, + + // DataView constructor + [BuiltInNames.DataView] = WorkerBuiltIns.DataViewConstructor, + }; + + // Add TypedArray constructors using centralized names + foreach (var typedArrayName in BuiltInNames.TypedArrayNames) + { + globals[typedArrayName] = WorkerBuiltIns.GetTypedArrayConstructor(typedArrayName); + } + + // Add Error constructors as global class variables + // This enables typeof Error, class MyError extends Error, const E = Error, etc. + var errorClass = new Runtime.Types.SharpTSErrorClass("Error", null); + globals[BuiltInNames.Error] = errorClass; + foreach (var errorTypeName in BuiltInNames.ErrorTypeNames) + { + if (errorTypeName != "Error") + globals[errorTypeName] = new Runtime.Types.SharpTSErrorClass(errorTypeName, errorClass); + } + + // Bare `Array` reference — needed for Array.prototype.X.apply() patterns + // that real-world CJS packages (yaml, lodash internals) rely on. + globals[BuiltInNames.Array] = Runtime.Types.SharpTSArrayGlobal.Instance; + + // Bare `Function` reference — required for `Function.prototype.call.bind(...)` + // patterns used by test262 propertyHelper.js (and many libraries' native- + // detection paths). Without this, the harness fails at load before any + // test body runs. + globals[BuiltInNames.Function] = Runtime.Types.SharpTSFunctionGlobal.Instance; + + // Node-style `global` alias for globalThis. CJS packages (lodash) + // detect the global object via `typeof global == 'object'` and alias + // its Array/Object/Date/etc. into a local scope. + var gtSingleton = BuiltInRegistry.Instance.GetSingleton(BuiltInNames.GlobalThis); + if (gtSingleton != null) + { + globals["global"] = gtSingleton; + } + + // Add built-in singletons (Math, JSON, Object, etc.) + // These are namespaces that resolve to singleton instances when accessed as variables + string[] singletonNames = + [ + BuiltInNames.Math, BuiltInNames.JSON, BuiltInNames.Object, + BuiltInNames.Number, BuiltInNames.String, BuiltInNames.Boolean, BuiltInNames.Symbol, + BuiltInNames.Console, BuiltInNames.Process, BuiltInNames.GlobalThis, + BuiltInNames.Reflect, BuiltInNames.Promise, BuiltInNames.Atomics, + "Buffer", + // WebCrypto global (#1063): bare `crypto` — an import binding shadows it. + "crypto", + ]; + foreach (var name in singletonNames) + { + var singleton = BuiltInRegistry.Instance.GetSingleton(name); + if (singleton != null) + { + globals[name] = singleton; + } + } + + // Add built-in constructors as global variables (Map, Set, Date, RegExp, etc.) + // Enables typeof Map, val instanceof Map, passing Map as value, Map.groupBy(), etc. + foreach (var (name, factory) in BuiltInConstructorFactory.GetConstructors()) + { + if (!globals.ContainsKey(name)) + globals[name] = new SharpTSBuiltInConstructor(name, factory); + } + + // Expose global functions (parseFloat, parseInt, isNaN, isFinite, + // structuredClone, setTimeout/clearTimeout, etc.) as first-class + // callable values so they can be referenced by name — not just + // invoked directly. CommonJS packages (lodash) alias `var + // freeParseFloat = parseFloat`, and user code may do + // `typeof parseFloat === 'function'`. + string[] globalFunctionNames = + [ + BuiltInNames.ParseInt, BuiltInNames.ParseFloat, + BuiltInNames.IsNaN, BuiltInNames.IsFinite, + BuiltInNames.StructuredClone, + BuiltInNames.EncodeURIComponent, BuiltInNames.DecodeURIComponent, + BuiltInNames.SetTimeout, BuiltInNames.ClearTimeout, + BuiltInNames.SetInterval, BuiltInNames.ClearInterval, + BuiltInNames.QueueMicrotask, + ]; + foreach (var name in globalFunctionNames) + { + if (!globals.ContainsKey(name)) + globals[name] = new SharpTSGlobalFunction(name); + } + + // Bind value-position globals for built-ins that were previously only + // reachable through special-cased `new` expressions or member access + // (#208): bare `AbortSignal`/`Intl`/`ReadableStream`/... otherwise + // throw "Undefined variable". + // + // AbortSignal and Intl are namespace-style globals: member access on + // SharpTSBuiltInConstructor routes through the namespace registry + // (AbortSignal.abort/timeout/any, Intl.NumberFormat/...), while + // direct construction throws per spec (AbortSignal has no public + // constructor; Intl is not a constructor). + globals["AbortSignal"] = new SharpTSBuiltInConstructor("AbortSignal", + _ => throw new Exception("Runtime Error: TypeError: AbortSignal cannot be constructed directly. Use AbortSignal.abort(), AbortSignal.timeout(), or AbortController.")); + globals["Intl"] = new SharpTSBuiltInConstructor("Intl", + _ => throw new Exception("Runtime Error: TypeError: Intl is not a constructor.")); + + // Web-streams constructors: the same singletons stream/web exports, + // so `new ReadableStream(...)`, `ReadableStream.from(...)`, and + // value-position references all share one identity. + globals[BuiltInNames.ReadableStream] = Runtime.Types.SharpTSReadableStreamConstructor.Instance; + globals[BuiltInNames.WritableStream] = Runtime.Types.SharpTSWritableStreamConstructor.Instance; + globals[BuiltInNames.TransformStream] = Runtime.Types.SharpTSTransformStreamConstructor.Instance; + + // MessageChannel as a value (construction already worked by name). + globals[BuiltInNames.MessageChannel] = WorkerBuiltIns.MessageChannelConstructor; + + // Symbol as a value-position global (#234): `typeof Symbol`, + // `const f = Symbol`, and `(Symbol as any).species` need a real + // binding. Its namespace is registered as non-singleton (member + // access routes through SymbolBuiltIns via GetMember), so the + // singleton loop above didn't bind it. The factory implements the + // call form Symbol(description); JS has no `new Symbol()`. + globals[BuiltInNames.Symbol] = new SharpTSBuiltInConstructor( + BuiltInNames.Symbol, + args => new SharpTSSymbol(args.Count > 0 && args[0] is not SharpTSUndefined + ? args[0]?.ToString() + : null)); + + // Promise needs a bare-reference global so `x instanceof Promise`, + // `typeof Promise === 'function'`, and stdlib modules that carry + // Promise as a value can type-check/run. Its namespace is registered + // as non-singleton (to preserve special `new Promise(executor)` + // handling), so it wasn't picked up by the loops above. Register a + // minimal constructor sentinel — `new Promise(executor)` has its + // own dedicated path and does not route through this factory. + if (!globals.ContainsKey(BuiltInNames.Promise)) + { + globals[BuiltInNames.Promise] = PromiseConstructorSentinel; + } + + return globals.ToFrozenDictionary(); + } + + /// + /// The value bare Promise resolves to — whatever the global table + /// actually holds (a registry singleton when one exists, otherwise + /// ). Surfaced as + /// promise.constructor by PromiseBuiltIns.GetMember so the + /// ECMA-262 §27.2.5.1 identity holds: + /// Promise.resolve(1).constructor === Promise (#221). + /// + internal static object PromiseGlobalValue => _globalConstants[BuiltInNames.Promise]; + + // Per-realm RegExp.prototype. Held on the Interpreter (not on the + // process-wide SharpTSBuiltInConstructor singleton) so user mutations + // — `delete RegExp.prototype[Symbol.split]`, `Object.defineProperty`, + // etc. — stay scoped to this realm. Lazily populated on first read of + // `RegExp.prototype`. + private Runtime.Types.SharpTSObject? _regExpPrototype; + internal Runtime.Types.SharpTSObject GetRegExpPrototype() + => _regExpPrototype ??= Runtime.BuiltIns.RegExpBuiltIns.BuildPrototype(); + + // Per-realm Symbol.for registry. Held on the Interpreter (not as a + // process-wide static on SharpTSSymbol) so `Symbol.for(k)` returns a + // symbol unique to this realm and `Symbol.keyFor` cannot leak + // registrations across Interpreter instances. Each realm is its own agent + // per ECMA-262, so a separate registry is the correct semantics — and it + // removes a cross-thread data race: the old static was a plain Dictionary + // mutated by every realm in the process, including concurrent worker + // threads. Mirrors the per-realm RegExp.prototype (#101). Well-known + // symbols (Symbol.iterator, …) are NOT in this registry; they remain + // process-wide singletons. Lazily allocated; thread-confined to this + // realm's execution thread, so no lock is needed. + private Dictionary? _symbolRegistry; + private Dictionary? _symbolReverseRegistry; + + /// + /// Returns this realm's registered symbol for , + /// creating and registering one on first use (ECMA-262 Symbol.for). + /// + internal Runtime.Types.SharpTSSymbol SymbolFor(string key) + { + _symbolRegistry ??= []; + if (_symbolRegistry.TryGetValue(key, out var existing)) + return existing; + + var symbol = new Runtime.Types.SharpTSSymbol(key); + _symbolRegistry[key] = symbol; + (_symbolReverseRegistry ??= [])[symbol] = key; + return symbol; + } + + /// + /// Returns the registry key for in this realm, or + /// null if it was not produced by this realm's Symbol.for + /// (ECMA-262 Symbol.keyFor). + /// + internal string? SymbolKeyFor(Runtime.Types.SharpTSSymbol symbol) + => _symbolReverseRegistry is not null + && _symbolReverseRegistry.TryGetValue(symbol, out var key) + ? key + : null; + + // Per-realm Math. Math is an extensible ECMA-262 object: guest code may add + // properties (`Math.x = 1`), which must not leak across realms or race + // across worker threads. Held per-Interpreter, mirroring RegExp.prototype + // (#101). The base members (PI, sqrt, …) are stateless and resolved the + // same way for every instance; only the per-instance `_extras` overlay + // differs. Within a realm both the bare `Math` global and `globalThis.Math` + // resolve to this one instance, so `Math === globalThis.Math` holds. + private Runtime.Types.SharpTSMath? _math; + internal Runtime.Types.SharpTSMath GetMath() => _math ??= new Runtime.Types.SharpTSMath(); + + /// + /// Resolves a per-realm mutable built-in intrinsic by its global name + /// (currently Math). These are the built-ins moved off process-global + /// singletons so a realm's guest mutations stay realm-local. Returns + /// false for every other name, leaving normal global resolution + /// unchanged. + /// + internal bool TryGetRealmIntrinsic(string name, out object? value) + { + if (IsRealmIntrinsicName(name)) + { + value = GetMath(); + return true; + } + value = null; + return false; + } + + /// + /// Names of the per-realm mutable built-ins resolved off the Interpreter + /// rather than the shared global-constants table or the namespace + /// fast-path. Used to keep all resolution routes (bare global, namespace + /// member access, globalThis) pointing at the one realm instance so + /// method identity holds (Math.max === Math.max). + /// + internal static bool IsRealmIntrinsicName(string name) => name == "Math"; + + // Per-realm String/Number/Boolean.prototype. Each is an extensible ECMA-262 + // object carrying a guest-writable _extras bag, so — like Math and + // RegExp.prototype (#101) — it is held per-Interpreter: guest writes + // (`String.prototype.x = …`, indexed/`length` assignments Test262 makes + // before calling Array.prototype.* on a primitive) stay realm-local and + // don't race across worker threads. The namespace objects + // (String/Number/Boolean themselves) are immutable and stay shared + // singletons; only the mutable prototypes are per-realm. + private Runtime.Types.SharpTSStringPrototype? _stringPrototype; + private Runtime.Types.SharpTSNumberPrototype? _numberPrototype; + private Runtime.Types.SharpTSBooleanPrototype? _booleanPrototype; + internal Runtime.Types.SharpTSStringPrototype GetStringPrototype() => _stringPrototype ??= new(); + internal Runtime.Types.SharpTSNumberPrototype GetNumberPrototype() => _numberPrototype ??= new(); + internal Runtime.Types.SharpTSBooleanPrototype GetBooleanPrototype() => _booleanPrototype ??= new(); + + // Per-realm globalThis. The global object holds guest-assigned properties + // (`globalThis.x = …`), which must stay realm-local and not race across + // worker threads, so each Interpreter owns its own — like RegExp.prototype + // (#101), Math, and the primitive prototypes. Built-in namespaces (Math, + // JSON, …) are still resolved live through the shared BuiltInRegistry, so + // `globalThis.JSON === JSON` and `globalThis.Math === Math` still hold; only + // the user-property bag is per-realm. Bare `globalThis` and the Node + // `global` alias resolve here (see LookupVariableRV), and sloppy-mode `this` + // binds to it. + private Runtime.Types.SharpTSGlobalThis? _globalThis; + internal Runtime.Types.SharpTSGlobalThis GlobalThis => _globalThis ??= new Runtime.Types.SharpTSGlobalThis(); + + /// + /// Resolves String/Number/Boolean.prototype to + /// this realm's prototype instance when is the + /// corresponding built-in namespace, so the read is realm-local rather than + /// the shared singleton. Returns false for any other receiver, + /// leaving normal member resolution unchanged. + /// + private bool TryGetRealmPrototypeForNamespace(object? obj, out object? prototype) + { + switch (obj) + { + case Runtime.Types.SharpTSStringNamespace: + prototype = GetStringPrototype(); + return true; + case Runtime.Types.SharpTSNumberNamespace: + prototype = GetNumberPrototype(); + return true; + case Runtime.Types.SharpTSBooleanNamespace: + prototype = GetBooleanPrototype(); + return true; + default: + prototype = null; + return false; + } + } + + /// + /// Reads a property off globalThis honoring per-realm intrinsics: a + /// guest own-assignment (globalThis.Math = x) wins, then the realm + /// intrinsic (so globalThis.Math === Math within a realm), then the + /// normal built-in/global resolution. Behaviour is identical to + /// globalThis.GetProperty for every non-intrinsic name. + /// + private object? ResolveGlobalThisRead(Runtime.Types.SharpTSGlobalThis globalThis, string key) + => !globalThis.HasUserProperty(key) && TryGetRealmIntrinsic(key, out var intrinsic) + ? intrinsic + : globalThis.GetProperty(key); +} diff --git a/Execution/Interpreter.Statements.cs b/Execution/Interpreter.Statements.cs index 72077be3..1d424bff 100644 --- a/Execution/Interpreter.Statements.cs +++ b/Execution/Interpreter.Statements.cs @@ -1,10 +1,17 @@ +using SharpTS.Modules; +using SharpTS.Modules.Stdlib; using SharpTS.Parsing; +using SharpTS.Parsing.Visitors; using SharpTS.Runtime; using SharpTS.Runtime.BuiltIns; using SharpTS.Runtime.BuiltIns.Modules; +using SharpTS.Runtime.BuiltIns.Modules.Interpreter; +using SharpTS.Runtime.DotNet; using SharpTS.Runtime.Exceptions; using SharpTS.Runtime.Types; using SharpTS.TypeSystem; +using System.Collections.Frozen; +using System.Threading; namespace SharpTS.Execution; @@ -1654,4 +1661,746 @@ private void DisposeResource(object? resource, bool isAsync) // For other types, return null (no symbol property access) return null; } + + /// + /// Internal wrapper for Execute that allows evaluation contexts to dispatch statements. + /// + /// The statement to execute. + /// The execution result. + internal ExecutionResult ExecuteStatement(Stmt stmt) => Execute(stmt); + + /// + /// Internal async wrapper for statement execution using registry-based dispatch. + /// Uses DispatchStmtAsync which falls back to sync handlers when no async handler exists. + /// + /// The statement to execute. + /// A task containing the execution result. + internal async Task ExecuteStatementAsync(Stmt stmt) + { + return await _registry.DispatchStmtAsync(stmt, this); + } + + /// + /// Dispatches a statement to the appropriate execution handler using the registry. + /// + /// The statement AST node to execute. + /// + /// Handles all statement types including control flow (if, while, for, switch), + /// declarations (var, function, class, enum), and control transfer (return, break, continue, throw). + /// Control flow uses for non-local jumps. + /// + private ExecutionResult Execute(Stmt stmt) + { + return _registry.DispatchStmt(stmt, this); + } + + // Statement handlers - called by the registry + + internal ExecutionResult VisitBlock(Stmt.Block block) => + ExecuteBlock(block.Statements, new RuntimeEnvironment(_environment)); + + internal ExecutionResult VisitLabeledStatement(Stmt.LabeledStatement labeledStmt) => + ExecuteLabeledStatement(labeledStmt); + + internal ExecutionResult VisitSequence(Stmt.Sequence seq) + { + // Execute in current scope (no new environment) + foreach (var s in seq.Statements) + { + var result = Execute(s); + if (result.IsAbrupt) return result; + } + return ExecutionResult.Success(); + } + + internal ExecutionResult VisitExpression(Stmt.Expression exprStmt) + { + Evaluate(exprStmt.Expr); + return ExecutionResult.Success(); + } + + internal ExecutionResult VisitIf(Stmt.If ifStmt) + { + if (IsTruthy(Evaluate(ifStmt.Condition))) + { + return Execute(ifStmt.ThenBranch); + } + else if (ifStmt.ElseBranch != null) + { + return Execute(ifStmt.ElseBranch); + } + return ExecutionResult.Success(); + } + + internal ExecutionResult VisitWhile(Stmt.While whileStmt) + { + var labels = TakePendingLoopLabels(); + return ExecuteWhileCore(_syncContext, whileStmt, labels).GetAwaiter().GetResult(); + } + + internal ExecutionResult VisitDoWhile(Stmt.DoWhile doWhileStmt) + { + var labels = TakePendingLoopLabels(); + return ExecuteDoWhileCore(_syncContext, doWhileStmt, labels).GetAwaiter().GetResult(); + } + + internal ExecutionResult VisitFor(Stmt.For forStmt) + { + // Drain labels parked by an enclosing labeled statement before running the initializer, + // so `continue