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
_______________________________________________
es-discuss mailing list
[email protected]
https://mail.mozilla.org/listinfo/es-discuss

Reply via email to