Closure memory leak

Report problems with documented functionality
User avatar
thqby
Posts: 416
Joined: 16 Apr 2021, 11:18
Contact:

Closure memory leak

25 Apr 2024, 08:28

Code: Select all

Closure.Prototype.__Delete := this => OutputDebug('closure dctor: ' ObjPtr(this) '`n')
ff() {
  a := 0
  b := f2
  OutputDebug('f1: ' ObjPtr(f1) '`n')
  OutputDebug('f2: ' ObjPtr(f2) '`n')
  f1()
  ; b := 0    ; Free variables was not released during closure destruction.

  f1() {
    b()
  }
  f2() {
    a := 1
  }
}
ff()
lexikos
Posts: 9624
Joined: 30 Sep 2013, 04:07
Contact:

Re: Closure memory leak

05 May 2024, 16:28

This is not a bug.
It is best not to store a reference to a closure in any of the closure's own free variables, since that creates a circular reference which must be broken (such as by clearing the variable) before the closure can be freed. However, a closure may safely refer to itself and other closures by their original variables without creating a circular reference.
Source: Functions - Definition & Usage | AutoHotkey v2
There is no "closure destruction" while a counted reference to the closure still exists, even if it is cyclic. It is the same for all objects. The reference to f1 is not cyclic because f1 is not captured by any closure.
User avatar
thqby
Posts: 416
Joined: 16 Apr 2021, 11:18
Contact:

Re: Closure memory leak

06 May 2024, 02:19

Will the variable b also be captured by f2? I remember that in the past, only referenced variables were captured.

Code: Select all

f() {
	a := 0
	b := { __delete: (*) => MsgBox() }
	f1() {
		(b)
	}
	f2(s?) {
		(a)
		s := 'b'
		MsgBox(IsSet(%s%))
		s := 'f1'
		MsgBox(IsSet(%s%))
	}
	return f2
}
t := f()
t()
However, in f2, variables b and f1 are unset.
lexikos
Posts: 9624
Joined: 30 Sep 2013, 04:07
Contact:

Re: Closure memory leak

06 May 2024, 03:26

By default, a nested function automatically "captures" a non-static local variable of an outer function when the following requirements are met:
  1. ...
  2. The inner function (or a function nested inside it) must refer to the variable non-dynamically.
...
Non-static local variables of the outer function cannot be accessed dynamically unless they have been captured.
Source: Functions - Definition & Usage | AutoHotkey v2
As you see, f2 does not refer to f1 or b non-dynamically, therefore it does not capture them, and cannot access them dynamically.

A closure is a nested function bound to a set of free variables. Free variables are local variables of the outer function which are also used by nested functions.
Source: Functions - Definition & Usage | AutoHotkey v2

What is probably not explained in the documentation yet is that all closures defined directly within a function share the same "set of free variables".

f1 is a closure because it captures b, and f2 is a closure because it captures a. Closures within a function are often related, so the design prioritises this and manages them as a single group, allowing them to refer to each other without causing reference cycles. This also means that the free variables can be allocated exactly once as a group, and do not need to be individually reference-counted. The drawback is that closures which are captured by other closures cannot be individually deleted. Holding a reference to any closure also holds a reference to the set of free variables used by all closures of the same outer function.

In your top post, f2 is not deleted because it is referenced by b, which is part of the set of free variables referenced by f2 itself.
User avatar
thqby
Posts: 416
Joined: 16 Apr 2021, 11:18
Contact:

Re: Closure memory leak

06 May 2024, 05:31

lexikos wrote:
06 May 2024, 03:26
What is probably not explained in the documentation yet is that all closures defined directly within a function share the same "set of free variables".
Yeah, I thought they had separate sets of free variables.

Return to “Bug Reports”

Who is online

Users browsing this forum: No registered users and 8 guests