Skip to content
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

Untenable situation with null values #1835

Closed
dschissler opened this issue Mar 24, 2019 · 12 comments
Closed

Untenable situation with null values #1835

dschissler opened this issue Mar 24, 2019 · 12 comments

Comments

@dschissler
Copy link

dschissler commented Mar 24, 2019

@sergeyklay I've been working with both Zephir 0.10.15 and 0.11.11 and to my horror I discovered the very sad state of null.

Basically == doesn't work in any contexts. Nor does === work. is_null and the empty operator both work correctly but only if the variable was initialized to null. Here is the real kicker though: var_export will always print the variable as null. Huh, so much for debugging with error logs. That is why it seemed like I was debugging polymorphic Heisencode.

Yeah well, Dave's an idiot. Well guess what? So are you guys @niden @CameronHall for approving and merging my PR when I removed the explicit setting of the values to null. It obviously doesn't work correctly in all circumstances and who knows what the consequences of those misconfigured nulls can be down the road. It could act super weird in some circumstances. Its not even a PHP value, its a weirdo value.

null is just TOO DAMN ARCANE as it is now. There needs to be compiler protection or something. Also why doesn't == or === work as expected? I guess that now I need to audit every use of null checking throughout my extension because its only working by luck.

echo "val == null\n";
echo "----------------\n";
this->testDoubleEqualNoInit();
echo "--\n";
this->testDoubleEqualWithInit();
echo "\n\n";

echo "val === null\n";
echo "----------------\n";
this->testTripleEqualNoInit();
echo "--\n";
this->testTripleEqualWithInit();
echo "\n\n";

echo "is_null(val)\n";
echo "----------------\n";
this->testIsNullNoInit();
echo "--\n";
this->testIsNullWithInit();
echo "\n\n";

echo "empty val\n";
echo "----------------\n";
this->testEmptyNoInit();
echo "--\n";
this->testEmptyWithInit();
echo "\n\n";

this->testTypeofNullNoInit();
    public function testDoubleEqualNoInit()
    {
        var conditional, val;

        let conditional = val == null;

        echo "testDoubleEqualNoInit: \n";
        echo "val: " . var_export(val, true) . "\n";
        echo "val == null -> " . var_export(conditional, true) . "\n";
    }

    public function testDoubleEqualWithInit()
    {
        var conditional, val;

        // Init val with null
        let val = null;

        let conditional = val == null;

        echo "testDoubleEqualWithInit: \n";
        echo "val: " . var_export(val, true) . "\n";
        echo "val == null -> " . var_export(conditional, true) . "\n";
    }

    public function testTripleEqualNoInit()
    {
        var conditional, val;

        let conditional = val === null;

        echo "testTripleEqualNoInit: \n";
        echo "val: " . var_export(val, true) . "\n";
        echo "val === null -> " . var_export(conditional, true) . "\n";
    }

    public function testTripleEqualWithInit()
    {
        var conditional, val;

        // Init val with null
        let val = null;

        let conditional = val === null;

        echo "testTripleEqualWithInit: \n";
        echo "val: " . var_export(val, true) . "\n";
        echo "val === null -> " . var_export(conditional, true) . "\n";
    }

    public function testIsNullNoInit()
    {
        var conditional, val;

        let conditional = is_null(val);

        echo "testIsNullNoInit: \n";
        echo "val: " . var_export(val, true) . "\n";
        echo "is_null(val) -> " . var_export(conditional, true) . "\n";
    }

    public function testIsNullWithInit()
    {
        var conditional, val;

        // Init val with null
        let val = null;

        let conditional = is_null(val);

        echo "testIsNullWithInit: \n";
        echo "val: " . var_export(val, true) . "\n";
        echo "is_null(val) -> " . var_export(conditional, true) . "\n";
    }

    public function testEmptyNoInit()
    {
        var conditional, val;

        let conditional = empty val;

        echo "testEmptyNoInit: \n";
        echo "val: " . var_export(val, true) . "\n";
        echo "empty val -> " . var_export(conditional, true) . "\n";
    }

    public function testEmptyWithInit()
    {
        var conditional, val;

        // Init val with null
        let val = null;

        let conditional = is_null(val);

        echo "testEmptyWithInit: \n";
        echo "val: " . var_export(val, true) . "\n";
        echo "empty val -> " . var_export(conditional, true) . "\n";
    }

    public function testTypeofNullNoInit()
    {
        var conditional, val;

        let conditional = typeof val;

        echo "testTypeofNullNoInit: \n";
        echo "val: " . var_export(val, true) . "\n";
        echo "typeof val -> " . var_export(conditional, true) . "\n";
    }

Output:

val == null
----------------
testDoubleEqualNoInit: 
val: NULL
val == null -> NULL
--
testDoubleEqualWithInit: 
val: NULL
val == null -> NULL


val === null
----------------
testTripleEqualNoInit: 
val: NULL
val === null -> NULL
--
testTripleEqualWithInit: 
val: NULL
val === null -> NULL


is_null(val)
----------------
testIsNullNoInit: 
val: NULL
is_null(val) -> false
--
testIsNullWithInit: 
val: NULL
is_null(val) -> true


empty val
----------------
testEmptyNoInit: 
val: NULL
empty val -> NULL
--
testEmptyWithInit: 
val: NULL
empty val -> true


testTypeofNullNoInit: 
val: NULL
typeof val -> 'unknown type'
@dschissler

This comment was marked as abuse.

@ViltusVilks
Copy link

ViltusVilks commented Mar 25, 2019

Basically, Zephir should handle ZVAL_UNDEF somehow, but it is very sensitive, because Zephir is just a proxy language to C

Not always == null means that zval variable has type/value NULL, it is really can be that zval can be null ref pointer (UNDEF)...

From sources...

  1. === null
  2. if (Z_TYPE_P(...) == IS_NULL)
  3. Z_TYPE_P(*zval) -> Z_TYPE(zval)
  4. Z_TYPE(zval) -> zval_get_type(&(zval))
  5. and inlined zval_get_type
static zend_always_inline zend_uchar zval_get_type(const zval* pz) {
	return pz->u1.v.type;
}

@dschissler

This comment was marked as abuse.

@danhunsaker
Copy link
Contributor

So the answer is no, this isn't a bug, but a side effect of how PHP (and the Zend engine that powers it) create and store variables. What PHP considers a null, C (and Zephir, by extension) sees as a complex data structure that can store any of several types of value, complete with a handful of supporting values to indicate that variable's type and state. This structure does not, in fact, equal null on the C/Zephir layer, and the equality tests you're trying will, correctly, state that fact. All those Z_, zend_, and zval bits mentioned above are part of the process needed to dig into the C versions of PHP variables to find out whether the variable contains a PHP null or not, at the C level.

The official PHP releases are written in C, with an architecture called the Zend engine serving as the interpreter, as well as the interface for extensions to access/be accessed by PHP code. Zephir code is translated to this Zend/C level, then compiled using the same mechanisms any other extension uses - which also means it's subject to the same limitations, such as complexity accessing/identifying underlying variable values.

Could this be improved, somehow? Maybe. But is it a bug? Not really. Just an oddity of the underlying architecture.

@dschissler

This comment was marked as abuse.

@dschissler

This comment was marked as abuse.

@joeyhub
Copy link

joeyhub commented Mar 26, 2019

This is the same bother with null you get in MySQL.

For reference: FATMOUSE + YOU = FATMOUSE

Replace FATMOUSE with NULL and now you know how it works.

The problem is not concisely explained. What's happening here is that it's doing this:

$var = null; var_dump($var === null); // In zephir this returns null rather than trudat.

As in it does something strange that would almost never be needed.

Just an oddity of the underlying architecture.

Shifting the burden doesn't solve anything and I really doubt this is mandatory. In fact I'm sure of it, it's just how the language is designed. PHP just turns it's byte code into C in runtime. If PHP can handle it, so can anything else. To blame PHP/C internals for zephir not being able to do something PHP/C internals do that zephir is meant to replicate is point blank codswallop. You have PHP with pipeline of PHP -> INTERNAL. It does it fine. You have then the pipe line of ZEPHIR -> INTERNAL. The problem is INTERNAL? I don't think so.

What's more likely here is that you've got one null to rule them all when under the hood you've got zval NULL, pointer NULL, key not in array, uninitialized zval, etc.

PHP's null is not always perfect either. If you notice how in JS you have separate undefined and null. In PHP if you deref a non-existent key you get null rather than any kind of undefined value, in fact the only thing PHP has to represent undefined in that case is null. If you don't have notices on then with $arr['key'] returning null you don't know if it's because the key is not there or the key is set to null, so you have to use array key exists the same problem as having to use is null here.

Basically null gets tossed back at you for all kinds of situations as a default and it's ambiguous which is in play. As in on fail return null, always, it's the universal exception, ubitquitous. The thing is people would expect with somethingThatsAFailToLookUp === null that the left side would fail in place and translate to null so the null comparison would return false. Instead the whole comparison fails which to me is just weird. That's like throwing an exception one scope above the current execution context. What happens if you do (somethingThatsAFailToLookUp) === null? Try the other way around as well. Also var dump without the assignment.

What if asignment has higher precedence? Try let conditional = (val === null);.

If you check the documentation zephir has no precedence. I think the developers took marketing seriously on that one. So you have to wrap EVERYTHING in brackets.

Don't worry though, JS is foobar as well also doing the same for undefined. Console:

> {wtf: undefined}
< {wtf: undefined}

In fact there's yet another type missing, unassigned, which should in this case toss an exception as it's being assigned.

Rambling aside, I'd categorically consider this a design defect at the least.

@dschissler

This comment was marked as abuse.

@joeyhub
Copy link

joeyhub commented Mar 27, 2019

If you have var x = null;return x; it doesn't fail and returns null even if your return type is -> int.

@dschissler

This comment was marked as abuse.

@Jeckerson
Copy link
Member

Work is in progress to standardize variable initialization when it can be nullable. Closing.

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

No branches or pull requests

6 participants