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

Implement GH-18261: Allow cast to be used in constant expressions #18264

Merged
merged 7 commits into from
Apr 11, 2025
Merged
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 NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ PHP NEWS
(timwolla, Volker Dusch)
. Added get_error_handler(), get_exception_handler() functions. (Arnaud)
. Fixed bug GH-15753 and GH-16198 (Bind traits before parent class). (ilutov)
. Added support for casts in constant expressions. (nielsdos)

- Curl:
. Added curl_multi_get_handles(). (timwolla)
Expand Down
1 change: 1 addition & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ PHP 8.5 UPGRADE NOTES
RFC: https://wiki.php.net/rfc/marking_return_value_as_important
. Added asymmetric visibility support for static properties.
RFC: https://wiki.php.net/rfc/static-aviz
. Added support for casts in constant expressions.

- Curl:
. Added support for share handles that are persisted across multiple PHP
Expand Down
64 changes: 64 additions & 0 deletions Zend/tests/constexpr/constant_expressions_cast.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
--TEST--
Constant expressions with cast
--FILE--
<?php
class X {
public int $foo = 3;
}

const T1 = (int) 0.3;
const T2 = (bool) 0.3;
const T3 = (string) [];
const T4 = (object) ["a" => 1];
const T5 = (float) 5;
const T6 = (array) "";
const T7 = (array) var_dump(...);
const T8 = (array) new X;
const T9 = (array) new DateTime;
const T10 = (int) new DateTime;

var_dump(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
?>
--EXPECTF--
Warning: Array to string conversion in %s on line %d

Warning: Object of class DateTime could not be converted to int in %s on line %d
int(0)
bool(true)
string(5) "Array"
object(stdClass)#%d (1) {
["a"]=>
int(1)
}
float(5)
array(1) {
[0]=>
string(0) ""
}
array(1) {
[0]=>
object(Closure)#%d (2) {
["function"]=>
string(8) "var_dump"
["parameter"]=>
array(2) {
["$value"]=>
string(10) "<required>"
["$values"]=>
string(10) "<optional>"
}
}
}
array(1) {
["foo"]=>
int(3)
}
array(3) {
["date"]=>
string(%d) "%s"
["timezone_type"]=>
int(%d)
["timezone"]=>
string(%d) "%s"
}
int(1)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--TEST--
Constant expressions with object cast in property
--FILE--
<?php
class X {
public $foo = (object) [];
}
?>
--EXPECTF--
Fatal error: Object casts are not supported in this context in %s on line %d
35 changes: 35 additions & 0 deletions Zend/zend_ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,41 @@ ZEND_API zend_result ZEND_FASTCALL zend_ast_evaluate_inner(
}
zval_ptr_dtor_nogc(&op1);
break;
case ZEND_AST_CAST:
if (UNEXPECTED(zend_ast_evaluate_ex(&op1, ast->child[0], scope, &short_circuited, ctx) != SUCCESS)) {
ret = FAILURE;
break;
}
if (ast->attr == Z_TYPE(op1)) {
ZVAL_COPY_VALUE(result, &op1);
} else {
switch (ast->attr) {
case _IS_BOOL:
ZVAL_BOOL(result, zend_is_true(&op1));
break;
case IS_LONG:
ZVAL_LONG(result, zval_get_long_func(&op1, false));
break;
case IS_DOUBLE:
ZVAL_DOUBLE(result, zval_get_double_func(&op1));
break;
case IS_STRING:
ZVAL_STR(result, zval_get_string_func(&op1));
break;
case IS_ARRAY:
zend_cast_zval_to_array(result, &op1, IS_VAR);
break;
case IS_OBJECT:
zend_cast_zval_to_object(result, &op1, IS_VAR);
break;
EMPTY_SWITCH_DEFAULT_CASE();
Copy link
Contributor

Choose a reason for hiding this comment

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

what about the new (void) cast?

Copy link
Member

Choose a reason for hiding this comment

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

Void casts are not allowed in expression context, only when their return value is not used. This never applies to constant expressions. This is also a different ast type (ZEND_AST_CAST_VOID), so it doesn't need to be handled here.

}
zval_ptr_dtor_nogc(&op1);
if (UNEXPECTED(EG(exception))) {
ret = FAILURE;
}
}
break;
case ZEND_AST_OR:
if (UNEXPECTED(zend_ast_evaluate_ex(&op1, ast->child[0], scope, &short_circuited, ctx) != SUCCESS)) {
ret = FAILURE;
Expand Down
7 changes: 7 additions & 0 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -11156,6 +11156,7 @@ static bool zend_is_allowed_in_const_expr(zend_ast_kind kind) /* {{{ */
|| kind == ZEND_AST_AND || kind == ZEND_AST_OR
|| kind == ZEND_AST_UNARY_OP
|| kind == ZEND_AST_UNARY_PLUS || kind == ZEND_AST_UNARY_MINUS
|| kind == ZEND_AST_CAST
|| kind == ZEND_AST_CONDITIONAL || kind == ZEND_AST_DIM
|| kind == ZEND_AST_ARRAY || kind == ZEND_AST_ARRAY_ELEM
|| kind == ZEND_AST_UNPACK
Expand Down Expand Up @@ -11430,6 +11431,12 @@ static void zend_compile_const_expr(zend_ast **ast_ptr, void *context) /* {{{ */
case ZEND_AST_MAGIC_CONST:
zend_compile_const_expr_magic_const(ast_ptr);
break;
case ZEND_AST_CAST:
if (ast->attr == IS_OBJECT && !ctx->allow_dynamic) {
zend_error_noreturn(E_COMPILE_ERROR,
"Object casts are not supported in this context");
}
break;
case ZEND_AST_NEW:
if (!ctx->allow_dynamic) {
zend_error_noreturn(E_COMPILE_ERROR,
Expand Down
54 changes: 54 additions & 0 deletions Zend/zend_execute.h
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,60 @@ static zend_always_inline void zend_safe_assign_to_variable_noref(zval *variable
}
}

static zend_always_inline void zend_cast_zval_to_object(zval *result, zval *expr, uint8_t op1_type) {
HashTable *ht;

ZVAL_OBJ(result, zend_objects_new(zend_standard_class_def));
if (Z_TYPE_P(expr) == IS_ARRAY) {
ht = zend_symtable_to_proptable(Z_ARR_P(expr));
if (GC_FLAGS(ht) & IS_ARRAY_IMMUTABLE) {
/* TODO: try not to duplicate immutable arrays as well ??? */
ht = zend_array_dup(ht);
}
Z_OBJ_P(result)->properties = ht;
} else if (Z_TYPE_P(expr) != IS_NULL) {
Z_OBJ_P(result)->properties = ht = zend_new_array(1);
expr = zend_hash_add_new(ht, ZSTR_KNOWN(ZEND_STR_SCALAR), expr);
if (op1_type == IS_CONST) {
if (UNEXPECTED(Z_OPT_REFCOUNTED_P(expr))) Z_ADDREF_P(expr);
} else {
if (Z_OPT_REFCOUNTED_P(expr)) Z_ADDREF_P(expr);
}
}
}

static zend_always_inline void zend_cast_zval_to_array(zval *result, zval *expr, uint8_t op1_type) {
extern zend_class_entry *zend_ce_closure;
if (op1_type == IS_CONST || Z_TYPE_P(expr) != IS_OBJECT || Z_OBJCE_P(expr) == zend_ce_closure) {
if (Z_TYPE_P(expr) != IS_NULL) {
ZVAL_ARR(result, zend_new_array(1));
expr = zend_hash_index_add_new(Z_ARRVAL_P(result), 0, expr);
if (op1_type == IS_CONST) {
if (UNEXPECTED(Z_OPT_REFCOUNTED_P(expr))) Z_ADDREF_P(expr);
} else {
if (Z_OPT_REFCOUNTED_P(expr)) Z_ADDREF_P(expr);
}
} else {
ZVAL_EMPTY_ARRAY(result);
}
} else if (ZEND_STD_BUILD_OBJECT_PROPERTIES_ARRAY_COMPATIBLE(expr)) {
/* Optimized version without rebuilding properties HashTable */
ZVAL_ARR(result, zend_std_build_object_properties_array(Z_OBJ_P(expr)));
} else {
HashTable *obj_ht = zend_get_properties_for(expr, ZEND_PROP_PURPOSE_ARRAY_CAST);
if (obj_ht) {
/* fast copy */
ZVAL_ARR(result, zend_proptable_to_symtable(obj_ht,
(Z_OBJCE_P(expr)->default_properties_count ||
Z_OBJ_P(expr)->handlers != &std_object_handlers ||
GC_IS_RECURSIVE(obj_ht))));
zend_release_properties(obj_ht);
} else {
ZVAL_EMPTY_ARRAY(result);
}
}
}

ZEND_API zend_result ZEND_FASTCALL zval_update_constant(zval *pp);
ZEND_API zend_result ZEND_FASTCALL zval_update_constant_ex(zval *pp, zend_class_entry *scope);
ZEND_API zend_result ZEND_FASTCALL zval_update_constant_with_ctx(zval *pp, zend_class_entry *scope, zend_ast_evaluate_ctx *ctx);
Expand Down
48 changes: 2 additions & 46 deletions Zend/zend_vm_def.h
Original file line number Diff line number Diff line change
Expand Up @@ -6460,7 +6460,6 @@ ZEND_VM_COLD_CONST_HANDLER(51, ZEND_CAST, CONST|TMP|VAR|CV, ANY, TYPE)
USE_OPLINE
zval *expr;
zval *result = EX_VAR(opline->result.var);
HashTable *ht;

SAVE_OPLINE();
expr = GET_OP1_ZVAL_PTR(BP_VAR_R);
Expand Down Expand Up @@ -6494,53 +6493,10 @@ ZEND_VM_COLD_CONST_HANDLER(51, ZEND_CAST, CONST|TMP|VAR|CV, ANY, TYPE)
}

if (opline->extended_value == IS_ARRAY) {
if (OP1_TYPE == IS_CONST || Z_TYPE_P(expr) != IS_OBJECT || Z_OBJCE_P(expr) == zend_ce_closure) {
if (Z_TYPE_P(expr) != IS_NULL) {
ZVAL_ARR(result, zend_new_array(1));
expr = zend_hash_index_add_new(Z_ARRVAL_P(result), 0, expr);
if (OP1_TYPE == IS_CONST) {
if (UNEXPECTED(Z_OPT_REFCOUNTED_P(expr))) Z_ADDREF_P(expr);
} else {
if (Z_OPT_REFCOUNTED_P(expr)) Z_ADDREF_P(expr);
}
} else {
ZVAL_EMPTY_ARRAY(result);
}
} else if (ZEND_STD_BUILD_OBJECT_PROPERTIES_ARRAY_COMPATIBLE(expr)) {
/* Optimized version without rebuilding properties HashTable */
ZVAL_ARR(result, zend_std_build_object_properties_array(Z_OBJ_P(expr)));
} else {
HashTable *obj_ht = zend_get_properties_for(expr, ZEND_PROP_PURPOSE_ARRAY_CAST);
if (obj_ht) {
/* fast copy */
ZVAL_ARR(result, zend_proptable_to_symtable(obj_ht,
(Z_OBJCE_P(expr)->default_properties_count ||
Z_OBJ_P(expr)->handlers != &std_object_handlers ||
GC_IS_RECURSIVE(obj_ht))));
zend_release_properties(obj_ht);
} else {
ZVAL_EMPTY_ARRAY(result);
}
}
zend_cast_zval_to_array(result, expr, OP1_TYPE);
} else {
ZEND_ASSERT(opline->extended_value == IS_OBJECT);
ZVAL_OBJ(result, zend_objects_new(zend_standard_class_def));
if (Z_TYPE_P(expr) == IS_ARRAY) {
ht = zend_symtable_to_proptable(Z_ARR_P(expr));
if (GC_FLAGS(ht) & IS_ARRAY_IMMUTABLE) {
/* TODO: try not to duplicate immutable arrays as well ??? */
ht = zend_array_dup(ht);
}
Z_OBJ_P(result)->properties = ht;
} else if (Z_TYPE_P(expr) != IS_NULL) {
Z_OBJ_P(result)->properties = ht = zend_new_array(1);
expr = zend_hash_add_new(ht, ZSTR_KNOWN(ZEND_STR_SCALAR), expr);
if (OP1_TYPE == IS_CONST) {
if (UNEXPECTED(Z_OPT_REFCOUNTED_P(expr))) Z_ADDREF_P(expr);
} else {
if (Z_OPT_REFCOUNTED_P(expr)) Z_ADDREF_P(expr);
}
}
zend_cast_zval_to_object(result, expr, OP1_TYPE);
}
}

Expand Down
Loading