Reason #1:
AutoHotkey's functions each have exactly one set of local variables (henceforth "Vars" to show that we're talking about an implementation detail, not semantics from an external perspective):IsByRef has been removed due to ambiguity in the implementation (an alias may be due to passing a reference in, taking a reference to the parameter itself, or referring to it in a closure).
Source: Preview of changes: scope, function and variable references - AutoHotkey Community
Putting aside closures for the moment, if a function already has a running "instance" (whether it is recursive or was interrupted by another thread), calling it causes all of its local Vars to be "backed up" and unset, ready for the new instance. When that instance returns or exits, the Vars are restored to their former state (including any aliasing). One likely reason for this approach is that early versions of AutoHotkey had only global variables and no functions. In any case, this is how local variables work.Each reference to a variable or function is resolved to a memory address, unless it is dynamic.
Source: Script Performance | AutoHotkey v2
In v1, this is a problem for recursive functions. When a function passes its own Var to itself ByRef, the new call to the function receives a reference to its own (now empty) local Var.
If a closure or &ref actually aliased a local Var, it would cease to have the correct value when the outer function returns or is called recursively or by an interrupting thread. So it doesn't work that way. Instead, any local Var which needs to outlive its function automatically becomes an alias for another Var.
For closures, the outer function allocates a block of FreeVars (free variables; "free" as in "not constrained by the duration of the function call") and these are aliased by both the outer function's "down vars" (local Vars which get captured) and by each closure's "up vars" (Vars which just act as pointers, local to the closure). This is the means by which the outer x and the inner x in outer(x) => inner() => x refer to the same storage space despite the functions potentially running at different times.
For &X, when a VarRef is needed for the first time, one is constructed with X's former value and X becomes an alias for the VarRef. If X is local, this lasts until the function returns.
In (x) => () => &x, there are four Vars:
- When the outer function prepares to execute, it allocates a reference-counted block of FreeVars, containing one Var.
- The first x is the "down var", an alias for the FreeVar of the currently running instance of the outer function.
- The second x is the "up var", an alias for the FreeVar of the currently running instance of the closure.
- When &x is evaluated, a VarRef is constructed and the value is moved from the FreeVar into the VarRef. Both the inner x and the FreeVar then become aliases for the VarRef. The outer x is unchanged, and may or may not even be aliasing the same FreeVar still.
For a ByRef parameter specifically, it can be:
- A normal local Var, not an alias.
- An optimized alias for the caller's Var.
- An alias for the caller's VarRef.
- An alias for a locally constructed VarRef.
- An alias for a FreeVar, which is a normal local Var.
- An alias for a FreeVar, which is an alias for a VarRef constructed by a closure.
For error-reporting purposes, a VarRef retains the name and "scope flags" of the source variable. It is possible to determine, for instance, that the parameter is an alias for a VarRef sourced from a parameter with the same name. However, it could be a parameter of a different function, a parameter of a different call to the same function, or the same parameter of the same call, having used &ref.
Reason #2:
It is redundant.
If you want to know whether a value was provided for a parameter, just declare it as p:=unset or p? and use IsSet(p). If you want to pass the parameter along, omitting it if unset, just use F(p?). If you want to be verbose, you can use F(IsSet(p) ? p : unset).
If you want to do the same with a parameter that accepts a VarRef - that is, if you want to know what value was passed for the parameter - then just declare the parameter as by-value. Why complicate it by making the parameter optionally an alias and then afterward asking whether a value was passed?
A parameter doesn't need to be marked with & when it is intended to take a variable reference. MP(X?, Y?) => MouseGetPos(X?, Y?) will pass along whatever VarRef you give MP, or omit the parameter(s). Why would you want to use MP(&X?, &Y?) => MouseGetPos(IsByRef(X) ? &X : unset, IsByRef(Y) ? &Y : unset)?
Reason #3:
Semantics and consistency within the language.
You would presumably want to call IsByRef(X) or IsByRef(&X).
As a function, IsByRef(X) should throw an error if X is unset, or an alias to an unset variable. In theory, evaluating X should produce the value of the target variable, not a reference to the alias. Permitting this call would mean making IsByRef an exception like IsSet; an operator rather than a function. This would mean another reserved word, and that it couldn't be called by reference, only by directly using the keyword. The exception makes sense for IsSet because its express purpose is to check whether the variable is set, and it also needs to affect load-time warnings. It makes less sense for IsByRef, even putting aside backward-compatibility.
IsByRef(&X) should create a VarRef and pass it to IsByRef. A VarRef is "a value representing a reference to a variable". Nowhere is it stated that a VarRef can be used to identify the original variable, or that it is an alias for the original variable. Without IsByRef, the distinction doesn't matter. It is intentionally left vague because in the current implementation, in ((&X) => X)(&Y), both X and Y become aliases for the VarRef. Even putting aside the implementation, X and Y could be interpreted as equivalent references to a variable or storage location.
Reason #4:
Avoiding the imposition of constraints on future implementations and semantics.
There is probably one way to resolve the ambiguity between a parameter which is an alias of the caller's variable and a parameter which is an alias for other purposes. That is to store additional information which has no other purpose than to implement IsByRef; specifically, making a distinction between aliases created for the purpose of ByRef and other aliases.
Making this distinction now means imposing additional requirements on all backward-compatible revisions to the implementation, or alternative implementations that might be created in future.
Reason #5:
Adhering to other lines of thought.
In Concepts - Variables, I explained variables in terms that I believe consistent with how Chris presented them; where a variable has one name, one scope, one storage location. There's another common line of thought that I think is more elegant ("pleasingly ingenious and simple"), and is directly taught with certain other languages. In that line of thought, one or more names can be bound to a value/object/location. A variable doesn't have to inherently be something that has its own name.
As with IsByRef imposing constraints on the implementation (#4), it would impose constraints on how one can think of a variable when explaining what IsByRef does. Different people think different ways, so supporting different ways of thinking is helpful.
This still isn't the full extent of my thoughts, but this post has already gone way "over-budget", so I will leave it at that.