diff --git a/App/lib/core/Gen/Type/TypeSpec.vp b/App/lib/core/Gen/Type/TypeSpec.vp index 0e5adaa..406e284 100644 --- a/App/lib/core/Gen/Type/TypeSpec.vp +++ b/App/lib/core/Gen/Type/TypeSpec.vp @@ -2,6 +2,7 @@ from stdint import * import mlirlite import memhub import string +import stdlib import viperlib import lib.core.CompilerFactory as CompilerFactory import lib.core.Gen.GenApi as GenApi @@ -105,7 +106,7 @@ def SpecSignFromTypeNameSpan(text: str, start: sizet, end: sizet) -> int: if _spec_name_eq(text, start, end, "ushort") != 0: return TS_SIGN_UNSIGNED if _spec_name_eq(text, start, end, "uint") != 0: return TS_SIGN_UNSIGNED if _spec_name_eq(text, start, end, "ulong") != 0: return TS_SIGN_UNSIGNED - if _spec_name_eq(text, start, end, "ulonglong") != 0: return TS_SIGN_UNSIGNED + if _spec_name_eq(text, start, end, "ulonglong") != 0: return TS_SIGN_UNSIGNED if _spec_name_eq(text, start, end, "uintptr") != 0: return TS_SIGN_UNSIGNED if _spec_name_eq(text, start, end, "int8") != 0: return TS_SIGN_SIGNED if _spec_name_eq(text, start, end, "int16") != 0: return TS_SIGN_SIGNED @@ -122,7 +123,7 @@ def SpecSignFromTypeNameSpan(text: str, start: sizet, end: sizet) -> int: if _spec_name_eq(text, start, end, "short") != 0: return TS_SIGN_SIGNED if _spec_name_eq(text, start, end, "long") != 0: return TS_SIGN_SIGNED if _spec_name_eq(text, start, end, "longlong") != 0: return TS_SIGN_SIGNED - if _spec_name_eq(text, start, end, "ptrdifft") != 0: return TS_SIGN_SIGNED + if _spec_name_eq(text, start, end, "ptrdifft") != 0: return TS_SIGN_SIGNED if _spec_name_eq(text, start, end, "intptr") != 0: return TS_SIGN_SIGNED # --- builtin lowercase scalars --- if _spec_name_eq(text, start, end, "bool") != 0: return TS_SIGN_UNSIGNED @@ -430,26 +431,41 @@ def _spec_to_text(sp: TypeSpec*, buf: str, buf_size: sizet, depth: int) -> int: if sp is None or buf is None or buf_size == 0 or depth > 32: return 1 buf[0] = '\0' if sp.Kind == TS_PTR: - # p: + inner - inner_buf: str = CompilerFactory.factory.PermPool.alloc(SPEC_TEXT_MAX) + # p: + inner. Transient buffers come from the heap and are freed immediately: + # PermPool is permanent, so per-call scratch there leaked ~256 B per recursion + # level and exhausted the 1 GiB pool while translating large modules. + inner_buf: str = stdlib.malloc(SPEC_TEXT_MAX) if inner_buf is None: return 1 - if _spec_to_text(sp.Elem, inner_buf, SPEC_TEXT_MAX, depth + 1) != 0: return 1 + if _spec_to_text(sp.Elem, inner_buf, SPEC_TEXT_MAX, depth + 1) != 0: + stdlib.free(inner_buf) + return 1 pos_p: sizet = _spec_text_append(buf, buf_size, 0, "p:") - if pos_p >= buf_size: return 1 + if pos_p >= buf_size: + stdlib.free(inner_buf) + return 1 pos_p = _spec_text_append(buf, buf_size, pos_p, inner_buf) + stdlib.free(inner_buf) if pos_p >= buf_size: return 1 return 0 if sp.Kind == TS_ARRAY: # arr[N]: + inner - inner_buf_a: str = CompilerFactory.factory.PermPool.alloc(SPEC_TEXT_MAX) + inner_buf_a: str = stdlib.malloc(SPEC_TEXT_MAX) if inner_buf_a is None: return 1 - if _spec_to_text(sp.Elem, inner_buf_a, SPEC_TEXT_MAX, depth + 1) != 0: return 1 - prefix_a: str = CompilerFactory.factory.PermPool.alloc(64) - if prefix_a is None: return 1 + if _spec_to_text(sp.Elem, inner_buf_a, SPEC_TEXT_MAX, depth + 1) != 0: + stdlib.free(inner_buf_a) + return 1 + prefix_a: str = stdlib.malloc(64) + if prefix_a is None: + stdlib.free(inner_buf_a) + return 1 viperlib.snprintf(prefix_a, 64, "arr[%d]:", sp.ArrayCount) pos_a: sizet = _spec_text_append(buf, buf_size, 0, prefix_a) - if pos_a >= buf_size: return 1 + stdlib.free(prefix_a) + if pos_a >= buf_size: + stdlib.free(inner_buf_a) + return 1 pos_a = _spec_text_append(buf, buf_size, pos_a, inner_buf_a) + stdlib.free(inner_buf_a) if pos_a >= buf_size: return 1 return 0 if sp.Kind == TS_FUNC: @@ -463,10 +479,13 @@ def _spec_to_text(sp: TypeSpec*, buf: str, buf_size: sizet, depth: int) -> int: if pos >= buf_size: return 1 param: TypeSpec* = _spec_param_at(sp, i) if param is None: return 1 - param_buf: str = CompilerFactory.factory.PermPool.alloc(SPEC_TEXT_MAX) + param_buf: str = stdlib.malloc(SPEC_TEXT_MAX) if param_buf is None: return 1 - if _spec_to_text(param, param_buf, SPEC_TEXT_MAX, depth + 1) != 0: return 1 + if _spec_to_text(param, param_buf, SPEC_TEXT_MAX, depth + 1) != 0: + stdlib.free(param_buf) + return 1 pos = _spec_text_append(buf, buf_size, pos, param_buf) + stdlib.free(param_buf) if pos >= buf_size: return 1 if sp.IsVarArg == 1: if sp.ParamCount > 0: pos = _spec_text_append(buf, buf_size, pos, ",...") @@ -474,10 +493,13 @@ def _spec_to_text(sp: TypeSpec*, buf: str, buf_size: sizet, depth: int) -> int: if pos >= buf_size: return 1 pos = _spec_text_append(buf, buf_size, pos, ")->") if pos >= buf_size: return 1 - ret_buf: str = CompilerFactory.factory.PermPool.alloc(SPEC_TEXT_MAX) + ret_buf: str = stdlib.malloc(SPEC_TEXT_MAX) if ret_buf is None: return 1 - if _spec_to_text(sp.Ret, ret_buf, SPEC_TEXT_MAX, depth + 1) != 0: return 1 + if _spec_to_text(sp.Ret, ret_buf, SPEC_TEXT_MAX, depth + 1) != 0: + stdlib.free(ret_buf) + return 1 pos = _spec_text_append(buf, buf_size, pos, ret_buf) + stdlib.free(ret_buf) if pos >= buf_size: return 1 return 0 # base type @@ -631,17 +653,39 @@ def _text_to_func(text: str, depth: int) -> TypeSpec*: return sp_f +# _spec_discard_temp - return a dedup-hit temporary TypeSpec to PermPool. +# The temporary's BaseName is an exclusive deep copy made by NewTypeStruct; a clone +# (_spec_clone) instead shares the pointer with the interned entry, so only a distinct +# pointer may be freed. Elem/Ret/Params are interned or caller-owned and never freed here. +def _spec_discard_temp(pp: memhub.MemBuddy*, sp: TypeSpec*, hit: TypeSpec*) -> None: + """Free a temporary TypeSpec that lost dedup; keeps shared sub-buffers alive""" + if sp is None or hit is None: return + if uint64(sp) == uint64(hit): return + if sp.BaseName is not None: + if hit.BaseName is None or uint64(sp.BaseName) != uint64(hit.BaseName): + pp.free(sp.BaseName) + pp.free(sp) + + # InternSpec - canonical interning: one instance per key, returns the interned instance. # Table full -> fatal_error; key too long for SPEC_TEXT_MAX -> keep instance, skip dedup. def InternSpec(arena: TypeSpecArena*, sp: TypeSpec*) -> TypeSpec*: - """Intern a TypeSpec by canonical text key; None on failure""" + """Intern a TypeSpec by canonical text key; None on failure. + + All callers pass a **freshly allocated** instance (NewType* / SpecWith*): on a dedup hit + that temporary is unreachable, so it (and the key buffer) must be returned to PermPool. + PermPool is permanent and never reset; leaking them per resolution exhausted the 1 GiB + pool while translating large modules (现场:includes/ast/parser.vp). + """ if arena is None or sp is None: return sp + pp: memhub.MemBuddy* = CompilerFactory.factory.PermPool if SpecArenaInit(arena, 0) != 0: return None - key_buf: str = CompilerFactory.factory.PermPool.alloc(SPEC_TEXT_MAX) + key_buf: str = pp.alloc(SPEC_TEXT_MAX) if key_buf is None: return None if SpecToText(sp, key_buf, SPEC_TEXT_MAX) != 0: # Key too long (deep nesting): keep the instance but skip dedup. Never store a # truncated key, which would false-match another type and return a wrong instance. + pp.free(key_buf) return sp # Hash-index fast path (expected O(1)). The index is kept complete as entries are # only appended, so a miss can append directly; fall back to linear scan only when unavailable. @@ -649,33 +693,49 @@ def InternSpec(arena: TypeSpecArena*, sp: TypeSpec*) -> TypeSpec*: found: int = _spec_index_find(arena, key_buf, key_hash) if found >= 0: slot_e: sizet* = (sizet*)(uint64(arena.Specs) + uint64(found * 8)) - if slot_e is None: return None - return (TypeSpec*)(uint64(slot_e[0])) + if slot_e is None: + pp.free(key_buf) + return None + hit_e: TypeSpec* = (TypeSpec*)(uint64(slot_e[0])) + pp.free(key_buf) + _spec_discard_temp(pp, sp, hit_e) + return hit_e if arena.Index is None: # index unavailable: fall back to the linear scan for i in range(int(arena.Count)): exist_key: str = _spec_key_at(arena.Keys, i) if exist_key is not None and string.strcmp(exist_key, key_buf) == 0: slot_e2: sizet* = (sizet*)(uint64(arena.Specs) + uint64(i * 8)) - if slot_e2 is None: return None - return (TypeSpec*)(uint64(slot_e2[0])) + if slot_e2 is None: + pp.free(key_buf) + return None + hit_e2: TypeSpec* = (TypeSpec*)(uint64(slot_e2[0])) + pp.free(key_buf) + _spec_discard_temp(pp, sp, hit_e2) + return hit_e2 # Miss: append. Table full = fail-fast (never return None, which would silently # lose type info up the call chain and corrupt signedness -> zext/sext direction). if arena.Count >= arena.Cap: + pp.free(key_buf) GenApi.fatal_error(None, f"TypeSpec intern table is full (limit {arena.Cap} entries), cannot register a new type", "InternalCompilerError") # Reachable only when fatal_error is a no-op: degrade to "no interning but return a # complete instance", keeping the never-return-None guarantee. return sp dst_key: str = _spec_key_at(arena.Keys, arena.Count) - if dst_key is None: return None + if dst_key is None: + pp.free(key_buf) + return None string.strcpy(dst_key, key_buf) slot_n: sizet* = (sizet*)(uint64(arena.Specs) + uint64(arena.Count * 8)) - if slot_n is None: return None + if slot_n is None: + pp.free(key_buf) + return None slot_n[0] = sizet(sp) # Store into the hash index too (index = Count before append). Insert failure only # means later lookups for this key fall back to the linear scan. _spec_index_insert(arena, key_buf, key_hash, arena.Count) arena.Count += 1 + pp.free(key_buf) return sp