@@ -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 {
0 commit comments