It has been pointed out that nested functions can't be referenced by name (with CallbackCreate, Func with a variable, etc.) in a static initializer. This is due to a more fundamental issue.
Conceptually, local variables only exist while the function is executing. Each time the function is called, it creates a new set of local variables. So how can a static initializer refer to a local variable, given that it executes when the script starts, without the function having been called?
This is due to the current implementation:
So currently, a static initializer can use a local variable, and it will retain its value until the first call to the function returns:I wrote:In AutoHotkey, all non-dynamic variable references are resolved at the moment the script launches, including local variables. Each function has one set of local variables which exist from the moment the script launches until it exits. The code within the function contains pointers to these variables. Recursion is handled by backing up the local variables when a second layer of the function begins and restoring them after it returns, so at any one time, the local variables belong to the top layer of the function.
Closures may be associated with any layer of a function still running, or a set of variables from a function which has since returned. So obviously, they can't refer to the "top-layer" local variables described above. What they need are "free variables", which are not tied to the top layer of the function, are not freed when the function returns, and are able to exist for as long as a closure refers to them.
Closures are implemented by detecting which variables need to be "free variables", allocating those when the outer function begins, and turning the top-layer variables in the outer and inner function into aliases of the free variables.
Source: nested-functions ByRef parameters - AutoHotkey Community
Code: Select all
fn() ; #2: 1
fn() ; #3: blank
fn() {
static x := MsgBox(y := 1) ; #1: 1
MsgBox y
}
Code: Select all
outer() {
static x := inner()
inner() {
MsgBox "inner"
}
}
Code: Select all
outer() {
static x := (MsgBox(outervar), inner())
outervar := 1
inner() {
MsgBox "inner"
(outervar)
}
}
Func("nested_function") (partly) works due to an optimization, which was originally only intended to improve performance, not affect behaviour. However, since the outer function isn't executing when Func() is called, what you get is a plain Func reference, never a Closure. If the outer function has free variables (that is, if nested_function should be a closure), the reference you get can only be called while the outer function is executing. If it is called, it is associated with the "top layer" of the outer function. For example:
Code: Select all
outer1() ; OK
outer2() ; OK
outer3().call() ; FAIL
outer1() {
static x := Func("inner1")
x.call()
inner1() {
MsgBox "inner1"
}
}
outer2() {
static x := Func("inner2")
local outervar
x.call()
inner2() {
MsgBox "inner2"
(outervar)
}
}
outer3() {
static x := Func("inner3")
local outervar
return x
inner3() {
MsgBox "inner3"
(outervar)
}
}
- The static initializer could assign a string or object to the upvar/downvar before it becomes an alias. This would never be freed.
- If a function is running, its upvars/downvars are aliases for whichever set of variables is associated with the "top layer" of the function. At some point after the last layer returns, the aliases become dangling pointers. So given static initializers X and Y (defined in that order), if X calls the function inside which Y is defined, Y may try to dereference a dangling pointer.
For just the last two problems, the "upvars/downvars" could be freed before becoming aliases and reverted to non-aliases after the function returns. However, this imposes a performance penalty on all legitimate calls just to allow virtually illegitimate ones. That is why "externally called subroutines" (as described here) are not permitted in v2.
1. References to nested functions or local variables in static initializers could be prohibited.
Care must be taken to not lose flexibility; for instance, a static initializer should be able to call a self-contained nested function. A function would need certain limitations to be considered self-contained, such as not having any "upvars", nor calling a non-self-contained nested function or referring to one by name, nor referring to any function by name if that name cannot be determined at load-time (e.g. Func(var) vs Func("name")). This seems complex, which is a problem for both implementation and usability. This is why "Func vs Closure" rule is very simple: if any of the outer function's local variables are referenced by any nested function, they all must be closures.
2. The minimum context could be established before evaluating the static initializers. That is, effectively "call" the function with blank parameters, and supply a set of empty free variables if the function is a closure.
3. Static initializers could be evaluated the first time the function is called. This would legitimize local variables and closures within static initializers, eliminating any weird behaviour. There are at least two ways it could be implemented:
- Evaluate all static initializers before execution "begins" at the top of the function.
- Evaluate each static initializer only if and when that line is reached. That is, each static line would act like a normal assignment (or multi-statement) the first time it is reached during execution, then it would become a no-op. This might add greater flexibility.
However, static initializers currently also serve as a means of auto-executing code on startup. A replacement for that could be cleaner and more flexible than static initializers. For instance:
Code: Select all
Initialize() {
;...
}
auto Initialize()
; ---- or ----
auto Initialize() {
;...
}
; ---- or ----
auto {
;... (global)
}
; ==== vs ====
Initialize() {
;...
static _ := Initialize()
}