Skip to content

Commit ecb7228

Browse files
committed
debugger help
1 parent 5cc4aa8 commit ecb7228

7 files changed

Lines changed: 495 additions & 49 deletions

File tree

FadeBasic/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.0.59] - 2026-04-1
9+
### Fixed
10+
- debugger shows hover values when looking at struct fields and array fields
11+
- debugger statement eval shows output
12+
813
## [0.0.58] - 2026-03-24
914
### Fixed
1015
- debugger handles suspended vm correctly

FadeBasic/DAP/FadeDebugAdapter_Eval.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,14 @@ protected override void HandleEvaluateRequestAsync(IRequestResponder<EvaluateArg
7575
}
7676
else
7777
{
78-
res.Result = "";
78+
res.Result = result.value ?? "";
79+
res.Type = result.type;
80+
if (result.fieldCount > 0 || result.elementCount > 0)
81+
{
82+
res.VariablesReference = result.scope?.id ?? 0;
83+
if (result.scope != null)
84+
db.AddScope(-1, result.scope);
85+
}
7986
}
8087
responder.SetResponse(res);
8188
});

FadeBasic/FadeBasic/Launch/DebugSession.cs

Lines changed: 224 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -565,8 +565,27 @@ public DebugEvalResult Eval(int frameId, string rightHandExpression, int overwri
565565

566566
const string SYNTHETIC_NAME2 = "fade________eval";
567567

568-
// Hover requests from the editor send only the word under the cursor, which may be
569-
// truncated: "y" instead of "y$", or "e" instead of "c.e". Resolve to the best
568+
// VS Code's debug hover may send partial expressions like ".e" (dot-prefixed field)
569+
// or "1).e" when it can't fully resolve the dotted path through parenthesized
570+
// array indexing. Strip leading junk to get the bare field name.
571+
if (rightHandExpression.Length > 0)
572+
{
573+
var dotIdx = rightHandExpression.LastIndexOf('.');
574+
if (dotIdx >= 0 && dotIdx < rightHandExpression.Length - 1)
575+
{
576+
var afterDot = rightHandExpression.Substring(dotIdx + 1);
577+
// If the part before the dot is NOT a valid identifier (e.g. "1)", "."),
578+
// treat it as a bare field hover and use only the part after the last dot.
579+
var beforeDot = rightHandExpression.Substring(0, dotIdx);
580+
if ((beforeDot.Length == 0 || !char.IsLetter(beforeDot[0]))
581+
&& afterDot.Length > 0 && char.IsLetter(afterDot[0]))
582+
{
583+
rightHandExpression = afterDot;
584+
}
585+
}
586+
}
587+
588+
// Hover requests from the editor send the word under the cursor. Resolve to the best
570589
// known eval-name before proceeding so the rest of Eval sees the correct expression.
571590
if (variableDb.TryResolveHoverExpression(rightHandExpression, out var resolved))
572591
rightHandExpression = resolved;
@@ -926,20 +945,171 @@ void AddVariable(DebugVariable local, bool isGlobal)
926945
elementCount = match.elementCount,
927946
id = match.id,
928947
};
929-
948+
930949
if (quickResult.fieldCount > 0 || quickResult.elementCount > 0)
931950
{
932951
quickResult.scope = variableDb.Expand(match.id);
933952
}
934953
}
935954

936-
if (parseErrors.Count > 0)
955+
// VS Code's default word pattern strips sigils ($/#), so "name$" becomes "name".
956+
// This helper compares a stored field name against a lookup name with sigil tolerance.
957+
bool FieldNameMatches(string storedName, string lookupName)
937958
{
938-
if (parseErrors[0].errorCode.code == ErrorCodes.ImplicitArrayDeclaration.code)
959+
if (string.Equals(storedName, lookupName, StringComparison.OrdinalIgnoreCase))
960+
return true;
961+
if (storedName.Length > 0 && (storedName[storedName.Length - 1] == '$' || storedName[storedName.Length - 1] == '#'))
962+
return string.Equals(storedName.Substring(0, storedName.Length - 1), lookupName, StringComparison.OrdinalIgnoreCase);
963+
return false;
964+
}
965+
966+
// For struct field references (e.g. "p.name$" or "people(0).name$"), walk the
967+
// left chain to find the root variable, then expand each level through the
968+
// variable database to read the live value directly.
969+
if (quickResult == null && finalStatement.expression is StructFieldReference fieldRef)
970+
{
971+
// Collect the field chain from right to left.
972+
var fieldChain = new List<string>();
973+
IExpressionNode current = fieldRef;
974+
while (current is StructFieldReference sfr)
939975
{
940-
if (quickResult != null) return quickResult;
976+
if (sfr.right is VariableRefNode rightVar)
977+
fieldChain.Add(rightVar.variableName);
978+
current = sfr.left;
941979
}
942-
980+
981+
// current is either a plain variable (VariableRefNode) or an array access (ArrayIndexReference)
982+
string rootName = null;
983+
int arrayIndex = -1;
984+
if (current is ArrayIndexReference arrayRef)
985+
{
986+
rootName = arrayRef.variableName;
987+
// Try to extract a constant index from the first rank expression
988+
if (arrayRef.rankExpressions.Count > 0 && arrayRef.rankExpressions[0] is LiteralIntExpression litInt)
989+
arrayIndex = litInt.value;
990+
}
991+
else if (current is VariableRefNode rootVar)
992+
{
993+
rootName = rootVar.variableName;
994+
}
995+
996+
if (rootName != null)
997+
{
998+
var rootMatch = locals.variables.FirstOrDefault(x => x.name == rootName)
999+
?? globals.variables.FirstOrDefault(x => x.name == rootName);
1000+
1001+
if (rootMatch != null && (rootMatch.fieldCount > 0 || (rootMatch.elementCount > 0 && arrayIndex >= 0)))
1002+
{
1003+
Launch.DebugVariable currentVar = rootMatch;
1004+
1005+
// If the root is an array, expand into the array and pick the element
1006+
if (arrayIndex >= 0 && currentVar.elementCount > 0)
1007+
{
1008+
var arrayScope = variableDb.Expand(currentVar.id);
1009+
if (arrayIndex < arrayScope.variables.Count)
1010+
currentVar = arrayScope.variables[arrayIndex];
1011+
else
1012+
currentVar = null;
1013+
}
1014+
1015+
// Walk down the field chain, expanding at each level
1016+
bool fieldResolved = currentVar != null;
1017+
if (fieldResolved)
1018+
{
1019+
for (int i = fieldChain.Count - 1; i >= 0; i--)
1020+
{
1021+
var scope = variableDb.Expand(currentVar.id);
1022+
var fieldMatch = scope.variables.FirstOrDefault(
1023+
v => FieldNameMatches(v.name, fieldChain[i]));
1024+
if (fieldMatch == null)
1025+
{
1026+
fieldResolved = false;
1027+
break;
1028+
}
1029+
currentVar = fieldMatch;
1030+
}
1031+
}
1032+
1033+
if (fieldResolved)
1034+
{
1035+
quickResult = new DebugEvalResult
1036+
{
1037+
value = currentVar.value,
1038+
type = currentVar.type,
1039+
fieldCount = currentVar.fieldCount,
1040+
elementCount = currentVar.elementCount,
1041+
id = currentVar.id,
1042+
};
1043+
if (quickResult.fieldCount > 0 || quickResult.elementCount > 0)
1044+
{
1045+
quickResult.scope = variableDb.Expand(currentVar.id);
1046+
}
1047+
}
1048+
}
1049+
}
1050+
}
1051+
1052+
// When hovering over a bare field name (e.g. "field" from "myStruct.field"),
1053+
// VS Code sends just the word under the cursor. If it didn't match any
1054+
// local/global variable, check whether it's a field on a struct in scope.
1055+
if (quickResult == null && match == null && finalStatement.expression is VariableRefNode bareField)
1056+
{
1057+
string resolvedFieldExpr = null;
1058+
bool fieldAmbiguous = false;
1059+
1060+
foreach (var v in locals.variables.Concat(globals.variables))
1061+
{
1062+
if (v.fieldCount > 0)
1063+
{
1064+
// Direct struct variable
1065+
var subScope = variableDb.Expand(v.id);
1066+
foreach (var sv in subScope.variables)
1067+
{
1068+
if (FieldNameMatches(sv.name, bareField.variableName))
1069+
{
1070+
if (resolvedFieldExpr != null) { fieldAmbiguous = true; break; }
1071+
resolvedFieldExpr = v.name + "." + sv.name;
1072+
}
1073+
}
1074+
}
1075+
else if (v.elementCount > 0)
1076+
{
1077+
// Array — check if it's an array-of-structs by expanding
1078+
// to get elements and checking if first element has fields.
1079+
var arrayScope = variableDb.Expand(v.id);
1080+
if (arrayScope.variables.Count > 0 && arrayScope.variables[0].fieldCount > 0)
1081+
{
1082+
var elemScope = variableDb.Expand(arrayScope.variables[0].id);
1083+
foreach (var sv in elemScope.variables)
1084+
{
1085+
if (FieldNameMatches(sv.name, bareField.variableName))
1086+
{
1087+
if (resolvedFieldExpr != null) { fieldAmbiguous = true; break; }
1088+
// Use element 0 as best guess — we don't know the index
1089+
resolvedFieldExpr = v.name + "(0)." + sv.name;
1090+
}
1091+
}
1092+
}
1093+
}
1094+
if (fieldAmbiguous) break;
1095+
}
1096+
1097+
if (resolvedFieldExpr != null)
1098+
{
1099+
// When ambiguous (field exists on multiple structs), use the first
1100+
// match rather than returning an error — showing some value is better
1101+
// than "Invalid Reference" on hover.
1102+
return Eval(frameId, resolvedFieldExpr, overwriteVariableId);
1103+
}
1104+
}
1105+
1106+
if (parseErrors.Count > 0)
1107+
{
1108+
// If the quick-result path already resolved the value (e.g. via struct
1109+
// field expansion), return it even if the scope error visitor reported
1110+
// errors like "Member not declared" or "Invalid Reference".
1111+
if (quickResult != null) return quickResult;
1112+
9431113
return DebugEvalResult.Failed($"{string.Join(",\n", parseErrors.Select(x => x.errorCode))}");
9441114
}
9451115

@@ -1157,6 +1327,26 @@ public DebugEvalResult ReplExec(int frameId, string code)
11571327
var parser = new Parser(lexResults.stream, _commandCollection);
11581328
var node = parser.ParseProgram(new ParseOptions { ignoreChecks = true });
11591329

1330+
// If parsing produced errors or the result is a bare expression,
1331+
// delegate to Eval which already handles type inference correctly.
1332+
// This handles cases like "x", "x + 1", "alan.x" that the parser
1333+
// can't treat as standalone statements.
1334+
var earlyErrors = node.GetAllErrors();
1335+
bool isBareExpr = (node.statements.Count > 0 && node.statements[node.statements.Count - 1] is ExpressionStatement);
1336+
if (isBareExpr || earlyErrors.Count > 0)
1337+
{
1338+
try
1339+
{
1340+
var evalResult = Eval(frameId, code);
1341+
if (evalResult != null && evalResult.id >= 0)
1342+
return evalResult;
1343+
}
1344+
catch
1345+
{
1346+
// If Eval also fails, fall through and let REPL report its own error.
1347+
}
1348+
}
1349+
11601350
// Capture the user's statements BEFORE variable-context declarations are injected.
11611351
var userStatements = new List<IStatementNode>(node.statements);
11621352

@@ -1470,6 +1660,33 @@ void AddVariable(DebugVariable local, bool isGlobal)
14701660
variableDb.InvalidateLocalScope(frameId);
14711661

14721662
result = new DebugEvalResult { value = "", type = "void", id = 0 };
1663+
1664+
// Try to produce a useful result for the debug console.
1665+
// If the last user statement was an assignment (or a bare expression
1666+
// rewritten to a synthetic assignment), read back the variable's value
1667+
// directly from the VM registers.
1668+
if (userStatements.Count > 0)
1669+
{
1670+
var lastStmt = userStatements[userStatements.Count - 1];
1671+
if (lastStmt is AssignmentStatement assign && assign.variable is VariableRefNode varRef)
1672+
{
1673+
var varName = varRef.variableName;
1674+
if (mergedVariableTable.TryGetValue(varName, out var compiledVar))
1675+
{
1676+
var scopeIdx = compiledVar.isGlobal ? 0 : vmScopeIdxForRepl;
1677+
var rawValue = _vm.scopeStack.buffer[scopeIdx].dataRegisters[compiledVar.registerAddress];
1678+
var tc = compiledVar.typeCode;
1679+
VmUtil.TryGetVariableTypeDisplay(tc, out var typeName);
1680+
1681+
result = new DebugEvalResult
1682+
{
1683+
value = VmUtil.ConvertRawToDisplayString(tc, rawValue, _vm.heap),
1684+
type = typeName,
1685+
id = 0
1686+
};
1687+
}
1688+
}
1689+
}
14731690
}
14741691
finally
14751692
{

FadeBasic/FadeBasic/Virtual/DebugUtil.cs

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -49,44 +49,9 @@ public DebugRuntimeVariable(VirtualMachine vm, string name, byte typeCode, ulong
4949

5050
public string GetValueDisplay()
5151
{
52-
switch (typeCode)
53-
{
54-
case TypeCodes.INT:
55-
return VmUtil.ConvertToInt(rawValue).ToString();
56-
case TypeCodes.DINT:
57-
return VmUtil.ConvertToDInt(rawValue).ToString();
58-
case TypeCodes.REAL:
59-
return VmUtil.ConvertToFloat(rawValue).ToString();
60-
case TypeCodes.DFLOAT:
61-
return VmUtil.ConvertToDFloat(rawValue).ToString();
62-
case TypeCodes.WORD:
63-
return VmUtil.ConvertToWord(rawValue).ToString();
64-
case TypeCodes.DWORD:
65-
return VmUtil.ConvertToDWord(rawValue).ToString();
66-
case TypeCodes.BYTE:
67-
return VmUtil.ConvertToByte(rawValue).ToString();
68-
case TypeCodes.STRING:
69-
var address = VmPtr.FromRaw(rawValue);
70-
if (vm.heap.TryGetAllocationSize(address, out var strSize))
71-
{
72-
vm.heap.Read(address, strSize, out var strBytes);
73-
return VmConverter.ToString(strBytes);
74-
}
75-
else
76-
{
77-
return "<?>";
78-
}
79-
case TypeCodes.BOOL:
80-
return rawValue == 0 ? "false" : "true";
81-
case TypeCodes.STRUCT:
82-
// LOOK UP IN HEAP?
83-
return "[" + GetTypeName() + "]";
84-
85-
break;
86-
default:
87-
return rawValue.ToString();
88-
}
89-
52+
if (typeCode == TypeCodes.STRUCT)
53+
return "[" + GetTypeName() + "]";
54+
return VmUtil.ConvertRawToDisplayString(typeCode, rawValue, vm.heap);
9055
}
9156

9257
public InternedType GetInternedType()

FadeBasic/FadeBasic/Virtual/VmUtil.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,40 @@ public static string ConvertValueToDisplayString(byte typeCode, VirtualMachine v
220220
throw new NotImplementedException($"don't know how to convert span of size=[{span.Length}] to typecode=[{typeCode}]");
221221
}
222222
}
223-
223+
224+
public static string ConvertRawToDisplayString(byte typeCode, ulong rawValue, VmHeap heap)
225+
{
226+
switch (typeCode)
227+
{
228+
case TypeCodes.INT:
229+
return ConvertToInt(rawValue).ToString();
230+
case TypeCodes.DINT:
231+
return ConvertToDInt(rawValue).ToString();
232+
case TypeCodes.REAL:
233+
return ConvertToFloat(rawValue).ToString();
234+
case TypeCodes.DFLOAT:
235+
return ConvertToDFloat(rawValue).ToString();
236+
case TypeCodes.WORD:
237+
return ConvertToWord(rawValue).ToString();
238+
case TypeCodes.DWORD:
239+
return ConvertToDWord(rawValue).ToString();
240+
case TypeCodes.BYTE:
241+
return ConvertToByte(rawValue).ToString();
242+
case TypeCodes.BOOL:
243+
return rawValue == 0 ? "false" : "true";
244+
case TypeCodes.STRING:
245+
var address = VmPtr.FromRaw(rawValue);
246+
if (heap.TryGetAllocationSize(address, out var strSize))
247+
{
248+
heap.Read(address, strSize, out var strBytes);
249+
return VmConverter.ToString(strBytes);
250+
}
251+
return "<?>";
252+
default:
253+
return rawValue.ToString();
254+
}
255+
}
256+
224257
[MethodImpl(MethodImplOptions.AggressiveInlining)]
225258
public static object DbgConvert(byte typeCode, ref ReadOnlySpan<byte> values)
226259
{

0 commit comments

Comments
 (0)