Fix overload resolution using argument compatibility and recursive array matching
Build / AerScript (push) Successful in 1m1s

This commit is contained in:
2026-10-23 23:36:25 +02:00
parent 206061c837
commit 3c7f1b8f5b
+414 -93
View File
@@ -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.