diff --git a/compiler/data/class-member-modifiers.h b/compiler/data/class-member-modifiers.h index 99e976871d..690bd5e918 100644 --- a/compiler/data/class-member-modifiers.h +++ b/compiler/data/class-member-modifiers.h @@ -16,6 +16,7 @@ enum class AbstractModifiers : uint8_t { not_modifier_, final_, abstract_, + readonly_, }; enum class AccessModifiers : uint8_t { diff --git a/compiler/data/class-members.cpp b/compiler/data/class-members.cpp index 11229413bd..77d2c0f899 100644 --- a/compiler/data/class-members.cpp +++ b/compiler/data/class-members.cpp @@ -103,7 +103,9 @@ ClassMemberInstanceField::ClassMemberInstanceField(ClassPtr klass, VertexAdaptor root->var_id = var; var->init_val = def_val; var->class_id = klass; - var->marked_as_const = klass->is_immutable || phpdoc_tag_exists(phpdoc_str, php_doc_tag::kphp_const); + var->marked_as_const = klass->is_immutable || + modifiers.is_readonly() || + phpdoc_tag_exists(phpdoc_str, php_doc_tag::kphp_const); } const TypeData *ClassMemberInstanceField::get_inferred_type() const { diff --git a/compiler/data/field-modifiers.h b/compiler/data/field-modifiers.h index 6b1841c1c5..70e8e2c492 100644 --- a/compiler/data/field-modifiers.h +++ b/compiler/data/field-modifiers.h @@ -12,39 +12,68 @@ class FieldModifiers { public: FieldModifiers() = default; + explicit FieldModifiers(AccessModifiers access_modifiers) : access_modifier_(access_modifiers) { } + FieldModifiers(AccessModifiers access_modifiers, AbstractModifiers abstract_modifiers) : + abstract_modifier_(abstract_modifiers), access_modifier_(access_modifiers) { + } + FieldModifiers &set_public() { - kphp_error(!is_private() && !is_protected(), "Mupliple access modifiers (e.g. public and private at the same time) are not allowed"); + kphp_error(!is_private() && !is_protected(), "Multiple access modifiers (e.g. public and private at the same time) are not allowed"); access_modifier_ = AccessModifiers::public_; return *this; } FieldModifiers &set_private() { - kphp_error(!is_public() && !is_protected(), "Mupliple access modifiers (e.g. public and private at the same time) are not allowed"); + kphp_error(!is_public() && !is_protected(), "Multiple access modifiers (e.g. public and private at the same time) are not allowed"); access_modifier_ = AccessModifiers::private_; return *this; } FieldModifiers &set_protected() { - kphp_error(!is_private() && !is_public(), "Mupliple access modifiers (e.g. public and private at the same time) are not allowed"); + kphp_error(!is_private() && !is_public(), "Multiple access modifiers (e.g. public and private at the same time) are not allowed"); access_modifier_ = AccessModifiers::protected_; return *this; } - bool is_public() const { return access_modifier_ == AccessModifiers::public_; } - bool is_private() const { return access_modifier_ == AccessModifiers::private_; } - bool is_protected() const { return access_modifier_ == AccessModifiers::protected_; } + FieldModifiers &set_readonly() { + abstract_modifier_ = AbstractModifiers::readonly_; + return *this; + } + + bool is_public() const { return access_modifier_ == AccessModifiers::public_; } + bool is_private() const { return access_modifier_ == AccessModifiers::private_; } + bool is_protected() const { return access_modifier_ == AccessModifiers::protected_; } + bool is_readonly() const { return abstract_modifier_ == AbstractModifiers::readonly_; } std::string to_string() const { + std::string res; switch (access_modifier_) { - case AccessModifiers::public_ : return "public"; - case AccessModifiers::private_ : return "private"; - case AccessModifiers::protected_: return "protected"; - default: return ""; + case AccessModifiers::public_: + res = "public"; + break; + case AccessModifiers::private_ : + res = "private"; + break; + case AccessModifiers::protected_: + res = "protected"; + break; + default: + break; + } + + if (is_readonly()) { + if (res.empty()) { + res += "readonly"; + } else { + res += " readonly"; + } } + + return res; } friend bool operator==(const FieldModifiers &lhs, const FieldModifiers &rhs) { @@ -52,5 +81,6 @@ class FieldModifiers { } private: + AbstractModifiers abstract_modifier_{AbstractModifiers ::not_modifier_}; AccessModifiers access_modifier_{AccessModifiers ::not_modifier_}; }; diff --git a/compiler/data/function-modifiers.h b/compiler/data/function-modifiers.h index 730f430476..07e99522be 100644 --- a/compiler/data/function-modifiers.h +++ b/compiler/data/function-modifiers.h @@ -28,17 +28,17 @@ class FunctionModifiers { } void set_public() { - kphp_error(!is_private() && !is_protected(), "Mupliple access modifiers (e.g. public and private at the same time) are not allowed"); + kphp_error(!is_private() && !is_protected(), "Multiply access modifiers (e.g. public and private at the same time) are not allowed"); access_modifier_ = AccessModifiers::public_; } void set_private() { - kphp_error(!is_public() && !is_protected(), "Mupliple access modifiers (e.g. public and private at the same time) are not allowed"); + kphp_error(!is_public() && !is_protected(), "Multiply access modifiers (e.g. public and private at the same time) are not allowed"); access_modifier_ = AccessModifiers::private_; } void set_protected() { - kphp_error(!is_private() && !is_public(), "Mupliple access modifiers (e.g. public and private at the same time) are not allowed"); + kphp_error(!is_private() && !is_public(), "Multiply access modifiers (e.g. public and private at the same time) are not allowed"); access_modifier_ = AccessModifiers::protected_; } @@ -52,6 +52,11 @@ class FunctionModifiers { abstract_modifier_ = AbstractModifiers::abstract_; } + void set_readonly() { + kphp_error(!is_readonly(), "Several readonly modifiers are not allowed"); + abstract_modifier_ = AbstractModifiers::readonly_; + } + bool is_static() const { return scope_modifier_ == ScopeModifiers::static_; } bool is_instance() const { return scope_modifier_ == ScopeModifiers::instance_; } bool is_nonmember() const { return !is_static() && !is_instance(); } @@ -60,8 +65,9 @@ class FunctionModifiers { bool is_private() const { return access_modifier_ == AccessModifiers::private_; } bool is_protected() const { return access_modifier_ == AccessModifiers::protected_; } - bool is_final() const { return abstract_modifier_ == AbstractModifiers::final_; } - bool is_abstract() const { return abstract_modifier_ == AbstractModifiers::abstract_; } + bool is_final() const { return abstract_modifier_ == AbstractModifiers::final_; } + bool is_abstract() const { return abstract_modifier_ == AbstractModifiers::abstract_; } + bool is_readonly() const { return abstract_modifier_ == AbstractModifiers::readonly_; } std::string to_string() const { std::string res; @@ -71,6 +77,7 @@ class FunctionModifiers { if (is_public()) res += " public"; if (is_private()) res += " private"; if (is_protected()) res += " protected"; + if (is_readonly()) res += " readonly"; if (is_static()) res += " static"; if (is_instance()) res += " instance"; @@ -83,6 +90,10 @@ class FunctionModifiers { return access_modifier_; } + AbstractModifiers abstract_modifiers() const { + return abstract_modifier_; + } + static FunctionModifiers instance_public() { FunctionModifiers res; res.set_instance(); diff --git a/compiler/debug.cpp b/compiler/debug.cpp index 0164a5f4d3..b1f584ef7e 100644 --- a/compiler/debug.cpp +++ b/compiler/debug.cpp @@ -81,6 +81,7 @@ std::string debugTokenName(TokenType t) { {tok_static, "tok_static"}, {tok_final, "tok_final"}, {tok_abstract, "tok_abstract"}, + {tok_readonly, "tok_readonly"}, {tok_goto, "tok_goto"}, {tok_isset, "tok_isset"}, {tok_declare, "tok_declare"}, diff --git a/compiler/gentree.cpp b/compiler/gentree.cpp index aebdcd0ddc..bc6ca40946 100644 --- a/compiler/gentree.cpp +++ b/compiler/gentree.cpp @@ -1438,6 +1438,9 @@ ClassMemberModifiers GenTree::parse_class_member_modifier_mask() { case tok_abstract: modifiers.set_abstract(); break; + case tok_readonly: + modifiers.set_readonly(); + break; default: return modifiers; } @@ -1482,9 +1485,9 @@ VertexPtr GenTree::get_class_member(vk::string_view phpdoc_str) { kphp_error(!cur_class->is_interface(), "Interfaces may not include member variables"); kphp_error(!modifiers.is_final() && !modifiers.is_abstract(), "Class fields can not be declared final/abstract"); if (modifiers.is_static()) { - return get_static_field_list(phpdoc_str, FieldModifiers{modifiers.access_modifier()}, type_hint); + return get_static_field_list(phpdoc_str, FieldModifiers{modifiers.access_modifier(), modifiers.abstract_modifiers()}, type_hint); } - get_instance_var_list(phpdoc_str, FieldModifiers{modifiers.access_modifier()}, type_hint); + get_instance_var_list(phpdoc_str, FieldModifiers{modifiers.access_modifier(), modifiers.abstract_modifiers()}, type_hint); return {}; } @@ -2078,6 +2081,7 @@ VertexPtr GenTree::get_statement(vk::string_view phpdoc_str) { return get_const_after_explicit_access_modifier(access); } // fall through + case tok_readonly: case tok_final: case tok_abstract: if (cur_function->type == FunctionData::func_class_holder) { diff --git a/compiler/keywords.gperf b/compiler/keywords.gperf index 2e48d8470a..c6f4db7425 100644 --- a/compiler/keywords.gperf +++ b/compiler/keywords.gperf @@ -39,6 +39,7 @@ global, tok_global static, tok_static final, tok_final abstract, tok_abstract +readonly, tok_readonly goto, tok_goto return, tok_return list, tok_list diff --git a/compiler/pipes/sort-and-inherit-classes.cpp b/compiler/pipes/sort-and-inherit-classes.cpp index cd511830d2..1f685f50ce 100644 --- a/compiler/pipes/sort-and-inherit-classes.cpp +++ b/compiler/pipes/sort-and-inherit-classes.cpp @@ -447,6 +447,21 @@ void SortAndInheritClassesF::check_on_finish(DataStream &os) { kphp_error(!c->parent_class->get_instance_method(m.local_name()), fmt_format("Cannot make non static method `{}` static", m.function->get_human_readable_name())); }); + + c->members.for_each([&c](const ClassMemberInstanceField &m) { + const auto *parent_field = c->parent_class->get_instance_field(m.local_name()); + if (parent_field) { + const auto is_readonly = m.modifiers.is_readonly(); + const auto parent_is_readonly = parent_field->modifiers.is_readonly(); + + if (is_readonly != parent_is_readonly) { + const char *current = is_readonly ? "readonly" : "writeable"; + const char *parent = parent_is_readonly ? "readonly" : "writeable"; + + kphp_error(0, fmt_format("Cannot redeclare {} {}::{} as {} {}::{}", parent, c->parent_class->get_name(), parent_field->local_name(), current, c->get_name(), m.local_name())); + } + } + }); } // For stable code generation of polymorphic classes body diff --git a/compiler/token.h b/compiler/token.h index fecd6473d6..19bad5d5ed 100644 --- a/compiler/token.h +++ b/compiler/token.h @@ -67,6 +67,7 @@ enum TokenType { tok_static, tok_final, tok_abstract, + tok_readonly, tok_goto, tok_isset, tok_declare, diff --git a/tests/cpp/compiler/lexer-test.cpp b/tests/cpp/compiler/lexer-test.cpp index 5a437b887a..0b65798192 100644 --- a/tests/cpp/compiler/lexer-test.cpp +++ b/tests/cpp/compiler/lexer-test.cpp @@ -113,6 +113,10 @@ TEST(lexer_test, test_php_tokens) { // combined tests {"echo \"{$x->y}\";", {"tok_echo(echo)", "tok_str_begin(\")", "tok_expr_begin({)", "tok_var_name($x)", "tok_arrow(->)", "tok_func_name(y)", "tok_expr_end(})", "tok_str_end(\")", "tok_semicolon(;)"}}, + + // readonly modifier + {"public readonly string $prop;", {"tok_public(public)", "tok_readonly(readonly)", "tok_string(string)", "tok_var_name($prop)", "tok_semicolon(;)"}}, + {"readonly $prop = 100;", {"tok_readonly(readonly)", "tok_var_name($prop)", "tok_eq1(=)", "tok_int_const(100)", "tok_semicolon(;)"}}, }; for (const auto &test : tests) { diff --git a/tests/phpt/php8/readonly_modifier/001_readonly_property.php b/tests/phpt/php8/readonly_modifier/001_readonly_property.php new file mode 100644 index 0000000000..63c74105c1 --- /dev/null +++ b/tests/phpt/php8/readonly_modifier/001_readonly_property.php @@ -0,0 +1,16 @@ +@kphp_should_fail +/Modification of const/ +prop = "world"; // ok + } +} + +$foo = new Foo(); +$foo->prop_readonly = "world"; // error +$foo->prop_default = "world"; // ok diff --git a/tests/phpt/php8/readonly_modifier/002_readonly_property_overriding.php b/tests/phpt/php8/readonly_modifier/002_readonly_property_overriding.php new file mode 100644 index 0000000000..4b0e23270d --- /dev/null +++ b/tests/phpt/php8/readonly_modifier/002_readonly_property_overriding.php @@ -0,0 +1,21 @@ +@kphp_should_fail +/You may not override field: `prop_readonly`, in class: `Boo`/ +/Cannot redeclare readonly Foo::prop_readonly as writeable Boo::prop_readonly/ +/Cannot redeclare writeable Goo::prop_readonly as readonly Doo::prop_readonly/ +