Re: [PHP-DEV] RFC idea: Block scoped variables with "let $x = expr"

This is only part of a thread. view whole thread
  109033
March 15, 2020 17:50 rowan.collins@gmail.com (Rowan Tommins)
Hi Tyson,

I think this is an interesting idea, but the way you describe it does 
seem to introduce a lot of caveats and additional restrictions.

For instance, this feels very odd to me:

> Correctness would be enforced as a best-effort at compile-time - the variables would continue to only be freed at the end of the function call.
Intuitively, I would expect this: {     let $x = new Foo;     $x->bar(); } somethingElse(); to be equivalent to this: let $x = new Foo; $x->bar(); unset($x); somethingElse(); Could you explain a bit more what the difficulties are in implementing it this way? Regards, -- Rowan Tommins (né Collins) [IMSoP]
  109034
March 15, 2020 18:17 tysonandre775@hotmail.com (tyson andre)
> Intuitively, I would expect this: > > { >      let $x = new Foo; >      $x->bar(); > } > somethingElse(); > > to be equivalent to this: > > let $x = new Foo; > $x->bar(); > unset($x); > somethingElse();
If this feature freed variables when going out of scope, it would be compiled more like the following (if there were multiple lets): JavaScript doesn't have destructors, but PHP does, which makes the implementation a tiny bit more complex. ``` try {     $x = new Foo();     $otherLet = new Other();     $x->bar(); } finally {     // try/finally is useful if unset($otherLet) could throw from __destruct     try {         unset($otherLet);     } finally {         unset($x);     } } somethingElse(); ``` Actually, thinking about this again, performance is hopefully less of a concern. I could safely avoid using `try/finally` if this was outside of a `try` block, as long as this was in a function scope instead of a global scope. (All variable go out of scope for uncaught exceptions) This idea is still a work in progress. It would definitely be useful to free immediately for objects (or arrays with objects) which call `__destruct` when the reference count goes to 0, such as RAII patterns. My concern about adding an extra `unset()` is that it would add some performance overhead, discouraging using this feature, but opcache should be able to optimize the unset out when unnecessary. I touched on the reasons for avoiding `try` blocks earlier, but hopefully they're not needed. The global scope is less likely to be vital to performance in most cases.
> - Freeing it immediately would likely require the equivalent of a try{} finally{} to handle exceptions. >   try statements prevent opcache from optimizing a function, the last time I checked.
See https://github.com/php/php-src/blob/43443857b74503246ee4ca25859b302ed0ebc078/ext/opcache/Optimizer/dfa_pass.c#L42-L49
  109039
March 15, 2020 20:03 rowan.collins@gmail.com (Rowan Tommins)
On 15/03/2020 18:17, tyson andre wrote:
> If this feature freed variables when going out of scope, > it would be compiled more like the following (if there were multiple lets): > > JavaScript doesn't have destructors, but PHP does, which makes the implementation a tiny bit more complex. > > ``` > try { >     $x = new Foo(); >     $otherLet = new Other(); >     $x->bar(); > } finally { >     // try/finally is useful if unset($otherLet) could throw from __destruct >     try { >         unset($otherLet); >     } finally { >         unset($x); >     } > } > somethingElse(); > ```
I'm still not 100% clear why all this would be necessary. Do you know how the equivalent code works with function-scoped variables? As far as I can see, returning from a function will successfully call multiple destructors even if one of them throws an exception. Could exiting block scope use that same algorithm? Having the variable become inaccessible but not actually deallocated seems like it would cause a lot of confusion. For instance: {     let $fh1 = fopen('/tmp/foo', 'wb');     flock($fh1);     fwrite($fh1, 'Hello World');     // no fclose(), but $fh1 has fallen out of scope, which would normally close it } {     let $fh2 = fopen('/tmp/foo', 'wb');     flock($fh2, LOCK_EX); // won't obtain lock, because $fh1 is still open, but no longer accessible } Regards, -- Rowan Tommins (né Collins) [IMSoP]
  109044
March 15, 2020 22:57 tysonandre775@hotmail.com (tyson andre)
Hi Rowan,

> Do you know how the equivalent code works with function-scoped variables? > As far as I can see, returning from a function will successfully call multiple > destructors even if one of them throws an exception. Could exiting block > scope use that same algorithm?
PHP literally frees all of the variables (even the ones which are definitely not reference counted) without stopping if any of them throw an exception until everything is freed. `i_free_compiled_variables` is called when exiting the function scope (e.g. throw/return). (the caller also decreases reference counts to the closure/$this, extra unexpected arguments, etc) ``` static zend_always_inline void i_free_compiled_variables(zend_execute_data *execute_data) /* {{{ */ { zval *cv = EX_VAR_NUM(0); int count = EX(func)->op_array.last_var; while (EXPECTED(count != 0)) { i_zval_ptr_dtor(cv); cv++; count--; } } ```
> Having the variable become inaccessible but not actually deallocated > seems like it would cause a lot of confusion.
Good point about resources, I forgot about freeing being automatic. I'm starting to lean towards deallocating. If this was used within a try block or a top-level statement, this could probably add a new opcode to free a variable that could throw without checking for the exception, and only the last opcode would need to throw the last exception. (adding a new opcode to ensure deallocation of all variables without the equivalent of a `try/finally` block for every additional `let` variable) For global variables, there's already the problem in the engine that `__destruct()` could itself create global variables of the same/different name. - Tyson