Skip to content

Convert funceval to UCO pattern#126809

Open
am11 wants to merge 34 commits intodotnet:mainfrom
am11:feature/MDCS-to-UCOA-pattern2
Open

Convert funceval to UCO pattern#126809
am11 wants to merge 34 commits intodotnet:mainfrom
am11:feature/MDCS-to-UCOA-pattern2

Conversation

@am11
Copy link
Copy Markdown
Member

@am11 am11 commented Apr 12, 2026

Built on top of #126542.

Last part of #123864 before the final cleanups.

@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Apr 12, 2026
@am11
Copy link
Copy Markdown
Member Author

am11 commented Apr 12, 2026

cc @jkotas, @janvorli

Tested it with this program in VSCode:

Program.cs

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

// Test various funceval scenarios that exercise DoNormalFuncEval
var dt = new DateTime(2026, 4, 12);
var list = new System.Collections.Generic.List<int> { 1, 2, 3 };
var str = "hello funceval";
int x = 42;
double pi = 3.14;
bool flag = true;
var dict = new System.Collections.Generic.Dictionary<string, int> { ["one"] = 1, ["two"] = 2 };
var arr = new int[] { 10, 20, 30 };
object boxed = 99;
var guid = Guid.NewGuid();
var span = "hello".AsSpan(); // not directly evaluable but interesting
var tuple = (Name: "Alice", Age: 30);

// Breakpoint here - then evaluate expressions in Watch/Immediate window:
//
// === Instance methods on value types (boxes 'this') ===
//   dt.ToString()              → value type instance method
//   dt.AddDays(1)              → value type method with arg, returns value type
//   guid.ToString()            → Guid.ToString()
//   pi.ToString("F1")          → double method with string arg
//   tuple.ToString()           → ValueTuple instance method
//
// === Instance methods on ref types ===
//   str.Length                  → string property (handle-based 'this')
//   str.Contains("func")       → string method with string arg
//   str.Substring(6, 4)        → string method with two int args
//   list.Count                 → generic ref type property
//   list.Contains(2)           → generic method with boxed int arg
//   list[0]                    → indexer (get_Item)
//   dict["one"]                → dictionary indexer
//   dict.ContainsKey("two")    → dictionary method
//   arr.Length                 → array property
//
// === Primitive 'this' (box + call) ===
//   x.ToString()               → int.ToString()
//   x.GetType()                → int.GetType() returns System.Int32
//   flag.ToString()            → bool.ToString()
//   x.CompareTo(10)            → int.CompareTo(int)
//
// === Static methods (this = null) ===
//   string.Concat("a", "b")   → static with ref type args
//   string.IsNullOrEmpty("")   → static with ref type arg
//   int.Parse("123")           → static returning primitive
//   Math.Max(3, 7)             → static with two primitive args
//   DateTime.UtcNow            → static property returning value type
//   Environment.ProcessId      → static property returning int
//   RuntimeInformation.FrameworkDescription → static property returning string
//   Guid.NewGuid()             → static method returning value type
//   Convert.ToInt32("42")      → static with string arg returning int
//
// === NEW_OBJECT (constructor) ===
//   new DateTime(2000, 1, 1)   → value type ctor with primitive args
//   new List<int>()            → ref type default ctor
//   new string('x', 5)        → string ctor with char + int
//   new object()               → simplest ctor
//
// === Boxed object ===
//   boxed.ToString()           → call on boxed int (ELEMENT_TYPE_OBJECT)
//   boxed.GetType()            → GetType on boxed int
//
Debugger.Break();

Console.WriteLine($"dt={dt}, list.Count={list.Count}, str={str}, x={x}");
Console.WriteLine($"dict={dict.Count}, arr={arr.Length}, guid={guid}, pi={pi}, flag={flag}");

launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "funceval-test (branch)",
            "type": "coreclr",
            "request": "launch",
            "program": "c:\\temp\\runtime\\artifacts\\bin\\testhost\\net11.0-windows-Release-x64\\dotnet.exe",
            "args": ["exec", "C:\\temp\\funceval-test\\bin\\Debug\\net11.0\\funceval-test.dll"],
            "cwd": "${workspaceFolder}",
            "stopAtEntry": false,
            "env": {
                "DOTNET_ROOT": "c:\\temp\\runtime\\artifacts\\bin\\testhost\\net11.0-windows-Release-x64"
            }
        },
        {
            "name": "funceval-test (main)",
            "type": "coreclr",
            "request": "launch",
            "program": "c:\\temp\\runtime\\runtime2\\artifacts\\bin\\testhost\\net11.0-windows-Release-x64\\dotnet.exe",
            "args": ["exec", "C:\\temp\\funceval-test\\bin\\Debug\\net11.0\\funceval-test.dll"],
            "cwd": "${workspaceFolder}",
            "stopAtEntry": false,
            "env": {
                "DOTNET_ROOT": "c:\\temp\\runtime\\runtime2\\artifacts\\bin\\testhost\\net11.0-windows-Release-x64"
            }
        }
    ]
}

Results match main branch:

PR Main

@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@jkotas
Copy link
Copy Markdown
Member

jkotas commented Apr 12, 2026

Built on top of #126542.

Does it depend on any of the changes in #126542 given that it just calls the public MethodInfo.Invoke API?

@am11
Copy link
Copy Markdown
Member Author

am11 commented Apr 12, 2026

Built on top of #126542.

Does it depend on any of the changes in #126542 given that it just calls the public MethodInfo.Invoke API?

#126542 moved InvokeMethod to managed and this is tested end to end that way. I can cherry-pick ffa8a51 on main if we want this one to go in first.

@jkotas
Copy link
Copy Markdown
Member

jkotas commented Apr 12, 2026

I am not sure which one will go first. If these changes are independent, it would make more sense to submit them as independent PRs.

Copy link
Copy Markdown
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the Microsoft maintainers will have to run internal VS debugger tests on this before it gets merged.

Comment thread src/coreclr/debug/ee/funceval.cpp Outdated
Comment thread src/coreclr/debug/ee/funceval.cpp
Comment thread src/coreclr/debug/ee/funceval.cpp Outdated
Comment thread src/coreclr/debug/ee/funceval.cpp Outdated
@am11
Copy link
Copy Markdown
Member Author

am11 commented Apr 14, 2026

Failures are known, the new one is unrelated deadlettered leg. Results still match before/after #126809 (comment)

@janvorli
Copy link
Copy Markdown
Member

Let me run the private diagnostic tests with this change.

Comment thread src/coreclr/debug/ee/funceval.cpp Outdated
Comment thread src/coreclr/debug/ee/funceval.cpp Outdated
@janvorli
Copy link
Copy Markdown
Member

There is about 15 failed tests with this change (compared to the commit right before your change). Moreover, I've found that we also have some additional failures in these tests that one of your previous changes - #126222 has introduced. Since I have run those tests at one point during your PR, it seems that some changes made after that has introduced the problem.
Let me get my head around these failures and then get back to you.

@janvorli
Copy link
Copy Markdown
Member

So, regarding the issues introduced by the old PR, the DacDbiInterfaceImpl::UnwindStackWalkFrame needs to be updated to skip frames of "System.Environment.CallEntryPoint". I've tried a quick hack (just comparing method name string) and found that fixes two of the three EH related failures.
We probably also want to skip it in the ProfilerStackWalkCallback, DebuggerStepper::IsInterestingFrame and maybe handle it in the DacDbiInterfaceImpl::GetStackWalkCurrentFrameInfo too (create a new enum entry in FrameType, return it from there and handle it in CordbStackWalk::GetFrameWorker). See my old PR that ported the NativeAOT EH, you'll see where I've added this handling.

The remaining issue from the old PR is strange. The debugger can see a stack where the System.Environment.CallEntryPoint is at the top of the stack, with Main at the bottom. That doesn't make sense, I am looking at the test to see if I can isolate a standalone repro.

Copy link
Copy Markdown
Member

@janvorli janvorli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you!

@jkotas
Copy link
Copy Markdown
Member

jkotas commented Apr 16, 2026

@dotnet/dotnet-diag-contrib Any concerns with this change?

It is switching funceval to use reflection invoke. It makes the code a lot simpler and fully portable that is going to help with the wasm port.

Copy link
Copy Markdown
Member

@noahfalk noahfalk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a great simplification. Looking over it it appears to be doing the right things. There are lots of subtleties I'm unlikely to catch just by reading the code so I'm also relying heavily on the testing you did @janvorli (thanks!)

@tommcdon - is there any additional testing we could do on this, perhaps VS, that would help flush out lingering issues?

@am11
Copy link
Copy Markdown
Member Author

am11 commented Apr 17, 2026

Thanks, will keep an eye out post-merge for any fallouts after daily SDK is released. (I think folks will be able to test conveniently after the signed binaries are out)

@janvorli
Copy link
Copy Markdown
Member

@jkotas, @am11 - @tommcdon has ran our Visual Studio tests suite that is more extensive than the diagnostic tests I've ran and found that this change breaks multiple tests there. He's going to look into those failures, so please let's hold on until he figures out what's wrong.

@am11
Copy link
Copy Markdown
Member Author

am11 commented Apr 20, 2026

Tested after merging main, seems to be working for most vals

image

If I know what type of cases have the problem, I can try to debug and fix them.

@am11
Copy link
Copy Markdown
Member Author

am11 commented Apr 21, 2026

@janvorli, @tommcdon, a note: this branch should be built from clean state (i.e. after deleting artifacts dir). I noticed that cmake or ninja doesn't reinstall some DAC related stuff if we built main before rebuilding this branch and we run into mismatch error when using debugger in VS with $env:VSDebugger_ValidateDotnetDebugLibSignatures=0. In all previous screenshots, I built clr from clean state.

@am11
Copy link
Copy Markdown
Member Author

am11 commented Apr 27, 2026

@janvorli, would it be possible to share the failing test scenarios so I can investigate? If not, I can close this PR and
leave it for diagnostics team who have access to tests. I tried various method calls (static, instance) and lambdas and they seem to be working in VS watch and immediate windows. I upgraded to VS2026 last week.

@janvorli
Copy link
Copy Markdown
Member

@am11 I know @tommcdon has been actively working on figuring out the issues. @tommcdon do you happen to have any details you could share?

@tommcdon
Copy link
Copy Markdown
Member

tommcdon commented May 1, 2026

@am11 I know @tommcdon has been actively working on figuring out the issues. @tommcdon do you happen to have any details you could share?

Hi @aml11! I apologize for the delay! Here's a summary of the issues that we found:

  1. Wrong ICorDebug frame type during nested func-eval

Nested break state shows a STUBFRAME_LIGHTWEIGHT_FUNCTION frame instead of STUBFRAME_FUNC_EVAL frame around the func-eval method invocation.

Repro case:

// Simpler repro via Visual Studio:
//   1. Open this project in VS 2022/2026 (with your custom .NET 11 runtime)
//   2. Set a breakpoint on the first line after the curly brace in Return42 --  ("int i = 0; i++;")
//   3. Run; pause at first Debugger.Break() (the one in Main)
//   4. In VS Immediate Window, evaluate: Return42()
//      (VS uses "all threads evaluate" mode for Immediate, which allows nested breakpoints)
//   5. The breakpoint inside Return42 fires; open the Call Stack window
//      Expected call stack entry: " Evaluation of: Return42()"
//      Actual call stack entry:   " [Lightweight Function]"

Debugger.Break(); // Pause here, then evaluate Return42() in the Immediate window
                  // with a breakpoint set inside Return42

Console.WriteLine(Return42());

static int Return42()
{
    int i = 0; i++; // <-- set breakpoint here; check call stack during func-eval
    return 42;
}
  1. Span<T>.Slice() func-eval throws NotSupportedException

Evaluating test.AsSpan().Slice(0, 2) (or any expression involving Span<T> return values or parameters) throws System.NotSupportedException: "Specified method is not supported." instead of returning the expected System.Span<int>[2].

// Repro steps (Visual Studio):
//   1. Attach VS 2022/2026 with your custom runtime
//   2. Run the program — it pauses at Debugger.Break()
//   3. Open Immediate Window (Debug > Windows > Immediate) or Watch window
//   4. Evaluate:  test.AsSpan().Slice(0, 2)
//
// Expected result:
//   System.Span<int>[2]   (a span containing elements 0 and 1)
//
// Actual result (with UCO commit):
//   'test.AsSpan().Slice(0, 2)' threw an exception of type 'System.NotSupportedException'
//   Message: "Specified method is not supported."
//
// Root cause hypothesis:
//   InvokeFuncEval (the UCO trampoline) uses MethodBase.Invoke() to call the target method.
//   Span<T> is a ref struct and cannot be boxed into object[]. When funceval.cpp attempts to
//   box the Span<T> arguments or return value for the object[] array, it fails.
//   The old PackArgumentArray / ARG_SLOT-based approach handled ref structs directly.

using System.Diagnostics;

int[] test = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

// Pause here. Evaluate: test.AsSpan().Slice(0, 2)
// Expected: System.Span<int>[2]
// Actual:   throws System.NotSupportedException
Debugger.Break();

Console.WriteLine("Done");
  1. Value-type constructor func-eval returns stale value

Evaluating new DateTime(2077, 5, 24, 3, 20, 52, DateTimeKind.Utc) in the debugger returns {12/18/2019 3:20:52 AM} — the pre-existing value of the local date variable — instead of the newly constructed value {5/24/2077 3:20:52 AM}.

Similarly, new decimal(5.0) returns 2 (the pre-existing value) instead of 5.

// Repro steps (Visual Studio):
//   1. Attach VS 2022/2026 with your custom runtime
//   2. Run the program — it pauses at Debugger.Break()
//   3. Open Immediate Window (Debug > Windows > Immediate) or Watch window
//   4. Evaluate each expression below and check the result
//
// Test 1 - DateTime constructor:
//   Evaluate:   date = new DateTime(2077, 5, 24, 3, 20, 52, DateTimeKind.Utc)
//   Expected:   {5/24/2077 3:20:52 AM}
//   Actual:     {12/18/2019 3:20:52 AM}   <-- stale pre-existing value
//
// Test 2 - decimal constructor:
//   Evaluate:   d = new decimal(5.0)
//   Expected:   5
//   Actual:     2                          <-- stale pre-existing value
//
// Root cause hypothesis:
//   InvokeFuncEval calls ConstructorInfo.Invoke(thisObj, args) where thisObj is the
//   pre-allocated boxed value. For value type constructors, MethodBase.Invoke may not
//   mutate the pre-allocated box in-place (it may box/unbox internally, losing changes),
//   so pDE->m_result still holds the original object's data.
//
// NOTE: The returned EvalResult shows the OLD value (before construction), proving
//       that the constructor ran on a different copy or the box was not updated.

using System.Diagnostics;

DateTime date = new DateTime(2019, 12, 18, 3, 20, 52, DateTimeKind.Utc);
decimal d = new decimal(2.0);

// Pause here. Evaluate:
//   date = new DateTime(2077, 5, 24, 3, 20, 52, DateTimeKind.Utc)   -> expected {5/24/2077 3:20:52 AM}
//   d = new decimal(5.0)                                             -> expected 5
Debugger.Break();

Console.WriteLine($"date = {date}, d = {d}");

@am11
Copy link
Copy Markdown
Member Author

am11 commented May 1, 2026

Thanks for sharing clear repro @tommcdon. I was able to repro it. I have pushed a commit with fixes. Now Span.Slice, stale assignment and extra frames issues are fixed. Please let me know if you find some other fallouts.

pBufferForArgsArray
DEBUG_ARG(pDataLocationArray)
);
// Byref-like args (e.g. Span<T>) need special handling: we cannot route them through
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@noahfalk How does CordbValue works for byref-like types (e.g. Span)? I do not see anything that protects the byref fields in these types when they are wrapped by CorDbValue. Is that a bug or am I missing some invariant that makes it unnecessary?

@am11
Copy link
Copy Markdown
Member Author

am11 commented May 5, 2026

@janvorli, @tommcdon, could you please rerun the tests? We can also land this and refine it later if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Diagnostics-coreclr community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants