Skip to content

[RFC] Make throw statement an expression #5279

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

iluuu1994
Copy link
Member

@iluuu1994 iluuu1994 commented Mar 19, 2020

Implementation for https://wiki.php.net/rfc/throw_expression.

Inspired by this tweet. Of course, this will require an RFC but I wanted to ask for opinions beforehand since this is my first contribution to the php interpreter.

@nikic
Copy link
Member

nikic commented Mar 19, 2020

Looks reasonable at first glance, though exact precedence may need some more thought.

@iluuu1994
Copy link
Member Author

@nikic Thanks for your initial assessment!

I wasn't sure about precedence either. throw in an expression will mostly be useful at the end (?? throw, ?: throw, etc). Since combining it with most other operators is nonsensical (e.g. throw new Exception() + 1) it's hard to decide what precedence is correct.

(throw new Exception()) + 1 is the one that would "work" right now. throw (new Exception() + 1) could make more sense if the RFC userspace operator overloading gets accepted.

Of course, there are many other things to consider.

@iluuu1994 iluuu1994 force-pushed the throw-expression branch 2 times, most recently from f26a4bd to 501a9e6 Compare March 21, 2020 15:25
@iluuu1994
Copy link
Member Author

@dstogov @nikic

As the main authors of the code in question I'm taking the liberty of consulting the two of you.

The following snippet:

var_dump(
    throw new Exception("exception 1"),
    throw new Exception("exception 2")
);

Caused a SEGFAULT in the zend_call_graph.c:

map[call->caller_call_opline - op_array->opcodes] = call;

because call->caller_call_opline was a null pointer. a95da8a fixes the SEGFAULT but I don't know enough about the optimizer to know if this is an appropriate fix.

@nikic
Copy link
Member

nikic commented Mar 23, 2020

@iluuu1994 Also reproduces with

<?php
var_dump(
    exit("exception 1"),
    exit("exception 2")
);

so we should fix this independently.

@nikic
Copy link
Member

nikic commented Mar 23, 2020

@iluuu1994 Should be fixed with 34f1266 / 2e8db5d. I don't want to put in a fake value, we have few enough uses that we can just check for NULL.

@iluuu1994
Copy link
Member Author

@nikic Ok, I'll rebase the branch and check if it works.

@iluuu1994
Copy link
Member Author

iluuu1994 commented Mar 23, 2020

Looks good, thanks @nikic!

@carusogabriel
Copy link
Contributor

@iluuu1994
Copy link
Member Author

@carusogabriel I'm planning to start the voting on this RFC next week. If it passes I can make another RFC for the remaining keywords 🙂

@cmb69
Copy link
Member

cmb69 commented Apr 5, 2020

It seems OPcache (the optimizer?) needs to be adjusted. Currently,

<?php

try {
    throw new Exception(throw new Exception('foo'));
} catch (Exception $ex) {}

leaks memory.

@iluuu1994
Copy link
Member Author

iluuu1994 commented Apr 5, 2020

Thanks @cmb69. I tracked it down to this:

152 bytes in 1 blocks are definitely lost in loss record 23 of 27
   at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
   by 0x5F60EA: __zend_malloc (zend_alloc.c:2976)
   by 0x5F504B: _malloc_custom (zend_alloc.c:2417)
   by 0x5F5179: _emalloc (zend_alloc.c:2536)
   by 0x67B6E5: zend_objects_new (zend_objects.c:196)
   by 0x658987: zend_default_exception_new_ex (zend_exceptions.c:212)
   by 0x658C8D: zend_default_exception_new (zend_exceptions.c:249)
   by 0x632B8B: _object_and_properties_init (zend_API.c:1308)
   by 0x632BFC: object_init_ex (zend_API.c:1322)
   by 0x6AB3A9: ZEND_NEW_SPEC_CONST_UNUSED_HANDLER (zend_vm_execute.h:8858)
   by 0x6FF342: execute_ex (zend_vm_execute.h:52808)
   by 0x702889: zend_execute (zend_vm_execute.h:56143)

(rebased onto master)

I'll see if I can find out what's going on tomorrow.

@dragoonis
Copy link
Contributor

Is the intention behind removing the ; because using ; means it's a statement, instead of an expression? T_THROW expr ';'

@iluuu1994
Copy link
Member Author

@dragoonis

Expressions evaluate to some value while statements do not. throw is somewhat special in that it can never actually evaluate to anything which is called a bottom type.

In type theory, a theory within mathematical logic, the bottom type is the type that has no values.

https://en.wikipedia.org/wiki/Bottom_type

The main reason for doing so is that throw can be used in places where it previously couldn't have. These are described in the RFC.

@dragoonis
Copy link
Contributor

@iluuu1994 thanks for explaining.

@iluuu1994
Copy link
Member Author

iluuu1994 commented Apr 6, 2020

Here are the unoptimized opcodes:

$_main:
     ; (lines=11, args=0, vars=1, tmps=4)
     ; (before optimizer)
     ; /tmp/php-src/Zend/tests/throw/001.php:1-124
     ; return  [] RANGE[0..0]
0000 V1 = NEW 1 string("Exception")
0001 V2 = NEW 1 string("Exception")
0002 SEND_VAL_EX string("foo") 1
0003 DO_FCALL
0004 THROW V2
0005 SEND_VAL_EX bool(true) 1
0006 DO_FCALL
0007 THROW V1
0008 JMP 0010
0009 CV0($e) = CATCH string("Exception")
0010 RETURN int(1)
LIVE RANGES:
     1: 0001 - 0007 (new)
     2: 0002 - 0004 (new)
EXCEPTION TABLE:
     0000, 0009, -, -

vs the optimized ones:

$_main:
     ; (lines=7, args=0, vars=1, tmps=1)
     ; (after optimizer)
     ; /tmp/php-src/Zend/tests/throw/001.php:1-124
0000 V1 = NEW 1 string("Exception")
0001 V1 = NEW 1 string("Exception")
0002 SEND_VAL_EX string("foo") 1
0003 DO_FCALL
0004 THROW V1
0005 CV0($e) = CATCH string("Exception")
0006 RETURN int(1)
LIVE RANGES:
     1: 0002 - 0004 (new)
EXCEPTION TABLE:
     0000, 0005, -, -

So it looks like it removes everything between the first THROW and the CATCH instruction, including the DO_FCALL but not the NEW instruction. I'm not familiar with the optimizer at all so it might take me a while to get to the bottom of this.

@iluuu1994
Copy link
Member Author

iluuu1994 commented Apr 6, 2020

It looks like exit suffers from the same problem.

var_dump(exit());

Unoptimized:

$_main:
     ; (lines=5, args=0, vars=0, tmps=1)
     ; (before optimizer)
     ; /tmp/php-src/Zend/tests/throw/001.php:1-126
     ; return  [] RANGE[0..0]
0000 INIT_FCALL 1 96 string("var_dump")
0001 EXIT
0002 SEND_VAL bool(true) 1
0003 DO_ICALL
0004 RETURN int(1)

Optimized:

$_main:
     ; (lines=2, args=0, vars=0, tmps=0)
     ; (after optimizer)
     ; /tmp/php-src/Zend/tests/throw/001.php:1-126
0000 INIT_FCALL 1 96 string("var_dump")
0001 EXIT

The INIT_FCALL instruction isn't removed but probably should be.

This happens in the CFG optimization pass (pass 5).

@iluuu1994
Copy link
Member Author

As discussed in #5358 the memory leak is related to the live ranges, not INIT_FCALL. Help from somebody experienced with the optimizer would be greatly appreciated!

@TysonAndre
Copy link
Contributor

TysonAndre commented Apr 19, 2020

EDIT: Never mind, my comments were already mentioned by others in the review comments for 5358 already > It looks like `exit` suffers from the same problem.

If valgrind doesn't warn about a memory leak, it's probably not a severe issue - optimizing bad php code like some_fn(exit()) isn't a high priority.

Also note that INIT_FCALL_BY_NAME would throw if the function was undefined, but INIT_FCALL should almost always have a defined function

; unoptimized
0000 V1 = NEW 1 string("Exception")
0001 V2 = NEW 1 string("Exception")
; optimized
0000 V1 = NEW 1 string("Exception")
0001 V1 = NEW 1 string("Exception")

A part of the problem is that the memory for V1 is getting reused, so the first incomplete object is lost track of. I don't know why opcache thought it was safe to reuse that temporary value - it might be specific to ZEND_THROW.

I see that throw new Exception(exit()) was also affected - I haven't tried with that patch but it looks like it should handle that

Actually, it already incorrectly optimizes exit(). It doesn't keep live range for result of the first NEW.

Before rebasing this branch

USE_ZEND_ALLOC=0 valgrind --leak-check=full which php -d zend_extension=opcache.so -d opcache.enable_cli=1 -d opcache.enable=1 -d opcache.opt_debug_level=0x20000 throw.php

0000 V0 = NEW 1 string("Exception")                                                                                                                
0001 EXIT
==10313== 152 bytes in 1 blocks are definitely lost in loss record 30 of 38
==10313==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==10313==    by 0x8A0998: __zend_malloc (in /path/to/bin/php)
==10313==    by 0x9114B9: zend_objects_new (in /path/to/bin/php)
==10313==    by 0x8FA217: zend_default_exception_new_ex (in /path/to/bin/php)
==10313==    by 0x8D87AF: object_init_ex (in /path/to/bin/php)
==10313==    by 0x94D7D0: ZEND_NEW_SPEC_CONST_UNUSED_HANDLER (in /path/to/bin/php)
==10313==    by 0x96B247: execute_ex (in /path/to/bin/php)
==10313==    by 0x974073: zend_execute (in /path/to/bin/php)
==10313==    by 0x8D6852: zend_execute_scripts (in /path/to/bin/php)
==10313==    by 0x861597: php_execute_script (in /path/to/bin/php)
==10313==    by 0x9766B2: do_cli (in /path/to/bin/php)
==10313==    by 0x464D24: main (in /path/to/bin/php)
(and a false positive for zend_accel_load_script)

Copy link
Member

@nikic nikic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll go ahead and merge this, so we have a possibility to test expression control flow issues in-tree. While exit has the same basic problem, it's use of bailout results in leaks being suppressed, so it's not a good test target.

@php-pulls php-pulls closed this in 0810fcd Apr 23, 2020
@iluuu1994
Copy link
Member Author

Thanks @nikic. I'll try to get more familiar with the whole memory management over the next couple of days. I doubt I can come up with a good solution though if you can't. 😆 Should I move the RFC to "Implemented" or wait until we've fully resolved these issues?

@carusogabriel
Copy link
Contributor

Shall we close https://bugs.php.net/bug.php?id=67184? I believe that one is now fixed, right?

@cmb69
Copy link
Member

cmb69 commented Apr 23, 2020

Good catch, @carusogabriel. I've closed that ticket.

@nikic
Copy link
Member

nikic commented Apr 23, 2020

@iluuu1994 I started some work on this in master...nikic:throw-leak. It's ugly, but I think it will work.

And yes, feel free to move to the RFC to Implemented.

@iluuu1994
Copy link
Member Author

Big thanks @nikic for the help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants