Repository: go-delve/delve
Pull request #2440 (2021-04-18 02:33:08)

dap: handle SetVariable requests
The handler invokes debugger.SetVariableInScope, except for
string type variables. For which, we rely on the `call` command.
Moved the call expression handling logic to the new `doCall`
function, so it can be reused by the SetVariable request handler.

Earlier attempt of this PR tried to trigger a Stopped Event whenever
a variable is changed in order to force the editors to refetch data and
refresh UI. But that seems too expensive and some may think annoying
so we decided to not do that. Editors like VSCode tries to make a local
update for the variable after a successful set variable request. However,
if there are aliases, some info may look inconsistent.

```
  i1 := 1
  p1 :=&i1
  pp1 := &p1
``` 
For example, after updating *p1 for the code, users would see

![Screen Shot 2021-05-07 at 1 47 52 PM](https://user-images.githubusercontent.com/4999471/117495916-40849600-af44-11eb-85b4-895973cab4d2.png)

This inconsistency will be corrected as soon as users continue debugging
and the scope variable is reloaded behind the scene. If this becomes a problem
we need to find a more clever way.

Future improvement planned:
  - This PR returns the requested arg as the response. If the value string
  is a function call, another variable, etc, the value isn't necessarily correct.
  We need to reevaluate and return the value.
  - Maybe accepting arbitrary expression as the arg can be dangerous and
  we can consider to limit acceptable values. Also, currently call is used only
  for string type. If we decide to allow a function call expression for string,
  we can consider to open it to other types too. 

Also fixed a small bug in the call expression evaluation -
Previously, dlv dap returned an error "Unable to evaluate
expression: call stopped" if the call expression is for
variable assignment.  (e.g. call animal = "rabbit").

We met a bug in the injected call path used for assignment. Given the arrival
of the new version of injected call support, I don't know how much of effort
we want to make to chase the bug though.

Fixes golang/vscode-go#1173

hyangah: The test failure is from arm64
```
------- Stdout: -------
=== RUN   TestSetVariable
DAP server listening at: [::]:49859
Connecting to server at: [::]:49859
server_test.go:2913: got &dap.ErrorResponse{Response:dap.Response{ProtocolMessage:dap.ProtocolMessage{Seq:0, Type:"response"}, RequestSeq:11, Success:false, Command:"setVariable", Message:"Unable to set variable"}, Body:dap.ErrorResponseBody{Error:dap.ErrorMessage{Id:2010, Format:"Unable to set variable: backend does not support function calls", Variables:map[string]string(nil), SendTelemetry:false, ShowUser:false, Url:"", UrlLabel:""}}}, want StoppedEvent
--- FAIL: TestSetVariable (0.47s)
```

Should we disable this test on arm64?
@polinasok @suzmue do you think dlv dap shouldn't set 'supportSetVariable' capability on arm64? I guess it's ok to set  'supportSetVariable' even on arm64 even with this limitation. 
aarzilli: PS. arm64 never supported call injection.
suzmue: > Should we disable this test on arm64?
@polinasok @suzmue do you think dlv dap shouldn't set 'supportSetVariable' capability on arm64? I guess it's ok to set 'supportSetVariable' even on arm64 even with this limitation.

If we don't support set variable on arm64 at all, I think it would probably be best if we didn't set 'supportSetVariable'. We can address that in a separate change tho.
hyangah: @suzmue set variable works for scalar types except string on arm64. Similarly, on amd64, set variable for compound types or that requires memory allocation won't work anyway.
suzmue: > @suzmue set variable works for scalar types except string on arm64. Similarly, on amd64, set variable for compound types or that requires memory allocation won't work anyway.

Ok then I think we should definitely still set 'supportsSetVariable'. It's probably worth checking the user experience when set variable fails.
hyangah: Again the DeepSource failure is not relevant to this. And I am not sure if the specific log security check is useful for the daptest package. Is there a way to disable the check for the package?
polinasok: > That shows how rarely this feature is used.

Or it could be an indication that people don't even bother filing issues and just decide quietly to not not rely on the broken feature. Hopefully not. 

aarzilli: > Again the DeepSource failure is not relevant to this.

It is not and it shouldn't be reported anyway because it isn't a new issue.

> And I am not sure if the specific log security check is useful for the daptest package. Is there a way to disable the check for the package?

No, just ignore it.
derekparker: @polinasok you're still waiting on your review comments to be addressed, correct?
polinasok: > @polinasok you're still waiting on your review comments to be addressed, correct?

I believe so.
derekparker: @hyangah please fix merge conflicts.

@polinasok I haven't seen any updated addressing your comments so I'll hold off on final review and merge for now, unless I'm mistaken.
polinasok: > @hyangah please fix merge conflicts.
> 
> @polinasok I haven't seen any updated addressing your comments so I'll hold off on final review and merge for now, unless I'm mistaken.

I would prefer if this did not get merged with the issues I pointed out without a further discussion. Thank you.
I believe @hyangah has been graciously waiting for the dust to settle on some of the other more urgent PRs to avoid unnecessary conflicts. As those get merged in, I expect there will be movement on this PR as well.
hyangah: The TeamCity builder failed due to a failure in pkg/proc test (windows). I don't know how this PR is related.

```
=== RUN   TestFrameEvaluation
support.go:252: enabling recording for github.com/go-delve/delve/pkg/proc_test.TestFrameEvaluation
...
proc_test.go:1315: Goroutine 1 not found
--- FAIL: TestFrameEvaluation (0.08s)
```
polinasok: > Sorry that resolving merge conflict was painful and I had to rebase this. Github review UI doesn't allow me to respond to comments on outdated version. Let me know if I missed anything.

I believe this happens because you rebase and force push. If you just push a new commit, then it would be possible to go back to previous commits and see and respond to the old comments. I am not sure how I will related the old comments to the new code now.
derekparker: @polinasok are you +1 on this?
aarzilli: Merge conflicts.
hyangah: Merged master, and passed tests. Thanks!

Patch:
diff --git a/Documentation/cli/README.md b/Documentation/cli/README.md
index 94a5a14c..94e7081c 100644
--- a/Documentation/cli/README.md
+++ b/Documentation/cli/README.md
@@ -34,6 +34,7 @@ Command | Description
 [on](#on) | Executes a command when a breakpoint is hit.
 [toggle](#toggle) | Toggles on or off a breakpoint.
 [trace](#trace) | Set tracepoint.
+[watch](#watch) | Set watchpoint.
 
 
 ## Viewing program variables and memory
@@ -581,6 +582,24 @@ Print package variables.
 If regex is specified only package variables with a name matching it will be returned. If -v is specified more information about each package variable will be shown.
 
 
+## watch
+Set watchpoint.
+	
+	watch [-r|-w|-rw] <expr>
+	
+	-r	stops when the memory location is read
+	-w	stops when the memory location is written
+	-rw	stops when the memory location is read or written
+
+The memory location is specified with the same expression language used by 'print', for example:
+
+	watch v
+
+will watch the address of variable 'v'.
+
+See also: "help print".
+
+
 ## whatis
 Prints type of an expression.
 
diff --git a/Documentation/cli/starlark.md b/Documentation/cli/starlark.md
index b64e9552..ee60d5dd 100644
--- a/Documentation/cli/starlark.md
+++ b/Documentation/cli/starlark.md
@@ -26,6 +26,7 @@ clear_breakpoint(Id, Name) | Equivalent to API call [ClearBreakpoint](https://go
 clear_checkpoint(ID) | Equivalent to API call [ClearCheckpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ClearCheckpoint)
 raw_command(Name, ThreadID, GoroutineID, ReturnInfoLoadConfig, Expr, UnsafeCall) | Equivalent to API call [Command](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Command)
 create_breakpoint(Breakpoint) | Equivalent to API call [CreateBreakpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.CreateBreakpoint)
+create_watchpoint(Scope, Expr, Type) | Equivalent to API call [CreateWatchpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.CreateWatchpoint)
 detach(Kill) | Equivalent to API call [Detach](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Detach)
 disassemble(Scope, StartPC, EndPC, Flavour) | Equivalent to API call [Disassemble](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Disassemble)
 dump_cancel() | Equivalent to API call [DumpCancel](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.DumpCancel)
diff --git a/pkg/proc/breakpoints.go b/pkg/proc/breakpoints.go
index b0ed757f..0b5dafea 100644
--- a/pkg/proc/breakpoints.go
+++ b/pkg/proc/breakpoints.go
@@ -35,6 +35,7 @@ type Breakpoint struct {
 	Name         string // User defined name of the breakpoint
 	LogicalID    int    // ID of the logical breakpoint that owns this physical breakpoint
 
+	WatchExpr    string
 	WatchType    WatchType
 	HWBreakIndex uint8 // hardware breakpoint index
 
@@ -316,7 +317,11 @@ func (t *Target) SetWatchpoint(scope *EvalScope, expr string, wtype WatchType, c
 		return nil, errors.New("can not watch stack allocated variable")
 	}
 
-	return t.setBreakpointInternal(xv.Addr, UserBreakpoint, wtype.withSize(uint8(sz)), cond)
+	bp, err := t.setBreakpointInternal(xv.Addr, UserBreakpoint, wtype.withSize(uint8(sz)), cond)
+	if bp != nil {
+		bp.WatchExpr = expr
+	}
+	return bp, err
 }
 
 func (t *Target) setBreakpointInternal(addr uint64, kind BreakpointKind, wtype WatchType, cond ast.Expr) (*Breakpoint, error) {
diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go
index 7209c782..b243ee52 100644
--- a/pkg/terminal/command.go
+++ b/pkg/terminal/command.go
@@ -130,6 +130,21 @@ See also: "help on", "help cond" and "help clear"`},
 A tracepoint is a breakpoint that does not stop the execution of the program, instead when the tracepoint is hit a notification is displayed. See $GOPATH/src/github.com/go-delve/delve/Documentation/cli/locspec.md for the syntax of linespec.
 
 See also: "help on", "help cond" and "help clear"`},
+		{aliases: []string{"watch"}, group: breakCmds, cmdFn: watchpoint, helpMsg: `Set watchpoint.
+	
+	watch [-r|-w|-rw] <expr>
+	
+	-r	stops when the memory location is read
+	-w	stops when the memory location is written
+	-rw	stops when the memory location is read or written
+
+The memory location is specified with the same expression language used by 'print', for example:
+
+	watch v
+
+will watch the address of variable 'v'.
+
+See also: "help print".`},
 		{aliases: []string{"restart", "r"}, group: runCmds, cmdFn: restart, helpMsg: `Restart process.
 
 For recorded targets the command takes the following forms:
@@ -1664,6 +1679,30 @@ func edit(t *Term, ctx callContext, args string) error {
 	return cmd.Run()
 }
 
+func watchpoint(t *Term, ctx callContext, args string) error {
+	v := strings.SplitN(args, " ", 2)
+	if len(v) != 2 {
+		return errors.New("wrong number of arguments: watch [-r|-w|-rw] <expr>")
+	}
+	var wtype api.WatchType
+	switch v[0] {
+	case "-r":
+		wtype = api.WatchRead
+	case "-w":
+		wtype = api.WatchWrite
+	case "-rw":
+		wtype = api.WatchRead | api.WatchWrite
+	default:
+		return fmt.Errorf("wrong argument %q to watch", v[0])
+	}
+	bp, err := t.client.CreateWatchpoint(ctx.Scope, v[1], wtype)
+	if err != nil {
+		return err
+	}
+	fmt.Printf("%s set at %s\n", formatBreakpointName(bp, true), t.formatBreakpointLocation(bp))
+	return nil
+}
+
 func examineMemoryCmd(t *Term, ctx callContext, argstr string) error {
 	var (
 		address uint64
@@ -2432,7 +2471,9 @@ func printcontextThread(t *Term, th *api.Thread) {
 	}
 
 	bpname := ""
-	if th.Breakpoint.Name != "" {
+	if th.Breakpoint.WatchExpr != "" {
+		bpname = fmt.Sprintf("watchpoint on [%s] ", th.Breakpoint.WatchExpr)
+	} else if th.Breakpoint.Name != "" {
 		bpname = fmt.Sprintf("[%s] ", th.Breakpoint.Name)
 	}
 
@@ -2794,6 +2835,9 @@ func formatBreakpointName(bp *api.Breakpoint, upcase bool) string {
 	if bp.Tracepoint {
 		thing = "tracepoint"
 	}
+	if bp.WatchExpr != "" {
+		thing = "watchpoint"
+	}
 	if upcase {
 		thing = strings.Title(thing)
 	}
@@ -2801,6 +2845,9 @@ func formatBreakpointName(bp *api.Breakpoint, upcase bool) string {
 	if id == "" {
 		id = strconv.Itoa(bp.ID)
 	}
+	if bp.WatchExpr != "" && bp.WatchExpr != bp.Name {
+		return fmt.Sprintf("%s %s on [%s]", thing, id, bp.WatchExpr)
+	}
 	state := "(enabled)"
 	if bp.Disabled {
 		state = "(disabled)"
@@ -2822,11 +2869,13 @@ func (t *Term) formatBreakpointLocation(bp *api.Breakpoint) string {
 		// In case we are connecting to an older version of delve that does not return the Addrs field.
 		fmt.Fprintf(&out, "%#x", bp.Addr)
 	}
-	fmt.Fprintf(&out, " for ")
-	p := t.formatPath(bp.File)
-	if bp.FunctionName != "" {
-		fmt.Fprintf(&out, "%s() ", bp.FunctionName)
+	if bp.WatchExpr == "" {
+		fmt.Fprintf(&out, " for ")
+		p := t.formatPath(bp.File)
+		if bp.FunctionName != "" {
+			fmt.Fprintf(&out, "%s() ", bp.FunctionName)
+		}
+		fmt.Fprintf(&out, "%s:%d", p, bp.Line)
 	}
-	fmt.Fprintf(&out, "%s:%d", p, bp.Line)
 	return out.String()
 }
diff --git a/pkg/terminal/starbind/starlark_mapping.go b/pkg/terminal/starbind/starlark_mapping.go
index 01e5a957..8f0bb02b 100644
--- a/pkg/terminal/starbind/starlark_mapping.go
+++ b/pkg/terminal/starbind/starlark_mapping.go
@@ -313,6 +313,54 @@ func (env *Env) starlarkPredeclare() starlark.StringDict {
 		}
 		return env.interfaceToStarlarkValue(rpcRet), nil
 	})
+	r["create_watchpoint"] = starlark.NewBuiltin("create_watchpoint", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
+		if err := isCancelled(thread); err != nil {
+			return starlark.None, decorateError(thread, err)
+		}
+		var rpcArgs rpc2.CreateWatchpointIn
+		var rpcRet rpc2.CreateWatchpointOut
+		if len(args) > 0 && args[0] != starlark.None {
+			err := unmarshalStarlarkValue(args[0], &rpcArgs.Scope, "Scope")
+			if err != nil {
+				return starlark.None, decorateError(thread, err)
+			}
+		} else {
+			rpcArgs.Scope = env.ctx.Scope()
+		}
+		if len(args) > 1 && args[1] != starlark.None {
+			err := unmarshalStarlarkValue(args[1], &rpcArgs.Expr, "Expr")
+			if err != nil {
+				return starlark.None, decorateError(thread, err)
+			}
+		}
+		if len(args) > 2 && args[2] != starlark.None {
+			err := unmarshalStarlarkValue(args[2], &rpcArgs.Type, "Type")
+			if err != nil {
+				return starlark.None, decorateError(thread, err)
+			}
+		}
+		for _, kv := range kwargs {
+			var err error
+			switch kv[0].(starlark.String) {
+			case "Scope":
+				err = unmarshalStarlarkValue(kv[1], &rpcArgs.Scope, "Scope")
+			case "Expr":
+				err = unmarshalStarlarkValue(kv[1], &rpcArgs.Expr, "Expr")
+			case "Type":
+				err = unmarshalStarlarkValue(kv[1], &rpcArgs.Type, "Type")
+			default:
+				err = fmt.Errorf("unknown argument %q", kv[0])
+			}
+			if err != nil {
+				return starlark.None, decorateError(thread, err)
+			}
+		}
+		err := env.ctx.Client().CallAPI("CreateWatchpoint", &rpcArgs, &rpcRet)
+		if err != nil {
+			return starlark.None, err
+		}
+		return env.interfaceToStarlarkValue(rpcRet), nil
+	})
 	r["detach"] = starlark.NewBuiltin("detach", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
 		if err := isCancelled(thread); err != nil {
 			return starlark.None, decorateError(thread, err)
diff --git a/service/api/conversions.go b/service/api/conversions.go
index 51b1c0c1..08514b4b 100644
--- a/service/api/conversions.go
+++ b/service/api/conversions.go
@@ -33,6 +33,8 @@ func ConvertBreakpoint(bp *proc.Breakpoint) *Breakpoint {
 		Variables:     bp.Variables,
 		LoadArgs:      LoadConfigFromProc(bp.LoadArgs),
 		LoadLocals:    LoadConfigFromProc(bp.LoadLocals),
+		WatchExpr:     bp.WatchExpr,
+		WatchType:     WatchType(bp.WatchType),
 		TotalHitCount: bp.TotalHitCount,
 		Addrs:         []uint64{bp.Addr},
 	}
diff --git a/service/api/types.go b/service/api/types.go
index 31e26005..26b165d5 100644
--- a/service/api/types.go
+++ b/service/api/types.go
@@ -83,6 +83,11 @@ type Breakpoint struct {
 	LoadArgs *LoadConfig
 	// LoadLocals requests loading function locals when the breakpoint is hit
 	LoadLocals *LoadConfig
+
+	// WatchExpr is the expression used to create this watchpoint
+	WatchExpr string
+	WatchType WatchType
+
 	// number of times a breakpoint has been reached in a certain goroutine
 	HitCount map[string]uint64 `json:"hitCount"`
 	// number of times a breakpoint has been reached
@@ -109,6 +114,14 @@ func ValidBreakpointName(name string) error {
 	return nil
 }
 
+// WatchType is the watchpoint type
+type WatchType uint8
+
+const (
+	WatchRead WatchType = 1 << iota
+	WatchWrite
+)
+
 // Thread is a thread within the debugged process.
 type Thread struct {
 	// ID is a unique identifier for the thread.
diff --git a/service/client.go b/service/client.go
index 4b6a7b79..a83831ef 100644
--- a/service/client.go
+++ b/service/client.go
@@ -66,6 +66,8 @@ type Client interface {
 	GetBreakpointByName(name string) (*api.Breakpoint, error)
 	// CreateBreakpoint creates a new breakpoint.
 	CreateBreakpoint(*api.Breakpoint) (*api.Breakpoint, error)
+	// CreateWatchpoint creates a new watchpoint.
+	CreateWatchpoint(api.EvalScope, string, api.WatchType) (*api.Breakpoint, error)
 	// ListBreakpoints gets all breakpoints.
 	ListBreakpoints() ([]*api.Breakpoint, error)
 	// ClearBreakpoint deletes a breakpoint by ID.
diff --git a/service/dap/error_ids.go b/service/dap/error_ids.go
index 2ae0f330..23d9d3c1 100644
--- a/service/dap/error_ids.go
+++ b/service/dap/error_ids.go
@@ -23,6 +23,7 @@ const (
 	UnableToEvaluateExpression = 2009
 	UnableToHalt               = 2010
 	UnableToGetExceptionInfo   = 2011
+	UnableToSetVariable        = 2012
 	// Add more codes as we support more requests
 	DebuggeeIsRunning = 4000
 	DisconnectError   = 5000
diff --git a/service/dap/server.go b/service/dap/server.go
index 28166c03..e678a480 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -12,8 +12,10 @@ import (
 	"bufio"
 	"bytes"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"go/constant"
+	"go/parser"
 	"io"
 	"net"
 	"os"
@@ -460,6 +462,14 @@ func (s *Server) handleRequest(request dap.Message) {
 			// For this to apply in cases other than api.Continue, we would also need to
 			// introduce a new version of halt that skips ClearInternalBreakpoints
 			// in proc.(*Target).Continue, leaving NextInProgress as true.
+		case *dap.SetFunctionBreakpointsRequest:
+			s.log.Debug("halting execution to set breakpoints")
+			_, err := s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil)
+			if err != nil {
+				s.sendErrorResponse(request.Request, UnableToSetBreakpoints, "Unable to set or clear breakpoints", err.Error())
+				return
+			}
+			s.onSetFunctionBreakpointsRequest(request)
 		default:
 			r := request.(dap.RequestMessage).GetRequest()
 			s.sendErrorResponse(*r, DebuggeeIsRunning, fmt.Sprintf("Unable to process `%s`", r.Command), "debuggee is running")
@@ -677,9 +687,8 @@ func (s *Server) onInitializeRequest(request *dap.InitializeRequest) {
 	response.Body.SupportTerminateDebuggee = true
 	response.Body.SupportsFunctionBreakpoints = true
 	response.Body.SupportsExceptionInfoRequest = true
+	response.Body.SupportsSetVariable = true
 	response.Body.SupportsEvaluateForHovers = true
-	// TODO(polina): support this to match vscode-go functionality
-	response.Body.SupportsSetVariable = false
 	// TODO(polina): support these requests in addition to vscode-go feature parity
 	response.Body.SupportsTerminateRequest = false
 	response.Body.SupportsRestartRequest = false
@@ -1440,12 +1449,38 @@ 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) {
-	v, ok := s.variableHandles.get(request.Arguments.VariablesReference)
+	ref := request.Arguments.VariablesReference
+	v, ok := s.variableHandles.get(ref)
 	if !ok {
-		s.sendErrorResponse(request.Request, UnableToLookupVariable, "Unable to lookup variable", fmt.Sprintf("unknown reference %d", request.Arguments.VariablesReference))
+		s.sendErrorResponse(request.Request, UnableToLookupVariable, "Unable to lookup variable", fmt.Sprintf("unknown reference %d", ref))
 		return
 	}
-	children := make([]dap.Variable, 0)
+
+	children, err := s.childrenToDAPVariables(v)
+	if err != nil {
+		s.sendErrorResponse(request.Request, UnableToLookupVariable, "Unable to lookup variable", err.Error())
+		return
+	}
+	if !s.clientCapabilities.supportsVariableType {
+		// If the client does not support variable type
+		// we cannot set the Type field in the response.
+		for i := range children {
+			children[i].Type = ""
+		}
+	}
+	response := &dap.VariablesResponse{
+		Response: *newResponse(request.Request),
+		Body:     dap.VariablesResponseBody{Variables: children},
+	}
+	s.send(response)
+}
+
+// childrenToDAPVariables returns the DAP presentation of the referenced variable's children.
+func (s *Server) 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.
+	var children []dap.Variable
 
 	switch v.Kind {
 	case reflect.Map:
@@ -1565,18 +1600,7 @@ func (s *Server) onVariablesRequest(request *dap.VariablesRequest) {
 			}
 		}
 	}
-	if !s.clientCapabilities.supportsVariableType {
-		// If the client does not support variable type
-		// we cannot set the Type field in the response.
-		for i := range children {
-			children[i].Type = ""
-		}
-	}
-	response := &dap.VariablesResponse{
-		Response: *newResponse(request.Request),
-		Body:     dap.VariablesResponseBody{Variables: children},
-	}
-	s.send(response)
+	return children, nil
 }
 
 func (s *Server) getTypeIfSupported(v *proc.Variable) string {
@@ -1773,71 +1797,12 @@ func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
 	response := &dap.EvaluateResponse{Response: *newResponse(request.Request)}
 	isCall, err := regexp.MatchString(`^\s*call\s+\S+`, request.Arguments.Expression)
 	if err == nil && isCall { // call {expression}
-		// 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
-		// expressions, it will evaluate them in the context of the topmost frame,
-		// and the user will get an unexpected result or an unexpected symbol error.
-		// We prevent this but disallowing any frames other than topmost.
-		if frame > 0 {
-			s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", "call is only supported with topmost stack frame", showErrorToUser)
-			return
-		}
-		stateBeforeCall, err := s.debugger.State( /*nowait*/ true)
-		if err != nil {
-			s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser)
-			return
-		}
-		// TODO(polina): since call will resume execution of all goroutines,
-		// we should do this asynchronously and send a continued event to the
-		// editor, followed by a stop event when the call completes.
-		state, err := s.debugger.Command(&api.DebuggerCommand{
-			Name:                 api.Call,
-			ReturnInfoLoadConfig: api.LoadConfigFromProc(&DefaultLoadConfig),
-			Expr:                 strings.Replace(request.Arguments.Expression, "call ", "", 1),
-			UnsafeCall:           false,
-			GoroutineID:          goid,
-		}, nil)
-		if _, isexited := err.(proc.ErrProcessExited); isexited || err == nil && state.Exited {
-			e := &dap.TerminatedEvent{Event: *newEvent("terminated")}
-			s.send(e)
-			return
-		}
+		expr := strings.Replace(request.Arguments.Expression, "call ", "", 1)
+		_, retVars, err := s.doCall(goid, frame, expr)
 		if err != nil {
 			s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser)
 			return
 		}
-		// After the call is done, the goroutine where we injected the call should
-		// return to the original stopped line with return values. However,
-		// it is not guaranteed to be selected due to the possibility of the
-		// of simultaenous breakpoints. Therefore, we check all threads.
-		var retVars []*proc.Variable
-		for _, t := range state.Threads {
-			if t.GoroutineID == stateBeforeCall.SelectedGoroutine.ID &&
-				t.Line == stateBeforeCall.SelectedGoroutine.CurrentLoc.Line && t.CallReturn {
-				// The call completed. Get the return values.
-				retVars, err = s.debugger.FindThreadReturnValues(t.ID, DefaultLoadConfig)
-				if err != nil {
-					s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser)
-					return
-				}
-				break
-			}
-		}
-		if retVars == nil {
-			// The call got interrupted by a stop (e.g. breakpoint in injected
-			// function call or in another goroutine)
-			s.resetHandlesForStoppedEvent()
-			stopped := &dap.StoppedEvent{Event: *newEvent("stopped")}
-			stopped.Body.AllThreadsStopped = true
-			stopped.Body.ThreadId = stoppedGoroutineID(state)
-			stopped.Body.Reason = s.debugger.StopReason().String()
-			s.send(stopped)
-			// TODO(polina): once this is asynchronous, we could wait to reply until the user
-			// continues, call ends, original stop point is hit and return values are available.
-			s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", "call stopped", showErrorToUser)
-			return
-		}
 		// The call completed and we can reply with its return values (if any)
 		if len(retVars) > 0 {
 			// Package one or more return values in a single scope-like nameless variable
@@ -1867,6 +1832,102 @@ func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
 	s.send(response)
 }
 
+func (s *Server) 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
+	// expressions, it will evaluate them in the context of the topmost frame,
+	// and the user will get an unexpected result or an unexpected symbol error.
+	// We prevent this but disallowing any frames other than topmost.
+	if frame > 0 {
+		return nil, nil, fmt.Errorf("call is only supported with topmost stack frame")
+	}
+	stateBeforeCall, err := s.debugger.State( /*nowait*/ true)
+	if err != nil {
+		return nil, nil, err
+	}
+	// TODO(polina): since call will resume execution of all goroutines,
+	// we should do this asynchronously and send a continued event to the
+	// editor, followed by a stop event when the call completes.
+	state, err := s.debugger.Command(&api.DebuggerCommand{
+		Name:                 api.Call,
+		ReturnInfoLoadConfig: api.LoadConfigFromProc(&DefaultLoadConfig),
+		Expr:                 expr,
+		UnsafeCall:           false,
+		GoroutineID:          goid,
+	}, nil)
+	if _, isexited := err.(proc.ErrProcessExited); isexited || err == nil && state.Exited {
+		e := &dap.TerminatedEvent{Event: *newEvent("terminated")}
+		s.send(e)
+		return nil, nil, errors.New("terminated")
+	}
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// After the call is done, the goroutine where we injected the call should
+	// return to the original stopped line with return values. However,
+	// it is not guaranteed to be selected due to the possibility of the
+	// of simultaenous breakpoints. Therefore, we check all threads.
+	var retVars []*proc.Variable
+	found := false
+	for _, t := range state.Threads {
+		if t.GoroutineID == stateBeforeCall.SelectedGoroutine.ID &&
+			t.Line == stateBeforeCall.SelectedGoroutine.CurrentLoc.Line && t.CallReturn {
+			found = true
+			// The call completed. Get the return values.
+			retVars, err = s.debugger.FindThreadReturnValues(t.ID, DefaultLoadConfig)
+			if err != nil {
+				return nil, nil, err
+			}
+			break
+		}
+	}
+	// Normal function calls expect return values, but call commands
+	// used for variable assignments do not return a value when they succeed.
+	// In go '=' is not an operator. Check if go/parser complains.
+	// If the above Call command passed but the expression is not a valid
+	// go expression, we just handled a variable assignment request.
+	isAssignment := false
+	if _, err := parser.ParseExpr(expr); err != nil {
+		isAssignment = true
+	}
+
+	// note: as described in https://github.com/golang/go/issues/25578, function call injection
+	// causes to resume the entire Go process. Due to this limitation, there is no guarantee
+	// that the process is in the same state even after the injected call returns normally
+	// without any surprises such as breakpoints or panic. To handle this correctly we need
+	// to reset all the handles (both variables and stack frames).
+	//
+	// We considered sending a stopped event after each call unconditionally, but a stopped
+	// event can be expensive and can interact badly with the client-side optimization
+	// to refresh information. For example, VS Code reissues scopes/evaluate (for watch) after
+	// completing a setVariable or evaluate request for repl context. Thus, for now, we
+	// do not trigger a stopped event and hope editors to refetch the updated state as soon
+	// as the user resumes debugging.
+
+	if !found || !isAssignment && retVars == nil {
+		// The call got interrupted by a stop (e.g. breakpoint in injected
+		// function call or in another goroutine).
+		s.resetHandlesForStoppedEvent()
+		s.sendStoppedEvent(state)
+
+		// TODO(polina): once this is asynchronous, we could wait to reply until the user
+		// continues, call ends, original stop point is hit and return values are available
+		// instead of returning an error 'call stopped' here.
+		return nil, nil, errors.New("call stopped")
+	}
+	return state, retVars, nil
+}
+
+func (s *Server) sendStoppedEvent(state *api.DebuggerState) {
+	stopped := &dap.StoppedEvent{Event: *newEvent("stopped")}
+	stopped.Body.AllThreadsStopped = true
+	stopped.Body.ThreadId = stoppedGoroutineID(state)
+	stopped.Body.Reason = s.debugger.StopReason().String()
+	s.send(stopped)
+}
+
 // onTerminateRequest sends a not-yet-implemented error response.
 // Capability 'supportsTerminateRequest' is not set in 'initialize' response.
 func (s *Server) onTerminateRequest(request *dap.TerminateRequest) {
@@ -1888,7 +1949,6 @@ func (s *Server) onSetFunctionBreakpointsRequest(request *dap.SetFunctionBreakpo
 		s.sendErrorResponse(request.Request, UnableToSetBreakpoints, "Unable to set or clear breakpoints", "running in noDebug mode")
 		return
 	}
-	// TODO(polina): handle this while running by halting first.
 
 	// According to the spec, setFunctionBreakpoints "replaces all existing function
 	// breakpoints with new function breakpoints." The simplest way is
@@ -1988,10 +2048,116 @@ func (s *Server) onReverseContinueRequest(request *dap.ReverseContinueRequest) {
 	s.sendNotYetImplementedErrorResponse(request.Request)
 }
 
-// onSetVariableRequest sends a not-yet-implemented error response.
-// Capability 'supportsSetVariable' is not set 'initialize' response.
-func (s *Server) onSetVariableRequest(request *dap.SetVariableRequest) { // TODO V0
-	s.sendNotYetImplementedErrorResponse(request.Request)
+// computeEvaluateName finds the named child, and computes its evaluate name.
+func (s *Server) computeEvaluateName(v *fullyQualifiedVariable, cname string) (string, error) {
+	children, err := s.childrenToDAPVariables(v)
+	if err != nil {
+		return "", err
+	}
+	for _, c := range children {
+		if c.Name == cname {
+			if c.EvaluateName != "" {
+				return c.EvaluateName, nil
+			}
+			return "", errors.New("cannot set the variable without evaluate name")
+		}
+	}
+	return "", errors.New("failed to find the named variable")
+}
+
+// onSetVariableRequest handles 'setVariable' requests.
+func (s *Server) onSetVariableRequest(request *dap.SetVariableRequest) {
+	arg := request.Arguments
+
+	v, ok := s.variableHandles.get(arg.VariablesReference)
+	if !ok {
+		s.sendErrorResponse(request.Request, UnableToSetVariable, "Unable to lookup variable", fmt.Sprintf("unknown reference %d", arg.VariablesReference))
+		return
+	}
+	// We need to translate the arg.Name to its evaluateName if the name
+	// refers to a field or element of a variable.
+	// https://github.com/microsoft/vscode/issues/120774
+	evaluateName, err := s.computeEvaluateName(v, arg.Name)
+	if err != nil {
+		s.sendErrorResponse(request.Request, UnableToSetVariable, "Unable to set variable", err.Error())
+		return
+	}
+
+	// By running EvalVariableInScope, we get the type info of the variable
+	// that can be accessed with the evaluateName, and ensure the variable we are
+	// trying to update is valid and accessible from the top most frame & the
+	// current goroutine.
+	goid, frame := -1, 0
+	evaluated, err := s.debugger.EvalVariableInScope(goid, frame, 0, evaluateName, DefaultLoadConfig)
+	if err != nil {
+		s.sendErrorResponse(request.Request, UnableToSetVariable, "Unable to lookup variable", err.Error())
+		return
+	}
+
+	useFnCall := false
+	switch evaluated.Kind {
+	case reflect.String:
+		useFnCall = true
+	default:
+		// TODO(hyangah): it's possible to set a non-string variable using (`call i = fn()`)
+		// and we don't support it through the Set Variable request yet.
+		// If we want to support it for non-string types, we need to parse arg.Value.
+	}
+
+	if useFnCall {
+		// TODO(hyangah): function call injection currentlly allows to assign return values of
+		// a function call to variables. So, curious users would find set variable
+		// on string would accept expression like `fn()`.
+		if state, retVals, err := s.doCall(goid, frame, fmt.Sprintf("%v=%v", evaluateName, arg.Value)); err != nil {
+			s.sendErrorResponse(request.Request, UnableToSetVariable, "Unable to set variable", err.Error())
+			return
+		} else if retVals != nil {
+			// The assignment expression isn't supposed to return values, but we got them.
+			// That indicates something went wrong (e.g. panic).
+			// TODO: isn't it simpler to do this in s.doCall?
+			s.resetHandlesForStoppedEvent()
+			s.sendStoppedEvent(state)
+
+			var r []string
+			for _, v := range retVals {
+				r = append(r, s.convertVariableToString(v))
+			}
+			msg := "interrupted"
+			if len(r) > 0 {
+				msg = "interrupted:" + strings.Join(r, ", ")
+			}
+
+			s.sendErrorResponse(request.Request, UnableToSetVariable, "Unable to set variable", msg)
+			return
+		}
+	} else {
+		if err := s.debugger.SetVariableInScope(goid, frame, 0, evaluateName, arg.Value); err != nil {
+			s.sendErrorResponse(request.Request, UnableToSetVariable, "Unable to set variable", err.Error())
+			return
+		}
+	}
+	// * Note on inconsistent state after set variable:
+	//
+	// The variable handles may be in inconsistent state - for example,
+	// let's assume there are two aliased variables pointing to the same
+	// memory and both are already loaded and cached in the variable handle.
+	// VSCode tries to locally update the UI when the set variable
+	// request succeeds, and may issue additional scopes or evaluate requests
+	// to update the variable/watch sections if necessary.
+	//
+	// More complicated situation is when the set variable involves call
+	// injection - after the injected call is completed, the debugee can
+	// be in a completely different state (see the note in doCall) due to
+	// how the call injection is implemented. Ideally, we need to also refresh
+	// the stack frames but that is complicated. For now we don't try to actively
+	// invalidate this state hoping that the editors will refetch the state
+	// as soon as the user resumes debugging.
+
+	response := &dap.SetVariableResponse{Response: *newResponse(request.Request)}
+	response.Body.Value = arg.Value
+	// TODO(hyangah): instead of arg.Value, reload the variable and return
+	// the presentation of the new value.
+	s.send(response)
 }
 
 // onSetExpression sends a not-yet-implemented error response.
diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go
index 0aa4de22..00ff9333 100644
--- a/service/debugger/debugger.go
+++ b/service/debugger/debugger.go
@@ -517,7 +517,9 @@ func (d *Debugger) Restart(rerecord bool, pos string, resetArgs bool, newArgs []
 		if oldBp.ID > maxID {
 			maxID = oldBp.ID
 		}
-		if len(oldBp.File) > 0 {
+		if oldBp.WatchExpr != "" {
+			discarded = append(discarded, api.DiscardedBreakpoint{Breakpoint: oldBp, Reason: "can not recreate watchpoints on restart"})
+		} else if len(oldBp.File) > 0 {
 			addrs, err := proc.FindFileLocation(p, oldBp.File, oldBp.Line)
 			if err != nil {
 				discarded = append(discarded, api.DiscardedBreakpoint{Breakpoint: oldBp, Reason: err.Error()})
@@ -527,6 +529,7 @@ func (d *Debugger) Restart(rerecord bool, pos string, resetArgs bool, newArgs []
 		} else {
 			// Avoid setting a breakpoint based on address when rebuilding
 			if rebuild {
+				discarded = append(discarded, api.DiscardedBreakpoint{Breakpoint: oldBp, Reason: "can not recreate address breakpoints on restart"})
 				continue
 			}
 			newBp, err := p.SetBreakpointWithID(oldBp.ID, oldBp.Addr)
@@ -730,6 +733,11 @@ func (d *Debugger) AmendBreakpoint(amend *api.Breakpoint) error {
 	defer d.targetMutex.Unlock()
 
 	originals := d.findBreakpoint(amend.ID)
+
+	if len(originals) > 0 && originals[0].WatchExpr != "" && amend.Disabled {
+		return errors.New("can not disable watchpoints")
+	}
+
 	_, disabled := d.disabledBreakpoints[amend.ID]
 	if originals == nil && !disabled {
 		return fmt.Errorf("no breakpoint with ID %d", amend.ID)
@@ -938,6 +946,22 @@ func (d *Debugger) findDisabledBreakpointByName(name string) *api.Breakpoint {
 	return nil
 }
 
+// CreateWatchpoint creates a watchpoint on the specified expression.
+func (d *Debugger) CreateWatchpoint(goid, frame, deferredCall int, expr string, wtype api.WatchType) (*api.Breakpoint, error) {
+	s, err := proc.ConvertEvalScope(d.target, goid, frame, deferredCall)
+	if err != nil {
+		return nil, err
+	}
+	bp, err := d.target.SetWatchpoint(s, expr, proc.WatchType(wtype), nil)
+	if err != nil {
+		return nil, err
+	}
+	if api.ValidBreakpointName(expr) == nil && d.findBreakpointByName(expr) == nil {
+		bp.Name = expr
+	}
+	return api.ConvertBreakpoint(bp), nil
+}
+
 // Threads returns the threads of the target process.
 func (d *Debugger) Threads() ([]proc.Thread, error) {
 	d.targetMutex.Lock()
@@ -1889,6 +1913,9 @@ func (v breakpointsByLogicalID) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
 
 func (v breakpointsByLogicalID) Less(i, j int) bool {
 	if v[i].LogicalID == v[j].LogicalID {
+		if v[i].WatchType != v[j].WatchType {
+			return v[i].WatchType > v[j].WatchType // if a logical breakpoint contains a watchpoint let the watchpoint sort first
+		}
 		return v[i].Addr < v[j].Addr
 	}
 	return v[i].LogicalID < v[j].LogicalID
diff --git a/service/rpc2/client.go b/service/rpc2/client.go
index 3a585fc2..314274fd 100644
--- a/service/rpc2/client.go
+++ b/service/rpc2/client.go
@@ -232,6 +232,12 @@ func (c *RPCClient) CreateBreakpoint(breakPoint *api.Breakpoint) (*api.Breakpoin
 	return &out.Breakpoint, err
 }
 
+func (c *RPCClient) CreateWatchpoint(scope api.EvalScope, expr string, wtype api.WatchType) (*api.Breakpoint, error) {
+	var out CreateWatchpointOut
+	err := c.call("CreateWatchpoint", CreateWatchpointIn{scope, expr, wtype}, &out)
+	return out.Breakpoint, err
+}
+
 func (c *RPCClient) ListBreakpoints() ([]*api.Breakpoint, error) {
 	var out ListBreakpointsOut
 	err := c.call("ListBreakpoints", ListBreakpointsIn{}, &out)
diff --git a/service/rpc2/server.go b/service/rpc2/server.go
index 81ee8aac..3d5edb1c 100644
--- a/service/rpc2/server.go
+++ b/service/rpc2/server.go
@@ -939,3 +939,19 @@ type DumpCancelOut struct {
 func (s *RPCServer) DumpCancel(arg DumpCancelIn, out *DumpCancelOut) error {
 	return s.debugger.DumpCancel()
 }
+
+type CreateWatchpointIn struct {
+	Scope api.EvalScope
+	Expr  string
+	Type  api.WatchType
+}
+
+type CreateWatchpointOut struct {
+	*api.Breakpoint
+}
+
+func (s *RPCServer) CreateWatchpoint(arg CreateWatchpointIn, out *CreateWatchpointOut) error {
+	var err error
+	out.Breakpoint, err = s.debugger.CreateWatchpoint(arg.Scope.GoroutineID, arg.Scope.Frame, arg.Scope.DeferredCall, arg.Expr, arg.Type)
+	return err
+}


Test patch:
diff --git a/_fixtures/testvariables2.go b/_fixtures/testvariables2.go
index f5988e3a..d350262b 100644
--- a/_fixtures/testvariables2.go
+++ b/_fixtures/testvariables2.go
@@ -331,8 +331,8 @@ func main() {
 
 	var nilstruct *astruct = nil
 
+	val := A{val: 1} // val vs val.val
 	var as2 astruct
-
 	s4 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
 
 	var iface2map interface{} = map[string]interface{}{
@@ -363,5 +363,5 @@ func main() {
 	longslice := make([]int, 100, 100)
 
 	runtime.Breakpoint()
-	fmt.Println(i1, i2, i3, p1, pp1, amb1, s1, s3, a0, a1, p2, p3, s2, as1, str1, f1, fn1, fn2, nilslice, nilptr, ch1, chnil, m1, mnil, m2, m3, m4, m5, upnil, up1, i4, i5, i6, err1, err2, errnil, iface1, iface2, ifacenil, arr1, parr, cpx1, const1, iface3, iface4, recursive1, recursive1.x, iface5, iface2fn1, iface2fn2, bencharr, benchparr, mapinf, mainMenu, b, b2, sd, anonstruct1, anonstruct2, anoniface1, anonfunc, mapanonstruct1, ifacearr, efacearr, ni8, ni16, ni32, ni64, pinf, ninf, nan, zsvmap, zsslice, zsvar, tm, rettm, errtypednil, emptyslice, emptymap, byteslice, runeslice, bytearray, runearray, longstr, nilstruct, as2, as2.NonPointerRecieverMethod, s4, iface2map, issue1578, ll, unread, w2, w3, w4, w5, longarr, longslice)
+	fmt.Println(i1, i2, i3, p1, pp1, amb1, s1, s3, a0, a1, p2, p3, s2, as1, str1, f1, fn1, fn2, nilslice, nilptr, ch1, chnil, m1, mnil, m2, m3, m4, m5, upnil, up1, i4, i5, i6, err1, err2, errnil, iface1, iface2, ifacenil, arr1, parr, cpx1, const1, iface3, iface4, recursive1, recursive1.x, iface5, iface2fn1, iface2fn2, bencharr, benchparr, mapinf, mainMenu, b, b2, sd, anonstruct1, anonstruct2, anoniface1, anonfunc, mapanonstruct1, ifacearr, efacearr, ni8, ni16, ni32, ni64, pinf, ninf, nan, zsvmap, zsslice, zsvar, tm, rettm, errtypednil, emptyslice, emptymap, byteslice, runeslice, bytearray, runearray, longstr, nilstruct, as2, as2.NonPointerRecieverMethod, s4, iface2map, issue1578, ll, unread, w2, w3, w4, w5, longarr, longslice, val)
 }
diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go
index 6241a1b9..550c0b89 100644
--- a/pkg/proc/proc_test.go
+++ b/pkg/proc/proc_test.go
@@ -1278,7 +1278,7 @@ func TestFrameEvaluation(t *testing.T) {
 		found := make([]bool, 10)
 		for _, g := range gs {
 			frame := -1
-			frames, err := g.Stacktrace(10, 0)
+			frames, err := g.Stacktrace(40, 0)
 			if err != nil {
 				t.Logf("could not stacktrace goroutine %d: %v\n", g.ID, err)
 				continue
diff --git a/service/dap/daptest/client.go b/service/dap/daptest/client.go
index 180b6161..cb2036b7 100644
--- a/service/dap/daptest/client.go
+++ b/service/dap/daptest/client.go
@@ -101,6 +101,7 @@ func (c *Client) ExpectInitializeResponseAndCapabilities(t *testing.T) *dap.Init
 		SupportsDelayedStackTraceLoading: true,
 		SupportTerminateDebuggee:         true,
 		SupportsExceptionInfoRequest:     true,
+		SupportsSetVariable:              true,
 		SupportsFunctionBreakpoints:      true,
 		SupportsEvaluateForHovers:        true,
 	}
@@ -353,8 +354,12 @@ func (c *Client) ReverseContinueRequest() {
 }
 
 // SetVariableRequest sends a 'setVariable' request.
-func (c *Client) SetVariableRequest() {
-	c.send(&dap.ReverseContinueRequest{Request: *c.newRequest("setVariable")})
+func (c *Client) SetVariableRequest(variablesRef int, name, value string) {
+	request := &dap.SetVariableRequest{Request: *c.newRequest("setVariable")}
+	request.Arguments.VariablesReference = variablesRef
+	request.Arguments.Name = name
+	request.Arguments.Value = value
+	c.send(request)
 }
 
 // RestartFrameRequest sends a 'restartFrame' request.
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index e41769ce..233b7769 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -1859,11 +1859,16 @@ func expectSetBreakpointsResponse(t *testing.T, client *daptest.Client, bps []Br
 
 func checkSetBreakpointsResponse(t *testing.T, client *daptest.Client, bps []Breakpoint, got *dap.SetBreakpointsResponse) {
 	t.Helper()
-	if len(got.Body.Breakpoints) != len(bps) {
-		t.Errorf("got %#v,\nwant len(Breakpoints)=%d", got, len(bps))
+	checkBreakpoints(t, client, bps, got.Body.Breakpoints)
+}
+
+func checkBreakpoints(t *testing.T, client *daptest.Client, bps []Breakpoint, breakpoints []dap.Breakpoint) {
+	t.Helper()
+	if len(breakpoints) != len(bps) {
+		t.Errorf("got %#v,\nwant len(Breakpoints)=%d", breakpoints, len(bps))
 		return
 	}
-	for i, bp := range got.Body.Breakpoints {
+	for i, bp := range breakpoints {
 		if bp.Line != bps[i].line || bp.Verified != bps[i].verified || bp.Source.Path != bps[i].path ||
 			!strings.HasPrefix(bp.Message, bps[i].msgPrefix) {
 			t.Errorf("got breakpoints[%d] = %#v, \nwant %#v", i, bp, bps[i])
@@ -2219,6 +2224,23 @@ func expectSetBreakpointsResponseAndStoppedEvent(t *testing.T, client *daptest.C
 	return se, br
 }
 
+func expectSetFunctionBreakpointsResponseAndStoppedEvent(t *testing.T, client *daptest.Client) (se *dap.StoppedEvent, br *dap.SetFunctionBreakpointsResponse) {
+	for i := 0; i < 2; i++ {
+		switch m := client.ExpectMessage(t).(type) {
+		case *dap.StoppedEvent:
+			se = m
+		case *dap.SetFunctionBreakpointsResponse:
+			br = m
+		default:
+			t.Fatalf("Unexpected message type: expect StoppedEvent or SetFunctionBreakpointsResponse, got %#v", m)
+		}
+	}
+	if se == nil || br == nil {
+		t.Fatal("Expected StoppedEvent and SetFunctionBreakpointsResponse")
+	}
+	return se, br
+}
+
 func TestSetBreakpointWhileRunning(t *testing.T) {
 	runTest(t, "integrationprog", func(client *daptest.Client, fixture protest.Fixture) {
 		runDebugSessionWithBPs(t, client, "launch",
@@ -2275,6 +2297,74 @@ func TestSetBreakpointWhileRunning(t *testing.T) {
 	})
 }
 
+func TestSetFunctionBreakpointWhileRunning(t *testing.T) {
+	runTest(t, "integrationprog", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
+			},
+			// Set breakpoints
+			fixture.Source, []int{16},
+			[]onBreakpoint{{
+				execute: func() {
+					// The program loops 3 times over lines 14-15-8-9-10-16
+					handleStop(t, client, 1, "main.main", 16) // Line that sleeps for 1 second
+
+					// We can set breakpoints while nexting
+					client.NextRequest(1)
+					client.ExpectNextResponse(t)
+					client.ExpectContinuedEvent(t)
+					client.SetFunctionBreakpointsRequest([]dap.FunctionBreakpoint{{Name: "main.sayhi"}}) // [16,] => [16, 8]
+					se, br := expectSetFunctionBreakpointsResponseAndStoppedEvent(t, client)
+					if se.Body.Reason != "pause" || !se.Body.AllThreadsStopped || se.Body.ThreadId != 0 && se.Body.ThreadId != 1 {
+						t.Errorf("\ngot  %#v\nwant Reason='pause' AllThreadsStopped=true ThreadId=0/1", se)
+					}
+					checkBreakpoints(t, client, []Breakpoint{{8, fixture.Source, true, ""}}, br.Body.Breakpoints)
+
+					client.SetBreakpointsRequest(fixture.Source, []int{}) // [16,8] => [8]
+					expectSetBreakpointsResponse(t, client, []Breakpoint{})
+
+					// Halt cancelled next, so if we continue we will not stop at line 14.
+					client.ContinueRequest(1)
+					client.ExpectContinueResponse(t)
+					se = client.ExpectStoppedEvent(t)
+					if se.Body.Reason != "function breakpoint" || !se.Body.AllThreadsStopped || se.Body.ThreadId != 1 {
+						t.Errorf("\ngot  %#v\nwant Reason='breakpoint' AllThreadsStopped=true ThreadId=1", se)
+					}
+					handleStop(t, client, 1, "main.sayhi", 8)
+
+					// We can set breakpoints while continuing
+					client.ContinueRequest(1)
+					client.ExpectContinueResponse(t)
+					client.SetFunctionBreakpointsRequest([]dap.FunctionBreakpoint{}) // [8,] => []
+					se, br = expectSetFunctionBreakpointsResponseAndStoppedEvent(t, client)
+					if se.Body.Reason != "pause" || !se.Body.AllThreadsStopped || se.Body.ThreadId != 0 && se.Body.ThreadId != 1 {
+						t.Errorf("\ngot  %#v\nwant Reason='pause' AllThreadsStopped=true ThreadId=0/1", se)
+					}
+					checkBreakpoints(t, client, []Breakpoint{}, br.Body.Breakpoints)
+					if se.Body.Reason != "pause" || !se.Body.AllThreadsStopped || se.Body.ThreadId != 0 && se.Body.ThreadId != 1 {
+						t.Errorf("\ngot  %#v\nwant Reason='pause' AllThreadsStopped=true ThreadId=0/1", se)
+					}
+					checkBreakpoints(t, client, []Breakpoint{}, br.Body.Breakpoints)
+
+					client.SetBreakpointsRequest(fixture.Source, []int{16}) // [] => [16]
+					expectSetBreakpointsResponse(t, client, []Breakpoint{{16, fixture.Source, true, ""}})
+
+					client.ContinueRequest(1)
+					client.ExpectContinueResponse(t)
+					se = client.ExpectStoppedEvent(t)
+					if se.Body.Reason != "breakpoint" || !se.Body.AllThreadsStopped || se.Body.ThreadId != 1 {
+						t.Errorf("\ngot  %#v\nwant Reason='breakpoint' AllThreadsStopped=true ThreadId=1", se)
+					}
+					handleStop(t, client, 1, "main.main", 16)
+
+				},
+				disconnect: true,
+			}})
+	})
+}
+
 // TestLaunchSubstitutePath sets a breakpoint using a path
 // that does not exist and expects the substitutePath attribute
 // in the launch configuration to take care of the mapping.
@@ -2712,8 +2802,22 @@ func TestEvaluateCallRequest(t *testing.T) {
 					if erres.Body.Error.Format != "Unable to evaluate expression: not enough arguments" {
 						t.Errorf("\ngot %#v\nwant Format=\"Unable to evaluate expression: not enough arguments\"", erres)
 					}
-					// Call can exit
+
+					// Assignment - expect no error, but no return value.
+					client.EvaluateRequest("call one = two", 1000, "this context will be ignored")
+					got = client.ExpectEvaluateResponse(t)
+					expectEval(t, got, "", noChildren)
+					// Check one=two was applied.
+					client.EvaluateRequest("one", 1000, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					expectEval(t, got, "2", noChildren)
+
+					// Call can exit.
 					client.EvaluateRequest("call callexit()", 1000, "this context will be ignored")
+					client.ExpectTerminatedEvent(t)
+					if res := client.ExpectVisibleErrorResponse(t); !strings.Contains(res.Body.Error.Format, "terminated") {
+						t.Errorf("\ngot %#v\nwant Format=.*terminated.*", res)
+					}
 				},
 				terminated: true,
 				disconnect: true,
@@ -3207,9 +3311,6 @@ func runDebugSessionWithBPs(t *testing.T, client *daptest.Client, cmd string, cm
 		client.ExpectStoppedEvent(t)
 		onBP.execute()
 		if onBP.disconnect {
-			if onBP.terminated {
-				client.ExpectTerminatedEvent(t)
-			}
 			client.DisconnectRequestWithKillOption(true)
 			if onBP.terminated {
 				client.ExpectOutputEventProcessExited(t, 0)
@@ -3531,6 +3632,378 @@ func TestUnupportedCommandResponses(t *testing.T) {
 	})
 }
 
+type helperForSetVariable struct {
+	t *testing.T
+	c *daptest.Client
+}
+
+func (h *helperForSetVariable) expectSetVariableAndStop(ref int, name, value string) {
+	h.t.Helper()
+	h.expectSetVariable0(ref, name, value, true)
+}
+func (h *helperForSetVariable) expectSetVariable(ref int, name, value string) {
+	h.t.Helper()
+	h.expectSetVariable0(ref, name, value, false)
+}
+
+func (h *helperForSetVariable) failSetVariable(ref int, name, value, wantErrInfo string) {
+	h.t.Helper()
+	h.failSetVariable0(ref, name, value, wantErrInfo, false)
+}
+
+func (h *helperForSetVariable) failSetVariableAndStop(ref int, name, value, wantErrInfo string) {
+	h.t.Helper()
+	h.failSetVariable0(ref, name, value, wantErrInfo, true)
+}
+
+func (h *helperForSetVariable) evaluate(expr, want string, hasRef bool) {
+	h.t.Helper()
+	h.c.EvaluateRequest(expr, 1000, "whatever")
+	got := h.c.ExpectEvaluateResponse(h.t)
+	expectEval(h.t, got, want, hasRef)
+}
+
+func (h *helperForSetVariable) evaluateRegex(expr, want string, hasRef bool) {
+	h.t.Helper()
+	h.c.EvaluateRequest(expr, 1000, "whatever")
+	got := h.c.ExpectEvaluateResponse(h.t)
+	expectEvalRegex(h.t, got, want, hasRef)
+}
+
+func (h *helperForSetVariable) expectSetVariable0(ref int, name, value string, wantStop bool) {
+	h.t.Helper()
+
+	h.c.SetVariableRequest(ref, name, value)
+	if wantStop {
+		h.c.ExpectStoppedEvent(h.t)
+	}
+	if got, want := h.c.ExpectSetVariableResponse(h.t), value; got.Success != true || got.Body.Value != want {
+		h.t.Errorf("SetVariableRequest(%v, %v)=%#v, want {Success=true, Body.Value=%q", name, value, got, want)
+	}
+}
+
+func (h *helperForSetVariable) failSetVariable0(ref int, name, value, wantErrInfo string, wantStop bool) {
+	h.t.Helper()
+
+	h.c.SetVariableRequest(ref, name, value)
+	if wantStop {
+		h.c.ExpectStoppedEvent(h.t)
+	}
+	resp := h.c.ExpectErrorResponse(h.t)
+	if got := resp.Body.Error.Format; !strings.Contains(got, wantErrInfo) {
+		h.t.Errorf("got %#v, want error string containing %v", got, wantErrInfo)
+	}
+}
+
+func (h *helperForSetVariable) variables(ref int) *dap.VariablesResponse {
+	h.t.Helper()
+	h.c.VariablesRequest(ref)
+	return h.c.ExpectVariablesResponse(h.t)
+}
+
+// TestSetVariable tests SetVariable features that do not need function call support.
+func TestSetVariable(t *testing.T) {
+	runTest(t, "testvariables", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			func() {
+				client.LaunchRequestWithArgs(map[string]interface{}{
+					"mode": "exec", "program": fixture.Path, "showGlobalVariables": true,
+				})
+			},
+			fixture.Source, []int{}, // breakpoints are set within the program.
+			[]onBreakpoint{{
+				execute: func() {
+					tester := &helperForSetVariable{t, client}
+
+					startLineno := 66 // after runtime.Breakpoint
+					if runtime.GOOS == "windows" && goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) {
+						// Go1.15 on windows inserts a NOP after the call to
+						// runtime.Breakpoint and marks it same line as the
+						// runtime.Breakpoint call, making this flaky, so skip the line check.
+						startLineno = -1
+					}
+
+					handleStop(t, client, 1, "main.foobar", startLineno)
+
+					// Args of foobar(baz string, bar FooBar)
+					args := tester.variables(1000)
+
+					expectVarExact(t, args, 1, "bar", "bar", `main.FooBar {Baz: 10, Bur: "lorem"}`, "main.FooBar", hasChildren)
+					tester.failSetVariable(1000, "bar", `main.FooBar {Baz: 42, Bur: "ipsum"}`, "*ast.CompositeLit not implemented")
+
+					// Nested field.
+					barRef := expectVarExact(t, args, 1, "bar", "bar", `main.FooBar {Baz: 10, Bur: "lorem"}`, "main.FooBar", hasChildren)
+					tester.expectSetVariable(barRef, "Baz", "42")
+					tester.evaluate("bar", `main.FooBar {Baz: 42, Bur: "lorem"}`, hasChildren)
+
+					tester.failSetVariable(barRef, "Baz", `"string"`, "can not convert")
+
+					// Local variables
+					locals := tester.variables(1001)
+
+					// int
+					expectVarExact(t, locals, -1, "a2", "a2", "6", "int", noChildren)
+					tester.expectSetVariable(1001, "a2", "42")
+					tester.evaluate("a2", "42", noChildren)
+
+					tester.failSetVariable(1001, "a2", "false", "can not convert")
+
+					// float
+					expectVarExact(t, locals, -1, "a3", "a3", "7.23", "float64", noChildren)
+					tester.expectSetVariable(1001, "a3", "-0.1")
+					tester.evaluate("a3", "-0.1", noChildren)
+
+					// array of int
+					a4Ref := expectVarExact(t, locals, -1, "a4", "a4", "[2]int [1,2]", "[2]int", hasChildren)
+					tester.expectSetVariable(a4Ref, "[1]", "-7")
+					tester.evaluate("a4", "[2]int [1,-7]", hasChildren)
+
+					tester.failSetVariable(1001, "a4", "[2]int{3, 4}", "not implemented")
+
+					// slice of int
+					a5Ref := expectVarExact(t, locals, -1, "a5", "a5", "[]int len: 5, cap: 5, [1,2,3,4,5]", "[]int", hasChildren)
+					tester.expectSetVariable(a5Ref, "[3]", "100")
+					tester.evaluate("a5", "[]int len: 5, cap: 5, [1,2,3,100,5]", hasChildren)
+
+					// composite literal and its nested fields.
+					a7Ref := expectVarExact(t, locals, -1, "a7", "a7", `*main.FooBar {Baz: 5, Bur: "strum"}`, "*main.FooBar", hasChildren)
+					a7Val := tester.variables(a7Ref)
+					a7ValRef := expectVarExact(t, a7Val, -1, "", "(*a7)", `main.FooBar {Baz: 5, Bur: "strum"}`, "main.FooBar", hasChildren)
+					tester.expectSetVariable(a7ValRef, "Baz", "7")
+					tester.evaluate("(*a7)", `main.FooBar {Baz: 7, Bur: "strum"}`, hasChildren)
+
+					// pointer
+					expectVarExact(t, locals, -1, "a9", "a9", `*main.FooBar nil`, "*main.FooBar", noChildren)
+					tester.expectSetVariable(1001, "a9", "&a6")
+					tester.evaluate("a9", `*main.FooBar {Baz: 8, Bur: "word"}`, hasChildren)
+
+					// slice of pointers
+					a13Ref := expectVarExact(t, locals, -1, "a13", "a13", `[]*main.FooBar len: 3, cap: 3, [*{Baz: 6, Bur: "f"},*{Baz: 7, Bur: "g"},*{Baz: 8, Bur: "h"}]`, "[]*main.FooBar", hasChildren)
+					a13 := tester.variables(a13Ref)
+					a13c0Ref := expectVarExact(t, a13, -1, "[0]", "a13[0]", `*main.FooBar {Baz: 6, Bur: "f"}`, "*main.FooBar", hasChildren)
+					a13c0 := tester.variables(a13c0Ref)
+					a13c0valRef := expectVarExact(t, a13c0, -1, "", "(*a13[0])", `main.FooBar {Baz: 6, Bur: "f"}`, "main.FooBar", hasChildren)
+					tester.expectSetVariable(a13c0valRef, "Baz", "777")
+					tester.evaluate("a13[0]", `*main.FooBar {Baz: 777, Bur: "f"}`, hasChildren)
+
+					// complex
+					tester.evaluate("c64", `(1 + 2i)`, hasChildren)
+					tester.expectSetVariable(1001, "c64", "(2 + 3i)")
+					tester.evaluate("c64", `(2 + 3i)`, hasChildren)
+					// note: complex's real, imaginary part can't be directly mutable.
+
+					//
+					// Global variables
+					//    p1 = 10
+					client.VariablesRequest(1002)
+					globals := client.ExpectVariablesResponse(t)
+
+					expectVarExact(t, globals, -1, "p1", "main.p1", "10", "int", noChildren)
+					tester.expectSetVariable(1002, "p1", "-10")
+					tester.evaluate("p1", "-10", noChildren)
+					tester.failSetVariable(1002, "p1", "0.1", "can not convert")
+				},
+				disconnect: true,
+			}})
+	})
+
+	runTest(t, "testvariables2", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			func() {
+				client.LaunchRequestWithArgs(map[string]interface{}{
+					"mode": "exec", "program": fixture.Path, "showGlobalVariables": true,
+				})
+			},
+			fixture.Source, []int{}, // breakpoints are set within the program.
+			[]onBreakpoint{{
+				execute: func() {
+					tester := &helperForSetVariable{t, client}
+
+					startLineno := 358 // after runtime.Breakpoint
+					if runtime.GOOS == "windows" && goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) {
+						startLineno = -1
+					}
+
+					handleStop(t, client, 1, "main.main", startLineno)
+					locals := tester.variables(1001)
+
+					// channel
+					tester.evaluate("chnil", "chan int nil", noChildren)
+					tester.expectSetVariable(1001, "chnil", "ch1")
+					tester.evaluate("chnil", "chan int 4/11", hasChildren)
+
+					// func
+					tester.evaluate("fn2", "nil", noChildren)
+					tester.expectSetVariable(1001, "fn2", "fn1")
+					tester.evaluate("fn2", "main.afunc", noChildren)
+
+					// interface
+					tester.evaluate("ifacenil", "interface {} nil", noChildren)
+					tester.expectSetVariable(1001, "ifacenil", "iface1")
+					tester.evaluate("ifacenil", "interface {}(*main.astruct) *{A: 1, B: 2}", hasChildren)
+
+					// interface.(data)
+					iface1Ref := expectVarExact(t, locals, -1, "iface1", "iface1", "interface {}(*main.astruct) *{A: 1, B: 2}", "interface {}", hasChildren)
+					iface1 := tester.variables(iface1Ref)
+					iface1DataRef := expectVarExact(t, iface1, -1, "data", "iface1.(data)", "*main.astruct {A: 1, B: 2}", "*main.astruct", hasChildren)
+					iface1Data := tester.variables(iface1DataRef)
+					iface1DataValueRef := expectVarExact(t, iface1Data, -1, "", "(*iface1.(data))", "main.astruct {A: 1, B: 2}", "main.astruct", hasChildren)
+					tester.expectSetVariable(iface1DataValueRef, "A", "2021")
+					tester.evaluate("iface1", "interface {}(*main.astruct) *{A: 2021, B: 2}", hasChildren)
+
+					// map: string -> struct
+					tester.evaluate(`m1["Malone"]`, "main.astruct {A: 2, B: 3}", hasChildren)
+					m1Ref := expectVarRegex(t, locals, -1, "m1", "m1", `.*map\[string\]main\.astruct.*`, `map\[string\]main\.astruct`, hasChildren)
+					m1 := tester.variables(m1Ref)
+					elem1 := m1.Body.Variables[0]
+					tester.expectSetVariable(elem1.VariablesReference, "A", "-9999")
+					tester.expectSetVariable(elem1.VariablesReference, "B", "10000")
+					tester.evaluate(elem1.EvaluateName, "main.astruct {A: -9999, B: 10000}", hasChildren)
+
+					// map: struct -> int
+					m3Ref := expectVarExact(t, locals, -1, "m3", "m3", "map[main.astruct]int [{A: 1, B: 1}: 42, {A: 2, B: 2}: 43, ]", "map[main.astruct]int", hasChildren)
+					tester.expectSetVariable(m3Ref, "main.astruct {A: 1, B: 1}", "8888")
+					// note: updating keys is possible, but let's not promise anything.
+					tester.evaluateRegex("m3", `.*\[\{A: 1, B: 1\}: 8888,.*`, hasChildren)
+
+					// map: struct -> struct
+					m4Ref := expectVarRegex(t, locals, -1, "m4", "m4", `map\[main\.astruct]main\.astruct.*\[\{A: 1, B: 1\}: \{A: 11, B: 11\}.*`, `map\[main\.astruct\]main\.astruct`, hasChildren)
+					m4 := tester.variables(m4Ref)
+					m4Val1Ref := expectVarRegex(t, m4, -1, "[val 0]", `.*0x[0-9a-f]+.*`, `main.astruct.*`, `main\.astruct`, hasChildren)
+					tester.expectSetVariable(m4Val1Ref, "A", "-9999")
+					tester.evaluateRegex("m4", `.*A: -9999,.*`, hasChildren)
+
+					// unsigned pointer
+					expectVarRegex(t, locals, -1, "up1", "up1", `unsafe\.Pointer\(0x[0-9a-f]+\)`, "unsafe.Pointer", noChildren)
+					tester.expectSetVariable(1001, "up1", "unsafe.Pointer(0x0)")
+					tester.evaluate("up1", "unsafe.Pointer(0x0)", noChildren)
+
+					// val := A{val: 1}
+					valRef := expectVarExact(t, locals, -1, "val", "val", `main.A {val: 1}`, "main.A", hasChildren)
+					tester.expectSetVariable(valRef, "val", "3")
+					tester.evaluate("val", `main.A {val: 3}`, hasChildren)
+				},
+				disconnect: true,
+			}})
+	})
+}
+
+// TestSetVariableWithCall tests SetVariable features that do not depend on function calls support.
+func TestSetVariableWithCall(t *testing.T) {
+	protest.MustSupportFunctionCalls(t, testBackend)
+
+	runTest(t, "testvariables", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			func() {
+				client.LaunchRequestWithArgs(map[string]interface{}{
+					"mode": "exec", "program": fixture.Path, "showGlobalVariables": true,
+				})
+			},
+			fixture.Source, []int{66, 67},
+			[]onBreakpoint{{
+				execute: func() {
+					tester := &helperForSetVariable{t, client}
+
+					startLineno := 66
+					if runtime.GOOS == "windows" && goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) {
+						// Go1.15 on windows inserts a NOP after the call to
+						// runtime.Breakpoint and marks it same line as the
+						// runtime.Breakpoint call, making this flaky, so skip the line check.
+						startLineno = -1
+					}
+
+					handleStop(t, client, 1, "main.foobar", startLineno)
+
+					// Args of foobar(baz string, bar FooBar)
+					args := tester.variables(1000)
+
+					expectVarExact(t, args, 0, "baz", "baz", `"bazburzum"`, "string", noChildren)
+					tester.expectSetVariable(1000, "baz", `"BazBurZum"`)
+					tester.evaluate("baz", `"BazBurZum"`, noChildren)
+
+					args = tester.variables(1000)
+					barRef := expectVarExact(t, args, 1, "bar", "bar", `main.FooBar {Baz: 10, Bur: "lorem"}`, "main.FooBar", hasChildren)
+					tester.expectSetVariable(barRef, "Bur", `"ipsum"`)
+					tester.evaluate("bar", `main.FooBar {Baz: 10, Bur: "ipsum"}`, hasChildren)
+
+					// Local variables
+					locals := tester.variables(1001)
+
+					expectVarExact(t, locals, -1, "a1", "a1", `"foofoofoofoofoofoo"`, "string", noChildren)
+					tester.expectSetVariable(1001, "a1", `"barbarbar"`)
+					tester.evaluate("a1", `"barbarbar"`, noChildren)
+
+					a6Ref := expectVarExact(t, locals, -1, "a6", "a6", `main.FooBar {Baz: 8, Bur: "word"}`, "main.FooBar", hasChildren)
+					tester.failSetVariable(a6Ref, "Bur", "false", "can not convert")
+
+					tester.expectSetVariable(a6Ref, "Bur", `"sentence"`)
+					tester.evaluate("a6", `main.FooBar {Baz: 8, Bur: "sentence"}`, hasChildren)
+				},
+			}, {
+				// Stop at second breakpoint and set a1.
+				execute: func() {
+					tester := &helperForSetVariable{t, client}
+
+					handleStop(t, client, 1, "main.barfoo", -1)
+					// Test: set string 'a1' in main.barfoo.
+					// This shouldn't affect 'a1' in main.foobar - we will check that in the next breakpoint.
+					locals := tester.variables(1001)
+					expectVarExact(t, locals, -1, "a1", "a1", `"bur"`, "string", noChildren)
+					tester.expectSetVariable(1001, "a1", `"fur"`)
+					tester.evaluate("a1", `"fur"`, noChildren)
+					// We will check a1 in main.foobar isn't affected from the next breakpoint.
+
+					client.StackTraceRequest(1, 1, 20)
+					res := client.ExpectStackTraceResponse(t)
+					if len(res.Body.StackFrames) < 1 {
+						t.Fatalf("stack trace response = %#v, wanted at least one stack frame", res)
+					}
+					outerFrame := res.Body.StackFrames[0].Id
+					client.EvaluateRequest("a1", outerFrame, "whatever_context")
+					evalRes := client.ExpectEvaluateResponse(t)
+					expectEval(t, evalRes, `"barbarbar"`, noChildren)
+				},
+				disconnect: true,
+			}})
+	})
+
+	runTest(t, "fncall", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			func() {
+				client.LaunchRequestWithArgs(map[string]interface{}{
+					"mode": "exec", "program": fixture.Path, "showGlobalVariables": true,
+				})
+			},
+			fixture.Source, []int{}, // breakpoints are set within the program.
+			[]onBreakpoint{{
+				// Stop at second breakpoint and set a1.
+				execute: func() {
+					tester := &helperForSetVariable{t, client}
+
+					handleStop(t, client, 1, "main.main", 197)
+
+					_ = tester.variables(1001)
+
+					// successful variable set using a function call.
+					tester.expectSetVariable(1001, "str", `callstacktrace()`)
+					tester.evaluateRegex("str", `.*in main.callstacktrace at.*`, noChildren)
+
+					tester.failSetVariableAndStop(1001, "str", `callpanic()`, `callpanic panicked`)
+					handleStop(t, client, 1, "main.main", 197)
+
+					// breakpoint during a function call.
+					tester.failSetVariableAndStop(1001, "str", `callbreak()`, "call stopped")
+
+					// TODO(hyangah): continue after this causes runtime error while resuming
+					// unfinished injected call.
+					//   runtime error: can not convert %!s(<nil>) constant to string
+					// This can be reproducible with dlv cli. (`call str = callbreak(); continue`)
+				},
+				disconnect: true,
+			}})
+	})
+}
+
 func TestOptionalNotYetImplementedResponses(t *testing.T) {
 	var got *dap.ErrorResponse
 	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
@@ -3556,9 +4029,6 @@ func TestOptionalNotYetImplementedResponses(t *testing.T) {
 		client.ReverseContinueRequest()
 		expectNotYetImplemented("reverseContinue")
 
-		client.SetVariableRequest()
-		expectNotYetImplemented("setVariable")
-
 		client.SetExpressionRequest()
 		expectNotYetImplemented("setExpression")