Skip to content

RFC: short and inner classes #17895

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 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a90015e
add syntax parsing
withinboredom Feb 16, 2025
ea41b0c
enable short syntax
withinboredom Feb 16, 2025
8ff2ec2
add tests
withinboredom Feb 16, 2025
b6291cb
add support for inner classes to the grammar
withinboredom Feb 16, 2025
ed99541
allow nested classes
withinboredom Feb 17, 2025
ee872d2
allow calling nested classes
withinboredom Feb 17, 2025
b0bc792
allow ::class to work
withinboredom Feb 20, 2025
728466a
add more tests
withinboredom Feb 20, 2025
82d9a24
make properties automatically public
withinboredom Feb 20, 2025
c4c0d62
allow returning inner classes
withinboredom Feb 22, 2025
46b4693
prevent access to private/protected inner classes
withinboredom Feb 22, 2025
8aa6ac4
show a more informative error when accessing a private class
withinboredom Feb 22, 2025
adaa60c
show an error when deeply nesting classes
withinboredom Feb 22, 2025
fac70f0
remove a debug comment
withinboredom Feb 22, 2025
3336bee
fix some formatting
withinboredom Feb 22, 2025
5b172fd
get working with opcache
withinboredom Feb 22, 2025
57c46bd
remove formatting changes
withinboredom Feb 22, 2025
30ab932
prevent nesting inside methods
withinboredom Feb 22, 2025
f8846fd
fix failing tests for non-opcache
withinboredom Feb 23, 2025
9352d56
Fix failing test
withinboredom Feb 24, 2025
cf1fb4d
add new opcode to opcache
withinboredom Mar 5, 2025
34d51be
show that an inner class does not work on anon classes
withinboredom Mar 5, 2025
89af238
do not override constructor on short and empty classes
withinboredom Mar 5, 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
1 change: 1 addition & 0 deletions Zend/Optimizer/block_pass.c
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ static void zend_optimize_block(zend_basic_block *block, zend_op_array *op_array
if (src >= op_array->opcodes + block->start &&
src->opcode != ZEND_FETCH_R &&
src->opcode != ZEND_FETCH_STATIC_PROP_R &&
src->opcode != ZEND_FETCH_INNER_CLASS &&
src->opcode != ZEND_FETCH_DIM_R &&
src->opcode != ZEND_FETCH_OBJ_R &&
src->opcode != ZEND_NEW &&
Expand Down
2 changes: 2 additions & 0 deletions Zend/Optimizer/compact_literals.c
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
case ZEND_ASSIGN_STATIC_PROP:
case ZEND_ASSIGN_STATIC_PROP_REF:
case ZEND_FETCH_STATIC_PROP_R:
case ZEND_FETCH_INNER_CLASS:
case ZEND_FETCH_STATIC_PROP_W:
case ZEND_FETCH_STATIC_PROP_RW:
case ZEND_FETCH_STATIC_PROP_IS:
Expand Down Expand Up @@ -690,6 +691,7 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
case ZEND_ASSIGN_STATIC_PROP:
case ZEND_ASSIGN_STATIC_PROP_REF:
case ZEND_FETCH_STATIC_PROP_R:
case ZEND_FETCH_INNER_CLASS:
case ZEND_FETCH_STATIC_PROP_W:
case ZEND_FETCH_STATIC_PROP_RW:
case ZEND_FETCH_STATIC_PROP_IS:
Expand Down
3 changes: 2 additions & 1 deletion Zend/Optimizer/zend_inference.c
Original file line number Diff line number Diff line change
Expand Up @@ -3816,14 +3816,15 @@ static zend_always_inline zend_result _zend_update_type_info(
}
break;
case ZEND_FETCH_STATIC_PROP_R:
case ZEND_FETCH_INNER_CLASS:
case ZEND_FETCH_STATIC_PROP_IS:
case ZEND_FETCH_STATIC_PROP_RW:
case ZEND_FETCH_STATIC_PROP_W:
case ZEND_FETCH_STATIC_PROP_UNSET:
case ZEND_FETCH_STATIC_PROP_FUNC_ARG:
tmp = zend_fetch_prop_type(script,
zend_fetch_static_prop_info(script, op_array, ssa, opline), &ce);
if (opline->opcode != ZEND_FETCH_STATIC_PROP_R
if (opline->opcode != ZEND_FETCH_STATIC_PROP_R && opline->opcode != ZEND_FETCH_INNER_CLASS
&& opline->opcode != ZEND_FETCH_STATIC_PROP_IS) {
tmp |= MAY_BE_REF | MAY_BE_INDIRECT;
if ((opline->extended_value & ZEND_FETCH_OBJ_FLAGS) == ZEND_FETCH_DIM_WRITE) {
Expand Down
2 changes: 2 additions & 0 deletions Zend/Optimizer/zend_optimizer.c
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ bool zend_optimizer_update_op1_const(zend_op_array *op_array,
case ZEND_ASSIGN_STATIC_PROP:
case ZEND_ASSIGN_STATIC_PROP_REF:
case ZEND_FETCH_STATIC_PROP_R:
case ZEND_FETCH_INNER_CLASS:
case ZEND_FETCH_STATIC_PROP_W:
case ZEND_FETCH_STATIC_PROP_RW:
case ZEND_FETCH_STATIC_PROP_IS:
Expand Down Expand Up @@ -463,6 +464,7 @@ bool zend_optimizer_update_op2_const(zend_op_array *op_array,
case ZEND_ASSIGN_STATIC_PROP:
case ZEND_ASSIGN_STATIC_PROP_REF:
case ZEND_FETCH_STATIC_PROP_R:
case ZEND_FETCH_INNER_CLASS:
case ZEND_FETCH_STATIC_PROP_W:
case ZEND_FETCH_STATIC_PROP_RW:
case ZEND_FETCH_STATIC_PROP_IS:
Expand Down
4 changes: 4 additions & 0 deletions Zend/zend.h
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ struct _zend_class_entry {
uint32_t num_hooked_props;
uint32_t num_hooked_prop_variance_checks;

// When set, prevent usage of this class outside of the given scope
zend_class_entry *required_scope;
char required_scope_absolute;

/* class_entry or string(s) depending on ZEND_ACC_LINKED */
union {
zend_class_entry **interfaces;
Expand Down
1 change: 1 addition & 0 deletions Zend/zend_API.h
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ typedef struct _zend_fcall_info_cache {
class_container.name = zend_string_init_interned(class_name, class_name_len, 1); \
class_container.default_object_handlers = &std_object_handlers; \
class_container.info.internal.builtin_functions = functions; \
class_container.required_scope = NULL; \
}

#define INIT_CLASS_ENTRY_INIT_METHODS(class_container, functions) \
Expand Down
1 change: 1 addition & 0 deletions Zend/zend_ast.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ enum _zend_ast_kind {
ZEND_AST_YIELD,
ZEND_AST_COALESCE,
ZEND_AST_ASSIGN_COALESCE,
ZEND_AST_INNER_CLASS,

ZEND_AST_STATIC,
ZEND_AST_WHILE,
Expand Down
174 changes: 168 additions & 6 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,37 @@ uint32_t zend_add_member_modifier(uint32_t flags, uint32_t new_flag, zend_modifi
return 0;
}
}
if (target == ZEND_MODIFIER_TARGET_INNER_CLASS) {
if ((flags & ZEND_ACC_PPP_MASK) && (new_flag & ZEND_ACC_PPP_MASK)) {
zend_throw_exception(zend_ce_compile_error,
"Multiple access type modifiers are not allowed", 0);
return 0;
}

if ((flags & ZEND_ACC_STATIC) || (new_flag & ZEND_ACC_STATIC)) {
zend_throw_exception(zend_ce_compile_error,
"Static inner classes are not allowed", 0);
return 0;
}

if ((flags & ZEND_ACC_PUBLIC_SET) || (new_flag & ZEND_ACC_PUBLIC_SET)) {
zend_throw_exception(zend_ce_compile_error,
"Public(set) inner classes are not allowed", 0);
return 0;
}

if ((flags & ZEND_ACC_PROTECTED_SET) || (new_flag & ZEND_ACC_PROTECTED_SET)) {
zend_throw_exception(zend_ce_compile_error,
"Protected(set) inner classes are not allowed", 0);
return 0;
}

if ((flags & ZEND_ACC_PRIVATE_SET) || (new_flag & ZEND_ACC_PRIVATE_SET)) {
zend_throw_exception(zend_ce_compile_error,
"Private(set) inner classes are not allowed", 0);
return 0;
}
}
return new_flags;
}
/* }}} */
Expand Down Expand Up @@ -2244,6 +2275,7 @@ static void zend_adjust_for_fetch_type(zend_op *opline, znode *result, uint32_t

switch (type) {
case BP_VAR_R:
case BP_VAR_INNER_CLASS:
opline->result_type = IS_TMP_VAR;
result->op_type = IS_TMP_VAR;
return;
Expand Down Expand Up @@ -3229,6 +3261,8 @@ static zend_op *zend_compile_static_prop(znode *result, zend_ast *ast, uint32_t

if (delayed) {
opline = zend_delayed_emit_op(result, ZEND_FETCH_STATIC_PROP_R, &prop_node, NULL);
} else if (ast->kind == ZEND_AST_INNER_CLASS) {
opline = zend_emit_op(result, ZEND_FETCH_INNER_CLASS, &prop_node, NULL);
} else {
opline = zend_emit_op(result, ZEND_FETCH_STATIC_PROP_R, &prop_node, NULL);
}
Expand Down Expand Up @@ -6962,6 +6996,18 @@ static zend_type zend_compile_single_typename(zend_ast *ast)
}

return (zend_type) ZEND_TYPE_INIT_CODE(ast->attr, 0, 0);
} else if (ast->kind == ZEND_AST_CLASS_CONST || ast->kind == ZEND_AST_INNER_CLASS) {
zval cnz;
zend_try_compile_const_expr_resolve_class_name(&cnz, ast->child[0]);
zend_string *class_name = Z_STR(cnz);
zend_string *const_name = zend_ast_get_str(ast->child[1]);
zend_string *inner_class_name = zend_string_concat3(
ZSTR_VAL(class_name), ZSTR_LEN(class_name),
"::", 2,
ZSTR_VAL(const_name), ZSTR_LEN(const_name));
zend_string_release(class_name);

return (zend_type) ZEND_TYPE_INIT_CLASS(inner_class_name, /* allow null */ false, 0);
} else {
zend_string *type_name = zend_ast_get_str(ast);
uint8_t type_code = zend_lookup_builtin_type_by_name(type_name);
Expand Down Expand Up @@ -9054,13 +9100,58 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel)

zend_class_entry *original_ce = CG(active_class_entry);

if (EXPECTED((decl->flags & ZEND_ACC_ANON_CLASS) == 0)) {
zend_string *unqualified_name = decl->name;
// handle short syntax, which involves rewiting the AST before continuing
if (decl->flags & ZEND_ACC_SHORT_SYNTAX) {
// child 0 is extends
// child 1 is implements
// child 2 is parameter list
// child 3 is traits

// we need to replace child 2 (stmt_ast) with a new AST:
// 1. that includes the constructor ensuring that the constructor properties are declared as "public" if not
// currently declared.
// 2. that includes the declared traits
// from there, we just continue as normal

// Create a new AST node for the new statement list which will eventually replace stmt_ast
zend_ast *new_stmt_ast = zend_ast_create_list(0, ZEND_AST_STMT_LIST);

// Create a new AST node for the constructor, setting any parameters to public
if (decl->child[2]) {
zend_ast_list *params = zend_ast_get_list(decl->child[2]);

for (uint32_t i = 0; i < params->children; i++) {
ZEND_ASSERT(params->child[i]->kind == ZEND_AST_PARAM);
if (params->child[i]->attr < ZEND_ACC_PUBLIC) {
params->child[i]->attr = ZEND_ACC_PUBLIC;
}
}

if (CG(active_class_entry)) {
zend_error_noreturn(E_COMPILE_ERROR, "Class declarations may not be nested");
if (params->children > 0) {
zend_ast *constructor_ast = zend_ast_create_decl(ZEND_AST_METHOD, ZEND_ACC_PUBLIC, decl->start_lineno, decl->doc_comment, zend_string_init("__construct", sizeof("__construct") - 1, 0), decl->child[2], NULL, zend_ast_create_list(0, ZEND_AST_STMT_LIST), NULL, NULL);

// Add the constructor to the new statement list
zend_ast_list_add(new_stmt_ast, constructor_ast);
}
}

// update both the new statement ast and the original children to be cleaned up.
stmt_ast = new_stmt_ast;
decl->child[2] = new_stmt_ast;

// finally, we now need to add "use" statements for the traits
if (decl->child[3]) {
zend_ast *use_trait_ast = zend_ast_create(ZEND_AST_USE_TRAIT, decl->child[3], NULL);
zend_ast_list_add(new_stmt_ast, use_trait_ast);
}

// and finally, remove the list of traits from the original decl
decl->child[3] = NULL;
}

if (EXPECTED((decl->flags & ZEND_ACC_ANON_CLASS) == 0)) {
zend_string *unqualified_name = decl->name;

const char *type = "a class name";
if (decl->flags & ZEND_ACC_ENUM) {
type = "an enum name";
Expand All @@ -9069,8 +9160,64 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel)
} else if (decl->flags & ZEND_ACC_TRAIT) {
type = "a trait name";
}

zend_assert_valid_class_name(unqualified_name, type);
name = zend_prefix_with_ns(unqualified_name);

// check to make sure we are in a class body and not a function body
if (CG(active_class_entry) && CG(active_op_array)->function_name) {
zend_error_noreturn(E_COMPILE_ERROR, "Class declarations may not be nested");
}

if (CG(active_class_entry)) {
// we have a nested class that needs to be renamed
// so append the unqualified name to the nested parent name
// but prevent nesting more than 1 level deep
if (zend_memnstr(ZSTR_VAL(CG(active_class_entry)->name), "::", sizeof("::") - 1, ZSTR_VAL(CG(active_class_entry)->name) + ZSTR_LEN(CG(active_class_entry)->name))) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot nest classes more than 1 level deep");
}
name = zend_string_concat3(
ZSTR_VAL(CG(active_class_entry)->name), ZSTR_LEN(CG(active_class_entry)->name),
"::", 2,
ZSTR_VAL(unqualified_name), ZSTR_LEN(unqualified_name));

zval name_zv;
ZVAL_STR(&name_zv, name);
GC_ADDREF(name);

// configure the current ce->flags for a nested class. This should only include:
// - final
// - readonly
// - abstract
ce->ce_flags |= decl->attr & (ZEND_ACC_FINAL|ZEND_ACC_READONLY|ZEND_ACC_ABSTRACT);

// configure the property/const stand-ins for a nested class. This should only include:
// - public
// - private
// - protected
int propFlags = (decl->attr & (ZEND_ACC_PUBLIC|ZEND_ACC_PROTECTED|ZEND_ACC_PRIVATE)) | ZEND_ACC_INNER_CLASS_REFERENCE;

// there are two things we need to inject into the nested parent:
// - a static property that contains the name of the nested class
// - a constant that contains the name of the nested class
// this "tricks" the engine into thinking that the nested class is a normal class
zend_type t = ZEND_TYPE_INIT_CODE(IS_STRING, 0, 0);
zval propz, constz;
ZVAL_COPY_OR_DUP(&propz, &name_zv);
ZVAL_COPY_OR_DUP(&constz, &name_zv);
zend_declare_typed_property(CG(active_class_entry), unqualified_name, &propz, propFlags | ZEND_ACC_STATIC | ZEND_ACC_READONLY, decl->doc_comment, t);
zend_declare_class_constant_ex(CG(active_class_entry), unqualified_name, &constz, propFlags, decl->doc_comment);
ZVAL_PTR_DTOR(&name_zv);

// if a class is private or protected, we need to require scope for type checks
ce->required_scope = propFlags & (ZEND_ACC_PROTECTED|ZEND_ACC_PRIVATE) ? CG(active_class_entry) : NULL;
ce->required_scope_absolute = propFlags & (ZEND_ACC_PRIVATE) ? true : false;

// oh, and make sure we emit the right opcodes
toplevel = true;
} else {
name = zend_prefix_with_ns(unqualified_name);
ce->required_scope = NULL;
}
name = zend_new_interned_string(name);
lcname = zend_string_tolower(name);

Expand Down Expand Up @@ -10853,7 +11000,18 @@ static void zend_compile_class_const(znode *result, zend_ast *ast) /* {{{ */
if (Z_TYPE_P(const_zv) == IS_STRING) {
zend_string *const_str = Z_STR_P(const_zv);
zend_string *resolved_name = zend_resolve_class_name_ast(class_ast);
if (zend_try_ct_eval_class_const(&result->u.constant, resolved_name, const_str)) {

// check to see if there is a class registered at resolved_name::const_str
zend_string *name = zend_string_concat3(
ZSTR_VAL(resolved_name), ZSTR_LEN(resolved_name),
"::", 2,
ZSTR_VAL(const_str), ZSTR_LEN(const_str));

zend_str_tolower(ZSTR_VAL(name), ZSTR_LEN(name));

zend_class_entry *ce = zend_lookup_class_ex(name, name, ZEND_FETCH_CLASS_NO_AUTOLOAD);
zend_string_release(name);
if (!ce && zend_try_ct_eval_class_const(&result->u.constant, resolved_name, const_str)) {
result->op_type = IS_CONST;
zend_string_release_ex(resolved_name, 0);
return;
Expand Down Expand Up @@ -11591,6 +11749,9 @@ static void zend_compile_expr_inner(znode *result, zend_ast *ast) /* {{{ */
case ZEND_AST_PARENT_PROPERTY_HOOK_CALL:
zend_compile_var(result, ast, BP_VAR_R, 0);
return;
case ZEND_AST_INNER_CLASS:
zend_compile_var(result, ast, BP_VAR_INNER_CLASS, 0);
return;
case ZEND_AST_ASSIGN:
zend_compile_assign(result, ast);
return;
Expand Down Expand Up @@ -11737,6 +11898,7 @@ static zend_op *zend_compile_var_inner(znode *result, zend_ast *ast, uint32_t ty
case ZEND_AST_NULLSAFE_PROP:
return zend_compile_prop(result, ast, type, by_ref);
case ZEND_AST_STATIC_PROP:
case ZEND_AST_INNER_CLASS:
return zend_compile_static_prop(result, ast, type, by_ref, 0);
case ZEND_AST_CALL:
zend_compile_call(result, ast, type);
Expand Down
10 changes: 8 additions & 2 deletions Zend/zend_compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ typedef struct _zend_oparray_context {
/* or IS_CONSTANT_VISITED_MARK | | | */
#define ZEND_CLASS_CONST_IS_CASE (1 << 6) /* | | | X */
/* | | | */
/* Property Flags (unused: 13...) | | | */
/* Property Flags (unused: 14...) | | | */
/* =========== | | | */
/* | | | */
/* Promoted property / parameter | | | */
Expand All @@ -267,14 +267,18 @@ typedef struct _zend_oparray_context {
#define ZEND_ACC_PROTECTED_SET (1 << 11) /* | | X | */
#define ZEND_ACC_PRIVATE_SET (1 << 12) /* | | X | */
/* | | | */
/* Class Flags (unused: 30,31) | | | */
/* Property is a reference | | X | */
#define ZEND_ACC_INNER_CLASS_REFERENCE (1 << 13) /* | | X | */
/* | | | */
/* Class Flags (unused: 31) | | | */
/* =========== | | | */
/* | | | */
/* Special class types | | | */
#define ZEND_ACC_INTERFACE (1 << 0) /* X | | | */
#define ZEND_ACC_TRAIT (1 << 1) /* X | | | */
#define ZEND_ACC_ANON_CLASS (1 << 2) /* X | | | */
#define ZEND_ACC_ENUM (1 << 28) /* X | | | */
#define ZEND_ACC_SHORT_SYNTAX (1 << 30) /* X | | | */
/* | | | */
/* Class linked with parent, interfaces and traits | | | */
#define ZEND_ACC_LINKED (1 << 3) /* X | | | */
Expand Down Expand Up @@ -894,6 +898,7 @@ typedef enum {
ZEND_MODIFIER_TARGET_CONSTANT,
ZEND_MODIFIER_TARGET_CPP,
ZEND_MODIFIER_TARGET_PROPERTY_HOOK,
ZEND_MODIFIER_TARGET_INNER_CLASS,
} zend_modifier_target;

/* Used during AST construction */
Expand Down Expand Up @@ -1051,6 +1056,7 @@ ZEND_API zend_string *zend_type_to_string(zend_type type);
#define BP_VAR_IS 3
#define BP_VAR_FUNC_ARG 4
#define BP_VAR_UNSET 5
#define BP_VAR_INNER_CLASS 6

#define ZEND_INTERNAL_FUNCTION 1
#define ZEND_USER_FUNCTION 2
Expand Down
Loading
Loading