diff --git a/docs/plans/archive/issue-1956-resolution.md b/docs/plans/archive/issue-1956-resolution.md new file mode 100644 index 000000000..872303c55 --- /dev/null +++ b/docs/plans/archive/issue-1956-resolution.md @@ -0,0 +1,34 @@ +# Issue #1956: escaped numeric closure verification + +Verified on Windows ARM64, .NET SDK 10.0.401/runtime 10.0.12, Release, on +2026-10-04. The existing `EscapedNumericClosureKeepsLiveUpdates` source and its +`13 14\ntrue Infinity\n` expectation remain enabled and unchanged. + +An independently restored/built archive of reported baseline +`78931b4add5a6937ab6e63452f2e0ad97fe12adb` reproduces the test-host access violation +in `CastHelpers.IsInstanceOfClass`, called from `$Program.create(Double)`. +Compiling that same source with the baseline CLI and `--standalone --verify` +reports: + +```text +[IL Error] $Program.create: StackObjRef {Offset=98, Found=Double} - Expected an ObjRef on the stack. +``` + +The emitted method loads the captured `Double current` field at IL offset +`0x005C`, then applies `isinst $LexicalUninitialized` at `0x0062` without boxing. +The verifier therefore identifies an invalid object-reference operation, +independently of the execution exception. + +An independently restored/built archive of current main +`987a8b58d5fe32ddac4d8edd25384c923665e301` passes both original API modes, and its +standalone assembly passes IL verification. Its generated method omits that +sentinel check on the promoted numeric slot. The responsible existing repair is +`1331f8f06` (PR #1969), in `ILEmitter.EmitLexicalAssignmentTdzGuard`: value-type +capture fields cannot hold the object-valued TDZ sentinel, so the guard returns +before loading and inspecting them. + +This consolidation adds deployment coverage for the original source and live +update/TDZ controls, including both declaration paths, verified standalone +execution and actual hosted initialization. It credits the existing product +repair; it does not claim a new compiler repair or infer behavior on other +architectures from the local exception. diff --git a/src/SharpTS/Compilation/Emitters/GlobalThisStaticEmitter.cs b/src/SharpTS/Compilation/Emitters/GlobalThisStaticEmitter.cs index 60d1b8997..a685ec960 100644 --- a/src/SharpTS/Compilation/Emitters/GlobalThisStaticEmitter.cs +++ b/src/SharpTS/Compilation/Emitters/GlobalThisStaticEmitter.cs @@ -85,6 +85,13 @@ public bool TryEmitStaticPropertyGet(IEmitterContext emitter, string propertyNam if (TryEmitBuiltInClassType(il, ctx, propertyName)) return true; + if (propertyName == "Math") + { + il.Emit(OpCodes.Ldstr, propertyName); + il.Emit(OpCodes.Call, ctx.Runtime!.GlobalObject.GetProperty); + return true; + } + // Check if this is a known built-in that has its own static emitter var staticEmitter = _registry.GetStaticStrategy(propertyName); if (staticEmitter != null) diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.Operators.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.Operators.cs index 4fe00d08c..8f15da458 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.Operators.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.Operators.cs @@ -10,6 +10,19 @@ namespace SharpTS.Compilation; /// public abstract partial class ExpressionEmitterBase { + // PutValue uses the strictness of the expression's enclosing code, including + // state-machine bodies. Call only after the read and any short-circuit check. + protected void EmitPropertyWrite(bool indexed) + { + if (Ctx.IsStrictMode) + { + IL.Emit(OpCodes.Ldc_I4_1); + IL.Emit(OpCodes.Call, indexed ? Ctx.Runtime!.ObjectWrite.IndexStrict : Ctx.Runtime!.ObjectWrite.PropertyStrict); + } + else + IL.Emit(OpCodes.Call, indexed ? Ctx.Runtime!.ObjectWrite.Index : Ctx.Runtime!.ObjectWrite.Property); + } + /// /// Emits the logical condition check for &&=, ||=, ??= operators. /// After this, the stack has the current value and control flows to either @@ -253,7 +266,7 @@ protected virtual void EmitPrefixIncrement(Expr.PrefixIncrement pi) EmitMemberAccessIncrement( isPrefix: true, pi.Operator.Type, objLocal, emitKey: () => IL.Emit(OpCodes.Ldstr, get.Name.Lexeme), - Ctx.Runtime!.ObjectRead.Property, Ctx.Runtime!.ObjectWrite.Property); + Ctx.Runtime!.ObjectRead.Property, indexed: false); return; } @@ -265,7 +278,7 @@ protected virtual void EmitPrefixIncrement(Expr.PrefixIncrement pi) EmitMemberAccessIncrement( isPrefix: true, pi.Operator.Type, objLocal, emitKey: () => IL.Emit(OpCodes.Ldloc, indexLocal), - Ctx.Runtime!.ObjectRead.Index, Ctx.Runtime!.ObjectWrite.Index); + Ctx.Runtime!.ObjectRead.Index, indexed: true); return; } @@ -314,7 +327,7 @@ protected virtual void EmitPostfixIncrement(Expr.PostfixIncrement poi) EmitMemberAccessIncrement( isPrefix: false, poi.Operator.Type, objLocal, emitKey: () => IL.Emit(OpCodes.Ldstr, get.Name.Lexeme), - Ctx.Runtime!.ObjectRead.Property, Ctx.Runtime!.ObjectWrite.Property); + Ctx.Runtime!.ObjectRead.Property, indexed: false); return; } @@ -326,7 +339,7 @@ protected virtual void EmitPostfixIncrement(Expr.PostfixIncrement poi) EmitMemberAccessIncrement( isPrefix: false, poi.Operator.Type, objLocal, emitKey: () => IL.Emit(OpCodes.Ldloc, indexLocal), - Ctx.Runtime!.ObjectRead.Index, Ctx.Runtime!.ObjectWrite.Index); + Ctx.Runtime!.ObjectRead.Index, indexed: true); return; } @@ -400,7 +413,7 @@ private bool TryEmitNativeNumericLocalIncrement( /// private void EmitMemberAccessIncrement( bool isPrefix, TokenType op, LocalBuilder objLocal, Action emitKey, - MethodBuilder getMethod, MethodBuilder setMethod) + MethodBuilder getMethod, bool indexed) { double delta = op == TokenType.PLUS_PLUS ? 1.0 : -1.0; @@ -418,7 +431,7 @@ private void EmitMemberAccessIncrement( IL.Emit(OpCodes.Ldloc, objLocal); emitKey(); IL.Emit(OpCodes.Ldloc, newValue); - IL.Emit(OpCodes.Call, setMethod); + EmitPropertyWrite(indexed); IL.Emit(OpCodes.Ldloc, resultValue); SetStackUnknown(); @@ -503,7 +516,7 @@ private bool TryEmitStaticFieldIncrement(Expr.Get get, bool isPrefix, TokenType IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); IL.Emit(OpCodes.Ldstr, get.Name.Lexeme); IL.Emit(OpCodes.Ldloc, newValue); - IL.Emit(OpCodes.Call, Ctx.Runtime!.ObjectWrite.Property); + EmitPropertyWrite(indexed: false); IL.Emit(OpCodes.Ldloc, resultValue); SetStackUnknown(); @@ -543,7 +556,7 @@ protected virtual void EmitLogicalSet(Expr.LogicalSet ls) IL.Emit(OpCodes.Ldloc, objLocal); IL.Emit(OpCodes.Ldstr, ls.Name.Lexeme); IL.Emit(OpCodes.Ldloc, resultLocal); - IL.Emit(OpCodes.Call, Ctx.Runtime!.ObjectWrite.Property); + EmitPropertyWrite(indexed: false); IL.Emit(OpCodes.Ldloc, resultLocal); IL.Emit(OpCodes.Br, endLabel); @@ -583,7 +596,7 @@ protected virtual void EmitLogicalSetIndex(Expr.LogicalSetIndex lsi) IL.Emit(OpCodes.Ldloc, objLocal); IL.Emit(OpCodes.Ldloc, indexLocal); IL.Emit(OpCodes.Ldloc, resultLocal); - IL.Emit(OpCodes.Call, Ctx.Runtime!.ObjectWrite.Index); + EmitPropertyWrite(indexed: true); IL.Emit(OpCodes.Ldloc, resultLocal); IL.Emit(OpCodes.Br, endLabel); @@ -641,7 +654,7 @@ protected virtual void EmitCompoundSet(Expr.CompoundSet cs) IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); IL.Emit(OpCodes.Ldstr, cs.Name.Lexeme); IL.Emit(OpCodes.Ldloc, inheritedResultTemp); - IL.Emit(OpCodes.Call, Ctx.Runtime!.ObjectWrite.Property); + EmitPropertyWrite(indexed: false); IL.Emit(OpCodes.Ldloc, inheritedResultTemp); SetStackUnknown(); @@ -677,7 +690,7 @@ protected virtual void EmitCompoundSet(Expr.CompoundSet cs) IL.Emit(OpCodes.Ldloc, objTemp); IL.Emit(OpCodes.Ldstr, cs.Name.Lexeme); IL.Emit(OpCodes.Ldloc, resultLocal); - IL.Emit(OpCodes.Call, Ctx.Runtime!.ObjectWrite.Property); + EmitPropertyWrite(indexed: false); IL.Emit(OpCodes.Ldloc, resultLocal); SetStackUnknown(); @@ -717,7 +730,7 @@ protected virtual void EmitCompoundSetIndex(Expr.CompoundSetIndex csi) IL.Emit(OpCodes.Ldloc, objTemp); IL.Emit(OpCodes.Ldloc, indexTemp); IL.Emit(OpCodes.Ldloc, resultLocal); - IL.Emit(OpCodes.Call, Ctx.Runtime!.ObjectWrite.Index); + EmitPropertyWrite(indexed: true); IL.Emit(OpCodes.Ldloc, resultLocal); SetStackUnknown(); diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.cs index 4954d0490..40001008e 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.cs @@ -369,8 +369,7 @@ public virtual void EmitExpression(Expr expr) /// /// Emits a literal value. Default uses helper methods (EmitNullConstant, EmitDoubleConstant, etc.). - /// ILEmitter overrides for BigInteger/SharpTSUndefined support. - /// AsyncArrowMoveNextEmitter overrides for eager boxing. + /// BigInteger literals are boxed consistently in synchronous and state-machine bodies. /// protected virtual void EmitLiteral(Expr.Literal lit) { @@ -380,6 +379,7 @@ protected virtual void EmitLiteral(Expr.Literal lit) case double d: EmitDoubleConstant(d); break; case bool b: EmitBoolConstant(b); break; case string s: EmitStringConstant(s); break; + case System.Numerics.BigInteger integer: EmitBigIntConstant(integer); break; // The `undefined` keyword parses to a Literal holding the $Undefined sentinel // (Parser.Expressions.cs), as do array holes. State-machine emitters use this base // method, so without an explicit arm `undefined` would fall to `default` and emit @@ -391,6 +391,22 @@ protected virtual void EmitLiteral(Expr.Literal lit) } } + protected void EmitBigIntConstant(System.Numerics.BigInteger value) + { + if (value >= long.MinValue && value <= long.MaxValue) + { + IL.Emit(OpCodes.Ldc_I8, (long)value); + IL.Emit(OpCodes.Newobj, Types.GetConstructor(Types.BigInteger, Types.Int64)); + } + else + { + IL.Emit(OpCodes.Ldstr, value.ToString(System.Globalization.CultureInfo.InvariantCulture)); + IL.Emit(OpCodes.Call, Types.GetMethod(Types.BigInteger, "Parse", Types.String)); + } + IL.Emit(OpCodes.Box, Types.BigInteger); + SetStackUnknown(); + } + /// /// Emits an expression boxed and spills it into a fresh object local. /// State-machine emitters (the only users of these base implementations) can suspend @@ -2284,6 +2300,7 @@ protected bool TryEmitBuiltInClassType(string name) // bare Symbol in its own pseudo-variable arm; this entry covers the // state-machine emitters that resolve through this base path. "Symbol" => Ctx.Runtime!.Symbols.Type, + "BigInt" => Types.BigInteger, // Omitted Web stream constructors fall through to ThrowUndefinedVariable. "ReadableStream" => Ctx.Runtime!.WebStreams?.ReadableType, "WritableStream" => Ctx.Runtime!.WebStreams?.WritableType, @@ -2623,6 +2640,12 @@ protected virtual void EmitLogical(Expr.Logical l) /// protected virtual void EmitUnary(Expr.Unary u) { + if (u.Operator.Type == TokenType.MINUS && u.Right is Expr.Literal { Value: System.Numerics.BigInteger value }) + { + EmitBigIntConstant(-value); + return; + } + switch (u.Operator.Type) { case TokenType.MINUS: diff --git a/src/SharpTS/Compilation/ILEmitter.Expressions.cs b/src/SharpTS/Compilation/ILEmitter.Expressions.cs index 8567916b0..7c60ef138 100644 --- a/src/SharpTS/Compilation/ILEmitter.Expressions.cs +++ b/src/SharpTS/Compilation/ILEmitter.Expressions.cs @@ -24,20 +24,7 @@ protected override void EmitLiteral(Expr.Literal lit) EmitBoolConstant(b); break; case System.Numerics.BigInteger bi: - if (bi >= long.MinValue && bi <= long.MaxValue) - { - // Optimization: Use BigInteger(long) constructor for small values - IL.Emit(OpCodes.Ldc_I8, (long)bi); - IL.Emit(OpCodes.Newobj, _ctx.Types.GetConstructor(_ctx.Types.BigInteger, _ctx.Types.Int64)); - } - else - { - // Fallback: Parse from string for large values - IL.Emit(OpCodes.Ldstr, bi.ToString()); - IL.Emit(OpCodes.Call, _ctx.Types.GetMethod(_ctx.Types.BigInteger, "Parse", _ctx.Types.String)); - } - IL.Emit(OpCodes.Box, _ctx.Types.BigInteger); - SetStackUnknown(); + EmitBigIntConstant(bi); break; case Runtime.Types.SharpTSUndefined: EmitUndefinedConstant(); @@ -1001,22 +988,32 @@ protected override void EmitTaggedTemplateLiteral(Expr.TaggedTemplateLiteral ttl } // Detect property access tag (obj.method`...`) for this binding - bool hasThisBinding = ttl.Tag is Expr.Get; + bool hasThisBinding = ttl.Tag is Expr.Get or Expr.GetIndex; LocalBuilder? receiverLocal = null; // 1. Emit the tag function reference (and receiver for property access tags) if (hasThisBinding) { - var g = (Expr.Get)ttl.Tag; + var receiver = ttl.Tag is Expr.Get g ? g.Object : ((Expr.GetIndex)ttl.Tag).Object; // Emit and save the receiver object - EmitExpression(g.Object); + EmitExpression(receiver); EnsureBoxed(); receiverLocal = _ctx.ILBuilder.DeclareLocal(_ctx.Types.Object); IL.Emit(OpCodes.Stloc, receiverLocal); // Get the method: GetProperty(obj, name) — handles all object types including dictionaries IL.Emit(OpCodes.Ldloc, receiverLocal); - IL.Emit(OpCodes.Ldstr, g.Name.Lexeme); - IL.Emit(OpCodes.Call, _ctx.Runtime!.ObjectRead.Property); + if (ttl.Tag is Expr.Get property) + { + IL.Emit(OpCodes.Ldstr, property.Name.Lexeme); + IL.Emit(OpCodes.Call, _ctx.Runtime!.ObjectRead.Property); + } + else + { + var index = ((Expr.GetIndex)ttl.Tag).Index; + EmitExpression(index); + EmitBoxIfNeeded(index); + IL.Emit(OpCodes.Call, _ctx.Runtime!.ObjectRead.Index); + } // Push thisArg (receiver) for WithThis call IL.Emit(OpCodes.Ldloc, receiverLocal); } diff --git a/src/SharpTS/Compilation/ILEmitter.Operators.cs b/src/SharpTS/Compilation/ILEmitter.Operators.cs index cb7a0dcc8..a7dc2f033 100644 --- a/src/SharpTS/Compilation/ILEmitter.Operators.cs +++ b/src/SharpTS/Compilation/ILEmitter.Operators.cs @@ -1107,7 +1107,7 @@ protected override void EmitLogicalSet(Expr.LogicalSet ls) var resultLocal = IL.DeclareLocal(_ctx.Types.Object); IL.Emit(OpCodes.Dup); IL.Emit(OpCodes.Stloc, resultLocal); - IL.Emit(OpCodes.Call, _ctx.Runtime!.ObjectWrite.Property); + EmitPropertyWrite(indexed: false); IL.Emit(OpCodes.Ldloc, resultLocal); builder.Emit_Br(endLabel); @@ -1181,7 +1181,7 @@ protected override void EmitLogicalSetIndex(Expr.LogicalSetIndex lsi) var resultLocal = IL.DeclareLocal(_ctx.Types.Object); IL.Emit(OpCodes.Dup); IL.Emit(OpCodes.Stloc, resultLocal); - IL.Emit(OpCodes.Call, _ctx.Runtime!.ObjectWrite.Index); + EmitPropertyWrite(indexed: true); IL.Emit(OpCodes.Ldloc, resultLocal); builder.Emit_Br(endLabel); @@ -2252,7 +2252,7 @@ private void EmitIncrementProperty(Expr.Get get, bool isIncrement, bool isPrefix IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Ldstr, get.Name.Lexeme); IL.Emit(OpCodes.Ldloc, newValue); - IL.Emit(OpCodes.Call, _ctx.Runtime!.ObjectWrite.Property); + EmitPropertyWrite(indexed: false); IL.Emit(OpCodes.Ldloc, isPrefix ? newValue : oldValue); SetStackUnknown(); } @@ -2276,7 +2276,7 @@ private void EmitIncrementIndex(Expr.GetIndex gi, bool isIncrement, bool isPrefi IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Ldloc, index); IL.Emit(OpCodes.Ldloc, newValue); - IL.Emit(OpCodes.Call, _ctx.Runtime!.ObjectWrite.Index); + EmitPropertyWrite(indexed: true); IL.Emit(OpCodes.Ldloc, isPrefix ? newValue : oldValue); SetStackUnknown(); } @@ -2896,7 +2896,7 @@ private bool TryEmitStaticFieldIncrement(Expr.Get get, bool isIncrement, bool is IL.Emit(OpCodes.Call, _ctx.Types.TypeGetTypeFromHandle); IL.Emit(OpCodes.Ldstr, get.Name.Lexeme); IL.Emit(OpCodes.Ldloc, newValue); - IL.Emit(OpCodes.Call, _ctx.Runtime!.ObjectWrite.Property); + EmitPropertyWrite(indexed: false); } if (isPrefix) @@ -2965,7 +2965,7 @@ protected override void EmitCompoundSet(Expr.CompoundSet cs) IL.Emit(OpCodes.Call, _ctx.Types.TypeGetTypeFromHandle); IL.Emit(OpCodes.Ldstr, cs.Name.Lexeme); IL.Emit(OpCodes.Ldloc, inheritedResultCS); - IL.Emit(OpCodes.Call, _ctx.Runtime!.ObjectWrite.Property); + EmitPropertyWrite(indexed: false); IL.Emit(OpCodes.Ldloc, inheritedResultCS); SetStackUnknown(); @@ -2998,7 +2998,7 @@ protected override void EmitCompoundSet(Expr.CompoundSet cs) IL.Emit(OpCodes.Ldloc, objectLocal); IL.Emit(OpCodes.Ldstr, cs.Name.Lexeme); IL.Emit(OpCodes.Ldloc, resultLocal); - IL.Emit(OpCodes.Call, _ctx.Runtime!.ObjectWrite.Property); + EmitPropertyWrite(indexed: false); // Leave result on stack IL.Emit(OpCodes.Ldloc, resultLocal); @@ -3125,7 +3125,7 @@ protected override void EmitCompoundSetIndex(Expr.CompoundSetIndex csi) IL.Emit(OpCodes.Ldloc, objLocal); IL.Emit(OpCodes.Ldloc, indexLocal); IL.Emit(OpCodes.Ldloc, resultLocal); - IL.Emit(OpCodes.Call, _ctx.Runtime!.ObjectWrite.Index); + EmitPropertyWrite(indexed: true); // Leave result on stack IL.Emit(OpCodes.Ldloc, resultLocal); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Coercion.cs b/src/SharpTS/Compilation/RuntimeEmitter.Coercion.cs index a8827d40a..66b5885a3 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Coercion.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Coercion.cs @@ -29,17 +29,6 @@ private readonly record struct AbstractNumberInputs( MethodInfo CreateException, ConstructorInfo TypeErrorCtor); - private readonly record struct ExplicitNumberInputs( - Type UndefinedType, - Type SymbolType, - Type ObjectType, - MethodInfo GetProperty, - MethodInfo InvokeMethodValue, - MethodInfo ToJsString, - MethodInfo BigIntToNumber, - MethodInfo CreateException, - ConstructorInfo TypeErrorCtor); - // Immutable inputs for language ToString while peer families retain their own declarations. // Constructed for this body only; never retained as emitter state. private readonly record struct StringCoercionInputs( @@ -947,52 +936,6 @@ private void EmitToJsString(TypeBuilder typeBuilder, EmittedStringCoercionRuntim il.MarkLabel(isObjectLikeLabel); - // Boxed primitive marker fast-path: if the receiver carries - // __primitiveType + __primitiveValue (Stage 4z19 wrappers), Stringify - // the underlying primitive directly. Without this, toString walks the - // prototype chain to the StringPrototypeGenericStub which doesn't read - // the marker — returns receiver-as-string instead of the primitive's - // natural string repr (`new Object(true).valueOf()` gives wrapper, not true). - var primValLocal = il.DeclareLocal(_types.Object); - var notBoxedLabel = il.DefineLabel(); - // #574: an own (instance) toString override must win over the boxed - // __primitiveValue fast-path — ECMA-262 OrdinaryToPrimitive(O, "string") - // calls the own toString first. When the wrapper carries an own toString, - // defer to the OrdinaryToPrimitive section below (which invokes it). An - // inherited prototype toString is NOT own, so un-overridden wrappers still - // take the fast-path. (HasOwnPropertyHelper does not walk the prototype.) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldstr, "toString"); - il.Emit(OpCodes.Call, peers.HasOwnProperty); - il.Emit(OpCodes.Brtrue, notBoxedLabel); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldstr, "__primitiveValue"); - il.Emit(OpCodes.Call, peers.GetProperty); - il.Emit(OpCodes.Stloc, primValLocal); - il.Emit(OpCodes.Ldloc, primValLocal); - il.Emit(OpCodes.Brfalse, notBoxedLabel); - il.Emit(OpCodes.Ldloc, primValLocal); - il.Emit(OpCodes.Isinst, peers.UndefinedType); - il.Emit(OpCodes.Brtrue, notBoxedLabel); - // ECMA-262 §7.1.17 step 2: throw TypeError if the unwrapped primitive - // is a Symbol. The entry-point check at line ~1856 only catches raw - // Symbol values — Object(Symbol("x")) wraps it as $Object with - // __primitiveValue=sym, and the unwrap below bypasses that guard, - // letting Stringify run on a Symbol (returns "Symbol(x)" rather than - // throwing). Required by indexOf/searchstring-tostring-errors et al. - var unwrapNotSymLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, primValLocal); - il.Emit(OpCodes.Isinst, peers.SymbolType); - il.Emit(OpCodes.Brfalse, unwrapNotSymLabel); - GuestErrorEmitter.ThrowError(il, peers.CreateException, peers.TypeErrorCtor, "Cannot convert a Symbol value to a string"); - il.MarkLabel(unwrapNotSymLabel); - // Re-enter language ToString for the primitive. This is observably - // different from debug Stringify for BigInt ("42" versus "42n"). - il.Emit(OpCodes.Ldloc, primValLocal); - il.Emit(OpCodes.Call, coercion.ToJsString); - il.Emit(OpCodes.Ret); - il.MarkLabel(notBoxedLabel); - // ECMA-262 7.1.1 ToPrimitive(input, "string"): GetMethod(input, @@toPrimitive) // takes priority over OrdinaryToPrimitive. Look up Symbol.toPrimitive in the // value's symbol-dict (compiled mode stores symbol-keyed properties separately @@ -1124,6 +1067,52 @@ private void EmitToJsString(TypeBuilder typeBuilder, EmittedStringCoercionRuntim il.Emit(OpCodes.Ret); il.MarkLabel(afterToPrimSymLabel); + // Boxed primitive marker fast-path: if the receiver carries + // __primitiveType + __primitiveValue (Stage 4z19 wrappers), Stringify + // the underlying primitive directly. Without this, toString walks the + // prototype chain to the StringPrototypeGenericStub which doesn't read + // the marker — returns receiver-as-string instead of the primitive's + // natural string repr (`new Object(true).valueOf()` gives wrapper, not true). + var primValLocal = il.DeclareLocal(_types.Object); + var notBoxedLabel = il.DefineLabel(); + // #574: an own (instance) toString override must win over the boxed + // __primitiveValue fast-path — ECMA-262 OrdinaryToPrimitive(O, "string") + // calls the own toString first. When the wrapper carries an own toString, + // defer to the OrdinaryToPrimitive section below (which invokes it). An + // inherited prototype toString is NOT own, so un-overridden wrappers still + // take the fast-path. (HasOwnPropertyHelper does not walk the prototype.) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldstr, "toString"); + il.Emit(OpCodes.Call, peers.HasOwnProperty); + il.Emit(OpCodes.Brtrue, notBoxedLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldstr, "__primitiveValue"); + il.Emit(OpCodes.Call, peers.GetProperty); + il.Emit(OpCodes.Stloc, primValLocal); + il.Emit(OpCodes.Ldloc, primValLocal); + il.Emit(OpCodes.Brfalse, notBoxedLabel); + il.Emit(OpCodes.Ldloc, primValLocal); + il.Emit(OpCodes.Isinst, peers.UndefinedType); + il.Emit(OpCodes.Brtrue, notBoxedLabel); + // ECMA-262 §7.1.17 step 2: throw TypeError if the unwrapped primitive + // is a Symbol. The entry-point check at line ~1856 only catches raw + // Symbol values — Object(Symbol("x")) wraps it as $Object with + // __primitiveValue=sym, and the unwrap below bypasses that guard, + // letting Stringify run on a Symbol (returns "Symbol(x)" rather than + // throwing). Required by indexOf/searchstring-tostring-errors et al. + var unwrapNotSymLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, primValLocal); + il.Emit(OpCodes.Isinst, peers.SymbolType); + il.Emit(OpCodes.Brfalse, unwrapNotSymLabel); + GuestErrorEmitter.ThrowError(il, peers.CreateException, peers.TypeErrorCtor, "Cannot convert a Symbol value to a string"); + il.MarkLabel(unwrapNotSymLabel); + // Re-enter language ToString for the primitive. This is observably + // different from debug Stringify for BigInt ("42" versus "42n"). + il.Emit(OpCodes.Ldloc, primValLocal); + il.Emit(OpCodes.Call, coercion.ToJsString); + il.Emit(OpCodes.Ret); + il.MarkLabel(notBoxedLabel); + // emptyArgs = new object[0] var emptyArgsLocal = il.DeclareLocal(_types.ObjectArray); il.Emit(OpCodes.Ldc_I4_0); @@ -1273,22 +1262,8 @@ void TryInvoke(string name, Label afterLabel) il.Emit(OpCodes.Ret); } - private void EmitToNumber(TypeBuilder typeBuilder, EmittedNumericCoercionRuntime numeric, - AbstractNumberInputs peers) + private void EmitNumericToPrimitive(ILGenerator il, AbstractNumberInputs peers, LocalBuilder argLocal) { - var method = (MethodBuilder)numeric.ToNumber; - - var il = method.GetILGenerator(); - var resultLocal = il.DeclareLocal(_types.Double); - - // ECMA-262 7.1.4 ToNumber on object: ToPrimitive(value, "number") which - // tries valueOf first, then toString. Without this, Math.hypot(obj-with- - // throwing-valueOf) silently returns NaN instead of propagating the - // throw. Apply for Dictionary or $Object only. - var argLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Stloc, argLocal); - var skipToPrimLabelTop = il.DefineLabel(); var doToPrimLabelTop = il.DefineLabel(); @@ -1540,6 +1515,25 @@ void TryToPrim2(string name, Label afterLabel) il.MarkLabel(afterTeT); il.MarkLabel(skipToPrimLabelTop); + } + + private void EmitToNumber(TypeBuilder typeBuilder, EmittedNumericCoercionRuntime numeric, + AbstractNumberInputs peers) + { + var method = (MethodBuilder)numeric.ToNumber; + + var il = method.GetILGenerator(); + var resultLocal = il.DeclareLocal(_types.Double); + + // ECMA-262 7.1.4 ToNumber on object: ToPrimitive(value, "number") which + // tries valueOf first, then toString. Without this, Math.hypot(obj-with- + // throwing-valueOf) silently returns NaN instead of propagating the + // throw. Apply for Dictionary or $Object only. + var argLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Stloc, argLocal); + + EmitNumericToPrimitive(il, peers, argLocal); // ECMA-262 7.1.4 ToNumber on Symbol → throws TypeError. Without this, // Convert.ToDouble would catch the InvalidCastException → NaN → 0, @@ -1890,7 +1884,7 @@ internal void DeclareConvertToNumber(TypeBuilder typeBuilder, EmittedNumericCoer } private void EmitConvertToNumber(TypeBuilder typeBuilder, EmittedNumericCoercionRuntime numeric, - ExplicitNumberInputs peers) + AbstractNumberInputs peers, MethodInfo bigIntToNumber) { var method = (MethodBuilder)numeric.ConvertToNumber; @@ -1914,101 +1908,7 @@ private void EmitConvertToNumber(TypeBuilder typeBuilder, EmittedNumericCoercion il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Stloc, argLocal); - var skipToPrimLabel = il.DefineLabel(); - var doToPrimLabel = il.DefineLabel(); - - // Number(array) uses the same Array-to-primitive conversion as the - // abstract ToNumber operation above. - var notArrayConvertLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.ListOfObject); - il.Emit(OpCodes.Brfalse, notArrayConvertLabel); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Call, peers.ToJsString); - il.Emit(OpCodes.Stloc, argLocal); - il.Emit(OpCodes.Br, skipToPrimLabel); - il.MarkLabel(notArrayConvertLabel); - - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, doToPrimLabel); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, peers.ObjectType); - il.Emit(OpCodes.Brfalse, skipToPrimLabel); - il.MarkLabel(doToPrimLabel); - - // ToPrimitive: try valueOf, then toString. Mirrors EmitLengthToPrimitive's - // logic but writes to argLocal so the existing branches below see the - // (possibly replaced) primitive value. - var emptyArgsLocal = il.DeclareLocal(_types.ObjectArray); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Newarr, _types.Object); - il.Emit(OpCodes.Stloc, emptyArgsLocal); - - void TryToPrim(string name, Label afterLabel) - { - var fnLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Ldstr, name); - il.Emit(OpCodes.Call, peers.GetProperty); - il.Emit(OpCodes.Stloc, fnLocal); - il.Emit(OpCodes.Ldloc, fnLocal); - il.Emit(OpCodes.Brfalse, afterLabel); - il.Emit(OpCodes.Ldloc, fnLocal); - il.Emit(OpCodes.Isinst, peers.UndefinedType); - il.Emit(OpCodes.Brtrue, afterLabel); - - var resultLocal = il.DeclareLocal(_types.Object); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Ldloc, fnLocal); - il.Emit(OpCodes.Ldloc, emptyArgsLocal); - il.Emit(OpCodes.Call, peers.InvokeMethodValue); - il.Emit(OpCodes.Stloc, resultLocal); - - // Still object? Fall through to next attempt without committing. - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, afterLabel); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Isinst, peers.ObjectType); - il.Emit(OpCodes.Brtrue, afterLabel); - il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(OpCodes.Stloc, argLocal); - } - - var afterValueOf = il.DefineLabel(); - TryToPrim("valueOf", afterValueOf); - il.MarkLabel(afterValueOf); - - var afterToString = il.DefineLabel(); - var stillObj = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, stillObj); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, peers.ObjectType); - il.Emit(OpCodes.Brfalse, afterToString); - il.MarkLabel(stillObj); - TryToPrim("toString", afterToString); - il.MarkLabel(afterToString); - - // ECMA-262 7.1.1.1 OrdinaryToPrimitive: if neither valueOf nor toString - // returned a primitive (both returned objects, or neither was callable), - // throw TypeError. Pre-fix: the value fell through to NaN, silently - // masking the spec-required throw. - var afterTypeErrorLabel = il.DefineLabel(); - var stillObjAfterToString = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); - il.Emit(OpCodes.Brtrue, stillObjAfterToString); - il.Emit(OpCodes.Ldloc, argLocal); - il.Emit(OpCodes.Isinst, peers.ObjectType); - il.Emit(OpCodes.Brfalse, afterTypeErrorLabel); - il.MarkLabel(stillObjAfterToString); - GuestErrorEmitter.ThrowError(il, peers.CreateException, peers.TypeErrorCtor, "Cannot convert object to primitive value"); - il.MarkLabel(afterTypeErrorLabel); - - il.MarkLabel(skipToPrimLabel); + EmitNumericToPrimitive(il, peers, argLocal); // null => 0.0 il.Emit(OpCodes.Ldloc, argLocal); @@ -2035,7 +1935,7 @@ void TryToPrim(string name, Label afterLabel) il.Emit(OpCodes.Brfalse, notBigIntConvLabel); il.Emit(OpCodes.Ldloc, argLocal); il.Emit(OpCodes.Unbox_Any, _types.BigInteger); - il.Emit(OpCodes.Call, peers.BigIntToNumber); + il.Emit(OpCodes.Call, bigIntToNumber); il.Emit(OpCodes.Ret); il.MarkLabel(notBigIntConvLabel); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.HasOwnProperty.cs b/src/SharpTS/Compilation/RuntimeEmitter.HasOwnProperty.cs index 7f7445a5a..801e38dd7 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.HasOwnProperty.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.HasOwnProperty.cs @@ -730,6 +730,18 @@ PropertyIsEnumerableHelperInputs inputs il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); il.Emit(OpCodes.Stloc, nameLocal); + // String exotic objects have a non-enumerable intrinsic length; their + // indexed characters still use the ordinary own-property fallback. + var notStringLength = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brfalse, notStringLength); + il.Emit(OpCodes.Ldloc, nameLocal); + il.Emit(OpCodes.Ldstr, "length"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brtrue, falseLabel); + il.MarkLabel(notStringLength); + // RegExp lastIndex is an intrinsic own non-enumerable property. It is // not placed in PDS until user code redefines its attributes. if (inputs.RegExps.Implementation is not null) diff --git a/src/SharpTS/Compilation/RuntimeEmitter.MathAdapters.cs b/src/SharpTS/Compilation/RuntimeEmitter.MathAdapters.cs index e6a6bcea2..1ce5dc4d8 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.MathAdapters.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.MathAdapters.cs @@ -440,6 +440,14 @@ private MethodBuilder EmitMathSignAdapter(TypeBuilder typeBuilder, MethodInfo to il.Emit(OpCodes.Ret); il.MarkLabel(notNaN); + var notZero = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, vLocal); + il.Emit(OpCodes.Ldc_R8, 0.0); + il.Emit(OpCodes.Bne_Un, notZero); + il.Emit(OpCodes.Ldloc, vLocal); + il.Emit(OpCodes.Box, _types.Double); + il.Emit(OpCodes.Ret); + il.MarkLabel(notZero); il.Emit(OpCodes.Ldloc, vLocal); il.Emit(OpCodes.Call, _types.GetMethod(_types.Math, "Sign", _types.Double)); il.Emit(OpCodes.Conv_R8); @@ -550,14 +558,12 @@ private MethodBuilder EmitMathMinMaxAdapter(TypeBuilder typeBuilder, MethodInfo il.MarkLabel(notNaN); - // if (isMax ? next > result : next < result) result = next; - var skipUpdate = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, nextLocal); + // Framework Min/Max implement the IEEE signed-zero tie rules as well + // as ordinary numeric ordering; a strict comparison loses zero signs. il.Emit(OpCodes.Ldloc, resultLocal); - il.Emit(isMax ? OpCodes.Ble : OpCodes.Bge, skipUpdate); il.Emit(OpCodes.Ldloc, nextLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Math, isMax ? "Max" : "Min", _types.Double, _types.Double)); il.Emit(OpCodes.Stloc, resultLocal); - il.MarkLabel(skipUpdate); // i++ il.Emit(OpCodes.Ldloc, iLocal); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Number.cs b/src/SharpTS/Compilation/RuntimeEmitter.Number.cs index 15537eabd..f84a6682a 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Number.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Number.cs @@ -778,16 +778,21 @@ private void EmitParseIntHelper(TypeBuilder typeBuilder, EmittedNumberRuntime nu // Check for 0x prefix (only if radix is 0 or 16) il.MarkLabel(checkHexLabel); il.Emit(OpCodes.Ldloc, radixLocal); - il.Emit(OpCodes.Brtrue, validateRadixLabel); // radix != 0, skip hex detection + var stripHexPrefixLabel = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, stripHexPrefixLabel); + il.Emit(OpCodes.Ldloc, radixLocal); + il.Emit(OpCodes.Ldc_I4, 16); + il.Emit(OpCodes.Bne_Un, validateRadixLabel); + il.MarkLabel(stripHexPrefixLabel); - // radix is 0, check for 0x + // A hexadecimal prefix is stripped for both inferred and explicit radix 16. il.Emit(OpCodes.Ldloc, strLocal); il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length")!.GetGetMethod()!); il.Emit(OpCodes.Ldloc, startIndexLocal); il.Emit(OpCodes.Ldc_I4_2); il.Emit(OpCodes.Add); var noRoomForHexLabel = il.DefineLabel(); - il.Emit(OpCodes.Ble, noRoomForHexLabel); + il.Emit(OpCodes.Blt, noRoomForHexLabel); // Check if str[startIndex] == '0' && (str[startIndex+1] == 'x' || str[startIndex+1] == 'X') il.Emit(OpCodes.Ldloc, strLocal); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Descriptors.DefineProperty.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Descriptors.DefineProperty.cs index 4595f3b73..413f23eef 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Descriptors.DefineProperty.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Descriptors.DefineProperty.cs @@ -196,6 +196,28 @@ LocalBuilder propNameLocal il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Isinst, inputs.IHasFieldsInterface); il.Emit(OpCodes.Brtrue, receiverIsSynthableLabel); + // Emitted class constructors expose static fields through System.Type. + // Only an own field supplies an existing data descriptor; inherited + // fields must retain the defaults for a newly defined own property. + var notOwnStaticField = il.DefineLabel(); + var staticOwner = il.DeclareLocal(_types.Type); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Type); + il.Emit(OpCodes.Stloc, staticOwner); + il.Emit(OpCodes.Ldloc, staticOwner); + il.Emit(OpCodes.Brfalse, notOwnStaticField); + il.Emit(OpCodes.Ldtoken, inputs.IHasFieldsInterface); + il.Emit(OpCodes.Call, _types.TypeGetTypeFromHandle); + il.Emit(OpCodes.Ldloc, staticOwner); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "IsAssignableFrom", _types.Type)); + il.Emit(OpCodes.Brfalse, notOwnStaticField); + var staticFieldOwner = EmitStaticMemberLookupOwner(il, staticOwner, inputs.IHasFieldsInterface); + il.Emit(OpCodes.Ldloc, staticFieldOwner); + il.Emit(OpCodes.Ldloc, propNameLocal); + il.Emit(OpCodes.Ldc_I4, (int)(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetField", _types.String, typeof(BindingFlags))); + il.Emit(OpCodes.Brtrue, receiverIsSynthableLabel); + il.MarkLabel(notOwnStaticField); var checkSynthListLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Isinst, inputs.ArrayStorage.Type); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs index 0e9b7d41b..f2f60a22d 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs @@ -1251,6 +1251,7 @@ void EmitTypeSetSkipName(string n) il.Emit(OpCodes.Ret); il.MarkLabel(newTypeDescriptorLabel); + EmitRejectInheritedStaticDataWrite(il, inputs.DescriptorStorage, inputs.IHasFieldsInterface, inputs.Errors, strict: false); var noComputedSetter = il.DefineLabel(); var computedSetter = il.DeclareLocal(_types.Object); il.Emit(OpCodes.Ldarg_0); @@ -1911,6 +1912,58 @@ private void EmitToLengthBoxed(ILGenerator il, Type undefinedType) il.MarkLabel(doneLabel); } + // A writable inherited data property may create an own shadow. A read-only + // descriptor must reject that write before either the CLR field bridge or PDS + // assignment creates the shadow. Existing own properties take precedence. + private void EmitRejectInheritedStaticDataWrite( + ILGenerator il, EmittedDescriptorStorageRuntime descriptors, Type fieldsInterface, + EmittedErrorRuntime errors, bool strict) + { + var done = il.DefineLabel(); + var owner = il.DeclareLocal(_types.Type); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.Type); + il.Emit(OpCodes.Stloc, owner); + il.Emit(OpCodes.Ldloc, owner); + il.Emit(OpCodes.Brfalse, done); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, descriptors.GetPropertyDescriptor); + il.Emit(OpCodes.Brtrue, done); + var fieldOwner = EmitStaticMemberLookupOwner(il, owner, fieldsInterface); + il.Emit(OpCodes.Ldloc, fieldOwner); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldc_I4, (int)(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetField", _types.String, typeof(BindingFlags))); + il.Emit(OpCodes.Brtrue, done); + var descriptor = il.DeclareLocal(descriptors.DescriptorType); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, descriptors.GetStaticShadow); + il.Emit(OpCodes.Stloc, descriptor); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Brfalse, done); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, descriptors.DescriptorGetter.GetGetMethod()!); + il.Emit(OpCodes.Brtrue, done); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, descriptors.DescriptorSetter.GetGetMethod()!); + il.Emit(OpCodes.Brtrue, done); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, descriptors.DescriptorWritable.GetGetMethod()!); + il.Emit(OpCodes.Brtrue, done); + if (strict) + { + var silent = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_3); + il.Emit(OpCodes.Brfalse, silent); + EmitThrowTypeErrorWithName(il, errors, "Cannot assign to read only property '", "' of class"); + il.MarkLabel(silent); + } + il.Emit(OpCodes.Ret); + il.MarkLabel(done); + } + /// /// Emits SetPropertyStrict(object obj, string name, object value, bool strictMode) -> void /// In strict mode, throws TypeError for modifications to frozen objects or new properties on sealed objects. @@ -2026,10 +2079,11 @@ SetPropertyStrictInputs inputs il.Emit(OpCodes.Isinst, _types.ListOfObject); il.Emit(OpCodes.Brtrue, arraySetStrictLabel); - // Class bodies are strict. Generic static data-field writes must use the + // Class bodies are strict. Emitted static data-field writes must use the // same storage/shadow path as value-position writes outside the class. - // Restrict this bridge to emitted generic classes with a reflected data + // Restrict this bridge to emitted classes with a reflected data // field; other receiver kinds retain their dedicated assignment paths. + EmitRejectInheritedStaticDataWrite(il, inputs.DescriptorStorage, inputs.IHasFieldsInterface, inputs.Errors, strict: true); var notGenericStaticField = il.DefineLabel(); var genericOwner = il.DeclareLocal(_types.Type); il.Emit(OpCodes.Ldarg_0); @@ -2037,9 +2091,6 @@ SetPropertyStrictInputs inputs il.Emit(OpCodes.Stloc, genericOwner); il.Emit(OpCodes.Ldloc, genericOwner); il.Emit(OpCodes.Brfalse, notGenericStaticField); - il.Emit(OpCodes.Ldloc, genericOwner); - il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.Type, "ContainsGenericParameters")!.GetGetMethod()!); - il.Emit(OpCodes.Brfalse, notGenericStaticField); il.Emit(OpCodes.Ldtoken, inputs.IHasFieldsInterface); il.Emit(OpCodes.Call, _types.TypeGetTypeFromHandle); il.Emit(OpCodes.Ldloc, genericOwner); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Utilities.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Utilities.cs index be0d6c62e..b95ba079f 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Utilities.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Utilities.cs @@ -48,6 +48,7 @@ EmittedSymbolRuntime Symbols // Scoped exact-sum construction inputs; peer declarations stay with their own families. private readonly record struct MathSumInputs( + EmittedArrayStorageRuntime Arrays, MethodInfo GetSymbolDict, FieldInfo SymbolIterator, MethodInfo GetIteratorFunction, @@ -757,6 +758,9 @@ private void EmitMathSumPrecise(TypeBuilder typeBuilder, EmittedMathRuntime math math.SumPrecise = method; var il = method.GetILGenerator(); + // Numeric-mode arrays inherit List with an empty boxed store. + // Materialize it before indexed summation observes Count and Item. + EmitDeoptArgIfNumericArrayStorage(il, peers.Arrays, 0); var list = il.DeclareLocal(_types.ListOfObject); var symbolDict = il.DeclareLocal(_types.DictionaryObjectObject); var iteratorFunction = il.DeclareLocal(_types.Object); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.PropertyDescriptorStore.cs b/src/SharpTS/Compilation/RuntimeEmitter.PropertyDescriptorStore.cs index 3349e117a..78001ea75 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.PropertyDescriptorStore.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.PropertyDescriptorStore.cs @@ -1239,7 +1239,8 @@ private void EmitPDSHasIndexedOwnProperty(TypeBuilder typeBuilder, EmittedDescri /// /// Emits: public static $CompiledPropertyDescriptor? GetStaticShadow(object typeObj, string propertyKey) /// Walks the constructor Type chain (typeObj and each BaseType) probing each class's PDS - /// store, returning the nearest own-shadow descriptor or null. A subclass static-field write + /// store, returning the nearest own-shadow descriptor or null when a nearer ordinary static + /// field takes precedence. A subclass static-field write /// (Sub.field = v) stores a per-subclass shadow keyed by the subclass Type (the SetProperty /// System.Type arm); inherited static-field reads consult this before falling back to the declaring /// base's field so JS own-shadow semantics hold in compiled mode (issue #339). Mirrors the inline @@ -1273,7 +1274,20 @@ private void EmitPDSGetStaticShadow(TypeBuilder typeBuilder, EmittedDescriptorSt il.Emit(OpCodes.Brfalse, returnNullLabel); // desc = GetPropertyDescriptor(walkType, propertyKey); + // Generic class constructor values use the definition token, whereas + // BaseType may be a constructed Base. Probe the same PDS identity. + var descriptorOwner = il.DeclareLocal(_types.Type); il.Emit(OpCodes.Ldloc, walkTypeLocal); + il.Emit(OpCodes.Stloc, descriptorOwner); + var probeDescriptor = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, descriptorOwner); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.Type, "IsGenericType").GetGetMethod()!); + il.Emit(OpCodes.Brfalse, probeDescriptor); + il.Emit(OpCodes.Ldloc, descriptorOwner); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Type, "GetGenericTypeDefinition")); + il.Emit(OpCodes.Stloc, descriptorOwner); + il.MarkLabel(probeDescriptor); + il.Emit(OpCodes.Ldloc, descriptorOwner); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Call, storage.GetPropertyDescriptor); il.Emit(OpCodes.Stloc, descLocal); @@ -1285,8 +1299,17 @@ private void EmitPDSGetStaticShadow(TypeBuilder typeBuilder, EmittedDescriptorSt il.Emit(OpCodes.Ldloc, descLocal); il.Emit(OpCodes.Ret); - // walkType = walkType.BaseType; continue; il.MarkLabel(nextLabel); + // An ordinary own field is a writable data property. It hides farther + // descriptors just as an own PDS entry does; callers fall back to the + // nearest resolved CLR field when no descriptor is returned. + il.Emit(OpCodes.Ldloc, descriptorOwner); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldc_I4, (int)(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetField", _types.String, typeof(BindingFlags))); + il.Emit(OpCodes.Brtrue, returnNullLabel); + + // walkType = walkType.BaseType; continue; il.Emit(OpCodes.Ldloc, walkTypeLocal); il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.Type, "BaseType").GetGetMethod()!); il.Emit(OpCodes.Stloc, walkTypeLocal); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs b/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs index 3d90d4a58..9694c91f9 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs @@ -1304,18 +1304,15 @@ void EmitLinkProto(FieldBuilder child) // ToNumber/ConvertToNumber bodies: emit AFTER GetProperty/InvokeMethodValue // so their ToPrimitive(value, "number") on Dictionary/$Object args can // call those helpers. - EmitToNumber(typeBuilder, runtime.NumericCoercion, - new AbstractNumberInputs(runtime.Sentinels.UndefinedType, runtime.Symbols.Type, runtime.ObjectStorage.Type, + var numberInputs = new AbstractNumberInputs(runtime.Sentinels.UndefinedType, runtime.Symbols.Type, runtime.ObjectStorage.Type, runtime.FunctionValues.Type, runtime.FunctionBindings.AnyType, runtime.ObjectFields.Interface, runtime.DescriptorStorage.DescriptorType, runtime.DescriptorStorage.DescriptorGetter.GetGetMethod()!, runtime.DescriptorStorage.DescriptorSetter.GetGetMethod()!, runtime.DescriptorStorage.DescriptorValue.GetGetMethod()!, runtime.Symbols.ToPrimitive, runtime.Symbols.GetStorage, runtime.ObjectRead.Property, runtime.Invocation.Method, runtime.Operators.TypeOf, runtime.StringCoercion.ToJsString, runtime.BoxedPrimitives.UnwrapIfBoxed, - runtime.Errors.CreateException, runtime.Errors.TypeErrorConstructor)); - EmitConvertToNumber(typeBuilder, runtime.NumericCoercion, - new ExplicitNumberInputs(runtime.Sentinels.UndefinedType, runtime.Symbols.Type, runtime.ObjectStorage.Type, - runtime.ObjectRead.Property, runtime.Invocation.Method, runtime.StringCoercion.ToJsString, - runtime.BigInt.ToNumber, runtime.Errors.CreateException, runtime.Errors.TypeErrorConstructor)); + runtime.Errors.CreateException, runtime.Errors.TypeErrorConstructor); + EmitToNumber(typeBuilder, runtime.NumericCoercion, numberInputs); + EmitConvertToNumber(typeBuilder, runtime.NumericCoercion, numberInputs, runtime.BigInt.ToNumber); // String.raw lives here so its body can read `template.raw` via // GetProperty and ToString-coerce substitutions via ToJsString. EmitStringRaw(typeBuilder, runtime.Templates, @@ -2502,7 +2499,7 @@ void EmitLinkProto(FieldBuilder child) // EmitRandom moved to before gOPD (see line ~660). The original site // here is now empty. EmitMathSumPrecise(typeBuilder, runtime.Math, new MathSumInputs( - runtime.Symbols.GetStorage, runtime.Symbols.Iterator, runtime.IteratorProtocol.Function, runtime.Sentinels.UndefinedType, runtime.Invocation.Method, runtime.IteratorRecords.NextMethod, runtime.IteratorRecords.InvokeNext, runtime.IteratorProtocol.Done, runtime.IteratorProtocol.Value, runtime.ObjectRead.Property, runtime.Errors.CreateException, runtime.Errors.TypeErrorConstructor)); + runtime.ArrayStorage, runtime.Symbols.GetStorage, runtime.Symbols.Iterator, runtime.IteratorProtocol.Function, runtime.Sentinels.UndefinedType, runtime.Invocation.Method, runtime.IteratorRecords.NextMethod, runtime.IteratorRecords.InvokeNext, runtime.IteratorProtocol.Done, runtime.IteratorProtocol.Value, runtime.ObjectRead.Property, runtime.Errors.CreateException, runtime.Errors.TypeErrorConstructor)); EmitDefineSymbolAccessor( typeBuilder, runtime.ObjectConstruction, diff --git a/tests/SharpTS.Tests/Compilation/CompilationServiceTests.cs b/tests/SharpTS.Tests/Compilation/CompilationServiceTests.cs index 1b583c33c..7eb2b7e20 100644 --- a/tests/SharpTS.Tests/Compilation/CompilationServiceTests.cs +++ b/tests/SharpTS.Tests/Compilation/CompilationServiceTests.cs @@ -19,8 +19,12 @@ public class CompilationServiceCollection } [Collection("CompilationService")] -public class CompilationServiceTests +public class CompilationServiceTests : IDisposable { + private readonly IDisposable _writers = AsyncLocalConsoleRedirector.SynchronizeWriters(); + + public void Dispose() => _writers.Dispose(); + // ------------------------------------------------------------------------- // Compile // ------------------------------------------------------------------------- @@ -223,10 +227,16 @@ public void Execute_RestoresConsoleAfterRun() var priorErr = Console.Error; var result = CompilationService.Compile("console.log(\"x\");"); - CompilationService.Execute(result.AssemblyBytes!, new StringWriter()); - - Assert.Same(priorOut, Console.Out); - Assert.Same(priorErr, Console.Error); + Assert.True(result.Success); + for (int invocation = 0; invocation < 2; invocation++) + { + using var output = new StringWriter(); + var run = CompilationService.Execute(result.AssemblyBytes!, output); + Assert.True(run.Success, run.Error); + Assert.Equal("x\n", output.ToString().Replace("\r\n", "\n")); + Assert.Same(priorOut, Console.Out); + Assert.Same(priorErr, Console.Error); + } } [Fact] diff --git a/tests/SharpTS.Tests/CompilerTests/HistoricalRuntimeDeploymentTests.cs b/tests/SharpTS.Tests/CompilerTests/HistoricalRuntimeDeploymentTests.cs index 180150903..8dc668066 100644 --- a/tests/SharpTS.Tests/CompilerTests/HistoricalRuntimeDeploymentTests.cs +++ b/tests/SharpTS.Tests/CompilerTests/HistoricalRuntimeDeploymentTests.cs @@ -11,16 +11,18 @@ namespace SharpTS.Tests.CompilerTests; -[Collection("ExternalProcessTests")] -public sealed class HistoricalRuntimeDeploymentTests +internal static class HistoricalRuntimeDeploymentTests { - [Theory] - [MemberData(nameof(HistoricalRuntimeRegressionTests.Cases), MemberType = typeof(HistoricalRuntimeRegressionTests))] - public void Isolated_PreservedSourcesExecuteStandaloneAndHosted(string file, string source, string expected) + public static void AssertDeployment(string file, string source, string expected) { using var directory = CliTestHelper.CreateTempDirectory(); var path = directory.CreateFile(file, source); - foreach (bool noLib in new[] { false, true }) + // Preserve historical snippets where current declarations reject + // sumPrecise availability or unchecked descriptor reads. The noLib probes + // retain the originals; typed controls also check default declarations. + var libModes = file is "issue1935-original.ts" or "issue1936-original.ts" or "issue1959-original.ts" or "issue1960-original.ts" + ? new[] { true } : new[] { false, true }; + foreach (bool noLib in libModes) foreach (bool hosted in new[] { false, true }) { string output = directory.GetPath($"guest-{noLib}-{hosted}.dll"); @@ -70,24 +72,16 @@ private static string RunStandalone(string dll) private static string RunHosted(string dll) { - lock (TestHarness.ConsoleLock) - { - var dispatcher = new DeterministicHostDispatcher(); - var sink = new RecordingErrorSink(); - var priorOut = Console.Out; - using var output = new StringWriter(); - Console.SetOut(output); - try - { - using var runtime = SharpTSHostedAssembly.CreateRuntime( - Assembly.Load(File.ReadAllBytes(dll)), dispatcher, new RecordingLifetime(), sink); - Task initialization = runtime.InitializeAsync(); - dispatcher.RunUntil(() => initialization.IsCompleted, timeout: TimeSpan.FromSeconds(30)); - initialization.GetAwaiter().GetResult(); - Assert.Empty(sink.Errors); - return output.ToString().Replace("\r\n", "\n"); - } - finally { Console.SetOut(priorOut); } - } + var dispatcher = new DeterministicHostDispatcher(); + var sink = new RecordingErrorSink(); + using var output = new StringWriter(); + using var capture = AsyncLocalConsoleRedirector.WithOut(output); + using var runtime = SharpTSHostedAssembly.CreateRuntime( + Assembly.Load(File.ReadAllBytes(dll)), dispatcher, new RecordingLifetime(), sink); + Task initialization = runtime.InitializeAsync(); + dispatcher.RunUntil(() => initialization.IsCompleted, timeout: TimeSpan.FromSeconds(30)); + initialization.GetAwaiter().GetResult(); + Assert.Empty(sink.Errors); + return output.ToString().Replace("\r\n", "\n"); } } diff --git a/tests/SharpTS.Tests/CompilerTests/StandaloneDllTests.ConsoleRestoration.cs b/tests/SharpTS.Tests/CompilerTests/StandaloneDllTests.ConsoleRestoration.cs new file mode 100644 index 000000000..3736993a9 --- /dev/null +++ b/tests/SharpTS.Tests/CompilerTests/StandaloneDllTests.ConsoleRestoration.cs @@ -0,0 +1,24 @@ +using System.Diagnostics; +using System.Reflection; +using SharpTS.ConsoleRestorationFixture; +using SharpTS.Testing; +using Xunit; + +namespace SharpTS.Tests.CompilerTests; + +public partial class StandaloneDllTests +{ + [Theory] + [InlineData(0)] + [InlineData(1)] + public void Isolated_Issue1930ConsoleRestoration_UsesFreshTestModule(int process) + { + var start = new ProcessStartInfo("dotnet"); + start.ArgumentList.Add(typeof(ConsoleRestorationFixtureMarker).Assembly.Location); + start.ArgumentList.Add(Assembly.GetExecutingAssembly().Location); + var result = TestProcess.Run(start, TimeSpan.FromSeconds(30), $"Console contract fresh process {process}"); + Assert.True(result.ExitCode == 0, result.StandardOutput + result.StandardError); + Assert.Empty(result.StandardError); + Assert.Equal("console restoration controls passed\n", result.StandardOutput.Replace("\r\n", "\n")); + } +} diff --git a/tests/SharpTS.Tests/CompilerTests/StandaloneDllTests.HistoricalRuntime.cs b/tests/SharpTS.Tests/CompilerTests/StandaloneDllTests.HistoricalRuntime.cs new file mode 100644 index 000000000..b812374d5 --- /dev/null +++ b/tests/SharpTS.Tests/CompilerTests/StandaloneDllTests.HistoricalRuntime.cs @@ -0,0 +1,36 @@ +using SharpTS.Tests.SharedTests; +using Xunit; + +namespace SharpTS.Tests.CompilerTests; + +public partial class StandaloneDllTests +{ + // The shard runner partitions by method. Bound each method's serial work + // while retaining every source and all standalone/hosted declaration paths. + public static IEnumerable HistoricalRuntimeDeploymentCases(int partition) + => HistoricalRuntimeRegressionTests.Cases().Where((_, index) => index % 6 == partition); + + [Theory, MemberData(nameof(HistoricalRuntimeDeploymentCases), 0)] + public void Isolated_HistoricalRuntimeSources0(string file, string source, string expected) + => HistoricalRuntimeDeploymentTests.AssertDeployment(file, source, expected); + + [Theory, MemberData(nameof(HistoricalRuntimeDeploymentCases), 1)] + public void Isolated_HistoricalRuntimeSources1(string file, string source, string expected) + => HistoricalRuntimeDeploymentTests.AssertDeployment(file, source, expected); + + [Theory, MemberData(nameof(HistoricalRuntimeDeploymentCases), 2)] + public void Isolated_HistoricalRuntimeSources2(string file, string source, string expected) + => HistoricalRuntimeDeploymentTests.AssertDeployment(file, source, expected); + + [Theory, MemberData(nameof(HistoricalRuntimeDeploymentCases), 3)] + public void Isolated_HistoricalRuntimeSources3(string file, string source, string expected) + => HistoricalRuntimeDeploymentTests.AssertDeployment(file, source, expected); + + [Theory, MemberData(nameof(HistoricalRuntimeDeploymentCases), 4)] + public void Isolated_HistoricalRuntimeSources4(string file, string source, string expected) + => HistoricalRuntimeDeploymentTests.AssertDeployment(file, source, expected); + + [Theory, MemberData(nameof(HistoricalRuntimeDeploymentCases), 5)] + public void Isolated_HistoricalRuntimeSources5(string file, string source, string expected) + => HistoricalRuntimeDeploymentTests.AssertDeployment(file, source, expected); +} diff --git a/tests/SharpTS.Tests/CompilerTests/StandaloneDllTests.cs b/tests/SharpTS.Tests/CompilerTests/StandaloneDllTests.cs index 1890d1ef6..8eee45a0f 100644 --- a/tests/SharpTS.Tests/CompilerTests/StandaloneDllTests.cs +++ b/tests/SharpTS.Tests/CompilerTests/StandaloneDllTests.cs @@ -16,7 +16,7 @@ namespace SharpTS.Tests.CompilerTests; /// /// Tests that ensure compiled DLLs remain standalone (no SharpTS.dll dependency). /// -public class StandaloneDllTests +public partial class StandaloneDllTests { [Theory] [MemberData(nameof(SharedTests.WindowsProcessShutdownTests.OriginalCases), MemberType = typeof(SharedTests.WindowsProcessShutdownTests))] @@ -4026,7 +4026,7 @@ public void Isolated_StringCoreMetadata_PreservesMethodsPrototypesAndUnicode(Dic { ["main.ts"] = "const obj:any={prefix:'P',tag(strings:any,...values:any[]){return this.prefix+':'+strings.join('|')+':'+values.join(',');}};console.log(obj.tag`a${7}b`);console.log(obj['tag']`c${8}d`);" }, - "P:a|b:7\nundefined:c|d:8\n", "main.ts", true + "P:a|b:7\nP:c|d:8\n", "main.ts", true }, new object[] { @@ -4404,7 +4404,7 @@ public void Isolated_StringCoercionMetadata_PreservesDisplayLanguageAndPrimitive { ["main.ts"] = "let hints='';const box:any=new Number(1);box[Symbol.toPrimitive]=function(hint:any){hints+=hint+';';return 4;};console.log(box+1,box==4,String(box),hints);" }, - "5 true 1 default;default;\n", "main.ts", true + "5 true 4 default;default;string;\n", "main.ts", true }, new object[] { @@ -4513,7 +4513,7 @@ public void Isolated_BoxedPrimitiveMetadata_PreservesWrappersAndDefaultHintConve { ["main.ts"] = "function parse(value:string,radix:number){return parseInt(value,radix);}console.log(parse('0xff',16),parse('10101',2),parse('zz',36),parse('-11',8),parse('10',1),parse('0X10',0));" }, - "0 21 1295 -9 NaN 16\n", "main.ts", true + "255 21 1295 -9 NaN 16\n", "main.ts", true }, new object[] { @@ -4719,7 +4719,7 @@ public void Isolated_NumberMetadata_PreservesParsingFormattingAndPrototypeBehavi { ["main.ts"] = "const sign:any=Math.sign;const pow:any=Math.pow;console.log(sign(-2),sign(0),sign(3),sign(NaN),Object.is(sign(-0),-0));console.log(pow('2','3'),pow(NaN,0),pow(-1,0.5));" }, - "-1 0 1 NaN false\n8 1 NaN\n", "main.ts", true + "-1 0 1 NaN true\n8 1 NaN\n", "main.ts", true }, new object[] { @@ -4743,7 +4743,7 @@ public void Isolated_NumberMetadata_PreservesParsingFormattingAndPrototypeBehavi { ["main.ts"] = "const m:any=Math;console.log(m.max(),m.min(),m.max(1,9,3),m.min(1,-2,3),m.max(1,NaN));console.log(Object.is(m.max(-0,0),-0),Object.is(m.min(0,-0),-0));" }, - "-Infinity Infinity 9 -2 NaN\ntrue false\n", "main.ts", true + "-Infinity Infinity 9 -2 NaN\nfalse true\n", "main.ts", true }, new object[] { @@ -4767,7 +4767,7 @@ public void Isolated_NumberMetadata_PreservesParsingFormattingAndPrototypeBehavi { ["main.ts"] = "const m:any=Math;console.log(m===globalThis.Math,m.floor===Math.floor,m.random===Math.random,m.sumPrecise===Math.sumPrecise);console.log(m.floor.name,m.floor.length,m.random.length,m.pow.length,m.hypot.length);" }, - "false true true true\nfloor 1 0 2 2\n", "main.ts", true + "true true true true\nfloor 1 0 2 2\n", "main.ts", true }, new object[] { @@ -4815,7 +4815,7 @@ public void Isolated_NumberMetadata_PreservesParsingFormattingAndPrototypeBehavi { ["main.ts"] = "const sum:any=Math.sumPrecise;console.log(sum([Infinity,1]),sum([-Infinity,1]),sum([Infinity,-Infinity]),sum([NaN,1]),sum([Number.MAX_VALUE,Number.MAX_VALUE]));" }, - "0 -Infinity NaN 0 Infinity\n", "main.ts", true + "Infinity -Infinity NaN NaN Infinity\n", "main.ts", true }, new object[] { @@ -5014,17 +5014,15 @@ public void Isolated_MathMetadata_PreservesAdaptersSummationAndSingletonBehavior "minimal", "const value=1;", "", "main.ts" }, - // Preserve the existing compatibility limitation while changing metadata ownership. new object[] { "async_static", "async function run(){const n=await Promise.resolve(7n);console.log(BigInt.asIntN(3,n),n+2n);}run().catch((e:any)=>console.log(e.name,e.message));", - "ReferenceError Undefined variable 'BigInt'.\n", "main.ts" + "-1n 9n\n", "main.ts" }, - // Preserve the existing compatibility limitation while changing metadata ownership. new object[] { "generator_literal", "function* values():Generator{yield 7n;yield 9n;}try{for(const n of values()){console.log(n*2n);}}catch(e:any){console.log('literal generator failed');}", - "literal generator failed\n", "main.ts" + "14n\n18n\n", "main.ts" }, ]; @@ -5181,11 +5179,10 @@ public void Isolated_BooleanMetadata_PreservesTruthinessAndPrototypeBehavior(str "boxed", "console.log(Number(Object(3)),Number(Object(false)),Number(Object('7')));", "3 0 7\n", "main.ts" }, - // Preserve the existing compatibility limitation while changing metadata ownership. new object[] { "exotic", "let trace='';const value:any={[Symbol.toPrimitive](hint:any){trace+=hint;return '9';}};console.log(Number(value),+value,trace);", - "NaN 9 number\n", "main.ts" + "9 9 numbernumber\n", "main.ts" }, new object[] { diff --git a/tests/SharpTS.Tests/Infrastructure/AsyncLocalConsoleRedirector.cs b/tests/SharpTS.Tests/Infrastructure/AsyncLocalConsoleRedirector.cs index af88cb169..c13f2f961 100644 --- a/tests/SharpTS.Tests/Infrastructure/AsyncLocalConsoleRedirector.cs +++ b/tests/SharpTS.Tests/Infrastructure/AsyncLocalConsoleRedirector.cs @@ -85,6 +85,27 @@ private static bool TrySetConsoleField(string fieldName, object proxy) return true; } + // Serial tests of APIs that use Console.SetOut/SetError need the same public + // writer contract as their callers. Restore the raw proxies afterward so + // parallel compiled tests retain context-local, unsynchronized routing. + public static IDisposable SynchronizeWriters() + { + var stdout = Console.Out; + var stderr = Console.Error; + Console.SetOut(stdout); + Console.SetError(stderr); + return new WriterRestorer(stdout, stderr); + } + + private sealed class WriterRestorer(TextWriter stdout, TextWriter stderr) : IDisposable + { + public void Dispose() + { + if (!TrySetConsoleField("s_out", stdout)) Console.SetOut(stdout); + if (!TrySetConsoleField("s_error", stderr)) Console.SetError(stderr); + } + } + /// /// Captures for the current logical-execution context into /// a fresh . Dispose the returned scope to restore the prior diff --git a/tests/SharpTS.Tests/SharedTests/ClassStaticDescriptorTests.cs b/tests/SharpTS.Tests/SharedTests/ClassStaticDescriptorTests.cs index 83536158e..aafb41cb8 100644 --- a/tests/SharpTS.Tests/SharedTests/ClassStaticDescriptorTests.cs +++ b/tests/SharpTS.Tests/SharedTests/ClassStaticDescriptorTests.cs @@ -20,9 +20,7 @@ class Box { static count: number = 4; } Assert.Equal("12\ntrue\n", TestHarness.Run(source, mode)); } - // #1900 owns the interpreter repair; the compiled inherited-descriptor - // failure is recorded separately from the frozen epic in #1958. - [Theory, InterpretedOnlyData] + [Theory, ModeData] public void StrictWritesRejectOwnAndInheritedNonWritableStatics(ExecutionMode mode) { const string source = """ @@ -56,8 +54,7 @@ class Box { static count = 4; } Assert.Equal("true\n4\n", TestHarness.Run(source, mode)); } - // The compiled read-modify-write path separately omits strict rejection (#1960). - [Theory, InterpretedOnlyData] + [Theory, ModeData] public void ReadModifyWriteOperationsRespectNonWritableStatic(ExecutionMode mode) { const string source = """ @@ -74,8 +71,7 @@ class Box { static count = 4; } Assert.Equal("true\ntrue\ntrue\ntrue\n4 4\n", TestHarness.Run(source, mode)); } - // Attribute-only compiled definitions have a separate pre-existing gap (#1959). - [Theory, InterpretedOnlyData] + [Theory, ModeData] public void AttributeOnlyDefinitionPreservesStaticValue(ExecutionMode mode) { const string source = """ diff --git a/tests/SharpTS.Tests/SharedTests/HistoricalRuntimeRegressionTests.cs b/tests/SharpTS.Tests/SharedTests/HistoricalRuntimeRegressionTests.cs index 067a4465b..21810164f 100644 --- a/tests/SharpTS.Tests/SharedTests/HistoricalRuntimeRegressionTests.cs +++ b/tests/SharpTS.Tests/SharedTests/HistoricalRuntimeRegressionTests.cs @@ -7,6 +7,77 @@ public sealed class HistoricalRuntimeRegressionTests { public static IEnumerable Cases() { + foreach (bool generic in new[] { false, true }) + { + string parameter = generic ? "" : ""; + string source = $$""" + class Base{{parameter}}{static count=1;} + class Middle{{parameter}} extends Base{{parameter}}{static count=4;} + class Leaf{{parameter}} extends Middle{{parameter}}{} + const b:any=Base,m:any=Middle,l:any=Leaf; + Object.defineProperty(Base,'count',{writable:false}); + l.count=9; + console.log(b.count,m.count,l.count,Object.hasOwn(Leaf,'count')); + class StrictLeaf{{parameter}} extends Middle{{parameter}}{} + const s:any=StrictLeaf; + function assign(){'use strict';s['count']=10;} + assign(); + console.log(b.count,m.count,s.count,Object.hasOwn(StrictLeaf,'count')); + function update(){'use strict';++l.count;} + update();console.log(l.count,m.count); + Object.defineProperty(Middle,'count',{writable:false}); + class Blocked{{parameter}} extends Middle{{parameter}}{} + const blocked:any=Blocked; + function reject(){'use strict';blocked.count=11;} + try{reject();}catch(e){console.log(e instanceof TypeError);} + console.log(blocked.count,Object.hasOwn(Blocked,'count')); + """; + yield return [$"issue1958-nearest-{generic}.ts", source, "1 4 9 true\n1 4 10 true\n10 4\ntrue\n4 false\n"]; + } + yield return ["issue1960-index-controls.ts", "\"use strict\";const a:any=[4];Object.defineProperty(a,'0',{writable:false});try{++a[0];}catch(e){console.log(e instanceof TypeError);}try{a[0]--;}catch(e){console.log(e instanceof TypeError);}try{a[0]+=2;}catch(e){console.log(e instanceof TypeError);}try{a[0]&&=9;}catch(e){console.log(e instanceof TypeError);}let hits=0;console.log(a[0]||=++hits,a[0]??=++hits,hits,a[0]);", "true\ntrue\ntrue\ntrue\n4 4 0 4\n"]; + yield return ["issue1960-original.ts", "\"use strict\";class Box{static count=4;}const box:any=Box;Object.defineProperty(Box,'count',{value:4,writable:false});try{box.count++;}catch(e){console.log(e instanceof TypeError);}try{box.count+=2;}catch(e){console.log(e instanceof TypeError);}try{box.count*=2;}catch(e){console.log(e instanceof TypeError);}try{box.count&&=3;}catch(e){console.log(e instanceof TypeError);}console.log(box.count,Object.getOwnPropertyDescriptor(Box,'count').value);", "true\ntrue\ntrue\ntrue\n4 4\n"]; + yield return ["issue1960-strict-controls.ts", "\"use strict\";class Box{static count=4;static zero=0;static nil:any=undefined;}const b:any=Box;for(const key of ['count','zero','nil']){Object.defineProperty(Box,key,{writable:false});}function attempt(action:any){try{action();}catch(e){console.log(e instanceof TypeError);}}attempt(()=>{'use strict';++b.count;});attempt(()=>{'use strict';b.count--;});attempt(()=>{'use strict';b.count+=2;});attempt(()=>{'use strict';b.count&&=9;});attempt(()=>{'use strict';b.zero||=1;});attempt(()=>{'use strict';b.nil??=1;});let hits=0;console.log(b.count||=++hits,b.count??=++hits,b.zero&&=++hits,hits);console.log(b.count,b.zero,b.nil);", "true\ntrue\ntrue\ntrue\ntrue\ntrue\n4 4 0 0\n4 0 undefined\n"]; + yield return ["issue1960-sloppy-controls.ts", "class Box{static count=4;}const b:any=Box;Object.defineProperty(Box,'count',{writable:false});console.log(b.count++,++b.count,b.count+=2,b.count*=2,b.count&&=3,b['count']);", "4 5 6 8 3 4\n"]; + yield return ["issue1960-state-controls.ts", "\"use strict\";class Box{static count=4;}const b:any=Box;Object.defineProperty(Box,'count',{writable:false});function* values(){'use strict';try{b.count++;}catch(e){console.log(e instanceof TypeError);}try{b.count+=2;}catch(e){console.log(e instanceof TypeError);}yield b.count;}for(const value of values()){console.log(value);}async function run(){'use strict';await Promise.resolve(1);try{++b.count;}catch(e){console.log(e instanceof TypeError);}try{b.count&&=9;}catch(e){console.log(e instanceof TypeError);}console.log(b.count);}run();", "true\ntrue\n4\ntrue\ntrue\n4\n"]; + yield return ["issue1956-original.ts", """ + function create(n: number): any { + let current: number = 0; + const object: any = { next() { return current++ + n; } }; + current = 3; + return object; + } + const object: any = create(10); + console.log(object.next(), object.next()); + console.log(Number.isNaN(create(NaN).next()), create(Infinity).next()); + """, "13 14\ntrue Infinity\n"]; + yield return ["issue1956-controls.ts", "function create(){let current:number=1;const object:any={read(){return current;},write(n:number){current=n;}};current=3;return object;}const a:any=create();const b:any=create();a.write(8);console.log(a.read(),b.read());a.write(NaN);console.log(Number.isNaN(a.read()));a.write(Infinity);console.log(a.read());function tdz(){const read:any=()=>value;try{read();}catch(e){console.log(e instanceof ReferenceError);}let value:any=4;console.log(read());}tdz();", "8 3\ntrue\nInfinity\ntrue\n4\n"]; + yield return ["issue1959-original.ts", "class Box{static count=4;}const box:any=Box;Object.defineProperty(Box,'count',{writable:false});box.count=9;console.log(box.count,Object.getOwnPropertyDescriptor(Box,'count').value);", "4 4\n"]; + yield return ["issue1959-controls.ts", "class Box{static count=4;static explicit=5;}const b:any=Box;Object.defineProperty(Box,'count',{enumerable:false});let d:any=Object.getOwnPropertyDescriptor(Box,'count');console.log(d.value,d.writable,d.enumerable,d.configurable);Object.defineProperty(Box,'count',{writable:false});b.count=9;console.log(b.count);Object.defineProperty(Box,'count',{writable:true,configurable:false});b.count=12;console.log(b.count);try{Object.defineProperty(Box,'count',{configurable:true});}catch(e){console.log(e instanceof TypeError);}Object.defineProperty(Box,'explicit',{value:undefined});console.log(b.explicit,(Object.getOwnPropertyDescriptor(Box,'explicit') as any).value);class Derived extends Box{}Object.defineProperty(Derived,'count',{writable:false});console.log((Derived as any).count,b.count);", "4 true false true\n4\n12\ntrue\nundefined undefined\nundefined 12\n"]; + yield return ["issue1958-original.ts", "\"use strict\";class Base{static count=4;}class Derived extends Base{}const derived:any=Derived;Object.defineProperty(Base,'count',{value:4,writable:false});try{derived['count']=10;}catch(e){console.log(e instanceof TypeError);}console.log((Base as any).count,derived.count,Object.hasOwn(Derived,'count'));", "true\n4 4 false\n"]; + yield return ["issue1958-controls.ts", "class Base{static count=4;static writable=5;}class Derived extends Base{}const b:any=Base;const d:any=Derived;Object.defineProperty(Base,'count',{value:4,writable:false});d.count=10;d['count']=11;d.writable=12;console.log(b.count,d.count,Object.hasOwn(Derived,'count'),b.writable,d.writable);function strictWrite(){'use strict';try{d.count=9;}catch(e){console.log(e instanceof TypeError);}}strictWrite();console.log(b.count,d.count);", "4 4 false 5 12\ntrue\n4 4\n"]; + yield return ["issue1954-original.ts", "const value:any='ab';console.log(Object.prototype.propertyIsEnumerable.call(value,'length'),Object.keys(value).join(','));", "false 0,1\n"]; + yield return ["issue1954-controls.ts", "const pie:any=Object.prototype.propertyIsEnumerable;for(const value of ['', 'ab', new String('ab')]){console.log(pie.call(value,'length'),pie.call(value,'missing'),Object.keys(value).join(','));}console.log(pie.call(new String('ab'),'0'));", "false false \nfalse false 0,1\nfalse false 0,1\ntrue\n"]; + yield return ["issue1939-original.ts", "async function run(){const n=await Promise.resolve(7n);console.log(BigInt.asIntN(3,n),n+2n);}run().catch((e:any)=>console.log(e.name,e.message));", "-1n 9n\n"]; + yield return ["issue1939-call-control.ts", "async function run(){const n=await Promise.resolve(7);console.log(BigInt(n));}run().catch((e:any)=>console.log(e.name,e.message));", "7n\n"]; + yield return ["issue1939-controls.ts", "async function run(){console.log(BigInt('8'));await Promise.resolve(1);const B:any=BigInt;console.log(B===BigInt,B('9'),B.asIntN(3,7n),B.asUintN(3,-1n));}run().catch((e:any)=>console.log(e.name,e.message));", "8n\ntrue 9n -1n 7n\n"]; + yield return ["issue1940-original.ts", "function* values():Generator{yield 7n;yield 9n;}try{for(const n of values()){console.log(n*2n);}}catch(e:any){console.log('literal generator failed');}", "14n\n18n\n"]; + yield return ["issue1940-controls.ts", "function* values():Generator{yield BigInt('7');yield BigInt('9');yield 9223372036854775808n;yield -9223372036854775809n;}for(const n of values()){console.log(n);}", "7n\n9n\n9223372036854775808n\n-9223372036854775809n\n"]; + yield return ["issue1938-original.ts", "const m:any=Math;console.log(m.max(),m.min(),m.max(1,9,3),m.min(1,-2,3),m.max(1,NaN));console.log(Object.is(m.max(-0,0),-0),Object.is(m.min(0,-0),-0));", "-Infinity Infinity 9 -2 NaN\nfalse true\n"]; + yield return ["issue1938-controls.ts", "const min:any=Math.min;const max:any=Math.max;console.log(Object.is(Math.max(-0,0),0),Object.is(Math.max(0,-0),0),Object.is(Math.min(-0,0),-0),Object.is(Math.min(0,-0),-0));console.log(Object.is(max(-0,0),0),Object.is(max(0,-0),0),Object.is(min(-0,0),-0),Object.is(min(0,-0),-0));console.log(min(),max(),min(1,NaN),max(NaN,1),min(7,2,3),max(7,2,3));", "true true true true\ntrue true true true\nInfinity -Infinity NaN NaN 2 7\n"]; + yield return ["issue1937-original.ts", "const sign:any=Math.sign;const pow:any=Math.pow;console.log(sign(-2),sign(0),sign(3),sign(NaN),Object.is(sign(-0),-0));console.log(pow('2','3'),pow(NaN,0),pow(-1,0.5));", "-1 0 1 NaN true\n8 1 NaN\n"]; + yield return ["issue1937-controls.ts", "const sign:any=Math.sign;console.log(Object.is(Math.sign(-0),-0),Object.is(sign(-0),-0),Object.is(Math.sign(0),0),Object.is(sign(0),0));console.log(sign('-0'),Object.is(sign('-0'),-0),sign(Infinity),sign(-Infinity),sign(NaN));", "true true true true\n0 true 1 -1 NaN\n"]; + yield return ["issue1936-original.ts", "const sum:any=Math.sumPrecise;console.log(sum([Infinity,1]),sum([-Infinity,1]),sum([Infinity,-Infinity]),sum([NaN,1]),sum([Number.MAX_VALUE,Number.MAX_VALUE]));", "Infinity -Infinity NaN NaN Infinity\n"]; + yield return ["issue1936-controls.ts", "const math:any=Math;const sum:any=math.sumPrecise;console.log(math.sumPrecise([Infinity,1]),math.sumPrecise([NaN,1]),math.sumPrecise([1,2,3]),sum([1,2,3]));console.log(sum([1e16,1,-1e16]),Object.is(sum([]),-0),Object.is(sum([-0,-0]),-0));", "Infinity NaN 6 6\n1 true true\n"]; + yield return ["issue1935-original.ts", "const m:any=Math;console.log(m===globalThis.Math,m.floor===Math.floor,m.random===Math.random,m.sumPrecise===Math.sumPrecise);console.log(m.floor.name,m.floor.length,m.random.length,m.pow.length,m.hypot.length);", "true true true true\nfloor 1 0 2 2\n"]; + yield return ["issue1935-controls.ts", "const root:any=globalThis;const m:any=Math;console.log(m===root.Math,m===globalThis['Math'],root.Math.floor===Math.floor);m.extra=7;console.log(root.Math.extra,globalThis.Math===m);", "true true true\n7 true\n"]; + yield return ["issue1934-original.ts", "function parse(value:string,radix:number){return parseInt(value,radix);}console.log(parse('0xff',16),parse('10101',2),parse('zz',36),parse('-11',8),parse('10',1),parse('0X10',0));", "255 21 1295 -9 NaN 16\n"]; + yield return ["issue1934-controls.ts", "function parse(value:string,radix:number){return parseInt(value,radix);}const alias:any=parseInt;console.log(parse('+0X10',16),parse('-0xff',16),parse('ff',16),alias('0xff',16),Number.parseInt('0X10',16),parse('0xff',10));console.log(parse('0x',16),parse('-0X',0));", "16 -255 255 255 16 0\nNaN NaN\n"]; + yield return ["issue1933-original.ts", "let trace='';const value:any={[Symbol.toPrimitive](hint:any){trace+=hint;return '9';}};console.log(Number(value),+value,trace);", "9 9 numbernumber\n"]; + yield return ["issue1933-controls.ts", "let trace='';const value:any={[Symbol.toPrimitive](hint:any){trace+=hint;return '9';}};console.log(Number(value),trace);console.log(Number(42n));const bad:any={[Symbol.toPrimitive](){return {};}};try{Number(bad);}catch(e:any){console.log(e.name);}", "9 number\n42\nTypeError\n"]; + yield return ["issue1932-original.ts", "let hints='';const box:any=new Number(1);box[Symbol.toPrimitive]=function(hint:any){hints+=hint+';';return 4;};console.log(box+1,box==4,String(box),hints);", "5 true 4 default;default;string;\n"]; + yield return ["issue1932-controls.ts", "const box:any=new Number(2);console.log(String(box));box.toString=function(){return 'own';};console.log(String(box));box[Symbol.toPrimitive]=function(hint:any){console.log(hint,this===box);return 5;};console.log(String(box));box[Symbol.toPrimitive]=function(){return {};};try{String(box);}catch(e:any){console.log(e.name);}", "2\nown\nstring true\n5\nTypeError\n"]; + yield return ["issue1931-original.ts", "const obj:any={prefix:'P',tag(strings:any,...values:any[]){return this.prefix+':'+strings.join('|')+':'+values.join(',');}};console.log(obj.tag`a${7}b`);console.log(obj['tag']`c${8}d`);", "P:a|b:7\nP:c|d:8\n"]; + yield return ["issue1931-controls.ts", "let trace='';const key='tag';const obj:any={prefix:'P',[key](strings:any,...values:any[]){trace+='call;';return this.prefix+':'+strings.join('|')+':'+values.join(',');}};function receiver(){trace+='receiver;';return obj;}function index(){trace+='index;';return key;}function value(n:number){trace+='value'+n+';';return n;}console.log(receiver()[index()]`a${value(1)}b${value(2)}c`);console.log(trace);", "P:a|b|c:1,2\nreceiver;index;value1;value2;call;\n"]; yield return ["issue1953-original.ts", "const value:any='ab';const d:any=Object.getOwnPropertyDescriptor(value,'length');console.log(d.value,d.writable,d.enumerable,d.configurable);console.log(Object.prototype.propertyIsEnumerable.call(value,'length'),Object.keys(value).join(','));", "2 false false false\nfalse 0,1\n"]; yield return ["issue1953-controls.ts", "for(const value of ['', 'ab',new String('ab')]){const d:any=Object.getOwnPropertyDescriptor(value,'length');console.log(d.value,d.writable,d.enumerable,d.configurable);}console.log(Object.getOwnPropertyDescriptor('ab','missing')===undefined,Object.keys('ab').join(','));", "0 false false false\n2 false false false\n2 false false false\ntrue 0,1\n"]; yield return ["issue1952-original.ts", "new Promise((resolve:any,reject:any)=>{for(const fn of [resolve,reject]){const d:any=Object.getOwnPropertyDescriptor(fn,'name');console.log(d.value,d.writable,d.enumerable,d.configurable);console.log(Object.prototype.propertyIsEnumerable.call(fn,'name'),Object.keys(fn).join(','));}resolve(1);});", " false false true\nfalse \n false false true\nfalse \n"]; @@ -83,7 +154,18 @@ public static IEnumerable ApiCases() { foreach (var item in Cases()) foreach (var mode in new[] { ExecutionMode.Interpreted, ExecutionMode.Compiled }) + { + // #1933 explicitly separates the interpreter's unary-plus discrepancy. + // Its Number(value) control remains dual-mode; preserve the complete + // original source and reference for compiled API/deployment execution. + // #1934 similarly separates the interpreter's radix-36 discrepancy. + // The extra #1960 indexed-array probe exercises compiled PutValue; + // interpreter array-index descriptor enforcement is a separate gap. + // The original static-field source and sloppy controls remain dual-mode. + if (item[0] is "issue1933-original.ts" or "issue1934-original.ts" or "issue1960-index-controls.ts" && mode == ExecutionMode.Interpreted) + continue; yield return [..item, mode]; + } } [Theory, MemberData(nameof(ApiCases))] @@ -97,11 +179,6 @@ public void PreservedSourcesMatchReference(string file, string source, string ex internal static void AssertReferenceOutput(string file, string expected, string actual) { - // #1953 scopes acceptance to the first descriptor line. Preserve the - // complete source and Node reference; its second-line predicate is #1954. - if (file == "issue1953-original.ts") - Assert.Equal(expected.Split('\n')[0], actual.Split('\n')[0]); - else - Assert.Equal(expected, actual); + Assert.Equal(expected, actual); } } diff --git a/tests/SharpTS.Tests/SharpTS.Tests.csproj b/tests/SharpTS.Tests/SharpTS.Tests.csproj index 47cb4b96a..34d404a24 100644 --- a/tests/SharpTS.Tests/SharpTS.Tests.csproj +++ b/tests/SharpTS.Tests/SharpTS.Tests.csproj @@ -41,6 +41,7 @@ + diff --git a/tests/fixtures/SharpTS.ConsoleRestorationFixture/Program.cs b/tests/fixtures/SharpTS.ConsoleRestorationFixture/Program.cs new file mode 100644 index 000000000..1309e946c --- /dev/null +++ b/tests/fixtures/SharpTS.ConsoleRestorationFixture/Program.cs @@ -0,0 +1,80 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; + +namespace SharpTS.ConsoleRestorationFixture; + +public static class ConsoleRestorationFixtureMarker { } + +internal static class Program +{ + private static int Main(string[] args) + { + TextWriter report = Console.Out; + TextWriter errors = Console.Error; + try + { + string assemblyPath = Path.GetFullPath(args.Single()); + string directory = Path.GetDirectoryName(assemblyPath)!; + // Resolve the real test assembly's dependencies from its output, + // without a project reference that would introduce a build cycle. + AssemblyLoadContext.Default.Resolving += (_, name) => + { + string dependency = Path.Combine(directory, name.Name + ".dll"); + return File.Exists(dependency) ? Assembly.LoadFrom(dependency) : null; + }; + Assembly tests = Assembly.LoadFrom(assemblyPath); + RuntimeHelpers.RunModuleConstructor(tests.ManifestModule.ModuleHandle); + Type contract = tests.GetType("SharpTS.Tests.Compilation.CompilationServiceTests", throwOnError: true)!; + TextWriter rawOut = Console.Out; + TextWriter rawErr = Console.Error; + if (rawOut.GetType().Name != "ProxyWriter" || rawErr.GetType().Name != "ProxyWriter") + throw new InvalidOperationException("The actual test module initializer did not install raw console proxies."); + + RunContract(contract); + AssertWriters(rawOut, rawErr); + + using var publicOut = new StringWriter(); + using var publicErr = new StringWriter(); + Console.SetOut(publicOut); + Console.SetError(publicErr); + TextWriter priorOut = Console.Out; + TextWriter priorErr = Console.Error; + RunContract(contract); + AssertWriters(priorOut, priorErr); + report.WriteLine("console restoration controls passed"); + return 0; + } + catch (Exception exception) + { + // The test initializer intentionally mutes unscoped stderr. + errors.WriteLine(exception); + return 1; + } + } + + private static void RunContract(Type contract) + { + using var instance = (IDisposable)Activator.CreateInstance(contract)!; + TextWriter priorOut = Console.Out; + TextWriter priorErr = Console.Error; + foreach (string method in new[] + { + "Execute_RestoresConsoleAfterRun", // First and repeated executions, exact output and writer identities. + "Execute_GuestThrow_ReturnsFailedRunResultWithoutThrowing", + "Execute_InvalidAssembly_ReportsFailedLoadAndAggregateTime", + "Execute_InfiniteLoop_CancellationUnwindsCooperatively" + }) + { + if (contract.GetMethod(method)!.Invoke(instance, null) is Task task) + task.GetAwaiter().GetResult(); + AssertWriters(priorOut, priorErr); + } + } + + private static void AssertWriters(TextWriter stdout, TextWriter stderr) + { + if (!ReferenceEquals(stdout, Console.Out) || !ReferenceEquals(stderr, Console.Error)) + throw new InvalidOperationException("Console stdout/stderr identities were not restored."); + } +} diff --git a/tests/fixtures/SharpTS.ConsoleRestorationFixture/SharpTS.ConsoleRestorationFixture.csproj b/tests/fixtures/SharpTS.ConsoleRestorationFixture/SharpTS.ConsoleRestorationFixture.csproj new file mode 100644 index 000000000..6fec3c7c4 --- /dev/null +++ b/tests/fixtures/SharpTS.ConsoleRestorationFixture/SharpTS.ConsoleRestorationFixture.csproj @@ -0,0 +1,6 @@ + + + Exe + false + +