Browse Source

fix: Better debug display for variable scopes

Matthias Ladkau 3 years ago
parent
commit
63811157fa
9 changed files with 242 additions and 41 deletions
  1. 1 1
      go.mod
  2. 2 2
      go.sum
  3. 42 7
      interpreter/debug.go
  4. 151 26
      interpreter/debug_test.go
  5. 1 1
      interpreter/rt_func.go
  6. 6 1
      parser/runtime.go
  7. 1 0
      scope/helper.go
  8. 25 3
      scope/varsscope.go
  9. 13 0
      scope/varsscope_test.go

+ 1 - 1
go.mod

@@ -2,4 +2,4 @@ module devt.de/krotik/ecal
 
 go 1.12
 
-require devt.de/krotik/common v1.3.8
+require devt.de/krotik/common v1.3.9

+ 2 - 2
go.sum

@@ -1,2 +1,2 @@
-devt.de/krotik/common v1.3.8 h1:+O4CLiELyJsQISagQeSrfxUQnD49fFjfz1sWpZDihco=
-devt.de/krotik/common v1.3.8/go.mod h1:X4nsS85DAxyHkwSg/Tc6+XC2zfmGeaVz+37F61+eSaI=
+devt.de/krotik/common v1.3.9 h1:/DdNkkaaplUXHeA+6juY3f+WMUOkc5zycar2TdPXHB0=
+devt.de/krotik/common v1.3.9/go.mod h1:X4nsS85DAxyHkwSg/Tc6+XC2zfmGeaVz+37F61+eSaI=

+ 42 - 7
interpreter/debug.go

@@ -18,6 +18,7 @@ import (
 	"strings"
 	"sync"
 
+	"devt.de/krotik/common/datautil"
 	"devt.de/krotik/common/errorutil"
 	"devt.de/krotik/ecal/parser"
 	"devt.de/krotik/ecal/scope"
@@ -104,14 +105,16 @@ func (ed *ecalDebugger) HandleInput(input string) (interface{}, error) {
 
 	args := strings.Fields(input)
 
-	if cmd, ok := DebugCommandsMap[args[0]]; ok {
-		if len(args) > 1 {
-			res, err = cmd.Run(ed, args[1:])
+	if len(args) > 0 {
+		if cmd, ok := DebugCommandsMap[args[0]]; ok {
+			if len(args) > 1 {
+				res, err = cmd.Run(ed, args[1:])
+			} else {
+				res, err = cmd.Run(ed, nil)
+			}
 		} else {
-			res, err = cmd.Run(ed, nil)
+			err = fmt.Errorf("Unknown command: %v", args[0])
 		}
-	} else {
-		err = fmt.Errorf("Unknown command: %v", args[0])
 	}
 
 	return res, err
@@ -484,18 +487,50 @@ func (ed *ecalDebugger) Describe(threadId uint64) interface{} {
 			"callStack":     ed.prettyPrintCallStack(threadCallStack),
 		}
 
+		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"] = is.vs.ToJSONObject()
+			res["vs"] = vsValues
 		}
 	}
 
 	return res
 }
 
+/*
+MergeMaps merges all given maps into a new map. Contents are shallow copies
+and conflicts are resolved as first-one-wins.
+*/
+func (ed *ecalDebugger) MergeMaps(maps ...map[string]interface{}) map[string]interface{} {
+	ret := make(map[string]interface{})
+
+	for _, m := range maps {
+		for k, v := range m {
+			if _, ok := ret[k]; !ok {
+				ret[k] = v
+			}
+		}
+	}
+
+	return ret
+}
+
 /*
 Describe decribes a thread currently observed by the debugger.
 */

+ 151 - 26
interpreter/debug_test.go

@@ -411,13 +411,13 @@ func TestStepDebugging(t *testing.T) {
 
 	code := `
 log("start")
-func fa() {
+func fa(x) {
   a := 1
   log("a enter")
-  fb()
+  fb(x)
   log("a exit")
 }
-func fb() {
+func fb(x) {
   b := 2
   log("b enter")
   fc()
@@ -429,7 +429,7 @@ func fc() {
   log("c enter")
   log("c exit")
 }
-fa()
+fa(1)
 func e() {
   log("e()")
 }
@@ -495,13 +495,15 @@ log("finish")
   "threads": {
     "1": {
       "callStack": [
-        "fa() (ECALEvalTest:21)",
-        "fb() (ECALEvalTest:6)"
+        "fa(1) (ECALEvalTest:21)",
+        "fb(x) (ECALEvalTest:6)"
       ],
       "threadRunning": false
     }
   },
-  "vs": {}
+  "vs": {
+    "x": 1
+  }
 }` {
 		t.Error("Unexpected state:", state)
 		return
@@ -524,14 +526,15 @@ log("finish")
   "threads": {
     "1": {
       "callStack": [
-        "fa() (ECALEvalTest:21)",
-        "fb() (ECALEvalTest:6)"
+        "fa(1) (ECALEvalTest:21)",
+        "fb(x) (ECALEvalTest:6)"
       ],
       "threadRunning": false
     }
   },
   "vs": {
-    "b": 2
+    "b": 2,
+    "x": 1
   }
 }` {
 		t.Error("Unexpected state:", state)
@@ -555,14 +558,15 @@ log("finish")
   "threads": {
     "1": {
       "callStack": [
-        "fa() (ECALEvalTest:21)",
-        "fb() (ECALEvalTest:6)"
+        "fa(1) (ECALEvalTest:21)",
+        "fb(x) (ECALEvalTest:6)"
       ],
       "threadRunning": false
     }
   },
   "vs": {
-    "b": 2
+    "b": 2,
+    "x": 1
   }
 }` {
 		t.Error("Unexpected state:", state)
@@ -586,8 +590,8 @@ log("finish")
   "threads": {
     "1": {
       "callStack": [
-        "fa() (ECALEvalTest:21)",
-        "fb() (ECALEvalTest:6)",
+        "fa(1) (ECALEvalTest:21)",
+        "fb(x) (ECALEvalTest:6)",
         "fc() (ECALEvalTest:12)"
       ],
       "threadRunning": false
@@ -616,14 +620,15 @@ log("finish")
   "threads": {
     "1": {
       "callStack": [
-        "fa() (ECALEvalTest:21)",
-        "fb() (ECALEvalTest:6)"
+        "fa(1) (ECALEvalTest:21)",
+        "fb(x) (ECALEvalTest:6)"
       ],
       "threadRunning": false
     }
   },
   "vs": {
-    "b": 2
+    "b": 2,
+    "x": 1
   }
 }` {
 		t.Error("Unexpected state:", state)
@@ -647,8 +652,8 @@ log("finish")
   "threads": {
     "1": {
       "callStack": [
-        "fa() (ECALEvalTest:21)",
-        "fb() (ECALEvalTest:6)",
+        "fa(1) (ECALEvalTest:21)",
+        "fb(x) (ECALEvalTest:6)",
         "fc() (ECALEvalTest:13)"
       ],
       "threadRunning": false
@@ -674,14 +679,15 @@ log("finish")
   "threads": {
     "1": {
       "callStack": [
-        "fa() (ECALEvalTest:21)",
-        "fb() (ECALEvalTest:6)"
+        "fa(1) (ECALEvalTest:21)",
+        "fb(x) (ECALEvalTest:6)"
       ],
       "threadRunning": false
     }
   },
   "vs": {
-    "b": 2
+    "b": 2,
+    "x": 1
   }
 }` {
 		t.Error("Unexpected state:", state)
@@ -704,13 +710,14 @@ log("finish")
   "threads": {
     "1": {
       "callStack": [
-        "fa() (ECALEvalTest:21)"
+        "fa(1) (ECALEvalTest:21)"
       ],
       "threadRunning": false
     }
   },
   "vs": {
-    "a": 1
+    "a": 1,
+    "x": 1
   }
 }` {
 		t.Error("Unexpected state:", state)
@@ -846,6 +853,125 @@ finish`[1:] {
 	}
 }
 
+func TestStepDebuggingWithImport(t *testing.T) {
+	var err error
+	defer func() {
+		testDebugger = nil
+	}()
+
+	testDebugger = NewECALDebugger(nil)
+
+	il := &util.MemoryImportLocator{Files: make(map[string]string)}
+	il.Files["foo/bar"] = `
+func myfunc(n) {
+  if (n <= 1) {
+      return n
+  }
+  n := n + 1
+  return n
+}
+`
+	code := `
+a := 1
+import "foo/bar" as foobar
+log("start")
+a := foobar.myfunc(a)
+log("finish: ", a)
+`
+
+	if _, err = testDebugger.HandleInput("break ECALEvalTest:4"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if _, err = testDebugger.HandleInput("break foo/bar:4"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	wg := &sync.WaitGroup{}
+	wg.Add(1)
+
+	go func() {
+		_, err = UnitTestEvalAndASTAndImport(code, nil, "", il)
+		if err != nil {
+			t.Error(err)
+		}
+		wg.Done()
+	}()
+
+	tid := waitForThreadSuspension(t)
+
+	if state := getDebuggerState(tid, t); state != `{
+  "breakpoints": {
+    "ECALEvalTest:4": true,
+    "foo/bar:4": true
+  },
+  "code": "log(\"start\")",
+  "threads": {
+    "1": {
+      "callStack": [],
+      "threadRunning": false
+    }
+  },
+  "vs": {
+    "a": 1,
+    "foobar": {
+      "myfunc": "ecal.function: myfunc (Line 2, Pos 1)"
+    }
+  }
+}` {
+		t.Error("Unexpected state:", state)
+		return
+	}
+
+	// Resume execution
+
+	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v resume", tid)); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+	tid = waitForThreadSuspension(t)
+
+	if state := getDebuggerState(tid, t); state != `{
+  "breakpoints": {
+    "ECALEvalTest:4": true,
+    "foo/bar:4": true
+  },
+  "code": "return n",
+  "threads": {
+    "1": {
+      "callStack": [
+        "myfunc(a) (ECALEvalTest:5)"
+      ],
+      "threadRunning": false
+    }
+  },
+  "vs": {
+    "n": 1
+  }
+}` {
+		t.Error("Unexpected state:", state)
+		return
+	}
+
+	// Continue until the end
+
+	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v Resume", tid)); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	wg.Wait()
+
+	if err != nil || testlogger.String() != `
+start
+finish: 1`[1:] {
+		t.Error("Unexpected result:", testlogger.String(), err)
+		return
+	}
+}
+
 func getDebuggerState(tid uint64, t *testing.T) string {
 	out, err := testDebugger.HandleInput(fmt.Sprintf("status"))
 	if err != nil {
@@ -860,7 +986,6 @@ func getDebuggerState(tid uint64, t *testing.T) string {
 		t.Error(err)
 		return ""
 	}
-
 	outMap2 := out.(map[string]interface{})
 
 	outMap["vs"] = outMap2["vs"]

+ 1 - 1
interpreter/rt_func.go

@@ -136,7 +136,7 @@ func (f *function) Run(instanceID string, vs parser.Scope, is map[string]interfa
 
 	// Create varscope for the body - not a child scope but a new root
 
-	fvs := scope.NewScope(fmt.Sprintf("func: %v", f.name))
+	fvs := scope.NewScope(fmt.Sprintf("%v %v", scope.FuncPrefix, f.name))
 
 	if f.this != nil {
 		fvs.SetValue("this", f.this)

+ 6 - 1
parser/runtime.go

@@ -49,6 +49,11 @@ Scope models an environment which stores data.
 */
 type Scope interface {
 
+	/*
+	   Name returns the name of this scope.
+	*/
+	Name() string
+
 	/*
 	   NewChild creates a new child scope.
 	*/
@@ -75,7 +80,7 @@ type Scope interface {
 	String() string
 
 	/*
-		ToJSONObject returns this ASTNode and all its children as a JSON object.
+	   ToJSONObject returns this ASTNode and all its children as a JSON object.
 	*/
 	ToJSONObject() map[string]interface{}
 }

+ 1 - 0
scope/helper.go

@@ -25,6 +25,7 @@ Default scope names
 */
 const (
 	GlobalScope = "GlobalScope"
+	FuncPrefix  = "func:"
 )
 
 /*

+ 25 - 3
scope/varsscope.go

@@ -19,6 +19,7 @@ import (
 	"strings"
 	"sync"
 
+	"devt.de/krotik/common/stringutil"
 	"devt.de/krotik/ecal/parser"
 )
 
@@ -92,6 +93,13 @@ func (s *varsScope) NewChild(name string) parser.Scope {
 	return child
 }
 
+/*
+Name returns the name of this scope.
+*/
+func (s *varsScope) Name() string {
+	return s.name
+}
+
 /*
 Parent returns the parent scope or nil.
 */
@@ -365,10 +373,24 @@ func (s *varsScope) ToJSONObject() map[string]interface{} {
 	s.lock.RLock()
 	defer s.lock.RUnlock()
 
-	var ret map[string]interface{}
+	ret := make(map[string]interface{})
+
+	for k, v := range s.storage {
+		var value interface{}
+
+		value = fmt.Sprintf("ComplexDataStructure: %#v", v)
 
-	bytes, _ := json.Marshal(s.storage)
-	json.Unmarshal(bytes, &ret)
+		bytes, err := json.Marshal(v)
+		if err != nil {
+			bytes, err = json.Marshal(stringutil.ConvertToJSONMarshalableObject(v))
+
+		}
+		if err == nil {
+			json.Unmarshal(bytes, &value)
+		}
+
+		ret[k] = value
+	}
 
 	return ret
 }

+ 13 - 0
scope/varsscope_test.go

@@ -291,6 +291,7 @@ func TestVarScopeDump(t *testing.T) {
 	for2Vs.SetValue("d", 4)
 	for21Vs.SetValue("e", 5)
 	for211Vs.SetValue("f", 6)
+	for211Vs.SetValue("x", ToObject(for211Vs))
 	sinkVs2.SetValue("g", 2)
 
 	// Dump the sinkVs1 scope
@@ -310,6 +311,7 @@ func TestVarScopeDump(t *testing.T) {
                     e (int) : 5
                     block: for2-1-1 {
                         f (int) : 6
+                        x (map[interface {}]interface {}) : {"f":6}
                     }
                 }
             }
@@ -326,6 +328,17 @@ func TestVarScopeDump(t *testing.T) {
 		return
 	}
 
+	bytes, _ = json.Marshal(for211Vs.ToJSONObject())
+	if res := string(bytes); res != `{"f":6,"x":{"f":6}}` {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res := globalVS1.Name(); res != "global" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
 	if res := sinkVs2.String(); res != `global {
     0 (int) : 0
     sink: 2 {