This commit is contained in:
+445
-14
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user