Hi

On 12/18/25 00:04, Rowan Tommins [IMSoP] wrote:
5) Wait, does that mean this is just a sequence of declaration statements in 
disguise?

Yes.

    let ($foo, $bar) { … }

is equivalent to

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

And by extension

    let ($scoped, $scoped) { … }

is equivalent to

    let ($scoped) {
        let ($scoped) { … }
    }

In the example the outer `$scoped` will then effectively be shadowed by the inner `$scoped`, preventing it from being overwritten inside the block.

The initializer behaves like regular assignments that PHP users are already familiar with, just with the extra feature that the old value will be backed up and then restored after the associated statement (list) finishes.

The (effective) desugaring is showcased in the Proposal section of the RFC and the first example in the “Examples” section also showcase all possible situations.

I have just updated the RFC to write this out more explicitly:

https://wiki.php.net/rfc/optin_block_scoping?do=diff&rev2%5B0%5D=1765892090&rev2%5B1%5D=1766015568&difftype=sidebyside

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.

Thinking about it, even the dynamic coding features of PHP you say would be so 
difficult aren't automatically prohibited:

I assume you are referring to this email here: https://news-web.php.net/php.internals/129641? I was specifically mentioning the dynamic coding features as problematic in combination with a possible “temporal dead zone”.

Since the `let()` construct requires all variables to be declared at the start of the block in a dedicated section there is no (or less) issue of there being multiple equally-valid interpretations for the behavior of variables that are declared “halfway through” a block:

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

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`.

To me the syntax of the `let()` construct very strongly suggests (3) and when there is only one variable declared (or one knows the desugaring of let($foo, bar) == let($foo) let($bar)) there is no other possible interpretation.

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.

Best regards
Tim Düsterhus

Reply via email to