Browse Source

feat: Thread inspection support

Matthias Ladkau 3 years ago
parent
commit
d8056737ea

BIN
ecal-support/Screenshot_2020-11-16_22-53-34.png


+ 111 - 19
ecal-support/src/ecalDebugAdapter.ts

@@ -15,12 +15,16 @@ import {
   InitializedEvent,
   BreakpointEvent,
   StoppedEvent,
+  StackFrame,
+  Scope,
+  Variable,
 } from "vscode-debugadapter";
 import { DebugProtocol } from "vscode-debugprotocol";
 import { WaitGroup } from "@jpwilliams/waitgroup";
 import { ECALDebugClient } from "./ecalDebugClient";
 import * as vscode from "vscode";
 import { ClientBreakEvent, DebugStatus } from "./types";
+import * as path from "path";
 
 /**
  * ECALDebugArguments are the arguments which VSCode can pass to the debug adapter.
@@ -65,9 +69,6 @@ export class ECALDebugSession extends LoggingDebugSession {
 
   private unconfirmedBreakpoints: DebugProtocol.Breakpoint[] = [];
 
-  private bpCount: number = 1;
-  private bpIds: Record<string, number> = {};
-
   public sendEvent(event: DebugProtocol.Event): void {
     super.sendEvent(event);
     console.error("#### Sending event:", event);
@@ -224,6 +225,7 @@ export class ECALDebugSession extends LoggingDebugSession {
       if (status) {
         breakpoints = (args.lines || []).map((line) => {
           const breakpointString = `${sourcePath}:${line}`;
+
           const bp: DebugProtocol.Breakpoint = new Breakpoint(
             status.breakpoints[breakpointString],
             line,
@@ -231,11 +233,13 @@ export class ECALDebugSession extends LoggingDebugSession {
             new Source(breakpointString, args.source.path)
           );
           bp.id = this.getBreakPointId(breakpointString);
+
           return bp;
         });
       } else {
-        for (const sbp of args.breakpoints || []) {
+        breakpoints = (args.breakpoints || []).map((sbp) => {
           const breakpointString = `${sourcePath}:${sbp.line}`;
+
           const bp: DebugProtocol.Breakpoint = new Breakpoint(
             false,
             sbp.line,
@@ -243,13 +247,11 @@ export class ECALDebugSession extends LoggingDebugSession {
             new Source(breakpointString, args.source.path)
           );
           bp.id = this.getBreakPointId(breakpointString);
-          breakpoints.push(bp);
-        }
+
+          return bp;
+        });
+
         this.unconfirmedBreakpoints = breakpoints;
-        console.log(
-          "Breakpoints to be confirmed:",
-          this.unconfirmedBreakpoints
-        );
       }
     }
 
@@ -319,18 +321,73 @@ export class ECALDebugSession extends LoggingDebugSession {
     this.sendResponse(response);
   }
 
-  protected stackTraceRequest(
+  private frameVariableScopes: Record<number, Record<string, any>> = {};
+
+  protected async stackTraceRequest(
     response: DebugProtocol.StackTraceResponse,
     args: DebugProtocol.StackTraceArguments
-  ): void {
-    console.error("##### stackTraceRequest:", args);
+  ) {
+    const stackFrames: StackFrame[] = [];
+    console.log("##### stackTraceRequest:", args);
+
+    const status = await this.client.status();
+    const threadStatus = status?.threads[String(args.threadId)];
+
+    if (threadStatus?.threadRunning === false) {
+      const ins = await this.client.describe(args.threadId);
+
+      if (ins) {
+        // Update the global variable scope
+
+        this.frameVariableScopes[1] = ins.callStackVs![0];
+
+        for (const [i, sf] of ins.callStack.entries()) {
+          const sfNode = ins.callStackNode![i];
+          const frameId = this.getStackFrameId(args.threadId, sf, i);
+          const breakpointString = `${sfNode.source}:${sfNode.line}`;
+
+          stackFrames.unshift(
+            new StackFrame(
+              frameId,
+              sf,
+              new Source(
+                breakpointString,
+                path.join(this.config.dir, sfNode.source)
+              ),
+              sfNode.line
+            )
+          );
+          this.frameVariableScopes[frameId] = ins.callStackVs![i];
+        }
+
+        const frameId = this.getStackFrameId(args.threadId, ins.code!, ins.callStack.length);
+        const breakpointString = `${ins.node!.source}:${ins.node!.line}`;
+
+        stackFrames.unshift(
+          new StackFrame(
+            frameId,
+            ins.code!,
+            new Source(
+              breakpointString,
+              path.join(this.config.dir, ins.node!.source)
+            ),
+            ins.node!.line
+          )
+        );
+        this.frameVariableScopes[frameId] = ins.vs!;
+      }
+    }
+
+    console.log("##### stackTraceRequest response", stackFrames);
 
     response.body = {
-      stackFrames: [],
+      stackFrames,
     };
     this.sendResponse(response);
   }
 
+  // TODO ############################
+
   protected scopesRequest(
     response: DebugProtocol.ScopesResponse,
     args: DebugProtocol.ScopesArguments
@@ -338,20 +395,29 @@ export class ECALDebugSession extends LoggingDebugSession {
     console.error("##### scopesRequest:", args);
 
     response.body = {
-      scopes: [],
+      scopes: [new Scope("Local", args.frameId), new Scope("Global", 1)],
     };
+
     this.sendResponse(response);
   }
 
   protected async variablesRequest(
     response: DebugProtocol.VariablesResponse,
-    args: DebugProtocol.VariablesArguments,
-    request?: DebugProtocol.Request
+    args: DebugProtocol.VariablesArguments
   ) {
-    console.error("##### variablesRequest", args, request);
+    console.error("##### variablesRequest", args);
+
+    let variables: Variable[] = [];
+    const vs = this.frameVariableScopes[args.variablesReference];
+
+    if (vs) {
+      for (const [name, val] of Object.entries(vs)) {
+        variables.push(new Variable(name, String(val)));
+      }
+    }
 
     response.body = {
-      variables: [],
+      variables,
     };
     this.sendResponse(response);
   }
@@ -540,6 +606,12 @@ export class ECALDebugSession extends LoggingDebugSession {
       });
   }
 
+  // Id functions
+  // ============
+
+  private bpCount: number = 1;
+  private bpIds: Record<string, number> = {};
+
   /**
    * Map a given breakpoint string to a breakpoint ID.
    */
@@ -551,6 +623,26 @@ export class ECALDebugSession extends LoggingDebugSession {
     }
     return id;
   }
+
+  private sfCount: number = 2;
+  private sfIds: Record<string, number> = {};
+
+  /**
+   * Map a given breakpoint string to a breakpoint ID.
+   */
+  private getStackFrameId(
+    threadId: string | number,
+    frameString: string,
+    frameIndex: number,
+  ): number {
+    const storageString = `${threadId}###${frameString}###${frameIndex}`;
+    let id = this.sfIds[storageString];
+    if (!id) {
+      id = this.sfCount++;
+      this.sfIds[storageString] = id;
+    }
+    return id;
+  }
 }
 
 class LogChannelAdapter {

+ 3 - 3
ecal-support/src/ecalDebugClient.ts

@@ -54,9 +54,9 @@ export class ECALDebugClient extends EventEmitter {
     }
   }
 
-  public async inspect(tid: number): Promise<ThreadInspection | null> {
+  public async describe(tid: number): Promise<ThreadInspection | null> {
     try {
-      return (await this.sendCommand("inspect", [
+      return (await this.sendCommand("describe", [
         String(tid),
       ])) as ThreadInspection;
     } catch (e) {
@@ -105,7 +105,7 @@ export class ECALDebugClient extends EventEmitter {
           // A thread was stopped inspect it
 
           let inspection: ThreadInspection = {
-            callstack: [],
+            callStack: [],
             threadRunning: false,
           };
 

+ 11 - 9
ecal-support/src/types.ts

@@ -12,20 +12,17 @@ export interface EcalAstNode {
 }
 
 export interface ThreadInspection {
-  callstack: string[];
+  callStack: string[];
+  callStackNode?: EcalAstNode[];
+  callStackVs?: Record<string, any>[];
   threadRunning: boolean;
   code?: string;
   node?: EcalAstNode;
-  vs?: any;
-}
-
-export interface ClientBreakEvent {
-  tid: number;
-  inspection: ThreadInspection;
+  vs?: Record<string, any>;
 }
 
 export interface ThreadStatus {
-  callstack: string[];
+  callStack: string[];
   threadRunning?: boolean;
 }
 
@@ -33,7 +30,7 @@ export interface DebugStatus {
   breakonstart: boolean;
   breakpoints: Record<string, boolean>;
   sources: string[];
-  threads: Record<number, ThreadStatus>;
+  threads: Record<string, ThreadStatus>;
 }
 
 /**
@@ -43,3 +40,8 @@ export interface LogOutputStream {
   log(value: string): void;
   error(value: string): void;
 }
+
+export interface ClientBreakEvent {
+  tid: number;
+  inspection: ThreadInspection;
+}

+ 40 - 23
interpreter/debug.go

@@ -29,13 +29,14 @@ import (
 ecalDebugger is the inbuild default debugger.
 */
 type ecalDebugger struct {
-	breakPoints         map[string]bool                // Break points (active or not)
-	interrogationStates map[uint64]*interrogationState // Collection of threads which are interrogated
-	callStacks          map[uint64][]*parser.ASTNode   // Call stacks of threads
-	sources             map[string]bool                // All known sources
-	breakOnStart        bool                           // Flag to stop at the start of the next execution
-	globalScope         parser.Scope                   // Global variable scope which can be used to transfer data
-	lock                *sync.RWMutex                  // Lock for this debugger
+	breakPoints         map[string]bool                     // Break points (active or not)
+	interrogationStates map[uint64]*interrogationState      // Collection of threads which are interrogated
+	callStacks          map[uint64][]*parser.ASTNode        // Call stack locations of threads
+	callStackVs         map[uint64][]map[string]interface{} // Variable scope snapshots of threads
+	sources             map[string]bool                     // All known sources
+	breakOnStart        bool                                // Flag to stop at the start of the next execution
+	globalScope         parser.Scope                        // Global variable scope which can be used to transfer data
+	lock                *sync.RWMutex                       // Lock for this debugger
 
 }
 
@@ -89,6 +90,7 @@ func NewECALDebugger(globalVS parser.Scope) util.ECALDebugger {
 		breakPoints:         make(map[string]bool),
 		interrogationStates: make(map[uint64]*interrogationState),
 		callStacks:          make(map[uint64][]*parser.ASTNode),
+		callStackVs:         make(map[uint64][]map[string]interface{}),
 		sources:             make(map[string]bool),
 		breakOnStart:        false,
 		globalScope:         globalVS,
@@ -144,6 +146,7 @@ func (ed *ecalDebugger) VisitState(node *parser.ASTNode, vs parser.Scope, tid ui
 
 		ed.lock.Lock()
 		ed.callStacks[tid] = make([]*parser.ASTNode, 0, 10)
+		ed.callStackVs[tid] = make([]map[string]interface{}, 0, 10)
 		ed.lock.Unlock()
 	}
 
@@ -219,6 +222,7 @@ func (ed *ecalDebugger) VisitStepInState(node *parser.ASTNode, vs parser.Scope,
 	var err util.TraceableRuntimeError
 
 	threadCallStack := ed.callStacks[tid]
+	threadCallStackVs := ed.callStackVs[tid]
 
 	is, ok := ed.interrogationStates[tid]
 
@@ -242,12 +246,13 @@ func (ed *ecalDebugger) VisitStepInState(node *parser.ASTNode, vs parser.Scope,
 				is.cmd = Stop
 			case StepOver:
 				is.cmd = StepOut
-				is.stepOutStack = ed.callStacks[tid]
+				is.stepOutStack = threadCallStack
 			}
 		}
 	}
 
 	ed.callStacks[tid] = append(threadCallStack, node)
+	ed.callStackVs[tid] = append(threadCallStackVs, ed.buildVsSnapshot(vs))
 
 	return err
 }
@@ -260,6 +265,7 @@ func (ed *ecalDebugger) VisitStepOutState(node *parser.ASTNode, vs parser.Scope,
 	defer ed.lock.Unlock()
 
 	threadCallStack := ed.callStacks[tid]
+	threadCallStackVs := ed.callStackVs[tid]
 	lastIndex := len(threadCallStack) - 1
 
 	ok, cerr := threadCallStack[lastIndex].Equals(node, false) // Sanity check step in node must be the same as step out node
@@ -268,6 +274,7 @@ func (ed *ecalDebugger) VisitStepOutState(node *parser.ASTNode, vs parser.Scope,
 			threadCallStack, node, cerr))
 
 	ed.callStacks[tid] = threadCallStack[:lastIndex] // Remove the last item
+	ed.callStackVs[tid] = threadCallStackVs[:lastIndex]
 
 	is, ok := ed.interrogationStates[tid]
 
@@ -481,38 +488,48 @@ func (ed *ecalDebugger) Describe(threadId uint64) interface{} {
 	threadCallStack, ok1 := ed.callStacks[threadId]
 
 	if is, ok2 := ed.interrogationStates[threadId]; ok1 && ok2 {
+		callStackNode := make([]map[string]interface{}, 0)
+
+		for _, sn := range threadCallStack {
+			callStackNode = append(callStackNode, sn.ToJSONObject())
+		}
 
 		res = map[string]interface{}{
 			"threadRunning": is.running,
 			"callStack":     ed.prettyPrintCallStack(threadCallStack),
+			"callStackNode": callStackNode,
+			"callStackVs":   ed.callStackVs[threadId],
 		}
 
-		vsValues := make(map[string]interface{})
-
-		parent := is.vs.Parent()
-		for parent != nil &&
-			parent.Name() != scope.GlobalScope &&
-			strings.HasPrefix(parent.Name(), scope.FuncPrefix) {
-
-			vsValues = datautil.MergeMaps(vsValues, parent.ToJSONObject())
-
-			parent = parent.Parent()
-		}
-
-		vsValues = ed.MergeMaps(vsValues, is.vs.ToJSONObject())
-
 		if !is.running {
 
 			codeString, _ := parser.PrettyPrint(is.node)
 			res["code"] = codeString
 			res["node"] = is.node.ToJSONObject()
-			res["vs"] = vsValues
+			res["vs"] = ed.buildVsSnapshot(is.vs)
 		}
 	}
 
 	return res
 }
 
+func (ed *ecalDebugger) buildVsSnapshot(vs parser.Scope) map[string]interface{} {
+	vsValues := make(map[string]interface{})
+
+	// Collect all parent scopes except the global scope
+
+	parent := vs.Parent()
+	for parent != nil &&
+		parent.Name() != scope.GlobalScope {
+
+		vsValues = datautil.MergeMaps(vsValues, parent.ToJSONObject())
+
+		parent = parent.Parent()
+	}
+
+	return ed.MergeMaps(vsValues, vs.ToJSONObject())
+}
+
 /*
 MergeMaps merges all given maps into a new map. Contents are shallow copies
 and conflicts are resolved as first-one-wins.

+ 26 - 0
interpreter/debug_test.go

@@ -95,6 +95,8 @@ log("test3")
 
 	if err != nil || outString != `{
   "callStack": [],
+  "callStackNode": [],
+  "callStackVs": [],
   "code": "log(\"test2\")",
   "node": {
     "allowescapes": false,
@@ -1072,6 +1074,30 @@ log("test3 b=", b)
   "callStack": [
     "myfunc() (ECALEvalTest:8)"
   ],
+  "callStackNode": [
+    {
+      "allowescapes": false,
+      "children": [
+        {
+          "name": "funccall"
+        }
+      ],
+      "id": 7,
+      "identifier": true,
+      "line": 8,
+      "linepos": 1,
+      "name": "identifier",
+      "pos": 69,
+      "source": "ECALEvalTest",
+      "value": "myfunc"
+    }
+  ],
+  "callStackVs": [
+    {
+      "b": 49,
+      "myfunc": "ecal.function: myfunc (Line 3, Pos 1)"
+    }
+  ],
   "code": "log(\"test2 a=\", a)",
   "node": {
     "allowescapes": false,