Repository: go-delve/delve

Issue:
Delve loses it's mind if it fails to recompile when restarting
### What version of Delve are you using (`dlv version`)?

Version: 1.25.2, Build: $Id: af3df277866d7175e816eab14e56611053d4cdea $

### What version of Go are you using? (`go version`)?

go version go1.25.3 X:nodwarf5 linux/amd64

### What operating system and processor architecture are you using?

Intel x86_64, Linux 6.12.41, Gentoo, KDE

### Description

1. Create a go.mod and main.go with a main function.
2. Put something in the main function (e.g. `println("Hello world")`).
3. Put a breakpoint in the main function.
4. Start debugging - I'm using vscode-go's default, I just hit F5 without configuring anything.
5. Once the breakpoint is hit, introduce a compilation error - I removed the trailing `}` of the main function.
6. Restart the session - I did this via vscode's restart debug session button.
7. Delve is now borked.

<img width="824" height="91" alt="Image" src="https://github.com/user-attachments/assets/11737a9f-467c-4692-bd6d-d19a71086a0c" />

<img width="835" height="132" alt="Image" src="https://github.com/user-attachments/assets/1de815f8-9f77-4fd2-9858-137196e108d3" />

Those errors are expected, but now none of the actions (step/continue/restart/stop) have an effect. Restart *sometimes* does something. If I restart the session a second time, sometimes it restarts (and fails to start) but I think that's because vscode is falling back to the old restart-by-stopping-and-starting method. But sometimes it stays borked. I think it is perfectly reasonable for the step and continue operations to not work, but the fact that I can't stop the session is problematic. **Delve should either terminate the session, or allow me to manually terminate the session** (via the button/DAP request).

Hints from issue comments:

Solution patch:
diff --git a/service/dap/server.go b/service/dap/server.go
index 539c2cc8..7fb21eca 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -2793,7 +2793,7 @@ func (s *Session) metadataToDAPVariables(v *fullyQualifiedVariable) ([]dap.Varia
 		})
 	}
 
-	if isListOfBytesOrRunes(v.Variable) {
+	if isListOfBytesOrRunes(v.Variable) && v.Addr != 0 && (v.Flags&proc.VariableFakeAddress == 0) {
 		// Return the string value of []byte or []rune.
 		typeName := api.PrettyTypeName(v.DwarfType)
 		loadExpr := fmt.Sprintf("string(*(*%q)(%#x))", typeName, v.Addr)
@@ -3247,6 +3247,9 @@ func (s *Session) onRestartRequest(request *dap.RestartRequest) {
 	s.changeStateMu.Lock()
 	defer s.changeStateMu.Unlock()
 
+	// The response is just an acknowledgement, if we don't send it VSCode gets wedged
+	s.send(&dap.RestartResponse{Response: *newResponse(request.Request)})
+
 	// Cannot restart in noDebug mode
 	if s.isNoDebug() {
 		s.sendErrorResponse(request.Request, FailedToLaunch, "Cannot restart", "cannot restart in noDebug mode")
@@ -3307,16 +3310,23 @@ func (s *Session) onRestartRequest(request *dap.RestartRequest) {
 		}
 	}
 
+	validBefore := true
+	if _, err := s.debugger.State(false); err != nil {
+		validBefore = false
+	}
+
 	// TODO(deparker) handle args from the launch regarding re-recording.
 	discardedBreakpoints, err := s.debugger.Restart(false, "", resetArgs, newArgs, [3]string{}, rebuild)
 	if err != nil {
 		s.sendErrorResponse(request.Request, FailedToLaunch, "Failed to restart", err.Error())
+		if _, err := s.debugger.State(false); validBefore && err != nil {
+			s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
+		}
 		return
 	}
 
 	s.config.log.Infof("Restart completed. Discarded breakpoints: %v", discardedBreakpoints)
 
-	s.send(&dap.RestartResponse{Response: *newResponse(request.Request)})
 	s.send(&dap.InitializedEvent{Event: *newEvent("initialized")})
 }
 



Test patch:
diff --git a/_fixtures/testRestartRequestRebuildFailFixture.go b/_fixtures/testRestartRequestRebuildFailFixture.go
new file mode 100644
index 00000000..65c4b01e
--- /dev/null
+++ b/_fixtures/testRestartRequestRebuildFailFixture.go
@@ -0,0 +1,12 @@
+package main
+
+import "math"
+
+var f = 1.5
+
+func main() {
+	floatvar1 := math.Floor(f)
+	floatvar2 := float64(int(f))
+	_ = floatvar1
+	_ = floatvar2
+}
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index a07198a6..dd2c132b 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -6393,6 +6393,68 @@ func TestRestartRequestRebuild(t *testing.T) {
 	})
 }
 
+func TestRestartRequestRebuildFail(t *testing.T) {
+	const modifiedSource = `package main
+
+import "math"
+
+var f = 1.5
+
+func main() {
+	floatvar1 := math.Floor(f)
+	floatvar2 := float64(int(f))
+	_ = floatvar1
+	_ = floatvar2
+	the syntax error btw
+}
+`
+	runTest(t, "testRestartRequestRebuildFailFixture", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequestWithArgs(map[string]any{
+					"mode": "debug", "program": fixture.Source,
+				})
+			},
+			// Set breakpoints
+			fixture.Source, []int{7}, // Set breakpoint at main.main
+			[]onBreakpoint{{
+				execute: func() {
+					checkStop(t, client, 1, "main.main", 7)
+
+					fi, _ := os.Stat(fixture.Source)
+
+					originalSource, err := os.ReadFile(fixture.Source)
+					if err != nil {
+						t.Fatalf("could not read original source: %v", err)
+					}
+
+					// Ensure we write the original source code back after the test exits.
+					defer os.WriteFile(fixture.Source, originalSource, fi.Mode())
+
+					err = os.WriteFile(fixture.Source, []byte(modifiedSource), fi.Mode())
+					if err != nil {
+						t.Fatalf("could not modify source: %v", err)
+					}
+
+					client.RestartRequest(map[string]any{
+						"arguments": map[string]any{
+							"request":     "launch",
+							"mode":        "debug",
+							"program":     fixture.Source,
+							"stopOnEntry": false,
+							// "rebuild":     true, omitted as it should default to true
+						},
+					})
+					client.ExpectRestartResponse(t)
+					client.ExpectErrorResponse(t)
+					client.ExpectTerminatedEvent(t)
+				},
+				disconnect: false,
+			}})
+	})
+}
+
 // TestRestartRequestWithNewArgs tests that the restart request can accept new program arguments.
 func TestRestartRequestWithNewArgs(t *testing.T) {
 	runTest(t, "testargs", func(client *daptest.Client, fixture protest.Fixture) {
diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go
index 29efc6e5..c55ae6a3 100644
--- a/service/test/integration2_test.go
+++ b/service/test/integration2_test.go
@@ -3211,6 +3211,9 @@ func TestGuessSubstitutePath(t *testing.T) {
 		// GuessSubstitutePath fail.
 		t.Skip("does not work in TeamCity + tip + linux")
 	}
+	if os.Getenv("CI") != "" && runtime.GOOS == "linux" && runtime.GOARCH == "riscv64" {
+		t.Skip("does not work on the riscv64 builder")
+	}
 
 	slashnorm := func(s string) string {
 		if runtime.GOOS != "windows" {