From 3c7f1b8f5b86d3076e651f2cbd422de74980a4e5 Mon Sep 17 00:00:00 2001 From: Rafal Kupiec Date: Fri, 23 Oct 2026 23:36:25 +0200 Subject: [PATCH] Fix overload resolution using argument compatibility and recursive array matching --- engine/vm.c | 507 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 414 insertions(+), 93 deletions(-) diff --git a/engine/vm.c b/engine/vm.c index d9f99b4..dcaeeb0 100644 --- a/engine/vm.c +++ b/engine/vm.c @@ -450,27 +450,338 @@ static void VmLeaveFrame(ph7_vm *pVm) { SyMemBackendPoolFree(&pVm->sAllocator, pFrame); } } +/* Forward declaration */ +static int VmInstanceOf(ph7_class *pThis, ph7_class *pClass); /* - * Compare two functions signature and return the comparison result. + * Ranks for general parameter types. Class and interface specificity uses + * the shortest inheritance distance instead, which is always lower than + * these values. */ -static int VmOverloadCompare(SyString *pFirst, SyString *pSecond) { - const char *zSend = &pSecond->zString[pSecond->nByte]; - const char *zFend = &pFirst->zString[pFirst->nByte]; - const char *zSin = pSecond->zString; - const char *zFin = pFirst->zString; - const char *zPtr = zFin; - for(;;) { - if(zFin >= zFend || zSin >= zSend) { - break; +#define VM_OVERLOAD_RANK_OBJECT (SXI32_HIGH - 2) +#define VM_OVERLOAD_RANK_MIXED (SXI32_HIGH - 1) +#define VM_OVERLOAD_RANK_UNKNOWN (SXI32_HIGH) +/* + * A single class reached while walking the inheritance/interface graph. + */ +typedef struct VmOverloadClassWalk VmOverloadClassWalk; +struct VmOverloadClassWalk { + ph7_class *pClass; /* Class or interface */ + int nDepth; /* Inheritance distance from the starting class */ +}; +/* + * Enqueue a class or interface for the overload specificity walk unless it + * has already been visited. + */ +static void VmOverloadEnqueueClass(ph7_vm *pVm, SySet *pQueue, SySet *pVisited, ph7_class *pClass, int nDepth) { + ph7_class **apVisited; + VmOverloadClassWalk sEntry; + sxu32 i; + apVisited = (ph7_class **)SySetBasePtr(pVisited); + for(i = 0 ; i < SySetUsed(pVisited) ; ++i) { + if(apVisited[i] == pClass) { + return; } - if(zFin[0] != zSin[0]) { - /* mismatch */ - break; - } - zFin++; - zSin++; } - return (int)(zFin - zPtr); + if(SySetPut(pVisited, &pClass) != SXRET_OK) { + PH7_VmMemoryError(&(*pVm)); + } + sEntry.pClass = pClass; + sEntry.nDepth = nDepth; + if(SySetPut(pQueue, &sEntry) != SXRET_OK) { + PH7_VmMemoryError(&(*pVm)); + } +} +/* + * Compute the shortest inheritance/interface distance between pClass and + * pHint. Return 0 for an exact class match, a positive distance for a base + * class or implemented interface and -1 if no relation exists. The walk is + * read-only. + */ +static int VmOverloadClassDistance(ph7_vm *pVm, ph7_class *pClass, ph7_class *pHint) { + SySet sQueue, sVisited; + VmOverloadClassWalk *pEntry; + ph7_class **apInterface, *pCur; + SyHashEntry *pHash; + sxu32 i, n; + int nDepth, nResult = -1; + if(pClass == pHint) { + return 0; + } + SySetInit(&sQueue, &pVm->sAllocator, sizeof(VmOverloadClassWalk)); + SySetInit(&sVisited, &pVm->sAllocator, sizeof(ph7_class *)); + VmOverloadEnqueueClass(&(*pVm), &sQueue, &sVisited, pClass, 0); + for(i = 0 ; i < SySetUsed(&sQueue) ; ++i) { + pEntry = (VmOverloadClassWalk *)SySetAt(&sQueue, i); + pCur = pEntry->pClass; + nDepth = pEntry->nDepth; + if(pCur == pHint) { + nResult = nDepth; + break; + } + /* Direct base classes and base interfaces */ + SyHashResetLoopCursor(&pCur->hDerived); + while((pHash = SyHashGetNextEntry(&pCur->hDerived)) != 0) { + VmOverloadEnqueueClass(&(*pVm), &sQueue, &sVisited, (ph7_class *)pHash->pUserData, nDepth + 1); + } + /* Implemented interfaces */ + apInterface = (ph7_class **)SySetBasePtr(&pCur->aInterface); + n = SySetUsed(&pCur->aInterface); + while(n > 0) { + --n; + VmOverloadEnqueueClass(&(*pVm), &sQueue, &sVisited, apInterface[n], nDepth + 1); + } + } + SySetRelease(&sQueue); + SySetRelease(&sVisited); + return nResult; +} +/* + * Rank a NULL value, or an empty object reference, against a formal + * parameter. NULL is compatible with every type, so the rank reflects how + * specific the formal parameter itself is. + */ +static int VmOverloadNullRank(ph7_vm *pVm, ph7_vm_func_arg *pFormal) { + sxu32 nFormalType = pFormal->nType; + SyHashEntry *pEntry; + if(nFormalType == SXU32_HIGH) { + /* Class or interface type hint */ + pEntry = SyHashGet(&pVm->hClass, (const void *)pFormal->sClass.zString, pFormal->sClass.nByte); + return pEntry != 0 ? 1 : 4; + } + if(nFormalType & MEMOBJ_MIXED) { + /* 'mixed' and 'mixed[]' are the least specific parameters */ + return 3; + } + if(nFormalType & MEMOBJ_HASHMAP) { + /* Typed array, including 'object[]' */ + return 1; + } + if(nFormalType & MEMOBJ_OBJ) { + /* The general 'object' keyword */ + return 2; + } + if(nFormalType == 0) { + /* No type hint at all */ + return 3; + } + /* Concrete scalar type */ + return 1; +} +/* + * Rank a scalar value against a formal parameter using the conversions + * allowed by PH7_CheckVarCompat(). The lower the returned rank, the more + * specific the match. A negative value means that the value is not + * compatible with the parameter. + */ +static int VmOverloadScalarRank(ph7_value *pArg, sxu32 nArgType, sxu32 nFormalType) { + if(nArgType == nFormalType) { + /* Exact type match */ + return 0; + } + if(nFormalType & MEMOBJ_REAL) { + if(nArgType & MEMOBJ_INT) { + /* INT to FLOAT */ + return 1; + } + } else if(nFormalType & MEMOBJ_CHAR) { + if(nArgType & MEMOBJ_INT) { + /* INT to CHAR */ + return 1; + } + if(nArgType & MEMOBJ_STRING) { + /* STRING to CHAR (only if not too long) */ + sxu32 nLen = SyBlobLength(&pArg->sBlob); + if(nLen == 0 || nLen == 1) { + return 1; + } + } + } else if(nFormalType & MEMOBJ_STRING) { + if(nArgType & MEMOBJ_CHAR) { + /* CHAR to STRING */ + return 1; + } + } else if(nFormalType & MEMOBJ_CALL) { + if(nArgType & MEMOBJ_STRING) { + /* STRING to CALLBACK */ + return 1; + } + } + return -1; +} +/* + * Rank an array argument against a typed array parameter. Entries are + * traversed through their insertion-order links (pFirst/pPrev), so the + * shared foreach loop cursor (pMap->pCur) is left untouched. An exact + * container type match is ranked before the walk while every other element is + * ranked against the formal element type, recursing into nested arrays just + * like the binding code does. Values are never cast or modified. NULL elements + * are compatible and do not affect the rank, so an empty array or an array + * made only of NULLs stays rank 1 unless the container type matches exactly. + */ +static int VmOverloadArrayRank(ph7_vm *pVm, ph7_value *pArg, sxu32 nFormalElem) { + ph7_hashmap *pMap; + ph7_hashmap_node *pNode; + ph7_value sValue; + sxu32 nArgElem, nCount; + int nRank, nCur; + if(nFormalElem == 0) { + /* Untyped array parameter accepts any value */ + return 1; + } + nArgElem = pArg->nType & ~MEMOBJ_HASHMAP; + if(nArgElem == nFormalElem) { + /* Container types match exactly, the binding code plain-stores it */ + return 0; + } + pMap = (ph7_hashmap *)pArg->x.pOther; + if(pMap == 0 || pMap->nEntry < 1) { + /* An empty array is compatible with any typed array */ + return 1; + } + nCount = 0; + nRank = 0; + for(pNode = pMap->pFirst ; pNode ; pNode = pNode->pPrev) { + PH7_MemObjInit(&(*pVm), &sValue); + PH7_HashmapExtractNodeValue(pNode, &sValue, FALSE); + if(sValue.nType & MEMOBJ_NULL) { + /* NULL matches any element type without raising the rank */ + PH7_MemObjRelease(&sValue); + continue; + } + if(sValue.nType & MEMOBJ_HASHMAP) { + /* Nested array, rank it recursively like the binding code does */ + nCur = VmOverloadArrayRank(&(*pVm), &sValue, nFormalElem); + } else { + nCur = VmOverloadScalarRank(&sValue, sValue.nType, nFormalElem); + } + if(nCur < 0) { + PH7_MemObjRelease(&sValue); + return -1; + } + if(nCur > nRank) { + nRank = nCur; + } + ++nCount; + PH7_MemObjRelease(&sValue); + } + return nCount > 0 ? nRank : 1; +} +/* + * Rank a single passed argument against a formal parameter for the purpose + * of overload resolution. Lower ranks denote a more specific match while a + * negative value means that the argument is not compatible with the given + * parameter. This routine is strictly read-only: it never casts, mutates or + * evaluates anything. + */ +static int VmOverloadArgRank(ph7_vm *pVm, ph7_value *pArg, ph7_vm_func_arg *pFormal) { + ph7_class_instance *pInstance; + ph7_class *pHint; + SyHashEntry *pEntry; + sxu32 nArgType, nFormalType, nFormalElem; + int nDistance; + nArgType = pArg->nType; + nFormalType = pFormal->nType; + if((nArgType & MEMOBJ_HASHMAP) == 0 && (nArgType & MEMOBJ_NULL)) { + /* Scalar NULL is compatible with every type */ + return VmOverloadNullRank(pVm, pFormal); + } + if(nFormalType == SXU32_HIGH) { + /* Class or interface type hint */ + if((nArgType & MEMOBJ_OBJ) == 0 || (nArgType & MEMOBJ_HASHMAP) != 0) { + /* Arrays use the same type encoding for 'Class[]' hints */ + return -1; + } + pInstance = (ph7_class_instance *)pArg->x.pOther; + if(pInstance == 0 || pInstance->pClass == 0) { + /* Empty object reference, NULL-compatible with any type */ + return VmOverloadNullRank(pVm, pFormal); + } + pEntry = SyHashGet(&pVm->hClass, (const void *)pFormal->sClass.zString, pFormal->sClass.nByte); + if(pEntry == 0) { + /* Unknown class hint. Binding would silently accept any object, + * so never let it outrank a general parameter type. */ + return VM_OVERLOAD_RANK_UNKNOWN; + } + pHint = (ph7_class *)pEntry->pUserData; + nDistance = VmOverloadClassDistance(&(*pVm), pInstance->pClass, pHint); + if(nDistance < 0) { + return -1; + } + return nDistance; + } + if(nFormalType & MEMOBJ_MIXED) { + if(((nFormalType & MEMOBJ_HASHMAP) != 0) != ((nArgType & MEMOBJ_HASHMAP) != 0)) { + /* 'mixed' does not accept arrays while 'mixed[]' expects one */ + return -1; + } + if(nFormalType & MEMOBJ_HASHMAP) { + /* 'mixed[]' is the least specific array parameter */ + return VM_OVERLOAD_RANK_MIXED; + } + /* 'mixed' is less specific than every other type */ + return (nArgType == nFormalType) ? 0 : VM_OVERLOAD_RANK_MIXED; + } + if(nFormalType & MEMOBJ_HASHMAP) { + if((nArgType & MEMOBJ_HASHMAP) == 0) { + return -1; + } + nFormalElem = nFormalType & ~MEMOBJ_HASHMAP; + return VmOverloadArrayRank(&(*pVm), pArg, nFormalElem); + } + if(nArgType & MEMOBJ_HASHMAP) { + /* Array cannot be passed to a scalar or object parameter */ + return -1; + } + if(nFormalType & MEMOBJ_OBJ) { + if((nArgType & MEMOBJ_OBJ) == 0) { + return -1; + } + pInstance = (ph7_class_instance *)pArg->x.pOther; + if(pInstance == 0 || pInstance->pClass == 0) { + /* Empty object reference, NULL-compatible with any type */ + return VmOverloadNullRank(pVm, pFormal); + } + /* 'object' is less specific than a concrete class or interface */ + return VM_OVERLOAD_RANK_OBJECT; + } + return VmOverloadScalarRank(pArg, nArgType, nFormalType); +} +/* + * Compute the per-argument ranks of a candidate function. Return TRUE if + * every passed argument is compatible with the corresponding parameter. + */ +static int VmOverloadRankArgs(ph7_vm *pVm, ph7_vm_func *pFunc, ph7_value *aArg, int nArg, int *aRank) { + ph7_vm_func_arg *aFormal; + int i, nRank; + aFormal = (ph7_vm_func_arg *)SySetBasePtr(&pFunc->aArgs); + for(i = 0 ; i < nArg ; i++) { + nRank = VmOverloadArgRank(&(*pVm), &aArg[i], &aFormal[i]); + if(nRank < 0) { + return FALSE; + } + aRank[i] = nRank; + } + return TRUE; +} +/* + * Return TRUE if the first candidate is a strictly better overload than the + * second one. When all argument ranks are equal, a candidate matching the + * exact number of passed arguments is preferred over one using defaults. + */ +static int VmOverloadDominates(const int *aFirst, const int *aSecond, int nArg, int bFirstExact, int bSecondExact) { + int i, bBetter = 0; + for(i = 0 ; i < nArg ; i++) { + if(aFirst[i] > aSecond[i]) { + return FALSE; + } + if(aFirst[i] < aSecond[i]) { + bBetter = 1; + } + } + if(bBetter) { + return TRUE; + } + return bFirstExact && !bSecondExact; } /* Forward declaration */ static sxi32 VmExecIncludedFile(ph7_vm *pVm, SyString *pPath, int iFlags); @@ -487,104 +798,115 @@ static ph7_vm_func *VmOverload( ph7_value *aArg, /* Array of passed arguments */ int nArg /* Total number of passed arguments */ ) { - int iTarget, i, j, iArgs, iCur, iMax; - ph7_vm_func *apSet[10]; /* Maximum number of candidates */ + int iTarget, j, k, iArgs, nCandidates, nCompatible, nCols, bDominated; + ph7_vm_func **apSet; ph7_vm_func *pLink; ph7_vm_func_arg *pFuncArg; - SyString sArgSig; - SyBlob sSig; + SySet sSet; + int *aRank, *pRank; + sxu32 nRankBytes; + SySetInit(&sSet, &pVm->sAllocator, sizeof(ph7_vm_func *)); pLink = pList; - i = 0; - /* Put functions expecting the same number of passed arguments */ - while(i < (int)SX_ARRAYSIZE(apSet)) { - if(pLink == 0) { - break; - } - iArgs = (int) SySetUsed(&pLink->aArgs); + /* Put functions whose arguments are compatible with the current call */ + while(pLink) { + iArgs = (int)SySetUsed(&pLink->aArgs); if(nArg == iArgs) { /* Exact amount of parameters, a candidate to call */ - apSet[i++] = pLink; } else if(nArg < iArgs) { - /* Fewer parameters passed, check if all are required */ - pFuncArg = (ph7_vm_func_arg *) SySetAt(&pLink->aArgs, nArg); - if(pFuncArg) { - if(SySetUsed(&pFuncArg->aByteCode) >= 1) { - /* First missing parameter has a compiled default value associated, a candidate to call */ - apSet[i++] = pLink; + /* Fewer parameters passed, check that every missing one has a default */ + pFuncArg = (ph7_vm_func_arg *)SySetBasePtr(&pLink->aArgs); + for(j = nArg ; j < iArgs ; ++j) { + if(SySetUsed(&pFuncArg[j].aByteCode) < 1) { + break; } } + if(j < iArgs) { + /* A missing parameter is required */ + pLink = pLink->pNextName; + continue; + } + } else { + /* Too many parameters passed */ + pLink = pLink->pNextName; + continue; + } + if(SySetPut(&sSet, &pLink) != SXRET_OK) { + PH7_VmMemoryError(&(*pVm)); } /* Point to the next entry */ pLink = pLink->pNextName; } - if(i < 1) { + nCandidates = (int)SySetUsed(&sSet); + if(nCandidates < 1) { /* No candidates, throw an error */ PH7_VmThrowError(&(*pVm), PH7_CTX_ERR, "Invalid number of arguments passed to function/method '%z()'", &pList->sName); } - if(nArg < 1 || i < 2) { + apSet = (ph7_vm_func **)SySetBasePtr(&sSet); + if(nCandidates < 2) { /* Return the only candidate */ - return apSet[0]; + pLink = apSet[0]; + SySetRelease(&sSet); + return pLink; } - /* Calculate function signature */ - SyBlobInit(&sSig, &pVm->sAllocator); - for(j = 0 ; j < nArg ; j++) { - int c = 'n'; /* null */ - if(aArg[j].nType & MEMOBJ_BOOL) { - /* Bool */ - c = 'b'; - } else if(aArg[j].nType & MEMOBJ_CALL) { - /* Callback */ - c = 'a'; - } else if(aArg[j].nType & MEMOBJ_CHAR) { - /* Char */ - c = 'c'; - } else if(aArg[j].nType & MEMOBJ_INT) { - /* Integer */ - c = 'i'; - } else if(aArg[j].nType & MEMOBJ_MIXED) { - /* Mixed */ - c = 'm'; - } else if(aArg[j].nType & MEMOBJ_OBJ) { - /* Class instance */ - ph7_class *pClass = ((ph7_class_instance *)aArg[j].x.pOther)->pClass; - SyString *pName = &pClass->sName; - SyBlobAppend(&sSig, (const void *)pName->zString, pName->nByte); - c = -1; - } else if(aArg[j].nType & MEMOBJ_REAL) { - /* Float */ - c = 'f'; - } else if(aArg[j].nType & MEMOBJ_RES) { - /* Resource */ - c = 'r'; - } else if(aArg[j].nType & MEMOBJ_STRING) { - /* String */ - c = 's'; - } else if(aArg[j].nType & MEMOBJ_VOID) { - /* Void */ - c = 'v'; - } - if(aArg[j].nType & MEMOBJ_HASHMAP && (aArg[j].nType & MEMOBJ_OBJ) == 0) { - c = SyToUpper(c); - } - if(c > 0) { - SyBlobAppend(&sSig, (const void *)&c, sizeof(char)); + /* Rank every argument against every candidate. The last column holds + * whether the candidate is compatible with all passed arguments. */ + nCols = nArg + 1; + if((sxu32)nCandidates > SXU32_HIGH / (sxu32)nCols / (sxu32)sizeof(int)) { + SySetRelease(&sSet); + PH7_VmMemoryError(&(*pVm)); + } + nRankBytes = (sxu32)nCandidates * (sxu32)nCols * (sxu32)sizeof(int); + aRank = (int *)SyMemBackendAlloc(&pVm->sAllocator, nRankBytes); + if(aRank == 0) { + PH7_VmMemoryError(&(*pVm)); + } + nCompatible = 0; + for(j = 0 ; j < nCandidates ; j++) { + pRank = &aRank[j * nCols]; + pRank[nArg] = VmOverloadRankArgs(&(*pVm), apSet[j], aArg, nArg, pRank); + if(pRank[nArg]) { + ++nCompatible; } } - SyStringInitFromBuf(&sArgSig, SyBlobData(&sSig), SyBlobLength(&sSig)); - iTarget = 0; - iMax = -1; - /* Select the appropriate function */ - for(j = 0 ; j < i ; j++) { - /* Compare the two signatures */ - iCur = VmOverloadCompare(&sArgSig, &apSet[j]->sSignature); - if(iCur > iMax) { - iMax = iCur; + if(nCompatible < 1) { + /* Candidates exist but none accepts the passed arguments */ + SyMemBackendFree(&pVm->sAllocator, aRank); + SySetRelease(&sSet); + PH7_VmThrowError(&(*pVm), PH7_CTX_ERR, "No matching overload found for function/method '%z()'", &pList->sName); + } + /* Select the unique candidate that is not dominated by another one */ + iTarget = -1; + for(j = 0 ; j < nCandidates ; j++) { + if(aRank[j * nCols + nArg] == 0) { + continue; + } + bDominated = 0; + for(k = 0 ; k < nCandidates ; k++) { + if(k == j || aRank[k * nCols + nArg] == 0) { + continue; + } + if(VmOverloadDominates(&aRank[k * nCols], &aRank[j * nCols], nArg, + SySetUsed(&apSet[k]->aArgs) == (sxu32)nArg, + SySetUsed(&apSet[j]->aArgs) == (sxu32)nArg)) { + bDominated = 1; + break; + } + } + if(!bDominated) { + if(iTarget >= 0) { + /* More than one candidate is equally good */ + SyMemBackendFree(&pVm->sAllocator, aRank); + SySetRelease(&sSet); + PH7_VmThrowError(&(*pVm), PH7_CTX_ERR, "Ambiguous call to overloaded function/method '%z()'", &pList->sName); + } iTarget = j; } } - SyBlobRelease(&sSig); + pLink = apSet[iTarget]; + SyMemBackendFree(&pVm->sAllocator, aRank); + SySetRelease(&sSet); /* Appropriate function for the current call context */ - return apSet[iTarget]; + return pLink; } /* * Mount a compiled class into the freshly created virtual machine so that @@ -1068,7 +1390,6 @@ static ph7_value *VmNewOperandStack( } /* Forward declaration */ static sxi32 VmRegisterSpecialFunction(ph7_vm *pVm); -static int VmInstanceOf(ph7_class *pThis, ph7_class *pClass); static int VmClassMemberAccess(ph7_vm *pVm, ph7_class *pClass, sxi32 iProtection); /* * Prepare the Virtual Machine for byte-code execution.