Skip to content

fix(translator): covariant typed class constants and interface constant contracts - #58

Merged
matyhtf merged 2 commits into
swoole:masterfrom
AlessioGiacobbe:split/constant-type-contracts
Sep 2, 2026
Merged

fix(translator): covariant typed class constants and interface constant contracts#58
matyhtf merged 2 commits into
swoole:masterfrom
AlessioGiacobbe:split/constant-type-contracts

Conversation

@AlessioGiacobbe

Copy link
Copy Markdown
Contributor

Two related gaps in class-constant type contracts:

  • Typed class constants were forced invariant: const int X overriding const int|string X was rejected, while PHP 8.3 constants are covariant (narrowing legal, widening not — widening stays rejected). ConstantDef now records the declared type's accepted-types DNF (composite types were previously collapsed to one variant type) and the override check uses the covariance machinery.
  • Interface constants were never validated at all: overriding a final const, incompatible types, non-public visibility, and the same constant name arriving ambiguously from two interfaces (including via parent classes and enum implements) all compiled. An effective-constants table preserving the original declaring interface now enforces Zend's rules for classes, enums, and interface-extends-interface.

Verified against Zend 8.4.13, 32-case probe matrix.

Part of the split of #39.

checkConstantOverride() required exact type equality between a child
constant and the parent's declared type, rejecting valid PHP 8.3
programs: class constant types are covariant, so a child may narrow
(parent `const int|string X` overridden by `const int X`, or
`?int` by `int`) but never widen or move to an unrelated type
(Zend: "Type of B::X must be compatible with A::X of type int").

Composite declared types (unions, nullables) were also collapsed to a
single variant type at parse time, making them unrepresentable in the
check. ConstantDef now records the accepted-types DNF of its declared
type (built by the existing buildTypeCheckFromNode machinery in a
parseClassConstDef override, while the declaration's name-resolution
context is still active), and the override check reuses the DNF
clause-subtyping used for covariant returns. Untyped parent constants
remain unchecked, and a typed parent still requires a typed child.
Interface constants were never validated: checkInterfaceImplementation()
had no constants loop and checkConstantOverride() only walks the class
extends chain. Incompatible retypings, final-constant overrides,
narrowed visibility and ambiguous multi-interface inheritance were all
accepted (all fatal in Zend 8.4).

Model Zend's constants-table merge (zend_do_inheritance +
do_inherit_constant_check): a class-like's effective table is built from
the parent class's table (private constants are not inherited), its own
declarations, then its interfaces, each entry keeping the ORIGINAL
declaring class/interface. When a same-name constant arrives from a
different declaration:

  - a FINAL inherited constant cannot be overridden — "C::X cannot
    override final constant I::X" — including through an ancestor class
    that implemented the interface (the origin travels with the entry);
  - two different declarations are ambiguous unless the type declares
    the constant itself — "Class C inherits both I1::X and I2::X,
    which is ambiguous" (a diamond of one declaration is fine);
  - an override of an interface constant must stay public — "Access
    level to C::X must be public (as in interface I)";
  - a typed interface constant requires a typed, covariant override; an
    untyped one may be redefined freely.

The same validation runs for interfaces extending interfaces and for
enums implementing interfaces. Enum cases live in a separate table in
Zend and never conflict with inherited constants.

@matyhtf matyhtf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The constant covariance and effective-interface-constant table logic are consistent with PHP 8.4 behavior. I also checked direct/transitive final constants, ambiguity ordering, inherited class constants, trait-injected constants, typed narrowing, and integration with the current master; the focused tests pass and the branch merges cleanly.

Non-blocking follow-up: these new tests only exercise the compiler's dry generation path. A real native build of const_override_covariant.php currently fails because the existing gen_stub.php emits ZVAL_LONG(&const_N_value, ); for const ?int N = null; DNF/intersection class-constant types also hit the existing PropertyInfo assertion in VariableLike::getTypeCode(). Those are generator gaps outside this PR's modified contract-validation path and should be covered by end-to-end PHPTs separately.

@matyhtf
matyhtf merged commit fde9e3a into swoole:master Sep 2, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants