Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Compilation/EmittedRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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!;
Expand Down
69 changes: 65 additions & 4 deletions Compilation/RuntimeEmitter.BroadcastChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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!;
Expand Down Expand Up @@ -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,
Expand All @@ -92,6 +100,9 @@ private void EmitBroadcastChannelClass(ModuleBuilder moduleBuilder, EmittedRunti
var bcRegistryCtor = _bcRegistryDictType.GetConstructor([typeof(IEqualityComparer<string>)])!;
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);
}

Expand Down Expand Up @@ -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<string, object>()
il.Emit(OpCodes.Newobj, _types.DictionaryStringObject.GetConstructor(Type.EmptyTypes)!);
il.Emit(OpCodes.Stloc, eventDataLocal);
Expand Down Expand Up @@ -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);
Expand Down
43 changes: 43 additions & 0 deletions Compilation/RuntimeEmitter.DataCloneError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Reflection;
using System.Reflection.Emit;

namespace SharpTS.Compilation;

/// <summary>
/// Emits the <c>$DataCloneError</c> exception type: a standalone (pure-IL, no
/// SharpTS.dll dependency) marker exception thrown by the emitted structured-clone
/// core (<see cref="RuntimeEmitter.EmitStructuredCloneHelper"/>) 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
/// <see cref="SharpTS.Runtime.Types.StructuredClone.DataCloneError"/> (#1255).
/// </summary>
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();
}
}
41 changes: 10 additions & 31 deletions Compilation/RuntimeEmitter.MessageChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);

Expand Down
16 changes: 15 additions & 1 deletion Compilation/RuntimeEmitter.Objects.Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,32 @@ 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);
il.Emit(OpCodes.Castclass, runtime.TSPromiseRejectedExceptionType);
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();
Expand Down
Loading
Loading