Browse Source

feat: Stepping support

Matthias Ladkau 3 years ago
parent
commit
3e105e2d9c

+ 2 - 0
cli/ecal.go

@@ -26,6 +26,8 @@ TODO:
 - debug support break on start
 - debug support errors
 - pretty printer
+- local variable definition (let)
+- watch files
 */
 
 func main() {

+ 88 - 155
ecal-support/src/ecalDebugAdapter.ts

@@ -23,7 +23,7 @@ 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 { ClientBreakEvent, ContType, DebugStatus } from "./types";
 import * as path from "path";
 
 /**
@@ -69,10 +69,13 @@ export class ECALDebugSession extends LoggingDebugSession {
 
   private unconfirmedBreakpoints: DebugProtocol.Breakpoint[] = [];
 
-  public sendEvent(event: DebugProtocol.Event): void {
-    super.sendEvent(event);
-    console.error("#### Sending event:", event);
-  }
+  private bpCount: number = 1;
+  private sfCount: number = 1;
+  private vsCount: number = 1;
+
+  private bpIds: Record<string, number> = {};
+  private sfIds: Record<string, number> = {};
+  private vsIds: Record<string, number> = {};
 
   /**
    * Create a new debug adapter which is used for one debug session.
@@ -140,9 +143,6 @@ export class ECALDebugSession extends LoggingDebugSession {
     // The adapter implements the configurationDoneRequest.
     response.body.supportsConfigurationDoneRequest = true;
 
-    // make VS Code to send cancelRequests
-    response.body.supportsCancelRequest = true;
-
     // make VS Code send the breakpointLocations request
     response.body.supportsBreakpointLocationsRequest = true;
 
@@ -322,6 +322,7 @@ export class ECALDebugSession extends LoggingDebugSession {
   }
 
   private frameVariableScopes: Record<number, Record<string, any>> = {};
+  private frameVariableGlobalScopes: Record<number, Record<string, any>> = {};
 
   protected async stackTraceRequest(
     response: DebugProtocol.StackTraceResponse,
@@ -337,10 +338,6 @@ export class ECALDebugSession extends LoggingDebugSession {
       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);
@@ -357,10 +354,17 @@ export class ECALDebugSession extends LoggingDebugSession {
               sfNode.line
             )
           );
-          this.frameVariableScopes[frameId] = ins.callStackVs![i];
+          this.frameVariableScopes[frameId] = ins.callStackVsSnapshot![i];
+          this.frameVariableGlobalScopes[
+            frameId
+          ] = ins.callStackVsSnapshotGlobal![i];
         }
 
-        const frameId = this.getStackFrameId(args.threadId, ins.code!, ins.callStack.length);
+        const frameId = this.getStackFrameId(
+          args.threadId,
+          ins.code!,
+          ins.callStack.length
+        );
         const breakpointString = `${ins.node!.source}:${ins.node!.line}`;
 
         stackFrames.unshift(
@@ -375,19 +379,16 @@ export class ECALDebugSession extends LoggingDebugSession {
           )
         );
         this.frameVariableScopes[frameId] = ins.vs!;
+        this.frameVariableGlobalScopes[frameId] = ins.vsGlobal!;
       }
     }
 
-    console.log("##### stackTraceRequest response", stackFrames);
-
     response.body = {
       stackFrames,
     };
     this.sendResponse(response);
   }
 
-  // TODO ############################
-
   protected scopesRequest(
     response: DebugProtocol.ScopesResponse,
     args: DebugProtocol.ScopesArguments
@@ -395,7 +396,10 @@ export class ECALDebugSession extends LoggingDebugSession {
     console.error("##### scopesRequest:", args);
 
     response.body = {
-      scopes: [new Scope("Local", args.frameId), new Scope("Global", 1)],
+      scopes: [
+        new Scope("Local", this.getVariableScopeId(args.frameId, "local")),
+        new Scope("Global", this.getVariableScopeId(args.frameId, "global")),
+      ],
     };
 
     this.sendResponse(response);
@@ -407,77 +411,73 @@ export class ECALDebugSession extends LoggingDebugSession {
   ) {
     console.error("##### variablesRequest", args);
 
+    let vs: Record<string, any> = {};
     let variables: Variable[] = [];
-    const vs = this.frameVariableScopes[args.variablesReference];
+
+    const [frameId, scopeType] = this.getScopeLookupInfo(
+      args.variablesReference
+    );
+
+    if (scopeType === "local") {
+      vs = this.frameVariableScopes[frameId];
+    } else if (scopeType === "global") {
+      vs = this.frameVariableGlobalScopes[frameId];
+    }
 
     if (vs) {
       for (const [name, val] of Object.entries(vs)) {
-        variables.push(new Variable(name, String(val)));
+        let valString: string;
+
+        try {
+          valString = JSON.stringify(val);
+        } catch (e) {
+          valString = String(val);
+        }
+
+        variables.push(new Variable(name, valString));
       }
     }
 
+    console.log("##### variablesRequest response", variables);
+
     response.body = {
       variables,
     };
     this.sendResponse(response);
   }
 
-  protected continueRequest(
+  protected async continueRequest(
     response: DebugProtocol.ContinueResponse,
     args: DebugProtocol.ContinueArguments
-  ): void {
-    console.error("##### continueRequest", args);
-    this.sendResponse(response);
-  }
-
-  protected reverseContinueRequest(
-    response: DebugProtocol.ReverseContinueResponse,
-    args: DebugProtocol.ReverseContinueArguments
-  ): void {
-    console.error("##### reverseContinueRequest", args);
+  ) {
+    await this.client.cont(args.threadId, ContType.Resume);
+    response.body = {
+      allThreadsContinued: false,
+    };
     this.sendResponse(response);
   }
 
-  protected nextRequest(
+  protected async nextRequest(
     response: DebugProtocol.NextResponse,
     args: DebugProtocol.NextArguments
-  ): void {
-    console.error("##### nextRequest", args);
-    this.sendResponse(response);
-  }
-
-  protected stepBackRequest(
-    response: DebugProtocol.StepBackResponse,
-    args: DebugProtocol.StepBackArguments
-  ): void {
-    console.error("##### stepBackRequest", args);
-    this.sendResponse(response);
-  }
-
-  protected stepInTargetsRequest(
-    response: DebugProtocol.StepInTargetsResponse,
-    args: DebugProtocol.StepInTargetsArguments
   ) {
-    console.error("##### stepInTargetsRequest", args);
-    response.body = {
-      targets: [],
-    };
+    await this.client.cont(args.threadId, ContType.StepOver);
     this.sendResponse(response);
   }
 
-  protected stepInRequest(
+  protected async stepInRequest(
     response: DebugProtocol.StepInResponse,
     args: DebugProtocol.StepInArguments
-  ): void {
-    console.error("##### stepInRequest", args);
+  ) {
+    await this.client.cont(args.threadId, ContType.StepIn);
     this.sendResponse(response);
   }
 
-  protected stepOutRequest(
+  protected async stepOutRequest(
     response: DebugProtocol.StepOutResponse,
     args: DebugProtocol.StepOutArguments
-  ): void {
-    console.error("##### stepOutRequest", args);
+  ) {
+    await this.client.cont(args.threadId, ContType.StepOut);
     this.sendResponse(response);
   }
 
@@ -505,95 +505,7 @@ export class ECALDebugSession extends LoggingDebugSession {
     this.sendResponse(response);
   }
 
-  protected dataBreakpointInfoRequest(
-    response: DebugProtocol.DataBreakpointInfoResponse,
-    args: DebugProtocol.DataBreakpointInfoArguments
-  ): void {
-    console.error("##### dataBreakpointInfoRequest", args);
-
-    response.body = {
-      dataId: null,
-      description: "cannot break on data access",
-      accessTypes: undefined,
-      canPersist: false,
-    };
-
-    this.sendResponse(response);
-  }
-
-  protected setDataBreakpointsRequest(
-    response: DebugProtocol.SetDataBreakpointsResponse,
-    args: DebugProtocol.SetDataBreakpointsArguments
-  ): void {
-    console.error("##### setDataBreakpointsRequest", args);
-
-    response.body = {
-      breakpoints: [],
-    };
-
-    this.sendResponse(response);
-  }
-
-  protected completionsRequest(
-    response: DebugProtocol.CompletionsResponse,
-    args: DebugProtocol.CompletionsArguments
-  ): void {
-    console.error("##### completionsRequest", args);
-
-    response.body = {
-      targets: [
-        {
-          label: "item 10",
-          sortText: "10",
-        },
-        {
-          label: "item 1",
-          sortText: "01",
-        },
-        {
-          label: "item 2",
-          sortText: "02",
-        },
-        {
-          label: "array[]",
-          selectionStart: 6,
-          sortText: "03",
-        },
-        {
-          label: "func(arg)",
-          selectionStart: 5,
-          selectionLength: 3,
-          sortText: "04",
-        },
-      ],
-    };
-    this.sendResponse(response);
-  }
-
-  protected cancelRequest(
-    response: DebugProtocol.CancelResponse,
-    args: DebugProtocol.CancelArguments
-  ) {
-    console.error("##### cancelRequest", args);
-    this.sendResponse(response);
-  }
-
-  protected customRequest(
-    command: string,
-    response: DebugProtocol.Response,
-    args: any
-  ) {
-    console.error("##### customRequest", args);
-
-    if (command === "toggleFormatting") {
-      this.sendResponse(response);
-    } else {
-      super.customRequest(command, response, args);
-    }
-  }
-
   public shutdown() {
-    console.log("#### Shutdown");
     this.client
       ?.shutdown()
       .then(() => {
@@ -609,9 +521,6 @@ 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.
    */
@@ -624,16 +533,13 @@ 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.
+   * Map a given stackframe to a stackframe ID.
    */
   private getStackFrameId(
     threadId: string | number,
     frameString: string,
-    frameIndex: number,
+    frameIndex: number
   ): number {
     const storageString = `${threadId}###${frameString}###${frameIndex}`;
     let id = this.sfIds[storageString];
@@ -643,6 +549,33 @@ export class ECALDebugSession extends LoggingDebugSession {
     }
     return id;
   }
+
+  /**
+   * Map a given variable scope to a variable scope ID.
+   */
+  private getVariableScopeId(frameId: number, scopeType: string): number {
+    const storageString = `${frameId}###${scopeType}`;
+    let id = this.vsIds[storageString];
+    if (!id) {
+      id = this.vsCount++;
+      this.vsIds[storageString] = id;
+    }
+    return id;
+  }
+
+  /**
+   * Map a given variable scope ID to a variable scope.
+   */
+  private getScopeLookupInfo(vsId: number): [number, string] {
+    for (const [k, v] of Object.entries(this.vsIds)) {
+      if (v === vsId) {
+        const s = k.split("###");
+        return [parseInt(s[0]), s[1]];
+      }
+    }
+
+    return [-1, ""];
+  }
 }
 
 class LogChannelAdapter {

+ 15 - 1
ecal-support/src/ecalDebugClient.ts

@@ -5,7 +5,12 @@
 import * as net from "net";
 import { EventEmitter } from "events";
 import PromiseSocket from "promise-socket";
-import { LogOutputStream, DebugStatus, ThreadInspection } from "./types";
+import {
+  LogOutputStream,
+  DebugStatus,
+  ThreadInspection,
+  ContType,
+} from "./types";
 
 interface BacklogCommand {
   cmd: string;
@@ -65,6 +70,15 @@ export class ECALDebugClient extends EventEmitter {
     }
   }
 
+  public async cont(tid: number, type: ContType) {
+    try {
+      await this.sendCommand("cont", [String(tid), type]);
+      delete this.threadInspection[tid];
+    } catch (e) {
+      this.out.error(`Could not continue thread ${tid}: ${e}`);
+    }
+  }
+
   public async setBreakpoint(breakpoint: string) {
     try {
       (await this.sendCommand(`break ${breakpoint}`)) as DebugStatus;

+ 10 - 1
ecal-support/src/types.ts

@@ -14,11 +14,13 @@ export interface EcalAstNode {
 export interface ThreadInspection {
   callStack: string[];
   callStackNode?: EcalAstNode[];
-  callStackVs?: Record<string, any>[];
+  callStackVsSnapshot?: Record<string, any>[];
+  callStackVsSnapshotGlobal?: Record<string, any>[];
   threadRunning: boolean;
   code?: string;
   node?: EcalAstNode;
   vs?: Record<string, any>;
+  vsGlobal?: Record<string, any>;
 }
 
 export interface ThreadStatus {
@@ -45,3 +47,10 @@ export interface ClientBreakEvent {
   tid: number;
   inspection: ThreadInspection;
 }
+
+export enum ContType {
+  Resume = "Resume",
+  StepIn = "StepIn",
+  StepOver = "StepOver",
+  StepOut = "StepOut",
+}

+ 50 - 25
interpreter/debug.go

@@ -29,14 +29,15 @@ 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 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
+	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
+	callStackVsSnapshots       map[uint64][]map[string]interface{} // Call stack variable scope snapshots of threads
+	callStackGlobalVsSnapshots map[uint64][]map[string]interface{} // Call stack global 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
 
 }
 
@@ -87,14 +88,15 @@ NewDebugger returns a new debugger object.
 */
 func NewECALDebugger(globalVS parser.Scope) util.ECALDebugger {
 	return &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,
-		lock:                &sync.RWMutex{},
+		breakPoints:                make(map[string]bool),
+		interrogationStates:        make(map[uint64]*interrogationState),
+		callStacks:                 make(map[uint64][]*parser.ASTNode),
+		callStackVsSnapshots:       make(map[uint64][]map[string]interface{}),
+		callStackGlobalVsSnapshots: make(map[uint64][]map[string]interface{}),
+		sources:                    make(map[string]bool),
+		breakOnStart:               false,
+		globalScope:                globalVS,
+		lock:                       &sync.RWMutex{},
 	}
 }
 
@@ -146,7 +148,8 @@ 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.callStackVsSnapshots[tid] = make([]map[string]interface{}, 0, 10)
+		ed.callStackGlobalVsSnapshots[tid] = make([]map[string]interface{}, 0, 10)
 		ed.lock.Unlock()
 	}
 
@@ -222,7 +225,8 @@ func (ed *ecalDebugger) VisitStepInState(node *parser.ASTNode, vs parser.Scope,
 	var err util.TraceableRuntimeError
 
 	threadCallStack := ed.callStacks[tid]
-	threadCallStackVs := ed.callStackVs[tid]
+	threadCallStackVs := ed.callStackVsSnapshots[tid]
+	threadCallStackGlobalVs := ed.callStackGlobalVsSnapshots[tid]
 
 	is, ok := ed.interrogationStates[tid]
 
@@ -252,7 +256,8 @@ func (ed *ecalDebugger) VisitStepInState(node *parser.ASTNode, vs parser.Scope,
 	}
 
 	ed.callStacks[tid] = append(threadCallStack, node)
-	ed.callStackVs[tid] = append(threadCallStackVs, ed.buildVsSnapshot(vs))
+	ed.callStackVsSnapshots[tid] = append(threadCallStackVs, ed.buildVsSnapshot(vs))
+	ed.callStackGlobalVsSnapshots[tid] = append(threadCallStackGlobalVs, ed.buildGlobalVsSnapshot(vs))
 
 	return err
 }
@@ -265,7 +270,8 @@ func (ed *ecalDebugger) VisitStepOutState(node *parser.ASTNode, vs parser.Scope,
 	defer ed.lock.Unlock()
 
 	threadCallStack := ed.callStacks[tid]
-	threadCallStackVs := ed.callStackVs[tid]
+	threadCallStackVs := ed.callStackVsSnapshots[tid]
+	threadCallStackGlobalVs := ed.callStackGlobalVsSnapshots[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
@@ -274,7 +280,8 @@ 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]
+	ed.callStackVsSnapshots[tid] = threadCallStackVs[:lastIndex]
+	ed.callStackGlobalVsSnapshots[tid] = threadCallStackGlobalVs[:lastIndex]
 
 	is, ok := ed.interrogationStates[tid]
 
@@ -495,10 +502,11 @@ func (ed *ecalDebugger) Describe(threadId uint64) interface{} {
 		}
 
 		res = map[string]interface{}{
-			"threadRunning": is.running,
-			"callStack":     ed.prettyPrintCallStack(threadCallStack),
-			"callStackNode": callStackNode,
-			"callStackVs":   ed.callStackVs[threadId],
+			"threadRunning":             is.running,
+			"callStack":                 ed.prettyPrintCallStack(threadCallStack),
+			"callStackNode":             callStackNode,
+			"callStackVsSnapshot":       ed.callStackVsSnapshots[threadId],
+			"callStackVsSnapshotGlobal": ed.callStackGlobalVsSnapshots[threadId],
 		}
 
 		if !is.running {
@@ -507,6 +515,7 @@ func (ed *ecalDebugger) Describe(threadId uint64) interface{} {
 			res["code"] = codeString
 			res["node"] = is.node.ToJSONObject()
 			res["vs"] = ed.buildVsSnapshot(is.vs)
+			res["vsGlobal"] = ed.buildGlobalVsSnapshot(is.vs)
 		}
 	}
 
@@ -530,6 +539,22 @@ func (ed *ecalDebugger) buildVsSnapshot(vs parser.Scope) map[string]interface{}
 	return ed.MergeMaps(vsValues, vs.ToJSONObject())
 }
 
+func (ed *ecalDebugger) buildGlobalVsSnapshot(vs parser.Scope) map[string]interface{} {
+	vsValues := make(map[string]interface{})
+
+	globalVs := vs
+	for globalVs != nil &&
+		globalVs.Name() != scope.GlobalScope {
+		globalVs = globalVs.Parent()
+	}
+
+	if globalVs != nil && globalVs.Name() == scope.GlobalScope {
+		vsValues = globalVs.ToJSONObject()
+	}
+
+	return vsValues
+}
+
 /*
 MergeMaps merges all given maps into a new map. Contents are shallow copies
 and conflicts are resolved as first-one-wins.

+ 15 - 3
interpreter/debug_test.go

@@ -96,7 +96,8 @@ log("test3")
 	if err != nil || outString != `{
   "callStack": [],
   "callStackNode": [],
-  "callStackVs": [],
+  "callStackVsSnapshot": [],
+  "callStackVsSnapshotGlobal": [],
   "code": "log(\"test2\")",
   "node": {
     "allowescapes": false,
@@ -128,7 +129,8 @@ log("test3")
     "value": "log"
   },
   "threadRunning": false,
-  "vs": {}
+  "vs": {},
+  "vsGlobal": {}
 }` {
 		t.Error("Unexpected result:", outString, err)
 		return
@@ -1092,7 +1094,13 @@ log("test3 b=", b)
       "value": "myfunc"
     }
   ],
-  "callStackVs": [
+  "callStackVsSnapshot": [
+    {
+      "b": 49,
+      "myfunc": "ecal.function: myfunc (Line 3, Pos 1)"
+    }
+  ],
+  "callStackVsSnapshotGlobal": [
     {
       "b": 49,
       "myfunc": "ecal.function: myfunc (Line 3, Pos 1)"
@@ -1142,6 +1150,10 @@ log("test3 b=", b)
   "threadRunning": false,
   "vs": {
     "a": 56
+  },
+  "vsGlobal": {
+    "b": 49,
+    "myfunc": "ecal.function: myfunc (Line 3, Pos 1)"
   }
 }` {
 		t.Error("Unexpected result:", outString, err)