191 lines
8.2 KiB
Plaintext
191 lines
8.2 KiB
Plaintext
from stdint import *
|
|
import stdlib
|
|
import memhub
|
|
import string
|
|
import json
|
|
import lib.VFs as VFs
|
|
import lib.VLog as VLog
|
|
import lib.VCache as VCache
|
|
|
|
|
|
# VJson - build the round JSON array (compact single line) with the repo json library.
|
|
# Ownership (never freed): Json_Buf/Conv_Buf by Vet_Mb; Json_Arena by stdlib.malloc; Json_Pool on heap.
|
|
|
|
define JSON_BUF_CAP = 1048576 # one round's JSON array buffer, 1 MiB
|
|
define JSON_POOL_CAP = 65536 # per-entry serialization pool, 64 KiB (reset and reused per entry)
|
|
define JSON_ENTRY_MAX = 4096 # max per entry = json library write default cap (4096)
|
|
define JSON_ENTRY_SLACK = 512 # per-entry fixed syntax overhead bound (measured 77)
|
|
|
|
# Upper bound of one entry's fixed pool usage (part of BuildEntry's AllocOk precheck): 7 JsonValue
|
|
# nodes + 6 key copies + json.write output buffer (4096) + 512 slack = 5158.
|
|
define JSON_ENTRY_POOL_FIXED = 5158
|
|
|
|
# Max length of executable / displayName values: both come from Conv_Buf, so each value is
|
|
# <= VFs.CONV_CAP-1 chars; 2047 = 2048 - 1.
|
|
define JSON_ENTRY_CONV_MAX = 2047
|
|
|
|
# injected by App/main.vp
|
|
_mbuddy: memhub.MemBuddy*
|
|
|
|
Json_Buf: bytes = None # full round array text (shared by stdout and cache file)
|
|
Json_Len: int = 0 # valid byte count of Json_Buf
|
|
Conv_Buf: bytes = None # ANSI -> UTF-8 conversion buffer
|
|
Json_Arena: bytes = None # serialization pool arena (stdlib.malloc, resident)
|
|
Json_Pool: memhub.MemPool* # serialization pool (constructed in place on the heap; see JsonInit)
|
|
|
|
|
|
# allocate JSON static buffers and pool (reused for process lifetime); 1 = ok, 0 = fail
|
|
def JsonInit() -> int:
|
|
global Json_Buf, Conv_Buf, Json_Arena, Json_Pool
|
|
Json_Buf = _mbuddy.alloc(JSON_BUF_CAP)
|
|
Conv_Buf = _mbuddy.alloc(VFs.CONV_CAP)
|
|
Json_Arena = stdlib.malloc(JSON_POOL_CAP)
|
|
if Json_Buf is None or Conv_Buf is None or Json_Arena is None:
|
|
return 0
|
|
# Construct the pool in place on the heap (memhub.NewMemPool): Json_Pool is used across frames,
|
|
# so it must not live on this function's stack, which dies on return.
|
|
Pool: memhub.MemPool* = memhub.NewMemPool(Json_Arena, JSON_POOL_CAP)
|
|
if Pool is None:
|
|
return 0
|
|
Json_Pool = Pool
|
|
return 1
|
|
|
|
|
|
# read accessor for VScan stdout / VCache cache-file writers
|
|
def JsonBuf() -> bytes:
|
|
return Json_Buf
|
|
|
|
|
|
def JsonLen() -> int:
|
|
return Json_Len
|
|
|
|
|
|
def JReset():
|
|
global Json_Len
|
|
Json_Len = 0
|
|
if Json_Buf is not None:
|
|
Json_Buf[0] = 0
|
|
|
|
|
|
# append literal/serialized text to Json_Buf (returns 0 if it would overflow; no partial write)
|
|
def JWrite(S: str) -> int:
|
|
global Json_Len
|
|
if S is None or Json_Buf is None:
|
|
return 0
|
|
n: sizet = string.strlen(S)
|
|
if sizet(Json_Len) + n + 1 > sizet(JSON_BUF_CAP):
|
|
return 0
|
|
for i in range(n):
|
|
Json_Buf[Json_Len + int(i)] = S[i]
|
|
Json_Len += int(n)
|
|
Json_Buf[Json_Len] = 0
|
|
return 1
|
|
|
|
|
|
# Build entry Idx and serialize it to compact single-line text; returns text (in Json_Pool,
|
|
# length < JSON_ENTRY_MAX) or None if skipped. Skip reasons are logged only with --verbose.
|
|
def BuildEntry(Idx: int) -> str:
|
|
# Pool capacity precheck (fail-closed): all allocations for this entry must fit, or the
|
|
# library's pool.alloc would printf + exit(1). Runs after EmitJson's per-entry reset.
|
|
ValNeed: sizet = sizet(JSON_ENTRY_CONV_MAX) + sizet(JSON_ENTRY_CONV_MAX)
|
|
ValNeed = ValNeed + string.strlen(VCache.CacheId(Idx))
|
|
ValNeed = ValNeed + string.strlen(VCache.CacheVer(Idx))
|
|
ValNeed = ValNeed + string.strlen(VCache.CacheArch(Idx))
|
|
ValNeed = ValNeed + string.strlen(VCache.CacheSrc(Idx)) # source ("workspace"/"check")
|
|
Need: sizet = sizet(JSON_ENTRY_POOL_FIXED) + ValNeed
|
|
if Json_Pool.AllocOk(Need) == 0:
|
|
VLog.VerboseFmtIS(" Warning: JSON pool insufficient (this entry needs %zu bytes), skipped: %s\n",
|
|
Need, VCache.CacheExe(Idx))
|
|
return None
|
|
Obj: json.JsonValue* = json.object(Json_Pool)
|
|
if Obj == None:
|
|
VLog.VerboseStr(" Warning: JSON object allocation failed (pool insufficient), entry skipped\n")
|
|
return None
|
|
# Key insertion order = output order. Convert executable/displayName to UTF-8 and hand to the
|
|
# library at once: Conv_Buf is reused, so a later copy would overwrite the executable string.
|
|
json.object_set(Json_Pool, Obj, "id", json.string_val(Json_Pool, VCache.CacheId(Idx)))
|
|
if VFs.AnsiToUtf8(VCache.CacheExe(Idx), Conv_Buf, VFs.CONV_CAP) == 0:
|
|
VFs.CopyRaw(Conv_Buf, VFs.CONV_CAP, VCache.CacheExe(Idx))
|
|
json.object_set(Json_Pool, Obj, "executable", json.string_val(Json_Pool, str(Conv_Buf)))
|
|
json.object_set(Json_Pool, Obj, "version", json.string_val(Json_Pool, VCache.CacheVer(Idx)))
|
|
json.object_set(Json_Pool, Obj, "arch", json.string_val(Json_Pool, VCache.CacheArch(Idx)))
|
|
# source from the cache entry (scan = "workspace"; check channel = "check")
|
|
json.object_set(Json_Pool, Obj, "source", json.string_val(Json_Pool, VCache.CacheSrc(Idx)))
|
|
if VFs.AnsiToUtf8(VCache.CacheDisp(Idx), Conv_Buf, VFs.CONV_CAP) == 0:
|
|
VFs.CopyRaw(Conv_Buf, VFs.CONV_CAP, VCache.CacheDisp(Idx))
|
|
json.object_set(Json_Pool, Obj, "displayName", json.string_val(Json_Pool, str(Conv_Buf)))
|
|
|
|
# Structural self-check: verify all 6 keys carry a name and a string value (a missed check
|
|
# could emit invalid JSON); also sum value lengths for the capacity guard.
|
|
Ok: int = 1
|
|
N: int = 0
|
|
Sum: sizet = 0
|
|
cu: json.JsonValue* = Obj.child
|
|
while cu != None:
|
|
if cu.key == None or cu.vtype != json.JSON_STRING or cu.str_val == None:
|
|
Ok = 0
|
|
else:
|
|
Sum = Sum + string.strlen(cu.str_val)
|
|
N += 1
|
|
cu = cu.next
|
|
if Ok == 0 or N != 6:
|
|
VLog.VerboseStr(" Warning: JSON entry construction incomplete (pool insufficient), entry skipped\n")
|
|
return None
|
|
|
|
# Capacity guard: the library's write default cap=4096 returns None on overflow; guard early
|
|
# to avoid a dropped entry. Output <= 2*Sum + SLACK(512) <= JSON_ENTRY_MAX.
|
|
if 2 * Sum + sizet(JSON_ENTRY_SLACK) > sizet(JSON_ENTRY_MAX):
|
|
VLog.VerboseFmtIS(" Warning: JSON entry too long (exceeds %zu bytes), skipped: %s\n",
|
|
sizet(JSON_ENTRY_MAX), VCache.CacheExe(Idx))
|
|
return None
|
|
|
|
Txt: str = json.write(Json_Pool, Obj, False)
|
|
if Txt is None:
|
|
VLog.VerboseStr(" Warning: JSON serialization failed (pool insufficient), entry skipped\n")
|
|
return None
|
|
return Txt
|
|
|
|
|
|
# build the whole cache as a single-line JSON array into Json_Buf (shared by stdout and cache file)
|
|
def EmitJson() -> int:
|
|
JReset()
|
|
if Json_Pool is None:
|
|
# invariant: EmitJson is reached only after JsonInit succeeds; this branch is only to avoid a crash
|
|
VLog.LogStr(" Warning: JSON pool not initialized, skipping JSON output this round\n")
|
|
return 0
|
|
Truncated: int = 0
|
|
Skipped: int = 0
|
|
Count: int = 0
|
|
if JWrite("[") == 0:
|
|
return 0
|
|
for i in range(VCache.CacheCount()):
|
|
# per-entry recycle: pool peak = one entry (nodes + string copies + library 4096-byte output buffer)
|
|
Json_Pool.reset()
|
|
Txt: str = BuildEntry(i)
|
|
if Txt is None:
|
|
Skipped += 1
|
|
else:
|
|
n: sizet = string.strlen(Txt)
|
|
Need: sizet = n + 3 # self + closing ']' + '\n' + NUL
|
|
if Count > 0:
|
|
Need = Need + 1 # separator comma
|
|
if sizet(Json_Len) + Need > sizet(JSON_BUF_CAP):
|
|
# buffer full: skip this entry (never write a partial entry); closing ']' still emitted
|
|
Truncated = 1
|
|
else:
|
|
if Count > 0:
|
|
JWrite(",")
|
|
JWrite(Txt)
|
|
Count += 1
|
|
if JWrite("]") == 0:
|
|
Truncated = 1
|
|
if JWrite("\n") == 0:
|
|
Truncated = 1
|
|
if Truncated != 0:
|
|
VLog.LogFmt2(" Warning: JSON output buffer insufficient, round truncated (wrote %zu entries / cache %zu entries)\n",
|
|
sizet(Count), sizet(VCache.CacheCount()))
|
|
if Skipped != 0:
|
|
VLog.LogFmt1(" Warning: %zu JSON entries skipped this round due to excessive length / insufficient pool\n",
|
|
sizet(Skipped))
|
|
return 0
|