diff --git a/engine/api.c b/engine/api.c index 40342fa..c01aefb 100644 --- a/engine/api.c +++ b/engine/api.c @@ -886,6 +886,25 @@ void *ph7_value_to_resource(ph7_value *pValue) { /* * [CAPIREF: ph7_value_compare()] * Please refer to the official documentation for function purpose and expected parameters. + * + * The comparison core does NOT mutate its operands [copy-on-compare], so the + * caller's values keep their type and contents after this call. + * If either pLeft or pRight is a NULL pointer, the call is harmless + * [legacy ticket 1433-24] and returns 1 [not equal] without inspecting + * the other value. + * Return 0 when both values are equal. For the ordered categories [numbers, + * strings] the result is exactly -1 or +1 [the comparison core normalizes + * ordering results to the 3-way value; a raw SyMemcmp byte delta such as + * -26 is never handed out]. The equality-only categories [arrays, objects, + * resources, callbacks] never return < 0: a positive result simply means + * "not equal", NOT "greater". + * SXERR_NOMATCH (-26) is returned when no 3-way ordering result exists: + * different comparison categories [e.g: string vs int, or any pair + * involving the incomparable void value], a NaN operand + * [unordered], an instantiated object against NULL [equality is known to be + * false, but the pair has no ordering], or, when bStrict is non-zero, values + * whose types are not identical. It is not an ordering result; test it before + * interpreting the sign. */ int ph7_value_compare(ph7_value *pLeft, ph7_value *pRight, int bStrict) { int rc; @@ -893,8 +912,9 @@ int ph7_value_compare(ph7_value *pLeft, ph7_value *pRight, int bStrict) { /* TICKET 1433-24: NULL values is harmless operation */ return 1; } - /* Perform the comparison */ - rc = PH7_MemObjCmp(&(*pLeft), &(*pRight), bStrict, 0); + /* Perform the comparison [non-erroring flavor: a host application + * must be able to probe values without risking a process exit] */ + rc = PH7_MemObjCmpSafe(&(*pLeft), &(*pRight), bStrict, 0, 0); /* Comparison result */ return rc; } diff --git a/engine/compiler.c b/engine/compiler.c index cccf5d3..39c68e0 100644 --- a/engine/compiler.c +++ b/engine/compiler.c @@ -70,6 +70,15 @@ static void PH7_GenStateInitBlock( ) { /* Initialize block fields */ pBlock->nFirstInstr = nFirstInstr; + /* Unique instance id: PH7_GenStateFreeBlock() returns the instance to the + * pool and the very same address is handed out to later blocks, so any + * identity test across block lifetimes [declared-type table] must use + * this serial, never the address. The counter is never reset inside one + * code-generator episode: PH7_InitCodeGenerator() starts a new episode and + * zeroes the counter together with the hDeclType table, and an include + * saves/restores the enclosing episode's counter in lockstep with that + * table [see PH7_CompileIncludedChunk]. */ + pBlock->nSerial = ++pGen->nBlockSerial; pBlock->pUserData = pUserData; pBlock->pGen = pGen; pBlock->iFlags = iType; @@ -1012,6 +1021,233 @@ static sxu32 GenStateNamedType(ph7_gen_state *pGen, SyString *pName) { } return ((i + 1) << 12) | MEMOBJ_OBJ; } +/* + * Classify a literal expression node for the static comparison checks. + * Returns one of the PH7_CMP_LIT_* values; PH7_CMP_LIT_NONE when the node + * is not a statically classifiable literal. + */ +#define PH7_CMP_LIT_NONE 0 +#define PH7_CMP_LIT_STRING 1 +#define PH7_CMP_LIT_INT 2 +#define PH7_CMP_LIT_FLOAT 3 +#define PH7_CMP_LIT_ARRAY 4 +#define PH7_CMP_LIT_BOOL 5 +#define PH7_CMP_LIT_NULL 6 +#define PH7_CMP_LIT_OBJECT 7 +/* + * int, float, bool and NULL literals all belong to the NUMBER comparison + * category: they are cross-comparable without an explicit rule. + */ +#define PH7_CMP_LIT_IS_NUMBERISH(X) ((X) == PH7_CMP_LIT_INT || (X) == PH7_CMP_LIT_FLOAT || (X) == PH7_CMP_LIT_BOOL || (X) == PH7_CMP_LIT_NULL) +/* + * Static comparison checker support for declared variables. + * A typed declaration stamps its mask onto the value at OP_DECLARE + * [engine/vm.c] and the runtime assignment guard keeps that category for the + * whole lifetime of the variable, even under a NULL store or a re-assignment. + * The verdicts the checker can make for literals can therefore also be made + * for variables whose declared mask pins exactly one comparison category. + * Conservative by construction: mixed, char, void, resource and callback + * masks pin no category [GenStateDeclaredCategory returns NONE], and so do + * array element types, class fields through '->', static properties, + * superglobals, foreach slots, by-ref formals [their caller-side binding is + * unproven at the declaration], call results and undeclared names. Everything + * left as NONE defers to the runtime guard, which stays authoritative. + * Entries are keyed by the interned variable name [the same hVar name pool + * that LOADV operands use] and owned by the block of the declaration: a + * lookup is valid only while that block still encloses the current block, + * walking the GenBlock chain and stopping at the function body boundary, + * exactly like the run-time variable lookup. + * INVARIANT: a block instance is identified by its unique serial + * [GenBlock::nSerial], NOT by its address. Blocks are returned to the + * memory pool on leave [PH7_GenStateFreeBlock] and the allocator hands the + * very same address to later block instances, so raw pointer identity can + * match a long-dead declaration in an unrelated function. The entry stamps + * the declaring block's serial at install time; the chain walk compares + * serials, so a recycled address never resurrects a stale entry. + * SECOND INVARIANT: an entry is a fact about the storage cell created by the + * declaration, not about the name forever. unset($name) destroys that cell + * [vm_builtin_unset → PH7_VmUnsetMemObj in engine/vm.c], so the compiler + * forgets the entry as soon as it emits such an unset [GenStateForgetUnsetName] + * and every later comparison of that name falls back to the runtime guard: + * an unset name can only be used again through a fresh declaration, and a + * declaration whose mask pins no category [mixed...] installs nothing. + * Missing static diagnostics are acceptable, false ones are not. + */ +typedef struct { + sxu32 nCat; /* PH7_CMP_LIT_* category pinned by the declaration */ + sxu32 nSerial; /* Serial of the block owning the declaration */ +} GenDeclType; +/* + * Map a declared type mask to its static comparison category, or NONE when + * the mask does not pin exactly one [mixed/char/void/resource/callback or an + * implicit declaration]. + */ +static sxi32 GenStateDeclaredCategory(sxu32 nType) { + if(nType & MEMOBJ_HASHMAP) { + /* T[] always holds an array value, whatever T says about elements */ + return PH7_CMP_LIT_ARRAY; + } + if(nType & MEMOBJ_OBJ) { + return PH7_CMP_LIT_OBJECT; + } + if(nType & MEMOBJ_STRING) { + return PH7_CMP_LIT_STRING; + } + if(nType & MEMOBJ_INT) { + return PH7_CMP_LIT_INT; + } + if(nType & MEMOBJ_REAL) { + return PH7_CMP_LIT_FLOAT; + } + if(nType & MEMOBJ_BOOL) { + return PH7_CMP_LIT_BOOL; + } + return PH7_CMP_LIT_NONE; +} +/* + * Return the persistent interned copy of a variable name, sharing the hVar + * name pool used by the LOADV operands. NULL on allocation failure. + */ +static char *GenStateInternVarName(ph7_gen_state *pGen, SyString *pName) { + SyHashEntry *pEntry = SyHashGet(&pGen->hVar, (const void *)pName->zString, pName->nByte); + char *zName; + if(pEntry != 0) { + /* Name already available */ + return (char *)pEntry->pUserData; + } + /* Duplicate name */ + zName = SyMemBackendStrDup(&pGen->pVm->sAllocator, pName->zString, pName->nByte); + if(zName == 0) { + return 0; + } + /* Install in the hashtable */ + if(SyHashInsert(&pGen->hVar, zName, pName->nByte, zName) != SXRET_OK) { + return 0; + } + return zName; +} +/* + * Drop the entry pinned for a name, if any, and release its storage. + */ +static void GenStateRemoveDeclaredType( + ph7_gen_state *pGen, /* Code generator state */ + const void *pKey, sxu32 nKeyLen /* Interned variable name */ +) { + void *pOld = 0; /* SyHashDeleteEntry leaves the OUT untouched when not found */ + SyHashDeleteEntry(&pGen->hDeclType, pKey, nKeyLen, &pOld); + if(pOld != 0) { + SyMemBackendFree(&pGen->pVm->sAllocator, pOld); + } +} +/* + * Record (or replace with the most recent visible declaration) the category + * pinned for a variable name by its declaration inside pBlock. + */ +static void GenStateInstallDeclaredType( + ph7_gen_state *pGen, /* Code generator state */ + const void *pKey, sxu32 nKeyLen, /* Interned variable name */ + sxi32 iCat, /* PH7_CMP_LIT_* category pinned by the declaration */ + GenBlock *pBlock /* Block owning the declaration */ +) { + GenDeclType *pDecl; + if(pKey == 0) { + /* Nothing statically known; the runtime guard stays authoritative */ + return; + } + /* One entry per name: the most recent visible declaration wins */ + GenStateRemoveDeclaredType(&(*pGen), pKey, nKeyLen); + pDecl = (GenDeclType *)SyMemBackendAlloc(&pGen->pVm->sAllocator, sizeof(GenDeclType)); + if(pDecl == 0) { + /* Don't worry about freeing memory, everything will be released shortly */ + return; + } + pDecl->nCat = (sxu32)iCat; + pDecl->nSerial = pBlock->nSerial; + if(SyHashInsert(&pGen->hDeclType, pKey, nKeyLen, pDecl) != SXRET_OK) { + SyMemBackendFree(&pGen->pVm->sAllocator, pDecl); + } +} +/* + * Look up the category pinned by the declaration of pName, honoured only + * while the declaring block still encloses the current block. The walk + * mirrors the scope rules: it stops at the enclosing function body block, + * so names declared in another method/closure or in a block that has already + * been left are never matched here. + */ +static sxi32 GenStateLookupDeclaredType(ph7_gen_state *pGen, SyString *pName) { + SyHashEntry *pEntry; + GenDeclType *pDecl; + GenBlock *pBlock; + pEntry = SyHashGet(&pGen->hDeclType, (const void *)pName->zString, pName->nByte); + if(pEntry == 0) { + return PH7_CMP_LIT_NONE; + } + pDecl = (GenDeclType *)pEntry->pUserData; + for(pBlock = pGen->pCurrent; pBlock != 0; pBlock = pBlock->pParent) { + if(pBlock->nSerial == pDecl->nSerial) { + return (sxi32)pDecl->nCat; + } + if(pBlock->iFlags & (GEN_BLOCK_PROTECTED | GEN_BLOCK_FUNC)) { + /* Function body boundary: different scope */ + break; + } + } + return PH7_CMP_LIT_NONE; +} +/* + * Return the name of a plain variable reference node, that is, exactly the + * '$' token plus the name token. Any other shape [member access, subscript, + * call result, variable-variable...] gives 0. Both the static checker and the + * unset() invalidation below must agree on this shape, so the test lives here. + */ +static SyString *GenStateVarNameNode(ph7_expr_node *pNode) { + if(pNode == 0 || pNode->xCode != PH7_CompileVariable || pNode->pStart == 0 || + pNode->pEnd != pNode->pStart + 2 || (pNode->pStart->nType & PH7_TK_DOLLAR) == 0) { + return 0; + } + return &pNode->pStart[1].sData; +} +/* + * TRUE for a call written as a bare name, 'unset(...)' in any spelling case + * [the same case-insensitive name test PH7_GenStateLoadLiteral applies to the + * reserved literals]. The callee is a single literal token here: method calls + * [$o->m()], static calls [X::m()] and dynamic calls [$f()] carry a different + * callee shape and are never matched. + */ +static sxi32 GenStateIsUnsetCall(ph7_expr_node *pNode) { + ph7_expr_node *pCallee = pNode->pLeft; + SyString *pName; + if(pCallee == 0 || pCallee->xCode != PH7_CompileLiteral || + pCallee->pEnd != pCallee->pStart + 1) { + return FALSE; + } + pName = &pCallee->pStart->sData; + return pName->nByte == sizeof("unset") - 1 && + SyStrnicmp(pName->zString, "unset", sizeof("unset") - 1) == 0; +} +/* + * Feed the static comparison checker for an unset() call: every plain variable + * argument loses the category pinned by its declaration, because the builtin + * destroys the storage cell behind the name at run-time + * [vm_builtin_unset → PH7_VmUnsetMemObj in engine/vm.c] and the next + * declaration of that name may pin another category or none at all. The entry + * is dropped before the arguments [and anything later] are compiled, so those + * comparisons defer to the runtime guard, which stays authoritative. + * Subscripted unsets [unset($a[1])], class fields through '->' and superglobals + * are not plain variable references: the variable cell itself survives those + * and their entries stay pinned. + */ +static void GenStateForgetUnsetName(ph7_gen_state *pGen, ph7_expr_node *pNode) { + ph7_expr_node **apNode; + sxi32 n; + apNode = (ph7_expr_node **)SySetBasePtr(&pNode->aNodeArgs); + for(n = 0 ; n < (sxi32)SySetUsed(&pNode->aNodeArgs) ; ++n) { + SyString *pName = GenStateVarNameNode(apNode[n]); + if(pName != 0) { + GenStateRemoveDeclaredType(&(*pGen), (const void *)pName->zString, pName->nByte); + } + } +} static sxi32 GenStateIsNamedDeclaration(SyToken *pIn, SyToken *pEnd) { if(pIn >= pEnd || (pIn->nType & PH7_TK_ID) == 0 || (pIn->nType & PH7_TK_OP)) { return FALSE; @@ -1207,25 +1443,15 @@ PH7_PRIVATE sxi32 PH7_CompileVariable(ph7_gen_state *pGen, sxi32 iCompileFlag) { return SXERR_ABORT; } p3 = 0; - SyHashEntry *pEntry; SyString *pName; - char *zName = 0; + char *zName; /* Extract variable name */ pName = &pGen->pIn->sData; /* Advance the stream cursor */ pGen->pIn++; - pEntry = SyHashGet(&pGen->hVar, (const void *)pName->zString, pName->nByte); - if(pEntry == 0) { - /* Duplicate name */ - zName = SyMemBackendStrDup(&pGen->pVm->sAllocator, pName->zString, pName->nByte); - if(zName == 0) { - PH7_GenCompileError(pGen, E_ERROR, nLine, "PH7 engine is running out-of-memory"); - } - /* Install in the hashtable */ - SyHashInsert(&pGen->hVar, zName, pName->nByte, zName); - } else { - /* Name already available */ - zName = (char *)pEntry->pUserData; + zName = GenStateInternVarName(&(*pGen), pName); + if(zName == 0) { + PH7_GenCompileError(pGen, E_ERROR, nLine, "PH7 engine is running out-of-memory"); } p3 = (void *)zName; /* Emit the load instruction */ @@ -2710,6 +2936,16 @@ static sxi32 PH7_CompileVar(ph7_gen_state *pGen) { /* Storage belongs to the declaration; visibility starts here. */ PH7_VmEmitInstr(pGen->pVm, pGen->pIn->nLine, PH7_OP_DECLARE, 2, SySetUsed(&pFunc->aStatic) - 1, pFunc, 0); + /* Same declared-category bookkeeping as plain declarations: the + * static keeps its category across every assignment, and the + * enclosing function block owns it. */ + { + sxi32 iCat = GenStateDeclaredCategory(nType); + if(iCat != PH7_CMP_LIT_NONE) { + GenStateInstallDeclaredType(&(*pGen), GenStateInternVarName(&(*pGen), pName), + pName->nByte, iCat, pBlock); + } + } } else { zDup = SyMemBackendStrDup(&pGen->pVm->sAllocator, pName->zString, pName->nByte); if(zDup == 0) { @@ -2718,6 +2954,16 @@ static sxi32 PH7_CompileVar(ph7_gen_state *pGen) { void *p3 = (void *) zDup; /* Emit OP_DECLARE instruction */ PH7_VmEmitInstr(pGen->pVm, pGen->pIn->nLine, PH7_OP_DECLARE, 0, nType, p3, 0); + /* Feed the static comparison checker: the declared mask pins this + * name's comparison category inside the current block [implicit + * declarations and category-less masks pin nothing]. */ + { + sxi32 iCat = GenStateDeclaredCategory(nType); + if(iCat != PH7_CMP_LIT_NONE) { + GenStateInstallDeclaredType(&(*pGen), GenStateInternVarName(&(*pGen), pName), + pName->nByte, iCat, pGen->pCurrent); + } + } /* Pop the l-value */ PH7_VmEmitInstr(pGen->pVm, pGen->pIn->nLine, PH7_OP_POP, 1, 0, 0, 0); /* Check if we have an expression to compile */ @@ -3293,6 +3539,27 @@ static sxi32 PH7_GenStateCompileFuncBody( if(rc != SXRET_OK) { PH7_GenCompileError(&(*pGen), E_ERROR, 1, "PH7 engine is running out-of-memory"); } + /* The formal arguments are declared for the whole body: hand their pinned + * categories to the static comparison checker, owned by this body block. + * By-ref formals are skipped on purpose [the task's conservative list]: + * the caller-side binding is unproven at the declaration, so their + * category stays in the runtime guard. */ + { + ph7_vm_func_arg *pArg = (ph7_vm_func_arg *)SySetBasePtr(&pFunc->aArgs); + sxu32 n, nArg = SySetUsed(&pFunc->aArgs); + for(n = 0 ; n < nArg ; ++n, ++pArg) { + sxi32 iCat; + if(pArg->iFlags & VM_FUNC_ARG_BY_REF) { + continue; + } + iCat = GenStateDeclaredCategory(pArg->nType); + if(iCat == PH7_CMP_LIT_NONE) { + continue; + } + GenStateInstallDeclaredType(&(*pGen), GenStateInternVarName(&(*pGen), &pArg->sName), + pArg->sName.nByte, iCat, pBlock); + } + } nGotoOfft = SySetUsed(&pGen->aGoto); /* Swap bytecode containers */ pInstrContainer = PH7_VmGetByteCodeContainer(pGen->pVm); @@ -4907,6 +5174,138 @@ static sxi32 PH7_CompileSwitch(ph7_gen_state *pGen) { } return SXRET_OK; } +/* + * Classify an operand node for the static comparison checks. + * Returns one of the PH7_CMP_LIT_* values [constants moved above]; + * PH7_CMP_LIT_NONE when the node is not statically classifiable. + */ +static sxi32 PH7_GenStateClassifyLiteralNode(ph7_gen_state *pGen, ph7_expr_node *pNode) { + if(pNode == 0 || pNode->pStart == 0) { + return PH7_CMP_LIT_NONE; + } + if(pNode->xCode == PH7_CompileVariable) { + /* A plain variable reference [shape test shared with the unset() + * invalidation]: the category is whatever the in-scope declaration + * pins; anything else [member access, index, static property...] is a + * different node shape and stays NONE. */ + SyString *pName = GenStateVarNameNode(pNode); + if(pName == 0) { + return PH7_CMP_LIT_NONE; + } + return GenStateLookupDeclaredType(pGen, pName); + } + if(pNode->xCode == PH7_CompileString || pNode->xCode == PH7_CompileSimpleString) { + /* Single or double quoted string literal */ + return PH7_CMP_LIT_STRING; + } + if(pNode->xCode == PH7_CompileNumLiteral) { + /* Integer or real literal: the token type already tells them apart */ + if(pNode->pStart->nType & PH7_TK_REAL) { + return PH7_CMP_LIT_FLOAT; + } + return PH7_CMP_LIT_INT; + } + if(pNode->xCode == PH7_CompileArray) { + /* Array literal */ + return PH7_CMP_LIT_ARRAY; + } + if(pNode->xCode == PH7_CompileLiteral) { + /* Keyword literals TRUE/FALSE/NULL: these reserved names are + * identifier-like tokens resolved by PH7_GenStateLoadLiteral() + * [same name test as PH7_GenStateIsReservedConstant()], never a + * user constant or function call [a call is an operator node]. */ + SyString *pName = &pNode->pStart->sData; + if(pName->nByte == sizeof("NULL") - 1) { + if(SyStrnicmp(pName->zString, "null", sizeof("null") - 1) == 0) { + return PH7_CMP_LIT_NULL; + } + if(SyStrnicmp(pName->zString, "true", sizeof("true") - 1) == 0) { + return PH7_CMP_LIT_BOOL; + } + } else if(pName->nByte == sizeof("FALSE") - 1 + && SyStrnicmp(pName->zString, "false", sizeof("false") - 1) == 0) { + return PH7_CMP_LIT_BOOL; + } + return PH7_CMP_LIT_NONE; + } + if(pNode->pOp != 0 && pNode->pOp->iOp == EXPR_OP_NEW) { + /* new X(): a freshly instantiated object literal */ + return PH7_CMP_LIT_OBJECT; + } + return PH7_CMP_LIT_NONE; +} +/* + * Static compile-time check for the relational operators [==,!=,<,<=,>,>=]: + * reject pairs of statically known incompatible operands. Detected kinds: + * literal string vs a number/bool/NULL literal, array vs any literal of + * another category, object literal [new] vs any literal of another category, + * and the same shapes for variables whose in-scope declaration pins a single + * category. int/float/bool/NULL belong to one NUMBER category and are + * mutually allowed [2==TRUE, 0==FALSE, NULL==0, true==1 stay valid]. + * Equality and ordering split at the escape rules: equality admits two arrays, + * two objects and object↔NULL [the reference rule decides], while ordering + * admits only NUMBER↔NUMBER and STRING↔STRING and rejects everything else + * statically, object↔NULL included. Everything the classification leaves as + * NONE [char literals, mixed-typed declared variables, elements, fields, + * call results...] defers to the runtime guards, and a rule that cannot be + * proven never fires [a static error on a legal program is a regression]. + */ +static void PH7_GenStateCheckCompareOperands( + ph7_gen_state *pGen, /* Code generator state */ + ph7_expr_node *pNode,/* Operator node */ + int bOrder /* TRUE for [<,<=,>,>=], FALSE for [==,!=] */ +) { + static const char *const aLitName[] = { + "", "string", "int", "float", "array", "bool", "NULL", "object" + }; + sxi32 iLeft, iRight; + /* Extract the raw operand nodes */ + ph7_expr_node *pL = pNode->pLeft; + ph7_expr_node *pR = pNode->pRight; + if(pL == 0 || pR == 0) { + return; + } + iLeft = PH7_GenStateClassifyLiteralNode(&(*pGen), pL); + iRight = PH7_GenStateClassifyLiteralNode(&(*pGen), pR); + if(iLeft == PH7_CMP_LIT_NONE || iRight == PH7_CMP_LIT_NONE) { + /* Not statically decidable */ + return; + } + if(PH7_CMP_LIT_IS_NUMBERISH(iLeft) && PH7_CMP_LIT_IS_NUMBERISH(iRight)) { + /* NUMBER category members cross-compare: allowed for both verdicts */ + return; + } + if(bOrder) { + if(iLeft == PH7_CMP_LIT_STRING && iRight == PH7_CMP_LIT_STRING) { + /* Byte order is a defined total order on strings */ + return; + } + /* Arrays, objects and object↔NULL have no ordering at all under the + * comparison contract; same verb as the runtime no-order guard + * [PH7_VmThrowNoOrder in engine/memobj.c]. */ + PH7_GenCompileError(pGen, E_ERROR, pNode->pStart->nLine, + "Cannot order '%z' %s with %s", + &pNode->pOp->sOp, aLitName[iLeft], aLitName[iRight]); + return; + } + if(iLeft == iRight) { + /* Same category: allowed [two arrays, two objects, two strings...] */ + return; + } + if((iLeft == PH7_CMP_LIT_OBJECT && iRight == PH7_CMP_LIT_NULL) + || (iRight == PH7_CMP_LIT_OBJECT && iLeft == PH7_CMP_LIT_NULL)) { + /* Object vs raw NULL is legal under the contract [the reference rule + * decides equality]; never reject it statically. */ + return; + } + /* Every remaining literal pair spans statically incompatible categories + * [array, object or string against a foreign category]. The operator + * spelling and precise literal type names are reported; the line comes + * from the error reporter. */ + PH7_GenCompileError(pGen, E_ERROR, pNode->pStart->nLine, + "Cannot compare '%z' %s with %s", + &pNode->pOp->sOp, aLitName[iLeft], aLitName[iRight]); +} /* * Generate bytecode for a given expression tree. * If something goes wrong while generating bytecode @@ -5020,6 +5419,12 @@ static sxi32 PH7_GenStateEmitExprCode( if(iVmOp == PH7_OP_CALL) { ph7_expr_node **apNode; sxi32 n; + if(GenStateIsUnsetCall(pNode)) { + /* unset() destroys the arguments' storage cells: forget the + * categories pinned for those names right here, so nothing + * later is checked against a dead declaration. */ + GenStateForgetUnsetName(&(*pGen), pNode); + } /* Recurse and generate bytecodes for function arguments */ apNode = (ph7_expr_node **)SySetBasePtr(&pNode->aNodeArgs); /* Read-only load */ @@ -5115,6 +5520,14 @@ static sxi32 PH7_GenStateEmitExprCode( } } if(iVmOp > 0) { + if(iVmOp == PH7_OP_EQ || iVmOp == PH7_OP_NEQ || iVmOp == PH7_OP_LT + || iVmOp == PH7_OP_LE || iVmOp == PH7_OP_GT || iVmOp == PH7_OP_GE) { + /* Typed comparison contract: reject statically incompatible + * operands. The operator spelling decides the verdict [equality + * vs ordering] and the matching diagnostic verb. */ + PH7_GenStateCheckCompareOperands(&(*pGen), pNode, + (iVmOp != PH7_OP_EQ && iVmOp != PH7_OP_NEQ) ? TRUE : FALSE); + } if(iVmOp == PH7_OP_INCR || iVmOp == PH7_OP_DECR) { if(pNode->iFlags & EXPR_NODE_PRE_INCR) { /* Pre-increment/decrement operator [i.e: ++$i,--$j ] */ @@ -5679,6 +6092,10 @@ PH7_PRIVATE sxi32 PH7_CompileIncludedChunk(ph7_vm *pVm, SyString *pScript, ph7_v SySetRelease(&pGen->aGoto); SyHashRelease(&pGen->hLiteral); SyHashRelease(&pGen->hVar); + SyHashRelease(&pGen->hDeclType); + /* Restore the enclosing episode in lockstep: its hDeclType table and the + * nBlockSerial counter that owns those entries come back together, so this + * unit's serials can never be read against another unit's entries. */ pVm->sCodeGen = sSaved; return rc; } @@ -5701,6 +6118,7 @@ PH7_PRIVATE sxi32 PH7_InitCodeGenerator( SySetInit(&pGen->aGoto, &pVm->sAllocator, sizeof(JumpFixup)); SyHashInit(&pGen->hLiteral, &pVm->sAllocator, 0, 0); SyHashInit(&pGen->hVar, &pVm->sAllocator, 0, 0); + SyHashInit(&pGen->hDeclType, &pVm->sAllocator, 0, 0); /* Error log buffer */ SyBlobInit(&pGen->sErrBuf, &pVm->sAllocator); /* General purpose working buffer */ @@ -5726,6 +6144,19 @@ PH7_PRIVATE sxi32 PH7_ResetCodeGenerator( SySetReset(&pGen->aGoto); SyBlobRelease(&pGen->sErrBuf); SyBlobRelease(&pGen->sWorker); + /* Declared-type entries are facts of a single compilation unit: each one + * carries the serial of a block that is recycled after the reset [VM reset, + * eval re-entry, chunk swap], so drop them here. The re-init is not the dead + * work it looks like: PH7_ResetCodeGenerator() is also the last generator + * call before a compilation on the paths that never run Init first + * [ph7_compile_code in engine/api.c, VmEvalChunk in engine/vm.c], and an + * invalid table silently switches the checker off [measured: three + * tests/errors declared-type fixtures degrade to runtime diagnostics]. + * hLiteral/hVar need no flush here because their entries are name→index + * maps carrying no scope facts; the include path releases them, and this + * fresh table, at the end of the unit. */ + SyHashRelease(&pGen->hDeclType); + SyHashInit(&pGen->hDeclType, &pVm->sAllocator, 0, 0); /* Point to the global scope */ pBlock = pGen->pCurrent; while(pBlock->pParent != 0) { diff --git a/engine/hashmap.c b/engine/hashmap.c index c916157..7f86573 100644 --- a/engine/hashmap.c +++ b/engine/hashmap.c @@ -14,7 +14,13 @@ * Default hash function for int [i.e; 64-bit integer] keys. */ static sxu32 IntHash(sxi64 iKey) { - return (sxu32)(iKey ^ (iKey << 8) ^ (iKey >> 8)); + /* Mix the bits using unsigned arithmetic: a signed left shift [iKey << 8] + * is undefined behavior once |iKey| exceeds 2^55, and the signed right + * shift is implementation defined for negative keys. Bucket choice only, + * iteration order is kept in the node chain and is left untouched. + */ + sxu64 uKey = (sxu64)iKey; + return (sxu32)(uKey ^ (uKey << 8) ^ (uKey >> 8)); } /* * Default hash function for string/BLOB keys. @@ -594,19 +600,54 @@ IntKey: rc = HashmapInsertIntKey(&(*pMap), pKey->x.iVal, &(*pVal), 0); if(rc == SXRET_OK) { if(pKey->x.iVal >= pMap->iNextIdx) { - /* Increment the automatic index */ - pMap->iNextIdx = pKey->x.iVal + 1; - /* Make sure the automatic index is not reserved */ - while(SXRET_OK == HashmapLookupIntKey(&(*pMap), pMap->iNextIdx, 0)) { - pMap->iNextIdx++; + if(pKey->x.iVal == SXI64_HIGH) { + /* Evaluating [iVal + 1] here is signed overflow (undefined + * behavior): SXI64_HIGH is the last representable int64 key, + * so there is no index past it. Saturate the boundary at + * SXI64_HIGH and flag the index space as exhausted instead. + */ + pMap->iNextIdx = SXI64_HIGH; + pMap->bIdxExhausted = TRUE; + } else { + /* Increment the automatic index */ + pMap->iNextIdx = pKey->x.iVal + 1; + /* Make sure the automatic index is not reserved. The probe + * stops at SXI64_HIGH: it is the last index that can ever be + * assigned, so it must never be incremented past. + */ + while(pMap->iNextIdx < SXI64_HIGH && + SXRET_OK == HashmapLookupIntKey(&(*pMap), pMap->iNextIdx, 0)) { + pMap->iNextIdx++; + } + if(pMap->iNextIdx == SXI64_HIGH && + SXRET_OK == HashmapLookupIntKey(&(*pMap), pMap->iNextIdx, 0)) { + /* The reserved index is the last one: nothing left to assign */ + pMap->bIdxExhausted = TRUE; + } } } } } else { - /* Assign an automatic index */ + /* Assign an automatic index. Once the index space is exhausted [the int + * key SXI64_HIGH has been consumed by any insert path], there is no + * successor index: raise a controlled fatal error [exit status 255] + * before any node is allocated, so the hashmap stays completely + * unmodified. No wraparound to the negative indices, no overwrite of + * the SXI64_HIGH entry and no fallback probing for a lower free key. + */ + if(pMap->bIdxExhausted) { + PH7_VmThrowError(pMap->pVm, PH7_CTX_ERR, + "Cannot append to the array: the automatic index space is exhausted"); + return SXERR_ABORT; /* NOT REACHED: PH7_CTX_ERR aborts script execution */ + } rc = HashmapInsertIntKey(&(*pMap), pMap->iNextIdx, &(*pVal), 0); if(rc == SXRET_OK) { - ++pMap->iNextIdx; + if(pMap->iNextIdx < SXI64_HIGH) { + ++pMap->iNextIdx; + } else { + /* The automatic append just consumed the last int64 index */ + pMap->bIdxExhausted = TRUE; + } } } /* Insertion result */ @@ -643,6 +684,12 @@ static sxi32 HashmapInsertNode(ph7_hashmap *pMap, ph7_hashmap_node *pNode, int b rc = HashmapInsert(&(*pMap), 0, pObj); } else { rc = HashmapInsertIntKey(&(*pMap), pNode->xKey.iKey, pObj, 0); + if(rc == SXRET_OK && pNode->xKey.iKey == SXI64_HIGH) { + /* Key-preserving insertion bypasses HashmapInsert(): maintain the + * exhausted-index boundary invariant here as well. + */ + pMap->bIdxExhausted = TRUE; + } } } else { /* Blob key */ @@ -654,7 +701,9 @@ static sxi32 HashmapInsertNode(ph7_hashmap *pMap, ph7_hashmap_node *pNode, int b /* * Compare two node values. * Return 0 if the node values are equals, > 0 if pLeft is greater than pRight - * or < 0 if pRight is greater than pLeft. + * or < 0 if pRight is greater than pLeft. Incomparable categories and NaN + * pairs [SXERR_NOMATCH from the comparison core] resolve to a deterministic + * "not equal" instead of aborting. * For a full description on ph7_values comparison,refer to the implementation * of the [PH7_MemObjCmp()] function defined in memobj.c or the official * documentation. @@ -669,19 +718,26 @@ static sxi32 HashmapNodeCmp(ph7_hashmap_node *pLeft, ph7_hashmap_node *pRight, i */ return 0; } - /* Do the comparison */ + /* Duplicate the node values [the comparison must not mutate them] */ PH7_MemObjInit(pLeft->pMap->pVm, &sObj1); PH7_MemObjInit(pLeft->pMap->pVm, &sObj2); PH7_HashmapExtractNodeValue(pLeft, &sObj1, FALSE); PH7_HashmapExtractNodeValue(pRight, &sObj2, FALSE); - rc = PH7_MemObjCmp(&sObj1, &sObj2, bStrict, 0); + /* Do the comparison. HashmapNodeCmp feeds PH7_HashmapCmp [equality only] + * and the default sort path; both need a silent result for incomparable + * or unordered pairs rather than a fatal abort. */ + rc = PH7_MemObjCmpSafe(&sObj1, &sObj2, bStrict, 0, 0); PH7_MemObjRelease(&sObj1); PH7_MemObjRelease(&sObj2); - return rc; + return rc == SXERR_NOMATCH ? 1 : rc; } /* * Rehash a node with a 64-bit integer key. * Refer to [merge_sort(),array_shift()] implementations for more information. + * Pre-condition: the caller re-numbers the whole map starting from zero + * [HashmapSortRehash(), ph7_hashmap_shift()] and clears bIdxExhausted at the + * same reset, so the key assignment here can never collide with an existing + * int key nor reach SXI64_HIGH [nEntry is bounded by SXU32_HIGH]. */ static void HashmapRehashIntNode(ph7_hashmap_node *pEntry) { ph7_hashmap *pMap = pEntry->pMap; @@ -716,6 +772,12 @@ static void HashmapRehashIntNode(ph7_hashmap_node *pEntry) { * Otherwise SXERR_NOTFOUND is returned on failure. * Refer to [array_intersect(),array_diff(),in_array(),...] implementations * for more information. + * + * Loose search follows the typed comparison contract: + * - NULL matches 0/FALSE and vice versa [numeric domain]; + * - a string never matches a number [different categories]; + * - incomparable categories mean "no match", never an error; + * - arrays compare by value, objects by identity. */ static int HashmapFindValue( ph7_hashmap *pMap, /* Target hashmap */ @@ -740,21 +802,25 @@ static int HashmapFindValue( /* Extract node value */ pVal = HashmapExtractNodeValue(pEntry); if(pVal) { - if((pVal->nType | pNeedle->nType) & MEMOBJ_NULL) { - sxi32 iF1 = pVal->nType; - sxi32 iF2 = pNeedle->nType; - if(iF1 == iF2) { - /* NULL values are equals */ + if(bStrict) { + /* Strict: type-identical values only [existing path] */ + PH7_MemObjLoad(pVal, &sVal); + PH7_MemObjLoad(pNeedle, &sNeedle); + rc = PH7_MemObjCmpSafe(&sNeedle, &sVal, TRUE, 0, 0); + PH7_MemObjRelease(&sVal); + PH7_MemObjRelease(&sNeedle); + if(rc == 0) { if(ppNode) { *ppNode = pEntry; } + /* Match found*/ return SXRET_OK; } - } else { - /* Duplicate value */ + } else if(PH7_MemObjComparable(pNeedle, pVal)) { + /* Loose: categories must be compatible, then compare on copies */ PH7_MemObjLoad(pVal, &sVal); PH7_MemObjLoad(pNeedle, &sNeedle); - rc = PH7_MemObjCmp(&sNeedle, &sVal, bStrict, 0); + rc = PH7_MemObjCmpSafe(&sNeedle, &sVal, FALSE, 0, 0); PH7_MemObjRelease(&sVal); PH7_MemObjRelease(&sNeedle); if(rc == 0) { @@ -765,6 +831,7 @@ static int HashmapFindValue( return SXRET_OK; } } + /* Incomparable category: skip [no match for this entry] */ } /* Point to the next entry */ pEntry = pEntry->pPrev; /* Reverse link */ @@ -815,7 +882,9 @@ static int HashmapFindValueByCallback( /* Perform an int cast */ PH7_MemObjToInteger(&sResult); } - rc = (sxi32)sResult.x.iVal; + /* Classify against zero on the full int64: a raw (sxi32) cast + * would fold 2^32 or INT64_MIN to 0 and report a bogus match. */ + rc = sResult.x.iVal != 0; PH7_MemObjRelease(&sResult); if(rc == 0) { /* Match found*/ @@ -879,6 +948,10 @@ static int HashmapFindValueByCallback( * string(6) "cherry" * } * Elements of arrays are equal for the comparison if they have the same key and value. + * Under the typed comparison contract this function is EQUALITY-ONLY: + * it returns 0 when both maps hold equal key/value sets and 1 otherwise + * (different entry count, missing key or differing value). No ordering is + * defined for arrays; the result is symmetric in (pLeft, pRight). */ PH7_PRIVATE sxi32 PH7_HashmapCmp( ph7_hashmap *pLeft, /* Left hashmap */ @@ -896,7 +969,7 @@ PH7_PRIVATE sxi32 PH7_HashmapCmp( } if(pLeft->nEntry != pRight->nEntry) { /* Must have the same number of entries */ - return pLeft->nEntry > pRight->nEntry ? 1 : -1; + return 1; } /* Point to the first inserted entry of the left hashmap */ pLe = pLeft->pFirst; @@ -929,10 +1002,13 @@ PH7_PRIVATE sxi32 PH7_HashmapCmp( if(!rc) { /* Compare nodes */ rc = HashmapNodeCmp(pLe, pRe, bStrict); + if(rc != 0) { + rc = 1; + } } if(rc != 0) { /* Nodes key/value differ */ - return rc; + return 1; } /* Point to the next entry */ pLe = pLe->pPrev; /* Reverse link */ @@ -1074,6 +1150,8 @@ PH7_PRIVATE sxi32 PH7_HashmapDup(ph7_hashmap *pSrc, ph7_hashmap *pDest) { } /* Save the next array index */ pDest->iNextIdx = pSrc->iNextIdx; + /* An exhausted index space survives the duplication */ + pDest->bIdxExhausted = pSrc->bIdxExhausted; return SXRET_OK; } /* @@ -1152,6 +1230,12 @@ PH7_PRIVATE sxi32 PH7_HashmapUnion(ph7_hashmap *pLeft, ph7_hashmap *pRight) { if(rc != SXRET_OK) { return rc; } + if(pEntry->xKey.iKey == SXI64_HIGH) { + /* Direct insertion bypasses HashmapInsert(): maintain the + * exhausted-index boundary invariant here as well. + */ + pLeft->bIdxExhausted = TRUE; + } } } } @@ -1289,6 +1373,7 @@ PH7_PRIVATE sxi32 PH7_HashmapRelease(ph7_hashmap *pMap, int FreeDS) { /* Keep the instance but reset it's fields */ pMap->apBucket = 0; pMap->iNextIdx = 0; + pMap->bIdxExhausted = FALSE; /* Empty map: the index space starts afresh */ pMap->nEntry = pMap->nSize = 0; pMap->pFirst = pMap->pLast = pMap->pCur = 0; } @@ -1425,111 +1510,362 @@ PH7_PRIVATE int PH7_HashmapValuesToSet(ph7_hashmap *pMap, SySet *pOut) { /* Node comparison callback signature */ typedef sxi32(*ProcNodeCmp)(ph7_hashmap_node *, ph7_hashmap_node *, void *); /* -** Inputs: -** a: A sorted, null-terminated linked list. (May be null). -** b: A sorted, null-terminated linked list. (May be null). -** cmp: A pointer to the comparison function. -** -** Return Value: -** A pointer to the head of a sorted list containing the elements -** of both a and b. -** -** Side effects: -** The "next","prev" pointers for elements in the lists a and b are -** changed. -*/ -static ph7_hashmap_node *HashmapNodeMerge(ph7_hashmap_node *pA, ph7_hashmap_node *pB, ProcNodeCmp xCmp, void *pCmpData) { - ph7_hashmap_node result, *pTail; - /* Prevent compiler warning */ - result.pNext = result.pPrev = 0; - pTail = &result; - while(pA && pB) { - if(xCmp(pA, pB, pCmpData) < 0) { - pTail->pPrev = pA; - pA->pNext = pTail; - pTail = pA; - pA = pA->pPrev; - } else { - pTail->pPrev = pB; - pB->pNext = pTail; - pTail = pB; - pB = pB->pPrev; - } + * Sort context shared by the pointer-sort driver and every node comparison + * callback. + * pCmpData : per-sort payload [comparison flags or the user callback value]; + * bAbort : separate status channel. A user-defined comparator whose call + * fails [exception/abort] raises this flag instead of overloading + * the integer result, so a callback legitimately returning any + * int [including -1073741824] is never mistaken for a failure. + * Once raised, the sort driver stops before any reordering is + * committed to the map. + * iPendingRC : original status [PH7_EXCEPTION or PH7_ABORT] reported by + * PH7_VmCallUserFunction for the first failing comparator call, + * 0 when nothing is pending. Internal comparators [flags, keys, + * random] never invoke user code and never set it. + * sPending : exception value transferred out of the failed comparator + * [valid only when iPendingRC == PH7_EXCEPTION]. A comparison + * callback runs deep inside the merge driver and cannot talk to + * the VM directly, so this small context is the carrier that + * preserves the thrown value and its original status across the + * driver, up to the builtin which re-raises it [ph7_result_value + * + PH7_EXCEPTION / PH7_ABORT] so the existing VM unwinding + * [catch/finally or script abort] sees exactly what the + * comparator produced. The first failing comparator wins; later + * callbacks never overwrite it, and a comparator that does not + * raise leaves the carrier untouched. + */ +typedef struct { + void *pCmpData; /* Comparison payload */ + sxi32 bAbort; /* TRUE when a comparator call failed */ + sxi32 iPendingRC; /* Original failed-call status, 0 when nothing pending */ + ph7_value sPending; /* Transferred exception value [iPendingRC == PH7_EXCEPTION] */ +} HashmapSortCtx; +/* + * Common tail for the sort builtins when the pointer-sort driver stopped + * [SXERR_ABORT]. The comparator's original status decides what the builtin + * hands back to the VM call machinery: an exception value stored into the + * result slot plus PH7_EXCEPTION [the VM moves that copy into the throw + * transfer and unwinds to catch/finally], or plain PH7_ABORT for exit() or a + * fatal raised inside the comparator. The pending carrier is released exactly + * once here. iPendingRC == 0 means nothing was pending: internal comparators + * [flags, keys, random] never invoke user code and never fill it, and the + * FALSE return is kept as a defensive fallback for them. The same fallback + * also catches any THIRD status PH7_VmCallUserFunction could report beyond + * PH7_EXCEPTION/PH7_ABORT [iPendingRC != 0 but neither known value]: every + * call site pre-checks callability and reachable call failures abort, so no + * script-visible path exercises it today; re-raising an unknown status would + * require fabricating a throw value the comparator never produced, so the + * conservative FALSE/PH7_OK degradation is deliberate, not an oversight. + */ +static int HashmapSortFailure(ph7_context *pCtx, HashmapSortCtx *pSortCtx) { + if(pSortCtx->iPendingRC == PH7_EXCEPTION) { + /* The VM owns the copy installed in the result slot from here on */ + ph7_result_value(pCtx, &pSortCtx->sPending); } - if(pA) { - pTail->pPrev = pA; - pA->pNext = pTail; - } else if(pB) { - pTail->pPrev = pB; - pB->pNext = pTail; - } else { - pTail->pPrev = pTail->pNext = 0; + PH7_MemObjRelease(&pSortCtx->sPending); + if(pSortCtx->iPendingRC == PH7_EXCEPTION) { + return PH7_EXCEPTION; } - return result.pPrev; + if(pSortCtx->iPendingRC == PH7_ABORT) { + return PH7_ABORT; + } + ph7_result_bool(pCtx, 0); + return PH7_OK; } /* -** Inputs: -** Map: Input hashmap -** cmp: A comparison function. -** -** Return Value: -** Sorted hashmap. -** -** Side effects: -** The "next" pointers for elements in list are changed. -*/ -#define N_SORT_BUCKET 32 -static sxi32 HashmapMergeSort(ph7_hashmap *pMap, ProcNodeCmp xCmp, void *pCmpData) { - ph7_hashmap_node *a[N_SORT_BUCKET], *p, *pIn; - sxu32 i; - SyZero(a, sizeof(a)); - /* Point to the first inserted entry */ - pIn = pMap->pFirst; - while(pIn) { - p = pIn; - pIn = p->pPrev; - p->pPrev = 0; - for(i = 0; i < N_SORT_BUCKET - 1; i++) { - if(a[i] == 0) { - a[i] = p; + * Sort value categories used by the default-mode pre-scan. + */ +#define HashmapSortCatNumber 1 +#define HashmapSortCatString 2 +#define HashmapSortCatOther 3 + +/* + * Classify a node value copy for the default-mode sort pre-scan. + * NUMBER: int, float, char, bool, raw NULL [the NULL-to-0 comparison rule]. + * STRING: byte strings. + * VOID and the other categories are rejected: void is incomparable and + * participates in no ordering, so it requires an explicit rule [cast mode + * or comparator] just like arrays and objects. + */ +static sxi32 MemObjSortValueCategory(ph7_value *pVal) { + sxu32 nType = pVal->nType; + if(nType & (MEMOBJ_HASHMAP | MEMOBJ_OBJ | MEMOBJ_RES | MEMOBJ_CALL | MEMOBJ_VOID)) { + return HashmapSortCatOther; + } + if(nType & MEMOBJ_STRING) { + return HashmapSortCatString; + } + return HashmapSortCatNumber; +} + +/* + * Verify the whole map holds one sortable value category before any + * reordering happens [default mode only]: + * - single NUMBER category: int/float/char/bool/NULL mixed are allowed; + * - single STRING category: byte order; + * - arrays/objects/resources/callbacks and the incomparable void value + * require an explicit rule [cast flag or comparator]. + * A NaN float makes the sort order undefined: controlled fatal error. + * Aborts via PH7_VmThrowError on violation, so the map stays untouched. + * The value builtins run the scan whatever the element count, so an + * unsortable singleton [void, array, object, NaN] is rejected too; only + * the empty map passes trivially. + */ +static void HashmapSortCheckValueCategory(ph7_hashmap *pMap) { + ph7_hashmap_node *pNode = pMap->pFirst; + sxu32 n = pMap->nEntry; + sxi32 iCat = 0; /* 0 = unknown, NUMBER, STRING */ + ph7_value sVal; + PH7_MemObjInit(pMap->pVm, &sVal); + while(n > 0) { + ph7_value *pRaw = HashmapExtractNodeValue(pNode); + if(pRaw == 0) { + /* Defensive: a node without an extractable value contributes + * nothing to the pre-scan [the entry is simply skipped]; the + * comparator and the runtime guards stay authoritative for it */ + pNode = pNode->pPrev; + n--; + continue; + } + PH7_MemObjLoad(pRaw, &sVal); + if(sVal.nType & MEMOBJ_REAL && sVal.x.rVal != sVal.x.rVal) { + /* NaN has no defined order */ + PH7_MemObjRelease(&sVal); + PH7_VmThrowError(pMap->pVm, PH7_CTX_ERR, + "Cannot sort: value is not a number (NaN)"); + } + switch(MemObjSortValueCategory(&sVal)) { + case HashmapSortCatNumber: + if(iCat == HashmapSortCatString) { + PH7_MemObjRelease(&sVal); + PH7_VmThrowError(pMap->pVm, PH7_CTX_ERR, + "Cannot sort values of mixed types without an explicit rule"); + } + iCat = HashmapSortCatNumber; break; - } else { - p = HashmapNodeMerge(a[i], p, xCmp, pCmpData); - a[i] = 0; + case HashmapSortCatString: + if(iCat == HashmapSortCatNumber) { + PH7_MemObjRelease(&sVal); + PH7_VmThrowError(pMap->pVm, PH7_CTX_ERR, + "Cannot sort values of mixed types without an explicit rule"); + } + iCat = HashmapSortCatString; + break; + default: + PH7_MemObjRelease(&sVal); + PH7_VmThrowError(pMap->pVm, PH7_CTX_ERR, + "Sorting this value requires an explicit comparator"); + break; + } + PH7_MemObjRelease(&sVal); + /* Point to the next entry */ + pNode = pNode->pPrev; /* Reverse link */ + n--; + } +} +/* + * Verify that no value becomes NaN under the explicit SORT_NUMERIC cast + * [sort()/rsort()/asort()/arsort() with the numeric flag]: a NaN has no + * defined order and no total order may be invented for it, so the explicit + * numeric path rejects it exactly like the default-mode pre-scan does. + * The value conversion performed by the numeric comparator + * [PH7_MemObjToNumeric(), the same cast used by HashmapCmpCallback1()] runs + * first on throwaway copies, then the whole set is scanned, BEFORE any + * relink: the violation raises the controlled fatal + * "Cannot sort: value is not a number (NaN)" and the map stays untouched. + * The four value-sort builtins run this scan whatever the element count + * [their nEntry > 1 gate only guards the pointer-sort driver], so a + * singleton NaN is rejected exactly like a NaN in a larger array; only the + * empty map scans trivially and stays a no-op success. + */ +static void HashmapSortCheckNumericValue(ph7_hashmap *pMap) { + ph7_hashmap_node *pNode = pMap->pFirst; + sxu32 n = pMap->nEntry; + ph7_value sVal; + PH7_MemObjInit(pMap->pVm, &sVal); + while(n > 0) { + ph7_value *pRaw = HashmapExtractNodeValue(pNode); + if(pRaw != 0) { + PH7_MemObjLoad(pRaw, &sVal); + PH7_MemObjToNumeric(&sVal); + if(sVal.nType & MEMOBJ_REAL && sVal.x.rVal != sVal.x.rVal) { + /* NaN: the numeric ordering is undefined */ + PH7_MemObjRelease(&sVal); + PH7_VmThrowError(pMap->pVm, PH7_CTX_ERR, + "Cannot sort: value is not a number (NaN)"); + /* NOT REACHED: PH7_CTX_ERR aborts script execution */ + return; } + PH7_MemObjRelease(&sVal); } - if(i == N_SORT_BUCKET - 1) { - /* To get here, there need to be 2^(N_SORT_BUCKET) elements in he input list. - * But that is impossible. - */ - a[i] = HashmapNodeMerge(a[i], p, xCmp, pCmpData); + /* Point to the next entry */ + pNode = pNode->pPrev; /* Reverse link */ + n--; + } +} +/* + * usort()/uasort()/uksort() comparator contract: the second argument is + * optional [omitting it selects the default comparator, which then applies + * the same category pre-checks as sort()], but when it IS supplied it must + * be callable. A present, non-callable comparator is a contract violation + * and raises a controlled fatal error naming the builtin [exit status 255]: + * the requested ordering rule can never be honoured, so falling back to the + * default comparator would only mask the mistake and can later report the + * misleading "Cannot sort values of mixed types" diagnostic. Raised before + * any reordering happens, so the array is left completely untouched. + */ +static void HashmapSortThrowNotCallable(ph7_hashmap *pMap, const char *zBuiltin) { + PH7_VmThrowError(pMap->pVm, PH7_CTX_ERR, + "%s(): The sorting comparator is not callable", zBuiltin); + /* NOT REACHED: PH7_CTX_ERR aborts script execution */ +} +/* + * Pointer-sort driver. The comparator operates directly on nodes through a + * shared HashmapSortCtx; a failing user callback raises the context abort + * flag [separate status channel, the comparator integer result is never + * overloaded], so the driver can stop before any reordering is committed + * to the map. + */ + +/* + * Stable merge of two sorted pointer arrays. On ties the left source wins, + * preserving the original insertion order [stable sort]. Returns SXRET_OK + * on success or SXERR_ABORT when the comparator raised the context abort + * flag; on failure the destination content is undefined and must be + * discarded. + */ +static sxi32 HashmapPtrMerge( + ph7_hashmap_node **apA, sxu32 nA, + ph7_hashmap_node **apB, sxu32 nB, + ph7_hashmap_node **apDest, + ProcNodeCmp xCmp, HashmapSortCtx *pCtx +) { + sxu32 i = 0, j = 0, k = 0; + while(i < nA && j < nB) { + sxi32 rc = xCmp(apA[i], apB[j], pCtx); + if(pCtx->bAbort) { + return SXERR_ABORT; + } + if(rc <= 0) { + apDest[k++] = apA[i++]; + } else { + apDest[k++] = apB[j++]; } } - p = a[0]; - for(i = 1; i < N_SORT_BUCKET; i++) { - p = HashmapNodeMerge(p, a[i], xCmp, pCmpData); + while(i < nA) { + apDest[k++] = apA[i++]; } - p->pNext = 0; - /* Reflect the change */ - pMap->pFirst = p; + while(j < nB) { + apDest[k++] = apB[j++]; + } + return SXRET_OK; +} + +/* + * Stable merge-sort over an array of node pointers. + * apSrc holds the input order, apDest receives the sorted order. + * On comparator failure no partial result escapes: the caller keeps + * the original list untouched. The recursion depth is O(log2(n)). + */ +static sxi32 HashmapPtrSortRec( + ph7_hashmap_node **apSrc, ph7_hashmap_node **apTmp, sxu32 n, + ProcNodeCmp xCmp, HashmapSortCtx *pCtx +) { + sxu32 nHalf = n / 2; + sxi32 rc; + if(n < 2) { + return SXRET_OK; /* Already sorted */ + } + /* Sort both halves into the scratch buffer */ + rc = HashmapPtrSortRec(apSrc, apTmp, nHalf, xCmp, pCtx); + if(rc != SXRET_OK) { + return rc; + } + rc = HashmapPtrSortRec(apSrc + nHalf, apTmp + nHalf, n - nHalf, xCmp, pCtx); + if(rc != SXRET_OK) { + return rc; + } + /* Merge the sorted halves into the scratch buffer, then copy back */ + rc = HashmapPtrMerge(apSrc, nHalf, apSrc + nHalf, n - nHalf, apTmp, xCmp, pCtx); + if(rc != SXRET_OK) { + return rc; + } + SyMemcpy(apTmp, apSrc, n * sizeof(ph7_hashmap_node *)); + return SXRET_OK; +} + +/* + * Sort the map by relinking an array of node pointers once the whole + * ordering is known. Guarantees: + * - a comparator failure [user callback error/exception raising the + * context abort flag] leaves the map completely untouched [fatal + * category violations are raised by a whole-set pre-scan before this + * driver runs]; + * - the sort is stable: equal elements keep their insertion order. + * Returns SXRET_OK or SXERR_ABORT. + */ +static sxi32 HashmapSortPointers(ph7_hashmap *pMap, ProcNodeCmp xCmp, HashmapSortCtx *pCtx) { + ph7_vm *pVm = pMap->pVm; + ph7_hashmap_node **apNodes, **apTmp; + ph7_hashmap_node *pNode; + sxu32 n = pMap->nEntry; + sxu32 i; + sxi32 rc; + /* Allocate the pointer arrays */ + apNodes = (ph7_hashmap_node **)SyMemBackendAlloc(&pVm->sAllocator, n * sizeof(ph7_hashmap_node *)); + if(apNodes == 0) { + PH7_VmMemoryError(pVm); + } + apTmp = (ph7_hashmap_node **)SyMemBackendAlloc(&pVm->sAllocator, n * sizeof(ph7_hashmap_node *)); + if(apTmp == 0) { + SyMemBackendFree(&pVm->sAllocator, apNodes); + PH7_VmMemoryError(pVm); + } + /* Collect the nodes in insertion order */ + pNode = pMap->pFirst; + for(i = 0 ; i < n ; ++i) { + apNodes[i] = pNode; + pNode = pNode->pPrev; /* Reverse link */ + } + /* Sort the pointers */ + rc = HashmapPtrSortRec(apNodes, apTmp, n, xCmp, pCtx); + if(rc != SXRET_OK) { + /* Comparator failed: the map is untouched */ + SyMemBackendFree(&pVm->sAllocator, apTmp); + SyMemBackendFree(&pVm->sAllocator, apNodes); + return rc; + } + /* Relink the map in the sorted order [only reached on success] */ + for(i = 0 ; i < n ; ++i) { + ph7_hashmap_node *pCur = apNodes[i]; + ph7_hashmap_node *pNext = i + 1 < n ? apNodes[i + 1] : (ph7_hashmap_node *)0; + pCur->pPrev = pNext; + pCur->pNext = i == 0 ? (ph7_hashmap_node *)0 : apNodes[i - 1]; + } + pMap->pFirst = apNodes[0]; + pMap->pLast = apNodes[n - 1]; /* Reset the loop cursor */ pMap->pCur = pMap->pFirst; + SyMemBackendFree(&pVm->sAllocator, apTmp); + SyMemBackendFree(&pVm->sAllocator, apNodes); return SXRET_OK; } /* * Node comparison callback. * used-by: [sort(),asort(),...] */ -static sxi32 HashmapCmpCallback1(ph7_hashmap_node *pA, ph7_hashmap_node *pB, void *pCmpData) { +static sxi32 HashmapCmpCallback1(ph7_hashmap_node *pA, ph7_hashmap_node *pB, void *pArg) { ph7_value sA, sB; sxi32 iFlags; int rc; - if(pCmpData == 0) { + HashmapSortCtx *pCtx = (HashmapSortCtx *)pArg; + if(pCtx->pCmpData == 0) { /* Perform a standard comparison */ rc = HashmapNodeCmp(pA, pB, FALSE); return rc; } - iFlags = SX_PTR_TO_INT(pCmpData); + iFlags = SX_PTR_TO_INT(pCtx->pCmpData); /* Duplicate node values */ PH7_MemObjInit(pA->pMap->pVm, &sA); PH7_MemObjInit(pA->pMap->pVm, &sB); @@ -1548,28 +1884,188 @@ static sxi32 HashmapCmpCallback1(ph7_hashmap_node *pA, ph7_hashmap_node *pB, voi PH7_MemObjToNumeric(&sA); PH7_MemObjToNumeric(&sB); } - /* Perform the comparison */ - rc = PH7_MemObjCmp(&sA, &sB, FALSE, 0); + /* Perform the comparison. Within an explicit sort mode the cast values + * are one category by construction, and the whole-set pre-scan has + * already rejected the NaN values that would report SXERR_NOMATCH + * [default mode: HashmapSortCheckValueCategory(), explicit numeric + * mode: HashmapSortCheckNumericValue()]. No sentinel can reach the + * descending comparator, so its negation only ever applies to the + * 3-way result. */ + rc = PH7_MemObjCmpSafe(&sA, &sB, FALSE, 0, 0); PH7_MemObjRelease(&sA); PH7_MemObjRelease(&sB); return rc; } +/* + * Verify the whole map holds one key category before any reordering happens + * [ksort()/krsort()/uksort() default mode]: + * - all-int keys or all-blob (string) keys are sortable; + * - a mix of int and string keys is a controlled fatal error raised by + * PH7_VmThrowError, so the map stays completely untouched [the error is + * raised before any relink]. + * Mirrors HashmapSortCheckValueCategory [which covers the value category + * pre-scan of sort()/asort()/...]; this one walks the key types instead. + */ +static void HashmapSortCheckKeyCategory(ph7_hashmap *pMap) { + ph7_hashmap_node *pNode = pMap->pFirst; + sxu32 n = pMap->nEntry; + sxi32 bInt = FALSE, bBlob = FALSE; + while(n > 0) { + if(pNode->iType == HASHMAP_INT_NODE) { + bInt = TRUE; + } else { + bBlob = TRUE; + } + if(bInt && bBlob) { + PH7_VmThrowError(pMap->pVm, PH7_CTX_ERR, + "Cannot sort keys of mixed types without an explicit rule"); + return; /* NOT REACHED on a fatal error */ + } + /* Point to the next entry */ + pNode = pNode->pPrev; /* Reverse link */ + n--; + } +} +/* + * Shared preamble for the flag-taking sort builtins [sort()/asort()/arsort()/ + * rsort() on values, ksort()/krsort() on keys]: extract the comparison flags + * from the optional second argument and run the category pre-scan that + * corresponds to those flags, so every controlled fatal [mixed categories, + * NaN under an explicit numeric mode] is raised before the pointer-sort + * driver ever relinks a node. Returns the effective flag set [0 == default + * mode] for the sort context. bKeys is TRUE only for the key-sorted + * builtins, whose nodes compare keys and are guarded by the key pre-scan. + * The value builtins [sort()/asort()/arsort()/rsort()] call this before + * their nEntry > 1 gate: the scan covers single-element arrays too [an + * empty map passes trivially], while the key-sorted builtins keep their + * gate, where a singleton can never hold a mixed key category anyway. + */ +static sxi32 HashmapSortPreCheck(ph7_hashmap *pMap, int nArg, ph7_value **apArg, int bKeys) { + sxi32 iCmpFlags = 0; + if(nArg > 1) { + /* Extract comparison flags */ + iCmpFlags = ph7_value_to_int(apArg[1]); + if(iCmpFlags == 3 /* SORT_REGULAR */) { + iCmpFlags = 0; /* Standard comparison */ + } + } + if(iCmpFlags == 0) { + if(bKeys) { + /* Default mode: verify the whole map holds one key category + * first, so the mixed-keys error is raised before any relink */ + HashmapSortCheckKeyCategory(pMap); + } else { + /* Default mode: verify the whole map is one sortable category first */ + HashmapSortCheckValueCategory(pMap); + } + } else if(iCmpFlags != 5 && !bKeys) { + /* Explicit numeric mode [HashmapCmpCallback1() casts every flag + * except SORT_STRING numerically]: run the cast on copies and + * reject NaN across the whole set before any relink */ + HashmapSortCheckNumericValue(pMap); + } + return iCmpFlags; +} +/* + * Unsigned byte lexicographic comparison of two blob keys [the STRING key + * ordering contract]: compare the common prefix byte by byte treating each + * byte as unsigned; when one key is an exact prefix of the other, the + * shorter key sorts first. Mirrors the string ordering implemented by + * MemObjCmpString() in memobj.c. Equality semantics are unchanged: the + * result is 0 exactly when both keys hold identical bytes. + */ +static sxi32 HashmapKeyBlobCmp(SyBlob *pLeft, SyBlob *pRight) { + const unsigned char *zA = (const unsigned char *)SyBlobData(pLeft); + const unsigned char *zB = (const unsigned char *)SyBlobData(pRight); + sxu32 nA = SyBlobLength(pLeft), nB = SyBlobLength(pRight); + sxu32 n = nA < nB ? nA : nB; + sxu32 i; + for(i = 0 ; i < n ; ++i) { + if(zA[i] != zB[i]) { + return zA[i] < zB[i] ? -1 : 1; + } + } + if(nA != nB) { + return nA < nB ? -1 : 1; + } + return 0; +} /* * Node comparison callback: Compare nodes by keys only. * used-by: [ksort()] + * + * Default mode (iFlags 0, SORT_REGULAR): the whole map must hold one key + * category only [enforced by the HashmapSortCheckKeyCategory() pre-scan, + * which runs before the sort driver so the error is raised before any + * relink]. All-int keys compare relationally with explicit sign logic + * [no (sxi32)(iA-iB) overflow], all-blob keys compare with the unsigned + * byte lexicographic order [HashmapKeyBlobCmp()]. + * + * SORT_NUMERIC (flag 4): blob keys are parsed as 64-bit integers [existing + * behavior; non-numeric keys yield 0] and compared against int keys. + * + * SORT_STRING (flag 5): all keys are compared as byte strings; int keys are + * rendered to their decimal representation. This is an explicitly chosen rule. */ -static sxi32 HashmapCmpCallback2(ph7_hashmap_node *pA, ph7_hashmap_node *pB, void *pCmpData) { - sxi32 rc; - SXUNUSED(pCmpData); /* cc warning */ +static sxi32 HashmapKeyCmpDefault(ph7_hashmap_node *pA, ph7_hashmap_node *pB) { + if(pA->iType == HASHMAP_INT_NODE && pB->iType == HASHMAP_INT_NODE) { + /* Relational int compare without overflow */ + if(pA->xKey.iKey > pB->xKey.iKey) { + return 1; + } else if(pA->xKey.iKey < pB->xKey.iKey) { + return -1; + } + return 0; + } if(pA->iType == HASHMAP_BLOB_NODE && pB->iType == HASHMAP_BLOB_NODE) { - /* Perform a string comparison */ - rc = SyBlobCmp(&pA->xKey.sKey, &pB->xKey.sKey); - } else { + /* Unsigned byte lexicographic string-key ordering */ + return HashmapKeyBlobCmp(&pA->xKey.sKey, &pB->xKey.sKey); + } + /* Mixed int/blob key categories: defensive only, the whole-set key + * pre-scan raises this fatal error before the sort driver runs. */ + PH7_VmThrowError(pA->pMap->pVm, PH7_CTX_ERR, + "Cannot sort keys of mixed types without an explicit rule"); + /* NOT REACHED */ + return 0; +} +static sxi32 HashmapCmpCallback2(ph7_hashmap_node *pA, ph7_hashmap_node *pB, void *pArg) { + HashmapSortCtx *pCtx = (HashmapSortCtx *)pArg; + sxi32 iFlags = SX_PTR_TO_INT(pCtx->pCmpData); + sxi32 rc; + if(iFlags == 5 /* SORT_STRING */) { + /* Compare all keys as byte strings; int keys use their decimal form */ + SyBlob sKeyA, sKeyB; + SyBlob sTmpA, sTmpB; + ph7_hashmap_node *apNode[2]; + sxu32 i; + apNode[0] = pA; + apNode[1] = pB; + SyBlobInit(&sTmpA, &pA->pMap->pVm->sAllocator); + SyBlobInit(&sTmpB, &pA->pMap->pVm->sAllocator); + SyBlobInit(&sKeyA, &pA->pMap->pVm->sAllocator); + SyBlobInit(&sKeyB, &pA->pMap->pVm->sAllocator); + for(i = 0 ; i < 2 ; ++i) { + SyBlob *pTarget = i == 0 ? &sKeyA : &sKeyB; + SyBlob *pTmp = i == 0 ? &sTmpA : &sTmpB; + if(apNode[i]->iType == HASHMAP_INT_NODE) { + /* Render the int key as a decimal string */ + SyBlobFormat(pTmp, "%qd", apNode[i]->xKey.iKey); + SyBlobDup(pTmp, pTarget); + SyBlobRelease(pTmp); + } else { + SyBlobDup(&apNode[i]->xKey.sKey, pTarget); + } + } + rc = HashmapKeyBlobCmp(&sKeyA, &sKeyB); + SyBlobRelease(&sKeyA); + SyBlobRelease(&sKeyB); + return rc; + } + if(iFlags == 4 /* SORT_NUMERIC */) { + /* Numeric key comparison; blob keys are parsed as integers */ SyString sStr; sxi64 iA, iB; - /* Perform a numeric comparison */ if(pA->iType == HASHMAP_BLOB_NODE) { - /* Cast to 64-bit integer */ SyStringInitFromBuf(&sStr, SyBlobData(&pA->xKey.sKey), SyBlobLength(&pA->xKey.sKey)); if(sStr.nByte < 1) { iA = 0; @@ -1580,7 +2076,6 @@ static sxi32 HashmapCmpCallback2(ph7_hashmap_node *pA, ph7_hashmap_node *pB, voi iA = pA->xKey.iKey; } if(pB->iType == HASHMAP_BLOB_NODE) { - /* Cast to 64-bit integer */ SyStringInitFromBuf(&sStr, SyBlobData(&pB->xKey.sKey), SyBlobLength(&pB->xKey.sKey)); if(sStr.nByte < 1) { iB = 0; @@ -1590,60 +2085,41 @@ static sxi32 HashmapCmpCallback2(ph7_hashmap_node *pA, ph7_hashmap_node *pB, voi } else { iB = pB->xKey.iKey; } - rc = (sxi32)(iA - iB); + if(iA > iB) { + rc = 1; + } else if(iA < iB) { + rc = -1; + } else { + rc = 0; + } + return rc; } - /* Comparison result */ - return rc; + /* Default mode */ + return HashmapKeyCmpDefault(pA, pB); } /* * Node comparison callback. * Used by: [rsort(),arsort()]; + * Same rules as HashmapCmpCallback1 with a negated result. The negation is + * safe because HashmapCmpCallback1 only ever returns the 3-way value + * [-1/0/+1]: the whole-set pre-scans reject the NaN pairs that would + * report SXERR_NOMATCH before the sort driver ever runs. */ -static sxi32 HashmapCmpCallback3(ph7_hashmap_node *pA, ph7_hashmap_node *pB, void *pCmpData) { - ph7_value sA, sB; - sxi32 iFlags; - int rc; - if(pCmpData == 0) { - /* Perform a standard comparison */ - rc = HashmapNodeCmp(pA, pB, FALSE); - return -rc; - } - iFlags = SX_PTR_TO_INT(pCmpData); - /* Duplicate node values */ - PH7_MemObjInit(pA->pMap->pVm, &sA); - PH7_MemObjInit(pA->pMap->pVm, &sB); - PH7_HashmapExtractNodeValue(pA, &sA, FALSE); - PH7_HashmapExtractNodeValue(pB, &sB, FALSE); - if(iFlags == 5) { - /* String cast */ - if((sA.nType & MEMOBJ_STRING) == 0) { - PH7_MemObjToString(&sA); - } - if((sB.nType & MEMOBJ_STRING) == 0) { - PH7_MemObjToString(&sB); - } - } else { - /* Numeric cast */ - PH7_MemObjToNumeric(&sA); - PH7_MemObjToNumeric(&sB); - } - /* Perform the comparison */ - rc = PH7_MemObjCmp(&sA, &sB, FALSE, 0); - PH7_MemObjRelease(&sA); - PH7_MemObjRelease(&sB); - return -rc; +static sxi32 HashmapCmpCallback3(ph7_hashmap_node *pA, ph7_hashmap_node *pB, void *pArg) { + return -HashmapCmpCallback1(pA, pB, pArg); /* Reverse result */ } /* * Node comparison callback: Invoke an user-defined callback for the purpose of node comparison. * used-by: [usort(),uasort()] */ -static sxi32 HashmapCmpCallback4(ph7_hashmap_node *pA, ph7_hashmap_node *pB, void *pCmpData) { +static sxi32 HashmapCmpCallback4(ph7_hashmap_node *pA, ph7_hashmap_node *pB, void *pArg) { ph7_value sResult, *pCallback; ph7_value *pV1, *pV2; ph7_value *apArg[2]; /* Callback arguments */ - sxi32 rc; + HashmapSortCtx *pCtx = (HashmapSortCtx *)pArg; + sxi32 rc, iCmp; /* Point to the desired callback */ - pCallback = (ph7_value *)pCmpData; + pCallback = (ph7_value *)pCtx->pCmpData; /* initialize the result value */ PH7_MemObjInit(pA->pMap->pVm, &sResult); /* Extract nodes values */ @@ -1654,71 +2130,62 @@ static sxi32 HashmapCmpCallback4(ph7_hashmap_node *pA, ph7_hashmap_node *pB, voi /* Invoke the callback */ rc = PH7_VmCallUserFunction(pA->pMap->pVm, pCallback, 2, apArg, &sResult); if(rc != SXRET_OK) { - /* An error occurred while calling user defined function [i.e: not defined] */ - rc = -1; /* Set a dummy result */ - } else { - /* Extract callback result */ - if((sResult.nType & MEMOBJ_INT) == 0) { - /* Perform an int cast */ - PH7_MemObjToInteger(&sResult); + /* An error occurred while calling user defined function: raise the + * context abort flag [separate status channel: the callback's own + * integer result is never overloaded] and stop the sort. The + * comparator's failure must reach the VM with its ORIGINAL status: + * on PH7_EXCEPTION the nested completion already stored the thrown + * value into sResult, so transfer it to the carrier [first failing + * comparator wins] instead of releasing it here. */ + if(!pCtx->bAbort) { + pCtx->iPendingRC = rc; + if(rc == PH7_EXCEPTION) { + PH7_MemObjStore(&sResult, &pCtx->sPending); + } + pCtx->bAbort = TRUE; } - rc = (sxi32)sResult.x.iVal; + PH7_MemObjRelease(&sResult); + return 0; } + /* Extract callback result. The comparator contract speaks in signs: + * classify the full int64 against zero BEFORE any narrowing. A plain + * (sxi32) cast would fold INT64_MAX to -1 [direction inverted], 2^31 + * to a sign flip, and ±2^32 or INT64_MIN to 0 [distinct magnitudes + * reported equal]. The reduced -1/0/+1 is also the only value the + * descending comparators ever negate [never a raw magnitude or + * signed MIN]. */ + if((sResult.nType & MEMOBJ_INT) == 0) { + /* Perform an int cast */ + PH7_MemObjToInteger(&sResult); + } + iCmp = sResult.x.iVal > 0 ? 1 : (sResult.x.iVal < 0 ? -1 : 0); PH7_MemObjRelease(&sResult); /* Callback result */ - return rc; + return iCmp; } /* * Node comparison callback: Compare nodes by keys only. * used-by: [krsort()] + * Mirrors HashmapCmpCallback2 with a negated result; refer to that function + * for the mode rules (default/SORT_NUMERIC/SORT_STRING). */ -static sxi32 HashmapCmpCallback5(ph7_hashmap_node *pA, ph7_hashmap_node *pB, void *pCmpData) { - sxi32 rc; - SXUNUSED(pCmpData); /* cc warning */ - if(pA->iType == HASHMAP_BLOB_NODE && pB->iType == HASHMAP_BLOB_NODE) { - /* Perform a string comparison */ - rc = SyBlobCmp(&pA->xKey.sKey, &pB->xKey.sKey); - } else { - SyString sStr; - sxi64 iA, iB; - /* Perform a numeric comparison */ - if(pA->iType == HASHMAP_BLOB_NODE) { - /* Cast to 64-bit integer */ - SyStringInitFromBuf(&sStr, SyBlobData(&pA->xKey.sKey), SyBlobLength(&pA->xKey.sKey)); - if(sStr.nByte < 1) { - iA = 0; - } else { - SyStrToInt64(sStr.zString, sStr.nByte, (void *)&iA, 0); - } - } else { - iA = pA->xKey.iKey; - } - if(pB->iType == HASHMAP_BLOB_NODE) { - /* Cast to 64-bit integer */ - SyStringInitFromBuf(&sStr, SyBlobData(&pB->xKey.sKey), SyBlobLength(&pB->xKey.sKey)); - if(sStr.nByte < 1) { - iB = 0; - } else { - SyStrToInt64(sStr.zString, sStr.nByte, (void *)&iB, 0); - } - } else { - iB = pB->xKey.iKey; - } - rc = (sxi32)(iA - iB); - } - return -rc; /* Reverse result */ +static sxi32 HashmapCmpCallback5(ph7_hashmap_node *pA, ph7_hashmap_node *pB, void *pArg) { + return -HashmapCmpCallback2(pA, pB, pArg); /* Reverse result */ } /* * Node comparison callback: Invoke an user-defined callback for the purpose of node comparison. * used-by: [uksort()] + * Mirrors HashmapCmpCallback4 on keys; a failed callback invocation raises + * the context abort flag. */ -static sxi32 HashmapCmpCallback6(ph7_hashmap_node *pA, ph7_hashmap_node *pB, void *pCmpData) { +static sxi32 HashmapCmpCallback6(ph7_hashmap_node *pA, ph7_hashmap_node *pB, void *pArg) { ph7_value sResult, *pCallback; ph7_value *apArg[2]; /* Callback arguments */ ph7_value sK1, sK2; - sxi32 rc; + HashmapSortCtx *pCtx = (HashmapSortCtx *)pArg; + sxi32 rc, iCmp; /* Point to the desired callback */ - pCallback = (ph7_value *)pCmpData; + pCallback = (ph7_value *)pCtx->pCmpData; /* initialize the result value */ PH7_MemObjInit(pA->pMap->pVm, &sResult); PH7_MemObjInit(pA->pMap->pVm, &sK1); @@ -1734,21 +2201,40 @@ static sxi32 HashmapCmpCallback6(ph7_hashmap_node *pA, ph7_hashmap_node *pB, voi /* Invoke the callback */ rc = PH7_VmCallUserFunction(pA->pMap->pVm, pCallback, 2, apArg, &sResult); if(rc != SXRET_OK) { - /* An error ocurred while calling user defined function [i.e: not defined] */ - rc = -1; /* Set a dummy result */ - } else { - /* Extract callback result */ - if((sResult.nType & MEMOBJ_INT) == 0) { - /* Perform an int cast */ - PH7_MemObjToInteger(&sResult); + /* An error ocurred while calling user defined function: raise the + * context abort flag [separate status channel] and stop the sort. + * As in HashmapCmpCallback4, the thrown value transferred into + * sResult is preserved in the carrier [first failing comparator + * wins] so the builtin can propagate the original status. */ + if(!pCtx->bAbort) { + pCtx->iPendingRC = rc; + if(rc == PH7_EXCEPTION) { + PH7_MemObjStore(&sResult, &pCtx->sPending); + } + pCtx->bAbort = TRUE; } - rc = (sxi32)sResult.x.iVal; + PH7_MemObjRelease(&sResult); + PH7_MemObjRelease(&sK1); + PH7_MemObjRelease(&sK2); + return 0; } + /* Extract callback result. The comparator contract speaks in signs: + * classify the full int64 against zero BEFORE any narrowing. A plain + * (sxi32) cast would fold INT64_MAX to -1 [direction inverted], 2^31 + * to a sign flip, and ±2^32 or INT64_MIN to 0 [distinct magnitudes + * reported equal]. The reduced -1/0/+1 is also the only value the + * descending comparators ever negate [never a raw magnitude or + * signed MIN]. */ + if((sResult.nType & MEMOBJ_INT) == 0) { + /* Perform an int cast */ + PH7_MemObjToInteger(&sResult); + } + iCmp = sResult.x.iVal > 0 ? 1 : (sResult.x.iVal < 0 ? -1 : 0); PH7_MemObjRelease(&sResult); PH7_MemObjRelease(&sK1); PH7_MemObjRelease(&sK2); /* Callback result */ - return rc; + return iCmp; } /* * Node comparison callback: Random node comparison. @@ -1775,10 +2261,11 @@ static void HashmapSortRehash(ph7_hashmap *pMap) { /* Rehash all entries */ pLast = p = pMap->pFirst; pMap->iNextIdx = 0; /* Reset the automatic index */ + pMap->bIdxExhausted = FALSE; /* All int keys are re-numbered from zero */ i = 0; for(;;) { if(i >= pMap->nEntry) { - pMap->pLast = pLast; /* Fix the last link broken by the merge-sort */ + pMap->pLast = pLast; /* Track the tail while rehashing [links are already sorted] */ break; } if(p->iType == HASHMAP_BLOB_NODE) { @@ -1820,6 +2307,7 @@ static void HashmapSortRehash(ph7_hashmap *pMap) { */ static int ph7_hashmap_sort(ph7_context *pCtx, int nArg, ph7_value **apArg) { ph7_hashmap *pMap; + sxi32 iCmpFlags; /* Make sure we are dealing with a valid hashmap */ if(nArg < 1 || !ph7_value_is_array(apArg[0])) { /* Missing/Invalid arguments, return FALSE */ @@ -1828,17 +2316,26 @@ static int ph7_hashmap_sort(ph7_context *pCtx, int nArg, ph7_value **apArg) { } /* Point to the internal representation of the input hashmap */ pMap = (ph7_hashmap *)apArg[0]->x.pOther; + /* The value-category pre-scan and the NaN validation run whatever the + * element count: a single-element array holding an unsortable value is + * the same controlled fatal error as in a larger array. The nEntry > 1 + * gate only guards the reordering driver, so an empty array and a valid + * single-element array stay a success without any relink or rehash + * [no key renumbering, no allocation churn]. */ + iCmpFlags = HashmapSortPreCheck(pMap, nArg, apArg, FALSE); if(pMap->nEntry > 1) { - sxi32 iCmpFlags = 0; - if(nArg > 1) { - /* Extract comparison flags */ - iCmpFlags = ph7_value_to_int(apArg[1]); - if(iCmpFlags == 3 /* SORT_REGULAR */) { - iCmpFlags = 0; /* Standard comparison */ - } + HashmapSortCtx sSortCtx; + sSortCtx.pCmpData = SX_INT_TO_PTR(iCmpFlags); + sSortCtx.bAbort = FALSE; + sSortCtx.iPendingRC = 0; + PH7_MemObjInit(pMap->pVm, &sSortCtx.sPending); + /* Sort via node pointers so a failure never reorders the map */ + if(HashmapSortPointers(pMap, HashmapCmpCallback1, &sSortCtx) != SXRET_OK) { + /* Comparator failure: the map is completely untouched and the + * comparator's original status [exception value / abort] is + * propagated instead of being flattened into a FALSE return */ + return HashmapSortFailure(pCtx, &sSortCtx); } - /* Do the merge sort */ - HashmapMergeSort(pMap, HashmapCmpCallback1, SX_INT_TO_PTR(iCmpFlags)); /* Rehash [Do not maintain index association as requested by the PHP specification] */ HashmapSortRehash(pMap); } @@ -1863,6 +2360,7 @@ static int ph7_hashmap_sort(ph7_context *pCtx, int nArg, ph7_value **apArg) { */ static int ph7_hashmap_asort(ph7_context *pCtx, int nArg, ph7_value **apArg) { ph7_hashmap *pMap; + sxi32 iCmpFlags; /* Make sure we are dealing with a valid hashmap */ if(nArg < 1 || !ph7_value_is_array(apArg[0])) { /* Missing/Invalid arguments, return FALSE */ @@ -1871,20 +2369,25 @@ static int ph7_hashmap_asort(ph7_context *pCtx, int nArg, ph7_value **apArg) { } /* Point to the internal representation of the input hashmap */ pMap = (ph7_hashmap *)apArg[0]->x.pOther; + /* The value-category pre-scan and the NaN validation run whatever the + * element count [see ph7_hashmap_sort()]: a single-element array is + * validated like any other, and only the reordering driver below is + * gated, so empty arrays and valid single-element arrays succeed + * without any relink [their association and keys stay untouched]. */ + iCmpFlags = HashmapSortPreCheck(pMap, nArg, apArg, FALSE); if(pMap->nEntry > 1) { - sxi32 iCmpFlags = 0; - if(nArg > 1) { - /* Extract comparison flags */ - iCmpFlags = ph7_value_to_int(apArg[1]); - if(iCmpFlags == 3 /* SORT_REGULAR */) { - iCmpFlags = 0; /* Standard comparison */ - } - } - /* Do the merge sort */ - HashmapMergeSort(pMap, HashmapCmpCallback1, SX_INT_TO_PTR(iCmpFlags)); - /* Fix the last link broken by the merge */ - while(pMap->pLast->pPrev) { - pMap->pLast = pMap->pLast->pPrev; + HashmapSortCtx sSortCtx; + sSortCtx.pCmpData = SX_INT_TO_PTR(iCmpFlags); + sSortCtx.bAbort = FALSE; + sSortCtx.iPendingRC = 0; + PH7_MemObjInit(pMap->pVm, &sSortCtx.sPending); + /* Sort via node pointers so a failure never reorders the map + * [the driver relinks and fixes pFirst/pLast on success only] */ + if(HashmapSortPointers(pMap, HashmapCmpCallback1, &sSortCtx) != SXRET_OK) { + /* Comparator failure: the map is completely untouched and the + * comparator's original status [exception value / abort] is + * propagated instead of being flattened into a FALSE return */ + return HashmapSortFailure(pCtx, &sSortCtx); } } /* All done, return TRUE */ @@ -1908,6 +2411,7 @@ static int ph7_hashmap_asort(ph7_context *pCtx, int nArg, ph7_value **apArg) { */ static int ph7_hashmap_arsort(ph7_context *pCtx, int nArg, ph7_value **apArg) { ph7_hashmap *pMap; + sxi32 iCmpFlags; /* Make sure we are dealing with a valid hashmap */ if(nArg < 1 || !ph7_value_is_array(apArg[0])) { /* Missing/Invalid arguments, return FALSE */ @@ -1916,20 +2420,25 @@ static int ph7_hashmap_arsort(ph7_context *pCtx, int nArg, ph7_value **apArg) { } /* Point to the internal representation of the input hashmap */ pMap = (ph7_hashmap *)apArg[0]->x.pOther; + /* The value-category pre-scan and the NaN validation run whatever the + * element count [see ph7_hashmap_sort(); the descending variants are + * validated identically], and only the reordering driver below is + * gated, so empty arrays and valid single-element arrays succeed + * without any relink [their association and keys stay untouched]. */ + iCmpFlags = HashmapSortPreCheck(pMap, nArg, apArg, FALSE); if(pMap->nEntry > 1) { - sxi32 iCmpFlags = 0; - if(nArg > 1) { - /* Extract comparison flags */ - iCmpFlags = ph7_value_to_int(apArg[1]); - if(iCmpFlags == 3 /* SORT_REGULAR */) { - iCmpFlags = 0; /* Standard comparison */ - } - } - /* Do the merge sort */ - HashmapMergeSort(pMap, HashmapCmpCallback3, SX_INT_TO_PTR(iCmpFlags)); - /* Fix the last link broken by the merge */ - while(pMap->pLast->pPrev) { - pMap->pLast = pMap->pLast->pPrev; + HashmapSortCtx sSortCtx; + sSortCtx.pCmpData = SX_INT_TO_PTR(iCmpFlags); + sSortCtx.bAbort = FALSE; + sSortCtx.iPendingRC = 0; + PH7_MemObjInit(pMap->pVm, &sSortCtx.sPending); + /* Sort via node pointers so a failure never reorders the map + * [the driver relinks and fixes pFirst/pLast on success only] */ + if(HashmapSortPointers(pMap, HashmapCmpCallback3, &sSortCtx) != SXRET_OK) { + /* Comparator failure: the map is completely untouched and the + * comparator's original status [exception value / abort] is + * propagated instead of being flattened into a FALSE return */ + return HashmapSortFailure(pCtx, &sSortCtx); } } /* All done, return TRUE */ @@ -1962,20 +2471,22 @@ static int ph7_hashmap_ksort(ph7_context *pCtx, int nArg, ph7_value **apArg) { /* Point to the internal representation of the input hashmap */ pMap = (ph7_hashmap *)apArg[0]->x.pOther; if(pMap->nEntry > 1) { - sxi32 iCmpFlags = 0; - if(nArg > 1) { - /* Extract comparison flags */ - iCmpFlags = ph7_value_to_int(apArg[1]); - if(iCmpFlags == 3 /* SORT_REGULAR */) { - iCmpFlags = 0; /* Standard comparison */ + sxi32 iCmpFlags = HashmapSortPreCheck(pMap, nArg, apArg, TRUE); + { + HashmapSortCtx sSortCtx; + sSortCtx.pCmpData = SX_INT_TO_PTR(iCmpFlags); + sSortCtx.bAbort = FALSE; + sSortCtx.iPendingRC = 0; + PH7_MemObjInit(pMap->pVm, &sSortCtx.sPending); + /* Sort via node pointers so a failure never reorders the map + * [the driver relinks and fixes pFirst/pLast on success only] */ + if(HashmapSortPointers(pMap, HashmapCmpCallback2, &sSortCtx) != SXRET_OK) { + /* Comparator failure: the map is completely untouched and the + * comparator's original status [exception value / abort] is + * propagated instead of being flattened into a FALSE return */ + return HashmapSortFailure(pCtx, &sSortCtx); } } - /* Do the merge sort */ - HashmapMergeSort(pMap, HashmapCmpCallback2, SX_INT_TO_PTR(iCmpFlags)); - /* Fix the last link broken by the merge */ - while(pMap->pLast->pPrev) { - pMap->pLast = pMap->pLast->pPrev; - } } /* All done, return TRUE */ ph7_result_bool(pCtx, 1); @@ -2007,20 +2518,22 @@ static int ph7_hashmap_krsort(ph7_context *pCtx, int nArg, ph7_value **apArg) { /* Point to the internal representation of the input hashmap */ pMap = (ph7_hashmap *)apArg[0]->x.pOther; if(pMap->nEntry > 1) { - sxi32 iCmpFlags = 0; - if(nArg > 1) { - /* Extract comparison flags */ - iCmpFlags = ph7_value_to_int(apArg[1]); - if(iCmpFlags == 3 /* SORT_REGULAR */) { - iCmpFlags = 0; /* Standard comparison */ + sxi32 iCmpFlags = HashmapSortPreCheck(pMap, nArg, apArg, TRUE); + { + HashmapSortCtx sSortCtx; + sSortCtx.pCmpData = SX_INT_TO_PTR(iCmpFlags); + sSortCtx.bAbort = FALSE; + sSortCtx.iPendingRC = 0; + PH7_MemObjInit(pMap->pVm, &sSortCtx.sPending); + /* Sort via node pointers so a failure never reorders the map + * [the driver relinks and fixes pFirst/pLast on success only] */ + if(HashmapSortPointers(pMap, HashmapCmpCallback5, &sSortCtx) != SXRET_OK) { + /* Comparator failure: the map is completely untouched and the + * comparator's original status [exception value / abort] is + * propagated instead of being flattened into a FALSE return */ + return HashmapSortFailure(pCtx, &sSortCtx); } } - /* Do the merge sort */ - HashmapMergeSort(pMap, HashmapCmpCallback5, SX_INT_TO_PTR(iCmpFlags)); - /* Fix the last link broken by the merge */ - while(pMap->pLast->pPrev) { - pMap->pLast = pMap->pLast->pPrev; - } } /* All done, return TRUE */ ph7_result_bool(pCtx, 1); @@ -2043,6 +2556,7 @@ static int ph7_hashmap_krsort(ph7_context *pCtx, int nArg, ph7_value **apArg) { */ static int ph7_hashmap_rsort(ph7_context *pCtx, int nArg, ph7_value **apArg) { ph7_hashmap *pMap; + sxi32 iCmpFlags; /* Make sure we are dealing with a valid hashmap */ if(nArg < 1 || !ph7_value_is_array(apArg[0])) { /* Missing/Invalid arguments, return FALSE */ @@ -2051,17 +2565,25 @@ static int ph7_hashmap_rsort(ph7_context *pCtx, int nArg, ph7_value **apArg) { } /* Point to the internal representation of the input hashmap */ pMap = (ph7_hashmap *)apArg[0]->x.pOther; + /* The value-category pre-scan and the NaN validation run whatever the + * element count [see ph7_hashmap_sort(); the descending variant is + * validated identically]. Only the reordering driver and its rehash + * below stay gated: an empty array and a valid single-element array + * succeed without any relink or rehash [no key renumbering]. */ + iCmpFlags = HashmapSortPreCheck(pMap, nArg, apArg, FALSE); if(pMap->nEntry > 1) { - sxi32 iCmpFlags = 0; - if(nArg > 1) { - /* Extract comparison flags */ - iCmpFlags = ph7_value_to_int(apArg[1]); - if(iCmpFlags == 3 /* SORT_REGULAR */) { - iCmpFlags = 0; /* Standard comparison */ - } + HashmapSortCtx sSortCtx; + sSortCtx.pCmpData = SX_INT_TO_PTR(iCmpFlags); + sSortCtx.bAbort = FALSE; + sSortCtx.iPendingRC = 0; + PH7_MemObjInit(pMap->pVm, &sSortCtx.sPending); + /* Sort via node pointers so a failure never reorders the map */ + if(HashmapSortPointers(pMap, HashmapCmpCallback3, &sSortCtx) != SXRET_OK) { + /* Comparator failure: the map is completely untouched and the + * comparator's original status [exception value / abort] is + * propagated instead of being flattened into a FALSE return */ + return HashmapSortFailure(pCtx, &sSortCtx); } - /* Do the merge sort */ - HashmapMergeSort(pMap, HashmapCmpCallback3, SX_INT_TO_PTR(iCmpFlags)); /* Rehash [Do not maintain index association as requested by the PHP specification] */ HashmapSortRehash(pMap); } @@ -2081,7 +2603,9 @@ static int ph7_hashmap_rsort(ph7_context *pCtx, int nArg, ph7_value **apArg) { * to, or greater than the second. * int callback ( mixed $a, mixed $b ) * Return - * TRUE on success or FALSE on failure. + * TRUE on success. A comparator that throws or aborts propagates its status + * to the caller [exception unwinding / script abort] instead of returning + * FALSE; the array is left untouched. */ static int ph7_hashmap_usort(ph7_context *pCtx, int nArg, ph7_value **apArg) { ph7_hashmap *pMap; @@ -2093,19 +2617,40 @@ static int ph7_hashmap_usort(ph7_context *pCtx, int nArg, ph7_value **apArg) { } /* Point to the internal representation of the input hashmap */ pMap = (ph7_hashmap *)apArg[0]->x.pOther; + /* A supplied comparator must be callable, whatever the array size: + * controlled fatal error [raised before any reordering, so the array + * is left completely untouched], never a silent fallback */ + if(nArg > 1 && !ph7_value_is_callable(apArg[1])) { + HashmapSortThrowNotCallable(pMap, "usort"); + /* NOT REACHED */ + } if(pMap->nEntry > 1) { ph7_value *pCallback = 0; ProcNodeCmp xCmp; xCmp = HashmapCmpCallback4; /* User-defined function as the comparison callback */ - if(nArg > 1 && ph7_value_is_callable(apArg[1])) { + if(nArg > 1) { /* Point to the desired callback */ pCallback = apArg[1]; } else { - /* Use the default comparison function */ + /* No comparator supplied: use the default comparison function */ xCmp = HashmapCmpCallback1; + /* Default mode: verify the whole map is one sortable category first */ + HashmapSortCheckValueCategory(pMap); + } + { + HashmapSortCtx sSortCtx; + sSortCtx.pCmpData = pCallback; + sSortCtx.bAbort = FALSE; + sSortCtx.iPendingRC = 0; + PH7_MemObjInit(pMap->pVm, &sSortCtx.sPending); + /* Sort via node pointers: a callback failure leaves the map untouched */ + if(HashmapSortPointers(pMap, xCmp, &sSortCtx) != SXRET_OK) { + /* Comparator failure: the map is completely untouched and the + * comparator's original status [exception value / abort] is + * propagated instead of being flattened into a FALSE return */ + return HashmapSortFailure(pCtx, &sSortCtx); + } } - /* Do the merge sort */ - HashmapMergeSort(pMap, xCmp, pCallback); /* Rehash [Do not maintain index association as requested by the PHP specification] */ HashmapSortRehash(pMap); } @@ -2126,7 +2671,9 @@ static int ph7_hashmap_usort(ph7_context *pCtx, int nArg, ph7_value **apArg) { * to, or greater than the second. * int callback ( mixed $a, mixed $b ) * Return - * TRUE on success or FALSE on failure. + * TRUE on success. A comparator that throws or aborts propagates its status + * to the caller [exception unwinding / script abort] instead of returning + * FALSE; the array and its index association are left untouched. */ static int ph7_hashmap_uasort(ph7_context *pCtx, int nArg, ph7_value **apArg) { ph7_hashmap *pMap; @@ -2138,22 +2685,40 @@ static int ph7_hashmap_uasort(ph7_context *pCtx, int nArg, ph7_value **apArg) { } /* Point to the internal representation of the input hashmap */ pMap = (ph7_hashmap *)apArg[0]->x.pOther; + /* A supplied comparator must be callable, whatever the array size: + * controlled fatal error [raised before any reordering, so the array + * is left completely untouched], never a silent fallback */ + if(nArg > 1 && !ph7_value_is_callable(apArg[1])) { + HashmapSortThrowNotCallable(pMap, "uasort"); + /* NOT REACHED */ + } if(pMap->nEntry > 1) { ph7_value *pCallback = 0; ProcNodeCmp xCmp; xCmp = HashmapCmpCallback4; /* User-defined function as the comparison callback */ - if(nArg > 1 && ph7_value_is_callable(apArg[1])) { + if(nArg > 1) { /* Point to the desired callback */ pCallback = apArg[1]; } else { - /* Use the default comparison function */ + /* No comparator supplied: use the default comparison function */ xCmp = HashmapCmpCallback1; + /* Default mode: verify the whole map is one sortable category first */ + HashmapSortCheckValueCategory(pMap); } - /* Do the merge sort */ - HashmapMergeSort(pMap, xCmp, pCallback); - /* Fix the last link broken by the merge */ - while(pMap->pLast->pPrev) { - pMap->pLast = pMap->pLast->pPrev; + { + HashmapSortCtx sSortCtx; + sSortCtx.pCmpData = pCallback; + sSortCtx.bAbort = FALSE; + sSortCtx.iPendingRC = 0; + PH7_MemObjInit(pMap->pVm, &sSortCtx.sPending); + /* Sort via node pointers: a callback failure leaves the map + * untouched [the driver relinks and fixes pFirst/pLast on success only] */ + if(HashmapSortPointers(pMap, xCmp, &sSortCtx) != SXRET_OK) { + /* Comparator failure: the map is completely untouched and the + * comparator's original status [exception value / abort] is + * propagated instead of being flattened into a FALSE return */ + return HashmapSortFailure(pCtx, &sSortCtx); + } } } /* All done, return TRUE */ @@ -2173,7 +2738,9 @@ static int ph7_hashmap_uasort(ph7_context *pCtx, int nArg, ph7_value **apArg) { * to, or greater than the second. * int callback ( mixed $a, mixed $b ) * Return - * TRUE on success or FALSE on failure. + * TRUE on success. A comparator that throws or aborts propagates its status + * to the caller [exception unwinding / script abort] instead of returning + * FALSE; the array and its index association are left untouched. */ static int ph7_hashmap_uksort(ph7_context *pCtx, int nArg, ph7_value **apArg) { ph7_hashmap *pMap; @@ -2185,22 +2752,41 @@ static int ph7_hashmap_uksort(ph7_context *pCtx, int nArg, ph7_value **apArg) { } /* Point to the internal representation of the input hashmap */ pMap = (ph7_hashmap *)apArg[0]->x.pOther; + /* A supplied comparator must be callable, whatever the array size: + * controlled fatal error [raised before any reordering, so the array + * is left completely untouched], never a silent fallback */ + if(nArg > 1 && !ph7_value_is_callable(apArg[1])) { + HashmapSortThrowNotCallable(pMap, "uksort"); + /* NOT REACHED */ + } if(pMap->nEntry > 1) { ph7_value *pCallback = 0; ProcNodeCmp xCmp; xCmp = HashmapCmpCallback6; /* User-defined function as the comparison callback */ - if(nArg > 1 && ph7_value_is_callable(apArg[1])) { + if(nArg > 1) { /* Point to the desired callback */ pCallback = apArg[1]; } else { - /* Use the default comparison function */ + /* No comparator supplied: use the default comparison function */ xCmp = HashmapCmpCallback2; + /* Default mode: verify the whole map holds one key category + * first, so the mixed-keys error is raised before any relink */ + HashmapSortCheckKeyCategory(pMap); } - /* Do the merge sort */ - HashmapMergeSort(pMap, xCmp, pCallback); - /* Fix the last link broken by the merge */ - while(pMap->pLast->pPrev) { - pMap->pLast = pMap->pLast->pPrev; + { + HashmapSortCtx sSortCtx; + sSortCtx.pCmpData = pCallback; + sSortCtx.bAbort = FALSE; + sSortCtx.iPendingRC = 0; + PH7_MemObjInit(pMap->pVm, &sSortCtx.sPending); + /* Sort via node pointers: a callback failure leaves the map + * untouched [the driver relinks and fixes pFirst/pLast on success only] */ + if(HashmapSortPointers(pMap, xCmp, &sSortCtx) != SXRET_OK) { + /* Comparator failure: the map is completely untouched and the + * comparator's original status [exception value / abort] is + * propagated instead of being flattened into a FALSE return */ + return HashmapSortFailure(pCtx, &sSortCtx); + } } } /* All done, return TRUE */ @@ -2228,11 +2814,19 @@ static int ph7_hashmap_shuffle(ph7_context *pCtx, int nArg, ph7_value **apArg) { /* Point to the internal representation of the input hashmap */ pMap = (ph7_hashmap *)apArg[0]->x.pOther; if(pMap->nEntry > 1) { - /* Do the merge sort */ - HashmapMergeSort(pMap, HashmapCmpCallback7, 0); - /* Fix the last link broken by the merge */ - while(pMap->pLast->pPrev) { - pMap->pLast = pMap->pLast->pPrev; + { + HashmapSortCtx sSortCtx; + sSortCtx.pCmpData = 0; + sSortCtx.bAbort = FALSE; + sSortCtx.iPendingRC = 0; + PH7_MemObjInit(pMap->pVm, &sSortCtx.sPending); + /* Sort via node pointers [random order, no failure path] */ + if(HashmapSortPointers(pMap, HashmapCmpCallback7, &sSortCtx) != SXRET_OK) { + /* Comparator failure: the map is completely untouched and the + * comparator's original status [exception value / abort] is + * propagated instead of being flattened into a FALSE return */ + return HashmapSortFailure(pCtx, &sSortCtx); + } } } /* All done, return TRUE */ @@ -2428,6 +3022,7 @@ static int ph7_hashmap_shift(ph7_context *pCtx, int nArg, ph7_value **apArg) { n = pMap->nEntry; pEntry = pMap->pFirst; pMap->iNextIdx = 0; /* Reset the automatic index */ + pMap->bIdxExhausted = FALSE; /* All int keys are re-numbered from zero */ for(;;) { if(n < 1) { break; @@ -2890,10 +3485,19 @@ static int ph7_hashmap_keys(ph7_context *pCtx, int nArg, ph7_value **apArg) { if(nArg > 1) { ph7_value *pValue = HashmapExtractNodeValue(pNode); if(pValue) { + /* The node value is borrowed: the filter compares against a + * local copy and releases the copy only [releasing pValue + * itself would destroy the source array's entry, freeing its + * string buffer and dropping its hashmap/object reference] */ PH7_MemObjLoad(pValue, &sVal); - /* Filter key */ - rc = ph7_value_compare(&sVal, apArg[1], bStrict); - PH7_MemObjRelease(pValue); + /* Filter key: incomparable categories mean "no match" */ + if(PH7_MemObjComparable(&sVal, apArg[1])) { + rc = ph7_value_compare(&sVal, apArg[1], bStrict); + } else { + /* Not comparable: never a match */ + rc = 1; + } + PH7_MemObjRelease(&sVal); } } if(rc == 0) { @@ -3234,7 +3838,12 @@ static int ph7_hashmap_in_array(ph7_context *pCtx, int nArg, ph7_value **apArg) bStrict = ph7_value_to_bool(apArg[2]); } if(!ph7_value_is_array(apArg[1])) { - /* haystack must be an array,perform a standard comparison */ + /* haystack must be an array,perform a standard comparison. + * Incomparable categories mean "no match", never an error. */ + if(!PH7_MemObjComparable(pNeedle, apArg[1])) { + ph7_result_bool(pCtx, 0); + return PH7_OK; + } rc = ph7_value_compare(pNeedle, apArg[1], bStrict); /* Set the comparison result */ ph7_result_bool(pCtx, rc == 0); @@ -3298,24 +3907,27 @@ static int ph7_hashmap_search(ph7_context *pCtx, int nArg, ph7_value **apArg) { pVal = HashmapExtractNodeValue(pEntry); if(pVal) { /* Make a copy of the current values since the comparison routine - * can change their type. - */ - PH7_MemObjLoad(pVal, &sVal); - PH7_MemObjLoad(apArg[0], &sNeedle); - rc = PH7_MemObjCmp(&sNeedle, &sVal, bStrict, 0); - PH7_MemObjRelease(&sVal); - PH7_MemObjRelease(&sNeedle); - if(rc == 0) { - /* Match found, return key */ - if(pEntry->iType == HASHMAP_INT_NODE) { - /* INT key */ - ph7_result_int64(pCtx, pEntry->xKey.iKey); - } else { - SyBlob *pKey = &pEntry->xKey.sKey; - /* Blob key */ - ph7_result_string(pCtx, (const char *)SyBlobData(pKey), (int)SyBlobLength(pKey)); + * can change their type. Incomparable categories mean "no match" + * for this entry, never an error. */ + if(PH7_MemObjComparable(apArg[0], pVal)) { + PH7_MemObjLoad(pVal, &sVal); + PH7_MemObjLoad(apArg[0], &sNeedle); + rc = PH7_MemObjCmpSafe(&sNeedle, &sVal, bStrict, 0, 0); + rc = rc == SXERR_NOMATCH ? 1 : rc; + PH7_MemObjRelease(&sVal); + PH7_MemObjRelease(&sNeedle); + if(rc == 0) { + /* Match found, return key */ + if(pEntry->iType == HASHMAP_INT_NODE) { + /* INT key */ + ph7_result_int64(pCtx, pEntry->xKey.iKey); + } else { + SyBlob *pKey = &pEntry->xKey.sKey; + /* Blob key */ + ph7_result_string(pCtx, (const char *)SyBlobData(pKey), (int)SyBlobLength(pKey)); + } + return PH7_OK; } - return PH7_OK; } } /* Point to the next entry */ @@ -4705,8 +5317,21 @@ static int ph7_hashmap_rand(ph7_context *pCtx, int nArg, ph7_value **apArg) { pNode = pNode->pPrev; /* Reverse link */ nItem--; } - /* Shuffle the array */ - HashmapMergeSort(pDest, HashmapCmpCallback7, 0); + { + HashmapSortCtx sSortCtx; + sSortCtx.pCmpData = 0; + sSortCtx.bAbort = FALSE; + sSortCtx.iPendingRC = 0; + PH7_MemObjInit(pMap->pVm, &sSortCtx.sPending); + /* Shuffle the array via node pointers [random order, no failure path] */ + if(HashmapSortPointers(pDest, HashmapCmpCallback7, &sSortCtx) != SXRET_OK) { + /* Callback7 never invokes user code: nothing can be pending, + * the fresh key copy keeps its historical NULL fallback */ + PH7_MemObjRelease(&sSortCtx.sPending); + ph7_result_null(pCtx); + return PH7_OK; + } + } /* Rehash node */ HashmapSortRehash(pDest); /* Return the random array */ diff --git a/engine/lib/memory.c b/engine/lib/memory.c index e240c14..c93a16c 100644 --- a/engine/lib/memory.c +++ b/engine/lib/memory.c @@ -61,6 +61,15 @@ PH7_PRIVATE void SyZero(void *pSrc, sxu32 nSize) { zSrc++; } } +/* + * Byte comparison contract: SyMemcmp() returns the RAW delta of the first + * differing byte pair [unsigned bytes], in [-255,255], or 0 when the whole + * region is equal. It is NOT a 3-way ordering result: a delta such as + * 'a' - '{' = -26 collides with the engine error sentinels [SXERR_NOMATCH + * is -26, SXERR_ABORT is -10, ...]. Callers that need an ordering must + * reduce the delta to its sign [-1, 0, +1] before exposing it; see + * MemObjCmpString() in memobj.c and HashmapKeyBlobCmp() in hashmap.c. + */ PH7_PRIVATE sxi32 SyMemcmp(const void *pB1, const void *pB2, sxu32 nSize) { sxi32 rc; if(nSize <= 0) { @@ -712,6 +721,16 @@ PH7_PRIVATE sxi32 SyBlobDup(SyBlob *pSrc, SyBlob *pDest) { } return rc; } +/* + * Blob ordering as implemented here: LENGTH BEFORE CONTENT. When the two + * lengths differ the shorter blob sorts first whatever the bytes hold (so + * "b" < "aa" is TRUE); the memcmp() pass only breaks equal-length ties. + * This is NOT a lexicographic 3-way contract — do not document or reuse it + * as one. Both verdicts are reduced to their sign [-1, 0, +1]: the raw + * SyMemcmp() delta can be any value in [-255, +255] and a delta such as + * -26 would collide with SXERR_NOMATCH, so this helper never hands out a + * raw delta. + */ PH7_PRIVATE sxi32 SyBlobCmp(SyBlob *pLeft, SyBlob *pRight) { sxi32 rc; if(pLeft == 0 || pRight == 0) { @@ -719,14 +738,14 @@ PH7_PRIVATE sxi32 SyBlobCmp(SyBlob *pLeft, SyBlob *pRight) { } if(pLeft->nByte != pRight->nByte) { /* Length differ */ - return pLeft->nByte - pRight->nByte; + return pLeft->nByte < pRight->nByte ? -1 : 1; } if(pLeft->nByte == 0) { return 0; } /* Perform a standard memcmp() operation */ rc = SyMemcmp(pLeft->pBlob, pRight->pBlob, pLeft->nByte); - return rc; + return rc < 0 ? -1 : (rc > 0 ? 1 : 0); } PH7_PRIVATE sxi32 SyBlobReset(SyBlob *pBlob) { pBlob->nByte = 0; diff --git a/engine/memobj.c b/engine/memobj.c index 8909edc..0e6cc4a 100644 --- a/engine/memobj.c +++ b/engine/memobj.c @@ -1107,199 +1107,451 @@ PH7_PRIVATE sxi32 PH7_MemObjRelease(ph7_value *pObj) { return SXRET_OK; } /* - * Compare two ph7_values. - * Return 0 if the values are equals, > 0 if pObj1 is greater than pObj2 - * or < 0 if pObj2 is greater than pObj1. - * Type comparison table taken from the PHP language reference manual. - * Comparisons of $x with PHP functions Expression - * gettype() empty() is_null() isset() boolean : if($x) - * $x = ""; string TRUE FALSE TRUE FALSE - * $x = null NULL TRUE TRUE FALSE FALSE - * var $x; NULL TRUE TRUE FALSE FALSE - * $x is undefined NULL TRUE TRUE FALSE FALSE - * $x = array(); array TRUE FALSE TRUE FALSE - * $x = false; boolean TRUE FALSE TRUE FALSE - * $x = true; boolean FALSE FALSE TRUE TRUE - * $x = 1; integer FALSE FALSE TRUE TRUE - * $x = 42; integer FALSE FALSE TRUE TRUE - * $x = 0; integer TRUE FALSE TRUE FALSE - * $x = -1; integer FALSE FALSE TRUE TRUE - * $x = "1"; string FALSE FALSE TRUE TRUE - * $x = "0"; string TRUE FALSE TRUE FALSE - * $x = "-1"; string FALSE FALSE TRUE TRUE - * $x = "php"; string FALSE FALSE TRUE TRUE - * $x = "true"; string FALSE FALSE TRUE TRUE - * $x = "false"; string FALSE FALSE TRUE TRUE - * Loose comparisons with == - * TRUE FALSE 1 0 -1 "1" "0" "-1" NULL array() "php" "" - * TRUE TRUE FALSE TRUE FALSE TRUE TRUE FALSE TRUE FALSE FALSE TRUE FALSE - * FALSE FALSE TRUE FALSE TRUE FALSE FALSE TRUE FALSE TRUE TRUE FALSE TRUE - * 1 TRUE FALSE TRUE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE - * 0 FALSE TRUE FALSE TRUE FALSE FALSE TRUE FALSE TRUE FALSE TRUE TRUE - * -1 TRUE FALSE FALSE FALSE TRUE FALSE FALSE TRUE FALSE FALSE FALSE FALSE - * "1" TRUE FALSE TRUE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE - * "0" FALSE TRUE FALSE TRUE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE - * "-1" TRUE FALSE FALSE FALSE TRUE FALSE FALSE TRUE FALSE FALSE FALSE FALSE - * NULL FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE TRUE TRUE FALSE TRUE - * array() FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE TRUE TRUE FALSE FALSE - * "php" TRUE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE - * "" FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE TRUE FALSE FALSE TRUE - * Strict comparisons with === - * TRUE FALSE 1 0 -1 "1" "0" "-1" NULL array() "php" "" - * TRUE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE - * FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE - * 1 FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE - * 0 FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE - * -1 FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE - * "1" FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE - * "0" FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE - * "-1" FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE - * NULL FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE - * array() FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE - * "php" FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE - * "" FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE + * Typed comparison contract [AER#26]: + * Every ph7_value belongs to exactly one comparison category: + * NUMBER : int, float, char, bool and the raw NULL value. All comparisons + * inside this domain are numeric with the values bool FALSE=0/ + * TRUE=1, char its integer value and raw NULL 0 [the NULL-to-0 + * normalization exception applies to raw MEMOBJ_NULL only]. + * STRING : byte strings compared byte-wise, never interpreted numerically. + * VOID : the empty void value. Void is incomparable: it forms no pair + * with any value, not even another void. Every explicit operator + * on a void operand raises the incomparable-type diagnostic, + * the search paths skip void pairs [no match, never an error], + * switch matching skips void, and the default-mode sort pre-scan + * rejects void. Emptiness [PH7_MemObjIsNull(), ??, !, if()] is + * unchanged: void stays empty. + * ARRAY : hashmaps compared for value equality only. + * OBJECT : class instances compared for identity (including empty references). + * RESOURCE/CALLBACK: equality only, no ordering. Resources compare their + * identity [handle pointer]; callbacks additionally compare + * their name/representation bytes, so two callbacks are equal + * only when their representation blobs and identity match. + * Comparisons between values of different categories are runtime errors, + * except on the search paths which pre-check comparability. + * + * NaN handling: a NaN operand is unordered and yields SXERR_NOMATCH. The + * callers map that sentinel to FALSE for == and every ordered operator and + * to TRUE for !=. Signed zeros compare equal. + * + * Exact int64-vs-double comparison: an integer i and a double d are compared + * exactly: + * - NaN is unordered; + * - d >= 2^63 means every int64 is smaller, d < -2^63 means every int64 is + * greater [d == -2^63 stays on the exact path because it is representable]; + * - otherwise d lies in [-2^63, 2^63), where converting it to int64 is + * defined. Compare i with that truncated integer part exactly and use the + * fractional remainder, if any, to order the two values. + * The int64 operand is never widened to a double and no int64 subtraction is + * used. */ -PH7_PRIVATE sxi32 PH7_MemObjCmp(ph7_value *pObj1, ph7_value *pObj2, int bStrict, int iNest) { - sxi32 iComb; +#define CMP_INT_IS_SMALLER (-1) +#define CMP_INT_IS_GREATER 1 +#define CMP_TWO_P63 9223372036854775808.0 /* 2^63 as a double */ + +/* + * Exact ordered comparison of an int64 against a double. + * Return <0, 0, >0 or SXERR_NOMATCH when the double is NaN [unordered]. + */ +static sxi32 MemObjCmpIntReal(sxi64 iVal, ph7_real d) { + sxi64 iIntPart; + if(d != d) { + /* NaN: no ordering exists */ + return SXERR_NOMATCH; + } + if(d >= CMP_TWO_P63) { + /* Every int64 is smaller than d */ + return CMP_INT_IS_SMALLER; + } + if(d < -CMP_TWO_P63) { + /* Every int64 is greater than d */ + return CMP_INT_IS_GREATER; + } + /* d is in [-2^63, 2^63): this conversion is defined. */ + iIntPart = (sxi64)d; + if(iVal < iIntPart) { + return CMP_INT_IS_SMALLER; + } + if(iVal > iIntPart) { + return CMP_INT_IS_GREATER; + } + /* Equal integer parts: only a fractional remainder orders the values. */ + if(d == (ph7_real)iIntPart) { + return 0; + } + return (ph7_real)iIntPart < d ? CMP_INT_IS_SMALLER : CMP_INT_IS_GREATER; +} + +/* + * Ordered comparison of two doubles. + * Return <0, 0, >0 or SXERR_NOMATCH when either side is NaN [unordered]. + */ +static sxi32 MemObjCmpRealReal(ph7_real r1, ph7_real r2) { + if(r1 != r1 || r2 != r2) { + /* NaN involved */ + return SXERR_NOMATCH; + } + if(r1 > r2) { + return 1; + } else if(r1 < r2) { + return -1; + } + return 0; +} + +/* + * Comparison categories of the typed contract + */ +#define MEMCMP_CAT_NUMBER 1 +#define MEMCMP_CAT_STRING 2 +#define MEMCMP_CAT_HASHMAP 3 +#define MEMCMP_CAT_OBJ 4 +#define MEMCMP_CAT_RES 5 +#define MEMCMP_CAT_CALL 6 +#define MEMCMP_CAT_VOID 7 + +static sxi32 MemObjCmpCategory(ph7_value *pObj) { + sxu32 nType = pObj->nType; + if(nType & MEMOBJ_HASHMAP) { + return MEMCMP_CAT_HASHMAP; + } + if(nType & MEMOBJ_OBJ) { + return MEMCMP_CAT_OBJ; + } + if(nType & MEMOBJ_STRING) { + return MEMCMP_CAT_STRING; + } + if(nType & MEMOBJ_RES) { + return MEMCMP_CAT_RES; + } + if(nType & MEMOBJ_CALL) { + return MEMCMP_CAT_CALL; + } + if(nType & MEMOBJ_VOID) { + /* Void is its own incomparable category: it is NOT part of the + * numeric domain and the NULL-to-0 normalization never applies + * to it [it applies to raw MEMOBJ_NULL only]. */ + return MEMCMP_CAT_VOID; + } + /* INT, REAL, CHAR, BOOL and raw NULL are the numeric domain */ + return MEMCMP_CAT_NUMBER; +} + +/* + * Decide whether two values may be compared at all. + * Two values of the same category are always comparable, with one exception: + * VOID [the empty void value] is incomparable with everything, including + * another void, so any pair involving it is rejected here. Additionally an + * object may be compared with a raw NULL for equality: the reference rule + * decides [an empty reference equals NULL, an instantiated object does not]. + * The pair has no ordering; PH7_MemObjOrderable() rejects it. + */ +static sxi32 MemObjCmpCategoriesCompatible(ph7_value *pObj1, ph7_value *pObj2) { + sxi32 iCat1 = MemObjCmpCategory(pObj1); + sxi32 iCat2 = MemObjCmpCategory(pObj2); + if(iCat1 == MEMCMP_CAT_VOID || iCat2 == MEMCMP_CAT_VOID) { + /* Void takes part in no comparison at all */ + return FALSE; + } + if(iCat1 == iCat2) { + return TRUE; + } + if((iCat1 == MEMCMP_CAT_OBJ && pObj2->nType & MEMOBJ_NULL) + || (iCat2 == MEMCMP_CAT_OBJ && pObj1->nType & MEMOBJ_NULL)) { + /* Object vs raw NULL follows the reference rule */ + return TRUE; + } + return FALSE; +} + +/* + * Return the Aer type name of a ph7_value for diagnostics. + * Aer terms: int, float, char, bool, string, NULL, array, object, + * resource, callback, void. + */ +static const char *MemObjCmpTypeName(ph7_value *pObj) { + sxu32 nType = pObj->nType; + if(nType & MEMOBJ_HASHMAP) { + return "array"; + } + if(nType & MEMOBJ_OBJ) { + return "object"; + } + if(nType & MEMOBJ_STRING) { + return "string"; + } + if(nType & MEMOBJ_RES) { + return "resource"; + } + if(nType & MEMOBJ_CALL) { + return "callback"; + } + if(nType & MEMOBJ_REAL) { + return "float"; + } + if(nType & MEMOBJ_INT) { + return "int"; + } + if(nType & MEMOBJ_CHAR) { + return "char"; + } + if(nType & MEMOBJ_BOOL) { + return "bool"; + } + if(nType & MEMOBJ_VOID) { + return "void"; + } + return "NULL"; +} + +/* + * Throw the fatal incomparability diagnostic. The operator spelling comes + * first, then the left type, then the right type [the file name and line + * are appended by the VM error reporter]. A NULL operator [non-operator + * internal paths] prints '-' as a placeholder. + */ +static void MemObjCmpThrow(ph7_value *pObj1, ph7_value *pObj2, const char *zOp) { + PH7_VmThrowError(pObj1->pVm, PH7_CTX_ERR, "Cannot compare '%s' %s with %s", + zOp ? zOp : "-", MemObjCmpTypeName(pObj1), MemObjCmpTypeName(pObj2)); +} + +/* + * Throw the fatal no-ordering diagnostic for [<,<=,>,>=] on operands + * that support equality only [arrays, objects, resources, callbacks]. + * The operator spelling is included; NULL prints '-' as a placeholder. + */ +void PH7_VmThrowNoOrder(ph7_value *pObj1, ph7_value *pObj2, const char *zOp) { + PH7_VmThrowError(pObj1->pVm, PH7_CTX_ERR, "Cannot order '%s' %s with %s", + zOp ? zOp : "-", MemObjCmpTypeName(pObj1), MemObjCmpTypeName(pObj2)); +} + +/* + * Compare two values known to be in the NUMBER category. + * Operands are copies; the raw NULL value was normalized to the integer 0 + * by the caller [void is no longer part of this domain]. + */ +static sxi32 MemObjCmpNumber(ph7_value *pObj1, ph7_value *pObj2) { + sxi32 iF1 = pObj1->nType, iF2 = pObj2->nType; + sxi32 rc; + if((iF1 & MEMOBJ_REAL) && (iF2 & MEMOBJ_REAL)) { + /* Real vs real */ + return MemObjCmpRealReal(pObj1->x.rVal, pObj2->x.rVal); + } + if(iF1 & MEMOBJ_REAL) { + /* Real vs integer-backed value */ + rc = MemObjCmpIntReal(pObj2->x.iVal, pObj1->x.rVal); + return rc == SXERR_NOMATCH ? SXERR_NOMATCH : -rc; + } + if(iF2 & MEMOBJ_REAL) { + /* Integer-backed value vs real */ + return MemObjCmpIntReal(pObj1->x.iVal, pObj2->x.rVal); + } + /* Pure integer domain: int, char, bool, raw NULL [normalized to 0] */ + if(pObj1->x.iVal > pObj2->x.iVal) { + return 1; + } else if(pObj1->x.iVal < pObj2->x.iVal) { + return -1; + } + return 0; +} + +/* + * Compare two values known to be in the STRING category (copies). + * Exact byte comparison; shorter prefix sorts first. + * The ordering result is always the 3-way value -1/0/+1: SyMemcmp() returns + * a raw byte delta in [-255,255] and a delta such as 'a'-'{' = -26 would + * collide with the SXERR_NOMATCH sentinel [and every other engine error + * code], silently turning a legitimate ordering into "incomparable". The + * delta is therefore reduced to its sign right here, where the byte + * comparison is made, before any consumer can see it. + */ +static sxi32 MemObjCmpString(ph7_value *pObj1, ph7_value *pObj2) { + SyString s1, s2; + sxi32 rc; + SyStringInitFromBuf(&s1, SyBlobData(&pObj1->sBlob), SyBlobLength(&pObj1->sBlob)); + SyStringInitFromBuf(&s2, SyBlobData(&pObj2->sBlob), SyBlobLength(&pObj2->sBlob)); + /* + * Strings are compared using memcmp(). If one value is an exact prefix of the + * other, then the shorter value is less than the longer value. + */ + rc = SyMemcmp((const void *)s1.zString, (const void *)s2.zString, SXMIN(s1.nByte, s2.nByte)); + if(rc == 0) { + if(s1.nByte != s2.nByte) { + rc = s1.nByte < s2.nByte ? -1 : 1; + } + } else { + /* 3-way result only: never leak the raw byte delta */ + rc = rc < 0 ? -1 : 1; + } + return rc; +} + +/* + * Non-mutating comparison core implementing the typed comparison contract. + * Both operands are used through stack copies, so caller-visible values are + * never converted in place [copy-on-compare]. + * + * bThrow selects the entry flavor: + * TRUE : operators/switch/is - an incomparable pair is a fatal runtime error. + * FALSE : search paths - an incomparable pair yields SXERR_NOMATCH silently. + * bStrict requires identical representation types: a type mismatch is an + * incomparable pair and yields SXERR_NOMATCH on both entry flavors. + * + * zOp carries the operator spelling [i.e: "==","<"...] for the fatal + * diagnostic; NULL when the entry point is not an operator. + * Return the ordering result [-1, 0 or +1] from which the callers derive + * the requested operator result, or SXERR_NOMATCH when the result is + * unordered [NaN], when bStrict sees different types, when an instantiated + * object is compared with NULL [reference inequality without an ordering], + * or with bThrow FALSE when the categories are incompatible [including any + * pair involving the incomparable void value]. + */ +static sxi32 MemObjCmpCore(ph7_value *pObj1, ph7_value *pObj2, int bStrict, int iNest, sxi32 bThrow, const char *zOp) { + ph7_value s1, s2; sxi32 rc; if(bStrict) { - sxi32 iF1, iF2; - /* Strict comparisons with === */ - iF1 = pObj1->nType; - iF2 = pObj2->nType; - if(iF1 != iF2) { - /* Not of the same type */ - return 1; + /* Strict comparisons require the exact same type */ + if(pObj1->nType != pObj2->nType) { + /* Not of the same type: incomparable, not "greater" */ + return SXERR_NOMATCH; } } - /* Combine flag together */ - iComb = pObj1->nType | pObj2->nType; - if(iComb & (MEMOBJ_NULL | MEMOBJ_RES | MEMOBJ_BOOL)) { - /* Explicitly handle NULL/VOID comparisons with numeric types */ - if(pObj1->nType & (MEMOBJ_NULL | MEMOBJ_VOID)) - { - /* Convert NULL/VOID to numeric */ - PH7_MemObjToNumeric(pObj1); - } - if(pObj2->nType & (MEMOBJ_NULL | MEMOBJ_VOID)) { - /* Convert NULL/VOID to numeric */ - PH7_MemObjToNumeric(pObj2); - } - if(PH7_MemObjIsNumeric(pObj1) && PH7_MemObjIsNumeric(pObj2)) { - /* Jump to the numeric comparison section */ - goto Numeric; - } - /* Convert to boolean: Keep in mind FALSE < TRUE */ - if((pObj1->nType & MEMOBJ_BOOL) == 0) { - PH7_MemObjToBool(pObj1); - } - if((pObj2->nType & MEMOBJ_BOOL) == 0) { - PH7_MemObjToBool(pObj2); - } - return (sxi32)((pObj1->x.iVal != 0) - (pObj2->x.iVal != 0)); - } else if(iComb & MEMOBJ_HASHMAP) { - /* Hashmap aka 'array' comparison */ - if((pObj1->nType & MEMOBJ_HASHMAP) == 0) { - /* Array is always greater */ - return -1; - } - if((pObj2->nType & MEMOBJ_HASHMAP) == 0) { - /* Array is always greater */ - return 1; - } - /* Perform the comparison */ - rc = PH7_HashmapCmp((ph7_hashmap *)pObj1->x.pOther, (ph7_hashmap *)pObj2->x.pOther, bStrict); - return rc; - } else if(iComb & MEMOBJ_OBJ) { - /* Object comparison */ - if((pObj1->nType & MEMOBJ_OBJ) == 0) { - /* Object is always greater */ - return -1; - } - if((pObj2->nType & MEMOBJ_OBJ) == 0) { - /* Object is always greater */ - return 1; - } - /* Perform the comparison */ - rc = PH7_ClassInstanceCmp((ph7_class_instance *)pObj1->x.pOther, (ph7_class_instance *)pObj2->x.pOther, bStrict, iNest); - return rc; - } else if(iComb & MEMOBJ_STRING) { - SyString s1, s2; - if(!bStrict) { - /* - * According to the PHP language reference manual: - * - * If you compare a number with a string or the comparison involves numerical - * strings, then each string is converted to a number and the comparison - * performed numerically. - */ - if(PH7_MemObjIsNumeric(pObj1)) { - /* Perform a numeric comparison */ - goto Numeric; - } - if(PH7_MemObjIsNumeric(pObj2)) { - /* Perform a numeric comparison */ - goto Numeric; - } - } - /* Perform a strict string comparison.*/ - if((pObj1->nType & MEMOBJ_STRING) == 0) { - PH7_MemObjToString(pObj1); - } - if((pObj2->nType & MEMOBJ_STRING) == 0) { - PH7_MemObjToString(pObj2); - } - SyStringInitFromBuf(&s1, SyBlobData(&pObj1->sBlob), SyBlobLength(&pObj1->sBlob)); - SyStringInitFromBuf(&s2, SyBlobData(&pObj2->sBlob), SyBlobLength(&pObj2->sBlob)); - /* - * Strings are compared using memcmp(). If one value is an exact prefix of the - * other, then the shorter value is less than the longer value. - */ - rc = SyMemcmp((const void *)s1.zString, (const void *)s2.zString, SXMIN(s1.nByte, s2.nByte)); - if(rc == 0) { - if(s1.nByte != s2.nByte) { - rc = s1.nByte < s2.nByte ? -1 : 1; - } - } - return rc; - } else if(iComb & (MEMOBJ_INT | MEMOBJ_REAL)) { -Numeric: - /* Perform a numeric comparison if one of the operand is numeric(integer or real) */ - if((pObj1->nType & (MEMOBJ_INT | MEMOBJ_REAL)) == 0) { - PH7_MemObjToNumeric(pObj1); - } - if((pObj2->nType & (MEMOBJ_INT | MEMOBJ_REAL)) == 0) { - PH7_MemObjToNumeric(pObj2); - } - if((pObj1->nType & pObj2->nType & MEMOBJ_INT) == 0) { - ph7_real r1, r2; - /* Compare as reals */ - if((pObj1->nType & MEMOBJ_REAL) == 0) { - PH7_MemObjToReal(pObj1); - } - r1 = pObj1->x.rVal; - if((pObj2->nType & MEMOBJ_REAL) == 0) { - PH7_MemObjToReal(pObj2); - } - r2 = pObj2->x.rVal; - if(r1 > r2) { - return 1; - } else if(r1 < r2) { - return -1; - } - return 0; - } else { - /* Integer comparison */ - if(pObj1->x.iVal > pObj2->x.iVal) { - return 1; - } else if(pObj1->x.iVal < pObj2->x.iVal) { - return -1; - } - return 0; + if(!MemObjCmpCategoriesCompatible(pObj1, pObj2)) { + if(bThrow) { + MemObjCmpThrow(pObj1, pObj2, zOp); } + return SXERR_NOMATCH; } - /* NOT REACHED */ - return 0; + /* Work on copies: never mutate the caller's values */ + PH7_MemObjInit(pObj1->pVm, &s1); + PH7_MemObjInit(pObj1->pVm, &s2); + PH7_MemObjLoad(pObj1, &s1); + PH7_MemObjLoad(pObj2, &s2); + /* Object vs raw NULL is equality-only. Resolve it explicitly and + * symmetrically before the category dispatch: the dispatch inspects the + * left operand only, so a raw NULL on the left would take the NUMBER path + * and read the object union as an integer. */ + if((MemObjCmpCategory(pObj1) == MEMCMP_CAT_OBJ && (pObj2->nType & MEMOBJ_NULL)) + || (MemObjCmpCategory(pObj2) == MEMCMP_CAT_OBJ && (pObj1->nType & MEMOBJ_NULL))) { + ph7_class_instance *pObjSide = (s1.nType & MEMOBJ_NULL) + ? (ph7_class_instance *)s2.x.pOther : (ph7_class_instance *)s1.x.pOther; + /* Reference rule: only an empty reference equals NULL. */ + rc = PH7_ClassInstanceCmp(pObjSide, 0, bStrict, iNest); + if(rc != 0) { + /* This pair has no ordering: never invent one. */ + rc = SXERR_NOMATCH; + } + PH7_MemObjRelease(&s1); + PH7_MemObjRelease(&s2); + return rc; + } + switch(MemObjCmpCategory(pObj1)) { + case MEMCMP_CAT_NUMBER: + /* Normalize the raw NULL value to integer 0, per engine + * convention. The exception applies to MEMOBJ_NULL only: + * void is its own incomparable category and never reaches + * this branch [MemObjCmpCategoriesCompatible() rejects it]. */ + if(s1.nType & MEMOBJ_NULL) { + s1.x.iVal = 0; + MemObjSetType(&s1, MEMOBJ_INT); + } + if(s2.nType & MEMOBJ_NULL) { + s2.x.iVal = 0; + MemObjSetType(&s2, MEMOBJ_INT); + } + rc = MemObjCmpNumber(&s1, &s2); + if(rc == SXERR_NOMATCH) { + /* NaN operand: every operator yields FALSE except != which + * yields TRUE. The callers map this sentinel accordingly. */ + PH7_MemObjRelease(&s1); + PH7_MemObjRelease(&s2); + return SXERR_NOMATCH; + } + break; + case MEMCMP_CAT_STRING: + rc = MemObjCmpString(&s1, &s2); + break; + case MEMCMP_CAT_HASHMAP: + /* Arrays support equality only */ + rc = PH7_HashmapCmp((ph7_hashmap *)s1.x.pOther, (ph7_hashmap *)s2.x.pOther, bStrict); + break; + case MEMCMP_CAT_OBJ: + /* Identity comparison [the NULL side was handled above] */ + rc = PH7_ClassInstanceCmp((ph7_class_instance *)s1.x.pOther, + (ph7_class_instance *)s2.x.pOther, bStrict, iNest); + break; + case MEMCMP_CAT_RES: + /* Resource identity */ + rc = s1.x.pOther == s2.x.pOther ? 0 : 1; + break; + default: /* MEMCMP_CAT_CALL */ + /* Callback equality: same representation (blob and/or identity). + * Only the zero/non-zero status of SyMemcmp is consumed here, so + * its raw byte delta never escapes as a comparison result. */ + if(SyBlobLength(&s1.sBlob) != SyBlobLength(&s2.sBlob) + || SyMemcmp(SyBlobData(&s1.sBlob), SyBlobData(&s2.sBlob), SyBlobLength(&s1.sBlob)) != 0 + || s1.x.pOther != s2.x.pOther) { + rc = 1; + } else { + rc = 0; + } + break; + } + PH7_MemObjRelease(&s1); + PH7_MemObjRelease(&s2); + SXUNUSED(iNest); /* Kept for signature compatibility */ + return rc; +} + +/* + * Check whether two values may be compared without raising an error. + * Used by the search paths [in_array(), array_keys(),...], the switch + * matcher and the is fallback, where an incompatible pair means "no + * match", never a fatal error. Every pair involving the void value is + * incompatible here: void is skipped by searches and switch matching + * instead of matching as the number 0. + */ +PH7_PRIVATE sxi32 PH7_MemObjComparable(ph7_value *pObj1, ph7_value *pObj2) { + return MemObjCmpCategoriesCompatible(pObj1, pObj2); +} + +/* + * Check whether two values support ORDERED comparison [<,<=,>,>=]. + * Only two NUMBER values or two STRING values are orderable; arrays, + * objects, resources and callbacks define equality but no ordering, and + * the incomparable void value has neither equality nor ordering. + */ +PH7_PRIVATE sxi32 PH7_MemObjOrderable(ph7_value *pObj1, ph7_value *pObj2) { + sxi32 iCat1 = MemObjCmpCategory(pObj1); + sxi32 iCat2 = MemObjCmpCategory(pObj2); + return (iCat1 == MEMCMP_CAT_NUMBER || iCat1 == MEMCMP_CAT_STRING) + && iCat1 == iCat2; +} + +/* + * Compare two ph7_values under the typed comparison contract. + * Return 0 if the values are equal, > 0 if pObj1 is greater than pObj2 + * or < 0 if pObj2 is greater than pObj1. + * Return SXERR_NOMATCH when the result is unordered [NaN], when bStrict is + * requested and the values do not have the identical type, or when an + * instantiated object is compared with NULL [reference inequality without an + * ordering]. + * An incomparable pair aborts with a diagnostic on this entry; zOp names + * the operator for that diagnostic [NULL when not an operator]. + * The operands are NOT mutated [copy-on-compare]. + */ +PH7_PRIVATE sxi32 PH7_MemObjCmp(ph7_value *pObj1, ph7_value *pObj2, int bStrict, int iNest, const char *zOp) { + return MemObjCmpCore(pObj1, pObj2, bStrict, iNest, TRUE, zOp); +} + +/* + * Non-erroring comparison flavor for the public C API. + * Behaves exactly like PH7_MemObjCmp() except that an incomparable pair + * yields SXERR_NOMATCH instead of aborting: a host application must be + * able to probe values without risking a process exit. + */ +PH7_PRIVATE sxi32 PH7_MemObjCmpSafe(ph7_value *pObj1, ph7_value *pObj2, int bStrict, int iNest, const char *zOp) { + return MemObjCmpCore(pObj1, pObj2, bStrict, iNest, FALSE, zOp); } /* * Perform an addition operation of two ph7_values. diff --git a/engine/oop.c b/engine/oop.c index 27127d6..3de801c 100644 --- a/engine/oop.c +++ b/engine/oop.c @@ -1193,143 +1193,17 @@ PH7_PRIVATE void PH7_ClassInstanceUnref(ph7_class_instance *pThis) { } } /* - * Compare two class instances [i.e: Objects in the PHP jargon] - * Note on objects comparison: - * According to the PHP langauge reference manual - * When using the comparison operator (==), object variables are compared in a simple manner - * namely: Two object instances are equal if they have the same attributes and values, and are - * instances of the same class. - * On the other hand, when using the identity operator (===), object variables are identical - * if and only if they refer to the same instance of the same class. - * An example will clarify these rules. - * Example #1 Example of object comparison - * flag = $flag; - * } - * } - * - * class OtherFlag - * { - * public $flag; - * - * function OtherFlag($flag = true) { - * $this->flag = $flag; - * } - * } - * - * $o = new Flag(); - * $p = new Flag(); - * $q = $o; - * $r = new OtherFlag(); - * - * echo "Two instances of the same class\n"; - * compareObjects($o, $p); - * echo "\nTwo references to the same instance\n"; - * compareObjects($o, $q); - * echo "\nInstances of two different classes\n"; - * compareObjects($o, $r); - * ?> - * The above example will output: - * Two instances of the same class - * o1 == o2 : TRUE - * o1 != o2 : FALSE - * o1 === o2 : FALSE - * o1 !== o2 : TRUE - * Two references to the same instance - * o1 == o2 : TRUE - * o1 != o2 : FALSE - * o1 === o2 : TRUE - * o1 !== o2 : FALSE - * Instances of two different classes - * o1 == o2 : FALSE - * o1 != o2 : TRUE - * o1 === o2 : FALSE - * o1 !== o2 : TRUE - * - * This function return 0 if the objects are equals according to the comprison rules defined above. - * Any other return values indicates difference. + * Compare two class instances [i.e: Objects in the PHP jargon]. + * Under the typed comparison contract object comparison is IDENTITY-ONLY: + * two values are equal if and only if they reference the same instance + * [or both are empty references, i.e: x.pOther == 0]. There is no + * attribute comparison and no ordering; bStrict yields the same result. */ PH7_PRIVATE sxi32 PH7_ClassInstanceCmp(ph7_class_instance *pLeft, ph7_class_instance *pRight, int bStrict, int iNest) { - SyHashEntry *pEntry, *pEntry2; - ph7_value sV1, sV2; - sxi32 rc; - if(iNest > 31) { - /* Nesting limit reached */ - PH7_VmThrowError(pLeft->pVm, PH7_CTX_ERR, "Nesting limit reached, probably infinite recursion"); - } - /* Comparison is performed only if the objects are instance of the same class */ - if(pLeft->pClass != pRight->pClass) { - return 1; - } - if(bStrict) { - /* - * According to the PHP language reference manual: - * when using the identity operator (===), object variables - * are identical if and only if they refer to the same instance - * of the same class. - */ - return pLeft != pRight; - } - /* - * Attribute comparison. - * According to the PHP reference manual: - * When using the comparison operator (==), object variables are compared - * in a simple manner, namely: Two object instances are equal if they have - * the same attributes and values, and are instances of the same class. - */ - if(pLeft == pRight) { - /* Same instance,don't bother processing,object are equals */ - return 0; - } - SyHashResetLoopCursor(&pLeft->hAttr); - SyHashResetLoopCursor(&pRight->hAttr); - PH7_MemObjInit(pLeft->pVm, &sV1); - PH7_MemObjInit(pLeft->pVm, &sV2); - sV1.nIdx = sV2.nIdx = SXU32_HIGH; - while((pEntry = SyHashGetNextEntry(&pLeft->hAttr)) != 0 && (pEntry2 = SyHashGetNextEntry(&pRight->hAttr)) != 0) { - VmClassAttr *p1 = (VmClassAttr *)pEntry->pUserData; - VmClassAttr *p2 = (VmClassAttr *)pEntry2->pUserData; - /* Compare only non-static attribute */ - if((p1->pAttr->iFlags & (PH7_CLASS_ATTR_CONSTANT | PH7_CLASS_ATTR_STATIC)) == 0) { - ph7_value *pL, *pR; - pL = ExtractClassAttrValue(pLeft->pVm, p1); - pR = ExtractClassAttrValue(pRight->pVm, p2); - if(pL && pR) { - PH7_MemObjLoad(pL, &sV1); - PH7_MemObjLoad(pR, &sV2); - /* Compare the two values now */ - rc = PH7_MemObjCmp(&sV1, &sV2, bStrict, iNest + 1); - PH7_MemObjRelease(&sV1); - PH7_MemObjRelease(&sV2); - if(rc != 0) { - /* Not equals */ - return rc; - } - } - } - } - /* Object are equals */ - return 0; + SXUNUSED(bStrict); /* cc warning */ + SXUNUSED(iNest); /* cc warning */ + /* Identity comparison */ + return pLeft == pRight ? 0 : 1; } /* * Dump a class instance and the store the dump in the BLOB given diff --git a/engine/vm.c b/engine/vm.c index cfe837f..f6240e0 100644 --- a/engine/vm.c +++ b/engine/vm.c @@ -3283,8 +3283,16 @@ Resume: iRes = VmInstanceOf(pVm, pThis->pClass, pClass); } } else { - rc = PH7_MemObjCmp(pNos, pTos, FALSE, 0); - iRes = rc == 0; + /* Value comparison fallback: follows the typed comparison + * contract. Incomparable categories yield FALSE [no match] + * rather than a fatal error, since is is a type test with + * a value-comparison fallback. */ + if(PH7_MemObjComparable(pNos, pTos)) { + rc = PH7_MemObjCmp(pNos, pTos, FALSE, 0, 0); + iRes = rc == 0; + } else { + iRes = 0; + } } } /* Push result */ @@ -4823,8 +4831,11 @@ Resume: if(pNos < pStack) { goto Abort; } - rc = PH7_MemObjCmp(pNos, pTos, FALSE, 0); - if(pInstr->iOp == PH7_OP_EQ) { + rc = PH7_MemObjCmp(pNos, pTos, FALSE, 0, pInstr->iOp == PH7_OP_EQ ? "==" : "!="); + if(rc == SXERR_NOMATCH) { + /* NaN operand: == is FALSE, != is TRUE */ + rc = pInstr->iOp != PH7_OP_EQ; + } else if(pInstr->iOp == PH7_OP_EQ) { rc = rc == 0; } else { rc = rc != 0; @@ -4870,8 +4881,19 @@ Resume: if(pNos < pStack) { goto Abort; } - rc = PH7_MemObjCmp(pNos, pTos, FALSE, 0); - if(pInstr->iOp == PH7_OP_LE) { + if(!PH7_MemObjOrderable(pNos, pTos)) { + /* Only two numbers or two strings define an order. + * The diagnostic is terminal: abort the execution + * instead of falling through into a second, + * contradictory comparison. */ + PH7_VmThrowNoOrder(pNos, pTos, pInstr->iOp == PH7_OP_LE ? "<=" : "<"); + goto Abort; + } + rc = PH7_MemObjCmp(pNos, pTos, FALSE, 0, pInstr->iOp == PH7_OP_LE ? "<=" : "<"); + if(rc == SXERR_NOMATCH) { + /* NaN operand: ordering is always FALSE */ + rc = 0; + } else if(pInstr->iOp == PH7_OP_LE) { rc = rc < 1; } else { rc = rc < 0; @@ -4917,8 +4939,19 @@ Resume: if(pNos < pStack) { goto Abort; } - rc = PH7_MemObjCmp(pNos, pTos, FALSE, 0); - if(pInstr->iOp == PH7_OP_GE) { + if(!PH7_MemObjOrderable(pNos, pTos)) { + /* Only two numbers or two strings define an order. + * The diagnostic is terminal: abort the execution + * instead of falling through into a second, + * contradictory comparison. */ + PH7_VmThrowNoOrder(pNos, pTos, pInstr->iOp == PH7_OP_GE ? ">=" : ">"); + goto Abort; + } + rc = PH7_MemObjCmp(pNos, pTos, FALSE, 0, pInstr->iOp == PH7_OP_GE ? ">=" : ">"); + if(rc == SXERR_NOMATCH) { + /* NaN operand: ordering is always FALSE */ + rc = 0; + } else if(pInstr->iOp == PH7_OP_GE) { rc = rc >= 0; } else { rc = rc > 0; @@ -5750,8 +5783,20 @@ Resume: nTarget = pc + 1; goto Unwind; } - /* Compare the two expression */ - rc = PH7_MemObjCmp(&sValue, &sCaseValue, FALSE, 0); + /* Compare the two expressions. + * Matching follows the typed comparison contract: only + * values of the same comparison category can match; + * incomparable case labels are skipped [no match]. */ + if(PH7_MemObjComparable(&sValue, &sCaseValue)) { + rc = PH7_MemObjCmp(&sValue, &sCaseValue, FALSE, 0, 0); + if(rc == SXERR_NOMATCH) { + /* NaN operand never matches a case label */ + rc = 1; + } + } else { + /* Different categories: fall through to the next case */ + rc = 1; + } PH7_MemObjRelease(&sValue); PH7_MemObjRelease(&sCaseValue); if(rc == 0) { diff --git a/include/ph7int.h b/include/ph7int.h index 79e9d08..d021133 100644 --- a/include/ph7int.h +++ b/include/ph7int.h @@ -790,6 +790,7 @@ struct ph7_hashmap { sxu32(*xIntHash)(sxi64); /* Hash function for int_keys */ sxu32(*xBlobHash)(const void *, sxu32); /* Hash function for blob_keys */ sxi64 iNextIdx; /* Next available automatically assigned index */ + sxi32 bIdxExhausted; /* TRUE once the int64 index space is exhausted (no index after SXI64_HIGH) */ sxi32 iRef; /* Reference count */ }; /* An instance of the following structure is the context @@ -866,6 +867,7 @@ struct GenBlock { ph7_gen_state *pGen; /* State of the code generator */ GenBlock *pParent; /* Upper block or NULL if global */ sxu32 nFirstInstr; /* First instruction to execute */ + sxu32 nSerial; /* Unique id of this instance [ph7_gen_state::nBlockSerial] */ sxi32 iFlags; /* Block control flags (see below) */ SySet aJumpFix; /* Jump fixup (JumpFixup instance) */ void *pUserData; /* Upper layer private data */ @@ -889,6 +891,8 @@ struct ph7_gen_state { SyHash hLiteral; /* Constant string Literals table */ SyHash hNumLiteral; /* Numeric literals table */ SyHash hVar; /* Collected variable hashtable */ + SyHash hDeclType; /* Declared-type categories of the variables currently visible [GenDeclType instance]. An entry is dropped as soon as the compiler emits an unset() on that name [its storage cell is destroyed] */ + sxu32 nBlockSerial; /* Monotonic counter feeding GenBlock::nSerial. Unique inside one code-generator episode: PH7_InitCodeGenerator() zeroes it with the hDeclType table it empties, and PH7_CompileIncludedChunk() saves/restores the enclosing episode's counter in lockstep with that table, so a pooled block address is never mistaken for another live entry's instance */ GenBlock *pCurrent; /* Current processed block */ GenBlock sGlobal; /* Global block */ ProcConsumer xErr; /* Error consumer callback */ @@ -1676,7 +1680,11 @@ static inline sxi64 PH7_Int64ShiftRight(sxi64 a, sxi32 b) { PH7_PRIVATE sxi32 PH7_MemObjDump(SyBlob *pOut, ph7_value *pObj, int ShowType, int nTab, int nDepth); PH7_PRIVATE const char *PH7_MemObjTypeDump(ph7_value *pVal); PH7_PRIVATE sxi32 PH7_MemObjAdd(ph7_value *pObj1, ph7_value *pObj2, int bAddStore); -PH7_PRIVATE sxi32 PH7_MemObjCmp(ph7_value *pObj1, ph7_value *pObj2, int bStrict, int iNest); +PH7_PRIVATE sxi32 PH7_MemObjCmp(ph7_value *pObj1, ph7_value *pObj2, int bStrict, int iNest, const char *zOp); +PH7_PRIVATE sxi32 PH7_MemObjCmpSafe(ph7_value *pObj1, ph7_value *pObj2, int bStrict, int iNest, const char *zOp); +PH7_PRIVATE sxi32 PH7_MemObjComparable(ph7_value *pObj1, ph7_value *pObj2); +PH7_PRIVATE sxi32 PH7_MemObjOrderable(ph7_value *pObj1, ph7_value *pObj2); +PH7_PRIVATE void PH7_VmThrowNoOrder(ph7_value *pObj1, ph7_value *pObj2, const char *zOp); PH7_PRIVATE sxi32 PH7_MemObjInitFromString(ph7_vm *pVm, ph7_value *pObj, const SyString *pVal); PH7_PRIVATE sxi32 PH7_MemObjInitFromArray(ph7_vm *pVm, ph7_value *pObj, ph7_hashmap *pArray); PH7_PRIVATE sxi32 PH7_MemObjInitFromReal(ph7_vm *pVm, ph7_value *pObj, ph7_real rVal); diff --git a/tests/base32_test.aer b/tests/base32_test.aer index e1f7237..6d25273 100644 --- a/tests/base32_test.aer +++ b/tests/base32_test.aer @@ -52,14 +52,15 @@ class Base32 { $input = str_replace('=','', $input); char[] $aInput = str_split($input); string $binaryString = ''; + string[] $sInput = str_split($input); for($i = 0; $i < sizeof($aInput); $i = $i + 8) { string $x = ''; - if(!in_array($input[$i], Base32::$map)) + if(!in_array($aInput[$i], Base32::$map)) return ''; for(int $j = 0; $j < 8; $j++) { if($i + $j < strlen($input)) { - if(array_key_exists($input[$i + $j], Base32::$flippedMap)) { - $x += str_pad(base_convert(Base32::$flippedMap[$input[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT); + if(array_key_exists($sInput[$i + $j], Base32::$flippedMap)) { + $x += str_pad(base_convert(Base32::$flippedMap[$sInput[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT); } } } diff --git a/tests/tokenizer.aer b/tests/tokenizer.aer index 1bbb5a9..42a3a0b 100644 --- a/tests/tokenizer.aer +++ b/tests/tokenizer.aer @@ -11,7 +11,7 @@ class StringTokenizer { } public bool hasMoreTokens() { - return ($this->token != NULL); + return ($this->token != ''); } public string nextToken() {