Skip to content

Zend type tree #6

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 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2bab522
Add a test that fails
withinboredom Mar 26, 2025
ab38c00
Add scope to class entries
withinboredom Mar 26, 2025
67f8bea
modify grammar
withinboredom Mar 26, 2025
8ddc013
namespaces: add support
withinboredom Mar 26, 2025
b4354c5
allow for defining nested classes
withinboredom Mar 26, 2025
179f489
add more tests and fix a memory leak
withinboredom Mar 26, 2025
a65ba61
add more tests
withinboredom Mar 26, 2025
8502401
add more tests
withinboredom Mar 26, 2025
a18f78e
add reflection support
withinboredom Mar 26, 2025
03e940e
rewrite scopes during opcache compilation
withinboredom Mar 26, 2025
9302221
add tests for visibility
withinboredom Mar 26, 2025
9f5f2a4
handle protected/private lookups in outer classes
withinboredom Mar 26, 2025
8bb387e
handle private/protected types in properties
withinboredom Mar 26, 2025
9ef21d4
do not elide return checks for private/protected inner classes
withinboredom Mar 26, 2025
700684a
do not elide return checks for private/protected inner classes
withinboredom Mar 26, 2025
4fc91b2
add and update tests
withinboredom Mar 26, 2025
64f1bb5
handle instantiation protections
withinboredom Mar 26, 2025
f29972a
start keeping track of own nested classes
withinboredom Mar 26, 2025
ac364dc
simplify resolution logic
withinboredom Mar 26, 2025
a505fe5
keep cased namespace names
withinboredom Mar 26, 2025
68f5063
add alias tests
withinboredom Mar 27, 2025
a2e21e4
initial type tree implementation
withinboredom Mar 27, 2025
e5f007e
fix jit
withinboredom Mar 27, 2025
b8468f7
try to make faster
withinboredom Mar 28, 2025
f46e4a8
remove unused function
withinboredom Mar 28, 2025
cad7f9d
fix issue
withinboredom Mar 28, 2025
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
2 changes: 1 addition & 1 deletion Zend/Optimizer/dfa_pass.c
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ static inline bool can_elide_list_type(
zend_string *lcname = zend_string_tolower(ZEND_TYPE_NAME(*single_type));
zend_class_entry *ce = zend_optimizer_get_class_entry(script, op_array, lcname);
zend_string_release(lcname);
bool result = ce && safe_instanceof(use_info->ce, ce);
bool result = ce && !ce->required_scope && safe_instanceof(use_info->ce, ce);
if (result == !is_intersection) {
return result;
}
Expand Down
2 changes: 1 addition & 1 deletion Zend/tests/errmsg/errmsg_027.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ class test {
echo "Done\n";
?>
--EXPECTF--
Fatal error: Class declarations may not be nested in %s on line %d
Fatal error: Class declarations may not be declared inside functions in %s on line %d
4 changes: 4 additions & 0 deletions Zend/zend.h
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ struct _zend_class_entry {
HashTable properties_info;
HashTable constants_table;

zend_class_entry *required_scope;
zend_class_entry *lexical_scope;
bool required_scope_absolute;

ZEND_MAP_PTR_DEF(zend_class_mutable_data*, mutable_data);
zend_inheritance_cache_entry *inheritance_cache;

Expand Down
246 changes: 243 additions & 3 deletions Zend/zend_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -1816,6 +1816,27 @@ static zend_always_inline zend_result _object_and_properties_init(zval *arg, zen
return FAILURE;
}

if (class_type->required_scope) {
const zend_class_entry *scope = zend_get_executed_scope();
if (UNEXPECTED(scope == NULL)) {
zend_type_error("Cannot instantiate class %s from the global scope", ZSTR_VAL(class_type->name));
ZVAL_NULL(arg);
Z_OBJ_P(arg) = NULL;
return FAILURE;
}

if (class_type->required_scope_absolute) {
if (scope != class_type->required_scope && scope->lexical_scope != class_type->required_scope) {
zend_type_error("Cannot instantiate private class %s from scope %s", ZSTR_VAL(class_type->name), ZSTR_VAL(scope->name));
ZVAL_NULL(arg);
Z_OBJ_P(arg) = NULL;
return FAILURE;
}
} else if (!instanceof_function(scope, class_type->required_scope) && !instanceof_function(scope->lexical_scope, class_type->required_scope)) {
zend_type_error("Cannot instantiate protected class %s from scope %s", ZSTR_VAL(class_type->name), ZSTR_VAL(scope->name));
}
}
Comment on lines +1819 to +1838
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Confirm consistent error handling for protected classes

In the “protected class” branch (line 1836), a zend_type_error() is thrown, but the function does not explicitly return or set the arg to NULL. While the thrown exception will likely terminate the current process of instantiation, consider adding a return statement (and clearing arg similarly to the “private class” branch) to keep logic consistent and avoid partial initialization if an exception is overlooked.

         } else if (!instanceof_function(scope, class_type->required_scope) && !instanceof_function(scope->lexical_scope, class_type->required_scope)) {
             zend_type_error("Cannot instantiate protected class %s from scope %s", ZSTR_VAL(class_type->name), ZSTR_VAL(scope->name));
+            ZVAL_NULL(arg);
+            Z_OBJ_P(arg) = NULL;
+            return FAILURE;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (class_type->required_scope) {
const zend_class_entry *scope = zend_get_executed_scope();
if (UNEXPECTED(scope == NULL)) {
zend_type_error("Cannot instantiate class %s from the global scope", ZSTR_VAL(class_type->name));
ZVAL_NULL(arg);
Z_OBJ_P(arg) = NULL;
return FAILURE;
}
if (class_type->required_scope_absolute) {
if (scope != class_type->required_scope && scope->lexical_scope != class_type->required_scope) {
zend_type_error("Cannot instantiate private class %s from scope %s", ZSTR_VAL(class_type->name), ZSTR_VAL(scope->name));
ZVAL_NULL(arg);
Z_OBJ_P(arg) = NULL;
return FAILURE;
}
} else if (!instanceof_function(scope, class_type->required_scope) && !instanceof_function(scope->lexical_scope, class_type->required_scope)) {
zend_type_error("Cannot instantiate protected class %s from scope %s", ZSTR_VAL(class_type->name), ZSTR_VAL(scope->name));
}
}
if (class_type->required_scope) {
const zend_class_entry *scope = zend_get_executed_scope();
if (UNEXPECTED(scope == NULL)) {
zend_type_error("Cannot instantiate class %s from the global scope", ZSTR_VAL(class_type->name));
ZVAL_NULL(arg);
Z_OBJ_P(arg) = NULL;
return FAILURE;
}
if (class_type->required_scope_absolute) {
if (scope != class_type->required_scope && scope->lexical_scope != class_type->required_scope) {
zend_type_error("Cannot instantiate private class %s from scope %s", ZSTR_VAL(class_type->name), ZSTR_VAL(scope->name));
ZVAL_NULL(arg);
Z_OBJ_P(arg) = NULL;
return FAILURE;
}
} else if (!instanceof_function(scope, class_type->required_scope) && !instanceof_function(scope->lexical_scope, class_type->required_scope)) {
zend_type_error("Cannot instantiate protected class %s from scope %s", ZSTR_VAL(class_type->name), ZSTR_VAL(scope->name));
ZVAL_NULL(arg);
Z_OBJ_P(arg) = NULL;
return FAILURE;
}
}


if (UNEXPECTED(!(class_type->ce_flags & ZEND_ACC_CONSTANTS_UPDATED))) {
if (UNEXPECTED(zend_update_class_constants(class_type) != SUCCESS)) {
ZVAL_NULL(arg);
Expand Down Expand Up @@ -2898,7 +2919,222 @@ ZEND_API void zend_add_magic_method(zend_class_entry *ce, zend_function *fptr, z
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arg_info_toString, 0, 0, IS_STRING, 0)
ZEND_END_ARG_INFO()

static zend_always_inline void zend_normalize_internal_type(zend_type *type) {
static HashTable *interned_type_tree = NULL;

// todo: move to zend_types.h
#define ADD_TO_TREE(list, count, value) \
do { \
list = erealloc(list, sizeof(zend_type) * (count + 1)); \
list[count++] = value; \
} while (0)

static int compare_simple_types(const zend_type a, const zend_type b) {
const uint32_t a_mask = ZEND_TYPE_FULL_MASK(a);
const uint32_t b_mask = ZEND_TYPE_FULL_MASK(b);

if (a_mask != b_mask) {
return a_mask < b_mask ? -1 : 1;
}

const bool a_has_name = ZEND_TYPE_HAS_NAME(a);
const bool b_has_name = ZEND_TYPE_HAS_NAME(b);

if (a_has_name && b_has_name) {
const zend_string *a_name = ZEND_TYPE_NAME(a);
const zend_string *b_name = ZEND_TYPE_NAME(b);
const int cmp = ZSTR_VAL(a_name) == ZSTR_VAL(b_name);
if (cmp != 0) {
return cmp;
}
}

const bool a_nullable = ZEND_TYPE_ALLOW_NULL(a);
const bool b_nullable = ZEND_TYPE_ALLOW_NULL(b);

if (a_nullable != b_nullable) {
return a_nullable ? 1 : -1;
}

// Types are equal
return 0;
}

static int compare_type_nodes(const void *a_, const void *b_) {
const zend_type_node *a = *(zend_type_node **)a_;
const zend_type_node *b = *(zend_type_node **)b_;

if (a->kind != b->kind) {
return (int)a->kind - (int)b->kind;
}

if (a->kind == ZEND_TYPE_SIMPLE) {
return compare_simple_types(a->simple_type, b->simple_type);
}

if (a->compound.num_types != b->compound.num_types) {
return (int)a->compound.num_types - (int)b->compound.num_types;
}

for (uint32_t i = 0; i < a->compound.num_types; i++) {
const int cmp = compare_type_nodes(&a->compound.types[i], &b->compound.types[i]);
if (cmp != 0) {
return cmp;
}
}

return 0;
}

zend_ulong zend_type_node_hash(const zend_type_node *node) {
zend_ulong hash = 2166136261u; // FNV-1a offset basis

hash ^= (zend_ulong)node->kind;
hash *= 16777619;

switch (node->kind) {
case ZEND_TYPE_SIMPLE: {
const zend_type type = node->simple_type;
hash ^= (zend_ulong)ZEND_TYPE_FULL_MASK(type);
hash *= 16777619;

if (ZEND_TYPE_HAS_NAME(type)) {
zend_string *name = ZEND_TYPE_NAME(type);
hash ^= zend_string_hash_val(name);
hash *= 16777619;
}

break;
}

case ZEND_TYPE_UNION:
case ZEND_TYPE_INTERSECTION: {
for (uint32_t i = 0; i < node->compound.num_types; ++i) {
const zend_ulong child_hash = zend_type_node_hash(node->compound.types[i]);
hash ^= child_hash;
hash *= 16777619;
}
break;
}
}

return hash;
}

bool zend_type_node_equals(const zend_type_node *a, const zend_type_node *b) {
if (a == b) return true;
if (a->kind != b->kind) return false;

if (a->kind == ZEND_TYPE_SIMPLE) {
const zend_type at = a->simple_type;
const zend_type bt = b->simple_type;

if (ZEND_TYPE_FULL_MASK(at) != ZEND_TYPE_FULL_MASK(bt)) {
return false;
}

const bool a_has_name = ZEND_TYPE_HAS_NAME(at);
const bool b_has_name = ZEND_TYPE_HAS_NAME(bt);
if (a_has_name != b_has_name) {
return false;
}

if (a_has_name) {
const zend_string *a_name = ZEND_TYPE_NAME(at);
const zend_string *b_name = ZEND_TYPE_NAME(bt);
if (!zend_string_equals(a_name, b_name)) {
return false;
}
}

return true;
}

// Compound type: union or intersection
if (a->compound.num_types != b->compound.num_types) {
return false;
}

for (uint32_t i = 0; i < a->compound.num_types; ++i) {
if (!zend_type_node_equals(a->compound.types[i], b->compound.types[i])) {
return false;
}
}

return true;
}

static zend_type_node *intern_type_node(zend_type_node *node) {
const zend_ulong hash = zend_type_node_hash(node);
zend_type_node *existing;

if (interned_type_tree == NULL) {
interned_type_tree = pemalloc(sizeof(HashTable), 1);
zend_hash_init(interned_type_tree, 64, NULL, NULL, 1);
}

if ((existing = zend_hash_index_find_ptr(interned_type_tree, hash))) {
if (zend_type_node_equals(existing, node)) {
pefree(node, 1);
return existing; // reuse interned node
}
}

zend_hash_index_add_new_ptr(interned_type_tree, hash, node);
return node;
}

ZEND_API zend_type_node *zend_type_to_interned_tree(const zend_type type) {
if (type.type_mask == 0) {
return NULL;
}

if (!ZEND_TYPE_HAS_LIST(type)) {
zend_type_node *node = pemalloc(sizeof(zend_type_node), 1);
node->kind = ZEND_TYPE_SIMPLE;
node->simple_type = type;
return intern_type_node(node);
}

zend_type_list *list = ZEND_TYPE_LIST(type);
const zend_type_node_kind kind = ZEND_TYPE_IS_INTERSECTION(type) ?
ZEND_TYPE_INTERSECTION : ZEND_TYPE_UNION;

zend_type_node **children = NULL;
uint32_t num_children = 0;

zend_type *subtype;

ZEND_TYPE_LIST_FOREACH(list, subtype) {
zend_type_node *child = zend_type_to_interned_tree(*subtype);

if (child->kind == kind) {
for (uint32_t i = 0; child->compound.num_types; i++) {
ADD_TO_TREE(children, num_children, child->compound.types[i]);
}
} else {
ADD_TO_TREE(children, num_children, child);
}
} ZEND_TYPE_LIST_FOREACH_END();

qsort(children, num_children, sizeof(zend_type_node*), compare_type_nodes);

size_t deduped_count = 0;
for (size_t i = 0; i < num_children; i++) {
if (i == 0 || compare_type_nodes(&children[i], &children[i - 1]) != 0) {
children[deduped_count++] = children[i];
}
}

zend_type_node *node = pemalloc(sizeof(zend_type_node), 1);
node->kind = kind;
node->compound.num_types = deduped_count;
node->compound.types = pemalloc(sizeof(zend_type_node *) * deduped_count, 1);
memcpy(node->compound.types, children, sizeof(zend_type_node *) * deduped_count);

return intern_type_node(node);
}
Comment on lines +3065 to +3135
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for potential memory allocation failures

The code for interning type nodes allocates memory but doesn't include checks for allocation failures, which could lead to crashes in low-memory situations.

Add error checking for memory allocations:

    zend_type_node *node = pemalloc(sizeof(zend_type_node), 1);
+   if (!node) {
+       return NULL; // Handle memory allocation failure
+   }
    node->kind = kind;
    node->compound.num_types = deduped_count;
    node->compound.types = pemalloc(sizeof(zend_type_node *) * deduped_count, 1);
+   if (!node->compound.types) {
+       pefree(node, 1);
+       return NULL; // Handle memory allocation failure
+   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static zend_type_node *intern_type_node(zend_type_node *node) {
const zend_ulong hash = zend_type_node_hash(node);
zend_type_node *existing;
if (interned_type_tree == NULL) {
interned_type_tree = pemalloc(sizeof(HashTable), 1);
zend_hash_init(interned_type_tree, 64, NULL, NULL, 1);
}
if ((existing = zend_hash_index_find_ptr(interned_type_tree, hash))) {
if (zend_type_node_equals(existing, node)) {
pefree(node, 1);
return existing; // reuse interned node
}
}
zend_hash_index_add_new_ptr(interned_type_tree, hash, node);
return node;
}
ZEND_API zend_type_node *zend_type_to_interned_tree(const zend_type type) {
if (type.type_mask == 0) {
return NULL;
}
if (!ZEND_TYPE_HAS_LIST(type)) {
zend_type_node *node = pemalloc(sizeof(zend_type_node), 1);
node->kind = ZEND_TYPE_SIMPLE;
node->simple_type = type;
return intern_type_node(node);
}
zend_type_list *list = ZEND_TYPE_LIST(type);
const zend_type_node_kind kind = ZEND_TYPE_IS_INTERSECTION(type) ?
ZEND_TYPE_INTERSECTION : ZEND_TYPE_UNION;
zend_type_node **children = NULL;
uint32_t num_children = 0;
zend_type *subtype;
ZEND_TYPE_LIST_FOREACH(list, subtype) {
zend_type_node *child = zend_type_to_interned_tree(*subtype);
if (child->kind == kind) {
for (uint32_t i = 0; child->compound.num_types; i++) {
ADD_TO_TREE(children, num_children, child->compound.types[i]);
}
} else {
ADD_TO_TREE(children, num_children, child);
}
} ZEND_TYPE_LIST_FOREACH_END();
qsort(children, num_children, sizeof(zend_type_node*), compare_type_nodes);
size_t deduped_count = 0;
for (size_t i = 0; i < num_children; i++) {
if (i == 0 || compare_type_nodes(&children[i], &children[i - 1]) != 0) {
children[deduped_count++] = children[i];
}
}
zend_type_node *node = pemalloc(sizeof(zend_type_node), 1);
node->kind = kind;
node->compound.num_types = deduped_count;
node->compound.types = pemalloc(sizeof(zend_type_node *) * deduped_count, 1);
memcpy(node->compound.types, children, sizeof(zend_type_node *) * deduped_count);
return intern_type_node(node);
}
ZEND_API zend_type_node *zend_type_to_interned_tree(const zend_type type) {
if (type.type_mask == 0) {
return NULL;
}
if (!ZEND_TYPE_HAS_LIST(type)) {
zend_type_node *node = pemalloc(sizeof(zend_type_node), 1);
node->kind = ZEND_TYPE_SIMPLE;
node->simple_type = type;
return intern_type_node(node);
}
zend_type_list *list = ZEND_TYPE_LIST(type);
const zend_type_node_kind kind = ZEND_TYPE_IS_INTERSECTION(type) ?
ZEND_TYPE_INTERSECTION : ZEND_TYPE_UNION;
zend_type_node **children = NULL;
uint32_t num_children = 0;
zend_type *subtype;
ZEND_TYPE_LIST_FOREACH(list, subtype) {
zend_type_node *child = zend_type_to_interned_tree(*subtype);
if (child->kind == kind) {
for (uint32_t i = 0; child->compound.num_types; i++) {
ADD_TO_TREE(children, num_children, child->compound.types[i]);
}
} else {
ADD_TO_TREE(children, num_children, child);
}
} ZEND_TYPE_LIST_FOREACH_END();
qsort(children, num_children, sizeof(zend_type_node*), compare_type_nodes);
size_t deduped_count = 0;
for (size_t i = 0; i < num_children; i++) {
if (i == 0 || compare_type_nodes(&children[i], &children[i - 1]) != 0) {
children[deduped_count++] = children[i];
}
}
zend_type_node *node = pemalloc(sizeof(zend_type_node), 1);
+ if (!node) {
+ return NULL; // Handle memory allocation failure
+ }
node->kind = kind;
node->compound.num_types = deduped_count;
node->compound.types = pemalloc(sizeof(zend_type_node *) * deduped_count, 1);
+ if (!node->compound.types) {
+ pefree(node, 1);
+ return NULL; // Handle memory allocation failure
+ }
memcpy(node->compound.types, children, sizeof(zend_type_node *) * deduped_count);
return intern_type_node(node);
}


static zend_always_inline zend_type_node *zend_normalize_internal_type(zend_type *type) {
ZEND_ASSERT(!ZEND_TYPE_HAS_LITERAL_NAME(*type));
if (ZEND_TYPE_PURE_MASK(*type) != MAY_BE_ANY) {
ZEND_ASSERT(!ZEND_TYPE_CONTAINS_CODE(*type, IS_RESOURCE) && "resource is not allowed in a zend_type");
Expand All @@ -2921,6 +3157,8 @@ static zend_always_inline void zend_normalize_internal_type(zend_type *type) {
} ZEND_TYPE_FOREACH_END();
}
} ZEND_TYPE_FOREACH_END();

return zend_type_to_interned_tree(*type);
}

/* registers all functions in *library_functions in the function hash */
Expand Down Expand Up @@ -3190,7 +3428,7 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
new_arg_info[i].type = legacy_iterable;
}

zend_normalize_internal_type(&new_arg_info[i].type);
new_arg_info[i].type_tree = zend_normalize_internal_type(&new_arg_info[i].type);
}
}

Expand Down Expand Up @@ -4665,7 +4903,9 @@ ZEND_API zend_property_info *zend_declare_typed_property(zend_class_entry *ce, z
property_info->type = type;

if (is_persistent_class(ce)) {
zend_normalize_internal_type(&property_info->type);
property_info->type_tree = zend_normalize_internal_type(&property_info->type);
} else {
property_info->type_tree = zend_type_to_interned_tree(property_info->type);
}

zend_hash_update_ptr(&ce->properties_info, name, property_info);
Expand Down
Loading
Loading