Skip to content

Conversation

@StunxFS
Copy link
Contributor

@StunxFS StunxFS commented Oct 31, 2025

Based on #22445 (comment).

This PR modifies the behavior of defer so that it only executes at the end of the scope where it was declared. Currently, defer only executes at the end of each function.

To test this new behavior, you can pass the -scoped-defer flag to the compiler.

For example, this piece of code:

fn main() {
	for i in 1 .. 4 {
		defer {
			println('Deferred execution for ${i}')
		}
		println('Hello, World! ${i}')
	}
}

If we compile it with the V compiler using the current behavior, we have:

$ v run x.v
Hello, World! 1
Hello, World! 2
Hello, World! 3
Deferred execution for -561879496

But with this PR we have:

$ v -scoped-defer run x.v
Hello, World! 1
Deferred execution for 1
Hello, World! 2
Deferred execution for 2
Hello, World! 3
Deferred execution for 3

The old behavior can still be obtained by using defer(fn) {.

Fix #14701.
Fix #22445.
Fix #24663.

@spytheman
Copy link
Member

imho it should be the default, without needing a flag

@StunxFS
Copy link
Contributor Author

StunxFS commented Oct 31, 2025

@spytheman

imho it should be the default, without needing a flag

That's exactly the idea. But vlib has code that uses defer that needs to be updated to the new behavior.

@spytheman
Copy link
Member

I am adding this example, since there was confusion on Discord about the order of execution with the scoped defers:

for i in 1 .. 4 {
	defer { println('Deferred execution for ${i}. Defer 1.') }
	defer { println('Deferred execution for ${i}. Defer 2.') }
	defer { println('Deferred execution for ${i}. Defer 3.') }
	println('Hello, World! ${i}')
}

produces:

#0 22:08:32 ^ feat/scoped-defer ~/v> ./v self && ./v -scoped-defer run xx.v
Hello, World! 1
Deferred execution for 1. Defer 3.
Deferred execution for 1. Defer 2.
Deferred execution for 1. Defer 1.
Hello, World! 2
Deferred execution for 2. Defer 3.
Deferred execution for 2. Defer 2.
Deferred execution for 2. Defer 1.
Hello, World! 3
Deferred execution for 3. Defer 3.
Deferred execution for 3. Defer 2.
Deferred execution for 3. Defer 1.
#0 22:08:34 ^ feat/scoped-defer ~/v>

i.e. with -scoped-defer, the defers in the same scope, are still executed in LIFO order, but just at the end of each iteration, not just once at the end of the function .

@StunxFS StunxFS changed the title all: scoped defer (experimental) all: implement scoped defer (part 1) Nov 1, 2025
@gechandesu
Copy link
Contributor

Why is there a special syntax for defer in functions (old behavior)? Isn't a function just another scope?

I would prefer to use the same syntax for both cases, so that no changes to existing code would be required. I think there is no need to clarify that the defer block will be executed on the function exit if it is declared in the function body not in a nested scope.

fn foo() {
    defer {
        println('Deferred execution for fn scope (old behavior)')
    }
 
    for i in 1 .. 4 {
        defer {
            println('Deferred execution for ${i}')
        }
        println('Hello, World! ${i}')
    }

    println('Hello, World!')
}

@StunxFS
Copy link
Contributor Author

StunxFS commented Nov 1, 2025

@gechandesu Since defer will change its default behavior from function-scoped to block-scoped, defer(fn) is used to make defer execute at the end of the function and not at the exit of the block where it was declared.

$ cat x.v
fn main() {
        {
                defer { println('defer inside main().scope.1') }
                println('hi from main().scope.1')
        }
        println('hi from main().scope.0')
}
$ ./v -scoped-defer run x.v
hi from main().scope.1
defer inside main().scope.1
hi from main().scope.0

With defer(fn):

$ cat x.v
fn main() {
        {
                defer(fn) { println('defer inside main().scope.1') }
                println('hi from main().scope.1')
        }
        println('hi from main().scope.0')
}
$ ./v -scoped-defer run x.v
hi from main().scope.1
hi from main().scope.0
defer inside main().scope.1

@StunxFS StunxFS marked this pull request as ready for review November 1, 2025 19:02
@StunxFS
Copy link
Contributor Author

StunxFS commented Nov 1, 2025

I consider this first part of the scoped-defer implementation complete. V can also parse, generate, and format the defer(fn) { syntax.

To do:

  • Format the necessary code in vlib to use defer(fn) { instead of defer {.
  • Make defer scoped by default, remove flag -scoped-defer.

@spytheman
Copy link
Member

Excellent work @StunxFS .

Thank you very much for working consistently on this important feature 🙇🏻‍♂️.
It will enable a lot of future improvements and cleanups.

I worked on another implementation of it last year in https://github.com/spytheman/v/tree/scoped_defer, but I could not progress it as much as you did here.

@spytheman
Copy link
Member

spytheman commented Nov 2, 2025

@gechandesu you are right, that defer{} in the top fn scope, is 1:1 equivalent to defer(fn){}, but there are some cases in the existing code, where defer{} is used deeply nested in other scopes, introduced by conditions/matches, and those places may have been written, with the current semantic in mind, and may or may not work with the scoped one, and we need a way to mark/re-work them.

(edit: my expectation is that at least for the V repo, in say 90% of the cases, they will work regardless, since they modify things for which the order does not matter, as long as they are executed once the function finish, but I may be wrong ... the biggest difference between the two is for defers in loops, and it was not used like that before)

@spytheman
Copy link
Member

spytheman commented Nov 2, 2025

@medvednikov I think that this PR is an excellent improvement.
What do you think? Do you agree with the new defer(fn){ syntax, for explicitly opting for the old behaviour?

@medvednikov
Copy link
Member

Great job @StunxFS

I agree with the new default behavior.

I'd use defer fn {, but this is also fine.

@medvednikov medvednikov merged commit 2ad8339 into vlang:master Nov 3, 2025
84 checks passed
@StunxFS StunxFS deleted the feat/scoped-defer branch November 3, 2025 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Undefined behavior if variable is out of scope when defer is called RUNTIME ERROR: invalid memory access defer not working correctly from loops

5 participants