diff --git a/Compilation/EmittedRuntime.cs b/Compilation/EmittedRuntime.cs index 9508cfe1..1ac0c895 100644 --- a/Compilation/EmittedRuntime.cs +++ b/Compilation/EmittedRuntime.cs @@ -2588,6 +2588,14 @@ public void RegisterBuiltInModuleMethod(string moduleName, string methodName, Me // NOTE: Must stay in sync with SharpTS.Runtime.Types.StructuredClone public MethodBuilder StructuredCloneClone { get; set; } = null!; + // $DataCloneError : Exception — thrown by StructuredCloneCore for uncloneable values + // (functions, symbols, class instances, Promises, etc.) and on nested occurrences + // inside objects/arrays/maps/sets. Dedicated type (not a generic Exception) so + // $MessagePort/$BroadcastChannel PostMessage can catch specifically a clone failure + // and convert it to a receiver-side 'messageerror' event without swallowing other bugs. + public TypeBuilder TSDataCloneErrorType { get; set; } = null!; + public ConstructorBuilder TSDataCloneErrorCtor { get; set; } = null!; + // worker_threads module methods public MethodBuilder WorkerThreadsIsMainThread { get; set; } = null!; public MethodBuilder WorkerThreadsThreadId { get; set; } = null!; diff --git a/Compilation/RuntimeEmitter.BroadcastChannel.cs b/Compilation/RuntimeEmitter.BroadcastChannel.cs index b6dec41b..260fbcfb 100644 --- a/Compilation/RuntimeEmitter.BroadcastChannel.cs +++ b/Compilation/RuntimeEmitter.BroadcastChannel.cs @@ -29,6 +29,10 @@ public partial class RuntimeEmitter private TypeBuilder _broadcastChannelType = null!; private FieldBuilder _broadcastChannelRegistryField = null!; private FieldBuilder _broadcastChannelNextIdField = null!; + // Shared clone-failure sentinel (#1255), mirroring $MessagePort's _cloneError: enqueued + // in place of a per-subscriber clone when the posted value cannot be structured-cloned. + // Drain compares by reference and fires 'messageerror' instead of 'message'. + private FieldBuilder _broadcastChannelCloneErrorField = null!; private FieldBuilder _broadcastChannelNameField = null!; private FieldBuilder _broadcastChannelIdField = null!; private FieldBuilder _broadcastChannelClosedField = null!; @@ -77,7 +81,11 @@ private void EmitBroadcastChannelClass(ModuleBuilder moduleBuilder, EmittedRunti "_nextId", _types.Int64, FieldAttributes.Private | FieldAttributes.Static); - // Static constructor: initialize _registry + _broadcastChannelCloneErrorField = typeBuilder.DefineField( + "_cloneError", _types.Object, + FieldAttributes.Private | FieldAttributes.Static | FieldAttributes.InitOnly); + + // Static constructor: initialize _registry + _cloneError var cctor = typeBuilder.DefineConstructor( MethodAttributes.Private | MethodAttributes.Static | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, @@ -92,6 +100,9 @@ private void EmitBroadcastChannelClass(ModuleBuilder moduleBuilder, EmittedRunti var bcRegistryCtor = _bcRegistryDictType.GetConstructor([typeof(IEqualityComparer)])!; il.Emit(OpCodes.Newobj, bcRegistryCtor); il.Emit(OpCodes.Stsfld, _broadcastChannelRegistryField); + // _cloneError = new object() + il.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(_types.Object)); + il.Emit(OpCodes.Stsfld, _broadcastChannelCloneErrorField); il.Emit(OpCodes.Ret); } @@ -233,6 +244,41 @@ private void EmitBroadcastChannelDrain(TypeBuilder typeBuilder, EmittedRuntime r il.Emit(OpCodes.Callvirt, tryDequeue); il.Emit(OpCodes.Brfalse, exitLabel); + // A clone-failure sentinel (from postMessage of an uncloneable value, #1255) fires + // 'messageerror' instead of 'message' — WHATWG receiver-side model, mirroring + // $MessagePort. ReferenceEquals(msg, _cloneError). + var notCloneErrorLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, msgLocal); + il.Emit(OpCodes.Ldsfld, _broadcastChannelCloneErrorField); + il.Emit(OpCodes.Bne_Un, notCloneErrorLabel); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldstr, "messageerror"); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Stloc, argsLocal); + il.Emit(OpCodes.Ldloc, argsLocal); + il.Emit(OpCodes.Callvirt, runtime.TSEventEmitterEmit); + il.Emit(OpCodes.Pop); + + // Also invoke the property-style onmessageerror handler if set. + var skipOnMessageErrorLabel = il.DefineLabel(); + var onMsgErrLocal = il.DeclareLocal(runtime.TSFunctionType); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, _broadcastChannelOnMessageErrorField); + il.Emit(OpCodes.Isinst, runtime.TSFunctionType); + il.Emit(OpCodes.Stloc, onMsgErrLocal); + il.Emit(OpCodes.Ldloc, onMsgErrLocal); + il.Emit(OpCodes.Brfalse, skipOnMessageErrorLabel); + il.Emit(OpCodes.Ldloc, onMsgErrLocal); + il.Emit(OpCodes.Ldloc, argsLocal); + il.Emit(OpCodes.Callvirt, runtime.TSFunctionInvoke); + il.Emit(OpCodes.Pop); + il.MarkLabel(skipOnMessageErrorLabel); + + il.Emit(OpCodes.Br, loopTop); + il.MarkLabel(notCloneErrorLabel); + // eventData = new Dictionary() il.Emit(OpCodes.Newobj, _types.DictionaryStringObject.GetConstructor(Type.EmptyTypes)!); il.Emit(OpCodes.Stloc, eventDataLocal); @@ -382,13 +428,28 @@ private void EmitBroadcastChannelPostMessage(TypeBuilder typeBuilder, EmittedRun // Deep-clone the message for this subscriber via $Runtime.StructuredClone(msg, null). // Per-subscriber clone matches the WHATWG spec and prevents mutation aliasing across - // receivers. StructuredClone handles primitives, lists, string/object-keyed dicts, - // and hashsets; unknown types pass through by reference (same as the interpreter). + // receivers. An uncloneable value (#1255) is NOT thrown back on the sender — the + // WHATWG model fires 'messageerror' on the receiver instead (matches the + // interpreter's catch around StructuredClone.Clone in SharpTSBroadcastChannel. + // PostMessage) — so catch $DataCloneError here and enqueue the shared _cloneError + // sentinel for THIS subscriber instead of a clone, then continue to the next one. + var clonedLocal = il.DeclareLocal(_types.Object); + var haveClonedLabel = il.DefineLabel(); + + il.BeginExceptionBlock(); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldnull); // transferList il.Emit(OpCodes.Call, runtime.StructuredCloneClone); - var clonedLocal = il.DeclareLocal(_types.Object); il.Emit(OpCodes.Stloc, clonedLocal); + il.Emit(OpCodes.Leave, haveClonedLabel); + + il.BeginCatchBlock(runtime.TSDataCloneErrorType); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldsfld, _broadcastChannelCloneErrorField); + il.Emit(OpCodes.Stloc, clonedLocal); + il.EndExceptionBlock(); + + il.MarkLabel(haveClonedLabel); // sub._pending.Enqueue(cloned) il.Emit(OpCodes.Ldloc, subLocal); diff --git a/Compilation/RuntimeEmitter.DataCloneError.cs b/Compilation/RuntimeEmitter.DataCloneError.cs new file mode 100644 index 00000000..c44be76f --- /dev/null +++ b/Compilation/RuntimeEmitter.DataCloneError.cs @@ -0,0 +1,43 @@ +using System.Reflection; +using System.Reflection.Emit; + +namespace SharpTS.Compilation; + +/// +/// Emits the $DataCloneError exception type: a standalone (pure-IL, no +/// SharpTS.dll dependency) marker exception thrown by the emitted structured-clone +/// core () for values the HTML +/// structured clone algorithm cannot clone (functions, symbols, class instances, +/// Promises, and any value nested inside an object/array/map/set), mirroring +/// (#1255). +/// +public partial class RuntimeEmitter +{ + private void EmitTSDataCloneErrorType(ModuleBuilder moduleBuilder, EmittedRuntime runtime) + { + var typeBuilder = moduleBuilder.DefineType( + "$DataCloneError", + TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.BeforeFieldInit, + _types.Exception + ); + runtime.TSDataCloneErrorType = typeBuilder; + + // public $DataCloneError(string message) : base("DataCloneError: " + message) + var ctor = typeBuilder.DefineConstructor( + MethodAttributes.Public, + CallingConventions.Standard, + [_types.String] + ); + runtime.TSDataCloneErrorCtor = ctor; + + var il = ctor.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldstr, "DataCloneError: "); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, _types.StringConcat2); + il.Emit(OpCodes.Call, _types.Exception.GetConstructor([_types.String])!); + il.Emit(OpCodes.Ret); + + typeBuilder.CreateType(); + } +} diff --git a/Compilation/RuntimeEmitter.MessageChannel.cs b/Compilation/RuntimeEmitter.MessageChannel.cs index 49d17381..ba5de418 100644 --- a/Compilation/RuntimeEmitter.MessageChannel.cs +++ b/Compilation/RuntimeEmitter.MessageChannel.cs @@ -369,7 +369,6 @@ private void EmitMessagePortPostMessage(TypeBuilder typeBuilder, EmittedRuntime var exitLabel = il.DefineLabel(); var partnerLocal = il.DeclareLocal(typeBuilder); var clonedLocal = il.DeclareLocal(_types.Object); - var typeofLocal = il.DeclareLocal(_types.String); // if (_closed) return il.Emit(OpCodes.Ldarg_0); @@ -388,45 +387,25 @@ private void EmitMessagePortPostMessage(TypeBuilder typeBuilder, EmittedRuntime il.Emit(OpCodes.Brtrue, exitLabel); // Node model: an uncloneable value is NOT thrown back on the sender — the receiver - // fires 'messageerror' (#1077). The emitted $Runtime.StructuredClone returns - // uncloneable values by reference (it has no throw path), so detect the uncloneable - // categories up front — functions/classes/callable wrappers (typeof "function") and - // symbols (typeof "symbol") — and enqueue the shared _cloneError sentinel instead of - // a clone. Drain converts it to 'messageerror'. This mirrors the try/catch around - // StructuredClone.Clone in SharpTSMessagePort.PostMessage. (Deeply nested uncloneables - // remain aliased, matching the emitted clone's existing shallow fallback.) - var markerLabel = il.DefineLabel(); + // fires 'messageerror' instead (#1077). $Runtime.StructuredClone throws + // $DataCloneError for any uncloneable value at ANY nesting depth (#1255); catch it + // here and enqueue the shared _cloneError sentinel instead of a clone. Drain + // converts it to 'messageerror'. Mirrors the try/catch around StructuredClone.Clone + // in SharpTSMessagePort.PostMessage. var haveValueLabel = il.DefineLabel(); - var strEquals = _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String); - // t = TypeOf(msg) - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, runtime.TypeOf); - il.Emit(OpCodes.Stloc, typeofLocal); - - // if (t == "function") goto markerLabel - il.Emit(OpCodes.Ldloc, typeofLocal); - il.Emit(OpCodes.Ldstr, "function"); - il.Emit(OpCodes.Call, strEquals); - il.Emit(OpCodes.Brtrue, markerLabel); - - // if (t == "symbol") goto markerLabel - il.Emit(OpCodes.Ldloc, typeofLocal); - il.Emit(OpCodes.Ldstr, "symbol"); - il.Emit(OpCodes.Call, strEquals); - il.Emit(OpCodes.Brtrue, markerLabel); - - // cloned = $Runtime.StructuredClone(msg, null); goto haveValue + il.BeginExceptionBlock(); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Call, runtime.StructuredCloneClone); il.Emit(OpCodes.Stloc, clonedLocal); - il.Emit(OpCodes.Br, haveValueLabel); + il.Emit(OpCodes.Leave, haveValueLabel); - // cloned = _cloneError sentinel - il.MarkLabel(markerLabel); + il.BeginCatchBlock(runtime.TSDataCloneErrorType); + il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldsfld, _messagePortCloneErrorField); il.Emit(OpCodes.Stloc, clonedLocal); + il.EndExceptionBlock(); il.MarkLabel(haveValueLabel); diff --git a/Compilation/RuntimeEmitter.Objects.Exceptions.cs b/Compilation/RuntimeEmitter.Objects.Exceptions.cs index c990a6b5..12e03b77 100644 --- a/Compilation/RuntimeEmitter.Objects.Exceptions.cs +++ b/Compilation/RuntimeEmitter.Objects.Exceptions.cs @@ -92,11 +92,12 @@ private void EmitWrapException(TypeBuilder typeBuilder, EmittedRuntime runtime) // Check for $PromiseRejectedException - return the Reason property il.MarkLabel(fallbackLabel); + var checkDataCloneErrorLabel = il.DefineLabel(); var checkNodeErrorLabel = il.DefineLabel(); il.Emit(OpCodes.Ldloc, exLocal); il.Emit(OpCodes.Isinst, runtime.TSPromiseRejectedExceptionType); - il.Emit(OpCodes.Brfalse, checkNodeErrorLabel); + il.Emit(OpCodes.Brfalse, checkDataCloneErrorLabel); // It's a $PromiseRejectedException - return its Reason property il.Emit(OpCodes.Ldloc, exLocal); @@ -104,6 +105,19 @@ private void EmitWrapException(TypeBuilder typeBuilder, EmittedRuntime runtime) il.Emit(OpCodes.Call, runtime.TSPromiseRejectedExceptionReasonGetter); il.Emit(OpCodes.Ret); + // Check for $DataCloneError (thrown by StructuredCloneCore, #1255) — return its + // Message directly (a raw string, not wrapped in $Error), matching the + // interpreter's catch binding for a non-guest-throw exception (Interpreter. + // Statements.cs: `ex is ThrowException tex ? tex.Value : ex.Message`). + il.MarkLabel(checkDataCloneErrorLabel); + il.Emit(OpCodes.Ldloc, exLocal); + il.Emit(OpCodes.Isinst, runtime.TSDataCloneErrorType); + il.Emit(OpCodes.Brfalse, checkNodeErrorLabel); + + il.Emit(OpCodes.Ldloc, exLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.Exception, "Message").GetGetMethod()!); + il.Emit(OpCodes.Ret); + // Check for __nodeError marker in Data (Node.js-style fs errors) il.MarkLabel(checkNodeErrorLabel); var standardFallbackLabel = il.DefineLabel(); diff --git a/Compilation/RuntimeEmitter.Worker.cs b/Compilation/RuntimeEmitter.Worker.cs index 7887cc09..bce12ef2 100644 --- a/Compilation/RuntimeEmitter.Worker.cs +++ b/Compilation/RuntimeEmitter.Worker.cs @@ -1482,12 +1482,30 @@ private void EmitStructuredCloneHelper(TypeBuilder runtimeType, EmittedRuntime r var setEnumLocal = coreIl.DeclareLocal(_types.IEnumerator); var currentLocal = coreIl.DeclareLocal(_types.Object); + // $Object (plain object literal, possibly with getters/setters) — clone via its + // Fields dictionary (#1255). + var objLiteralSourceLocal = coreIl.DeclareLocal(runtime.TSObjectType); + var objLiteralClonedFieldsLocal = coreIl.DeclareLocal(_types.DictionaryStringObject); + + // $Error hierarchy — clone via name-based reconstruction + Stack preservation (#1255). + var errSourceLocal = coreIl.DeclareLocal(runtime.TSErrorType); + var errNameLocal = coreIl.DeclareLocal(_types.String); + var errMsgLocal = coreIl.DeclareLocal(_types.String); + var clonedErrLocal = coreIl.DeclareLocal(runtime.TSErrorType); + var nextCheckNull = coreIl.DefineLabel(); var checkSharedArrayBuffer = coreIl.DefineLabel(); + var checkArrayBuffer = coreIl.DefineLabel(); + var checkTypedArray = coreIl.DefineLabel(); var checkList = coreIl.DefineLabel(); var checkStringDict = coreIl.DefineLabel(); + var checkObjectLiteral = coreIl.DefineLabel(); var checkObjectDict = coreIl.DefineLabel(); var checkSet = coreIl.DefineLabel(); + var checkDate = coreIl.DefineLabel(); + var checkRegExp = coreIl.DefineLabel(); + var checkBuffer = coreIl.DefineLabel(); + var checkError = coreIl.DefineLabel(); var fallbackReturn = coreIl.DefineLabel(); var returnClonedList = coreIl.DefineLabel(); var returnClonedStringDict = coreIl.DefineLabel(); @@ -1509,6 +1527,50 @@ private void EmitStructuredCloneHelper(TypeBuilder runtimeType, EmittedRuntime r coreIl.Emit(OpCodes.Ret); coreIl.MarkLabel(nextCheckNull); + // Primitives (number/string/boolean/bigint) and the `undefined` singleton are + // immutable/shared — return as-is, no cloning needed. Matches the interpreter's + // early `value is double or string or bool or BigInteger` return in CloneInternal + // (#1255) — without this, throwing-on-fallback below would wrongly reject every + // clone of a container holding a boxed number/string/bool/bigint/undefined value. + var checkPrimitiveString = coreIl.DefineLabel(); + var checkPrimitiveBool = coreIl.DefineLabel(); + var checkPrimitiveBigInt = coreIl.DefineLabel(); + var checkPrimitiveUndefined = coreIl.DefineLabel(); + + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Isinst, _types.Double); + coreIl.Emit(OpCodes.Brfalse, checkPrimitiveString); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Ret); + + coreIl.MarkLabel(checkPrimitiveString); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Isinst, _types.String); + coreIl.Emit(OpCodes.Brfalse, checkPrimitiveBool); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Ret); + + coreIl.MarkLabel(checkPrimitiveBool); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Isinst, _types.Boolean); + coreIl.Emit(OpCodes.Brfalse, checkPrimitiveBigInt); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Ret); + + coreIl.MarkLabel(checkPrimitiveBigInt); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Isinst, _types.BigInteger); + coreIl.Emit(OpCodes.Brfalse, checkPrimitiveUndefined); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Ret); + + coreIl.MarkLabel(checkPrimitiveUndefined); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Isinst, runtime.UndefinedType); + coreIl.Emit(OpCodes.Brfalse, checkSharedArrayBuffer); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Ret); + // SharedArrayBuffer is transferred by reference. Skip when typed // arrays aren't emitted — no SharedArrayBuffer values can exist. coreIl.MarkLabel(checkSharedArrayBuffer); @@ -1516,13 +1578,71 @@ private void EmitStructuredCloneHelper(TypeBuilder runtimeType, EmittedRuntime r { coreIl.Emit(OpCodes.Ldarg_0); coreIl.Emit(OpCodes.Isinst, runtime.SharedArrayBufferType); - coreIl.Emit(OpCodes.Brfalse, checkList); + coreIl.Emit(OpCodes.Brfalse, checkArrayBuffer); coreIl.Emit(OpCodes.Ldarg_0); coreIl.Emit(OpCodes.Ret); } else { // Always fall through to the next check. + coreIl.Emit(OpCodes.Br, checkArrayBuffer); + } + + // $ArrayBuffer / $TypedArray — deep clone (#1255). A non-shared buffer is copied + // independently; a TypedArray view backed by a SharedArrayBuffer clones to a NEW + // VIEW over the SAME buffer (sharing is the entire point of SharedArrayBuffer, + // matching the interpreter's CreateTypedArrayView for the shared case). + coreIl.MarkLabel(checkArrayBuffer); + if (_features.HasAnyTypedArray) + { + var abLocal = coreIl.DeclareLocal(runtime.ArrayBufferType); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Isinst, runtime.ArrayBufferType); + coreIl.Emit(OpCodes.Stloc, abLocal); + coreIl.Emit(OpCodes.Ldloc, abLocal); + coreIl.Emit(OpCodes.Brfalse, checkTypedArray); + + // return source.Slice(0, source.ByteLength) + coreIl.Emit(OpCodes.Ldloc, abLocal); + coreIl.Emit(OpCodes.Ldc_I4_0); + coreIl.Emit(OpCodes.Ldloc, abLocal); + coreIl.Emit(OpCodes.Callvirt, runtime.ArrayBufferByteLengthGetter); + coreIl.Emit(OpCodes.Callvirt, runtime.ArrayBufferSlice); + coreIl.Emit(OpCodes.Ret); + + coreIl.MarkLabel(checkTypedArray); + var taLocal = coreIl.DeclareLocal(runtime.TypedArrayBaseType); + var taSharedLabel = coreIl.DefineLabel(); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Isinst, runtime.TypedArrayBaseType); + coreIl.Emit(OpCodes.Stloc, taLocal); + coreIl.Emit(OpCodes.Ldloc, taLocal); + coreIl.Emit(OpCodes.Brfalse, checkList); + + coreIl.Emit(OpCodes.Ldloc, taLocal); + coreIl.Emit(OpCodes.Callvirt, runtime.TypedArrayBufferGetter); + coreIl.Emit(OpCodes.Isinst, runtime.SharedArrayBufferType); + coreIl.Emit(OpCodes.Brtrue, taSharedLabel); + + // return source.Slice(0, source.Length) — independent copy + coreIl.Emit(OpCodes.Ldloc, taLocal); + coreIl.Emit(OpCodes.Ldc_I4_0); + coreIl.Emit(OpCodes.Ldloc, taLocal); + coreIl.Emit(OpCodes.Callvirt, runtime.TypedArrayLengthGetter); + coreIl.Emit(OpCodes.Callvirt, runtime.TypedArraySlice); + coreIl.Emit(OpCodes.Ret); + + // return source.Subarray(0, source.Length) — shares the SharedArrayBuffer + coreIl.MarkLabel(taSharedLabel); + coreIl.Emit(OpCodes.Ldloc, taLocal); + coreIl.Emit(OpCodes.Ldc_I4_0); + coreIl.Emit(OpCodes.Ldloc, taLocal); + coreIl.Emit(OpCodes.Callvirt, runtime.TypedArrayLengthGetter); + coreIl.Emit(OpCodes.Callvirt, runtime.TypedArraySubarray); + coreIl.Emit(OpCodes.Ret); + } + else + { coreIl.Emit(OpCodes.Br, checkList); } @@ -1569,7 +1689,7 @@ private void EmitStructuredCloneHelper(TypeBuilder runtimeType, EmittedRuntime r coreIl.Emit(OpCodes.Isinst, _types.DictionaryStringObject); coreIl.Emit(OpCodes.Stloc, sourceDictStringLocal); coreIl.Emit(OpCodes.Ldloc, sourceDictStringLocal); - coreIl.Emit(OpCodes.Brfalse, checkObjectDict); + coreIl.Emit(OpCodes.Brfalse, checkObjectLiteral); coreIl.Emit(OpCodes.Newobj, _types.DictionaryStringObjectCtor); coreIl.Emit(OpCodes.Stloc, clonedDictStringLocal); @@ -1600,6 +1720,27 @@ private void EmitStructuredCloneHelper(TypeBuilder runtimeType, EmittedRuntime r coreIl.Emit(OpCodes.Brtrue, stringDictLoopBody); coreIl.Emit(OpCodes.Br, returnClonedStringDict); + // $Object (object literal, possibly with getters/setters not present on a plain + // Dictionary) — clone its Fields dict via a recursive cloneCore + // call (reuses the Dictionary branch above), then rebuild (#1255). + // Getters/setters are not preserved — matches the interpreter's CloneObject, which + // only copies data fields. + coreIl.MarkLabel(checkObjectLiteral); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Isinst, runtime.TSObjectType); + coreIl.Emit(OpCodes.Stloc, objLiteralSourceLocal); + coreIl.Emit(OpCodes.Ldloc, objLiteralSourceLocal); + coreIl.Emit(OpCodes.Brfalse, checkObjectDict); + + coreIl.Emit(OpCodes.Ldloc, objLiteralSourceLocal); + coreIl.Emit(OpCodes.Callvirt, runtime.TSObjectFieldsGetter); + coreIl.Emit(OpCodes.Call, cloneCore); + coreIl.Emit(OpCodes.Castclass, _types.DictionaryStringObject); + coreIl.Emit(OpCodes.Stloc, objLiteralClonedFieldsLocal); + coreIl.Emit(OpCodes.Ldloc, objLiteralClonedFieldsLocal); + coreIl.Emit(OpCodes.Newobj, runtime.TSObjectCtor); + coreIl.Emit(OpCodes.Ret); + // Dictionary deep clone (Map backing store). coreIl.MarkLabel(checkObjectDict); coreIl.Emit(OpCodes.Ldarg_0); @@ -1643,7 +1784,7 @@ private void EmitStructuredCloneHelper(TypeBuilder runtimeType, EmittedRuntime r coreIl.Emit(OpCodes.Isinst, _types.HashSetOfObject); coreIl.Emit(OpCodes.Stloc, sourceSetLocal); coreIl.Emit(OpCodes.Ldloc, sourceSetLocal); - coreIl.Emit(OpCodes.Brfalse, fallbackReturn); + coreIl.Emit(OpCodes.Brfalse, checkDate); coreIl.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(_types.HashSetOfObject)); coreIl.Emit(OpCodes.Stloc, clonedSetLocal); @@ -1669,10 +1810,218 @@ private void EmitStructuredCloneHelper(TypeBuilder runtimeType, EmittedRuntime r coreIl.Emit(OpCodes.Brtrue, setLoopBody); coreIl.Emit(OpCodes.Br, returnClonedSet); + // $TSDate — deep clone via epoch milliseconds (#1255). + coreIl.MarkLabel(checkDate); + if (_features.UsesDate) + { + var dateLocal = coreIl.DeclareLocal(runtime.TSDateType); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Isinst, runtime.TSDateType); + coreIl.Emit(OpCodes.Stloc, dateLocal); + coreIl.Emit(OpCodes.Ldloc, dateLocal); + coreIl.Emit(OpCodes.Brfalse, checkRegExp); + + coreIl.Emit(OpCodes.Ldloc, dateLocal); + coreIl.Emit(OpCodes.Call, _tsDateGetTimeMethod); + coreIl.Emit(OpCodes.Newobj, runtime.TSDateCtorMilliseconds); + coreIl.Emit(OpCodes.Ret); + } + else + { + coreIl.Emit(OpCodes.Br, checkRegExp); + } + + // $RegExp — clone pattern + flags (#1255). Matches the interpreter's CloneInternal, + // which likewise rebuilds from Source/Flags only (lastIndex is not preserved). + coreIl.MarkLabel(checkRegExp); + if (_features.UsesRegExp) + { + var regexpLocal = coreIl.DeclareLocal(runtime.TSRegExpType); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Isinst, runtime.TSRegExpType); + coreIl.Emit(OpCodes.Stloc, regexpLocal); + coreIl.Emit(OpCodes.Ldloc, regexpLocal); + coreIl.Emit(OpCodes.Brfalse, checkBuffer); + + coreIl.Emit(OpCodes.Ldloc, regexpLocal); + coreIl.Emit(OpCodes.Callvirt, runtime.TSRegExpSourceGetter); + coreIl.Emit(OpCodes.Ldloc, regexpLocal); + coreIl.Emit(OpCodes.Callvirt, runtime.TSRegExpFlagsGetter); + coreIl.Emit(OpCodes.Newobj, runtime.TSRegExpCtorPatternFlags); + coreIl.Emit(OpCodes.Ret); + } + else + { + coreIl.Emit(OpCodes.Br, checkBuffer); + } + + // $Buffer — deep-copy bytes via the existing Buffer.from(buffer) factory (#1255). + coreIl.MarkLabel(checkBuffer); + if (_features.UsesBuffer) + { + var bufferLocal = coreIl.DeclareLocal(runtime.TSBufferType); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Isinst, runtime.TSBufferType); + coreIl.Emit(OpCodes.Stloc, bufferLocal); + coreIl.Emit(OpCodes.Ldloc, bufferLocal); + coreIl.Emit(OpCodes.Brfalse, checkError); + + coreIl.Emit(OpCodes.Ldloc, bufferLocal); + coreIl.Emit(OpCodes.Call, runtime.TSBufferFromBuffer); + coreIl.Emit(OpCodes.Ret); + } + else + { + coreIl.Emit(OpCodes.Br, checkError); + } + + // $Error hierarchy — clone via name-based reconstruction + Stack preservation + // (#1255), mirroring the interpreter's CloneError. A user-defined class extending + // Error (`class Foo extends Error`) compiles with $Error as its real CLR base + // (ILCompiler.Classes.cs) but — like every user class — ALSO implements + // $IHasFields, which built-in $Error/$TypeError/etc. instances never do. Excluding + // that case here routes it to the generic uncloneable fallback below, matching the + // interpreter's narrower CloneError (only the fixed Error/TypeError/RangeError/... + // hierarchy — a SharpTSInstance user subclass takes a different, field-copying path + // the compiled side does not replicate). + coreIl.MarkLabel(checkError); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Isinst, runtime.TSErrorType); + coreIl.Emit(OpCodes.Stloc, errSourceLocal); + coreIl.Emit(OpCodes.Ldloc, errSourceLocal); + coreIl.Emit(OpCodes.Brfalse, fallbackReturn); + + coreIl.Emit(OpCodes.Ldloc, errSourceLocal); + coreIl.Emit(OpCodes.Isinst, runtime.IHasFieldsInterface); + coreIl.Emit(OpCodes.Brtrue, fallbackReturn); + + coreIl.Emit(OpCodes.Ldloc, errSourceLocal); + coreIl.Emit(OpCodes.Callvirt, runtime.TSErrorNameGetter); + coreIl.Emit(OpCodes.Stloc, errNameLocal); + coreIl.Emit(OpCodes.Ldloc, errSourceLocal); + coreIl.Emit(OpCodes.Callvirt, runtime.TSErrorMessageGetter); + coreIl.Emit(OpCodes.Stloc, errMsgLocal); + + var strEquals = _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String); + var isTypeErrorLabel = coreIl.DefineLabel(); + var isRangeErrorLabel = coreIl.DefineLabel(); + var isReferenceErrorLabel = coreIl.DefineLabel(); + var isSyntaxErrorLabel = coreIl.DefineLabel(); + var isURIErrorLabel = coreIl.DefineLabel(); + var isEvalErrorLabel = coreIl.DefineLabel(); + var defaultErrorLabel = coreIl.DefineLabel(); + var haveClonedErrLabel = coreIl.DefineLabel(); + + coreIl.Emit(OpCodes.Ldloc, errNameLocal); + coreIl.Emit(OpCodes.Ldstr, "TypeError"); + coreIl.Emit(OpCodes.Call, strEquals); + coreIl.Emit(OpCodes.Brtrue, isTypeErrorLabel); + coreIl.Emit(OpCodes.Ldloc, errNameLocal); + coreIl.Emit(OpCodes.Ldstr, "RangeError"); + coreIl.Emit(OpCodes.Call, strEquals); + coreIl.Emit(OpCodes.Brtrue, isRangeErrorLabel); + coreIl.Emit(OpCodes.Ldloc, errNameLocal); + coreIl.Emit(OpCodes.Ldstr, "ReferenceError"); + coreIl.Emit(OpCodes.Call, strEquals); + coreIl.Emit(OpCodes.Brtrue, isReferenceErrorLabel); + coreIl.Emit(OpCodes.Ldloc, errNameLocal); + coreIl.Emit(OpCodes.Ldstr, "SyntaxError"); + coreIl.Emit(OpCodes.Call, strEquals); + coreIl.Emit(OpCodes.Brtrue, isSyntaxErrorLabel); + coreIl.Emit(OpCodes.Ldloc, errNameLocal); + coreIl.Emit(OpCodes.Ldstr, "URIError"); + coreIl.Emit(OpCodes.Call, strEquals); + coreIl.Emit(OpCodes.Brtrue, isURIErrorLabel); + coreIl.Emit(OpCodes.Ldloc, errNameLocal); + coreIl.Emit(OpCodes.Ldstr, "EvalError"); + coreIl.Emit(OpCodes.Call, strEquals); + coreIl.Emit(OpCodes.Brtrue, isEvalErrorLabel); + coreIl.Emit(OpCodes.Br, defaultErrorLabel); + + coreIl.MarkLabel(isTypeErrorLabel); + coreIl.Emit(OpCodes.Ldloc, errMsgLocal); + coreIl.Emit(OpCodes.Newobj, runtime.TSTypeErrorCtor); + coreIl.Emit(OpCodes.Stloc, clonedErrLocal); + coreIl.Emit(OpCodes.Br, haveClonedErrLabel); + + coreIl.MarkLabel(isRangeErrorLabel); + coreIl.Emit(OpCodes.Ldloc, errMsgLocal); + coreIl.Emit(OpCodes.Newobj, runtime.TSRangeErrorCtor); + coreIl.Emit(OpCodes.Stloc, clonedErrLocal); + coreIl.Emit(OpCodes.Br, haveClonedErrLabel); + + coreIl.MarkLabel(isReferenceErrorLabel); + coreIl.Emit(OpCodes.Ldloc, errMsgLocal); + coreIl.Emit(OpCodes.Newobj, runtime.TSReferenceErrorCtor); + coreIl.Emit(OpCodes.Stloc, clonedErrLocal); + coreIl.Emit(OpCodes.Br, haveClonedErrLabel); + + coreIl.MarkLabel(isSyntaxErrorLabel); + coreIl.Emit(OpCodes.Ldloc, errMsgLocal); + coreIl.Emit(OpCodes.Newobj, runtime.TSSyntaxErrorCtor); + coreIl.Emit(OpCodes.Stloc, clonedErrLocal); + coreIl.Emit(OpCodes.Br, haveClonedErrLabel); + + coreIl.MarkLabel(isURIErrorLabel); + coreIl.Emit(OpCodes.Ldloc, errMsgLocal); + coreIl.Emit(OpCodes.Newobj, runtime.TSURIErrorCtor); + coreIl.Emit(OpCodes.Stloc, clonedErrLocal); + coreIl.Emit(OpCodes.Br, haveClonedErrLabel); + + coreIl.MarkLabel(isEvalErrorLabel); + coreIl.Emit(OpCodes.Ldloc, errMsgLocal); + coreIl.Emit(OpCodes.Newobj, runtime.TSEvalErrorCtor); + coreIl.Emit(OpCodes.Stloc, clonedErrLocal); + coreIl.Emit(OpCodes.Br, haveClonedErrLabel); + + coreIl.MarkLabel(defaultErrorLabel); + coreIl.Emit(OpCodes.Ldloc, errMsgLocal); + coreIl.Emit(OpCodes.Newobj, runtime.TSErrorCtorMessage); + coreIl.Emit(OpCodes.Stloc, clonedErrLocal); + + coreIl.MarkLabel(haveClonedErrLabel); + coreIl.Emit(OpCodes.Ldloc, clonedErrLocal); + coreIl.Emit(OpCodes.Ldloc, errSourceLocal); + coreIl.Emit(OpCodes.Callvirt, runtime.TSErrorStackGetter); + coreIl.Emit(OpCodes.Callvirt, runtime.TSErrorStackSetter); + coreIl.Emit(OpCodes.Ldloc, clonedErrLocal); + coreIl.Emit(OpCodes.Ret); + + // Everything else — functions/closures, Symbols, class instances, Promises, + // WeakMap/WeakSet, iterators/generators, EventEmitters, etc. — cannot be + // structured-cloned (#1255). Throw rather than alias by reference, matching the + // interpreter's exhaustive switch (default arm: "Cannot clone value of type X"). coreIl.MarkLabel(fallbackReturn); + + // A value whose CLR type is NOT defined in this dynamically-emitted assembly is + // foreign — most commonly a SharpTS.dll interpreter runtime value (SharpTSObject/ + // SharpTSArray/etc.) crossing in via CompiledMessagePortBridge, which already + // structured-clones on the interpreter side before handing off to this (compiled) + // port's PostMessage (see CompiledMessagePortBridge.PostMessage's "pass-through + // no-op for interpreter-shaped values" comment). Pass it through unchanged rather + // than throwing — only OUR OWN uncloneable constructs ($TSFunction, $TSSymbol, + // class instances, $Promise, etc., all defined in this assembly) should throw. + var sameAssemblyLabel = coreIl.DefineLabel(); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "GetType")); + coreIl.Emit(OpCodes.Callvirt, _types.GetProperty(_types.Type, "Assembly").GetGetMethod()!); + coreIl.Emit(OpCodes.Ldtoken, runtimeType); + coreIl.Emit(OpCodes.Call, _types.TypeGetTypeFromHandle); + coreIl.Emit(OpCodes.Callvirt, _types.GetProperty(_types.Type, "Assembly").GetGetMethod()!); + coreIl.Emit(OpCodes.Ceq); + coreIl.Emit(OpCodes.Brtrue, sameAssemblyLabel); coreIl.Emit(OpCodes.Ldarg_0); coreIl.Emit(OpCodes.Ret); + coreIl.MarkLabel(sameAssemblyLabel); + coreIl.Emit(OpCodes.Ldstr, "Cannot clone value of type "); + coreIl.Emit(OpCodes.Ldarg_0); + coreIl.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "GetType")); + coreIl.Emit(OpCodes.Callvirt, _types.GetProperty(_types.Type, "Name").GetGetMethod()!); + coreIl.Emit(OpCodes.Call, _types.StringConcat2); + coreIl.Emit(OpCodes.Newobj, runtime.TSDataCloneErrorCtor); + coreIl.Emit(OpCodes.Throw); + coreIl.MarkLabel(returnClonedList); coreIl.Emit(OpCodes.Ldloc, clonedListLocal); coreIl.Emit(OpCodes.Ret); diff --git a/Compilation/RuntimeEmitter.cs b/Compilation/RuntimeEmitter.cs index fdd0785a..e277f0ad 100644 --- a/Compilation/RuntimeEmitter.cs +++ b/Compilation/RuntimeEmitter.cs @@ -102,6 +102,11 @@ public EmittedRuntime EmitAll(ModuleBuilder moduleBuilder, RuntimeFeatureSet fea // NOTE: Must stay in sync with SharpTS.Runtime.Types.SharpTSError and subclasses EmitTSErrorClasses(moduleBuilder, runtime); + // Emit $DataCloneError exception type — thrown by StructuredCloneCore (#1255). + // Unconditional: StructuredCloneCore itself is always emitted (EmitWorkerHelpers + // runs unconditionally inside EmitRuntimeClass below). + EmitTSDataCloneErrorType(moduleBuilder, runtime); + // Emit $Promise class for standalone Promise support // NOTE: Must stay in sync with SharpTS.Runtime.Types.SharpTSPromise EmitTSPromiseClass(moduleBuilder, runtime); diff --git a/Runtime/Types/SharpTSBroadcastChannel.cs b/Runtime/Types/SharpTSBroadcastChannel.cs index 4af9a37c..e85533cc 100644 --- a/Runtime/Types/SharpTSBroadcastChannel.cs +++ b/Runtime/Types/SharpTSBroadcastChannel.cs @@ -212,6 +212,12 @@ private void EnqueueAndSchedule(object? cloned) /// /// Enqueues a messageerror notification and schedules a drain on the owning loop. /// + /// + /// Mirrors : the scheduled closure does NOT re-check + /// _closed — an already-enqueued notification still fires even if the receiver + /// closes before the timer runs, matching Node's "in-flight deliveries complete after + /// Close()" semantics (#1255). Only the initial (enqueue-time) guard checks _closed. + /// private void EnqueueMessageError() { if (_closed || OwnerInterpreter == null) @@ -219,9 +225,17 @@ private void EnqueueMessageError() OwnerInterpreter.ScheduleTimer(0, 0, () => { - if (_closed || OwnerInterpreter == null) + if (OwnerInterpreter == null) return; EmitEvent(OwnerInterpreter, "messageerror", []); + + // Also invoke the property-style onmessageerror handler if set (WHATWG spec), + // mirroring DrainPendingMessages' handling of onmessage (#1255). + if (_onMessageErrorHandler is ISharpTSCallable callable) + { + try { callable.Call(OwnerInterpreter, []); } + catch { /* listeners may not throw out of delivery */ } + } }, false); } diff --git a/SharpTS.Tests/SharedTests/BroadcastChannelTests.cs b/SharpTS.Tests/SharedTests/BroadcastChannelTests.cs index dcd4cc56..da2caf63 100644 --- a/SharpTS.Tests/SharedTests/BroadcastChannelTests.cs +++ b/SharpTS.Tests/SharedTests/BroadcastChannelTests.cs @@ -194,6 +194,36 @@ public void BroadcastChannel_StructuredCloneMutationIsolation(ExecutionMode mode Assert.Equal("1\n2\n", output); } + /// + /// #1255: posting an uncloneable value (a function) fires 'messageerror' on the + /// receiver — both via on('messageerror', ...) and the property-style + /// onmessageerror setter — instead of throwing on the sender (WHATWG receiver-side + /// model, matching SharpTSBroadcastChannel.PostMessage's catch). Compiled mode had + /// no clone-failure handling at all before this fix (cloneCore aliased uncloneable values + /// by reference, so the function passed straight through as 'message'); the interpreter's + /// onmessageerror property handler was itself never invoked (only the EventEmitter + /// 'messageerror' event fired) — both are fixed together here. + /// + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void BroadcastChannel_PostUncloneable_FiresMessageError(ExecutionMode mode) + { + var source = @" + const a = new BroadcastChannel('msgerr'); + const b = new BroadcastChannel('msgerr'); + b.on('messageerror', () => console.log('on-err')); + b.onmessageerror = () => console.log('prop-err'); + b.on('message', () => console.log('should-not-fire')); + a.postMessage(() => {}); + a.close(); + b.close(); + "; + var output = TestHarness.Run(source, mode); + Assert.Contains("on-err\n", output); + Assert.Contains("prop-err\n", output); + Assert.DoesNotContain("should-not-fire", output); + } + [Theory] [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] public void BroadcastChannel_WorksInsideAsyncFunction(ExecutionMode mode) diff --git a/SharpTS.Tests/SharedTests/WorkerThreadsTests.cs b/SharpTS.Tests/SharedTests/WorkerThreadsTests.cs index b733b958..c625cc4c 100644 --- a/SharpTS.Tests/SharedTests/WorkerThreadsTests.cs +++ b/SharpTS.Tests/SharedTests/WorkerThreadsTests.cs @@ -458,6 +458,159 @@ public void StructuredClone_ClonesSet(ExecutionMode mode) Assert.Equal("3\n4\n", output); } + /// + /// #1255: structuredClone must deep-clone Date/RegExp/TypedArray/Buffer/Error — + /// mutating (or, for RegExp, just reading the identity of) the source afterward must not + /// affect the clone. Before the fix, compiled mode's $Runtime.StructuredClone + /// aliased all of these by reference (only List/Dictionary/Set were deep-cloned). + /// + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void StructuredClone_ClonesDateIndependently(ExecutionMode mode) + { + var source = @" + let d = new Date(1000); + let cloned: any = structuredClone(d); + d.setTime(2000); + console.log(cloned.getTime()); + "; + var output = TestHarness.Run(source, mode); + Assert.Equal("1000\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void StructuredClone_ClonesRegExpSourceAndFlags(ExecutionMode mode) + { + var source = @" + let r = /abc/gi; + let cloned: any = structuredClone(r); + console.log(cloned.source); + console.log(cloned.flags); + console.log(cloned !== r); + "; + var output = TestHarness.Run(source, mode); + Assert.Equal("abc\ngi\ntrue\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void StructuredClone_ClonesTypedArrayIndependently(ExecutionMode mode) + { + var source = @" + let a = new Int32Array([5, 6, 7]); + let cloned: any = structuredClone(a); + a[0] = 99; + console.log(cloned[0]); + "; + var output = TestHarness.Run(source, mode); + Assert.Equal("5\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void StructuredClone_ClonesBufferIndependently(ExecutionMode mode) + { + var source = @" + let b = Buffer.from([1, 2, 3]); + let cloned: any = structuredClone(b); + b[0] = 99; + console.log(cloned[0]); + "; + var output = TestHarness.Run(source, mode); + Assert.Equal("1\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void StructuredClone_ClonesErrorPreservingNameAndMessage(ExecutionMode mode) + { + var source = @" + let e = new TypeError('bad'); + let cloned: any = structuredClone(e); + console.log(cloned.name); + console.log(cloned.message); + console.log(cloned !== e); + "; + var output = TestHarness.Run(source, mode); + Assert.Equal("TypeError\nbad\ntrue\n", output); + } + + /// + /// #1255: uncloneable values — functions, symbols, and class instances — must throw + /// (spec: DataCloneError), both at the top level and nested inside a plain object. + /// Before the fix, compiled mode's shallow fallback aliased these by reference instead + /// of throwing. + /// + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void StructuredClone_ThrowsForFunction(ExecutionMode mode) + { + // Dual-mode parity check: a non-guest-throw exception's catch value is the raw + // message string in both modes (interpreter: `ex.Message` for a non-ThrowException; + // compiled: WrapException's $DataCloneError branch), not an Error instance. + var source = @" + try { + structuredClone(() => {}); + console.log('no-throw'); + } catch (e) { + console.log('threw:' + typeof e); + console.log((e as string).includes('DataCloneError')); + } + "; + var output = TestHarness.Run(source, mode); + Assert.Equal("threw:string\ntrue\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void StructuredClone_ThrowsForNestedFunction(ExecutionMode mode) + { + var source = @" + try { + structuredClone({ fn: () => {} }); + console.log('no-throw'); + } catch { + console.log('threw'); + } + "; + var output = TestHarness.Run(source, mode); + Assert.Equal("threw\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void StructuredClone_ThrowsForSymbol(ExecutionMode mode) + { + var source = @" + try { + structuredClone(Symbol('x')); + console.log('no-throw'); + } catch { + console.log('threw'); + } + "; + var output = TestHarness.Run(source, mode); + Assert.Equal("threw\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void StructuredClone_ThrowsForClassInstance(ExecutionMode mode) + { + var source = @" + class Foo { x = 1; } + try { + structuredClone(new Foo()); + console.log('no-throw'); + } catch { + console.log('threw'); + } + "; + var output = TestHarness.Run(source, mode); + Assert.Equal("threw\n", output); + } + #endregion #region Worker.terminate() event-loop liveness (#324) @@ -1175,11 +1328,11 @@ public void Worker_ParentPostsUncloneable_WorkerParentPortFiresMessageError(Exec /// /// #1001/#1077: a MessageChannel port whose peer posts an uncloneable value (a /// function) fires 'messageerror' — not 'message' — on the receiver. Now - /// dual-mode: the compiled $MessagePort.postMessage detects the uncloneable value - /// (typeof "function"/"symbol") up front and enqueues a clone-failure sentinel that - /// Drain converts to 'messageerror', matching the interpreter's - /// receiver-side model (previously compiled returned the function by reference and fired - /// 'message'). + /// dual-mode: the compiled $Runtime.StructuredClone throws $DataCloneError + /// for the uncloneable value (#1255), which the compiled $MessagePort.PostMessage + /// catches and converts to the shared clone-failure sentinel that Drain turns into + /// 'messageerror', matching the interpreter's receiver-side model (previously + /// compiled returned the function by reference and fired 'message'). /// [Theory] [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] @@ -1204,6 +1357,33 @@ public void MessageChannelPort_PostUncloneable_FiresMessageError(ExecutionMode m Assert.DoesNotContain("port-msg", output); } + /// + /// #1255: an uncloneable value NESTED inside an object (not just at the top level) + /// must also fire 'messageerror'. Before the fix, the emitted $Runtime. + /// StructuredClone only detected the top-level typeof and otherwise aliased + /// unknown values by reference, so a nested function silently passed through as + /// 'message' in compiled mode. + /// + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void MessageChannelPort_PostNestedUncloneable_FiresMessageError(ExecutionMode mode) + { + var files = new Dictionary + { + ["main.ts"] = """ + import { MessageChannel } from "worker_threads"; + const { port1, port2 } = new MessageChannel(); + port2.on("messageerror", () => { console.log("port-err"); port1.close(); port2.close(); }); + port2.on("message", () => { console.log("port-msg"); port1.close(); port2.close(); }); + port1.postMessage({ fn: () => {} }); + """ + }; + + var output = TestHarness.RunModules(files, "main.ts", mode); + Assert.Contains("port-err", output); + Assert.DoesNotContain("port-msg", output); + } + #endregion #region Running-Worker event-loop liveness (#329)