Fix value comparison and sorting semantics
Build / AerScript (push) Successful in 31s

This commit is contained in:
2026-11-05 22:26:00 +01:00
parent 3c3c15281a
commit d98f6a8e91
10 changed files with 1956 additions and 681 deletions
+445 -14
View File
@@ -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) {