lexical for-in/for-of loose end
Brendan Eich
brendan at mozilla.org
Sat Feb 4 12:55:17 PST 2012
Allen Wirfs-Brock wrote:
> On Feb 4, 2012, at 9:49 AM, Brendan Eich wrote:
>> I agree we want to capture the first-iteration bindings in any
>> closures in those declarators' initializers.
>
> It isn't clear to me why capture first-iteration is abstractly any
> better than "capture a hidden second x". In both cases, in most
> iterations of the loop, evaluation of any such captures is going to
> reference the "wrong" binding.
The argument is as follows:
for (let i = 0, a = some.array, n = a.length; i < n; i++) { ... }
here we definitely want the a in a.length (n's initializer) to be scoped
by the head let -- to be the a declared by the second declarator.
Now consider a bit of eta conversion:
for (let i = 0, a = some.array, n = (function(){return a})().length;
i < n; i++) { ... }
It would be quite wrong for the a captured by the anonymous function
expression to be other than the a binding declared and initialized
immediately to the left. It would be bad for the eta conversion to break
equivalence (use a block-lambda instead of a function expression for
full TCP).
> From a user perspective, the main advantage I see for capture first
> iteration is that it has a slightly smaller window of wrongness. The
> captures evaluated in the first iteration will reference the
> correction binding, while latter iterations reference the wrong binding.
Users expect and even (now that they know, and Dart raises the ante)
demand that each iteration gets fresh let bindings. Any who do capture
an initial binding in a closure must know, or will learn, that it's just
the first one, which fits the model.
If this were really a footgun (I don't believe it is without actual
evidence from the field) we could try to ban closures capturing the
initial bindings. That ad-hoc restriction would be quite a wart. It
doesn't seem warranted.
> From an implementation perspective, it is probably a bit simpler to
> not have the extra hidden binding for capture.
I don't think so. The unrolling I showed was to use a tail-recursive
block-lambda helper. But real implementations will do closure analysis
and optimization (flat AKA display closures, e.g.) and use branch
instructions for loops, jumps for breaks, etc. Having the first binding
rib open a bit earlier than subsequent ribs is (I think) a small or
zero-cost issue.
> I really don't like the first iteration is different semantics
Different how? Making the first iteration's binding initialization
capture guaranteed errors would be different semantics. Capturing the
first iteration's bindings from closures in their initializers is not
"different" any more than having initializers is "different". The
initialization part of the for loop is already special. It's not like
the update part.
> and think we should think about the above alternative.
Eta equivalence matters. Given that we want n = a.length to use the a
declared to the left in the same for-head declaration, I don't see how
we can make closures in a right-ward initializer capture some outer binding.
Capturing an error-only binding would need evidence of the footgun not
being useful for shooting other things. We don't have such evidence, not
by a long shot.
> However, such closure capture is very rare (could use of block lambda
> based patterns change that??)
I don't think so -- equivalences are stronger, not weaker or different,
with block-lambdas vs. functions, due to TCP.
> so it may come down to judgements about implementation costs. Is
> capture first going to be significantly easier to implement than my
> alternative scoping? The answer is obvious to me.
Did you mean "isn't"?
> In either case an implementation is like to special case loops with
> closure capture in their initializers.
Varying a sketch Jason posted to the SpiderMonkey internals list:
for (let V = INIT; TEST; UPD) STMT
compiles to
enterblock <V>
INIT
goto L2
L1: STMT
reenterblock
UPD
L2: TEST
iftrue L1
leaveblock
This is very close to how SpiderMonkey compiles for(let;;) already --
the only new instruction is reenterblock, which exits the current block
and does the equivalent of enterblock <V> again.
<V> is an immediate operand, in SpiderMonkey a block object literal
created by the compiler. It holds all the bindings and their stack
offsets, however many declarators with or without destructuring occur
after let.
I claim implementation is not the driver here. User expectations, esp.
savvy users who might make some practical or theoretical (testing) use
of eta conversion, matter more.
/be
More information about the es-discuss
mailing list