Skip to content
21 changes: 17 additions & 4 deletions src/NodeDev.Blazor/DiagramsModels/GraphNodeWidgetPort.razor
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,22 @@

private string GetDebugValue(GraphPortModel port)
{
if (!GraphCanvas.Graph.Project.IsLiveDebuggingEnabled || port.Connection.GraphIndex == -1)
return port.Connection.Type.FriendlyName;

return GraphCanvas.DebuggedPathService.GetDebugValue(port.Connection);
// Check if we're paused at a breakpoint in hard debugging mode
if (GraphCanvas.Graph.Project.IsPausedAtBreakpoint)
{
var (value, success) = GraphCanvas.Graph.Project.GetVariableValueAtBreakpoint(port.Connection.Id);
if (success)
return value;
// If not successful (e.g., variable not in current scope), fall through to show type
}

// Check for live debugging (soft debugging with GraphExecutor)
if (GraphCanvas.Graph.Project.IsLiveDebuggingEnabled && port.Connection.GraphIndex >= 0)
{
return GraphCanvas.DebuggedPathService.GetDebugValue(port.Connection);
}

// Default: just show the type name
return port.Connection.Type.FriendlyName;
}
}
17 changes: 13 additions & 4 deletions src/NodeDev.Core/Class/RoslynNodeClassCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class RoslynNodeClassCompiler
private readonly Project _project;
private readonly BuildOptions _options;
private readonly List<NodeBreakpointInfo> _allBreakpoints = new();
private readonly List<ConnectionVariableMapping> _allVariableMappings = new();

public RoslynNodeClassCompiler(Project project, BuildOptions options)
{
Expand All @@ -31,8 +32,9 @@ public RoslynNodeClassCompiler(Project project, BuildOptions options)
/// </summary>
public CompilationResult Compile()
{
// Clear breakpoints from previous compilation
// Clear breakpoints and variable mappings from previous compilation
_allBreakpoints.Clear();
_allVariableMappings.Clear();

// Generate the compilation unit (full source code)
var compilationUnit = GenerateCompilationUnit();
Expand Down Expand Up @@ -112,7 +114,13 @@ public CompilationResult Compile()
SourceFilePath = syntaxTree.FilePath
};

return new CompilationResult(assembly, sourceText, peStream.ToArray(), pdbStream.ToArray(), breakpointMappingInfo);
// Create variable mapping info
var variableMappingInfo = new VariableMappingInfo
{
Mappings = _allVariableMappings
};

return new CompilationResult(assembly, sourceText, peStream.ToArray(), pdbStream.ToArray(), breakpointMappingInfo, variableMappingInfo);
}

/// <summary>
Expand Down Expand Up @@ -210,8 +218,9 @@ private MethodDeclarationSyntax GenerateMethod(NodeClassMethod method)
var builder = new RoslynGraphBuilder(method.Graph, _options.BuildExpressionOptions.RaiseNodeExecutedEvents);
var methodSyntax = builder.BuildMethod();

// Collect breakpoint mappings from the builder's context
// Collect breakpoint mappings and variable mappings from the builder's context
_allBreakpoints.AddRange(builder.GetBreakpointMappings());
_allVariableMappings.AddRange(builder.GetVariableMappings());

return methodSyntax;
}
Expand Down Expand Up @@ -246,7 +255,7 @@ private List<MetadataReference> GetMetadataReferences()
/// <summary>
/// Result of a Roslyn compilation
/// </summary>
public record CompilationResult(Assembly Assembly, string SourceCode, byte[] PEBytes, byte[] PDBBytes, BreakpointMappingInfo BreakpointMappings);
public record CompilationResult(Assembly Assembly, string SourceCode, byte[] PEBytes, byte[] PDBBytes, BreakpointMappingInfo BreakpointMappings, VariableMappingInfo VariableMappings);

/// <summary>
/// Exception thrown when compilation fails
Expand Down
35 changes: 35 additions & 0 deletions src/NodeDev.Core/CodeGeneration/GenerationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public class GenerationContext
private readonly HashSet<string> _usedVariableNames = new();
private int _uniqueCounter = 0;

// Track variable mappings for debugging
private readonly List<ConnectionVariableMapping> _variableMappings = new();
private string? _currentClassName;
private string? _currentMethodName;

public GenerationContext(bool isDebug)
{
IsDebug = isDebug;
Expand All @@ -33,6 +38,21 @@ public GenerationContext(bool isDebug)
/// </summary>
public List<NodeBreakpointInfo> BreakpointMappings { get; } = new();

/// <summary>
/// Collection of connection-to-variable mappings for debugging.
/// </summary>
public List<ConnectionVariableMapping> VariableMappings => _variableMappings;

/// <summary>
/// Sets the current class and method being generated.
/// Used for variable mapping tracking.
/// </summary>
public void SetCurrentMethod(string className, string methodName)
{
_currentClassName = className;
_currentMethodName = methodName;
}

/// <summary>
/// Gets the variable name for a connection, or null if not yet registered
/// </summary>
Expand All @@ -48,6 +68,21 @@ public GenerationContext(bool isDebug)
public void RegisterVariableName(Connection connection, string variableName)
{
_connectionToVariableName[connection.Id] = variableName;

// Track this mapping for debugging (if we have method context)
if (_currentClassName != null && _currentMethodName != null)
{
// Note: SlotIndex will be -1 initially, as we don't know it until after compilation
// It could be determined later by analyzing the PDB, but for now we'll use variable name lookup
_variableMappings.Add(new ConnectionVariableMapping
{
ConnectionId = connection.Id,
VariableName = variableName,
SlotIndex = -1, // Unknown at code generation time
ClassName = _currentClassName,
MethodName = _currentMethodName
});
}
}

/// <summary>
Expand Down
12 changes: 9 additions & 3 deletions src/NodeDev.Core/CodeGeneration/RoslynGraphBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,22 @@ public RoslynGraphBuilder(Graph graph, GenerationContext context)
/// </summary>
public List<NodeBreakpointInfo> GetBreakpointMappings() => _context.BreakpointMappings;

/// <summary>
/// Gets the variable mappings collected during code generation.
/// </summary>
public List<ConnectionVariableMapping> GetVariableMappings() => _context.VariableMappings;

/// <summary>
/// Builds a complete method syntax from the graph
/// </summary>
public MethodDeclarationSyntax BuildMethod()
{
var method = _graph.SelfMethod;

// Set the current method in context for variable mapping
string fullClassName = $"{_graph.SelfClass.Namespace}.{_graph.SelfClass.Name}";
_context.SetCurrentMethod(fullClassName, method.Name);

// Find the entry node
var entryNode = _graph.Nodes.Values.FirstOrDefault(x => x is EntryNode)
?? throw new Exception($"No entry node found in graph {method.Name}");
Expand Down Expand Up @@ -95,9 +104,6 @@ public MethodDeclarationSyntax BuildMethod()
// Build the execution flow starting from entry
var chunks = _graph.GetChunks(entryOutput, allowDeadEnd: false);

// Get full class name for breakpoint info
string fullClassName = $"{_graph.SelfClass.Namespace}.{_graph.SelfClass.Name}";

// In debug builds, always track line numbers for all nodes (not just those with breakpoints)
// This allows breakpoints to be set dynamically during debugging
var bodyStatements = _context.IsDebug
Expand Down
175 changes: 175 additions & 0 deletions src/NodeDev.Core/Debugger/DebugSessionEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,181 @@ internal void OnDebugCallback(DebugCallbackEventArgs args)
DebugCallback?.Invoke(this, args);
}

/// <summary>
/// Gets the value of a local variable from the current active thread's top frame.
/// This should be called when the debugger is paused (e.g., at a breakpoint).
/// Uses ICorDebug to inspect the actual runtime value of the variable.
/// </summary>
/// <param name="variableName">The name of the local variable to inspect.</param>
/// <returns>A tuple containing the value as a string and a flag indicating success.</returns>
public (string Value, bool Success) GetLocalVariableValue(string variableName)
{
ThrowIfDisposed();

if (CurrentProcess == null)
return ("Not attached", false);

try
{
// Get all threads in the process
var threads = CurrentProcess.Threads?.ToArray();
if (threads == null || threads.Length == 0)
return ("No threads found", false);

// Get the first thread (typically the main thread that hit the breakpoint)
var thread = threads[0];

// Get the active chain (call stack)
var chains = thread.Chains?.ToArray();
if (chains == null || chains.Length == 0)
return ("No chains found", false);

var chain = chains[0];

// Get frames in the chain
var frames = chain.Frames?.ToArray();
if (frames == null || frames.Length == 0)
return ("No frames found", false);

// Get the top frame (current execution point)
var frame = frames[0];

// Try to create an IL frame from the frame
// CorDebugILFrame wraps ICorDebugILFrame
CorDebugILFrame? ilFrame = null;
try
{
ilFrame = new CorDebugILFrame(frame.Raw as ICorDebugILFrame);
}
catch
{
return ("Frame is not an IL frame", false);
}

if (ilFrame == null)
return ("Frame is not an IL frame", false);

// Try to find the variable by enumerating locals
// For now, try slots 0-10 (common case)
for (int slot = 0; slot < 10; slot++)
{
try
{
var localValue = ilFrame.GetLocalVariable(slot);
if (localValue != null)
{
// Get the value as string
var valueStr = GetValueAsString(localValue);

// For demonstration, return the first valid local variable
// In a full implementation, we'd match slot index to variable name using metadata
return (valueStr, true);
}
}
catch
{
// Slot might not exist, continue
continue;
}
}

return ($"Variable '{variableName}' not found in local slots", false);
}
catch (Exception ex)
{
return ($"Error: {ex.Message}", false);
}
}

/// <summary>
/// Converts an ICorDebugValue to a string representation.
/// </summary>
private string GetValueAsString(CorDebugValue value)
{
try
{
// Try to dereference if it's a reference value
var refValue = value.As<CorDebugReferenceValue>();
if (refValue != null && !refValue.IsNull)
{
try
{
value = refValue.Dereference();
}
catch
{
// If dereferencing fails, use original value
}
}

// Try to get as a generic value (primitives like int, bool, etc.)
var genericValue = value.As<CorDebugGenericValue>();
if (genericValue != null)
{
var size = (int)value.Size;
var buffer = Marshal.AllocHGlobal(size);
try
{
genericValue.GetValue(buffer);
var bytes = new byte[size];
Marshal.Copy(buffer, bytes, 0, size);

// Interpret based on element type
var type = value.Type;
return type switch
{
CorElementType.Boolean => BitConverter.ToBoolean(bytes, 0).ToString(),
CorElementType.I1 => ((sbyte)bytes[0]).ToString(),
CorElementType.U1 => bytes[0].ToString(),
CorElementType.I2 => BitConverter.ToInt16(bytes, 0).ToString(),
CorElementType.U2 => BitConverter.ToUInt16(bytes, 0).ToString(),
CorElementType.I4 => BitConverter.ToInt32(bytes, 0).ToString(),
CorElementType.U4 => BitConverter.ToUInt32(bytes, 0).ToString(),
CorElementType.I8 => BitConverter.ToInt64(bytes, 0).ToString(),
CorElementType.U8 => BitConverter.ToUInt64(bytes, 0).ToString(),
CorElementType.R4 => BitConverter.ToSingle(bytes, 0).ToString(),
CorElementType.R8 => BitConverter.ToDouble(bytes, 0).ToString(),
CorElementType.Char => BitConverter.ToChar(bytes, 0).ToString(),
_ => $"<{type}>"
};
}
finally
{
Marshal.FreeHGlobal(buffer);
}
}

// Try to get as a string value
var stringValue = value.As<CorDebugStringValue>();
if (stringValue != null)
{
var length = (int)stringValue.Length;
var str = stringValue.GetString(length);
return $"\"{str}\"";
}

// Try to get as an object value
var objectValue = value.As<CorDebugObjectValue>();
if (objectValue != null)
{
return $"<object>";
}

// Try to get as an array value
var arrayValue = value.As<CorDebugArrayValue>();
if (arrayValue != null)
{
return $"<array[{arrayValue.Count}]>";
}

return $"<{value.Type}>";
}
catch (Exception ex)
{
return $"<error: {ex.Message}>";
}
}

private void EnsureInitialized()
{
if (_dbgShim == null)
Expand Down
Loading
Loading