[PHP-DEV] [RFC][Discussion] use construct (Block Scoping)

On 23 November 2025 14:23:40 GMT, "Tim Düsterhus" <tim@bastelstu.be> wrote:
>We have now added a “Design Choices” section to the RFC explaining why we opted for “declarations need to be at the start of the scope”:
>
>PHP: rfc:optin_block_scoping

Hi Tim,

Thanks for the updates.

I think the reason comparing to other languages is so important relates to what Steve Klabnik calls "the Language Strangeness Budget": The language strangeness budget

Anything that is going to surprise users coming from other languages carries a cost which we need to justify. Generally, that means either a) the expected way wouldn't work in PHP for some reason; or b) we think we can do better by learning from the problems of other languages.

In basically every language derived from ALGOL and not from Pascal, variable declarations take the form of a statement inside a block, so doing something different definitely costs us some Strangeness Budget.

You've tried to make the case that PHP has unique challenges with variable declarations, but I'm not convinced.

In particular, I don't think this statement is accurate:

> Other languages with block scoping, particularly statically typed languages, avoid this ambiguity by requiring all variables to be explicitly declared

In any language with ALGOL-style block scoping, you can write the equivalent of this code:

some_code_here(); // A
{
my_var = something(); // B
let my_var; // C
}

Every such language has to answer the same question: does "my_var" at line B refer to the block-scoped variable declared below it at line C?

If the answer is "yes", then any code outside the block, at section A, is irrelevant. Either the declaration is "hoisted" as though lines B and C were in the opposite order; or an error is raised because the variable is accessed (B) before declaration (C).

If the answer is "no", then line B does whatever it would have done if line C didn't exist. If it's mandatory to declare "my_var" in some surrounding scope, an error will be raised if it wasn't; if the variable can instead be implicitly created in some surrounding scope, it will be.

The only way to *avoid* the ambiguity is to forbid all statements between the start of the scope and a declaration - that is, raise an error even if line B doesn't reference "my_var". Notably, if you keep the ALGOL-style declarations, you can start with this rule, and then relax it later, as happened with C99.

So if it's not because we can't implement ALGOL-style declarations, is there something we think we can do better than them?

As far as I can see, any proposed statement of the form "let($foo) { ... }" is directly equivalent to ALGOL-style "{ let $foo; ... }"

The unique innovation appears to be when using it with a single statement rather than a block, such as in this example from the RFC:

let ($user = $repository->find(1)) if ($user !== null) { ... }

With ALGOL-style declarations, that requires an extra pair of braces:

{ let $user = $repository->find(1); if ($user !== null) { ... } }

Since an if statement can also take a single statement, you can also write this:

if ( $repository !== null ) let ( $user = $repository->find(1) ) { ... }

Which is equivalent to this in ALGOL-style:

if ( $repository !== null ) { let $user = $repository->find(1); ... }

This reversibility would also be there with other block types: "let($foo=something()) while($bar) { ... }" is subtly different from "while($bar) let($foo=something() { ... }", and so on.

It's an interesting feature, but whether it's worth the cost in "strangeness", I'm not sure.

The specific case of foreach-by-reference is a strong one, but as mentioned before, I think it would be better served by a specific syntax like "foreach ( $foo as let &$bar )", which avoids the repetition of "let($bar) foreach ( $foo as &$bar )".

On the other hand, I note that the "process_file" example in the RFC can't make use of the single-statement form: "let ( $lock = $file->lock(LockType::Shared) ) try { ... }" would be legal, but wouldn't release the lock until after the catch block.

In the example given, that's an exit point of the function anyway (when the new Exception is thrown), so a function-scoped variable would be cleaned up at the same time as a block-scoped one.

If there is no code after "// The file lock is released here ...", then the ALGOL style saves a pair of braces:

try {
let $file = File\open_read_only($path),
$lock = $file->lock(LockType::Shared);
$content = $file->readAll();
} catch ...

If there is, it looks very similar:

try {
{
let $file = File\open_read_only($path),
$lock = $file->lock(LockType::Shared);
$content = $file->readAll();
}
more_code_here();
} catch ...

Over all, I'm still not sold on having a special new block for this, rather than just using "let" for optional declarations, as in JavaScript.

--
Rowan Tommins
[IMSoP]

On 11/29/25 13:12, Rowan Tommins [IMSoP] wrote:

I think the reason comparing to other languages is so important
relates to what Steve Klabnik calls "the Language Strangeness
Budget": https:// steveklabnik.com/writing/the-language-strangeness-
budget/

Anything that is going to surprise users coming from other
languages carries a cost which we need to justify. Generally, that
means either a) the expected way wouldn't work in PHP for some
reason; or b) we think we can do better by learning from the
problems of other languages.

Oooh! This is very cool!

This sounds a lot like "Jakob's Law of Internet User Experience,"
which states:

Users spend most of their time on other sites. This means that users
prefer your site to work the same way as all the other sites they
already know.[^1]

I've given a talk where I argue this principle carries over to
developer experience, as well. That is, developers prefer their tools
(and languages) to work the same way as other tools they already know.

Cheers,
Ben

[^1]: This is from the article "End of Web Design," published in 2000 by
Jakob Nielsen. https://www.nngroup.com/articles/end-of-web-design/

Hi

Am 2025-11-29 20:12, schrieb Rowan Tommins [IMSoP]:

I think the reason comparing to other languages is so important relates to what Steve Klabnik calls "the Language Strangeness Budget": The language strangeness budget

Anything that is going to surprise users coming from other languages carries a cost which we need to justify. Generally, that means either a) the expected way wouldn't work in PHP for some reason; or b) we think we can do better by learning from the problems of other languages.

In basically every language derived from ALGOL and not from Pascal, variable declarations take the form of a statement inside a block, so doing something different definitely costs us some Strangeness Budget.

I generally agree that it is important to not needlessly invent new stuff folks need to learn and I'm trying hard in the design of my RFCs and the discussion of other RFCs to figure out how to simplify things or make them compose better. Syntax that is unfamiliar to users coming from other languages is bad, but that shouldn't come at the expense of users that are already familiar with PHP or the internal consistency of PHP.

I'd also argue that the proposed `let()` block syntax is intuitive to understand when seeing it when familiar with block scoping, so it has only a small impact on the strangeness cost.

FWIW: The proposed `let()` block is not too dissimilar from the `let <var> = <expr> in <expr>` syntax available in ML-style languages such as OCaml or Haskell and also Nix.

You've tried to make the case that PHP has unique challenges with variable declarations, but I'm not convinced.

In particular, I don't think this statement is accurate:

Other languages with block scoping, particularly statically typed languages, avoid this ambiguity by requiring all variables to be explicitly declared

[…]

Every such language has to answer the same question: does "my_var" at line B refer to the block-scoped variable declared below it at line C?

That sentence you quoted was specifically in the context of the initial paragraph of that section, contrasting PHP - where block scoping is expected to be used comparatively sparingly - against languages where variable declarations are a more “bread and butter” part of the development process, because formally / explicitly declaring variables is a necessity for one reason or another.

The only way to *avoid* the ambiguity is to forbid all statements between the start of the scope and a declaration - that is, raise an error even if line B doesn't reference "my_var". Notably, if you keep the ALGOL-style declarations, you can start with this rule, and then relax it later, as happened with C99.

So if it's not because we can't implement ALGOL-style declarations, is there something we think we can do better than them?

I feel that the C99 requirements and syntax would still have more ambiguity compared to the proposed `let()` syntax in cases like this:

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

         let $baz = 1;
     }

because there is a much less direct / less rigid relationship between the individual `let` statements, leaving room for interpretation of “what is considered a statement”. As an example, is a goto jump label a statement?

     {
         let $foo = 1;
  label:
         let $bar = $foo++;
         goto label;
     }

Forcing all the declarations into a single statement would resolve that ambiguity, but I feel like that those restriction would feel arbitrary and have a strangeness cost without any of associated benefits that the `let()` block has.

As far as I can see, any proposed statement of the form "let($foo) { ... }" is directly equivalent to ALGOL-style "{ let $foo; ... }"

Yes, that is my understanding.

The unique innovation appears to be when using it with a single statement rather than a block, such as in this example from the RFC:

let ($user = $repository->find(1)) if ($user !== null) { ... }

With ALGOL-style declarations, that requires an extra pair of braces:

{ let $user = $repository->find(1); if ($user !== null) { ... } }

[…]

It's an interesting feature, but whether it's worth the cost in "strangeness", I'm not sure.

Being able to declare variables with “if” lifetime that I can also check is a big part of the benefits of the proposed syntax and something I'm missing in other languages. C++ as a language in the “PHP syntax family” added it in C++17 with the following syntax (taken from if statement - cppreference.com):

     if (char buf[10]; std::fgets(buf, 10, stdin))
     if (std::lock_guard lock(mx); shared_flag)

Translated to PHP this would be:

     if (let $user = $repository->find(1); $user !== null) { }

which would somewhat match the syntax of a `for()` loop with the semicolon. But then the more composable

     let ($user = $repository->find(1)) if ($user !== null) { }

would not be so different syntax-wise and would not require adding the grammar to each and every control structure.

On the other hand, I note that the "process_file" example in the RFC can't make use of the single-statement form: "let ( $lock = $file->lock(LockType::Shared) ) try { ... }" would be legal, but wouldn't release the lock until after the catch block.

As discussed in the sibling thread, allowing a single statement on `try` should be possible (if necessary with a special case for `let`): php.internals: Re: Examples comparing Block Scoped RAII and Context Managers

Best regards
Tim Düsterhus

Hi

I just became aware of a mistake in the desugaring at the start of the proposal section:

The value of `$c` was not correctly kept after breaking the reference with `unset($c)`. This was fixed by adding a `$c = $c_original;`. The "Simple example" in the Examples section already correctly represented the intent with the `$b` variable, where it still is `"original b"` within the `let()` block.

I have also clarified in the desugaring that when the original values are restored after the block that the restored values will become a reference again if it originally was a reference. As far as I am aware this is not representable purely with PHP code, so it's just noted in the comment.

Diff is at: PHP: rfc:optin_block_scoping

Best regards
Tim Düsterhus

On 10/12/2025 16:23, Tim Düsterhus wrote:

That sentence you quoted was specifically in the context of the initial paragraph of that section, contrasting PHP - where block scoping is expected to be used comparatively sparingly - against languages where variable declarations are a more “bread and butter” part of the development process, because formally / explicitly declaring variables is a necessity for one reason or another.

I don't think that changes anything I said in my previous reply: as soon as you declare a variable half-way through a block, there is an ambiguity about its range of visibility. Having more variable declarations makes that *more* likely to come up, not *less*, so I'm not sure why you think it "avoids" the problem.

There's also an assumption that if PHP added block scoping, it would only rarely be used. We have no way to know, but I'm not sure that's true. I can easily imagine code styles adding a rule that all local variables be declared at an appropriate level. I can also imagine new users coming from other languages - particularly JS - adding "let" out of habit, even if seasoned PHP coders wouldn't.

I feel that the C99 requirements and syntax would still have more ambiguity compared to the proposed `let()` syntax in cases like this:

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

    let $baz = 1;
\}

Probably the simplest solution is to re-use our existing definition of "constant expression". In fact, we already have variable declarations using that rule:

function foo() {
static $a = 1; // OK
static $b = $a; // Fatal error: Constant expression contains invalid operations
}

As an example, is a goto jump label a statement?

\{
    let $foo = 1;

label:
let $bar = $foo++;
goto label;
}

PHP already limits where "goto" can jump to; I don't know how that's implemented, but I don't think we need to get into philosophical definitions to say "you can't jump into the middle of a declaration list".

Or, we could just bite the bullet and answer the "which way does it resolve" question, as loads of other languages have already done.

Being able to declare variables with “if” lifetime that I can also check is a big part of the benefits of the proposed syntax and something I'm missing in other languages.

if \(let $user = $repository\-&gt;find\(1\); $user \!== null\) \{ \}

I find this more readable than the proposed version:

let \($user = $repository\-&gt;find\(1\)\) if \($user \!== null\) \{ \}

Skimming down a piece of code, I can spot where code is being run conditionally without reading the condition itself:

if ............ {

With the proposed syntax, that first glance is:

let ........... {

On closer inspection, it's actually:

let ..... if ..... {

Maybe it's also because I've dabbled in Perl, which has post-fix conditions, so a very similar line would have a very different meaning:

my $foo=do_bar() if ($baz != 0);

is equivalent to:

my $foo;
if ($baz != 0) { $foo=do_bar(); }

Which is also a word order we can use in English, e.g. "hang the wet clothes inside if it is raining".

In terms of making it less of a special case, some languages have a "," operator which lets you glue any two expressions together and get the right-hand result.

In Perl, you can write this:

my $a = 'outer', $b = 'whatever';
if ( my $a='inner', $b == 'whatever' ) {
     say $a; // 'inner'
}
say $a; // 'outer'

This gives the desired scope for $a, but the if statement is still just accepting a single expression.

JavaScript has the same operator, but apparently doesn't allow "let" in an expression, so you can write:

if ( a="inner", b=="whatever" ) { }

but can't use it to declare a local version of "a".

I haven't thought through exactly how to apply that to PHP, but it might give us an option for "both and": a concise and reusable syntax for the if use case, and a separate syntax for cases like the closure example I gave earlier: [RFC][Discussion] use construct (Block Scoping) - Externals

--
Rowan Tommins
[IMSoP]

On 2025-12-12 11:21, Rowan Tommins [IMSoP] wrote:

function foo() {
static $a = 1; // OK
static $b = $a; // Fatal error: Constant expression contains invalid operations
}

Though this example is legal since 8.3.

On 11 December 2025 23:37:06 GMT, Morgan <weedpacket@varteg.nz> wrote:

On 2025-12-12 11:21, Rowan Tommins [IMSoP] wrote:

function foo() {
static $a = 1; // OK
static $b = $a; // Fatal error: Constant expression contains invalid operations
}

Though this example is legal since 8.3.

Oh, I must have had the wrong version selected on 3v4l when I tested it. Thanks for pointing that out.

I guess the point stands that we *could* use it, if we really wanted to avoid deciding how ambiguous scope worked.

Rowan Tommins
[IMSoP]

On Thu, Dec 11, 2025, at 4:21 PM, Rowan Tommins [IMSoP] wrote:

Being able to declare variables with “if” lifetime that I can also
check is a big part of the benefits of the proposed syntax and
something I'm missing in other languages.

if \(let $user = $repository\-&gt;find\(1\); $user \!== null\) \{ \}

I find this more readable than the proposed version:

let \($user = $repository\-&gt;find\(1\)\) if \($user \!== null\) \{ \}

Skimming down a piece of code, I can spot where code is being run
conditionally without reading the condition itself:

if ............ {

With the proposed syntax, that first glance is:

let ........... {

On closer inspection, it's actually:

let ..... if ..... {

Maybe it's also because I've dabbled in Perl, which has post-fix
conditions, so a very similar line would have a very different meaning:

my $foo=do_bar() if ($baz != 0);

is equivalent to:

my $foo;
if ($baz != 0) { $foo=do_bar(); }

Which is also a word order we can use in English, e.g. "hang the wet
clothes inside if it is raining".

In terms of making it less of a special case, some languages have a ","
operator which lets you glue any two expressions together and get the
right-hand result.

In Perl, you can write this:

my $a = 'outer', $b = 'whatever';
if ( my $a='inner', $b == 'whatever' ) {
     say $a; // 'inner'
}
say $a; // 'outer'

This gives the desired scope for $a, but the if statement is still just
accepting a single expression.

JavaScript has the same operator, but apparently doesn't allow "let" in
an expression, so you can write:

if ( a="inner", b=="whatever" ) { }

but can't use it to declare a local version of "a".

I haven't thought through exactly how to apply that to PHP, but it might
give us an option for "both and": a concise and reusable syntax for the
if use case, and a separate syntax for cases like the closure example I
gave earlier: [RFC][Discussion] use construct (Block Scoping) - Externals

The more I think on this, the more I think that the auto-unsetting behavior of a `let` would be useful *only* in combination with some other existing block, not as its own block.

if (let $x=stuff(); $x < 1) { ... }

for (let $i = 0; $i < 10; $i++) { ... }

foreach (let $arr as $k =>v) { ... } (applies to both $k and $v)

And so on. (I'm not sure if it makes sense on a while? Possibly.) Exact syntax above is just spitballing.

But that would allow for the mask/unset logic for variables that have special meaning in existing block constructs, which is generally what you'd be interested in. I don't think there's a huge use case for unsetting arbitrary variables in arbitrary places. It would also be cleaner than the current pattern of if ($x = stuff() && $x < 1) {}, which always felt clunky and "leaks" $x.

If you need some thing more arbitrary and custom than cleaning up an existing block construct, then the additional setup of a Context Manager is fully justified, and more robust.

--Larry Garfield

Hello,

On Sat, Dec 13, 2025 at 6:10 AM Larry Garfield <larry@garfieldtech.com> wrote:

The more I think on this, the more I think that the auto-unsetting behavior of a `let` would be useful *only* in combination with some other existing block, not as its own block.

if (let $x=stuff(); $x < 1) { ... }

for (let $i = 0; $i < 10; $i++) { ... }

foreach (let $arr as $k =>v) { ... } (applies to both $k and $v)

And so on. (I'm not sure if it makes sense on a while? Possibly.) Exact syntax above is just spitballing.

I remain skeptical about adding block scoping to PHP at all, but if we
do pursue this, I have concerns about the approach. I have been
working on some tools I need and spent quite some time on php scoping
at large, and in detail, that's mainly the reason I am jumping in for
a change.

My primary concern with the suggestions of integrating let into
existing control structures (if (let $x=stuff(); ...), for (let $i =
0; ...)) is ambiguity. The same for sequence-like syntax proposed
earlier. PHP doesn't currently have block scoping, and introducing it
in a way that reuses or resembles existing syntax patterns will be
difficult to distinguish from current behavior. This could create
hidden bugs that could be hard to catch. Without talking about
changing let to "semi" reserved word. Not sure what that means tho'.
:slight_smile:

The 'using' syntax proposed earlier in this thread, mapping maybe the
'with' behavior may also provide such an unambiguous definition.

If we add block scoping, it needs to be extremely explicit and
unambiguous. The RFC's current syntax does achieve this. Alternative
syntaxes like using (potentially mapping to with behavior) might also
provide that clarity, whereas let integrated into control structures
may not.

I am also wondering about the actual need of having a block only
scope, and potentially for some variables only (if that's part of the
behaviors as well). When a scope is needed but one does not want to
define a separate function, closure and co are cheap now. But I may
miss some obvious benefits that could benefit from such additions.
That would be a great addition to the RFC, actual use cases and
benefits over alternative existing ways to do it. Almost all other
languages provide block scoping, except bash (nice friend here ;), but
it does not mean we have to go that way. If we do, I emphasize again,
it needs to be extremely explicit.

PS: If we were back to php2/3, heh, even 4, I would propose to simply
introduce block scoping in full, that would have been a more standard
approach. But time flies, and we are at php 8 and it is tricky to
introduce this at this stage :slight_smile:

In short, If we do proceed, explicitness must be non-negotiable :slight_smile:

best,
--
Pierre

@pierrejoye | http://www.libgd.org

Hi

Am 2025-12-13 04:18, schrieb Pierre Joye:

My primary concern with the suggestions of integrating let into
existing control structures (if (let $x=stuff(); ...), for (let $i =
0; ...)) is ambiguity. The same for sequence-like syntax proposed
earlier. PHP doesn't currently have block scoping, and introducing it
in a way that reuses or resembles existing syntax patterns will be
difficult to distinguish from current behavior. This could create

Yes, this is a big part of the reason we've chosen the current syntax with its dedicated construct that also forces all block-scoped variables to be defined together at the start of their respective scope.

hidden bugs that could be hard to catch. Without talking about
changing let to "semi" reserved word. Not sure what that means tho'.
:slight_smile:

`let()` being a semi-reserved keyword means that it will become unavailable for use as a free-standing function, since `let($foo = 1);` would be ambiguous otherwise, but it will remain available as a method name, since the `->` makes it clear that it must be a method call.

The 'using' syntax proposed earlier in this thread, mapping maybe the
'with' behavior may also provide such an unambiguous definition.

Sorry, I'm afraid I don't understand what you mean by “mapping the 'with' behavior”.

I am also wondering about the actual need of having a block only
scope, and potentially for some variables only (if that's part of the
behaviors as well). When a scope is needed but one does not want to
define a separate function, closure and co are cheap now. But I may
miss some obvious benefits that could benefit from such additions.

Perhaps the new “Example showing memory-efficient batch processing” example (added yesterday) is making a good case? The alternatives there would be `unset()` or a function/Closure which then come with the boilerplate of passing along all variables that are required for the scaling logic to the function scope. I find having the logic inline to be pretty natural in that example.

That would be a great addition to the RFC, actual use cases and
benefits over alternative existing ways to do it. Almost all other
languages provide block scoping, except bash (nice friend here ;), but
it does not mean we have to go that way. If we do, I emphasize again,
it needs to be extremely explicit.

The “Examples” section is starting with examples that show the feature in isolation to make the semantics clear, but the later examples are intended to be representative of real-world use-cases where we would've liked to have block scoping in the past. Do you wish to see more explicit comparisons there? Perhaps you have another use case to add?

PS: If we were back to php2/3, heh, even 4, I would propose to simply
introduce block scoping in full, that would have been a more standard
approach. But time flies, and we are at php 8 and it is tricky to
introduce this at this stage :slight_smile:

Yes, that is also what the RFC tried to say in the “Design Choices” section (and also in my replies to Rowan). We are adding block scoping to a language after the fact and thus different design considerations apply compared to a language where block scoping is an integral part of the language semantics.

Best regards
Tim Düsterhus

Hi

Am 2025-12-12 22:54, schrieb Larry Garfield:

The more I think on this, the more I think that the auto-unsetting behavior of a `let` would be useful *only* in combination with some other existing block, not as its own block.

We very strongly disagree on this. Arbitrary block scoped variables outside of control structures have proven their value in other programming languages and the same use cases also apply to the use in PHP.

if (let $x=stuff(); $x < 1) { ... }

for (let $i = 0; $i < 10; $i++) { ... }

foreach (let $arr as $k =>v) { ... } (applies to both $k and $v)

And so on. (I'm not sure if it makes sense on a while? Possibly.) Exact syntax above is just spitballing.

It does make sense on a while:

     let ($row) while ($row = $statement->fetch()) {
         // …
     }

But that would allow for the mask/unset logic for variables that have special meaning in existing block constructs, which is generally what you'd be interested in. I don't think there's a huge use case for unsetting arbitrary variables in arbitrary places. It would also be cleaner than the current pattern of if ($x = stuff() && $x < 1) {}, which always felt clunky and "leaks" $x.

If you need some thing more arbitrary and custom than cleaning up an existing block construct, then the additional setup of a Context Manager is fully justified, and more robust.

We have added a new example use-case “Example showing memory-efficient batch processing” to the RFC that shows the value of stand-alone block scoping for a case where the goal is the unsetting and freeing of memory and not the side-effect of calling `__destruct()`. Somehow attempting to merge the block declaration of `$scaled` into the `foreach()` probably not going to be particularly readable. Limiting block scoping to control structure initializers blocks some use-cases and does not provide a meaningful (syntactical) value-add over the dedicated construct that composes with the existing control structures.

Best regards
Tim Düsterhus

Hi

Am 2025-12-11 23:21, schrieb Rowan Tommins [IMSoP]:

That sentence you quoted was specifically in the context of the initial paragraph of that section, contrasting PHP - where block scoping is expected to be used comparatively sparingly - against languages where variable declarations are a more “bread and butter” part of the development process, because formally / explicitly declaring variables is a necessity for one reason or another.

I don't think that changes anything I said in my previous reply: as soon as you declare a variable half-way through a block, there is an ambiguity about its range of visibility. Having more variable declarations makes that *more* likely to come up, not *less*, so I'm not sure why you think it "avoids" the problem.

The difference I'm seeing is that for languages where variable declarations (and block scoping) are a core part of the language, the scoping rules are “moulding” (if that word makes sense here) how code in that language is written and how folks reason about the code. This is different for a language where block scoping is added after-the-fact and remains an optional part of the language.

There's also an assumption that if PHP added block scoping, it would only rarely be used. We have no way to know, but I'm not sure that's true. I can easily imagine code styles adding a rule that all local variables be declared at an appropriate level. I can also imagine new users coming from other languages - particularly JS - adding "let" out of habit, even if seasoned PHP coders wouldn't.

From my experience, a majority of functions in modern code bases are reasonably short and single-purpose where intermediate variables are meant to live for the remainder of the function scope. And of course with additions such as the pipe operator, the number of temporaries will likely also go down further. From my own PHP code, I would guess block scoping to be useful for less than 10% of functions. For the ones where it would be useful, it would be very useful, though, since those are the functions that are on the more complex end of things.

I feel that the C99 requirements and syntax would still have more ambiguity compared to the proposed `let()` syntax in cases like this:

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

    let $baz = 1;
\}

Probably the simplest solution is to re-use our existing definition of "constant expression". In fact, we already have variable declarations using that rule:

function foo() {
static $a = 1; // OK
static $b = $a; // Fatal error: Constant expression contains invalid operations
}

Morgan already correctly noted that `static` supports arbitrary expressions nowadays. I would like to add that supporting arbitrary expressions within the initializer is also something we expect from block scoping to avoid boilerplate, since most if we don't store a dynamically computed value in a variable, we might as well use a constant or hardcode the value.ö

As an example, is a goto jump label a statement?

\{
    let $foo = 1;

label:
let $bar = $foo++;
goto label;
}

PHP already limits where "goto" can jump to; I don't know how that's implemented, but I don't think we need to get into philosophical definitions to say "you can't jump into the middle of a declaration list".

Another, perhaps better, example that is not handled well by any C-derived language that we are aware of is block scoping in combination with `switch()`:

     switch ($var) {
         let $tmp;
     case "foo":
         let $tmp2;
         break;
     case "bar":
     case "baz":
         let $tmp2;
         let $tmp3;
         break;
     }

Which of the `$tmp`s is placed at the “start of a block”? What is the end of the block for each of them? Is it legal for `$tmp2` to be declared in two locations?

Or, we could just bite the bullet and answer the "which way does it resolve" question, as loads of other languages have already done.

Other languages have other ecosystems and other user expectations. PHP has extensive “scope introspection” functionality by means of `extract()`, `compact()`, `get_defined_vars()` and variable variables. Folks are used to being able to access arbitrary variables (it's just a Warning, not an Error to access undefined variables) and there's also constructs like `isset()` that can act on plain old local-scope variables. Adding semantics like the “temporal dead zone” from JavaScript that you suggested in the other thread would mean that we would need to have entirely new semantics and interactions with various existing language features that folks already know, adding to the complexity of the language. The RFC, as currently proposed, avoids all that by preserving all the existing semantics about “variable existence” and just adding the “backup and restore old value” semantics that are known from other languages and reasonably intuitive to understand even when not intimately familiar with block scoping.

let \($user = $repository\-&gt;find\(1\)\) if \($user \!== null\) \{ \}

Skimming down a piece of code, I can spot where code is being run conditionally without reading the condition itself:

For me this works, because the `let()` is preparing me that “this code is doing user processing” and the `if()` is just an “implementation detail” / “means to an end” of that. By the block scoping semantics I know that when I read the closing brace, the user processing is finished. The function is a <h1>, the user processing is a <h2> and the `if()` is a <h3> if that analogy makes sense. If I just want to get an overview over the function, I only care about the <h2> headings.

Maybe it's also because I've dabbled in Perl, which has post-fix conditions, so a very similar line would have a very different meaning:

I understand that some languages have postfix conditions, but being able to place an `if()` after another control structure is not a new thing. The same would apply to:

     foreach ($users as $user) if ($user->isAdmin()) {
         echo "User is admin";
     }

which is already valid PHP.

In terms of making it less of a special case, some languages have a "," operator which lets you glue any two expressions together and get the right-hand result.

In Perl, you can write this:

my $a = 'outer', $b = 'whatever';
if ( my $a='inner', $b == 'whatever' ) {
    say $a; // 'inner'
}
say $a; // 'outer'

This gives the desired scope for $a, but the if statement is still just accepting a single expression.

The comma would leave ambiguity in cases like `if (let $repository = $container->getRepository(), $user = $repository->find(1))`. Are both $repository and $user block-scoped or only $repository of them? Assignments are valid expressions in a condition. That's probably why C++ uses the `;` as a delimiter there.

JavaScript has the same operator, but apparently doesn't allow "let" in an expression, so you can write:

if ( a="inner", b=="whatever" ) { }

but can't use it to declare a local version of "a".

I haven't thought through exactly how to apply that to PHP, but it might give us an option for "both and": a concise and reusable syntax for the if use case, and a separate syntax for cases like the closure example I gave earlier: [RFC][Discussion] use construct (Block Scoping) - Externals

Adding “inline” support for other control structures certainly is something that can be done as future scope. But we believe the “top of the block” semantics are important for block scoping to work well in PHP due to its unique semantics and 30y history.

Best regards
Tim Düsterhus

PS: With that both Seifeddine and I are going to be enjoying our end-of-the-year vacations and are expected to be back on the list next year.

Hi

to also put it into the correct thread: As just mentioned in the “Examples comparing Block Scoped RAII and Context Managers” thread I have (tried to) improve the explanation at the start of the “Proposal” section:

No functional changes have been made. Particularly the desugaring already represented the described semantics. They are now just spelled out more explicitly in “English prose”.

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

Best regards
Tim Düsterhus

Hi

On 11/3/25 22:46, Seifeddine Gmati wrote:

Please find the following resources for your reference:

    -

    RFC: PHP: rfc:optin_block_scoping
    -

    POC:
    Comparing php:master...TimWolla:block-scope · php/php-src · GitHub

The actual proposal of this RFC has been unchanged since the middle of November and the last meaningful change to the RFC text was made in the middle of December to improve the explanation.

While the discussion in the related “comparison thread” by Rowan still has been active before the holiday break (with one reply of mine after the break), it was only discussing fundamental syntax (and semantic) choices, which we do not plan to change at this point. Both because Seifeddine and I are confident in the design of the RFC and because changing them would be equivalent to a full rewrite of the RFC (which would be more appropriate as a new v2 RFC).

Even though we ultimately disagreed, we would like to thank Rowan in particular for the insightful discussion that provided valuable help to further refine and clarify the RFC text, hopefully helping folks make an informed decision.

We therefore plan to open voting later this week, unless someone speaks up pointing out a major oversight or any other meaningful concerns we all missed.

Best regards
Tim Düsterhus

On 17/12/2025 19:09, Tim Düsterhus wrote:

The difference I'm seeing is that for languages where variable declarations (and block scoping) are a core part of the language, the scoping rules are “moulding” (if that word makes sense here) how code in that language is written and how folks reason about the code. This is different for a language where block scoping is added after-the-fact and remains an optional part of the language.

I can sort of see where you're coming from, but counter-examples include Perl and JS, both of which treat declarations as optional, and innovated new variants of them, but stuck to the "ALGOL-style" syntax.

I feel that the C99 requirements and syntax would still have more ambiguity compared to the proposed `let()` syntax in cases like this:

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

    let $baz = 1;
\}

As discussed on the other thread, this part of the discussion turns out be moot, because exactly the same ambiguity exists in the proposed syntax:

let($foo = bar($baz), $baz=1) { ... }

The syntax alone doesn't tell you what that will do, only knowing the choices made in the RFC.

PHP already limits where "goto" can jump to; I don't know how that's implemented, but I don't think we need to get into philosophical definitions to say "you can't jump into the middle of a declaration list".

Another, perhaps better, example that is not handled well by any C-derived language that we are aware of is block scoping in combination with `switch()`

Compile Error: 'let' declarations whose closest block is a 'switch' are forbidden.

Problem solved.

Other languages have other ecosystems and other user expectations. PHP has extensive “scope introspection” functionality by means of `extract()`, `compact()`, `get_defined_vars()` and variable variables. Folks are used to being able to access arbitrary variables (it's just a Warning, not an Error to access undefined variables) and there's also constructs like `isset()` that can act on plain old local-scope variables. Adding semantics like the “temporal dead zone” from JavaScript that you suggested in the other thread would mean that we would need to have entirely new semantics and interactions with various existing language features that folks already know, adding to the complexity of the language.

I don't think most of these would need special semantics at all. If it's an error to read from $foo, it follows that it's an error to read from $$x when $x is 'foo', and an error to run compact('foo').

It seems equally obvious to me that get_defined_vars() would omit the variable, and isset() would return false.

There might be some nuances to the implementation, but I imagine it would be similar to uninitialized object properties: a sentinel value in the zval, and checks for that sentinel in suitable places.

I don't know JS well enough to name them, but I bet the ECMA committee had to consider similar features, and most users simply never encounter them.

For me this works, because the `let()` is preparing me that “this code is doing user processing” and the `if()` is just an “implementation detail” / “means to an end” of that. By the block scoping semantics I know that when I read the closing brace, the user processing is finished. The function is a <h1>, the user processing is a <h2> and the `if()` is a <h3> if that analogy makes sense. If I just want to get an overview over the function, I only care about the <h2> headings.

I can't think of any situation where "this block of code contains a scoped variable" is more important information than "this block of code might not run at all".

In the analogy, I would always class an if() as an h2; it's one of the most fundamental pieces of control flow.

I understand that some languages have postfix conditions, but being able to place an `if()` after another control structure is not a new thing. The same would apply to:

foreach \($users as $user\) if \($user\-&gt;isAdmin\(\)\) \{
    echo &quot;User is admin&quot;;
\}

which is already valid PHP.

That syntax would never occur to me, and if I saw it, I'd immediately add the "missing" braces.

The comma would leave ambiguity in cases like `if (let $repository = $container->getRepository(), $user = $repository->find(1))`.

I think you missed the context: in Perl, the comma isn't part of the "if" or "let", it's just a general purpose operator, so this ambiguity is just the precedence of "," vs "my".

That's probably why C++ uses the `;` as a delimiter there.

But sure, that works too. I was just exploring possibilities.

I think, in a nutshell, this is where we have opposing opinions:

we believe the “top of the block” semantics are important for block scoping to work well in PHP due to its unique semantics and 30y history.

I believe using familiar declaration syntax is important for block scoping to work well in PHP due to its strong similarity to other languages in the ALGOL-C-Java family, and the 65 year history of that family.

--
Rowan Tommins
[IMSoP]

Hi

Am 2026-01-20 23:26, schrieb Rowan Tommins [IMSoP]:

As discussed on the other thread, this part of the discussion turns out be moot, because exactly the same ambiguity exists in the proposed syntax:

let($foo = bar($baz), $baz=1) { ... }

The syntax alone doesn't tell you what that will do, only knowing the choices made in the RFC.

Yes, syntax alone does not give you any guarantees. But it provides an indication of which possible choice is more likely to be made. That's also why I tried to emphasize and explain how the different syntax works with my intuition (e.g. by using “suggests” or “strongly suggests” as the phrasing). You may disagree of course. Different brains work differently and different experiences with different programming languages probably also shape how one reasons about the code.

Other languages have other ecosystems and other user expectations. PHP has extensive “scope introspection” functionality by means of `extract()`, `compact()`, `get_defined_vars()` and variable variables. Folks are used to being able to access arbitrary variables (it's just a Warning, not an Error to access undefined variables) and there's also constructs like `isset()` that can act on plain old local-scope variables. Adding semantics like the “temporal dead zone” from JavaScript that you suggested in the other thread would mean that we would need to have entirely new semantics and interactions with various existing language features that folks already know, adding to the complexity of the language.

I don't think most of these would need special semantics at all. If it's an error to read from $foo, it follows that it's an error to read from $$x when $x is 'foo', and an error to run compact('foo').

I could've probably phrases it more clearly: I don't think the existence of a temporal dead zone is a good idea for PHP. I find it especially unexpected to be disallowed to write to a variable (i.e. more extract() rather than compact()).

For me this works, because the `let()` is preparing me that “this code is doing user processing” and the `if()` is just an “implementation detail” / “means to an end” of that. By the block scoping semantics I know that when I read the closing brace, the user processing is finished. The function is a <h1>, the user processing is a <h2> and the `if()` is a <h3> if that analogy makes sense. If I just want to get an overview over the function, I only care about the <h2> headings.

I can't think of any situation where "this block of code contains a scoped variable" is more important information than "this block of code might not run at all".

Note that the quoted wording said “this code deals with user processing” not “this block contains a scoped variable”. The variable *name* is important information there and knowing that this variable name will not be used after the block is important to me, because then I know that user processing is finished.

In the analogy, I would always class an if() as an h2; it's one of the most fundamental pieces of control flow.

How the user is being processed (e.g. conditionally) is then the h3.

I think, in a nutshell, this is where we have opposing opinions:

It looks like that. And IMO it is totally fine. If we always were in agreement, we wouldn't need RFCs, Discussions and Votes and would just commit to master :slight_smile: As I mentioned in the “Intent to Vote” email, we nevertheless found this discussion very valuable and are thankful you took the time to engage.

Best regards
Tim Düsterhus