From fc3d32ba28b226c7430a0440526e782f4dd6fc7d Mon Sep 17 00:00:00 2001 From: Pavel Koneski Date: Thu, 22 Oct 2026 15:12:44 -0700 Subject: [PATCH 1/2] Don't give a class body a function's call target on sys.settrace --- src/core/IronPython/Runtime/FunctionCode.cs | 24 +++++++++++++++---- .../suite/modules/system_related/test_sys.py | 15 ++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/core/IronPython/Runtime/FunctionCode.cs b/src/core/IronPython/Runtime/FunctionCode.cs index f46e8b1b8..c3204451f 100644 --- a/src/core/IronPython/Runtime/FunctionCode.cs +++ b/src/core/IronPython/Runtime/FunctionCode.cs @@ -687,11 +687,9 @@ internal void UpdateDelegate(PythonContext context, bool forceCreation) { if (context.EnableTracing && _lambda != null) { if (_tracingLambda == null) { if (!forceCreation) { - if (_lambda is Compiler.Ast.ClassDefinition) return; // ClassDefinition does not have the same signature as call targets // the user just called sys.settrace(), don't force re-compilation of every method in the system. Instead // we'll just re-compile them as they're invoked. - PythonCallTargets.GetPythonTargetType(_lambda.ParameterNames.Length > PythonCallTargets.MaxArgs, _lambda.ParameterNames.Length, out Delegate target); - SetTarget(target); + SetLazyTarget(); return; } _tracingLambda = GetGeneratorOrNormalLambdaTracing(context); @@ -708,8 +706,7 @@ internal void UpdateDelegate(PythonContext context, bool forceCreation) { // we cannot create the delegate when forceCreation is false because we hold the // _CodeCreateAndUpdateDelegateLock and compiling the delegate may create a FunctionCode // object which requires the lock. - PythonCallTargets.GetPythonTargetType(_lambda.ParameterNames.Length > PythonCallTargets.MaxArgs, _lambda.ParameterNames.Length, out Delegate target); - SetTarget(target); + SetLazyTarget(); return; } _normalDelegate = CompileLambda(GetGeneratorOrNormalLambda(), new TargetUpdaterForCompilation(context, this).SetCompiledTarget); @@ -723,6 +720,23 @@ internal void UpdateDelegate(PythonContext context, bool forceCreation) { SetTarget(finalTarget); } + /// + /// Sets a target that creates the delegate for the current mode (tracing or not) when it is first needed. + /// + /// + /// A function gets a call target that compiles its delegate on the first call. A class body cannot get one, since its + /// delegate has a different signature (Func<CodeContext, CodeContext>); it gets a null target instead, as when it + /// has not been compiled yet, which makes compile it the next time the class is created. + /// + private void SetLazyTarget() { + if (_lambda is Compiler.Ast.ClassDefinition) { + SetTarget(null); + } else { + PythonCallTargets.GetPythonTargetType(_lambda.ParameterNames.Length > PythonCallTargets.MaxArgs, _lambda.ParameterNames.Length, out Delegate target); + SetTarget(target); + } + } + /// /// Called to set the initial target delegate when the user has passed -X:Debug to enable /// .NET style debugging. diff --git a/tests/suite/modules/system_related/test_sys.py b/tests/suite/modules/system_related/test_sys.py index fdc38dfa7..f48396e9a 100644 --- a/tests/suite/modules/system_related/test_sys.py +++ b/tests/suite/modules/system_related/test_sys.py @@ -218,6 +218,21 @@ def g(): sys.settrace(None) + def test_class_body_after_settrace_none(self): + """A class body that has only run traced still runs after sys.settrace(None).""" + def f(): + class Boom(Exception): pass + return Boom + + sys.settrace(lambda *args: None) + try: + f() + finally: + sys.settrace(None) + # Before the fix, this failed (intermittently) with a TypeError about a Func`2[PythonFunction,Object] cast: + # sys.settrace(None) gave the class body a function's call target. + self.assertTrue(issubclass(f(), Exception)) + @skipUnlessIronPython() @unittest.expectedFailure # dummy getrefcount removed in https://github.com/IronLanguages/ironpython3/pull/577 def test_getrefcount(self): From 02618e1488e86c2466be8caa0c8cc9ca6a2f345e Mon Sep 17 00:00:00 2001 From: Pavel Koneski Date: Fri, 23 Oct 2026 14:34:01 -0700 Subject: [PATCH 2/2] Read FunctionCode.Target only once when running code --- src/core/IronPython/Runtime/FunctionCode.cs | 27 ++++++++++++------- .../Runtime/Operations/PythonOps.cs | 8 +++--- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/core/IronPython/Runtime/FunctionCode.cs b/src/core/IronPython/Runtime/FunctionCode.cs index c3204451f..f80df1d40 100644 --- a/src/core/IronPython/Runtime/FunctionCode.cs +++ b/src/core/IronPython/Runtime/FunctionCode.cs @@ -584,19 +584,21 @@ internal object Call(CodeContext/*!*/ context) { throw PythonOps.TypeError("cannot exec code object that contains free variables: {0}", co_freevars.__repr__(context)); } - if (Target == null || (Target.GetMethodInfo()?.DeclaringType == typeof(PythonCallTargets))) { - UpdateDelegate(context.LanguageContext, true); + // Target is read only once, since switching tracing on or off on another thread may change it at any time. + Delegate target = Target; + if (target == null || (target.GetMethodInfo()?.DeclaringType == typeof(PythonCallTargets))) { + target = UpdateDelegate(context.LanguageContext, true); } - if (Target is Func classTarget) { + if (target is Func classTarget) { return classTarget(context); } - if (Target is LookupCompilationDelegate moduleCode) { + if (target is LookupCompilationDelegate moduleCode) { return moduleCode(context, this); } - if (Target is Func optimizedModuleCode) { + if (target is Func optimizedModuleCode) { return optimizedModuleCode(this); } @@ -678,10 +680,14 @@ internal void LazyCompileFirstTarget(PythonFunction function) { } /// - /// Updates the delegate based upon current Python context settings for recursion enforcement - /// and for tracing. + /// Updates the delegate based upon current Python context settings for recursion enforcement and for tracing. /// - internal void UpdateDelegate(PythonContext context, bool forceCreation) { + /// + /// The delegate set as the target, or null if only a lazy target was set (only when is false). + /// A caller that needs the delegate should use this result rather than read again: another thread may change + /// it at any time, e.g. by switching tracing on or off. + /// + internal Delegate UpdateDelegate(PythonContext context, bool forceCreation) { Delegate finalTarget; if (context.EnableTracing && _lambda != null) { @@ -690,7 +696,7 @@ internal void UpdateDelegate(PythonContext context, bool forceCreation) { // the user just called sys.settrace(), don't force re-compilation of every method in the system. Instead // we'll just re-compile them as they're invoked. SetLazyTarget(); - return; + return null; } _tracingLambda = GetGeneratorOrNormalLambdaTracing(context); } @@ -707,7 +713,7 @@ internal void UpdateDelegate(PythonContext context, bool forceCreation) { // _CodeCreateAndUpdateDelegateLock and compiling the delegate may create a FunctionCode // object which requires the lock. SetLazyTarget(); - return; + return null; } _normalDelegate = CompileLambda(GetGeneratorOrNormalLambda(), new TargetUpdaterForCompilation(context, this).SetCompiledTarget); } @@ -718,6 +724,7 @@ internal void UpdateDelegate(PythonContext context, bool forceCreation) { finalTarget = AddRecursionCheck(context, finalTarget); SetTarget(finalTarget); + return finalTarget; } /// diff --git a/src/core/IronPython/Runtime/Operations/PythonOps.cs b/src/core/IronPython/Runtime/Operations/PythonOps.cs index 8c3540c12..e9c5969aa 100644 --- a/src/core/IronPython/Runtime/Operations/PythonOps.cs +++ b/src/core/IronPython/Runtime/Operations/PythonOps.cs @@ -1589,10 +1589,10 @@ public static void InitializeForFinalization(CodeContext/*!*/ context, object ne static Func GetClassCode(CodeContext/*!*/ context, FunctionCode funcCode, Func body) { if (body == null) { - if (funcCode.Target == null) { - funcCode.UpdateDelegate(context.LanguageContext, true); - } - return (Func)funcCode.Target!; + // Target is read only once, since switching tracing on or off on another thread may reset it to null at any time + // (see FunctionCode.UpdateDelegate). If it is null, compile and use the delegate that compilation returns. + var target = (Func?)funcCode.Target; + return target ?? (Func)funcCode.UpdateDelegate(context.LanguageContext, true); } else { if (funcCode.Target == null) { funcCode.SetTarget(body);