Re: Examples comparing Block Scoped RAII and Context Managers

From: Date: Thu, 18 Dec 2025 00:17:55 +0000
Subject: Re: Examples comparing Block Scoped RAII and Context Managers
References: 1 2 3 4 5  Groups: php.internals 
Request: Send a blank email to internals+get-129647@lists.php.net to get a copy of this message
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

« previous php.internals (#129647) next »