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->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: https://externals.io/message/129059#129075

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.

Reply via email to