From c6e7151e7260bc399743cb3e0dafbe4cb9642865 Mon Sep 17 00:00:00 2001 From: Rafal Kupiec Date: Sat, 24 Oct 2026 19:20:52 +0200 Subject: [PATCH] Implement cooperative parent calls across runtime C3 MRO and enforce lifecycle method rules --- engine/compiler.c | 6 ++ engine/vm.c | 138 ++++++++++++++++++++++++-------------- tests/beaufort_cipher.aer | 2 +- 3 files changed, 96 insertions(+), 50 deletions(-) diff --git a/engine/compiler.c b/engine/compiler.c index 0c4ff39..b1b82ef 100644 --- a/engine/compiler.c +++ b/engine/compiler.c @@ -3437,6 +3437,12 @@ static sxi32 PH7_GenStateCompileClassMethod( /* Peek method name */ pName = &pGen->pIn->sData; nLine = pGen->pIn->nLine; + if(((pName->nByte == sizeof("__construct") - 1 && + SyMemcmp(pName->zString, "__construct", sizeof("__construct") - 1) == 0) || + (pName->nByte == sizeof("__destruct") - 1 && + SyMemcmp(pName->zString, "__destruct", sizeof("__destruct") - 1) == 0)) && nType != MEMOBJ_VOID) { + PH7_GenCompileError(pGen, E_ERROR, nLine, "Method '%z::%z()' must return void", &pClass->sName, pName); + } /* Jump the method name */ pGen->pIn++; if(iFlags & PH7_CLASS_ATTR_VIRTUAL) { diff --git a/engine/vm.c b/engine/vm.c index dd1781b..bfeadf5 100644 --- a/engine/vm.c +++ b/engine/vm.c @@ -811,6 +811,23 @@ static sxi32 VmExecIncludedFile(ph7_vm *pVm, SyString *pPath, int iFlags); static sxi32 VmLocalExec(ph7_vm *pVm, SySet *pByteCode, ph7_value *pResult); static sxi32 VmClassMemberAccess(ph7_vm *pVm, ph7_class *pClass, sxi32 iProtection); static ph7_class *VmMethodClass(ph7_vm *pVm); +static sxi32 VmCallClassMethod(ph7_vm *pVm, ph7_class_instance *pThis, ph7_class_method *pMethod, + ph7_value *pResult, int nArg, ph7_value **apArg, sxi32 bInternal); +/* Cooperative calls continue after the lexical owner in the receiver's C3 order. */ +static sxu32 VmMethodStart(ph7_vm *pVm, ph7_class *pLookup, sxi32 bParent) { + sxu32 i; + ph7_class *pScope; + if(!bParent) { + return 0; + } + pScope = VmMethodClass(pVm); + for(i = 0; i < SySetUsed(&pLookup->aMro); i++) { + if(*(ph7_class **)SySetAt(&pLookup->aMro, i) == pScope) { + return i + 1; + } + } + return SySetUsed(&pLookup->aMro); +} /* Find a mounted name to transport a method call, not an eligible overload. * The caller must retain its lookup root and receiver for VmOverload(). */ static ph7_class_method *VmMethodSeed(ph7_vm *pVm, ph7_class *pLookup, SyString *pName, @@ -819,7 +836,7 @@ static ph7_class_method *VmMethodSeed(ph7_vm *pVm, ph7_class *pLookup, SyString if(pPreferred && SyHashGet(&pVm->hFunction, pPreferred->sVmName.zString, pPreferred->sVmName.nByte)) { return pPreferred; } - for(i = bParent ? 1 : 0; i < SySetUsed(&pLookup->aMro); i++) { + for(i = VmMethodStart(pVm, pLookup, bParent); i < SySetUsed(&pLookup->aMro); i++) { ph7_class *pClass = *(ph7_class **)SySetAt(&pLookup->aMro, i); ph7_class_method *pMeth = PH7_ClassExtractMethod(pClass, pName->zString, pName->nByte); if(pMeth && pMeth->sFunc.pClass == pClass && @@ -994,7 +1011,7 @@ static ph7_vm_func *VmOverload( } /* Walk C3 without modifying any declaration's overload chain. Only * accessible declarations participate in identical-signature hiding. */ - for(i = bParent ? 1 : 0; i < SySetUsed(&pLookup->aMro); i++) { + for(i = VmMethodStart(pVm, pLookup, bParent); i < SySetUsed(&pLookup->aMro); i++) { ph7_class *pClass = *(ph7_class **)SySetAt(&pLookup->aMro, i); ph7_class_method *pMeth = PH7_ClassExtractMethod(pClass, pList->sName.zString, pList->sName.nByte); SyHashEntry *pEntry; @@ -2461,13 +2478,14 @@ static sxi32 VmByteCodeExec( if(pLastRef) { *pLastRef = pTos->nIdx; } - /* Ensure we are in active frame. Force abort all loops */ + /* Unwind local loops, but let VmExecExceptionBlock() release + * the handler without freeing its borrowed owner frames. */ if(pInstr->p3) { /* Called from a return statement */ - while(pVm->pFrame->pParent && (pVm->pFrame->iFlags & VM_FRAME_ACTIVE) == 0) { + while(pVm->pFrame->pParent && (pVm->pFrame->iFlags & (VM_FRAME_ACTIVE | VM_FRAME_CATCH | VM_FRAME_FINALLY)) == 0) { VmLeaveFrame(&(*pVm)); } } else if(pVm->pFrame->iFlags & VM_FRAME_LOOP) { - while(pVm->pFrame->pParent && (pVm->pFrame->iFlags & VM_FRAME_ACTIVE) == 0) { + while(pVm->pFrame->pParent && (pVm->pFrame->iFlags & (VM_FRAME_ACTIVE | VM_FRAME_CATCH | VM_FRAME_FINALLY)) == 0) { VmLeaveFrame(&(*pVm)); } } @@ -4696,8 +4714,8 @@ static sxi32 VmByteCodeExec( /* Point to the instantiated class */ pClass = pThis->pClass; pScope = VmMethodClass(pVm); - if(pNos->iFlags == MEMOBJ_PARENTOBJ) { - /* Parent lookup starts at the declaring class, not the receiver. */ + if(pNos->iFlags == MEMOBJ_PARENTOBJ && !pInstr->iP2) { + /* Preserve lexical parent lookup for fields. */ if(pScope) { pClass = pScope; } @@ -4987,7 +5005,7 @@ static sxi32 VmByteCodeExec( SySetPut(&aArg, (const void *)&pArg); pArg++; } - PH7_VmCallClassMethod(&(*pVm), pNew, pCons, 0, (int)SySetUsed(&aArg), (ph7_value **)SySetBasePtr(&aArg)); + VmCallClassMethod(&(*pVm), pNew, pCons, 0, (int)SySetUsed(&aArg), (ph7_value **)SySetBasePtr(&aArg), FALSE); /* TICKET 1433-52: Unsetting $this in the constructor body */ if(pNew->iRef < 1) { pNew->iRef = 1; @@ -5156,8 +5174,7 @@ static sxi32 VmByteCodeExec( /* Instance already loaded */ pThis = (ph7_class_instance *)pTarget->x.pOther; pLookup = pThis->pClass; - if(pTarget->iFlags == MEMOBJ_PARENTOBJ && VmMethodClass(pVm)) { - pLookup = VmMethodClass(pVm); + if(pTarget->iFlags == MEMOBJ_PARENTOBJ) { bParent = 1; } pThis->iRef += 2; @@ -6775,13 +6792,14 @@ static int vm_builtin_is_subclass_of(ph7_context *pCtx, int nArg, ph7_value **ap * Return SXRET_OK if the method was successfully called.Any other * return value indicates failure. */ -PH7_PRIVATE sxi32 PH7_VmCallClassMethod( +static sxi32 VmCallClassMethod( ph7_vm *pVm, /* Target VM */ ph7_class_instance *pThis, /* Target class instance [i.e: Object in the PHP jargon]*/ ph7_class_method *pMethod, /* Method name */ ph7_value *pResult, /* Store method return value here. NULL otherwise */ int nArg, /* Total number of given arguments */ - ph7_value **apArg /* Method arguments */ + ph7_value **apArg, /* Method arguments */ + sxi32 bInternal /* VM-owned lifecycle call, not source new */ ) { ph7_value *aStack; VmInstr aInstr[2]; @@ -6804,7 +6822,7 @@ PH7_PRIVATE sxi32 PH7_VmCallClassMethod( aStack[i].nIdx = apArg[i]->nIdx; } iCursor = nArg + 1; - iEntry = 2; /* VM-owned method call: preserve access to lifecycle methods. */ + iEntry = bInternal ? 2 : 0; if(pThis) { /* * Push the class instance so that the '$this' variable will be available. @@ -6812,7 +6830,7 @@ PH7_PRIVATE sxi32 PH7_VmCallClassMethod( pThis->iRef++; /* Increment reference count */ aStack[i].x.pOther = pThis; aStack[i].nType = MEMOBJ_OBJ; - if(SyStrncmp(pThis->pClass->sName.zString, "Program", 7) == 0) { + if(bInternal && SyStrncmp(pThis->pClass->sName.zString, "Program", 7) == 0) { if((SyStrncmp(pMethod->sFunc.sName.zString, "main", 4) == 0) || (SyStrncmp(pMethod->sFunc.sName.zString, "__construct", 11) == 0)) { /* Do not overload entry point */ iEntry |= 1; @@ -6860,6 +6878,10 @@ PH7_PRIVATE sxi32 PH7_VmCallClassMethod( SyMemBackendFree(&pVm->sAllocator, aStack); return PH7_OK; } +PH7_PRIVATE sxi32 PH7_VmCallClassMethod(ph7_vm *pVm, ph7_class_instance *pThis, ph7_class_method *pMethod, + ph7_value *pResult, int nArg, ph7_value **apArg) { + return VmCallClassMethod(pVm, pThis, pMethod, pResult, nArg, apArg, TRUE); +} /* * Call a user defined or foreign function where the name of the function * is stored in the pFunc parameter and the given arguments are stored @@ -8400,27 +8422,61 @@ static int vm_builtin_debug_backtrace(ph7_context *pCtx, int nArg, ph7_value **a */ return PH7_OK; } +/* Handlers run synchronously before the throwing calls unwind. Borrow their + * owner's context without releasing the suspended calls or overwriting their + * self stack slots when a handler invokes another method. */ +static sxi32 VmExecExceptionBlock(ph7_vm *pVm, ph7_exception *pException, + ph7_exception_block *pCatch, ph7_class_instance *pThis) { + VmFrame *pSavedFrame = pVm->pFrame; + VmFrame *pOwner = pException->pFrame; + VmFrame *pFrame; + SySet sSelf, sSavedSelf; + sxu32 nSelf = SySetUsed(&pVm->aSelf), i; + sxi32 rc; + for(pFrame = pSavedFrame; pFrame != pOwner; pFrame = pFrame->pParent) { + ph7_vm_func *pFunc = (ph7_vm_func *)pFrame->pUserData; + if((pFrame->iFlags & VM_FRAME_ACTIVE) && pFunc && (pFunc->iFlags & VM_FUNC_CLASS_METHOD)) { + nSelf--; + } + } + SySetInit(&sSelf, &pVm->sAllocator, sizeof(ph7_class *)); + for(i = 0; i < nSelf; i++) { + if(SySetPut(&sSelf, SySetAt(&pVm->aSelf, i)) != SXRET_OK) { + SySetRelease(&sSelf); + return SXERR_MEM; + } + } + sSavedSelf = pVm->aSelf; + pVm->aSelf = sSelf; + pVm->pFrame = pOwner; + rc = VmEnterFrame(pVm, 0, 0, &pFrame); + if(rc == SXRET_OK) { + if(pCatch) { + ph7_value *pObj = VmCreateMemObj(pVm, &pCatch->sThis, FALSE); + pFrame->iFlags |= VM_FRAME_CATCH; + if(pObj) { + pThis->iRef++; + pObj->x.pOther = pThis; + MemObjSetType(pObj, MEMOBJ_OBJ); + } + } else { + pFrame->iFlags |= VM_FRAME_FINALLY; + } + rc = VmLocalExec(pVm, pCatch ? &pCatch->sByteCode : &pException->sFinally, 0); + while(pVm->pFrame != pOwner) { + VmLeaveFrame(pVm); + } + } + pVm->pFrame = pSavedFrame; + SySetRelease(&pVm->aSelf); + pVm->aSelf = sSavedSelf; + return rc; +} static sxi32 VmExecFinallyBlock( ph7_vm *pVm, /* Target VM */ ph7_exception *pException /* Exception thrown */ ) { - sxi32 rc; - VmFrame *pFrame = pVm->pFrame; - while(pFrame->pParent && (pFrame->iFlags & VM_FRAME_EXCEPTION)) { - /* Safely ignore the exception frame */ - pFrame = pFrame->pParent; - } - /* Create a private frame first */ - rc = VmEnterFrame(&(*pVm), 0, 0, &pFrame); - if(rc == SXRET_OK) { - /* Mark as 'finally' frame */ - pFrame->iFlags |= VM_FRAME_FINALLY; - /* Execute the block */ - rc = VmLocalExec(&(*pVm), &pException->sFinally, 0); - /* Leave the frame */ - VmLeaveFrame(&(*pVm)); - } - return rc; + return VmExecExceptionBlock(pVm, pException, 0, 0); } /* * The following routine is invoked by the engine when an uncaught @@ -8491,7 +8547,7 @@ static sxi32 VmThrowException( ph7_exception_block *pCatch; /* Catch block to execute */ ph7_exception **apException; ph7_exception *pException; - sxi32 rc, rcc, rcf; + sxi32 rcc, rcf; /* Point to the stack of loaded exceptions */ apException = (ph7_exception **)SySetBasePtr(&pVm->aException); pException = 0; @@ -8532,23 +8588,7 @@ static sxi32 VmThrowException( /* Tell the upper layer that the exception was caught */ pFrame->iFlags &= ~VM_FRAME_THROW; } - /* Create a private frame first */ - rc = VmEnterFrame(&(*pVm), 0, 0, &pFrame); - if(rc == SXRET_OK) { - /* Mark as catch frame */ - ph7_value *pObj = VmCreateMemObj(&(*pVm), &pCatch->sThis, FALSE); - pFrame->iFlags |= VM_FRAME_CATCH; - if(pObj) { - /* Install the exception instance */ - pThis->iRef++; /* Increment reference count */ - pObj->x.pOther = pThis; - MemObjSetType(pObj, MEMOBJ_OBJ); - } - /* Execute the block */ - rcc = VmLocalExec(&(*pVm), &pCatch->sByteCode, 0); - /* Leave the frame */ - VmLeaveFrame(&(*pVm)); - } + rcc = VmExecExceptionBlock(pVm, pException, pCatch, pThis); } /* Execute the 'finally' block if available */ if(pException && SySetUsed(&pException->sFinally)) { diff --git a/tests/beaufort_cipher.aer b/tests/beaufort_cipher.aer index 318a624..89bf2fa 100644 --- a/tests/beaufort_cipher.aer +++ b/tests/beaufort_cipher.aer @@ -1,7 +1,7 @@ class Beaufort { private string $cipher; - public string __construct(string $text, string $key) { + public void __construct(string $text, string $key) { string $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; for(int $i = 0; $i < strlen($text); $i++) { int $j;