On 18/12/2025 00:17, Tim Düsterhus wrote:
Yes.

    let ($foo, $bar) { … }

is equivalent to

    let ($foo) {
        let($bar) { … }
    }


Yeah, I guess I didn't realise the implications of that equivalence.

From the syntax alone, I vaguely assumed that the two declarations happened "simultaneously", and didn't think very hard what that meant.


If you can do that, presumably you can do this:

let(
      $foo = bar($baz), // What is $baz referring to? Particularly if it is a by-reference out parameter.
      $baz = 1,
  )

Which is a direct translation of an example you gave here: <https://externals.io/message/129059#129583>

The `$baz` in `bar($baz)` is referring to whatever value `$baz` has at that point in time.


As written, that sentence doesn't really say anything; but from the context of the discussion, I get what you're trying to say.

The point though is that any answer you give is just a design decision you've made; and the exact same decision could be made with more traditional syntax, and apply to the original example.


1. The “temporal dead zone” is not something that can exist.
2. And users do not need to wonder if declarations are hoisted.


This is just plain false. The exact same ambiguity exists, you have just chosen how to resolve it.


For the example in the email you linked, I am including it here once more (with an additional $baz = 2 assignment at the start):

    $baz = 2;
    {
         let $foo = bar($baz);

         let $baz = 1;
     }

1. If there is a temporal dead zone, the call `bar($baz)` is invalid (throws an Error). 2. If declarations are hoisted, the call to `bar($baz)` could be (1) an access to an undefined variable (if it behaves as if there was an `unset($baz)`, which would be behavior that is technically different from the TDZ). It could also be a valid access to a variable containing `null` (if all variables are initialized to `null`). *Theoretically* it could also be `1`, if only constant expressions are legal and the initializer is also hoisted. 3. If the lifetime of the block-scoped `$baz` only starts at the point of declaration - effectively an invisible nested block - it behaves as if it was `bar(2)`, since the current value of `$baz` is `2`.


Agreed.

It's probably worth calling out that (1) is effectively a subset of (2) designed to avoid the confusion that full hoisting causes. There's also a variant of (3) where variable shadowing is forbidden, so the "let $baz = 1" would throw an Error.

And all of those options are available to a comma-separated version as well.


To me the syntax of the `let()` construct very strongly suggests (3)


I don't really see why. As I said above, the comma-separated list made me think of "simultaneous" action, which I think would imply (1), because accessing a variable "while it's being declared" would make no sense.

Option 3 is not necessarily a "wrong" choice, but it's a choice you have made, and you could equally use more traditional syntax and make that same choice.


This is what I meant by “there is a less rigid relationship between the individual statements” in the previous email. Note that it also said “Forcing all the declarations into a single statement would resolve that ambiguity […]”, since that would be isomorphic to the `let()` construct if the declaration is forced to be at the top of the block.


I don't think separating the declarations with "," vs ";" makes any difference. The only way to fully avoid the ambiguity is to limit the initialisers to constant expressions. As soon as you allow "let $foo = $bar" and "let $bar", with whatever punctuation you choose, there is a chance for ambiguity about how to resolve "$bar".

The only thing that using commas automatically does is forbids jump targets (goto labels or switch cases) in between the declarations.


On 18/12/2025 00:21, Tim Düsterhus wrote:

And with that I'm *really* going to be off for vacation.


Have a great break :)


--
Rowan Tommins
[IMSoP]

Reply via email to