Repository: go-delve/delve
Pull request #2659 (2021-08-16 15:45:26)

proc/gdbserver: support watchpoints
### proc/gdbserver: support watchpoints

Adds watchpoint support to gdbserver backend for rr debugger and
debugserver on macOS/amd64 and macOS/arm64.

### proc/native: support watchpoints on Windows




aarzilli: This PR contains #2651

Patch:
diff --git a/Documentation/cli/README.md b/Documentation/cli/README.md
index 4e83cc98..b84643cc 100644
--- a/Documentation/cli/README.md
+++ b/Documentation/cli/README.md
@@ -669,6 +669,8 @@ The memory location is specified with the same expression language used by 'prin
 
 will watch the address of variable 'v'.
 
+Note that writes that do not change the value of the watched memory address might not be reported.
+
 See also: "help print".
 
 
diff --git a/_fixtures/databpeasy.go b/_fixtures/databpeasy.go
index c5aa8e51..4b0dc996 100644
--- a/_fixtures/databpeasy.go
+++ b/_fixtures/databpeasy.go
@@ -15,13 +15,13 @@ func main() { // Position 0
 	globalvar2 = globalvar1 + 1
 	globalvar1 = globalvar2 + 1
 	fmt.Printf("%d %d\n", globalvar1, globalvar2) // Position 1
-	runtime.Breakpoint()
+
 	globalvar2 = globalvar2 + 1          // Position 2
 	globalvar2 = globalvar1 + globalvar2 // Position 3
 	fmt.Printf("%d %d\n", globalvar1, globalvar2)
 	globalvar1 = globalvar2 + 1
 	fmt.Printf("%d %d\n", globalvar1, globalvar2)
-	runtime.Breakpoint()
+
 	done := make(chan struct{}) // Position 4
 	go f(done)
 	<-done
@@ -29,6 +29,6 @@ func main() { // Position 0
 
 func f(done chan struct{}) {
 	runtime.LockOSThread()
-	globalvar1 = globalvar2 + 1
+	globalvar1 = globalvar2 + 2
 	close(done) // Position 5
 }
diff --git a/_fixtures/databpstack.go b/_fixtures/databpstack.go
index ce85c2a5..e2c33308 100644
--- a/_fixtures/databpstack.go
+++ b/_fixtures/databpstack.go
@@ -2,12 +2,12 @@ package main
 
 import (
 	"fmt"
-	"runtime"
+	//
 )
 
 func f() {
 	w := 0
-	runtime.Breakpoint()
+
 	g(1000, &w) // Position 0
 }
 
diff --git a/_fixtures/genericbp.go b/_fixtures/genericbp.go
new file mode 100644
index 00000000..b3f43d8d
--- /dev/null
+++ b/_fixtures/genericbp.go
@@ -0,0 +1,12 @@
+package main
+
+import "fmt"
+
+func testfn[T any](arg T) {
+	fmt.Println(arg)
+}
+
+func main() {
+	testfn[uint16](1)
+	testfn[float64](2.1)
+}
diff --git a/pkg/dwarf/godwarf/type.go b/pkg/dwarf/godwarf/type.go
index ba189115..49ff1c0a 100644
--- a/pkg/dwarf/godwarf/type.go
+++ b/pkg/dwarf/godwarf/type.go
@@ -27,6 +27,7 @@ const (
 	AttrGoEmbeddedField dwarf.Attr = 0x2903
 	AttrGoRuntimeType   dwarf.Attr = 0x2904
 	AttrGoPackageName   dwarf.Attr = 0x2905
+	AttrGoDictIndex     dwarf.Attr = 0x2906
 )
 
 // Basic type encodings -- the value for AttrEncoding in a TagBaseType Entry.
@@ -513,6 +514,11 @@ func (t *ChanType) stringIntl(recCheck recCheck) string {
 	return "chan " + t.ElemType.String()
 }
 
+type ParametricType struct {
+	TypedefType
+	DictIndex int64
+}
+
 // An UnsupportedType is a placeholder returned in situations where we
 // encounter a type that isn't supported.
 type UnsupportedType struct {
@@ -1011,7 +1017,15 @@ func readType(d *dwarf.Data, name string, r *dwarf.Reader, off dwarf.Offset, typ
 			typeCache[off] = it
 			t = &it.TypedefType
 		default:
-			typ = t
+			if dictIndex, ok := e.Val(AttrGoDictIndex).(int64); ok {
+				pt := new(ParametricType)
+				pt.DictIndex = dictIndex
+				typ = pt
+				typeCache[off] = pt
+				t = &pt.TypedefType
+			} else {
+				typ = t
+			}
 		}
 		typeCache[off] = typ
 		t.Name, _ = e.Val(dwarf.AttrName).(string)
diff --git a/pkg/dwarf/line/state_machine.go b/pkg/dwarf/line/state_machine.go
index bf3029a6..d2e3afa7 100644
--- a/pkg/dwarf/line/state_machine.go
+++ b/pkg/dwarf/line/state_machine.go
@@ -277,76 +277,35 @@ func (sm *StateMachine) PCToLine(pc uint64) (string, int, bool) {
 	return "", 0, false
 }
 
-// LineToPC returns the first PC address associated with filename:lineno.
-func (lineInfo *DebugLineInfo) LineToPC(filename string, lineno int) uint64 {
+// PCStmt is a PC address with its is_stmt flag
+type PCStmt struct {
+	PC   uint64
+	Stmt bool
+}
+
+// LineToPCs returns all PCs associated with filename:lineno
+func (lineInfo *DebugLineInfo) LineToPCs(filename string, lineno int) []PCStmt {
 	if lineInfo == nil {
-		return 0
+		return nil
 	}
 
 	sm := newStateMachine(lineInfo, lineInfo.Instructions, lineInfo.ptrSize)
 
-	// if no instruction marked is_stmt is found fallback to the first
-	// instruction assigned to the filename:line.
-	var fallbackPC uint64
+	pcstmts := []PCStmt{}
 
 	for {
 		if err := sm.next(); err != nil {
 			if lineInfo.Logf != nil && err != io.EOF {
-				lineInfo.Logf("LineToPC error: %v", err)
+				lineInfo.Logf("LineToPCs error: %v", err)
 			}
 			break
 		}
 		if sm.line == lineno && sm.file == filename && sm.valid {
-			if sm.isStmt {
-				return sm.address
-			} else if fallbackPC == 0 {
-				fallbackPC = sm.address
-			}
-		}
-	}
-	return fallbackPC
-}
-
-// LineToPCIn returns the first PC for filename:lineno in the interval [startPC, endPC).
-// This function is used to find the instruction corresponding to
-// filename:lineno for a function that has been inlined.
-// basePC will be used for caching, it's normally the entry point for the
-// function containing pc.
-func (lineInfo *DebugLineInfo) LineToPCIn(filename string, lineno int, basePC, startPC, endPC uint64) uint64 {
-	if lineInfo == nil {
-		return 0
-	}
-	if basePC > startPC {
-		panic(fmt.Errorf("basePC after startPC %#x %#x", basePC, startPC))
-	}
-
-	sm := lineInfo.stateMachineFor(basePC, startPC)
-
-	var fallbackPC uint64
-
-	for {
-		if sm.valid && sm.started {
-			if sm.address >= endPC {
-				break
-			}
-			if sm.line == lineno && sm.file == filename && sm.address >= startPC {
-				if sm.isStmt {
-					return sm.address
-				} else {
-					fallbackPC = sm.address
-				}
-			}
+			pcstmts = append(pcstmts, PCStmt{sm.address, sm.isStmt})
 		}
-		if err := sm.next(); err != nil {
-			if lineInfo.Logf != nil && err != io.EOF {
-				lineInfo.Logf("LineToPC error: %v", err)
-			}
-			break
-		}
-
 	}
 
-	return fallbackPC
+	return pcstmts
 }
 
 // PrologueEndPC returns the first PC address marked as prologue_end in the half open interval [start, end)
diff --git a/pkg/dwarf/reader/variables.go b/pkg/dwarf/reader/variables.go
index b7871a0e..d39f9d68 100644
--- a/pkg/dwarf/reader/variables.go
+++ b/pkg/dwarf/reader/variables.go
@@ -23,6 +23,7 @@ const (
 	VariablesOnlyVisible VariablesFlags = 1 << iota
 	VariablesSkipInlinedSubroutines
 	VariablesTrustDeclLine
+	VariablesNoDeclLineCheck
 )
 
 // Variables returns a list of variables contained inside 'root'.
@@ -60,7 +61,7 @@ func variablesInternal(v []Variable, root *godwarf.Tree, depth int, pc uint64, l
 			// are defined.
 			o = 1
 		}
-		if declLine, ok := root.Val(dwarf.AttrDeclLine).(int64); !ok || line >= int(declLine)+o {
+		if declLine, ok := root.Val(dwarf.AttrDeclLine).(int64); (flags&VariablesNoDeclLineCheck != 0) || !ok || line >= int(declLine)+o {
 			return append(v, Variable{root, depth})
 		}
 		return v
diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go
index fc4f7d81..6e40601a 100644
--- a/pkg/proc/bininfo.go
+++ b/pkg/proc/bininfo.go
@@ -149,21 +149,158 @@ func (err *ErrFunctionNotFound) Error() string {
 
 // FindFileLocation returns the PC for a given file:line.
 // Assumes that `file` is normalized to lower case and '/' on Windows.
-func FindFileLocation(p Process, fileName string, lineno int) ([]uint64, error) {
-	pcs, err := p.BinInfo().LineToPC(fileName, lineno)
-	if err != nil {
-		return nil, err
+func FindFileLocation(p Process, filename string, lineno int) ([]uint64, error) {
+	// A single file:line can appear in multiple concrete functions, because of
+	// generics instantiation as well as multiple inlined calls into other
+	// concrete functions.
+
+	// 1. Find all instructions assigned in debug_line to filename:lineno.
+
+	bi := p.BinInfo()
+
+	fileFound := false
+	pcs := []line.PCStmt{}
+	for _, image := range bi.Images {
+		for _, cu := range image.compileUnits {
+			if cu.lineInfo == nil || cu.lineInfo.Lookup[filename] == nil {
+				continue
+			}
+
+			fileFound = true
+			pcs = append(pcs, cu.lineInfo.LineToPCs(filename, lineno)...)
+		}
 	}
+
+	if len(pcs) == 0 {
+		// Check if the line contained a call to a function that was inlined, in
+		// that case it's possible for the line itself to not appear in debug_line
+		// at all, but it will still be in debug_info as the call site for an
+		// inlined subroutine entry.
+		for _, pc := range bi.inlinedCallLines[fileLine{filename, lineno}] {
+			pcs = append(pcs, line.PCStmt{PC: pc, Stmt: true})
+		}
+	}
+
+	if len(pcs) == 0 {
+		return nil, &ErrCouldNotFindLine{fileFound, filename, lineno}
+	}
+
+	// 2. assign all occurences of filename:lineno to their containing function
+
+	pcByFunc := map[*Function][]line.PCStmt{}
+	sort.Slice(pcs, func(i, j int) bool { return pcs[i].PC < pcs[j].PC })
 	var fn *Function
-	for i := range pcs {
-		if fn == nil || pcs[i] < fn.Entry || pcs[i] >= fn.End {
-			fn = p.BinInfo().PCToFunc(pcs[i])
+	for _, pcstmt := range pcs {
+		if fn == nil || (pcstmt.PC < fn.Entry) || (pcstmt.PC >= fn.End) {
+			fn = p.BinInfo().PCToFunc(pcstmt.PC)
 		}
-		if fn != nil && fn.Entry == pcs[i] {
-			pcs[i], _ = FirstPCAfterPrologue(p, fn, true)
+		if fn != nil {
+			pcByFunc[fn] = append(pcByFunc[fn], pcstmt)
 		}
 	}
-	return pcs, nil
+
+	selectedPCs := []uint64{}
+
+	for fn, pcs := range pcByFunc {
+
+		// 3. for each concrete function split instruction between the inlined functions it contains
+
+		if strings.Contains(fn.Name, "·dwrap·") || fn.trampoline {
+			// skip autogenerated functions
+			continue
+		}
+
+		dwtree, err := fn.cu.image.getDwarfTree(fn.offset)
+		if err != nil {
+			return nil, fmt.Errorf("loading DWARF for %s@%#x: %v", fn.Name, fn.offset, err)
+		}
+		inlrngs := allInlineCallRanges(dwtree)
+
+		// findInlRng returns the DWARF offset of the inlined call containing pc.
+		// If multiple nested inlined calls contain pc the deepest one is returned
+		// (since allInlineCallRanges returns inlined call by decreasing depth
+		// this is the first matching entry of the slice).
+		findInlRng := func(pc uint64) dwarf.Offset {
+			for _, inlrng := range inlrngs {
+				if inlrng.rng[0] <= pc && pc < inlrng.rng[1] {
+					return inlrng.off
+				}
+			}
+			return fn.offset
+		}
+
+		pcsByOff := map[dwarf.Offset][]line.PCStmt{}
+
+		for _, pc := range pcs {
+			off := findInlRng(pc.PC)
+			pcsByOff[off] = append(pcsByOff[off], pc)
+		}
+
+		// 4. pick the first instruction with stmt set for each inlined call as
+		//    well as the main body of the concrete function. If nothing has
+		//    is_stmt set pick the first instruction instead.
+
+		for off, pcs := range pcsByOff {
+			sort.Slice(pcs, func(i, j int) bool { return pcs[i].PC < pcs[j].PC })
+
+			var selectedPC uint64
+			for _, pc := range pcs {
+				if pc.Stmt {
+					selectedPC = pc.PC
+					break
+				}
+			}
+
+			if selectedPC == 0 && len(pcs) > 0 {
+				selectedPC = pcs[0].PC
+			}
+
+			if selectedPC == 0 {
+				continue
+			}
+
+			// 5. if we picked the entry point of the function, skip it
+
+			if off == fn.offset && fn.Entry == selectedPC {
+				selectedPC, _ = FirstPCAfterPrologue(p, fn, true)
+			}
+
+			selectedPCs = append(selectedPCs, selectedPC)
+		}
+	}
+
+	return selectedPCs, nil
+}
+
+// inlRnage is the range of an inlined call
+type inlRange struct {
+	off   dwarf.Offset
+	depth uint32
+	rng   [2]uint64
+}
+
+// allInlineCallRanges returns all inlined calls contained inside 'tree' in
+// reverse nesting order (i.e. the most nested calls are returned first).
+// Note that a single inlined call might not have a continuous range of
+// addresses and therefore appear multiple times in the returned slice.
+func allInlineCallRanges(tree *godwarf.Tree) []inlRange {
+	r := []inlRange{}
+
+	var visit func(*godwarf.Tree, uint32)
+	visit = func(n *godwarf.Tree, depth uint32) {
+		if n.Tag == dwarf.TagInlinedSubroutine {
+			for _, rng := range n.Ranges {
+				r = append(r, inlRange{off: n.Offset, depth: depth, rng: rng})
+			}
+		}
+		for _, child := range n.Children {
+			visit(child, depth+1)
+		}
+	}
+	visit(tree, 0)
+
+	sort.SliceStable(r, func(i, j int) bool { return r[i].depth > r[j].depth })
+	return r
 }
 
 // FindFunctionLocation finds address of a function's line
@@ -175,27 +312,28 @@ func FindFunctionLocation(p Process, funcName string, lineOffset int) ([]uint64,
 		return nil, &ErrFunctionNotFound{funcName}
 	}
 
-	if lineOffset <= 0 {
-		r := make([]uint64, 0, len(origfn.InlinedCalls)+1)
-		if origfn.Entry > 0 {
-			// add concrete implementation of the function
-			pc, err := FirstPCAfterPrologue(p, origfn, false)
-			if err != nil {
-				return nil, err
-			}
-			r = append(r, pc)
-		}
-		// add inlined calls to the function
-		for _, call := range origfn.InlinedCalls {
-			r = append(r, call.LowPC)
-		}
-		if len(r) == 0 {
-			return nil, &ErrFunctionNotFound{funcName}
+	if lineOffset > 0 {
+		filename, lineno := origfn.cu.lineInfo.PCToLine(origfn.Entry, origfn.Entry)
+		return FindFileLocation(p, filename, lineno+lineOffset)
+	}
+
+	r := make([]uint64, 0, len(origfn.InlinedCalls)+1)
+	if origfn.Entry > 0 {
+		// add concrete implementation of the function
+		pc, err := FirstPCAfterPrologue(p, origfn, false)
+		if err != nil {
+			return nil, err
 		}
-		return r, nil
+		r = append(r, pc)
+	}
+	// add inlined calls to the function
+	for _, call := range origfn.InlinedCalls {
+		r = append(r, call.LowPC)
+	}
+	if len(r) == 0 {
+		return nil, &ErrFunctionNotFound{funcName}
 	}
-	filename, lineno := origfn.cu.lineInfo.PCToLine(origfn.Entry, origfn.Entry)
-	return bi.LineToPC(filename, lineno+lineOffset)
+	return r, nil
 }
 
 // FirstPCAfterPrologue returns the address of the first
@@ -314,6 +452,8 @@ type Function struct {
 	offset     dwarf.Offset
 	cu         *compileUnit
 
+	trampoline bool // DW_AT_trampoline attribute set to true
+
 	// InlinedCalls lists all inlined calls to this function
 	InlinedCalls []InlinedCall
 }
@@ -519,69 +659,6 @@ func (err *ErrCouldNotFindLine) Error() string {
 	return fmt.Sprintf("could not find file %s", err.filename)
 }
 
-// LineToPC converts a file:line into a list of matching memory addresses,
-// corresponding to the first instruction matching the specified file:line
-// in the containing function and all its inlined calls.
-func (bi *BinaryInfo) LineToPC(filename string, lineno int) (pcs []uint64, err error) {
-	fileFound := false
-	var pc uint64
-pcsearch:
-	for _, image := range bi.Images {
-		for _, cu := range image.compileUnits {
-			if cu.lineInfo == nil || cu.lineInfo.Lookup[filename] == nil {
-				continue
-			}
-			fileFound = true
-			pc = cu.lineInfo.LineToPC(filename, lineno)
-			if pc != 0 {
-				break pcsearch
-			}
-		}
-	}
-
-	if pc == 0 {
-		// Check if the line contained a call to a function that was inlined, in
-		// that case it's possible for the line itself to not appear in debug_line
-		// at all, but it will still be in debug_info as the call site for an
-		// inlined subroutine entry.
-		if pcs := bi.inlinedCallLines[fileLine{filename, lineno}]; len(pcs) != 0 {
-			return pcs, nil
-		}
-		return nil, &ErrCouldNotFindLine{fileFound, filename, lineno}
-	}
-	// The code above will find the first occurence of an instruction
-	// corresponding to filename:line. If the function corresponding to that
-	// instruction has been inlined we don't just want to return the first
-	// occurence (which could be either the concrete version of the function or
-	// one of the inlinings) but instead:
-	// - the first instruction corresponding to filename:line in the concrete
-	//   version of the function
-	// - the first instruction corresponding to filename:line in each inlined
-	//   instance of the function.
-	fn := bi.PCToInlineFunc(pc)
-	if fn == nil {
-		return []uint64{pc}, nil
-	}
-	pcs = make([]uint64, 0, len(fn.InlinedCalls)+1)
-	pcs = appendLineToPCIn(pcs, filename, lineno, fn.cu, fn, fn.Entry, fn.End)
-	for _, call := range fn.InlinedCalls {
-		pcs = appendLineToPCIn(pcs, filename, lineno, call.cu, bi.PCToFunc(call.LowPC), call.LowPC, call.HighPC)
-	}
-	return pcs, nil
-}
-
-func appendLineToPCIn(pcs []uint64, filename string, lineno int, cu *compileUnit, containingFn *Function, lowPC, highPC uint64) []uint64 {
-	var entry uint64
-	if containingFn != nil {
-		entry = containingFn.Entry
-	}
-	pc := cu.lineInfo.LineToPCIn(filename, lineno, entry, lowPC, highPC)
-	if pc != 0 {
-		return append(pcs, pc)
-	}
-	return pcs
-}
-
 // AllPCsForFileLines returns a map providing all PC addresses for filename and each line in linenos
 func (bi *BinaryInfo) AllPCsForFileLines(filename string, linenos []int) map[int][]uint64 {
 	r := make(map[int][]uint64)
@@ -614,27 +691,6 @@ func (bi *BinaryInfo) PCToFunc(pc uint64) *Function {
 	return nil
 }
 
-// PCToInlineFunc returns the function containing the given PC address.
-// If the PC address belongs to an inlined call it will return the inlined function.
-func (bi *BinaryInfo) PCToInlineFunc(pc uint64) *Function {
-	fn := bi.PCToFunc(pc)
-	dwarfTree, err := fn.cu.image.getDwarfTree(fn.offset)
-	if err != nil {
-		return fn
-	}
-	entries := reader.InlineStack(dwarfTree, pc)
-	if len(entries) == 0 {
-		return fn
-	}
-
-	fnname, okname := entries[0].Val(dwarf.AttrName).(string)
-	if !okname {
-		return fn
-	}
-
-	return bi.LookupFunc[fnname]
-}
-
 // PCToImage returns the image containing the given PC address.
 func (bi *BinaryInfo) PCToImage(pc uint64) *Image {
 	fn := bi.PCToFunc(pc)
@@ -1824,7 +1880,7 @@ func (bi *BinaryInfo) loadDebugInfoMaps(image *Image, debugInfoBytes, debugLineB
 	if bi.regabi {
 		// prepare patch for runtime.mallocgc's DIE
 		fn := bi.LookupFunc["runtime.mallocgc"]
-		if fn != nil {
+		if fn != nil && fn.cu.image == image {
 			tree, err := image.getDwarfTree(fn.offset)
 			if err == nil {
 				tree.Children, err = regabiMallocgcWorkaround(bi)
@@ -2017,6 +2073,8 @@ func (bi *BinaryInfo) addConcreteSubprogram(entry *dwarf.Entry, ctxt *loadDebugI
 		bi.logger.Warnf("reading debug_info: concrete subprogram without name at %#x", entry.Offset)
 	}
 
+	trampoline, _ := entry.Val(dwarf.AttrTrampoline).(bool)
+
 	originIdx := ctxt.lookupAbstractOrigin(bi, entry.Offset)
 	fn := &bi.Functions[originIdx]
 
@@ -2025,6 +2083,7 @@ func (bi *BinaryInfo) addConcreteSubprogram(entry *dwarf.Entry, ctxt *loadDebugI
 	fn.End = highpc
 	fn.offset = entry.Offset
 	fn.cu = cu
+	fn.trampoline = trampoline
 
 	if entry.Children {
 		bi.loadDebugInfoMapsInlinedCalls(ctxt, reader, cu)
diff --git a/pkg/proc/breakpoints.go b/pkg/proc/breakpoints.go
index c5b9bfe4..b8434c4f 100644
--- a/pkg/proc/breakpoints.go
+++ b/pkg/proc/breakpoints.go
@@ -372,6 +372,13 @@ func isPanicCall(frames []Stackframe) (bool, int) {
 }
 
 func isDeferReturnCall(frames []Stackframe, deferReturns []uint64) (bool, uint64) {
+	if len(frames) >= 2 && (len(deferReturns) > 0) {
+		// On Go 1.18 and later runtime.deferreturn doesn't use jmpdefer anymore,
+		// it's a normal function making normal calls to deferred functions.
+		if frames[1].Current.Fn != nil && frames[1].Current.Fn.Name == "runtime.deferreturn" {
+			return true, 0
+		}
+	}
 	if len(frames) >= 1 {
 		for _, pc := range deferReturns {
 			if frames[0].Ret == pc {
@@ -892,7 +899,7 @@ func (rbpi *returnBreakpointInfo) Collect(t *Target, thread Thread) []*Variable
 		return returnInfoError("could not read function entry", err, thread.ProcessMemory())
 	}
 
-	vars, err := scope.Locals()
+	vars, err := scope.Locals(0)
 	if err != nil {
 		return returnInfoError("could not evaluate return variables", err, thread.ProcessMemory())
 	}
diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go
index 9e968777..d67c1e68 100644
--- a/pkg/proc/eval.go
+++ b/pkg/proc/eval.go
@@ -20,10 +20,13 @@ import (
 	"github.com/go-delve/delve/pkg/dwarf/op"
 	"github.com/go-delve/delve/pkg/dwarf/reader"
 	"github.com/go-delve/delve/pkg/goversion"
+	"github.com/go-delve/delve/pkg/logflags"
 )
 
 var errOperationOnSpecialFloat = errors.New("operations on non-finite floats not implemented")
 
+const goDictionaryName = ".dict"
+
 // EvalScope is the scope for variable evaluation. Contains the thread,
 // current location (PC), and canonical frame address.
 type EvalScope struct {
@@ -50,12 +53,22 @@ type EvalScope struct {
 	// evaluation is complete by closing the continueRequest channel.
 	callCtx *callContext
 
-	// If trustArgOrder is true function arguments that don't have an address
-	// will have one assigned by looking at their position in the argument
-	// list.
-	trustArgOrder bool
+	dictAddr uint64 // dictionary address for instantiated generic functions
 }
 
+type localsFlags uint8
+
+const (
+	// If localsTrustArgOrder is set function arguments that don't have an
+	// address will have one assigned by looking at their position in the argument
+	// list.
+	localsTrustArgOrder localsFlags = 1 << iota
+
+	// If localsNoDeclLineCheck the declaration line isn't checked at
+	// all to determine if the variable is in scope.
+	localsNoDeclLineCheck
+)
+
 // ConvertEvalScope returns a new EvalScope in the context of the
 // specified goroutine ID and stack frame.
 // If deferCall is > 0 the eval scope will be relative to the specified deferred call.
@@ -201,12 +214,12 @@ func isAssignment(err error) (int, bool) {
 }
 
 // Locals returns all variables in 'scope'.
-func (scope *EvalScope) Locals() ([]*Variable, error) {
+func (scope *EvalScope) Locals(flags localsFlags) ([]*Variable, error) {
 	if scope.Fn == nil {
 		return nil, errors.New("unable to find function context")
 	}
 
-	trustArgOrder := scope.trustArgOrder && scope.BinInfo.Producer() != "" && goversion.ProducerAfterOrEqual(scope.BinInfo.Producer(), 1, 12) && scope.Fn != nil && (scope.PC == scope.Fn.Entry)
+	trustArgOrder := (flags&localsTrustArgOrder != 0) && scope.BinInfo.Producer() != "" && goversion.ProducerAfterOrEqual(scope.BinInfo.Producer(), 1, 12) && scope.Fn != nil && (scope.PC == scope.Fn.Entry)
 
 	dwarfTree, err := scope.image().getDwarfTree(scope.Fn.offset)
 	if err != nil {
@@ -214,15 +227,43 @@ func (scope *EvalScope) Locals() ([]*Variable, error) {
 	}
 
 	variablesFlags := reader.VariablesOnlyVisible
+	if flags&localsNoDeclLineCheck != 0 {
+		variablesFlags = reader.VariablesNoDeclLineCheck
+	}
 	if scope.BinInfo.Producer() != "" && goversion.ProducerAfterOrEqual(scope.BinInfo.Producer(), 1, 15) {
 		variablesFlags |= reader.VariablesTrustDeclLine
 	}
 
 	varEntries := reader.Variables(dwarfTree, scope.PC, scope.Line, variablesFlags)
+
+	// look for dictionary entry
+	if scope.dictAddr == 0 {
+		for _, entry := range varEntries {
+			name, _ := entry.Val(dwarf.AttrName).(string)
+			if name == goDictionaryName {
+				dictVar, err := extractVarInfoFromEntry(scope.target, scope.BinInfo, scope.image(), scope.Regs, scope.Mem, entry.Tree, 0)
+				if err != nil {
+					logflags.DebuggerLogger().Errorf("could not load %s variable: %v", name, err)
+				} else if dictVar.Unreadable != nil {
+					logflags.DebuggerLogger().Errorf("could not load %s variable: %v", name, dictVar.Unreadable)
+				} else {
+					scope.dictAddr, err = readUintRaw(dictVar.mem, dictVar.Addr, int64(scope.BinInfo.Arch.PtrSize()))
+					if err != nil {
+						logflags.DebuggerLogger().Errorf("could not load %s variable: %v", name, err)
+					}
+				}
+				break
+			}
+		}
+	}
+
 	vars := make([]*Variable, 0, len(varEntries))
 	depths := make([]int, 0, len(varEntries))
 	for _, entry := range varEntries {
-		val, err := extractVarInfoFromEntry(scope.target, scope.BinInfo, scope.image(), scope.Regs, scope.Mem, entry.Tree)
+		if name, _ := entry.Val(dwarf.AttrName).(string); name == goDictionaryName {
+			continue
+		}
+		val, err := extractVarInfoFromEntry(scope.target, scope.BinInfo, scope.image(), scope.Regs, scope.Mem, entry.Tree, scope.dictAddr)
 		if err != nil {
 			// skip variables that we can't parse yet
 			continue
@@ -417,7 +458,7 @@ func (scope *EvalScope) SetVariable(name, value string) error {
 
 // LocalVariables returns all local variables from the current function scope.
 func (scope *EvalScope) LocalVariables(cfg LoadConfig) ([]*Variable, error) {
-	vars, err := scope.Locals()
+	vars, err := scope.Locals(0)
 	if err != nil {
 		return nil, err
 	}
@@ -431,7 +472,7 @@ func (scope *EvalScope) LocalVariables(cfg LoadConfig) ([]*Variable, error) {
 
 // FunctionArguments returns the name, value, and type of all current function arguments.
 func (scope *EvalScope) FunctionArguments(cfg LoadConfig) ([]*Variable, error) {
-	vars, err := scope.Locals()
+	vars, err := scope.Locals(0)
 	if err != nil {
 		return nil, err
 	}
@@ -478,7 +519,7 @@ func (scope *EvalScope) PackageVariables(cfg LoadConfig) ([]*Variable, error) {
 		}
 
 		// Ignore errors trying to extract values
-		val, err := extractVarInfoFromEntry(scope.target, scope.BinInfo, pkgvar.cu.image, regsReplaceStaticBase(scope.Regs, pkgvar.cu.image), scope.Mem, godwarf.EntryToTree(entry))
+		val, err := extractVarInfoFromEntry(scope.target, scope.BinInfo, pkgvar.cu.image, regsReplaceStaticBase(scope.Regs, pkgvar.cu.image), scope.Mem, godwarf.EntryToTree(entry), 0)
 		if val != nil && val.Kind == reflect.Invalid {
 			continue
 		}
@@ -515,7 +556,7 @@ func (scope *EvalScope) findGlobalInternal(name string) (*Variable, error) {
 			if err != nil {
 				return nil, err
 			}
-			return extractVarInfoFromEntry(scope.target, scope.BinInfo, pkgvar.cu.image, regsReplaceStaticBase(scope.Regs, pkgvar.cu.image), scope.Mem, godwarf.EntryToTree(entry))
+			return extractVarInfoFromEntry(scope.target, scope.BinInfo, pkgvar.cu.image, regsReplaceStaticBase(scope.Regs, pkgvar.cu.image), scope.Mem, godwarf.EntryToTree(entry), 0)
 		}
 	}
 	for _, fn := range scope.BinInfo.Functions {
@@ -1134,7 +1175,7 @@ func (scope *EvalScope) evalIdent(node *ast.Ident) (*Variable, error) {
 		return nilVariable, nil
 	}
 
-	vars, err := scope.Locals()
+	vars, err := scope.Locals(0)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/proc/fncall.go b/pkg/proc/fncall.go
index b5294385..4ce9cc4d 100644
--- a/pkg/proc/fncall.go
+++ b/pkg/proc/fncall.go
@@ -566,7 +566,7 @@ func funcCallCopyOneArg(scope *EvalScope, fncall *functionCallState, actualArg *
 	var formalArgVar *Variable
 	if formalArg.dwarfEntry != nil {
 		var err error
-		formalArgVar, err = extractVarInfoFromEntry(scope.target, formalScope.BinInfo, formalScope.image(), formalScope.Regs, formalScope.Mem, formalArg.dwarfEntry)
+		formalArgVar, err = extractVarInfoFromEntry(scope.target, formalScope.BinInfo, formalScope.image(), formalScope.Regs, formalScope.Mem, formalArg.dwarfEntry, 0)
 		if err != nil {
 			return err
 		}
@@ -883,9 +883,13 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState, thread Thread
 
 		// pretend we are still inside the function we called
 		fakeFunctionEntryScope(retScope, fncall.fn, int64(regs.SP()), regs.SP()-uint64(bi.Arch.PtrSize()))
-		retScope.trustArgOrder = !bi.regabi
+		var flags localsFlags
+		flags |= localsNoDeclLineCheck // if the function we are calling is an autogenerated stub then declaration lines have no meaning
+		if !bi.regabi {
+			flags |= localsTrustArgOrder
+		}
 
-		fncall.retvars, err = retScope.Locals()
+		fncall.retvars, err = retScope.Locals(flags)
 		if err != nil {
 			fncall.err = fmt.Errorf("could not get return values: %v", err)
 			break
diff --git a/pkg/proc/gdbserial/gdbserver.go b/pkg/proc/gdbserial/gdbserver.go
index 5e422db0..862fcf47 100644
--- a/pkg/proc/gdbserial/gdbserver.go
+++ b/pkg/proc/gdbserial/gdbserver.go
@@ -174,8 +174,9 @@ type gdbThread struct {
 	regs              gdbRegisters
 	CurrentBreakpoint proc.BreakpointState
 	p                 *gdbProcess
-	sig               uint8 // signal received by thread after last stop
-	setbp             bool  // thread was stopped because of a breakpoint
+	sig               uint8  // signal received by thread after last stop
+	setbp             bool   // thread was stopped because of a breakpoint
+	watchAddr         uint64 // if > 0 this is the watchpoint address
 	common            proc.CommonThread
 }
 
@@ -225,6 +226,7 @@ func newProcess(process *os.Process) *gdbProcess {
 			inbuf:               make([]byte, 0, initialInputBufferSize),
 			direction:           proc.Forward,
 			log:                 logger,
+			goarch:              runtime.GOARCH,
 		},
 		threads:        make(map[int]*gdbThread),
 		bi:             proc.NewBinaryInfo(runtime.GOOS, runtime.GOARCH),
@@ -822,10 +824,9 @@ func (p *gdbProcess) ContinueOnce() (proc.Thread, proc.StopReason, error) {
 	var atstart bool
 continueLoop:
 	for {
-		var err error
-		var sig uint8
 		tu.Reset()
-		threadID, sig, err = p.conn.resume(p.threads, &tu)
+		sp, err := p.conn.resume(p.threads, &tu)
+		threadID = sp.threadID
 		if err != nil {
 			if _, exited := err.(proc.ErrProcessExited); exited {
 				p.exited = true
@@ -842,7 +843,8 @@ continueLoop:
 		if trapthread != nil && !p.threadStopInfo {
 			// For stubs that do not support qThreadStopInfo we manually set the
 			// reason the thread returned by resume() stopped.
-			trapthread.sig = sig
+			trapthread.sig = sp.sig
+			trapthread.watchAddr = sp.watchAddr
 		}
 
 		var shouldStop bool
@@ -1075,7 +1077,7 @@ func (p *gdbProcess) Restart(pos string) (proc.Thread, error) {
 
 	// for some reason we have to send a vCont;c after a vRun to make rr behave
 	// properly, because that's what gdb does.
-	_, _, err = p.conn.resume(nil, nil)
+	_, err = p.conn.resume(nil, nil)
 	if err != nil {
 		return nil, err
 	}
@@ -1087,8 +1089,8 @@ func (p *gdbProcess) Restart(pos string) (proc.Thread, error) {
 	p.clearThreadSignals()
 	p.clearThreadRegisters()
 
-	for addr := range p.breakpoints.M {
-		p.conn.setBreakpoint(addr, p.breakpointKind)
+	for _, bp := range p.breakpoints.M {
+		p.WriteBreakpoint(bp)
 	}
 
 	return p.currentThread, p.setCurrentBreakpoints()
@@ -1224,15 +1226,33 @@ func (p *gdbProcess) FindBreakpoint(pc uint64) (*proc.Breakpoint, bool) {
 	return nil, false
 }
 
+func watchTypeToBreakpointType(wtype proc.WatchType) breakpointType {
+	switch {
+	case wtype.Read() && wtype.Write():
+		return accessWatchpoint
+	case wtype.Write():
+		return writeWatchpoint
+	case wtype.Read():
+		return readWatchpoint
+	default:
+		return swBreakpoint
+	}
+}
+
 func (p *gdbProcess) WriteBreakpoint(bp *proc.Breakpoint) error {
+	kind := p.breakpointKind
 	if bp.WatchType != 0 {
-		return errors.New("hardware breakpoints not supported")
+		kind = bp.WatchType.Size()
 	}
-	return p.conn.setBreakpoint(bp.Addr, p.breakpointKind)
+	return p.conn.setBreakpoint(bp.Addr, watchTypeToBreakpointType(bp.WatchType), kind)
 }
 
 func (p *gdbProcess) EraseBreakpoint(bp *proc.Breakpoint) error {
-	return p.conn.clearBreakpoint(bp.Addr, p.breakpointKind)
+	kind := p.breakpointKind
+	if bp.WatchType != 0 {
+		kind = bp.WatchType.Size()
+	}
+	return p.conn.clearBreakpoint(bp.Addr, watchTypeToBreakpointType(bp.WatchType), kind)
 }
 
 type threadUpdater struct {
@@ -1324,7 +1344,7 @@ func (p *gdbProcess) updateThreadList(tu *threadUpdater) error {
 
 	for _, th := range p.threads {
 		if p.threadStopInfo {
-			sig, reason, err := p.conn.threadStopInfo(th.strID)
+			sp, err := p.conn.threadStopInfo(th.strID)
 			if err != nil {
 				if isProtocolErrorUnsupported(err) {
 					p.threadStopInfo = false
@@ -1332,10 +1352,12 @@ func (p *gdbProcess) updateThreadList(tu *threadUpdater) error {
 				}
 				return err
 			}
-			th.setbp = (reason == "breakpoint" || (reason == "" && sig == breakpointSignal))
-			th.sig = sig
+			th.setbp = (sp.reason == "breakpoint" || (sp.reason == "" && sp.sig == breakpointSignal) || (sp.watchAddr > 0))
+			th.sig = sp.sig
+			th.watchAddr = sp.watchAddr
 		} else {
 			th.sig = 0
+			th.watchAddr = 0
 		}
 	}
 
@@ -1452,12 +1474,12 @@ func (t *gdbThread) Common() *proc.CommonThread {
 // StepInstruction will step exactly 1 CPU instruction.
 func (t *gdbThread) StepInstruction() error {
 	pc := t.regs.PC()
-	if _, atbp := t.p.breakpoints.M[pc]; atbp {
-		err := t.p.conn.clearBreakpoint(pc, t.p.breakpointKind)
+	if bp, atbp := t.p.breakpoints.M[pc]; atbp && bp.WatchType == 0 {
+		err := t.p.conn.clearBreakpoint(pc, swBreakpoint, t.p.breakpointKind)
 		if err != nil {
 			return err
 		}
-		defer t.p.conn.setBreakpoint(pc, t.p.breakpointKind)
+		defer t.p.conn.setBreakpoint(pc, swBreakpoint, t.p.breakpointKind)
 	}
 	// Reset thread registers so the next call to
 	// Thread.Registers will not be cached.
@@ -1677,13 +1699,16 @@ func (t *gdbThread) reloadGAtPC() error {
 	// around by clearing and re-setting the breakpoint in a specific sequence
 	// with the memory writes.
 	// Additionally all breakpoints in [pc, pc+len(movinstr)] need to be removed
-	for addr := range t.p.breakpoints.M {
+	for addr, bp := range t.p.breakpoints.M {
+		if bp.WatchType != 0 {
+			continue
+		}
 		if addr >= pc && addr <= pc+uint64(len(movinstr)) {
-			err := t.p.conn.clearBreakpoint(addr, t.p.breakpointKind)
+			err := t.p.conn.clearBreakpoint(addr, swBreakpoint, t.p.breakpointKind)
 			if err != nil {
 				return err
 			}
-			defer t.p.conn.setBreakpoint(addr, t.p.breakpointKind)
+			defer t.p.conn.setBreakpoint(addr, swBreakpoint, t.p.breakpointKind)
 		}
 	}
 
@@ -1795,6 +1820,13 @@ func (t *gdbThread) SetCurrentBreakpoint(adjustPC bool) error {
 	// adjustPC is ignored, it is the stub's responsibiility to set the PC
 	// address correctly after hitting a breakpoint.
 	t.clearBreakpointState()
+	if t.watchAddr > 0 {
+		t.CurrentBreakpoint.Breakpoint = t.p.Breakpoints().M[t.watchAddr]
+		if t.CurrentBreakpoint.Breakpoint == nil {
+			return fmt.Errorf("could not find watchpoint at address %#x", t.watchAddr)
+		}
+		return nil
+	}
 	regs, err := t.Registers()
 	if err != nil {
 		return err
diff --git a/pkg/proc/gdbserial/gdbserver_conn.go b/pkg/proc/gdbserial/gdbserver_conn.go
index ca5dc61b..fbe19a83 100644
--- a/pkg/proc/gdbserial/gdbserver_conn.go
+++ b/pkg/proc/gdbserial/gdbserver_conn.go
@@ -45,6 +45,7 @@ type gdbConn struct {
 	threadSuffixSupported bool // thread suffix supported by stub
 	isDebugserver         bool // true if the stub is debugserver
 	xcmdok                bool // x command can be used to transfer memory
+	goarch                string
 
 	log *logrus.Entry
 }
@@ -404,18 +405,28 @@ func (conn *gdbConn) qXfer(kind, annex string, binary bool) ([]byte, error) {
 	return out, nil
 }
 
+type breakpointType uint8
+
+const (
+	swBreakpoint     breakpointType = 0
+	hwBreakpoint     breakpointType = 1
+	writeWatchpoint  breakpointType = 2
+	readWatchpoint   breakpointType = 3
+	accessWatchpoint breakpointType = 4
+)
+
 // setBreakpoint executes a 'Z' (insert breakpoint) command of type '0' and kind '1' or '4'
-func (conn *gdbConn) setBreakpoint(addr uint64, kind int) error {
+func (conn *gdbConn) setBreakpoint(addr uint64, typ breakpointType, kind int) error {
 	conn.outbuf.Reset()
-	fmt.Fprintf(&conn.outbuf, "$Z0,%x,%d", addr, kind)
+	fmt.Fprintf(&conn.outbuf, "$Z%d,%x,%d", typ, addr, kind)
 	_, err := conn.exec(conn.outbuf.Bytes(), "set breakpoint")
 	return err
 }
 
 // clearBreakpoint executes a 'z' (remove breakpoint) command of type '0' and kind '1' or '4'
-func (conn *gdbConn) clearBreakpoint(addr uint64, kind int) error {
+func (conn *gdbConn) clearBreakpoint(addr uint64, typ breakpointType, kind int) error {
 	conn.outbuf.Reset()
-	fmt.Fprintf(&conn.outbuf, "$z0,%x,%d", addr, kind)
+	fmt.Fprintf(&conn.outbuf, "$z%d,%x,%d", typ, addr, kind)
 	_, err := conn.exec(conn.outbuf.Bytes(), "clear breakpoint")
 	return err
 }
@@ -537,7 +548,7 @@ func (conn *gdbConn) writeRegister(threadID string, regnum int, data []byte) err
 // resume each thread. If a thread has sig == 0 the 'c' action will be used,
 // otherwise the 'C' action will be used and the value of sig will be passed
 // to it.
-func (conn *gdbConn) resume(threads map[int]*gdbThread, tu *threadUpdater) (string, uint8, error) {
+func (conn *gdbConn) resume(threads map[int]*gdbThread, tu *threadUpdater) (stopPacket, error) {
 	if conn.direction == proc.Forward {
 		conn.outbuf.Reset()
 		fmt.Fprintf(&conn.outbuf, "$vCont")
@@ -549,7 +560,7 @@ func (conn *gdbConn) resume(threads map[int]*gdbThread, tu *threadUpdater) (stri
 		fmt.Fprintf(&conn.outbuf, ";c")
 	} else {
 		if err := conn.selectThread('c', "p-1.-1", "resume"); err != nil {
-			return "", 0, err
+			return stopPacket{}, err
 		}
 		conn.outbuf.Reset()
 		fmt.Fprint(&conn.outbuf, "$bc")
@@ -557,7 +568,7 @@ func (conn *gdbConn) resume(threads map[int]*gdbThread, tu *threadUpdater) (stri
 	conn.manualStopMutex.Lock()
 	if err := conn.send(conn.outbuf.Bytes()); err != nil {
 		conn.manualStopMutex.Unlock()
-		return "", 0, err
+		return stopPacket{}, err
 	}
 	conn.running = true
 	conn.manualStopMutex.Unlock()
@@ -584,7 +595,7 @@ func (conn *gdbConn) step(threadID string, tu *threadUpdater, ignoreFaultSignal
 		if err := conn.send(conn.outbuf.Bytes()); err != nil {
 			return err
 		}
-		_, _, err := conn.waitForvContStop("singlestep", threadID, tu)
+		_, err := conn.waitForvContStop("singlestep", threadID, tu)
 		return err
 	}
 	var sig uint8 = 0
@@ -601,8 +612,8 @@ func (conn *gdbConn) step(threadID string, tu *threadUpdater, ignoreFaultSignal
 		if tu != nil {
 			tu.Reset()
 		}
-		var err error
-		_, sig, err = conn.waitForvContStop("singlestep", threadID, tu)
+		sp, err := conn.waitForvContStop("singlestep", threadID, tu)
+		sig = sp.sig
 		if err != nil {
 			return err
 		}
@@ -626,7 +637,7 @@ func (conn *gdbConn) step(threadID string, tu *threadUpdater, ignoreFaultSignal
 
 var errThreadBlocked = errors.New("thread blocked")
 
-func (conn *gdbConn) waitForvContStop(context string, threadID string, tu *threadUpdater) (string, uint8, error) {
+func (conn *gdbConn) waitForvContStop(context, threadID string, tu *threadUpdater) (stopPacket, error) {
 	count := 0
 	failed := false
 	for {
@@ -647,24 +658,36 @@ func (conn *gdbConn) waitForvContStop(context string, threadID string, tu *threa
 			}
 			count++
 		} else if failed {
-			return "", 0, errThreadBlocked
+			return stopPacket{}, errThreadBlocked
 		} else if err != nil {
-			return "", 0, err
+			return stopPacket{}, err
 		} else {
 			repeat, sp, err := conn.parseStopPacket(resp, threadID, tu)
 			if !repeat {
-				return sp.threadID, sp.sig, err
+				return sp, err
 			}
 		}
 	}
 }
 
 type stopPacket struct {
-	threadID string
-	sig      uint8
-	reason   string
+	threadID  string
+	sig       uint8
+	reason    string
+	watchAddr uint64
 }
 
+// Mach exception codes used to decode metype/medata keys in stop packets (necessary to support watchpoints with debugserver).
+// See:
+//  https://opensource.apple.com/source/xnu/xnu-4570.1.46/osfmk/mach/exception_types.h.auto.html
+//  https://opensource.apple.com/source/xnu/xnu-4570.1.46/osfmk/mach/i386/exception.h.auto.html
+//  https://opensource.apple.com/source/xnu/xnu-4570.1.46/osfmk/mach/arm/exception.h.auto.html
+const (
+	_EXC_BREAKPOINT   = 6     // mach exception type for hardware breakpoints
+	_EXC_I386_SGL     = 1     // mach exception code for single step on x86, for some reason this is also used for watchpoints
+	_EXC_ARM_DA_DEBUG = 0x102 // mach exception code for debug fault on arm/arm64
+)
+
 // executes 'vCont' (continue/step) command
 func (conn *gdbConn) parseStopPacket(resp []byte, threadID string, tu *threadUpdater) (repeat bool, sp stopPacket, err error) {
 	switch resp[0] {
@@ -683,6 +706,9 @@ func (conn *gdbConn) parseStopPacket(resp []byte, threadID string, tu *threadUpd
 			conn.log.Debugf("full stop packet: %s", string(resp))
 		}
 
+		var metype int
+		var medata = make([]uint64, 0, 10)
+
 		buf := resp[3:]
 		for buf != nil {
 			colon := bytes.Index(buf, []byte{':'})
@@ -712,6 +738,32 @@ func (conn *gdbConn) parseStopPacket(resp []byte, threadID string, tu *threadUpd
 				}
 			case "reason":
 				sp.reason = string(value)
+			case "watch", "awatch", "rwatch":
+				sp.watchAddr, err = strconv.ParseUint(string(value), 16, 64)
+				if err != nil {
+					return false, stopPacket{}, fmt.Errorf("malformed stop packet: %s (wrong watch address)", string(resp))
+				}
+			case "metype":
+				// mach exception type (debugserver extension)
+				metype, _ = strconv.Atoi(string(value))
+			case "medata":
+				// mach exception data (debugserver extension)
+				d, _ := strconv.ParseUint(string(value), 16, 64)
+				medata = append(medata, d)
+			}
+		}
+
+		// Debugserver does not report watchpoint stops in the standard way preferring
+		// instead the semi-undocumented metype/medata keys.
+		// These values also have different meanings depending on the CPU architecture.
+		switch conn.goarch {
+		case "amd64":
+			if metype == _EXC_BREAKPOINT && len(medata) >= 2 && medata[0] == _EXC_I386_SGL {
+				sp.watchAddr = medata[1] // this should be zero if this is really a single step stop and non-zero for watchpoints
+			}
+		case "arm64":
+			if metype == _EXC_BREAKPOINT && len(medata) >= 2 && medata[0] == _EXC_ARM_DA_DEBUG {
+				sp.watchAddr = medata[1]
 			}
 		}
 
@@ -967,18 +1019,18 @@ func (conn *gdbConn) allocMemory(sz uint64) (uint64, error) {
 
 // threadStopInfo executes a 'qThreadStopInfo' and returns the reason the
 // thread stopped.
-func (conn *gdbConn) threadStopInfo(threadID string) (sig uint8, reason string, err error) {
+func (conn *gdbConn) threadStopInfo(threadID string) (sp stopPacket, err error) {
 	conn.outbuf.Reset()
 	fmt.Fprintf(&conn.outbuf, "$qThreadStopInfo%s", threadID)
 	resp, err := conn.exec(conn.outbuf.Bytes(), "thread stop info")
 	if err != nil {
-		return 0, "", err
+		return stopPacket{}, err
 	}
-	_, sp, err := conn.parseStopPacket(resp, "", nil)
+	_, sp, err = conn.parseStopPacket(resp, "", nil)
 	if err != nil {
-		return 0, "", err
+		return stopPacket{}, err
 	}
-	return sp.sig, sp.reason, nil
+	return sp, nil
 }
 
 // restart executes a 'vRun' command.
diff --git a/pkg/proc/stackwatch.go b/pkg/proc/stackwatch.go
index 9b671f77..2b33f70f 100644
--- a/pkg/proc/stackwatch.go
+++ b/pkg/proc/stackwatch.go
@@ -72,6 +72,28 @@ func (t *Target) setStackWatchBreakpoints(scope *EvalScope, watchpoint *Breakpoi
 	retbreaklet.watchpoint = watchpoint
 	retbreaklet.callback = woos
 
+	if recorded, _ := t.Recorded(); recorded && retframe.Current.Fn != nil {
+		// Must also set a breakpoint on the call instruction immediately
+		// preceding retframe.Current.PC, because the watchpoint could also go out
+		// of scope while we are running backwards.
+		callerText, err := disassemble(t.Memory(), nil, t.Breakpoints(), t.BinInfo(), retframe.Current.Fn.Entry, retframe.Current.Fn.End, false)
+		if err != nil {
+			return err
+		}
+		for i, instr := range callerText {
+			if instr.Loc.PC == retframe.Current.PC && i > 0 {
+				retbp2, err := t.SetBreakpoint(callerText[i-1].Loc.PC, WatchOutOfScopeBreakpoint, retFrameCond)
+				if err != nil {
+					return err
+				}
+				retbreaklet2 := retbp2.Breaklets[len(retbp.Breaklets)-1]
+				retbreaklet2.watchpoint = watchpoint
+				retbreaklet2.callback = woos
+				break
+			}
+		}
+	}
+
 	// Stack Resize Sentinel
 
 	fn := t.BinInfo().LookupFunc["runtime.copystack"]
diff --git a/pkg/proc/target.go b/pkg/proc/target.go
index 89d68d19..f4d65491 100644
--- a/pkg/proc/target.go
+++ b/pkg/proc/target.go
@@ -518,7 +518,7 @@ func (t *Target) dwrapUnwrap(fn *Function) *Function {
 	if fn == nil {
 		return nil
 	}
-	if !strings.Contains(fn.Name, "·dwrap·") {
+	if !strings.Contains(fn.Name, "·dwrap·") && !fn.trampoline {
 		return fn
 	}
 	if unwrap := t.BinInfo().dwrapUnwrapCache[fn.Entry]; unwrap != nil {
diff --git a/pkg/proc/target_exec.go b/pkg/proc/target_exec.go
index b9dae997..1d87e4d7 100644
--- a/pkg/proc/target_exec.go
+++ b/pkg/proc/target_exec.go
@@ -819,6 +819,10 @@ func isAutogenerated(loc Location) bool {
 	return loc.File == "<autogenerated>" && loc.Line == 1
 }
 
+func isAutogeneratedOrDeferReturn(loc Location) bool {
+	return isAutogenerated(loc) || (loc.Fn != nil && loc.Fn.Name == "runtime.deferreturn")
+}
+
 // skipAutogeneratedWrappers skips autogenerated wrappers when setting a
 // step-into breakpoint.
 // See genwrapper in: $GOROOT/src/cmd/compile/internal/gc/subr.go
@@ -878,12 +882,13 @@ func skipAutogeneratedWrappersIn(p Process, startfn *Function, startpc uint64) (
 // skipAutogeneratedWrappersOut skip autogenerated wrappers when setting a
 // step out breakpoint.
 // See genwrapper in: $GOROOT/src/cmd/compile/internal/gc/subr.go
+// It also skips runtime.deferreturn frames (which are only ever on the stack on Go 1.18 or later)
 func skipAutogeneratedWrappersOut(g *G, thread Thread, startTopframe, startRetframe *Stackframe) (topframe, retframe *Stackframe) {
 	topframe, retframe = startTopframe, startRetframe
 	if startTopframe.Ret == 0 {
 		return
 	}
-	if !isAutogenerated(startRetframe.Current) {
+	if !isAutogeneratedOrDeferReturn(startRetframe.Current) {
 		return
 	}
 	retfn := thread.BinInfo().PCToFunc(startTopframe.Ret)
@@ -909,7 +914,7 @@ func skipAutogeneratedWrappersOut(g *G, thread Thread, startTopframe, startRetfr
 			return
 		}
 		file, line := frame.Current.Fn.cu.lineInfo.PCToLine(frame.Current.Fn.Entry, frame.Current.Fn.Entry)
-		if !isAutogenerated(Location{File: file, Line: line, Fn: frame.Current.Fn}) {
+		if !isAutogeneratedOrDeferReturn(Location{File: file, Line: line, Fn: frame.Current.Fn}) {
 			return &frames[i-1], &frames[i]
 		}
 	}
@@ -1015,13 +1020,20 @@ func stepOutReverse(p *Target, topframe, retframe Stackframe, sameGCond ast.Expr
 		if err != nil {
 			return err
 		}
-	} else if ok, pc := isDeferReturnCall(frames, deferReturns); ok {
-		callpc = pc
 	} else {
 		callpc, err = findCallInstrForRet(p, p.Memory(), topframe.Ret, retframe.Current.Fn)
 		if err != nil {
 			return err
 		}
+
+		// check if the call instruction to this frame is a call to runtime.deferreturn
+		if len(frames) > 0 {
+			frames[0].Ret = callpc
+		}
+		if ok, pc := isDeferReturnCall(frames, deferReturns); ok && pc != 0 {
+			callpc = pc
+		}
+
 	}
 
 	_, err = allowDuplicateBreakpoint(p.SetBreakpoint(callpc, NextBreakpoint, sameGCond))
diff --git a/pkg/proc/types.go b/pkg/proc/types.go
index 4626a2bf..b52914ac 100644
--- a/pkg/proc/types.go
+++ b/pkg/proc/types.go
@@ -174,6 +174,34 @@ func runtimeTypeToDIE(_type *Variable, dataAddr uint64) (typ godwarf.Type, kind
 	return typ, kind, nil
 }
 
+// resolveParametricType returns the real type of t if t is a parametric
+// type, by reading the correct dictionary entry.
+func resolveParametricType(tgt *Target, bi *BinaryInfo, mem MemoryReadWriter, t godwarf.Type, dictAddr uint64) (godwarf.Type, error) {
+	ptyp, _ := t.(*godwarf.ParametricType)
+	if ptyp == nil {
+		return t, nil
+	}
+	if dictAddr == 0 {
+		return ptyp.TypedefType.Type, errors.New("parametric type without a dictionary")
+	}
+	rtypeAddr, err := readUintRaw(mem, dictAddr+uint64(ptyp.DictIndex*int64(bi.Arch.PtrSize())), int64(bi.Arch.PtrSize()))
+	if err != nil {
+		return ptyp.TypedefType.Type, err
+	}
+	runtimeType, err := bi.findType("runtime._type")
+	if err != nil {
+		return ptyp.TypedefType.Type, err
+	}
+	_type := newVariable("", rtypeAddr, runtimeType, bi, mem)
+
+	typ, _, err := runtimeTypeToDIE(_type, 0)
+	if err != nil {
+		return ptyp.TypedefType.Type, err
+	}
+
+	return typ, nil
+}
+
 type nameOfRuntimeTypeEntry struct {
 	typename string
 	kind     int64
diff --git a/pkg/proc/variables.go b/pkg/proc/variables.go
index 30ad20ff..e8bc08d4 100644
--- a/pkg/proc/variables.go
+++ b/pkg/proc/variables.go
@@ -18,6 +18,7 @@ import (
 	"github.com/go-delve/delve/pkg/dwarf/godwarf"
 	"github.com/go-delve/delve/pkg/dwarf/op"
 	"github.com/go-delve/delve/pkg/goversion"
+	"github.com/go-delve/delve/pkg/logflags"
 )
 
 const (
@@ -1135,7 +1136,7 @@ func readVarEntry(entry *godwarf.Tree, image *Image) (name string, typ godwarf.T
 
 // Extracts the name and type of a variable from a dwarf entry
 // then executes the instructions given in the  DW_AT_location attribute to grab the variable's address
-func extractVarInfoFromEntry(tgt *Target, bi *BinaryInfo, image *Image, regs op.DwarfRegisters, mem MemoryReadWriter, entry *godwarf.Tree) (*Variable, error) {
+func extractVarInfoFromEntry(tgt *Target, bi *BinaryInfo, image *Image, regs op.DwarfRegisters, mem MemoryReadWriter, entry *godwarf.Tree, dictAddr uint64) (*Variable, error) {
 	if entry.Tag != dwarf.TagFormalParameter && entry.Tag != dwarf.TagVariable {
 		return nil, fmt.Errorf("invalid entry tag, only supports FormalParameter and Variable, got %s", entry.Tag.String())
 	}
@@ -1145,6 +1146,12 @@ func extractVarInfoFromEntry(tgt *Target, bi *BinaryInfo, image *Image, regs op.
 		return nil, err
 	}
 
+	t, err = resolveParametricType(tgt, bi, mem, t, dictAddr)
+	if err != nil {
+		// Log the error, keep going with t, which will be the shape type
+		logflags.DebuggerLogger().Errorf("could not resolve parametric type of %s", n)
+	}
+
 	addr, pieces, descr, err := bi.Location(entry, dwarf.AttrLocation, regs.PC(), regs, mem)
 	if pieces != nil {
 		var cmem *compositeMemory
diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go
index cd230d70..028bd13f 100644
--- a/pkg/terminal/command.go
+++ b/pkg/terminal/command.go
@@ -147,6 +147,8 @@ The memory location is specified with the same expression language used by 'prin
 
 will watch the address of variable 'v'.
 
+Note that writes that do not change the value of the watched memory address might not be reported.
+
 See also: "help print".`},
 		{aliases: []string{"restart", "r"}, group: runCmds, cmdFn: restart, helpMsg: `Restart process.
 
diff --git a/service/dap/server.go b/service/dap/server.go
index fed11666..e0ba5382 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -92,15 +92,20 @@ import (
 // wrap-up and exit.
 type Server struct {
 	// config is all the information necessary to start the debugger and server.
-	config *service.Config
+	config *Config
 	// listener is used to accept the client connection.
 	listener net.Listener
-	// stopTriggered is closed when the server is Stop()-ed.
-	stopTriggered chan struct{}
-	// reader is used to read requests from the connection.
-	reader *bufio.Reader
-	// log is used for structured logging.
-	log *logrus.Entry
+	// session is the debug session that comes with an client connection.
+	session   *Session
+	sessionMu sync.Mutex
+}
+
+// Session is an abstraction for serving and shutting down
+// a DAP debug session with a pre-connected client.
+// TODO(polina): move this to a different file/package
+type Session struct {
+	config *Config
+
 	// stackFrameHandles maps frames of each goroutine to unique ids across all goroutines.
 	// Reset at every stop.
 	stackFrameHandles *handlesMap
@@ -120,7 +125,7 @@ type Server struct {
 	mu sync.Mutex
 
 	// conn is the accepted client connection.
-	conn net.Conn
+	conn io.ReadWriteCloser
 	// debugger is the underlying debugger service.
 	debugger *debugger.Debugger
 	// binaryToRemove is the temp compiled binary to be removed on disconnect (if any).
@@ -128,7 +133,7 @@ type Server struct {
 	// noDebugProcess is set for the noDebug launch process.
 	noDebugProcess *exec.Cmd
 
-	// sendingMu synchronizes writing to net.Conn
+	// sendingMu synchronizes writing to conn
 	// to ensure that messages do not get interleaved
 	sendingMu sync.Mutex
 
@@ -148,6 +153,18 @@ type Server struct {
 	changeStateMu sync.Mutex
 }
 
+// Config is all the information needed to start the debugger, handle
+// DAP connection traffic and signal to the server when it is time to stop.
+type Config struct {
+	*service.Config
+
+	// log is used for structured logging.
+	log *logrus.Entry
+	// stopTriggered is closed when the server is Stop()-ed.
+	// Can be used to safeguard against duplicate shutdown sequences.
+	stopTriggered chan struct{}
+}
+
 // launchAttachArgs captures arguments from launch/attach request that
 // impact handling of subsequent requests.
 type launchAttachArgs struct {
@@ -227,10 +244,26 @@ func NewServer(config *service.Config) *Server {
 	logflags.WriteDAPListeningMessage(config.Listener.Addr())
 	logger.Debug("DAP server pid = ", os.Getpid())
 	return &Server{
+		config: &Config{
+			Config:        config,
+			log:           logger,
+			stopTriggered: make(chan struct{}),
+		},
+		listener: config.Listener,
+	}
+}
+
+// NewSession creates a new client session that can handle DAP traffic.
+// It takes an open connection and provides a Close() method to shut it
+// down when the DAP session disconnects or a connection error occurs.
+func NewSession(conn io.ReadWriteCloser, config *Config) *Session {
+	if config.log == nil {
+		config.log = logflags.DAPLogger()
+	}
+	config.log.Debug("DAP connection started")
+	return &Session{
 		config:            config,
-		listener:          config.Listener,
-		stopTriggered:     make(chan struct{}),
-		log:               logger,
+		conn:              conn,
 		stackFrameHandles: newHandlesMap(),
 		variableHandles:   newVariablesHandlesMap(),
 		args:              defaultArgs,
@@ -240,7 +273,7 @@ func NewServer(config *service.Config) *Server {
 
 // If user-specified options are provided via Launch/AttachRequest,
 // we override the defaults for optional args.
-func (s *Server) setLaunchAttachArgs(args LaunchAttachCommonConfig) error {
+func (s *Session) setLaunchAttachArgs(args LaunchAttachCommonConfig) error {
 	s.args.stopOnEntry = args.StopOnEntry
 	if depth := args.StackTraceDepth; depth > 0 {
 		s.args.stackTraceDepth = depth
@@ -263,11 +296,25 @@ func (s *Server) setLaunchAttachArgs(args LaunchAttachCommonConfig) error {
 // connection. It shuts down the underlying debugger and kills the target
 // process if it was launched by it or stops the noDebug process.
 // This method mustn't be called more than once.
+// stopTriggered notifies other goroutines that stop is in progreess.
 func (s *Server) Stop() {
-	s.log.Debug("DAP server stopping...")
-	close(s.stopTriggered)
+	s.config.log.Debug("DAP server stopping...")
+	defer s.config.log.Debug("DAP server stopped")
+	close(s.config.stopTriggered)
+	// If run goroutine is blocked on accept, this will unblock it.
 	_ = s.listener.Close()
 
+	s.sessionMu.Lock()
+	defer s.sessionMu.Unlock()
+	if s.session == nil {
+		return
+	}
+	// If run goroutine is blocked on read, this will unblock it.
+	s.session.Close()
+}
+
+// Close closes the underlying debugger/process and connection.
+func (s *Session) Close() {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
@@ -282,25 +329,22 @@ func (s *Server) Stop() {
 		gobuild.Remove(s.binaryToRemove)
 	}
 	// Close client connection last, so other shutdown stages
-	// can send client notifications
-	if s.conn != nil {
-		// Unless Stop() was called after serveDAPCodec()
-		// returned, this will result in closed connection error
-		// on next read, breaking out of the read loop and
-		// allowing the run goroutine to exit.
-		_ = s.conn.Close()
-	}
-	s.log.Debug("DAP server stopped")
+	// can send client notifications.
+	// Unless Stop() was called after read loop in serveDAPCodec()
+	// returned, this will result in a closed connection error
+	// on next read, breaking out the read loop andd
+	// allowing the run goroutinee to exit.
+	_ = s.conn.Close()
 }
 
-// triggerServerStop closes config.DisconnectChan if not nil, which
+// triggerServerStop closes DisconnectChan if not nil, which
 // signals that client sent a disconnect request or there was connection
 // failure or closure. Since the server currently services only one
 // client, this is used as a signal to stop the entire server.
 // The function safeguards agaist closing the channel more
 // than once and can be called multiple times. It is not thread-safe
 // and is currently only called from the run goroutine.
-func (s *Server) triggerServerStop() {
+func (c *Config) triggerServerStop() {
 	// Avoid accidentally closing the channel twice and causing a panic, when
 	// this function is called more than once. For example, we could have the
 	// following sequence of events:
@@ -310,9 +354,9 @@ func (s *Server) triggerServerStop() {
 	// -- main goroutine: Stop() closes client connection (or client closed it)
 	// -- run goroutine: serveDAPCodec() gets "closed network connection"
 	// -- run goroutine: serveDAPCodec() returns and calls triggerServerStop()
-	if s.config.DisconnectChan != nil {
-		close(s.config.DisconnectChan)
-		s.config.DisconnectChan = nil
+	if c.DisconnectChan != nil {
+		close(c.DisconnectChan)
+		c.DisconnectChan = nil
 	}
 	// There should be no logic here after the stop-server
 	// signal that might cause everything to shutdown before this
@@ -331,34 +375,35 @@ func (s *Server) Run() {
 		conn, err := s.listener.Accept() // listener is closed in Stop()
 		if err != nil {
 			select {
-			case <-s.stopTriggered:
+			case <-s.config.stopTriggered:
 			default:
-				s.log.Errorf("Error accepting client connection: %s\n", err)
-				s.triggerServerStop()
+				s.config.log.Errorf("Error accepting client connection: %s\n", err)
+				s.config.triggerServerStop()
 			}
 			return
 		}
 		if s.config.CheckLocalConnUser {
 			if !sameuser.CanAccept(s.listener.Addr(), conn.LocalAddr(), conn.RemoteAddr()) {
-				s.log.Error("Error accepting client connection: Only connections from the same user that started this instance of Delve are allowed to connect. See --only-same-user.")
-				s.triggerServerStop()
+				s.config.log.Error("Error accepting client connection: Only connections from the same user that started this instance of Delve are allowed to connect. See --only-same-user.")
+				s.config.triggerServerStop()
 				return
 			}
 		}
-		s.mu.Lock()
-		s.conn = conn // closed in Stop()
-		s.mu.Unlock()
-		s.serveDAPCodec()
+		s.sessionMu.Lock()
+		s.session = NewSession(conn, s.config) // closed in Stop()
+		s.sessionMu.Unlock()
+		s.session.serveDAPCodec()
 	}()
 }
 
 // serveDAPCodec reads and decodes requests from the client
 // until it encounters an error or EOF, when it sends
 // a disconnect signal and returns.
-func (s *Server) serveDAPCodec() {
-	s.reader = bufio.NewReader(s.conn)
+func (s *Session) serveDAPCodec() {
+	// TODO(polina): defer-close conn/session like in serveJSONCodec
+	reader := bufio.NewReader(s.conn)
 	for {
-		request, err := dap.ReadProtocolMessage(s.reader)
+		request, err := dap.ReadProtocolMessage(reader)
 		// Handle dap.DecodeProtocolMessageFieldError errors gracefully by responding with an ErrorResponse.
 		// For example:
 		// -- "Request command 'foo' is not supported" means we
@@ -368,18 +413,19 @@ func (s *Server) serveDAPCodec() {
 		// Other errors, such as unmarshalling errors, will log the error and cause the server to trigger
 		// a stop.
 		if err != nil {
+			s.config.log.Debug("DAP error: ", err)
 			select {
-			case <-s.stopTriggered:
+			case <-s.config.stopTriggered:
 			default:
-				if err != io.EOF {
+				if err != io.EOF { // EOF means client closed connection
 					if decodeErr, ok := err.(*dap.DecodeProtocolMessageFieldError); ok {
 						// Send an error response to the users if we were unable to process the message.
 						s.sendInternalErrorResponse(decodeErr.Seq, err.Error())
 						continue
 					}
-					s.log.Error("DAP error: ", err)
+					s.config.log.Error("DAP error: ", err)
 				}
-				s.triggerServerStop()
+				s.config.triggerServerStop()
 			}
 			return
 		}
@@ -390,17 +436,17 @@ func (s *Server) serveDAPCodec() {
 // In case a handler panics, we catch the panic to avoid crashing both
 // the server and the target. We send an error response back, but
 // in case its a dup and ignored by the client, we also log the error.
-func (s *Server) recoverPanic(request dap.Message) {
+func (s *Session) recoverPanic(request dap.Message) {
 	if ierr := recover(); ierr != nil {
-		s.log.Errorf("recovered panic: %s\n%s\n", ierr, debug.Stack())
+		s.config.log.Errorf("recovered panic: %s\n%s\n", ierr, debug.Stack())
 		s.sendInternalErrorResponse(request.GetSeq(), fmt.Sprintf("%v", ierr))
 	}
 }
 
-func (s *Server) handleRequest(request dap.Message) {
+func (s *Session) handleRequest(request dap.Message) {
 	defer s.recoverPanic(request)
 	jsonmsg, _ := json.Marshal(request)
-	s.log.Debug("[<- from client]", string(jsonmsg))
+	s.config.log.Debug("[<- from client]", string(jsonmsg))
 
 	if _, ok := request.(dap.RequestMessage); !ok {
 		s.sendInternalErrorResponse(request.GetSeq(), fmt.Sprintf("Unable to process non-request %#v\n", request))
@@ -476,7 +522,7 @@ func (s *Server) handleRequest(request dap.Message) {
 		case *dap.SetBreakpointsRequest:
 			s.changeStateMu.Lock()
 			defer s.changeStateMu.Unlock()
-			s.log.Debug("halting execution to set breakpoints")
+			s.config.log.Debug("halting execution to set breakpoints")
 			_, err := s.halt()
 			if err != nil {
 				s.sendErrorResponse(request.Request, UnableToSetBreakpoints, "Unable to set or clear breakpoints", err.Error())
@@ -501,7 +547,7 @@ func (s *Server) handleRequest(request dap.Message) {
 		case *dap.SetFunctionBreakpointsRequest:
 			s.changeStateMu.Lock()
 			defer s.changeStateMu.Unlock()
-			s.log.Debug("halting execution to set breakpoints")
+			s.config.log.Debug("halting execution to set breakpoints")
 			_, err := s.halt()
 			if err != nil {
 				s.sendErrorResponse(request.Request, UnableToSetBreakpoints, "Unable to set or clear breakpoints", err.Error())
@@ -687,18 +733,21 @@ func (s *Server) handleRequest(request dap.Message) {
 	}
 }
 
-func (s *Server) send(message dap.Message) {
+func (s *Session) send(message dap.Message) {
 	jsonmsg, _ := json.Marshal(message)
-	s.log.Debug("[-> to client]", string(jsonmsg))
+	s.config.log.Debug("[-> to client]", string(jsonmsg))
 	// TODO(polina): consider using a channel for all the sends and to have a dedicated
 	// goroutine that reads from that channel and sends over the connection.
 	// This will avoid blocking on slow network sends.
 	s.sendingMu.Lock()
 	defer s.sendingMu.Unlock()
-	_ = dap.WriteProtocolMessage(s.conn, message)
+	err := dap.WriteProtocolMessage(s.conn, message)
+	if err != nil {
+		s.config.log.Debug(err)
+	}
 }
 
-func (s *Server) logToConsole(msg string) {
+func (s *Session) logToConsole(msg string) {
 	s.send(&dap.OutputEvent{
 		Event: *newEvent("output"),
 		Body: dap.OutputEventBody{
@@ -707,7 +756,7 @@ func (s *Server) logToConsole(msg string) {
 		}})
 }
 
-func (s *Server) onInitializeRequest(request *dap.InitializeRequest) {
+func (s *Session) onInitializeRequest(request *dap.InitializeRequest) {
 	s.setClientCapabilities(request.Arguments)
 	if request.Arguments.PathFormat != "path" {
 		s.sendErrorResponse(request.Request, FailedToInitialize, "Failed to initialize",
@@ -750,7 +799,7 @@ func (s *Server) onInitializeRequest(request *dap.InitializeRequest) {
 	s.send(response)
 }
 
-func (s *Server) setClientCapabilities(args dap.InitializeRequestArguments) {
+func (s *Session) setClientCapabilities(args dap.InitializeRequestArguments) {
 	s.clientCapabilities.supportsMemoryReferences = args.SupportsMemoryReferences
 	s.clientCapabilities.supportsProgressReporting = args.SupportsProgressReporting
 	s.clientCapabilities.supportsRunInTerminalRequest = args.SupportsRunInTerminalRequest
@@ -769,7 +818,7 @@ func cleanExeName(name string) string {
 	return name
 }
 
-func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
+func (s *Session) onLaunchRequest(request *dap.LaunchRequest) {
 	if s.debugger != nil {
 		s.sendShowUserErrorResponse(request.Request, FailedToLaunch,
 			"Failed to launch", "debugger already started - use remote attach to connect to a server with an active debug session")
@@ -834,7 +883,7 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
 		s.config.Debugger.Backend = "core"
 	}
 
-	s.log.Debugf("debug backend is '%s'", s.config.Debugger.Backend)
+	s.config.log.Debugf("debug backend is '%s'", s.config.Debugger.Backend)
 
 	// Prepare the debug executable filename, build flags and build it
 	if mode == "debug" || mode == "test" {
@@ -853,7 +902,7 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
 		var cmd string
 		var out []byte
 		wd, _ := os.Getwd()
-		s.log.Debugf("building program '%s' in '%s' with flags '%v'", program, wd, buildFlags)
+		s.config.log.Debugf("building program '%s' in '%s' with flags '%v'", program, wd, buildFlags)
 		switch mode {
 		case "debug":
 			cmd, out, err = gobuild.GoBuildCombinedOutput(debugbinary, []string{program}, buildFlags)
@@ -890,7 +939,7 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
 		s.config.Debugger.WorkingDir = args.Cwd
 	}
 
-	s.log.Debugf("running binary '%s' in '%s'", program, s.config.Debugger.WorkingDir)
+	s.config.log.Debugf("running binary '%s' in '%s'", program, s.config.Debugger.WorkingDir)
 	if args.NoDebug {
 		s.mu.Lock()
 		cmd, err := s.newNoDebugProcess(program, args.Args, s.config.Debugger.WorkingDir)
@@ -906,7 +955,7 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
 		// Start the program on a different goroutine, so we can listen for disconnect request.
 		go func() {
 			if err := cmd.Wait(); err != nil {
-				s.log.Debugf("program exited with error: %v", err)
+				s.config.log.Debugf("program exited with error: %v", err)
 			}
 			stopped := false
 			s.mu.Lock()
@@ -946,7 +995,7 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
 
 // newNoDebugProcess is called from onLaunchRequest (run goroutine) and
 // requires holding mu lock. It prepares process exec.Cmd to be started.
-func (s *Server) newNoDebugProcess(program string, targetArgs []string, wd string) (*exec.Cmd, error) {
+func (s *Session) newNoDebugProcess(program string, targetArgs []string, wd string) (*exec.Cmd, error) {
 	if s.noDebugProcess != nil {
 		return nil, fmt.Errorf("another launch request is in progress")
 	}
@@ -961,7 +1010,7 @@ func (s *Server) newNoDebugProcess(program string, targetArgs []string, wd strin
 
 // stopNoDebugProcess is called from Stop (main goroutine) and
 // onDisconnectRequest (run goroutine) and requires holding mu lock.
-func (s *Server) stopNoDebugProcess() {
+func (s *Session) stopNoDebugProcess() {
 	if s.noDebugProcess == nil {
 		// We already handled termination or there was never a process
 		return
@@ -981,8 +1030,8 @@ func (s *Server) stopNoDebugProcess() {
 // (in our case this TCP server) can be terminated.
 // TODO(polina): differentiate between single- and multi-client
 // server mode when handling requests for debug session shutdown.
-func (s *Server) onDisconnectRequest(request *dap.DisconnectRequest) {
-	defer s.triggerServerStop()
+func (s *Session) onDisconnectRequest(request *dap.DisconnectRequest) {
+	defer s.config.triggerServerStop()
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
@@ -1011,12 +1060,13 @@ func (s *Server) onDisconnectRequest(request *dap.DisconnectRequest) {
 // stopDebugSession is called from Stop (main goroutine) and
 // onDisconnectRequest (run goroutine) and requires holding mu lock.
 // Returns any detach error other than proc.ErrProcessExited.
-func (s *Server) stopDebugSession(killProcess bool) error {
+func (s *Session) stopDebugSession(killProcess bool) error {
 	s.changeStateMu.Lock()
 	defer s.changeStateMu.Unlock()
 	if s.debugger == nil {
 		return nil
 	}
+	// TODO(polina): reset debuggeer to nil at the end
 	var err error
 	var exited error
 	// Halting will stop any debugger command that's pending on another
@@ -1027,7 +1077,7 @@ func (s *Server) stopDebugSession(killProcess bool) error {
 	// Additional clean-up might be especially critical when we support multiple clients.
 	state, err := s.halt()
 	if err == proc.ErrProcessDetached {
-		s.log.Debug("halt returned error: ", err)
+		s.config.log.Debug("halt returned error: ", err)
 		return nil
 	}
 	if err != nil {
@@ -1035,14 +1085,14 @@ func (s *Server) stopDebugSession(killProcess bool) error {
 		case proc.ErrProcessExited:
 			exited = err
 		default:
-			s.log.Error("halt returned error: ", err)
+			s.config.log.Error("halt returned error: ", err)
 			if err.Error() == "no such process" {
 				exited = err
 			}
 		}
 	} else if state.Exited {
 		exited = proc.ErrProcessExited{Pid: s.debugger.ProcessPid(), Status: state.ExitStatus}
-		s.log.Debug("halt returned state: ", exited)
+		s.config.log.Debug("halt returned state: ", exited)
 	}
 	if exited != nil {
 		// TODO(suzmue): log exited error when the process exits, which may have been before
@@ -1058,11 +1108,11 @@ func (s *Server) stopDebugSession(killProcess bool) error {
 	if err != nil {
 		switch err.(type) {
 		case proc.ErrProcessExited:
-			s.log.Debug(err)
+			s.config.log.Debug(err)
 			s.logToConsole(exited.Error())
 			err = nil
 		default:
-			s.log.Error("detach returned error: ", err)
+			s.config.log.Error("detach returned error: ", err)
 		}
 	}
 	return err
@@ -1070,7 +1120,7 @@ func (s *Server) stopDebugSession(killProcess bool) error {
 
 // halt sends a halt request if the debuggee is running.
 // changeStateMu should be held when calling (*Server).halt.
-func (s *Server) halt() (*api.DebuggerState, error) {
+func (s *Session) halt() (*api.DebuggerState, error) {
 	s.setHaltRequested(true)
 	// Only send a halt request if the debuggee is running.
 	if s.debugger.IsRunning() {
@@ -1079,13 +1129,13 @@ func (s *Server) halt() (*api.DebuggerState, error) {
 	return s.debugger.State(false)
 }
 
-func (s *Server) isNoDebug() bool {
+func (s *Session) isNoDebug() bool {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 	return s.noDebugProcess != nil
 }
 
-func (s *Server) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) {
+func (s *Session) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) {
 	if request.Arguments.Source.Path == "" {
 		s.sendErrorResponse(request.Request, UnableToSetBreakpoints, "Unable to set or clear breakpoints", "empty file path")
 		return
@@ -1192,7 +1242,7 @@ func updateBreakpointsResponse(breakpoints []dap.Breakpoint, i int, err error, g
 // in this request.
 const functionBpPrefix = "functionBreakpoint"
 
-func (s *Server) onSetFunctionBreakpointsRequest(request *dap.SetFunctionBreakpointsRequest) {
+func (s *Session) onSetFunctionBreakpointsRequest(request *dap.SetFunctionBreakpointsRequest) {
 	// According to the spec, setFunctionBreakpoints "replaces all existing function
 	// breakpoints with new function breakpoints." The simplest way is
 	// to clear all and then set all. To maintain state (for hit count conditions)
@@ -1286,7 +1336,7 @@ func (s *Server) onSetFunctionBreakpointsRequest(request *dap.SetFunctionBreakpo
 			continue
 		}
 		if len(locs) > 0 {
-			s.log.Debugf("multiple locations found for %s", want.Name)
+			s.config.log.Debugf("multiple locations found for %s", want.Name)
 			breakpoints[i].Message = fmt.Sprintf("multiple locations found for %s, function breakpoint is only set for the first location", want.Name)
 		}
 
@@ -1307,7 +1357,7 @@ func (s *Server) onSetFunctionBreakpointsRequest(request *dap.SetFunctionBreakpo
 	s.send(response)
 }
 
-func (s *Server) clearBreakpoints(existingBps map[string]*api.Breakpoint, bpAdded map[string]struct{}) error {
+func (s *Session) clearBreakpoints(existingBps map[string]*api.Breakpoint, bpAdded map[string]struct{}) error {
 	for req, bp := range existingBps {
 		if _, ok := bpAdded[req]; ok {
 			continue
@@ -1320,7 +1370,7 @@ func (s *Server) clearBreakpoints(existingBps map[string]*api.Breakpoint, bpAdde
 	return nil
 }
 
-func (s *Server) getMatchingBreakpoints(prefix string) map[string]*api.Breakpoint {
+func (s *Session) getMatchingBreakpoints(prefix string) map[string]*api.Breakpoint {
 	existing := s.debugger.Breakpoints(false)
 	matchingBps := make(map[string]*api.Breakpoint, len(existing))
 	for _, bp := range existing {
@@ -1337,7 +1387,7 @@ func (s *Server) getMatchingBreakpoints(prefix string) map[string]*api.Breakpoin
 	return matchingBps
 }
 
-func (s *Server) onSetExceptionBreakpointsRequest(request *dap.SetExceptionBreakpointsRequest) {
+func (s *Session) onSetExceptionBreakpointsRequest(request *dap.SetExceptionBreakpointsRequest) {
 	// Unlike what DAP documentation claims, this request is always sent
 	// even though we specified no filters at initialization. Handle as no-op.
 	s.send(&dap.SetExceptionBreakpointsResponse{Response: *newResponse(request.Request)})
@@ -1358,7 +1408,7 @@ func closeIfOpen(ch chan struct{}) {
 // This is an optional request enabled by capability ‘supportsConfigurationDoneRequest’.
 // It gets triggered after all the debug requests that follow initalized event,
 // so the s.debugger is guaranteed to be set.
-func (s *Server) onConfigurationDoneRequest(request *dap.ConfigurationDoneRequest, allowNextStateChange chan struct{}) {
+func (s *Session) onConfigurationDoneRequest(request *dap.ConfigurationDoneRequest, allowNextStateChange chan struct{}) {
 	defer closeIfOpen(allowNextStateChange)
 	if s.args.stopOnEntry {
 		e := &dap.StoppedEvent{
@@ -1377,7 +1427,7 @@ func (s *Server) onConfigurationDoneRequest(request *dap.ConfigurationDoneReques
 
 // onContinueRequest handles 'continue' request.
 // This is a mandatory request to support.
-func (s *Server) onContinueRequest(request *dap.ContinueRequest, allowNextStateChange chan struct{}) {
+func (s *Session) onContinueRequest(request *dap.ContinueRequest, allowNextStateChange chan struct{}) {
 	s.send(&dap.ContinueResponse{
 		Response: *newResponse(request.Request),
 		Body:     dap.ContinueResponseBody{AllThreadsContinued: true}})
@@ -1409,7 +1459,7 @@ func fnPackageName(loc *proc.Location) string {
 // (dummy) thread". Therefore, this handler never returns
 // an error response. If the dummy thread is returned in its place,
 // the next waterfall request for its stackTrace will return the error.
-func (s *Server) onThreadsRequest(request *dap.ThreadsRequest) {
+func (s *Session) onThreadsRequest(request *dap.ThreadsRequest) {
 	var err error
 	var gs []*proc.G
 	var next int
@@ -1423,7 +1473,7 @@ func (s *Server) onThreadsRequest(request *dap.ThreadsRequest) {
 		case proc.ErrProcessExited:
 			// If the program exits very quickly, the initial threads request will complete after it has exited.
 			// A TerminatedEvent has already been sent. Ignore the err returned in this case.
-			s.log.Debug(err)
+			s.config.log.Debug(err)
 		default:
 			s.send(&dap.OutputEvent{
 				Event: *newEvent("output"),
@@ -1438,7 +1488,7 @@ func (s *Server) onThreadsRequest(request *dap.ThreadsRequest) {
 	} else {
 		state, err := s.debugger.State( /*nowait*/ true)
 		if err != nil {
-			s.log.Debug("Unable to get debugger state: ", err)
+			s.config.log.Debug("Unable to get debugger state: ", err)
 		}
 
 		if next >= 0 {
@@ -1457,7 +1507,7 @@ func (s *Server) onThreadsRequest(request *dap.ThreadsRequest) {
 				if !selectedFound {
 					g, err := s.debugger.FindGoroutine(state.SelectedGoroutine.ID)
 					if err != nil {
-						s.log.Debug("Error getting selected goroutine: ", err)
+						s.config.log.Debug("Error getting selected goroutine: ", err)
 					} else {
 						// TODO(suzmue): Consider putting the selected goroutine at the top.
 						// To be consistent we may want to do this for all threads requests.
@@ -1502,7 +1552,7 @@ func (s *Server) onThreadsRequest(request *dap.ThreadsRequest) {
 //      Required args: processID
 // -- "remote" - attaches client to a debugger already attached to a process
 //      Required args: none (host/port are used externally to connect)
-func (s *Server) onAttachRequest(request *dap.AttachRequest) {
+func (s *Session) onAttachRequest(request *dap.AttachRequest) {
 	var args AttachConfig = defaultAttachConfig // narrow copy for initializing non-zero default values
 	if err := unmarshalLaunchAttachArgs(request.Arguments, &args); err != nil {
 		s.sendShowUserErrorResponse(request.Request, FailedToAttach, "Failed to attach", fmt.Sprintf("invalid debug configuration - %v", err))
@@ -1527,7 +1577,7 @@ func (s *Server) onAttachRequest(request *dap.AttachRequest) {
 			return
 		}
 		s.config.Debugger.AttachPid = args.ProcessID
-		s.log.Debugf("attaching to pid %d", args.ProcessID)
+		s.config.log.Debugf("attaching to pid %d", args.ProcessID)
 		if backend := args.Backend; backend != "" {
 			s.config.Debugger.Backend = backend
 		} else {
@@ -1548,7 +1598,7 @@ func (s *Server) onAttachRequest(request *dap.AttachRequest) {
 			s.sendShowUserErrorResponse(request.Request, FailedToAttach, "Failed to attach", "no debugger found")
 			return
 		}
-		s.log.Debug("debugger already started")
+		s.config.log.Debug("debugger already started")
 		// TODO(polina): once we allow initialize and attach request while running,
 		// halt before sending initialized event. onConfigurationDone will restart
 		// execution if user requested !stopOnEntry.
@@ -1577,26 +1627,26 @@ func (s *Server) onAttachRequest(request *dap.AttachRequest) {
 
 // onNextRequest handles 'next' request.
 // This is a mandatory request to support.
-func (s *Server) onNextRequest(request *dap.NextRequest, allowNextStateChange chan struct{}) {
+func (s *Session) onNextRequest(request *dap.NextRequest, allowNextStateChange chan struct{}) {
 	s.sendStepResponse(request.Arguments.ThreadId, &dap.NextResponse{Response: *newResponse(request.Request)})
 	s.stepUntilStopAndNotify(api.Next, request.Arguments.ThreadId, allowNextStateChange)
 }
 
 // onStepInRequest handles 'stepIn' request
 // This is a mandatory request to support.
-func (s *Server) onStepInRequest(request *dap.StepInRequest, allowNextStateChange chan struct{}) {
+func (s *Session) onStepInRequest(request *dap.StepInRequest, allowNextStateChange chan struct{}) {
 	s.sendStepResponse(request.Arguments.ThreadId, &dap.StepInResponse{Response: *newResponse(request.Request)})
 	s.stepUntilStopAndNotify(api.Step, request.Arguments.ThreadId, allowNextStateChange)
 }
 
 // onStepOutRequest handles 'stepOut' request
 // This is a mandatory request to support.
-func (s *Server) onStepOutRequest(request *dap.StepOutRequest, allowNextStateChange chan struct{}) {
+func (s *Session) onStepOutRequest(request *dap.StepOutRequest, allowNextStateChange chan struct{}) {
 	s.sendStepResponse(request.Arguments.ThreadId, &dap.StepOutResponse{Response: *newResponse(request.Request)})
 	s.stepUntilStopAndNotify(api.StepOut, request.Arguments.ThreadId, allowNextStateChange)
 }
 
-func (s *Server) sendStepResponse(threadId int, message dap.Message) {
+func (s *Session) sendStepResponse(threadId int, message dap.Message) {
 	// All of the threads will be continued by this request, so we need to send
 	// a continued event so the UI can properly reflect the current state.
 	s.send(&dap.ContinuedEvent{
@@ -1620,7 +1670,7 @@ func stoppedGoroutineID(state *api.DebuggerState) (id int) {
 
 // stoppedOnBreakpointGoroutineID gets the goroutine id of the first goroutine
 // that is stopped on a real breakpoint, starting with the selected goroutine.
-func (s *Server) stoppedOnBreakpointGoroutineID(state *api.DebuggerState) (int, *api.Breakpoint) {
+func (s *Session) stoppedOnBreakpointGoroutineID(state *api.DebuggerState) (int, *api.Breakpoint) {
 	// Check if the selected goroutine is stopped on a real breakpoint
 	// since we would prefer to use that one.
 	goid := stoppedGoroutineID(state)
@@ -1647,17 +1697,17 @@ func (s *Server) stoppedOnBreakpointGoroutineID(state *api.DebuggerState) (int,
 // a channel that will be closed to signal that an
 // asynchornous command has completed setup or was interrupted
 // due to an error, so the server is ready to receive new requests.
-func (s *Server) stepUntilStopAndNotify(command string, threadId int, allowNextStateChange chan struct{}) {
+func (s *Session) stepUntilStopAndNotify(command string, threadId int, allowNextStateChange chan struct{}) {
 	defer closeIfOpen(allowNextStateChange)
 	_, err := s.debugger.Command(&api.DebuggerCommand{Name: api.SwitchGoroutine, GoroutineID: threadId}, nil)
 	if err != nil {
-		s.log.Errorf("Error switching goroutines while stepping: %v", err)
+		s.config.log.Errorf("Error switching goroutines while stepping: %v", err)
 		// If we encounter an error, we will have to send a stopped event
 		// since we already sent the step response.
 		stopped := &dap.StoppedEvent{Event: *newEvent("stopped")}
 		stopped.Body.AllThreadsStopped = true
 		if state, err := s.debugger.State(false); err != nil {
-			s.log.Errorf("Error retrieving state: %e", err)
+			s.config.log.Errorf("Error retrieving state: %e", err)
 		} else {
 			stopped.Body.ThreadId = stoppedGoroutineID(state)
 		}
@@ -1671,7 +1721,7 @@ func (s *Server) stepUntilStopAndNotify(command string, threadId int, allowNextS
 
 // onPauseRequest handles 'pause' request.
 // This is a mandatory request to support.
-func (s *Server) onPauseRequest(request *dap.PauseRequest) {
+func (s *Session) onPauseRequest(request *dap.PauseRequest) {
 	s.changeStateMu.Lock()
 	defer s.changeStateMu.Unlock()
 	_, err := s.halt()
@@ -1698,7 +1748,7 @@ type stackFrame struct {
 // This is a mandatory request to support.
 // As per DAP spec, this request only gets triggered as a follow-up
 // to a successful threads request as part of the "request waterfall".
-func (s *Server) onStackTraceRequest(request *dap.StackTraceRequest) {
+func (s *Session) onStackTraceRequest(request *dap.StackTraceRequest) {
 	if s.debugger == nil {
 		s.sendErrorResponse(request.Request, UnableToProduceStackTrace, "Unable to produce stack trace", "debugger is nil")
 		return
@@ -1767,7 +1817,7 @@ func (s *Server) onStackTraceRequest(request *dap.StackTraceRequest) {
 // It is automatically sent as part of the threads > stacktrace > scopes > variables
 // "waterfall" to highlight the topmost frame at stops, after an evaluate request
 // for the selected scope or when a user selects different scopes in the UI.
-func (s *Server) onScopesRequest(request *dap.ScopesRequest) {
+func (s *Session) onScopesRequest(request *dap.ScopesRequest) {
 	sf, ok := s.stackFrameHandles.get(request.Arguments.FrameId)
 	if !ok {
 		s.sendErrorResponse(request.Request, UnableToListLocals, "Unable to list locals", fmt.Sprintf("unknown frame id %d", request.Arguments.FrameId))
@@ -1858,7 +1908,7 @@ func slicePtrVarToSliceVar(vars []*proc.Variable) []proc.Variable {
 
 // onVariablesRequest handles 'variables' requests.
 // This is a mandatory request to support.
-func (s *Server) onVariablesRequest(request *dap.VariablesRequest) {
+func (s *Session) onVariablesRequest(request *dap.VariablesRequest) {
 	ref := request.Arguments.VariablesReference
 	v, ok := s.variableHandles.get(ref)
 	if !ok {
@@ -1902,7 +1952,7 @@ func (s *Server) onVariablesRequest(request *dap.VariablesRequest) {
 	s.send(response)
 }
 
-func (s *Server) maybeLoadResliced(v *fullyQualifiedVariable, start, count int) (*fullyQualifiedVariable, error) {
+func (s *Session) maybeLoadResliced(v *fullyQualifiedVariable, start, count int) (*fullyQualifiedVariable, error) {
 	if start == 0 && count == len(v.Children) {
 		// If we have already loaded the correct children,
 		// just return the variable.
@@ -1927,7 +1977,7 @@ func getIndexedVariableCount(c *proc.Variable) int {
 }
 
 // childrenToDAPVariables returns the DAP presentation of the referenced variable's children.
-func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variable, error) {
+func (s *Session) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variable, error) {
 	// TODO(polina): consider convertVariableToString instead of convertVariable
 	// and avoid unnecessary creation of variable handles when this is called to
 	// compute evaluate names when this is called from onSetVariableRequest.
@@ -2083,7 +2133,7 @@ func getNamedVariableCount(v *proc.Variable) int {
 
 // metadataToDAPVariables returns the DAP presentation of the referenced variable's metadata.
 // These are included as named variables
-func (s *Server) metadataToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variable, error) {
+func (s *Session) metadataToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variable, error) {
 	children := []dap.Variable{} // must return empty array, not null, if no children
 
 	if v.Kind == reflect.Map && v.Len > 0 {
@@ -2100,7 +2150,7 @@ func (s *Server) metadataToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab
 		typeName := api.PrettyTypeName(v.DwarfType)
 		loadExpr := fmt.Sprintf("string(*(*%q)(%#x))", typeName, v.Addr)
 
-		s.log.Debugf("loading %s (type %s) with %s", v.fullyQualifiedNameOrExpr, typeName, loadExpr)
+		s.config.log.Debugf("loading %s (type %s) with %s", v.fullyQualifiedNameOrExpr, typeName, loadExpr)
 		// We know that this is an array/slice of Uint8 or Int32, so we will load up to MaxStringLen.
 		config := DefaultLoadConfig
 		config.MaxArrayValues = config.MaxStringLen
@@ -2127,7 +2177,7 @@ func isListOfBytesOrRunes(v *proc.Variable) bool {
 	return false
 }
 
-func (s *Server) getTypeIfSupported(v *proc.Variable) string {
+func (s *Session) getTypeIfSupported(v *proc.Variable) string {
 	if !s.clientCapabilities.supportsVariableType {
 		return ""
 	}
@@ -2143,11 +2193,11 @@ func (s *Server) getTypeIfSupported(v *proc.Variable) string {
 // variables request can be issued to get the elements of the compound variable. As a
 // custom, a zero reference, reminiscent of a zero pointer, is used to indicate that
 // a scalar variable cannot be "dereferenced" to get its elements (as there are none).
-func (s *Server) convertVariable(v *proc.Variable, qualifiedNameOrExpr string) (value string, variablesReference int) {
+func (s *Session) convertVariable(v *proc.Variable, qualifiedNameOrExpr string) (value string, variablesReference int) {
 	return s.convertVariableWithOpts(v, qualifiedNameOrExpr, 0)
 }
 
-func (s *Server) convertVariableToString(v *proc.Variable) string {
+func (s *Session) convertVariableToString(v *proc.Variable) string {
 	val, _ := s.convertVariableWithOpts(v, "", skipRef)
 	return val
 }
@@ -2171,7 +2221,7 @@ const (
 // a string representation of the variable. When the variable is a compound or reference
 // type variable and its full string representation can be larger than defaultMaxValueLen,
 // this returns a truncated value unless showFull option flag is set.
-func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr string, opts convertVariableFlags) (value string, variablesReference int) {
+func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr string, opts convertVariableFlags) (value string, variablesReference int) {
 	canHaveRef := false
 	maybeCreateVariableHandle := func(v *proc.Variable) int {
 		canHaveRef = true
@@ -2197,7 +2247,7 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
 		value = api.ConvertVar(v).SinglelineString()
 		typeName := api.PrettyTypeName(v.DwarfType)
 		loadExpr := fmt.Sprintf("*(*%q)(%#x)", typeName, v.Addr)
-		s.log.Debugf("loading %s (type %s) with %s", qualifiedNameOrExpr, typeName, loadExpr)
+		s.config.log.Debugf("loading %s (type %s) with %s", qualifiedNameOrExpr, typeName, loadExpr)
 		// Make sure we can load the pointers directly, not by updating just the child
 		// This is not really necessary now because users have no way of setting FollowPointers to false.
 		config := DefaultLoadConfig
@@ -2232,7 +2282,7 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
 					// TODO(polina): see if reloadVariable can be reused here
 					cTypeName := api.PrettyTypeName(v.Children[0].DwarfType)
 					cLoadExpr := fmt.Sprintf("*(*%q)(%#x)", cTypeName, v.Children[0].Addr)
-					s.log.Debugf("loading *(%s) (type %s) with %s", qualifiedNameOrExpr, cTypeName, cLoadExpr)
+					s.config.log.Debugf("loading *(%s) (type %s) with %s", qualifiedNameOrExpr, cTypeName, cLoadExpr)
 					cLoaded, err := s.debugger.EvalVariableInScope(-1, 0, 0, cLoadExpr, DefaultLoadConfig)
 					if err != nil {
 						value += fmt.Sprintf(" - FAILED TO LOAD: %s", err)
@@ -2330,7 +2380,7 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
 // TODO(polina): users have complained about having to click to expand multi-level
 // variables, so consider also adding the following:
 // -- print {expression} - return the result as a string like from dlv cli
-func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
+func (s *Session) onEvaluateRequest(request *dap.EvaluateRequest) {
 	showErrorToUser := request.Arguments.Context != "watch" && request.Arguments.Context != "repl" && request.Arguments.Context != "hover"
 	if s.debugger == nil {
 		s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", "debugger is nil", showErrorToUser)
@@ -2385,7 +2435,7 @@ func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
 					loadCfg := DefaultLoadConfig
 					loadCfg.MaxStringLen = maxSingleStringLen
 					if v, err := s.debugger.EvalVariableInScope(goid, frame, 0, request.Arguments.Expression, loadCfg); err != nil {
-						s.log.Debugf("Failed to load more for %v: %v", request.Arguments.Expression, err)
+						s.config.log.Debugf("Failed to load more for %v: %v", request.Arguments.Expression, err)
 					} else {
 						exprVar = v
 					}
@@ -2404,7 +2454,7 @@ func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
 	s.send(response)
 }
 
-func (s *Server) doCall(goid, frame int, expr string) (*api.DebuggerState, []*proc.Variable, error) {
+func (s *Session) doCall(goid, frame int, expr string) (*api.DebuggerState, []*proc.Variable, error) {
 	// This call might be evaluated in the context of the frame that is not topmost
 	// if the editor is set to view the variables for one of the parent frames.
 	// If the call expression refers to any of these variables, unlike regular
@@ -2500,7 +2550,7 @@ func (s *Server) doCall(goid, frame int, expr string) (*api.DebuggerState, []*pr
 	return state, retVars, nil
 }
 
-func (s *Server) sendStoppedEvent(state *api.DebuggerState) {
+func (s *Session) sendStoppedEvent(state *api.DebuggerState) {
 	stopped := &dap.StoppedEvent{Event: *newEvent("stopped")}
 	stopped.Body.AllThreadsStopped = true
 	stopped.Body.ThreadId = stoppedGoroutineID(state)
@@ -2510,19 +2560,19 @@ func (s *Server) sendStoppedEvent(state *api.DebuggerState) {
 
 // onTerminateRequest sends a not-yet-implemented error response.
 // Capability 'supportsTerminateRequest' is not set in 'initialize' response.
-func (s *Server) onTerminateRequest(request *dap.TerminateRequest) {
+func (s *Session) onTerminateRequest(request *dap.TerminateRequest) {
 	s.sendNotYetImplementedErrorResponse(request.Request)
 }
 
 // onRestartRequest sends a not-yet-implemented error response
 // Capability 'supportsRestartRequest' is not set in 'initialize' response.
-func (s *Server) onRestartRequest(request *dap.RestartRequest) {
+func (s *Session) onRestartRequest(request *dap.RestartRequest) {
 	s.sendNotYetImplementedErrorResponse(request.Request)
 }
 
 // onStepBackRequest handles 'stepBack' request.
 // This is an optional request enabled by capability ‘supportsStepBackRequest’.
-func (s *Server) onStepBackRequest(request *dap.StepBackRequest, allowNextStateChange chan struct{}) {
+func (s *Session) onStepBackRequest(request *dap.StepBackRequest, allowNextStateChange chan struct{}) {
 	s.sendStepResponse(request.Arguments.ThreadId, &dap.StepBackResponse{Response: *newResponse(request.Request)})
 	s.stepUntilStopAndNotify(api.ReverseNext, request.Arguments.ThreadId, allowNextStateChange)
 }
@@ -2530,7 +2580,7 @@ func (s *Server) onStepBackRequest(request *dap.StepBackRequest, allowNextStateC
 // onReverseContinueRequest performs a rewind command call up to the previous
 // breakpoint or the start of the process
 // This is an optional request enabled by capability ‘supportsStepBackRequest’.
-func (s *Server) onReverseContinueRequest(request *dap.ReverseContinueRequest, allowNextStateChange chan struct{}) {
+func (s *Session) onReverseContinueRequest(request *dap.ReverseContinueRequest, allowNextStateChange chan struct{}) {
 	s.send(&dap.ReverseContinueResponse{
 		Response: *newResponse(request.Request),
 	})
@@ -2538,7 +2588,7 @@ func (s *Server) onReverseContinueRequest(request *dap.ReverseContinueRequest, a
 }
 
 // computeEvaluateName finds the named child, and computes its evaluate name.
-func (s *Server) computeEvaluateName(v *fullyQualifiedVariable, cname string) (string, error) {
+func (s *Session) computeEvaluateName(v *fullyQualifiedVariable, cname string) (string, error) {
 	children, err := s.childrenToDAPVariables(v)
 	if err != nil {
 		return "", err
@@ -2555,7 +2605,7 @@ func (s *Server) computeEvaluateName(v *fullyQualifiedVariable, cname string) (s
 }
 
 // onSetVariableRequest handles 'setVariable' requests.
-func (s *Server) onSetVariableRequest(request *dap.SetVariableRequest) {
+func (s *Session) onSetVariableRequest(request *dap.SetVariableRequest) {
 	arg := request.Arguments
 
 	v, ok := s.variableHandles.get(arg.VariablesReference)
@@ -2651,37 +2701,37 @@ func (s *Server) onSetVariableRequest(request *dap.SetVariableRequest) {
 
 // onSetExpression sends a not-yet-implemented error response.
 // Capability 'supportsSetExpression' is not set 'initialize' response.
-func (s *Server) onSetExpressionRequest(request *dap.SetExpressionRequest) {
+func (s *Session) onSetExpressionRequest(request *dap.SetExpressionRequest) {
 	s.sendNotYetImplementedErrorResponse(request.Request)
 }
 
 // onLoadedSourcesRequest sends a not-yet-implemented error response.
 // Capability 'supportsLoadedSourcesRequest' is not set 'initialize' response.
-func (s *Server) onLoadedSourcesRequest(request *dap.LoadedSourcesRequest) {
+func (s *Session) onLoadedSourcesRequest(request *dap.LoadedSourcesRequest) {
 	s.sendNotYetImplementedErrorResponse(request.Request)
 }
 
 // onReadMemoryRequest sends a not-yet-implemented error response.
 // Capability 'supportsReadMemoryRequest' is not set 'initialize' response.
-func (s *Server) onReadMemoryRequest(request *dap.ReadMemoryRequest) {
+func (s *Session) onReadMemoryRequest(request *dap.ReadMemoryRequest) {
 	s.sendNotYetImplementedErrorResponse(request.Request)
 }
 
 // onDisassembleRequest sends a not-yet-implemented error response.
 // Capability 'supportsDisassembleRequest' is not set 'initialize' response.
-func (s *Server) onDisassembleRequest(request *dap.DisassembleRequest) {
+func (s *Session) onDisassembleRequest(request *dap.DisassembleRequest) {
 	s.sendNotYetImplementedErrorResponse(request.Request)
 }
 
 // onCancelRequest sends a not-yet-implemented error response.
 // Capability 'supportsCancelRequest' is not set 'initialize' response.
-func (s *Server) onCancelRequest(request *dap.CancelRequest) {
+func (s *Session) onCancelRequest(request *dap.CancelRequest) {
 	s.sendNotYetImplementedErrorResponse(request.Request)
 }
 
 // onExceptionInfoRequest handles 'exceptionInfo' requests.
 // Capability 'supportsExceptionInfoRequest' is set in 'initialize' response.
-func (s *Server) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
+func (s *Session) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
 	goroutineID := request.Arguments.ThreadId
 	var body dap.ExceptionInfoResponseBody
 	// Get the goroutine and the current state.
@@ -2766,7 +2816,7 @@ func (s *Server) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
 	s.send(response)
 }
 
-func (s *Server) stacktrace(goroutineID int, g *proc.G) (string, error) {
+func (s *Session) stacktrace(goroutineID int, g *proc.G) (string, error) {
 	frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0)
 	if err != nil {
 		return "", err
@@ -2791,15 +2841,15 @@ func (s *Server) stacktrace(goroutineID int, g *proc.G) (string, error) {
 	return buf.String(), nil
 }
 
-func (s *Server) throwReason(goroutineID int) (string, error) {
+func (s *Session) throwReason(goroutineID int) (string, error) {
 	return s.getExprString("s", goroutineID, 0)
 }
 
-func (s *Server) panicReason(goroutineID int) (string, error) {
+func (s *Session) panicReason(goroutineID int) (string, error) {
 	return s.getExprString("(*msgs).arg.(data)", goroutineID, 0)
 }
 
-func (s *Server) getExprString(expr string, goroutineID, frame int) (string, error) {
+func (s *Session) getExprString(expr string, goroutineID, frame int) (string, error) {
 	exprVar, err := s.debugger.EvalVariableInScope(goroutineID, frame, 0, expr, DefaultLoadConfig)
 	if err != nil {
 		return "", err
@@ -2812,7 +2862,7 @@ func (s *Server) getExprString(expr string, goroutineID, frame int) (string, err
 
 // sendErrorResponseWithOpts offers configuration options.
 //   showUser - if true, the error will be shown to the user (e.g. via a visible pop-up)
-func (s *Server) sendErrorResponseWithOpts(request dap.Request, id int, summary, details string, showUser bool) {
+func (s *Session) sendErrorResponseWithOpts(request dap.Request, id int, summary, details string, showUser bool) {
 	er := &dap.ErrorResponse{}
 	er.Type = "response"
 	er.Command = request.Command
@@ -2822,24 +2872,24 @@ func (s *Server) sendErrorResponseWithOpts(request dap.Request, id int, summary,
 	er.Body.Error.Id = id
 	er.Body.Error.Format = fmt.Sprintf("%s: %s", summary, details)
 	er.Body.Error.ShowUser = showUser
-	s.log.Debug(er.Body.Error.Format)
+	s.config.log.Debug(er.Body.Error.Format)
 	s.send(er)
 }
 
 // sendErrorResponse sends an error response with showUser disabled (default).
-func (s *Server) sendErrorResponse(request dap.Request, id int, summary, details string) {
+func (s *Session) sendErrorResponse(request dap.Request, id int, summary, details string) {
 	s.sendErrorResponseWithOpts(request, id, summary, details, false /*showUser*/)
 }
 
 // sendShowUserErrorResponse sends an error response with showUser enabled.
-func (s *Server) sendShowUserErrorResponse(request dap.Request, id int, summary, details string) {
+func (s *Session) sendShowUserErrorResponse(request dap.Request, id int, summary, details string) {
 	s.sendErrorResponseWithOpts(request, id, summary, details, true /*showUser*/)
 }
 
 // sendInternalErrorResponse sends an "internal error" response back to the client.
 // We only take a seq here because we don't want to make assumptions about the
 // kind of message received by the server that this error is a reply to.
-func (s *Server) sendInternalErrorResponse(seq int, details string) {
+func (s *Session) sendInternalErrorResponse(seq int, details string) {
 	er := &dap.ErrorResponse{}
 	er.Type = "response"
 	er.RequestSeq = seq
@@ -2847,16 +2897,16 @@ func (s *Server) sendInternalErrorResponse(seq int, details string) {
 	er.Message = "Internal Error"
 	er.Body.Error.Id = InternalError
 	er.Body.Error.Format = fmt.Sprintf("%s: %s", er.Message, details)
-	s.log.Debug(er.Body.Error.Format)
+	s.config.log.Debug(er.Body.Error.Format)
 	s.send(er)
 }
 
-func (s *Server) sendUnsupportedErrorResponse(request dap.Request) {
+func (s *Session) sendUnsupportedErrorResponse(request dap.Request) {
 	s.sendErrorResponse(request, UnsupportedCommand, "Unsupported command",
 		fmt.Sprintf("cannot process %q request", request.Command))
 }
 
-func (s *Server) sendNotYetImplementedErrorResponse(request dap.Request) {
+func (s *Session) sendNotYetImplementedErrorResponse(request dap.Request) {
 	s.sendErrorResponse(request, NotYetImplemented, "Not yet implemented",
 		fmt.Sprintf("cannot process %q request", request.Command))
 }
@@ -2888,7 +2938,7 @@ Unable to propagate EXC_BAD_ACCESS signal to target process and panic (see https
 const BetterNextWhileNextingError = `Unable to step while the previous step is interrupted by a breakpoint.
 Use 'Continue' to resume the original step command.`
 
-func (s *Server) resetHandlesForStoppedEvent() {
+func (s *Session) resetHandlesForStoppedEvent() {
 	s.stackFrameHandles.reset()
 	s.variableHandles.reset()
 	s.exceptionErr = nil
@@ -2898,25 +2948,26 @@ func processExited(state *api.DebuggerState, err error) bool {
 	_, isexited := err.(proc.ErrProcessExited)
 	return isexited || err == nil && state.Exited
 }
-func (s *Server) setRunningCmd(running bool) {
+
+func (s *Session) setRunningCmd(running bool) {
 	s.runningMu.Lock()
 	defer s.runningMu.Unlock()
 	s.runningCmd = running
 }
 
-func (s *Server) isRunningCmd() bool {
+func (s *Session) isRunningCmd() bool {
 	s.runningMu.Lock()
 	defer s.runningMu.Unlock()
 	return s.runningCmd
 }
 
-func (s *Server) setHaltRequested(requested bool) {
+func (s *Session) setHaltRequested(requested bool) {
 	s.haltMu.Lock()
 	defer s.haltMu.Unlock()
 	s.haltRequested = requested
 }
 
-func (s *Server) checkHaltRequested() bool {
+func (s *Session) checkHaltRequested() bool {
 	s.haltMu.Lock()
 	defer s.haltMu.Unlock()
 	return s.haltRequested
@@ -2924,7 +2975,7 @@ func (s *Server) checkHaltRequested() bool {
 
 // resumeOnce is a helper function to resume the execution
 // of the target when the program is halted.
-func (s *Server) resumeOnce(command string, allowNextStateChange chan struct{}) (bool, *api.DebuggerState, error) {
+func (s *Session) resumeOnce(command string, allowNextStateChange chan struct{}) (bool, *api.DebuggerState, error) {
 	// No other goroutines should be able to try to resume
 	// or halt execution while this goroutine is resuming
 	// execution, so we do not miss those events.
@@ -2954,7 +3005,7 @@ func (s *Server) resumeOnce(command string, allowNextStateChange chan struct{})
 // a channel that will be closed to signal that an
 // asynchornous command has completed setup or was interrupted
 // due to an error, so the server is ready to receive new requests.
-func (s *Server) runUntilStopAndNotify(command string, allowNextStateChange chan struct{}) {
+func (s *Session) runUntilStopAndNotify(command string, allowNextStateChange chan struct{}) {
 	state, err := s.runUntilStop(command, allowNextStateChange)
 
 	if processExited(state, err) {
@@ -2967,7 +3018,7 @@ func (s *Server) runUntilStopAndNotify(command string, allowNextStateChange chan
 	if state != nil && state.CurrentThread != nil {
 		file, line = state.CurrentThread.File, state.CurrentThread.Line
 	}
-	s.log.Debugf("%q command stopped - reason %q, location %s:%d", command, stopReason, file, line)
+	s.config.log.Debugf("%q command stopped - reason %q, location %s:%d", command, stopReason, file, line)
 
 	s.resetHandlesForStoppedEvent()
 	stopped := &dap.StoppedEvent{Event: *newEvent("stopped")}
@@ -2976,7 +3027,7 @@ func (s *Server) runUntilStopAndNotify(command string, allowNextStateChange chan
 	if err == nil {
 		if stopReason == proc.StopManual {
 			if err := s.debugger.CancelNext(); err != nil {
-				s.log.Error(err)
+				s.config.log.Error(err)
 			} else {
 				state.NextInProgress = false
 			}
@@ -3018,14 +3069,14 @@ func (s *Server) runUntilStopAndNotify(command string, allowNextStateChange chan
 		// so that the stop reason is determined by that function which
 		// has all the context.
 		if stopped.Body.Reason != "exception" && s.checkHaltRequested() {
-			s.log.Debugf("manual halt requested, stop reason %q converted to \"pause\"", stopped.Body.Reason)
+			s.config.log.Debugf("manual halt requested, stop reason %q converted to \"pause\"", stopped.Body.Reason)
 			stopped.Body.Reason = "pause"
 			stopped.Body.HitBreakpointIds = []int{}
 		}
 
 	} else {
 		s.exceptionErr = err
-		s.log.Error("runtime error: ", err)
+		s.config.log.Error("runtime error: ", err)
 		stopped.Body.Reason = "exception"
 		stopped.Body.Description = "runtime error"
 		stopped.Body.Text = err.Error()
@@ -3056,7 +3107,7 @@ func (s *Server) runUntilStopAndNotify(command string, allowNextStateChange chan
 	}
 }
 
-func (s *Server) runUntilStop(command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) {
+func (s *Session) runUntilStop(command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) {
 	// Clear any manual stop requests that came in before we started running.
 	s.setHaltRequested(false)
 
@@ -3073,11 +3124,11 @@ func (s *Server) runUntilStop(command string, allowNextStateChange chan struct{}
 }
 
 // Make this a var so it can be stubbed in testing.
-var resumeOnceAndCheckStop = func(s *Server, command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) {
+var resumeOnceAndCheckStop = func(s *Session, command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) {
 	return s.resumeOnceAndCheckStop(command, allowNextStateChange)
 }
 
-func (s *Server) resumeOnceAndCheckStop(command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) {
+func (s *Session) resumeOnceAndCheckStop(command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) {
 	resumed, state, err := s.resumeOnce(command, allowNextStateChange)
 	// We should not try to process the log points if the program was not
 	// resumed or there was an error.
@@ -3098,7 +3149,7 @@ func (s *Server) resumeOnceAndCheckStop(command string, allowNextStateChange cha
 	return state, err
 }
 
-func (s *Server) handleLogPoints(state *api.DebuggerState) bool {
+func (s *Session) handleLogPoints(state *api.DebuggerState) bool {
 	foundRealBreakpoint := false
 	for _, th := range state.Threads {
 		if bp := th.Breakpoint; bp != nil {
@@ -3111,7 +3162,7 @@ func (s *Server) handleLogPoints(state *api.DebuggerState) bool {
 	return foundRealBreakpoint
 }
 
-func (s *Server) logBreakpointMessage(bp *api.Breakpoint) bool {
+func (s *Session) logBreakpointMessage(bp *api.Breakpoint) bool {
 	if !bp.Tracepoint {
 		return false
 	}
@@ -3129,24 +3180,24 @@ func (s *Server) logBreakpointMessage(bp *api.Breakpoint) bool {
 	return true
 }
 
-func (s *Server) toClientPath(path string) string {
+func (s *Session) toClientPath(path string) string {
 	if len(s.args.substitutePathServerToClient) == 0 {
 		return path
 	}
 	clientPath := locspec.SubstitutePath(path, s.args.substitutePathServerToClient)
 	if clientPath != path {
-		s.log.Debugf("server path=%s converted to client path=%s\n", path, clientPath)
+		s.config.log.Debugf("server path=%s converted to client path=%s\n", path, clientPath)
 	}
 	return clientPath
 }
 
-func (s *Server) toServerPath(path string) string {
+func (s *Session) toServerPath(path string) string {
 	if len(s.args.substitutePathClientToServer) == 0 {
 		return path
 	}
 	serverPath := locspec.SubstitutePath(path, s.args.substitutePathClientToServer)
 	if serverPath != path {
-		s.log.Debugf("client path=%s converted to server path=%s\n", path, serverPath)
+		s.config.log.Debugf("client path=%s converted to server path=%s\n", path, serverPath)
 	}
 	return serverPath
 }


Test patch:
diff --git a/Documentation/backend_test_health.md b/Documentation/backend_test_health.md
index 17958f8c..ed231acf 100644
--- a/Documentation/backend_test_health.md
+++ b/Documentation/backend_test_health.md
@@ -4,12 +4,9 @@ Tests skipped by each supported backend:
 	* 1 broken
 	* 3 broken - cgo stacktraces
 	* 3 not implemented
-* arm64 skipped = 5
+* arm64 skipped = 2
 	* 1 broken
 	* 1 broken - global variable symbolication
-	* 3 not implemented
-* darwin skipped = 3
-	* 3 not implemented
 * darwin/arm64 skipped = 1
 	* 1 broken - cgo stacktraces
 * darwin/lldb skipped = 1
@@ -19,12 +16,11 @@ Tests skipped by each supported backend:
 	* 4 not implemented
 * linux/386/pie skipped = 1
 	* 1 broken
-* linux/arm64 skipped = 1
+* linux/arm64 skipped = 4
 	* 1 broken - cgo stacktraces
+	* 3 not implemented
 * pie skipped = 2
 	* 2 upstream issue - https://github.com/golang/go/issues/29322
-* rr skipped = 3
-	* 3 not implemented
 * windows skipped = 2
 	* 1 broken
 	* 1 upstream issue
diff --git a/_fixtures/testvariables_generic.go b/_fixtures/testvariables_generic.go
new file mode 100644
index 00000000..25860026
--- /dev/null
+++ b/_fixtures/testvariables_generic.go
@@ -0,0 +1,22 @@
+package main
+
+import (
+	"fmt"
+	"runtime"
+)
+
+type astruct struct {
+	x, y int
+}
+
+func testfn[T any, K comparable](arg1 T, arg2 K) {
+	m := make(map[K]T)
+	m[arg2] = arg1
+	runtime.Breakpoint()
+	fmt.Println(arg1, arg2, m)
+}
+
+func main() {
+	testfn[int, float32](3, 2.1)
+	testfn(&astruct{0, 1}, astruct{2, 3})
+}
diff --git a/_scripts/test_linux.sh b/_scripts/test_linux.sh
index a35e8acf..e7156bf2 100755
--- a/_scripts/test_linux.sh
+++ b/_scripts/test_linux.sh
@@ -21,7 +21,10 @@ function getgo {
 }
 
 if [ "$version" = "gotip" ]; then
-	exit 0
+	# TODO: remove this
+	if [ "$arch" != "amd64" ]; then
+		exit 0
+	fi
 	echo Building Go from tip
 	getgo $(curl https://golang.org/VERSION?m=text)
 	export GOROOT_BOOTSTRAP=$GOROOT
diff --git a/_scripts/test_mac.sh b/_scripts/test_mac.sh
index 78ef0755..f4395a84 100644
--- a/_scripts/test_mac.sh
+++ b/_scripts/test_mac.sh
@@ -8,7 +8,10 @@ ARCH=$2
 TMPDIR=$3
 
 if [ "$GOVERSION" = "gotip" ]; then
-    exit 0
+    # TODO: remove this
+    if [ "$arch" != "amd64" ]; then
+        exit 0
+    fi
     bootstrapver=$(curl https://golang.org/VERSION?m=text)
     cd $TMPDIR
     curl -sSL "https://storage.googleapis.com/golang/$bootstrapver.darwin-$ARCH.tar.gz" | tar -xz
diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go
index f7ff3a25..bb120c47 100644
--- a/pkg/proc/proc_test.go
+++ b/pkg/proc/proc_test.go
@@ -517,7 +517,7 @@ func testseq2Args(wd string, args []string, buildFlags protest.BuildFlags, t *te
 			}
 			switch pos := tc.pos.(type) {
 			case int:
-				if ln != pos {
+				if pos >= 0 && ln != pos {
 					t.Fatalf("Program did not continue to correct next location expected %d was %s:%d (%#x) (testcase %d)", pos, filepath.Base(f), ln, pc, i)
 				}
 			case string:
@@ -3544,7 +3544,7 @@ func testDeclLineCount(t *testing.T, p *proc.Target, lineno int, tgtvars []strin
 	assertLineNumber(p, t, lineno, "Program did not continue to correct next location")
 	scope, err := proc.GoroutineScope(p, p.CurrentThread())
 	assertNoError(err, t, fmt.Sprintf("GoroutineScope (:%d)", lineno))
-	vars, err := scope.Locals()
+	vars, err := scope.Locals(0)
 	assertNoError(err, t, fmt.Sprintf("Locals (:%d)", lineno))
 	if len(vars) != len(tgtvars) {
 		t.Fatalf("wrong number of variables %d (:%d)", len(vars), lineno)
@@ -3575,27 +3575,27 @@ func TestDeclLine(t *testing.T) {
 		setFileBreakpoint(p, t, fixture.Source, 11)
 		setFileBreakpoint(p, t, fixture.Source, 14)
 
-		assertNoError(p.Continue(), t, "Continue")
+		assertNoError(p.Continue(), t, "Continue 1")
 		if goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) {
 			testDeclLineCount(t, p, 8, []string{})
 		} else {
 			testDeclLineCount(t, p, 8, []string{"a"})
 		}
 
-		assertNoError(p.Continue(), t, "Continue")
+		assertNoError(p.Continue(), t, "Continue 2")
 		testDeclLineCount(t, p, 9, []string{"a"})
 
-		assertNoError(p.Continue(), t, "Continue")
+		assertNoError(p.Continue(), t, "Continue 3")
 		if goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) {
 			testDeclLineCount(t, p, 10, []string{"a"})
 		} else {
 			testDeclLineCount(t, p, 10, []string{"a", "b"})
 		}
 
-		assertNoError(p.Continue(), t, "Continue")
+		assertNoError(p.Continue(), t, "Continue 4")
 		testDeclLineCount(t, p, 11, []string{"a", "b"})
 
-		assertNoError(p.Continue(), t, "Continue")
+		assertNoError(p.Continue(), t, "Continue 5")
 		testDeclLineCount(t, p, 14, []string{"a", "b"})
 	})
 }
@@ -3811,7 +3811,7 @@ func TestInlinedStacktraceAndVariables(t *testing.T) {
 	}
 
 	withTestProcessArgs("testinline", t, ".", []string{}, protest.EnableInlining, func(p *proc.Target, fixture protest.Fixture) {
-		pcs, err := p.BinInfo().LineToPC(fixture.Source, 7)
+		pcs, err := proc.FindFileLocation(p, fixture.Source, 7)
 		assertNoError(err, t, "LineToPC")
 		if len(pcs) < 2 {
 			t.Fatalf("expected at least two locations for %s:%d (got %d: %#x)", fixture.Source, 7, len(pcs), pcs)
@@ -3960,7 +3960,7 @@ func TestInlineBreakpoint(t *testing.T) {
 		t.Skip("inlining not supported")
 	}
 	withTestProcessArgs("testinline", t, ".", []string{}, protest.EnableInlining|protest.EnableOptimization, func(p *proc.Target, fixture protest.Fixture) {
-		pcs, err := p.BinInfo().LineToPC(fixture.Source, 17)
+		pcs, err := proc.FindFileLocation(p, fixture.Source, 17)
 		t.Logf("%#v\n", pcs)
 		if len(pcs) != 1 {
 			t.Fatalf("unable to get PC for inlined function call: %v", pcs)
@@ -4816,33 +4816,64 @@ func TestBackwardNextDeferPanic(t *testing.T) {
 	if testBackend != "rr" {
 		t.Skip("Reverse stepping test needs rr")
 	}
-	testseq2(t, "defercall", "", []seqTest{
-		{contContinue, 12},
-		{contReverseNext, 11},
-		{contReverseNext, 10},
-		{contReverseNext, 9},
-		{contReverseNext, 27},
-
-		{contContinueToBreakpoint, 12}, // skip first call to sampleFunction
-		{contContinueToBreakpoint, 6},  // go to call to sampleFunction through deferreturn
-		{contReverseNext, 13},
-		{contReverseNext, 12},
-		{contReverseNext, 11},
-		{contReverseNext, 10},
-		{contReverseNext, 9},
-		{contReverseNext, 27},
-
-		{contContinueToBreakpoint, 18}, // go to panic call
-		{contNext, 6},                  // panic so the deferred call happens
-		{contReverseNext, 18},
-		{contReverseNext, 17},
-		{contReverseNext, 16},
-		{contReverseNext, 15},
-		{contReverseNext, 23},
-		{contReverseNext, 22},
-		{contReverseNext, 21},
-		{contReverseNext, 28},
-	})
+	if goversion.VersionAfterOrEqual(runtime.Version(), 1, 18) {
+		testseq2(t, "defercall", "", []seqTest{
+			{contContinue, 12},
+			{contReverseNext, 11},
+			{contReverseNext, 10},
+			{contReverseNext, 9},
+			{contReverseNext, 27},
+
+			{contContinueToBreakpoint, 12}, // skip first call to sampleFunction
+			{contContinueToBreakpoint, 6},  // go to call to sampleFunction through deferreturn
+			{contReverseNext, -1},          // runtime.deferreturn, maybe we should try to skip this
+			{contReverseStepout, 13},
+			{contReverseNext, 12},
+			{contReverseNext, 11},
+			{contReverseNext, 10},
+			{contReverseNext, 9},
+			{contReverseNext, 27},
+
+			{contContinueToBreakpoint, 18}, // go to panic call
+			{contNext, 6},                  // panic so the deferred call happens
+			{contReverseNext, 18},
+			{contReverseNext, 17},
+			{contReverseNext, 16},
+			{contReverseNext, 15},
+			{contReverseNext, 23},
+			{contReverseNext, 22},
+			{contReverseNext, 21},
+			{contReverseNext, 28},
+		})
+	} else {
+		testseq2(t, "defercall", "", []seqTest{
+			{contContinue, 12},
+			{contReverseNext, 11},
+			{contReverseNext, 10},
+			{contReverseNext, 9},
+			{contReverseNext, 27},
+
+			{contContinueToBreakpoint, 12}, // skip first call to sampleFunction
+			{contContinueToBreakpoint, 6},  // go to call to sampleFunction through deferreturn
+			{contReverseNext, 13},
+			{contReverseNext, 12},
+			{contReverseNext, 11},
+			{contReverseNext, 10},
+			{contReverseNext, 9},
+			{contReverseNext, 27},
+
+			{contContinueToBreakpoint, 18}, // go to panic call
+			{contNext, 6},                  // panic so the deferred call happens
+			{contReverseNext, 18},
+			{contReverseNext, 17},
+			{contReverseNext, 16},
+			{contReverseNext, 15},
+			{contReverseNext, 23},
+			{contReverseNext, 22},
+			{contReverseNext, 21},
+			{contReverseNext, 28},
+		})
+	}
 }
 
 func TestIssue1925(t *testing.T) {
@@ -5333,13 +5364,22 @@ func TestVariablesWithExternalLinking(t *testing.T) {
 
 func TestWatchpointsBasic(t *testing.T) {
 	skipOn(t, "not implemented", "freebsd")
-	skipOn(t, "not implemented", "darwin")
 	skipOn(t, "not implemented", "386")
-	skipOn(t, "not implemented", "arm64")
-	skipOn(t, "not implemented", "rr")
+	skipOn(t, "not implemented", "linux", "arm64")
+	protest.AllowRecording(t)
+
+	position1 := 17
+	position5 := 33
+
+	if runtime.GOARCH == "arm64" {
+		position1 = 16
+		position5 = 32
+	}
 
 	withTestProcess("databpeasy", t, func(p *proc.Target, fixture protest.Fixture) {
 		setFunctionBreakpoint(p, t, "main.main")
+		setFileBreakpoint(p, t, fixture.Source, 19) // Position 2 breakpoint
+		setFileBreakpoint(p, t, fixture.Source, 25) // Position 4 breakpoint
 		assertNoError(p.Continue(), t, "Continue 0")
 		assertLineNumber(p, t, 11, "Continue 0") // Position 0
 
@@ -5350,7 +5390,11 @@ func TestWatchpointsBasic(t *testing.T) {
 		assertNoError(err, t, "SetDataBreakpoint(write-only)")
 
 		assertNoError(p.Continue(), t, "Continue 1")
-		assertLineNumber(p, t, 17, "Continue 1") // Position 1
+		assertLineNumber(p, t, position1, "Continue 1") // Position 1
+
+		if curbp := p.CurrentThread().Breakpoint().Breakpoint; curbp == nil || (curbp.LogicalID() != bp.LogicalID()) {
+			t.Fatal("breakpoint not set")
+		}
 
 		p.ClearBreakpoint(bp.Addr)
 
@@ -5368,22 +5412,21 @@ func TestWatchpointsBasic(t *testing.T) {
 		assertNoError(p.Continue(), t, "Continue 4")
 		assertLineNumber(p, t, 25, "Continue 4") // Position 4
 
+		t.Logf("setting final breakpoint")
 		_, err = p.SetWatchpoint(scope, "globalvar1", proc.WatchWrite, nil)
 		assertNoError(err, t, "SetDataBreakpoint(write-only, again)")
 
 		assertNoError(p.Continue(), t, "Continue 5")
-		assertLineNumber(p, t, 33, "Continue 5") // Position 5
+		assertLineNumber(p, t, position5, "Continue 5") // Position 5
 	})
 }
 
 func TestWatchpointCounts(t *testing.T) {
 	skipOn(t, "not implemented", "freebsd")
-	skipOn(t, "not implemented", "darwin")
 	skipOn(t, "not implemented", "386")
-	skipOn(t, "not implemented", "arm64")
-	skipOn(t, "not implemented", "rr")
-
+	skipOn(t, "not implemented", "linux", "arm64")
 	protest.AllowRecording(t)
+
 	withTestProcess("databpcountstest", t, func(p *proc.Target, fixture protest.Fixture) {
 		setFunctionBreakpoint(p, t, "main.main")
 		assertNoError(p.Continue(), t, "Continue 0")
@@ -5495,12 +5538,18 @@ func TestDwrapStartLocation(t *testing.T) {
 
 func TestWatchpointStack(t *testing.T) {
 	skipOn(t, "not implemented", "freebsd")
-	skipOn(t, "not implemented", "darwin")
 	skipOn(t, "not implemented", "386")
-	skipOn(t, "not implemented", "arm64")
-	skipOn(t, "not implemented", "rr")
+	skipOn(t, "not implemented", "linux", "arm64")
+	protest.AllowRecording(t)
+
+	position1 := 17
+
+	if runtime.GOARCH == "arm64" {
+		position1 = 16
+	}
 
 	withTestProcess("databpstack", t, func(p *proc.Target, fixture protest.Fixture) {
+		setFileBreakpoint(p, t, fixture.Source, 11) // Position 0 breakpoint
 		clearlen := len(p.Breakpoints().M)
 
 		assertNoError(p.Continue(), t, "Continue 0")
@@ -5512,8 +5561,13 @@ func TestWatchpointStack(t *testing.T) {
 		_, err = p.SetWatchpoint(scope, "w", proc.WatchWrite, nil)
 		assertNoError(err, t, "SetDataBreakpoint(write-only)")
 
-		if len(p.Breakpoints().M) != clearlen+3 {
-			// want 1 watchpoint, 1 stack resize breakpoint, 1 out of scope sentinel
+		watchbpnum := 3
+		if recorded, _ := p.Recorded(); recorded {
+			watchbpnum = 4
+		}
+
+		if len(p.Breakpoints().M) != clearlen+watchbpnum {
+			// want 1 watchpoint, 1 stack resize breakpoint, 1 out of scope sentinel (2 if recorded)
 			t.Errorf("wrong number of breakpoints after setting watchpoint: %d", len(p.Breakpoints().M)-clearlen)
 		}
 
@@ -5527,16 +5581,21 @@ func TestWatchpointStack(t *testing.T) {
 			}
 		}
 
+		// Note: for recorded processes retaddr will not always be the return
+		// address, ~50% of the times it will be the address of the CALL
+		// instruction preceding the return address, this does not matter for this
+		// test.
+
 		_, err = p.SetBreakpoint(retaddr, proc.UserBreakpoint, nil)
 		assertNoError(err, t, "SetBreakpoint")
 
-		if len(p.Breakpoints().M) != clearlen+3 {
-			// want 1 watchpoint, 1 stack resize breakpoint, 1 out of scope sentinel (which is also a user breakpoint)
+		if len(p.Breakpoints().M) != clearlen+watchbpnum {
+			// want 1 watchpoint, 1 stack resize breakpoint, 1 out of scope sentinel (which is also a user breakpoint) (and another out of scope sentinel if recorded)
 			t.Errorf("wrong number of breakpoints after setting watchpoint: %d", len(p.Breakpoints().M)-clearlen)
 		}
 
 		assertNoError(p.Continue(), t, "Continue 1")
-		assertLineNumber(p, t, 17, "Continue 1") // Position 1
+		assertLineNumber(p, t, position1, "Continue 1") // Position 1
 
 		assertNoError(p.Continue(), t, "Continue 2")
 		t.Logf("%#v", p.CurrentThread().Breakpoint().Breakpoint)
@@ -5560,3 +5619,52 @@ func TestWatchpointStack(t *testing.T) {
 		}
 	})
 }
+
+func TestWatchpointStackBackwardsOutOfScope(t *testing.T) {
+	skipUnlessOn(t, "only for recorded targets", "rr")
+	protest.AllowRecording(t)
+
+	withTestProcess("databpstack", t, func(p *proc.Target, fixture protest.Fixture) {
+		setFileBreakpoint(p, t, fixture.Source, 11) // Position 0 breakpoint
+		clearlen := len(p.Breakpoints().M)
+
+		assertNoError(p.Continue(), t, "Continue 0")
+		assertLineNumber(p, t, 11, "Continue 0") // Position 0
+
+		scope, err := proc.GoroutineScope(p, p.CurrentThread())
+		assertNoError(err, t, "GoroutineScope")
+
+		_, err = p.SetWatchpoint(scope, "w", proc.WatchWrite, nil)
+		assertNoError(err, t, "SetDataBreakpoint(write-only)")
+
+		assertNoError(p.Continue(), t, "Continue 1")
+		assertLineNumber(p, t, 17, "Continue 1") // Position 1
+
+		p.ChangeDirection(proc.Backward)
+
+		assertNoError(p.Continue(), t, "Continue 2")
+		t.Logf("%#v", p.CurrentThread().Breakpoint().Breakpoint)
+		assertLineNumber(p, t, 16, "Continue 2") // Position 1 again (because of inverted movement)
+
+		assertNoError(p.Continue(), t, "Continue 3")
+		t.Logf("%#v", p.CurrentThread().Breakpoint().Breakpoint)
+		assertLineNumber(p, t, 11, "Continue 3") // Position 0 (breakpoint 1 hit)
+
+		assertNoError(p.Continue(), t, "Continue 4")
+		t.Logf("%#v", p.CurrentThread().Breakpoint().Breakpoint)
+		assertLineNumber(p, t, 23, "Continue 4") // Position 2 (watchpoint gone out of scope)
+
+		if len(p.Breakpoints().M) != clearlen {
+			t.Errorf("wrong number of breakpoints after watchpoint goes out of scope: %d", len(p.Breakpoints().M)-clearlen)
+		}
+
+		if len(p.Breakpoints().WatchOutOfScope) != 1 {
+			t.Errorf("wrong number of out-of-scope watchpoints after watchpoint goes out of scope: %d", len(p.Breakpoints().WatchOutOfScope))
+		}
+
+		if len(p.Breakpoints().M) != clearlen {
+			// want 1 user breakpoint set at retaddr
+			t.Errorf("wrong number of breakpoints after removing user breakpoint: %d", len(p.Breakpoints().M)-clearlen)
+		}
+	})
+}
diff --git a/pkg/proc/test/support.go b/pkg/proc/test/support.go
index 4677fe7d..924a0b03 100644
--- a/pkg/proc/test/support.go
+++ b/pkg/proc/test/support.go
@@ -385,7 +385,9 @@ func RegabiSupported() bool {
 	switch {
 	case !goversion.VersionAfterOrEqual(runtime.Version(), 1, 17): // < 1.17
 		return false
-	default: // >= 1.17
+	case goversion.VersionAfterOrEqual(runtime.Version(), 1, 17):
 		return runtime.GOARCH == "amd64" && (runtime.GOOS == "android" || runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows")
+	default: // >= 1.18
+		return runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64"
 	}
 }
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index d3b23ce2..393525f8 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -747,7 +747,11 @@ func checkVar(t *testing.T, got *dap.VariablesResponse, i int, name, evalName, v
 	goti := got.Body.Variables[i]
 	matchedName := false
 	if useExactMatch {
-		matchedName = (goti.Name == name)
+		if strings.HasPrefix(name, "~r") {
+			matchedName = strings.HasPrefix(goti.Name, "~r")
+		} else {
+			matchedName = (goti.Name == name)
+		}
 	} else {
 		matchedName, _ = regexp.MatchString(name, goti.Name)
 	}
@@ -2804,7 +2808,7 @@ func TestHaltPreventsAutoResume(t *testing.T) {
 						// Send a halt request when trying to resume the program after being
 						// interrupted. This should allow the log message to be processed,
 						// but keep the process from continuing beyond the line.
-						resumeOnceAndCheckStop = func(s *Server, command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) {
+						resumeOnceAndCheckStop = func(s *Session, command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) {
 							// This should trigger after the log message is sent, but before
 							// execution is resumed.
 							if command == api.DirectionCongruentContinue {
@@ -5575,9 +5579,15 @@ func launchDebuggerWithTargetHalted(t *testing.T, fixture string) *debugger.Debu
 func runTestWithDebugger(t *testing.T, dbg *debugger.Debugger, test func(c *daptest.Client)) {
 	serverStopped := make(chan struct{})
 	server, _ := startDapServer(t, serverStopped)
-	// TODO(polina): update once the server interface is refactored to take debugger as arg
-	server.debugger = dbg
 	client := daptest.NewClient(server.listener.Addr().String())
+	time.Sleep(100 * time.Millisecond) // Give time for connection to be set as dap.Session
+	// TODO(polina): update once the server interface is refactored to take debugger as arg
+	server.sessionMu.Lock()
+	if server.session == nil {
+		t.Fatal("dap session is not ready")
+	}
+	server.session.debugger = dbg
+	server.sessionMu.Unlock()
 	defer client.Close()
 	client.InitializeRequest()
 	client.ExpectInitializeResponseAndCapabilities(t)
diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go
index f332b9b4..b2ed5bff 100644
--- a/service/test/integration2_test.go
+++ b/service/test/integration2_test.go
@@ -2475,3 +2475,38 @@ func TestLongStringArg(t *testing.T) {
 		}
 	})
 }
+
+func TestGenericsBreakpoint(t *testing.T) {
+	if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 18) {
+		t.Skip("generics")
+	}
+	// Tests that setting breakpoints inside a generic function with multiple
+	// instantiations results in a single logical breakpoint with N physical
+	// breakpoints (N = number of instantiations).
+	withTestClient2("genericbp", t, func(c service.Client) {
+		fp := testProgPath(t, "genericbp")
+		bp, err := c.CreateBreakpoint(&api.Breakpoint{File: fp, Line: 6})
+		assertNoError(err, t, "CreateBreakpoint")
+		if len(bp.Addrs) != 2 {
+			t.Fatalf("wrong number of physical breakpoints: %d", len(bp.Addrs))
+		}
+
+		frame1Line := func() int {
+			frames, err := c.Stacktrace(-1, 10, 0, nil)
+			assertNoError(err, t, "Stacktrace")
+			return frames[1].Line
+		}
+
+		state := <-c.Continue()
+		assertNoError(state.Err, t, "Continue")
+		if line := frame1Line(); line != 10 {
+			t.Errorf("wrong line after first continue, expected 10, got %d", line)
+		}
+
+		state = <-c.Continue()
+		assertNoError(state.Err, t, "Continue")
+		if line := frame1Line(); line != 11 {
+			t.Errorf("wrong line after first continue, expected 11, got %d", line)
+		}
+	})
+}
diff --git a/service/test/variables_test.go b/service/test/variables_test.go
index 44474084..74e80110 100644
--- a/service/test/variables_test.go
+++ b/service/test/variables_test.go
@@ -1602,7 +1602,48 @@ func TestCgoEval(t *testing.T) {
 					t.Fatalf("Unexpected error. Expected %s got %s", tc.err.Error(), err.Error())
 				}
 			}
+		}
+	})
+}
+
+func TestEvalExpressionGenerics(t *testing.T) {
+	if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 18) {
+		t.Skip("generics not supported")
+	}
+
+	testcases := [][]varTest{
+		// testfn[int, float32]
+		[]varTest{
+			{"arg1", true, "3", "", "int", nil},
+			{"arg2", true, "2.1", "", "float32", nil},
+			{"m", true, "map[float32]int [2.1: 3, ]", "", "map[float32]int", nil},
+		},
+
+		// testfn[*astruct, astruct]
+		[]varTest{
+			{"arg1", true, "*main.astruct {x: 0, y: 1}", "", "*main.astruct", nil},
+			{"arg2", true, "main.astruct {x: 2, y: 3}", "", "main.astruct", nil},
+			{"m", true, "map[main.astruct]*main.astruct [{x: 2, y: 3}: *{x: 0, y: 1}, ]", "", "map[main.astruct]*main.astruct", nil},
+		},
+	}
 
+	withTestProcess("testvariables_generic", t, func(p *proc.Target, fixture protest.Fixture) {
+		for i, tcs := range testcases {
+			assertNoError(p.Continue(), t, fmt.Sprintf("Continue() returned an error (%d)", i))
+			for _, tc := range tcs {
+				variable, err := evalVariable(p, tc.name, pnormalLoadConfig)
+				if tc.err == nil {
+					assertNoError(err, t, fmt.Sprintf("EvalExpression(%s) returned an error", tc.name))
+					assertVariable(t, variable, tc)
+				} else {
+					if err == nil {
+						t.Fatalf("Expected error %s, got no error (%s)", tc.err.Error(), tc.name)
+					}
+					if tc.err.Error() != err.Error() {
+						t.Fatalf("Unexpected error. Expected %s got %s", tc.err.Error(), err.Error())
+					}
+				}
+			}
 		}
 	})
 }