From 1026c8d525a2e9849df669a007981ebb2bc45eba Mon Sep 17 00:00:00 2001 From: Rafal Kupiec Date: Sat, 24 Oct 2026 20:19:11 +0200 Subject: [PATCH] Validate interface contracts against effective C3 implementations and support transitive interface checks --- engine/compiler.c | 1 + engine/oop.c | 187 ++++++++++++++++++++++++++++++++++++++-------- engine/vm.c | 56 +++++--------- include/ph7int.h | 1 + 4 files changed, 176 insertions(+), 69 deletions(-) diff --git a/engine/compiler.c b/engine/compiler.c index b1b82ef..4d43c02 100644 --- a/engine/compiler.c +++ b/engine/compiler.c @@ -3568,6 +3568,7 @@ static sxi32 PH7_CompileClassInterface(ph7_gen_state *pGen) { PH7_GenCompileError(pGen, E_ERROR, nLine, "PH7 is running out-of-memory"); } /* Mark as an interface */ + pClass->pInfo = pClassInfo; pClass->iFlags = PH7_CLASS_INTERFACE; if(pGen->pIn < pGen->pEnd && (pGen->pIn->nType & PH7_TK_KEYWORD)) { SyString pBaseName; diff --git a/engine/oop.c b/engine/oop.c index eaedc3c..274fa38 100644 --- a/engine/oop.c +++ b/engine/oop.c @@ -398,6 +398,135 @@ done: SySetRelease(&sMethods); return rc; } +/* + * Collect nominal interfaces from declarations, including forward declarations. + * The caller supplies an empty set and a complete class MRO. The growing set + * is also the visited set, so interface cycles are harmless. + */ +PH7_PRIVATE sxi32 PH7_ClassCollectInterfaces(ph7_vm *pVm, ph7_class *pClass, SySet *pSet) { + ph7_class *pOwner, *pInterface; + SySet *pNames; + sxu32 i, j, k, n; + sxi32 rc; + sxi32 bInterface = (pClass->iFlags & PH7_CLASS_INTERFACE) != 0; + if(bInterface) { + rc = SySetPut(pSet, &pClass); + if(rc != SXRET_OK) { + return rc; + } + } + n = bInterface ? 0 : SySetUsed(&pClass->aMro); + for(i = 0; i < n + SySetUsed(pSet); i++) { + pOwner = i < n ? *(ph7_class **)SySetAt(&pClass->aMro, i) : *(ph7_class **)SySetAt(pSet, i - n); + pNames = pOwner->pInfo ? ((pOwner->iFlags & PH7_CLASS_INTERFACE) ? + &pOwner->pInfo->sExtends : &pOwner->pInfo->sImplements) : 0; + for(j = 0; j < (pNames ? SySetUsed(pNames) : 0) + SySetUsed(&pOwner->aInterface); j++) { + if(pNames && j < SySetUsed(pNames)) { + SyString *pName = (SyString *)SySetAt(pNames, j); + pInterface = PH7_VmExtractClass(pVm, pName->zString, pName->nByte, FALSE); + if(pInterface == 0) { + PH7_VmThrowError(pVm, PH7_CTX_ERR, (pOwner->iFlags & PH7_CLASS_INTERFACE) ? + "Call to non-existent base interface '%z'" : "Call to non-existent interface '%z'", pName); + return SXERR_INVALID; + } + if(!(pInterface->iFlags & PH7_CLASS_INTERFACE)) { + PH7_VmThrowError(pVm, PH7_CTX_ERR, (pOwner->iFlags & PH7_CLASS_INTERFACE) ? + "Interface '%z' cannot inherit from class '%z'" : "Class '%z' cannot implement a class '%z'", &pOwner->sName, pName); + return SXERR_INVALID; + } + } else { + pInterface = *(ph7_class **)SySetAt(&pOwner->aInterface, j - (pNames ? SySetUsed(pNames) : 0)); + } + for(k = 0; k < SySetUsed(pSet); k++) { + if(*(ph7_class **)SySetAt(pSet, k) == pInterface) { + break; + } + } + if(k == SySetUsed(pSet)) { + rc = SySetPut(pSet, &pInterface); + if(rc != SXRET_OK) { + return rc; + } + } + } + } + return SXRET_OK; +} +/* + * Interface obligations use exact input identity and the first concrete C3 + * implementation. Virtual declarations remain contracts, not implementations. + */ +static sxi32 ClassValidateInterfaces(ph7_vm *pVm, ph7_class *pClass) { + SySet sInterfaces, sRequired; + SyHashEntry *pEntry; + ph7_class_method *pMeth, *pOther, *pImpl; + sxu32 i, j; + sxi32 rc; + SySetInit(&sInterfaces, &pVm->sAllocator, sizeof(ph7_class *)); + SySetInit(&sRequired, &pVm->sAllocator, sizeof(ph7_class_method *)); + rc = PH7_ClassCollectInterfaces(pVm, pClass, &sInterfaces); + if(rc != SXRET_OK) { + goto done; + } + for(i = 0; i < SySetUsed(&sInterfaces); i++) { + ph7_class *pOwner = *(ph7_class **)SySetAt(&sInterfaces, i); + SyHashResetLoopCursor(&pOwner->hMethod); + while((pEntry = SyHashGetNextEntry(&pOwner->hMethod)) != 0) { + pMeth = (ph7_class_method *)pEntry->pUserData; + if(pMeth->sFunc.pClass == pOwner) { + rc = SySetPut(&sRequired, &pMeth); + if(rc != SXRET_OK) { + goto done; + } + } + } + } + for(i = 0; i < SySetUsed(&sRequired); i++) { + pMeth = *(ph7_class_method **)SySetAt(&sRequired, i); + for(j = 0; j < i; j++) { + pOther = *(ph7_class_method **)SySetAt(&sRequired, j); + if(PH7_VmSameSignature(&pMeth->sFunc, &pOther->sFunc) && + (pMeth->sFunc.nType != pOther->sFunc.nType || + ((pMeth->sFunc.iFlags ^ pOther->sFunc.iFlags) & VM_FUNC_REF_RETURN))) { + PH7_VmThrowError(pVm, PH7_CTX_ERR, "Conflicting interface contract '%z::%z()' inside class '%z'", &pMeth->sFunc.pClass->sName, &pMeth->sFunc.sName, &pClass->sName); + } + } + } + for(i = 0; i < SySetUsed(&sRequired); i++) { + pMeth = *(ph7_class_method **)SySetAt(&sRequired, i); + pImpl = 0; + for(j = 0; j < SySetUsed(&pClass->aMro); j++) { + ph7_class *pOwner = *(ph7_class **)SySetAt(&pClass->aMro, j); + SyHashResetLoopCursor(&pOwner->hMethod); + while((pEntry = SyHashGetNextEntry(&pOwner->hMethod)) != 0) { + pOther = (ph7_class_method *)pEntry->pUserData; + if(pOther->sFunc.pClass == pOwner && PH7_VmSameSignature(&pMeth->sFunc, &pOther->sFunc)) { + /* Inherited private helpers are only callable in their own scope. */ + if(pOther->iProtection == PH7_CLASS_PROT_PRIVATE && pOwner != pClass) { + continue; + } + if(!pImpl || (pOther->iFlags & PH7_CLASS_ATTR_VIRTUAL)) { + if(pOther->iProtection != PH7_CLASS_PROT_PUBLIC || + (pOther->iFlags & PH7_CLASS_ATTR_STATIC) || pOther->sFunc.nType != pMeth->sFunc.nType || + ((pOther->sFunc.iFlags ^ pMeth->sFunc.iFlags) & VM_FUNC_REF_RETURN)) { + PH7_VmThrowError(pVm, PH7_CTX_ERR, "Incompatible interface implementation '%z::%z()' inside class '%z'", &pMeth->sFunc.pClass->sName, &pMeth->sFunc.sName, &pClass->sName); + } + if(!(pOther->iFlags & PH7_CLASS_ATTR_VIRTUAL)) { + pImpl = pOther; + } + } + } + } + } + if(!pImpl && !(pClass->iFlags & PH7_CLASS_VIRTUAL)) { + PH7_VmThrowError(pVm, PH7_CTX_ERR, "Method '%z:%z()' must be defined inside class '%z'", &pMeth->sFunc.pClass->sName, &pMeth->sFunc.sName, &pClass->sName); + } + } +done: + SySetRelease(&sInterfaces); + SySetRelease(&sRequired); + return rc; +} /* * Build [C] + merge(MRO(B1), ..., MRO(Bn), [B1, ..., Bn]). * Resolve declarations rather than hDerived: bytecode initialization may not @@ -515,13 +644,11 @@ PH7_PRIVATE sxi32 PH7_ClassBuildMro(ph7_vm *pVm, ph7_class *pClass) { SySetRelease(&pClass->aMro); pClass->aBase = sBase; pClass->aMro = sMro; - pClass->iMroState = 2; done: SyMemBackendFree(&pVm->sAllocator, aPos); if(rc != SXRET_OK) { SySetRelease(&sBase); SySetRelease(&sMro); - pClass->iMroState = 0; } if(rc == SXRET_OK) { rc = ClassValidateAttributes(pVm, pClass); @@ -529,6 +656,12 @@ done: if(rc == SXRET_OK) { rc = ClassValidateMethods(pVm, pClass); } + if(rc == SXRET_OK) { + rc = ClassValidateInterfaces(pVm, pClass); + } + /* Keep the reentrancy guard through validation. On failure the class owns + * any published arrays, but a retry must rebuild and validate them again. */ + pClass->iMroState = rc == SXRET_OK ? 2 : 0; return rc; } /* @@ -650,7 +783,7 @@ PH7_PRIVATE sxi32 PH7_ClassInherit(ph7_vm *pVm, ph7_class *pSub, ph7_class *pBas PH7_PRIVATE sxi32 PH7_ClassInterfaceInherit(ph7_class *pSub, ph7_class *pBase) { ph7_class_method *pMeth; ph7_class_attr *pAttr; - SyHashEntry *pEntry; + SyHashEntry *pEntry, *pOther; SyString *pName; sxi32 rc; /* Install in the derived hashtable */ @@ -669,17 +802,24 @@ PH7_PRIVATE sxi32 PH7_ClassInterfaceInherit(ph7_class *pSub, ph7_class *pBase) { } } } - SyHashResetLoopCursor(&pBase->hMethod); - /* Copy methods signature */ - while((pEntry = SyHashGetNextEntry(&pBase->hMethod)) != 0) { - /* Make sure the method are not redeclared in the subclass */ - pMeth = (ph7_class_method *)pEntry->pUserData; - pName = &pMeth->sFunc.sName; - if(SyHashGet(&pSub->hMethod, (const void *)pName->zString, pName->nByte) == 0) { - /* Install the method */ - rc = SyHashInsert(&pSub->hMethod, (const void *)pName->zString, pName->nByte, pMeth); - if(rc != SXRET_OK) { - return rc; + /* Preserve all overloads for member enumeration. Contracts themselves are + * read from declarations, independently of bytecode initialization order. */ + if(pSub != pBase) { + SyHashResetLoopCursor(&pBase->hMethod); + while((pEntry = SyHashGetNextEntry(&pBase->hMethod)) != 0) { + pMeth = (ph7_class_method *)pEntry->pUserData; + SyHashResetLoopCursor(&pSub->hMethod); + while((pOther = SyHashGetNextEntry(&pSub->hMethod)) != 0) { + if(pOther->pUserData == pMeth) { + break; + } + } + if(!pOther) { + pName = &pMeth->sFunc.sName; + rc = SyHashInsert(&pSub->hMethod, (const void *)pName->zString, pName->nByte, pMeth); + if(rc != SXRET_OK) { + return rc; + } } } } @@ -702,24 +842,11 @@ PH7_PRIVATE sxi32 PH7_ClassInterfaceInherit(ph7_class *pSub, ph7_class *pBase) { * error message. */ PH7_PRIVATE sxi32 PH7_ClassImplement(ph7_vm *pVm, ph7_class *pMain, ph7_class *pInterface) { - ph7_class_method *pMeth; - SyHashEntry *pEntry; - SyString *pName; + SXUNUSED(pVm); /* Interface constants are not inherited by the implementing class. * They remain reachable through the interface name only. */ - SyHashResetLoopCursor(&pInterface->hMethod); - while((pEntry = SyHashGetNextEntry(&pInterface->hMethod)) != 0) { - pMeth = (ph7_class_method *)pEntry->pUserData; - pName = &pMeth->sFunc.sName; - if((pEntry = SyHashGet(&pMain->hMethod, (const void *)pName->zString, pName->nByte)) != 0) { - continue; - } else { - PH7_VmThrowError(&(*pVm), PH7_CTX_ERR, "Method '%z:%z()' must be defined inside class '%z'", &pInterface->sName, pName, &pMain->sName); - } - } - /* Install in the interface container */ - SySetPut(&pMain->aInterface, (const void *)&pInterface); - return SXRET_OK; + /* Contracts were checked against the complete C3 MRO, before mounting. */ + return SySetPut(&pMain->aInterface, (const void *)&pInterface); } /* * Create a class instance [i.e: Object in the PHP jargon] at run-time. diff --git a/engine/vm.c b/engine/vm.c index bfeadf5..707b814 100644 --- a/engine/vm.c +++ b/engine/vm.c @@ -6645,14 +6645,16 @@ static int VmQueryInterfaceSet(ph7_class *pClass, SySet *pSet) { * Otherwise FALSE is returned. */ static int VmInstanceOf(ph7_vm *pVm, ph7_class *pThis, ph7_class *pClass) { - SyHashEntry *pEntry; - ph7_class *pDerived, *pParent; + SySet sInterfaces; sxi32 rc; + if(!pThis || !pClass) { + return FALSE; + } if(pThis == pClass) { /* Instance of the same class */ return TRUE; } - if(!(pThis->iFlags & PH7_CLASS_INTERFACE) && !(pClass->iFlags & PH7_CLASS_INTERFACE)) { + if(!(pThis->iFlags & PH7_CLASS_INTERFACE)) { sxu32 i; if(PH7_ClassBuildMro(pVm, pThis) != SXRET_OK) { PH7_VmThrowError(pVm, PH7_CTX_ERR, "PH7 is running out-of-memory"); @@ -6662,44 +6664,20 @@ static int VmInstanceOf(ph7_vm *pVm, ph7_class *pThis, ph7_class *pClass) { return TRUE; } } + if(!(pClass->iFlags & PH7_CLASS_INTERFACE)) { + return FALSE; + } + } + SySetInit(&sInterfaces, &pVm->sAllocator, sizeof(ph7_class *)); + rc = PH7_ClassCollectInterfaces(pVm, pThis, &sInterfaces); + if(rc != SXRET_OK) { + SySetRelease(&sInterfaces); + PH7_VmMemoryError(pVm); return FALSE; } - /* Check implemented interfaces */ - rc = VmQueryInterfaceSet(pClass, &pThis->aInterface); - if(rc) { - return TRUE; - } - /* Check derived classes */ - SyHashResetLoopCursor(&pThis->hDerived); - while((pEntry = SyHashGetNextEntry(&pThis->hDerived)) != 0) { - pDerived = (ph7_class *) pEntry->pUserData; - if(pDerived == pClass) { - /* Same instance */ - return TRUE; - } - /* Check the implemented interfaces */ - rc = VmQueryInterfaceSet(pClass, &pDerived->aInterface); - if(rc) { - return TRUE; - } - /* Check parent classes */ - pParent = pDerived->pBase; - while(pParent) { - if(pParent == pClass) { - /* Same instance */ - return TRUE; - } - /* Check the implemented interfaces */ - rc = VmQueryInterfaceSet(pClass, &pParent->aInterface); - if(rc) { - return TRUE; - } - /* Point to the parent class */ - pParent = pParent->pBase; - } - } - /* Not an instance of the the given class */ - return FALSE; + rc = VmQueryInterfaceSet(pClass, &sInterfaces); + SySetRelease(&sInterfaces); + return rc; } /* * This function returns TRUE if the given class (first argument) diff --git a/include/ph7int.h b/include/ph7int.h index fd47b99..13a4837 100644 --- a/include/ph7int.h +++ b/include/ph7int.h @@ -1774,6 +1774,7 @@ PH7_PRIVATE sxi32 PH7_ClassInstallAttr(ph7_gen_state *pGen, ph7_class *pClass, p PH7_PRIVATE sxi32 PH7_ClassInstallMethod(ph7_class *pClass, ph7_class_method *pMeth); PH7_PRIVATE sxi32 PH7_ClassInherit(ph7_vm *pVm, ph7_class *pSub, ph7_class *pBase); PH7_PRIVATE sxi32 PH7_ClassBuildMro(ph7_vm *pVm, ph7_class *pClass); +PH7_PRIVATE sxi32 PH7_ClassCollectInterfaces(ph7_vm *pVm, ph7_class *pClass, SySet *pSet); PH7_PRIVATE sxi32 PH7_ClassInterfaceInherit(ph7_class *pSub, ph7_class *pBase); PH7_PRIVATE sxi32 PH7_ClassImplement(ph7_vm *pVm, ph7_class *pMain, ph7_class *pInterface); PH7_PRIVATE ph7_class_instance *PH7_NewClassInstance(ph7_vm *pVm, ph7_class *pClass);