From 61edaf0bb27d5c28518fd7beea97a400765a91b2 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sat, 4 Jul 2026 20:24:16 -0700 Subject: [PATCH] fix(compilation): deep-clone Date/RegExp/TypedArray/Buffer/Error in compiled structuredClone and throw DataCloneError on uncloneable values (#1255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compiled StructuredCloneCore previously only deep-cloned List/Dictionary/ HashSet and aliased everything else by reference (Date, RegExp, TypedArray, ArrayBuffer, Buffer, Error) or silently passed through uncloneable values (functions, symbols, class instances, Promises) instead of throwing, unlike the interpreter's StructuredClone.Clone. Extends cloneCore to deep-clone the former and throw a new standalone $DataCloneError for the latter — at any nesting depth, not just top-level — flipping the previous alias-fallback to throw by default so unrecognized constructs fail closed like the interpreter's exhaustive switch. $MessagePort/$BroadcastChannel now catch $DataCloneError around their clone call and convert it to the existing receiver-side 'messageerror' sentinel instead of relying on a narrower top-level typeof pre-check. WrapException recognizes $DataCloneError so a caught value is the raw message string, matching the interpreter's non- guest-throw catch binding. Also fixes two bugs found along the way: a value whose CLR type lives outside the emitted assembly (interpreter runtime values crossing via CompiledMessagePortBridge) must pass through unchanged rather than throw, and SharpTSBroadcastChannel's onmessageerror property handler was never invoked (only the EventEmitter event fired) and dropped delivery if the receiver closed before the scheduled notification ran. --- Compilation/EmittedRuntime.cs | 8 + .../RuntimeEmitter.BroadcastChannel.cs | 69 +++- Compilation/RuntimeEmitter.DataCloneError.cs | 43 +++ Compilation/RuntimeEmitter.MessageChannel.cs | 41 +- .../RuntimeEmitter.Objects.Exceptions.cs | 16 +- Compilation/RuntimeEmitter.Worker.cs | 355 +++++++++++++++++- Compilation/RuntimeEmitter.cs | 5 + Runtime/Types/SharpTSBroadcastChannel.cs | 16 +- .../SharedTests/BroadcastChannelTests.cs | 30 ++ .../SharedTests/WorkerThreadsTests.cs | 190 +++++++++- 10 files changed, 728 insertions(+), 45 deletions(-) create mode 100644 Compilation/RuntimeEmitter.DataCloneError.cs 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)