Skip to content

Commit d948924

Browse files
committed
Support 'static const CONST_NAME = dynamic_expr;'
Currently, this is limited to the global scope. This can be thought of as similar to C: the static const can only be seen within the namespace block scope. Differences from global constants: - A name can only be declared once per namespace block. - The name can only be used by the following statements in the namespace block. - This can support any expression define() would accept (function calls, $variable, properties, etc.). - This is eagerly evaluated when the expression might be dynamic. This is deliberate, and meant to make accidental infinite recursion (and unexpected side effects of fetching a constant for the first time) less likely. This may throw in the future. - The value from the first successful declaration is used and permanently stored. Similarities to global constants: - 'static const' rejects the same types (references, objects, and reference cycles.) - 'static const' cannot be modified once declared The current implementation allows code such as this: ```php static const LOCAL_X = "Local $x"; class Example { const X = LOCAL_X; } ``` Which is shorthand for: ``` if (!defined('UNIQUE_NAME_FOR_LOCAL_X')) { define('UNIQUE_NAME_FOR_LOCAL_X', "Local $x"); } class Example { const X = \LOCAL_NAME_FOR_LOCAL_X; } ``` Unfinished work: - Work out what to do if the expression throws an exception/error. (Fatal error? Leave as is?) - Decide what this should do if the `define()` call fails. (Fatal error? Throw a regular Error?) - (Optional) opcache optimizations such as evaluating functions at compile time.
1 parent ade217d commit d948924

9 files changed

+220
-1
lines changed

Zend/tests/static_const.phpt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
--TEST--
2+
static const supports dynamic expressions
3+
--FILE--
4+
<?php
5+
6+
namespace {
7+
function trace($val) {
8+
echo "Declaring\n";
9+
var_dump($val);
10+
return $val;
11+
}
12+
13+
$eof = "\n";
14+
static const VAL1 = "World"; // sprintf("Hello, %s", "World");
15+
// echo "VAL1 is " . VAL1 . "\n";
16+
static const VAL2 = sprintf("Hello, %s%s", VAL1, $eof);
17+
18+
function main() {
19+
echo VAL2;
20+
}
21+
}
22+
23+
namespace {
24+
25+
$local = "Other string\n";
26+
static const VAL2 = $local;
27+
28+
main();
29+
echo VAL2;
30+
main();
31+
}
32+
33+
--EXPECT--
34+
Hello, World
35+
Other string
36+
Hello, World

Zend/tests/static_const_is_const.phpt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--TEST--
2+
static const cannot be modified
3+
--FILE--
4+
<?php
5+
6+
namespace Example;
7+
$var = "Example";
8+
static const VAL1 = [$var];
9+
try {
10+
preg_match('/\w+/', 'Test message', VAL1);
11+
} catch (\Error $e) {
12+
echo "Caught: " . $e->getMessage() . "\n";
13+
}
14+
var_dump(VAL1);
15+
--EXPECT--
16+
Caught: Cannot pass parameter 3 by reference
17+
array(1) {
18+
[0]=>
19+
string(7) "Example"
20+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
--TEST--
2+
static const cannot be repeated in the same namespace block
3+
--FILE--
4+
<?php
5+
6+
namespace {
7+
static const VAL = "First";
8+
static const VAL = "Second";
9+
}
10+
--EXPECTF--
11+
Fatal error: Cannot declare static const VAL because the name is already in use in %s on line 5
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
--TEST--
2+
static const can only be used at the top level
3+
--FILE--
4+
<?php
5+
6+
if (true) {
7+
static const VAL = "First";
8+
}
9+
echo VAL;
10+
--EXPECTF--
11+
Parse error: syntax error, unexpected 'const' (T_CONST), expecting :: (T_PAAMAYIM_NEKUDOTAYIM) in %s on line 4

Zend/zend_ast.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,6 +1540,7 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
15401540

15411541
case ZEND_AST_CONST_DECL:
15421542
case ZEND_AST_CLASS_CONST_DECL:
1543+
case ZEND_AST_STATIC_CONST_DECL:
15431544
smart_str_appends(str, "const ");
15441545
goto simple_list;
15451546
case ZEND_AST_NAME_LIST:

Zend/zend_ast.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ enum _zend_ast_kind {
5858
ZEND_AST_PROP_DECL,
5959
ZEND_AST_CONST_DECL,
6060
ZEND_AST_CLASS_CONST_DECL,
61+
ZEND_AST_STATIC_CONST_DECL,
6162
ZEND_AST_NAME_LIST,
6263
ZEND_AST_TRAIT_ADAPTATIONS,
6364
ZEND_AST_USE,

Zend/zend_compile.c

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ void zend_file_context_begin(zend_file_context *prev_context) /* {{{ */
362362
FC(current_namespace) = NULL;
363363
FC(in_namespace) = 0;
364364
FC(has_bracketed_namespaces) = 0;
365+
FC(namespace_id) = 0;
365366
FC(declarables).ticks = 0;
366367
zend_hash_init(&FC(seen_symbols), 8, NULL, NULL, 0);
367368
}
@@ -835,6 +836,29 @@ uint32_t zend_add_member_modifier(uint32_t flags, uint32_t new_flag) /* {{{ */
835836
}
836837
/* }}} */
837838

839+
zend_string *zend_concat6(char *str1, size_t str1_len, char *str2, size_t str2_len, char *str3, size_t str3_len, char *str4, size_t str4_len, char* str5, size_t str5_len, char* str6, size_t str6_len) /* {{{ */
840+
{
841+
size_t len = str1_len + str2_len + str3_len + str4_len + str5_len + str6_len;
842+
zend_string *res = zend_string_alloc(len, 0);
843+
844+
char* ptr = ZSTR_VAL(res);
845+
memcpy(ptr, str1, str1_len);
846+
ptr += str1_len;
847+
memcpy(ptr, str2, str2_len);
848+
ptr += str2_len;
849+
memcpy(ptr, str3, str3_len);
850+
ptr += str3_len;
851+
memcpy(ptr, str4, str4_len);
852+
ptr += str4_len;
853+
memcpy(ptr, str5, str5_len);
854+
ptr += str5_len;
855+
memcpy(ptr, str6, str6_len);
856+
ptr[str6_len] = '\0';
857+
858+
return res;
859+
}
860+
/* }}} */
861+
838862
zend_string *zend_concat3(char *str1, size_t str1_len, char *str2, size_t str2_len, char *str3, size_t str3_len) /* {{{ */
839863
{
840864
size_t len = str1_len + str2_len + str3_len;
@@ -847,11 +871,28 @@ zend_string *zend_concat3(char *str1, size_t str1_len, char *str2, size_t str2_l
847871

848872
return res;
849873
}
874+
/* }}} */
850875

851876
zend_string *zend_concat_names(char *name1, size_t name1_len, char *name2, size_t name2_len) {
852877
return zend_concat3(name1, name1_len, "\\", 1, name2, name2_len);
853878
}
854879

880+
/* NOTE: Using the \0 byte seems to make tests fail. */
881+
zend_string *zend_prefix_with_file(zend_string *name) { /* {{{ */
882+
zend_string *filename = CG(compiled_filename);
883+
zend_string *namespace_id_str = zend_long_to_str(FC(namespace_id));
884+
zend_string *result = zend_concat6(
885+
"static const ", sizeof("static const ") - 1,
886+
ZSTR_VAL(name), ZSTR_LEN(name),
887+
" at ", sizeof(" at ") - 1,
888+
ZSTR_VAL(filename), ZSTR_LEN(filename),
889+
"#", 1,
890+
ZSTR_VAL(namespace_id_str), ZSTR_LEN(namespace_id_str));
891+
zend_string_release_ex(namespace_id_str, 0);
892+
return result;
893+
}
894+
/* }}} */
895+
855896
zend_string *zend_prefix_with_ns(zend_string *name) {
856897
if (FC(current_namespace)) {
857898
zend_string *ns = FC(current_namespace);
@@ -6947,6 +6988,93 @@ void zend_compile_const_decl(zend_ast *ast) /* {{{ */
69476988
}
69486989
/* }}}*/
69496990

6991+
void zend_compile_static_const_elem(zend_ast *const_ast) { /* {{{ */
6992+
zend_ast *name_ast = const_ast->child[0];
6993+
zend_ast *value_ast = const_ast->child[1];
6994+
zend_string *unqualified_name = zend_ast_get_str(name_ast);
6995+
6996+
zend_string *name;
6997+
if (zend_get_special_const(ZSTR_VAL(unqualified_name), ZSTR_LEN(unqualified_name))) {
6998+
zend_error_noreturn(E_COMPILE_ERROR,
6999+
"Cannot redeclare static constant '%s'", ZSTR_VAL(unqualified_name));
7000+
}
7001+
7002+
// "static const" lasts for the life of a namespace block.
7003+
name = zend_prefix_with_file(unqualified_name);
7004+
name = zend_new_interned_string(name);
7005+
7006+
if (!zend_hash_add_ptr(zend_get_import_ht(ZEND_SYMBOL_CONST), unqualified_name, name)) {
7007+
zend_error_noreturn(E_COMPILE_ERROR, "Cannot declare static const %s because "
7008+
"the name is already in use", ZSTR_VAL(unqualified_name));
7009+
}
7010+
zend_eval_const_expr(&value_ast);
7011+
7012+
/* 0. Check if it can be evaluated at compile time. */
7013+
if (value_ast->kind != ZEND_AST_ZVAL) {
7014+
/*
7015+
* This is a dynamic expression, and could not be evaluated with the information available while compiling the file.
7016+
* 1. JMPNZ if defined('UNIQUE_CONST_NAME')
7017+
* 2. Call define('UNIQUE_CONST_NAME', $dynamicValue)
7018+
* 3. update target of JMPNZ
7019+
*/
7020+
uint32_t opnum_jmpz;
7021+
znode defined_opline_result;
7022+
zend_op *defined_opline;
7023+
// TODO: Skip emitting the DEFINED and just emit DECLARE_CONST directly
7024+
// if this can be determined at compile time.
7025+
defined_opline = zend_emit_op_tmp(&defined_opline_result, ZEND_DEFINED, NULL, NULL);
7026+
defined_opline->op1_type = IS_CONST;
7027+
LITERAL_STR(defined_opline->op1, name);
7028+
defined_opline->extended_value = zend_alloc_cache_slot();
7029+
7030+
opnum_jmpz = zend_emit_cond_jump(ZEND_JMPNZ, &defined_opline_result, 0);
7031+
7032+
{
7033+
/* Generate the AST for a call to define('GLOBAL_UNIQUE_CONST', $dynamic) and compile it. */
7034+
zend_ast *define_name_ast;
7035+
zend_ast *define_call_ast;
7036+
zend_ast *args_ast;
7037+
zend_string *define_str = zend_new_interned_string(zend_string_init("define", 6, 0));
7038+
7039+
define_name_ast = zend_ast_create_zval_from_str(define_str);
7040+
define_name_ast->attr = ZEND_NAME_FQ;
7041+
args_ast = zend_ast_create_list(2, ZEND_AST_ARG_LIST, zend_ast_create_zval_from_str(name), value_ast);
7042+
define_call_ast = zend_ast_create(ZEND_AST_CALL, define_name_ast, args_ast);
7043+
7044+
zend_compile_expr(NULL, define_call_ast);
7045+
/* Temporary ASTs get freed when the arena is freed */
7046+
}
7047+
zend_update_jump_target_to_next(opnum_jmpz);
7048+
} else {
7049+
/* This expression can be evaluated at compile time */
7050+
znode name_node, value_node;
7051+
zval *value_zv = &value_node.u.constant;
7052+
7053+
value_node.op_type = IS_CONST;
7054+
7055+
zend_const_expr_to_zval(value_zv, value_ast);
7056+
zend_string_addref(name);
7057+
7058+
name_node.op_type = IS_CONST;
7059+
ZVAL_STR(&name_node.u.constant, name);
7060+
7061+
zend_emit_op(NULL, ZEND_DECLARE_CONST, &name_node, &value_node);
7062+
}
7063+
7064+
zend_register_seen_symbol(name, ZEND_SYMBOL_CONST);
7065+
}
7066+
/* }}} */
7067+
7068+
void zend_compile_static_const_decl(zend_ast *ast) /* {{{ */
7069+
{
7070+
zend_ast_list *list = zend_ast_get_list(ast);
7071+
uint32_t i;
7072+
for (i = 0; i < list->children; ++i) {
7073+
zend_compile_static_const_elem(list->child[i]);
7074+
}
7075+
}
7076+
/* }}}*/
7077+
69507078
void zend_compile_namespace(zend_ast *ast) /* {{{ */
69517079
{
69527080
zend_ast *name_ast = ast->child[0];
@@ -7011,6 +7139,7 @@ void zend_compile_namespace(zend_ast *ast) /* {{{ */
70117139
if (with_bracket) {
70127140
FC(has_bracketed_namespaces) = 1;
70137141
}
7142+
FC(namespace_id)++;
70147143

70157144
if (stmt_ast) {
70167145
zend_compile_top_stmt(stmt_ast);
@@ -8706,6 +8835,9 @@ void zend_compile_stmt(zend_ast *ast) /* {{{ */
87068835
case ZEND_AST_CONST_DECL:
87078836
zend_compile_const_decl(ast);
87088837
break;
8838+
case ZEND_AST_STATIC_CONST_DECL:
8839+
zend_compile_static_const_decl(ast);
8840+
break;
87098841
case ZEND_AST_NAMESPACE:
87108842
zend_compile_namespace(ast);
87118843
break;

Zend/zend_compile.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ typedef struct _zend_file_context {
115115
HashTable *imports_const;
116116

117117
HashTable seen_symbols;
118+
zend_long namespace_id;
118119
} zend_file_context;
119120

120121
typedef union _zend_parser_stack_elem {

Zend/zend_language_parser.y

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
244244
%type <ast> variable_class_name dereferencable_scalar constant dereferencable
245245
%type <ast> callable_expr callable_variable static_member new_variable
246246
%type <ast> encaps_var encaps_var_offset isset_variables
247-
%type <ast> top_statement_list use_declarations const_list inner_statement_list if_stmt
247+
%type <ast> top_statement_list use_declarations const_list static_const_list inner_statement_list if_stmt
248248
%type <ast> alt_if_stmt for_exprs switch_case_list global_var_list static_var_list
249249
%type <ast> echo_expr_list unset_variables catch_name_list catch_list parameter_list class_statement_list
250250
%type <ast> implements_list case_list if_stmt_without_else
@@ -334,6 +334,7 @@ top_statement:
334334
| T_USE use_declarations ';' { $$ = $2; $$->attr = ZEND_SYMBOL_CLASS; }
335335
| T_USE use_type use_declarations ';' { $$ = $3; $$->attr = $2; }
336336
| T_CONST const_list ';' { $$ = $2; }
337+
| T_STATIC T_CONST static_const_list ';' { $$ = $3; }
337338
;
338339

339340
use_type:
@@ -403,6 +404,11 @@ const_list:
403404
| const_decl { $$ = zend_ast_create_list(1, ZEND_AST_CONST_DECL, $1); }
404405
;
405406

407+
static_const_list:
408+
static_const_list ',' const_decl { $$ = zend_ast_list_add($1, $3); }
409+
| const_decl { $$ = zend_ast_create_list(1, ZEND_AST_STATIC_CONST_DECL, $1); }
410+
;
411+
406412
inner_statement_list:
407413
inner_statement_list inner_statement
408414
{ $$ = zend_ast_list_add($1, $2); }

0 commit comments

Comments
 (0)