Waldemar Horwat wrote:
> The problem is that you then get a plethora of ways
> to define things:
> [...]
> Furthermore, some of them don't make sense (such as
> "function" without "let") because they can conditionally
> capture variables that may not even exist.
Despite very much searching in the discussion archives I can't
find a single description that convinces me that there is in
fact a problem. My analyses find simple solutions for all the
situations I can think of, including nonexistent let variables.
But maybe I just haven't understood the problem. Could you give
me a link to a description?
In my analyses, all you need to do is specify what |function|
without |let| is supposed to mean, in a way that is well suited
to how ECMAScript declarations behave.
You get useful, helpful semantics, and proper throwing of errors
on incorrect access, with simple implementation, if you think how
you would manually make the function name accessible in the outer
scope, and then let the compiler make that very arrangement. The
result is useful and intuitive and avoids peculiar irregularities.
Of course it works only if it can be handled with rules that are
simple enough that the compiler can deal with all cases. I'll take
this in steps to show that the rules become simple enough.
Consider the semantics of a standard function declaration-and-
definition-combined:
Fn();
function Fn() {return 1}
As with any declaration in ECMAScript, the declaration of the name
Fn takes effect before you enter the scope.
This name gets the special treatment that is afforded to functions:
It is assigned its value before you enter the scope. This value is
the function object. So the call to Fn is successful.
Consider the difference when it's conditional:
Fn();
if (Unknown)
function Fn() {return 1}
else
function Fn() {return 2}
Again, as with any declaration, the declaration of the name takes
effect before you enter the scope.
However, in this case it can't be assigned any value before you
enter the scope, since there isn't any known value. Fn exists but
is unassigned, it has the special value |undefined|. The call to
Fn() throws an error as an attempt to call undefined().
The above is equivalent to the following, which is how you would
do the same thing manually if you wanted the same result including
the bug:
var Fn; // Automatically assigned the value |undefined|.
Fn();
if (Unknown)
Fn = function Fn() {return 1}
else
Fn = function Fn() {return 2}
So where the programmer wrote function declarations the compiler
arranges assignment in cases like these.
Even though this changes the behavior of function(), in that the
assignment comes later than usual, this is not a case of hidden
surprising semantics. The programmer did specify that the function
Fn depends on if(Unknown). This obeys what the programmer said.
It would be wrong to decide upon one of the two functions and assign
that before scope entry, as it would violate the requirement that
Fn depend on if(Unknown). What's more, even if the condition is
known at compilation time, the programmer is using a construct
that is intrinsically sequential. So regardless of what is known,
the most exact interpretation is still to maintain the sequential
nature. This way you get simple, consistent semantics.
Let's move the function call into a block and have Fn hoist out
of that:
print (Fn);
if (Unknown)
{ Fn();
function Fn() {return 1} // Hoisted to global scope.
}
The compiler should do this:
var Fn; // Hoisted name, assigned the value |undefined|.
print (Fn);
if (Unknown)
{ Fn = HiddenName; // Early assignment at beginning of block.
Fn();
function HiddenName() {return 1}
}
(Except the function knows itself as Fn rather than HiddenName.)
Here the name Fn cannot have a value at the beginning of the global
scope, but it can have a value at the beginning of the block where
it's declared-and-defined.
The special treatment of functions, where the compiler moves the
assignment to the beginning of the block, should happen when the
compiler can determine that this is correct, using simple rules,
rules that are simple not only for the compiler but also for the
programmer. In any situation where the compiler can't easily
determine this, it assigns |undefined| at block entry, and then
assigns function object at the spot where the function is defined.
There may be two block-entry points to consider, as above.
Let's hoist with a nonexistent let variable:
Fn();
if (Unknown)
{ let LetVal = 3; // Nonexistent when Fn() is called.
if (Maybe)
{ function Fn()// Hoisting to Outer.
{ return LetVal; // Return the let variable.
}
}
}
This is just like the other cases. At the beginning of the global
scope the name Fn exists and has the value