Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/debug.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ std::string debugTokenName(TokenType t) {
{tok_double_arrow, "tok_double_arrow"},
{tok_double_colon, "tok_double_colon"},
{tok_arrow, "tok_arrow"},
{tok_nullsafe_arrow, "tok_nullsafe_arrow"},
{tok_class_c, "tok_class_c"},
{tok_file_c, "tok_file_c"},
{tok_file_relative_c, "tok_file_relative_c"},
Expand Down
9 changes: 6 additions & 3 deletions compiler/gentree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "compiler/name-gen.h"
#include "compiler/phpdoc.h"
#include "compiler/stage.h"
#include "compiler/token.h"
#include "compiler/type-hint.h"
#include "compiler/utils/string-utils.h"
#include "compiler/vertex.h"
Expand Down Expand Up @@ -332,12 +333,12 @@ VertexPtr GenTree::get_postfix_expression(VertexPtr res, bool parenthesized) {
}
res.set_location(location);
need = true;
} else if (tp == tok_arrow) {
} else if (vk::any_of_equal(tp, tok_arrow, tok_nullsafe_arrow)) {
auto location = auto_location();
next_cur();
VertexPtr rhs = get_expr_top(true);
CE (!kphp_error(rhs, "Failed to parse right argument of '->'"));
res = process_arrow(res, rhs);
res = process_arrow(res, rhs, tp == tok_nullsafe_arrow);
CE(res);
res.set_location(location);
need = true;
Expand Down Expand Up @@ -1782,17 +1783,19 @@ VertexAdaptor<op_var> GenTree::auto_capture_this_in_lambda(FunctionPtr f_lambda)
return v_captured_this;
}

VertexPtr GenTree::process_arrow(VertexPtr lhs, VertexPtr rhs) {
VertexPtr GenTree::process_arrow(VertexPtr lhs, VertexPtr rhs, bool is_null_safe) {
if (rhs->type() == op_func_name) {
auto inst_prop = VertexAdaptor<op_instance_prop>::create(lhs);
inst_prop->str_val = rhs->get_string();
inst_prop->is_null_safe = is_null_safe;
return inst_prop;

} else if (auto as_func_call = rhs.try_as<op_func_call>()) {
auto new_root = VertexAdaptor<op_func_call>::create(lhs, as_func_call->args());
new_root->extra_type = op_ex_func_call_arrow;
new_root->str_val = as_func_call->str_val;
new_root->reifiedTs = as_func_call->reifiedTs;
new_root->is_null_safe = is_null_safe;
return new_root;

} else {
Expand Down
2 changes: 1 addition & 1 deletion compiler/gentree.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ class GenTree {
VertexPtr get_class(const PhpDocComment *phpdoc, ClassType class_type);
void parse_extends_implements();

static VertexPtr process_arrow(VertexPtr lhs, VertexPtr rhs);
static VertexPtr process_arrow(VertexPtr lhs, VertexPtr rhs, bool is_null_safe = false);

private:
const TypeHint *get_typehint();
Expand Down
13 changes: 11 additions & 2 deletions compiler/lexer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,9 @@ void LexerData::hack_last_tokens() {
}

/**
* For a case when we encounter a keyword after the '->' it should be a tok_func_name,
* For a case when we encounter a keyword after the '->' and '?->' it should be a tok_func_name,
* not tok_array, tok_try, etc.
* For example: $c->array, $c->try
* For example: $c->array, $c?->try
*/
if (are_last_tokens(tok_arrow, any_token_tag{})) {
if (!tokens.back().str_val.empty() && is_alpha(tokens.back().str_val[0])) {
Expand All @@ -215,6 +215,13 @@ void LexerData::hack_last_tokens() {
}
}

if (are_last_tokens(tok_nullsafe_arrow, any_token_tag{})) {
if (!tokens.back().str_val.empty() && is_alpha(tokens.back().str_val[0])) {
tokens.back().type_ = tok_func_name;
return;
}
}

// replace elseif with else+if, but not if the previous token can cause elseif
// to be interpreted as identifier (class const and method)
if (are_last_tokens(tok_elseif)) {
Expand Down Expand Up @@ -1112,6 +1119,7 @@ void TokenLexerCommon::init() {
add_rule(h, "=>", tok_double_arrow);
add_rule(h, "::", tok_double_colon);
add_rule(h, "->", tok_arrow);
add_rule(h, "?->", tok_nullsafe_arrow);
add_rule(h, "...", tok_varg);
}

Expand Down Expand Up @@ -1180,6 +1188,7 @@ void TokenLexerPHPDoc::init() {
add_rule(h, "::", tok_double_colon);
add_rule(h, "=>", tok_double_arrow);
add_rule(h, "->", tok_arrow);
add_rule(h, "?->", tok_nullsafe_arrow);
add_rule(h, "...", tok_varg);
add_rule(h, "=", tok_eq1);
}
Expand Down
60 changes: 60 additions & 0 deletions compiler/pipes/gen-tree-postprocess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@

#include "compiler/pipes/gen-tree-postprocess.h"

#include "auto/compiler/vertex/vertex-types.h"
#include "compiler/compiler-core.h"
#include "compiler/data/class-data.h"
#include "compiler/data/lib-data.h"
#include "compiler/data/src-file.h"
#include "compiler/data/vertex-adaptor.h"
#include "compiler/kphp_assert.h"
#include "compiler/name-gen.h"
#include "compiler/vertex-util.h"
#include "runtime/php_assert.h"

namespace {
template <typename F>
Expand Down Expand Up @@ -236,6 +241,61 @@ VertexPtr GenTreePostprocessPass::on_exit_vertex(VertexPtr root) {
}
}

// Transformation of expr?->field_or_func_call to
// {
// $tmp_unique_name = expr;
// $tmp_unique_name === null ? null : $tmp_unique_name->field_or_func_call;
// }
// We use op_seq_rval for State Exprs extension (the last statement in the block
// is used as value of whole block)
// Naive implementation could look like
// $expr === null? null : $expr->field_or_func_call
// But in case of foo()?->field_or_func_call it could call "foo()" twice
// That is why we store "expr" to temporary variable
auto transform_nullsufe = [](VertexPtr root) {
const auto before_nullsafe_arrow = [&root]() {
if (root->type() == op_instance_prop) {
return root.as<op_instance_prop>()->instance();
}
kphp_assert_msg(root->type() == op_func_call, "Internal compiler error: transformation of nullsafe operator failf");
return *root.as<op_func_call>()->begin();
}();
auto tmp_var = VertexAdaptor<op_var>::create().set_location(root);
tmp_var->str_val = gen_unique_name("tmp_before_nullsafe_arrow");
tmp_var->extra_type = op_ex_var_superlocal_inplace;

auto assignment = VertexAdaptor<op_set>::create(tmp_var, before_nullsafe_arrow).set_location(root);
auto cond = VertexAdaptor<op_eq3>::create(VertexAdaptor<op_null>::create(), tmp_var).set_location(root);

VertexPtr transformed_vertex {};
if (auto as_instance_prop = root.try_as<op_instance_prop>()) {
transformed_vertex = VertexAdaptor<op_instance_prop>::create(tmp_var).set_location(root);
transformed_vertex.as<op_instance_prop>()->str_val = as_instance_prop->str_val;
}
else if (auto as_call = root.try_as<op_func_call>()) {
transformed_vertex = as_call.clone();
*transformed_vertex->begin() = tmp_var;
}
else {
kphp_fail_msg("Internal compiler error: transformation of nullsafe operator fail");
}

auto ternary = VertexAdaptor<op_ternary>::create(VertexAdaptor<op_conv_bool>::create(cond),
VertexAdaptor<op_null>::create(),
transformed_vertex).set_location(root);

return VertexAdaptor<op_seq_rval>::create(assignment, ternary).set_location(root);
};

auto is_nullsafe_construction = [](VertexPtr vertex) {
return (vertex->type() == op_instance_prop && vertex.as<op_instance_prop>()->is_null_safe)||
(vertex->type() == op_func_call && vertex.as<op_func_call>()->is_null_safe);
};

if (is_nullsafe_construction(root)) {
return transform_nullsufe(root);
}

if (auto call = root.try_as<op_func_call>()) {
if (!G->settings().profiler_level.get() && call->size() == 1 &&
vk::any_of_equal(call->get_string(), "profiler_set_log_suffix", "profiler_set_function_label")) {
Expand Down
1 change: 1 addition & 0 deletions compiler/token.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ enum TokenType {
tok_double_arrow,
tok_double_colon,
tok_arrow,
tok_nullsafe_arrow,

tok_class_c,
tok_file_c,
Expand Down
8 changes: 8 additions & 0 deletions compiler/vertex-desc.json
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@
"reifiedTs": {
"type": "GenericsInstantiationMixin *",
"default": "nullptr"
},
"is_null_safe": {
"type": "bool",
"default": "false"
}
}
},
Expand Down Expand Up @@ -519,6 +523,10 @@
"access_type": {
"type": "InstancePropAccessType",
"default": "InstancePropAccessType::Default"
},
"is_null_safe": {
"type": "bool",
"default": "false"
}
}
},
Expand Down
3 changes: 3 additions & 0 deletions tests/cpp/compiler/lexer-test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ TEST(lexer_test, test_php_tokens) {
{"$obj->exit", {"tok_var_name($obj)", "tok_arrow(->)", "tok_func_name(exit)"}},
{"$obj->exit()", {"tok_var_name($obj)", "tok_arrow(->)", "tok_func_name(exit)", "tok_oppar(()", "tok_clpar())"}},
{"$obj->throw", {"tok_var_name($obj)", "tok_arrow(->)", "tok_func_name(throw)"}},
{"$obj?->exit", {"tok_var_name($obj)", "tok_nullsafe_arrow(?->)", "tok_func_name(exit)"}},
{"$obj?->exit()", {"tok_var_name($obj)", "tok_nullsafe_arrow(?->)", "tok_func_name(exit)", "tok_oppar(()", "tok_clpar())"}},
{"$obj?->throw", {"tok_var_name($obj)", "tok_nullsafe_arrow(?->)", "tok_func_name(throw)"}},
{"Example::for", {"tok_func_name(Example::for)"}},
{"Example::for()", {"tok_func_name(Example::for)", "tok_oppar(()", "tok_clpar())"}},

Expand Down
176 changes: 176 additions & 0 deletions tests/phpt/php8/nullsafe_op/001_nullsafe_op_simple.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
@ok php8
<?php

class C {
public int $dd;
function __construct() {
$this->dd = 42;
}
function null() {
var_dump('C::null()');
if (2 > 1) {
return $this;
}
return null;
}

function self() {
var_dump('C::self()');
return $this;
}

function d() {
var_dump('C::d()');
return $this->dd;
}

function d_param(int $tmp, int $tmp2, int $tmp3) {
var_dump('C::d_param(int, int, int)');
return $this->dd;
}
}

class B {
public C $cc;
function __construct() {
$this->cc = new C;
}
function null() {
var_dump('B::null()');
if (2 > 1) {
return $this;
}
return null;
}

function self() {
var_dump('B::self()');
return $this;
}

function c() {
var_dump('B::c()');
return $this->cc;
}

function c_param(int $tmp, int $tmp2) {
var_dump('B::c_param(int, int)');
return $this->cc;
}
}

class A {
public B $bb;
function __construct() {
$this->bb = new B;
}
function null() {
var_dump('A::null()');
if (2 > 1) {
return $this;
}
return null;
}

function self() {
var_dump('A::self()');
return $this;
}

function b() {
var_dump('A::b()');
return $this->bb;
}

function b_param(int $tmp) {
var_dump('A::b_param(int)');
return $this->bb;
}

public static B $b_static;
public static function get_b_static() {
return self::$b_static;
}

}

function getA() {
return new A;
}

/// Start testing

function field_chain(?A $a) {
return $a?->bb?->cc?->dd;
}

function field_chain_starts_with_fun_call() {
return getA()?->bb?->cc?->dd;
}

var_dump(field_chain(new A));
var_dump(field_chain(NULL));
var_dump(field_chain_starts_with_fun_call());

function method_chain(?A $a) {
return $a?->self()?->b()?->self()?->c()?->self()?->d();
}

function method_chain_starts_with_fun_call() {
return getA()?->self()?->b()?->self()?->c()?->self()?->dd;
}

var_dump(method_chain(new A));
var_dump(method_chain(NULL));
var_dump(method_chain_starts_with_fun_call());


function method_params_chain(?A $a) {
return $a?->self()?->b_param(1)?->self()?->c_param(42, 146)?->self()?->d_param(42, 146, 42);
}

var_dump(method_params_chain(new A));
var_dump(method_params_chain(NULL));

function mixed_chain(?A $a) {
return $a?->self()?->bb?->self()?->cc?->self()?->d();
}

function mixed_chain_starts_with_fun_call() {
return getA()?->bb?->self()?->cc?->self()?->dd;
}

var_dump(mixed_chain(new A));
var_dump(mixed_chain(NULL));
var_dump(mixed_chain_starts_with_fun_call());

function null_chain_1(?A $a) {
return $a?->null()?->bb?->self()?->cc?->self()?->d();
}

function null_chain_2(?A $a) {
return $a?->self()?->b()?->null()?->c()?->self()?->d();
}

function null_chain_3(?A $a) {
return $a?->self()?->bb?->self()?->c()?->null()?->d();
}

var_dump(null_chain_1(new A));
var_dump(null_chain_1(null));
var_dump(null_chain_2(new A));
var_dump(null_chain_2(null));
var_dump(null_chain_3(new A));
var_dump(null_chain_3(null));

function or_null(?A $a) {
return $a?->self()?->bb?->self()?->c();
}
$res = or_null(getA());
if ($res) {
var_dump($res->d());
}

A::$b_static = new B;
var_dump(A::$b_static?->null()?->c()?->self()?->d());
var_dump(A::get_b_static()?->null()?->c()?->self()?->d());
Loading