Files
2026-10-17 21:57:25 +08:00

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