On Thu, Jan 25, 2024, at 11:28 AM, Ilija Tovilo wrote:

> This leads to very similar issues as break/continue inside blocks. See:
> https://wiki.php.net/rfc/match_blocks#technical_implications_of_control_statements
>
> I'll try to explain.
>
> The VM works with temporary variables. For the expression foo() +
> bar() two temporary variables for the result of foo() and bar() will
> be created, which are then used for the + operation. Normally, + will
> consume both operands, i.e. use and then free them. However, with
> break/continue etc. being expressions, it would become possible to
> skip over the consuming instructions.
>
>     do {
>         echo foo() + break;
>     } while (true);
>     echo 'Done';
>
> Pseudo opcodes:
>
>     0000 V1 = CALL foo
>     0001 JMP 0005
>     0002 V2 = ADD V1 false ; false is here represents a bottom value
> that will never actually be used
>     0003 ECHO V2
>     0004 JMP 0000
>     0005 ECHO 'Done'
>
> Since JMP will skip over the ADD instruction, V1 remains unused. A
> similar problem already exists for break/continue in foreach itself.
>
>     foreach ($foos as $foo) {
>        foreach ($bars as $bar) {
>            break 2;
>        }
>     }
>
> foreach holds a copy of $bars (in case it gets modified) that normally
> gets cleaned up when the loop ends. With break over multiple
> loop-boundaries, we can completely skip over this freeing mechanism.
> PHP solves this by inserting an explicit FE_FREE instruction before
> the break 2, which itself is essentially just a JMP to the end of the
> outer loop.
>
> Hopefully it's now more evident why this is a problem:
>
>     while (true) {
>        foo() && break;
>     }
>
> foo() returns a value that would normally be consumed by the &&
> operation. However, with break, we may skip over the && operation
> entirely. As such, the break itself becomes responsible for freeing
> these values. This requires significant changes in the compiler to
> track variables that are currently "live" (i.e. haven't been consumed
> yet), and emitting FREE opcodes for them as needed. I've implemented
> this for match blocks here:
>
> https://github.com/php/php-src/compare/master...iluuu1994:php-src:match-blocks-var-tracking
>
> However, note that due to complexity, I've decided to disallow using
> break/continue and the likes in such contexts to avoid this issue
> completely, which isn't possible for what you are suggesting.
>
> There's another related issue.
>
>     foo(bar(), break);
>
> Function calls in PHP consist of multiple instructions, namely an
> INIT_CALL, 0-n SEND and a DO_CALL opcode. INIT_CALL creates a stack
> frame, SEND pushes arguments onto the stack frame, and DO_CALL starts
> the execution of the function and frees both arguments and stack frame
> when the function ends. If prior to a SEND opcode we break, we skip
> over the DO_CALL, so the stack frame needs to be freed manually.
>
> The patch linked above solves this by inserting CLEAN_UNFINISHED_CALLS
> opcodes that do as the name suggests. This mechanism is already used
> for exceptions. This should work for you, but was insufficient for
> match blocks, for reasons I won't get into here.
>
> All this to say: Don't expect the implementation here to be trivial.
>
> Regards,
> Ilija


I'm curious, how did `throw` expressions manage to avoid these issues?  Or was 
it just "Ilija did the hard work of tracking down the weirdness?"

--Larry Garfield

-- 
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: https://www.php.net/unsub.php

Reply via email to