Skip to content

Instantly share code, notes, and snippets.

@aarzilli
Created August 16, 2023 13:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aarzilli/0cd2ca5e4b955321e9df0f2c6f6c39e1 to your computer and use it in GitHub Desktop.
Save aarzilli/0cd2ca5e4b955321e9df0f2c6f6c39e1 to your computer and use it in GitHub Desktop.

Under the assumption that the range-over-function proposal is accepted and that the following code:

for x := range seq(3) {
	fmt.Println(x)
}

compiles equivalently to:

seq(3)(func(x int) bool {
	fmt.Println(x)
	return true
})

we need some way to tie the stack frames where the loop body closure is called with the stack frame that it logically belongs to (lets call it owner frame). We need this for various reasons:

  1. so that while the debugger is inside the loop body it can show the variables from the scope of the owner function
  2. so that the 'stepout' command can work correctly
  3. so that 'next' and 'step' can function correctly if the loop body uses 'break' or 'return'
  4. so that 'next' and 'step' can step to a line in the owner function outside of the loop
  5. so that when the owner function starts the iteration 'next' and 'step' can enter the body of the loop

In particolar for 5 note that just setting an unconditional breakpoint on the first statement of the loop body does not work:

	  1	func seq(n, m int) func(yield func(int, int) bool) bool {
	  2		if m == 0 {
	  3			return func(yield func(int, int) bool) bool {
	  4				for i := 0; i < n; i++ {
	  5					fmt.Println("m=0 ", i)
	  6					if !yield(i, 0) {
	  7						break
	  8					}
	  9				}
	10				return true
	11			}
	12		}
	13		return func(yield func(int, int) bool) bool {
=>	14			for x, y := range seq(n, m-1) {
	15				fmt.Println("m=", m, " ", x, y)
	16				if !yield(x, m) {
	17					break
	18				}
	19			}
	20			return true
	21		}
	22	}

in this example if we are stopped on line 14 and have 'm=3' we want 'next' to take us to line 15 on the frame where m=3 but a conditional breakpoint will take us to the frame where 'm=1'.

One possible way to do this is to compile:

for x := range seq(3) {
	fmt.Println(x)
}

to this:

__owner_frameoff := RSP - g.stack.hi
seq(3)(func(x int) bool {
	fmt.Println(x)
	return true
	runtime.KeepAlive(__owner_frameoff)
})

__owner_frameoff would be a variable that would have the following properties:

  • has a name that can't be specified by the user (like .dict)
  • captured by the loop body
  • is excluded from the variables of the function that contains the loop
  • is kept alive for the entire duration of the loop body

Then delve can check if __owner_frameoff exists and if it does search for the owner frame and also display the variables that are in scope there (solves problem 1). If __owner_frameoff exists we would set breakpoints on the function running in the owner frame with the condition RSP - g.stack.hi == <current value of __owner_frameoff>. Instead of setting the normal step out breakpoint on the return address of the current frame we would set one for the owner frame (solves problems 2, 3, 4). For 'next' and 'step' when the current function has any closure that captures a variable called __owner_frameoff we would also set breakpoints on those closure functions with the condition __owner_frameoff == <current value of RSP> - g.stack.hi (solves problem 5).

I picked RSP - g.stack.hi because it doesn't change if the goroutine stack moves, but if the runtime takes care of updating it RSP would also be fine, obviously.

I've also looked at the way the new defer is implemented and an alternative solution would be to have runtime.deferrangefunc return the *_defer instead and save it in a variable with the properties described above (this would have to happen always, as opposed to only when the loop body uses defer).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment