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