Files
ViperC/App/lib/core/Gen/Class/ClassGenEnum.vp
T

1202 lines
71 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import lib.core.Gen.GenTypes as GenTypes
from stdint import *
import ast
import mlirlite
import memhub
import string
import viperlib
import lib.core.VLogger as VLogger
import lib.core.Gen.Translator as HT
import lib.core.CompilerFactory as CompilerFactory
from . import ClassGenBase
# Local constant copies (originally defined in HandlesEnum/HandlesClassDef; lost when the file was split).
# Undefined names fold to 0 in constant context, silently skipping enum member collection and CEnum registration.
define ENUM_MEMBER_MAX_LOCAL = 128
define RENUM_CLASS_MAX = 64
define RENUM_VARIANT_PER_CLASS = 32
define RENUM_ALIAS_MAX = 512
# ClassGenEnum - split from ClassGenMixin (stage D): enum/union/renum definitions and variant tables.
# ============================================================
novirtual class ClassGenEnum(ClassGenBase.ClassGenBase):
"""enum/union/renum definitions and variant tables"""
def _resolve_enum_member_type(self, pool: memhub.MemBuddy*, ann_node: ast.AST*, imported_modules: str, from_imports: str) -> mlirlite.LLVMType*:
"""Resolve an enum member's type annotation to an LLVM integer type"""
if ann_node is None: return mlirlite.Int32(pool)
k: int = ann_node.kind()
# Attribute node: t.State / int / int8 etc.
if k == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(ann_node)
if at.attr is None:
# [S4 fail-fast] Attribute missing the member name (corrupt AST); no silent i32 fallback
self.Gen.TypeGenErr.fatal_error(ann_node, "enum member type annotation Attribute is missing the member name", "TypeError")
return mlirlite.Int32(pool) # unreachable (fatal_error already exited)
# t.State -> default i32
if string.strcmp(at.attr, "State") == 0: return mlirlite.Int32(pool)
# [enum bool rule] `t.CBool` and bare `bool` both materialize as 8 bits in enum member annotations,
# else the same member would split into i1 / i8; the global `t.CBool` stays i1 (only enums differ).
if string.strcmp(at.attr, "CBool") == 0: return mlirlite.Int8(pool)
# Other t.CXxx types: unified (CTypedef dynamic table first, fall back to map_builtin_type)
ty: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_typedef_or_map(pool, at.attr, imported_modules, from_imports)
if ty is not None: return ty
# [S4 fail-fast] annotated but unresolvable -> no silent i32 fallback
self.Gen.TypeGenErr.fatal_error(ann_node, f"failed to resolve enum member type annotation: t.{at.attr}", "NameError")
return mlirlite.Int32(pool) # unreachable (fatal_error already exited)
# Name node: from-import form (State / CInt)
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(ann_node)
if nm.id is None:
# [S4 fail-fast] Name missing the identifier (corrupt AST); no silent i32 fallback
self.Gen.TypeGenErr.fatal_error(ann_node, "enum member type annotation Name is missing the identifier", "TypeError")
return mlirlite.Int32(pool) # unreachable (fatal_error already exited)
if string.strcmp(nm.id, "State") == 0: return mlirlite.Int32(pool)
# Builtin bare integer types (same rule as resolve_annotation_type's Name branch): enum members are
# integer constants, so `int` / `char` / `bool` pass through; other bare names fail fast on a typedef miss.
if string.strcmp(nm.id, "int") == 0: return mlirlite.Int32(pool)
if string.strcmp(nm.id, "char") == 0: return mlirlite.Int8(pool)
if string.strcmp(nm.id, "bool") == 0:
# [enum bool rule] enum storage must be byte-addressable, and i1 cannot be an enum width,
# so an enum member's bool is 8 bits. This does not change the global bool = i1.
return mlirlite.Int8(pool)
ty2: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_typedef_or_map(pool, nm.id, imported_modules, from_imports)
if ty2 is not None: return ty2
# [S4 fail-fast] annotated but unresolvable -> no silent i32 fallback
self.Gen.TypeGenErr.fatal_error(ann_node, f"failed to resolve enum member type annotation: {nm.id}", "NameError")
return mlirlite.Int32(pool) # unreachable (fatal_error already exited)
# Marker wrapper (CConst[T] / CVolatile[T] / ...): resolve the wrapped type.
if k == ast.ASTKind.Subscript:
sub_em: ast.Subscript* = ast.Subscript*(ann_node)
if sub_em.value is not None and sub_em.value.kind() == ast.ASTKind.Name:
em_nm: ast.Name* = ast.Name*(sub_em.value)
if em_nm.id is not None and (string.strcmp(em_nm.id, "CConst") == 0 or string.strcmp(em_nm.id, "CVolatile") == 0 \
or string.strcmp(em_nm.id, "BigEndian") == 0 or string.strcmp(em_nm.id, "LittleEndian") == 0 \
or string.strcmp(em_nm.id, "State") == 0 or string.strcmp(em_nm.id, "CStatic") == 0 \
or string.strcmp(em_nm.id, "CExport") == 0 or string.strcmp(em_nm.id, "CExtern") == 0 \
or string.strcmp(em_nm.id, "CRef") == 0 or string.strcmp(em_nm.id, "CTempRef") == 0):
return self._resolve_enum_member_type(pool, sub_em.slice, imported_modules, from_imports)
self.Gen.TypeGenErr.fatal_type_error(ann_node, "unsupported enum member type: only scalar types and their markers are allowed")
return mlirlite.Int32(pool)
# BinOp nodes are never enum member types (`T1 | T2` unions retired).
if k == ast.ASTKind.BinOp:
self.Gen.TypeGenErr.fatal_type_error(ann_node, "enum member type uses the retired `T1 | T2` union; write the concrete type directly")
return mlirlite.Int32(pool)
# fail-fast: unknown annotation node kind; no silent i32 fallback
self.Gen.TypeGenErr.fatal_error(ann_node, f"failed to resolve enum member type annotation: {self.Gen.TypeGenResolve.annotation_to_str(ann_node)}", "TypeError")
return mlirlite.Int32(pool) # unreachable (fatal_error already exited)
def _is_state_annotation(self, node: ast.AST*) -> int:
"""Check whether an annotation node is t.State, 1=yes / 0=no"""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(node)
if at.attr is not None and string.strcmp(at.attr, "State") == 0: return 1
return 0
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm.id is not None and string.strcmp(nm.id, "State") == 0: return 1
return 0
return 0
def _enum_annotation_name_is_ptr(self, node: ast.AST*) -> int:
"""Whether the annotation's top name (Name/Attribute/Subscript) is a pointer form, 1=yes / 0=no"""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(node)
if at.attr is not None:
if string.strcmp(at.attr, "CPtr") == 0: return 1
if string.strcmp(at.attr, "CRef") == 0: return 1
if string.strcmp(at.attr, "CTempRef") == 0: return 1
if string.strcmp(at.attr, "CAutoPtr") == 0: return 1
if string.strcmp(at.attr, "rawptr") == 0: return 1
if string.strcmp(at.attr, "str") == 0: return 1
if string.strcmp(at.attr, "bytes") == 0: return 1
return 0
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm.id is not None:
if string.strcmp(nm.id, "CPtr") == 0: return 1
if string.strcmp(nm.id, "CRef") == 0: return 1
if string.strcmp(nm.id, "CTempRef") == 0: return 1
if string.strcmp(nm.id, "CAutoPtr") == 0: return 1
if string.strcmp(nm.id, "rawptr") == 0: return 1
if string.strcmp(nm.id, "str") == 0: return 1
if string.strcmp(nm.id, "bytes") == 0: return 1
return 0
if k == ast.ASTKind.Subscript:
sub: ast.Subscript* = ast.Subscript*(node)
if sub.value is not None and self._enum_annotation_name_is_ptr(sub.value) != 0: return 1
if sub.slice is not None: return self._enum_annotation_name_is_ptr(sub.slice)
return 0
return 0
def _enum_member_annotation_is_ptr(self, ann_node: ast.AST*) -> int:
"""Whether an enum member annotation contains a pointer type, 1=yes / 0=no"""
return self._enum_annotation_name_is_ptr(ann_node)
def _extract_int_value(self, node: ast.AST*) -> int64:
"""Extract an integer value from an AST node, 0 on failure"""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Constant:
cn: ast.Constant* = ast.Constant*(node)
if cn.const_kind == ast.CONST_INT: return cn.int_val
return 0
if k == ast.ASTKind.UnaryOp:
uo: ast.UnaryOp* = ast.UnaryOp*(node)
if uo.op == ast.OpKind.USub:
inner: int64 = self._extract_int_value(uo.operand)
return -inner
return 0
return 0
# "Visible name vs registered name" fallback check for enum members (same rule source as TPC).
# Any unrecognized member form makes the visible-name count exceed the registered count -> fail-fast.
def _enum_elts_visible_names(self, elts: list[ast.AST*]*) -> int:
"""Count visible names in an element table (shared by Tuple/List)."""
if elts is None: return 0
n: int = 0
for i in range(int(len(elts))): n += self._enum_target_visible_names(elts.get(i))
return n
def _enum_target_visible_names(self, node: ast.AST*) -> int:
"""Count visible names at a member-name position (Name / Tuple / List items, Starred recursion)."""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Name: return 1
if k == ast.ASTKind.Tuple:
tup: ast.Tuple* = ast.Tuple*(node)
return self._enum_elts_visible_names(tup.elts)
if k == ast.ASTKind.List:
lst: ast.List* = ast.List*(node)
return self._enum_elts_visible_names(lst.elts)
if k == ast.ASTKind.Starred:
st: ast.Starred* = ast.Starred*(node)
return self._enum_target_visible_names(st.value)
return 0
def _enum_body_visible_names(self, children: list[ast.AST*]*) -> int:
"""Total source-visible member names in an enum class body (including forms the registrar rejects)."""
if children is None: return 0
n: int = 0
for ci in range(int(len(children))):
stmt: ast.AST* = children.get(ci)
if stmt is None: continue
sk: int = stmt.kind()
if sk == ast.ASTKind.Assign:
ag: ast.Assign* = ast.Assign*(stmt)
if ag.targets is not None:
for ti in range(int(len(ag.targets))): n += self._enum_target_visible_names(ag.targets.get(ti))
elif sk == ast.ASTKind.AnnAssign:
aa: ast.AnnAssign* = ast.AnnAssign*(stmt)
n += self._enum_target_visible_names(aa.target)
elif sk == ast.ASTKind.Expr:
ex: ast.Expr* = ast.Expr*(stmt)
n += self._enum_target_visible_names(ex.value)
return n
def _enum_first_unregistered_node(self, children: list[ast.AST*]*) -> ast.AST*:
"""First statement with visible member names the registrar rejects, for fail-fast line location; None if none."""
if children is None: return None
for ci in range(int(len(children))):
stmt: ast.AST* = children.get(ci)
if stmt is None: continue
sk: int = stmt.kind()
if sk == ast.ASTKind.Expr:
ex: ast.Expr* = ast.Expr*(stmt)
if self._enum_target_visible_names(ex.value) > 0: return stmt
elif sk == ast.ASTKind.AnnAssign:
aa: ast.AnnAssign* = ast.AnnAssign*(stmt)
if aa.target is not None and aa.target.kind() != ast.ASTKind.Name and self._enum_target_visible_names(aa.target) > 0: return stmt
elif sk == ast.ASTKind.Assign:
ag: ast.Assign* = ast.Assign*(stmt)
if ag.targets is not None:
for ti in range(int(len(ag.targets))):
tgt: ast.AST* = ag.targets.get(ti)
if tgt is not None and tgt.kind() != ast.ASTKind.Name and self._enum_target_visible_names(tgt) > 0: return stmt
return None
def _translate_enum_def(self, trans: HT.Translator*, cd: ast.ClassDef*) -> int:
"""Translate an enum class definition, return 0"""
if cd is None or cd.name is None or trans is None: return 0
pool: memhub.MemBuddy* = trans.Pool
class_name: str = cd.name
# Do not skip by global same-name: cross-module enums may share a name (e.g. FRESULT), and
# skipping would drop the later module's members. register_enum/add_enum_member are idempotent.
children: list[ast.AST*]* = cd.children
if children is None:
VLogger.warning(f"{class_name} has no body", "ENUM")
return 0
cn: sizet = len(children)
# Temporary arrays holding member info
member_names_buf: sizet* = pool.alloc(8 * ENUM_MEMBER_MAX_LOCAL)
member_types_buf: sizet* = pool.alloc(8 * ENUM_MEMBER_MAX_LOCAL)
member_values_buf: int64* = pool.alloc(8 * ENUM_MEMBER_MAX_LOCAL)
if member_names_buf is None or member_types_buf is None or member_values_buf is None: return 0
string.memset(member_names_buf, 0, 8 * ENUM_MEMBER_MAX_LOCAL)
string.memset(member_types_buf, 0, 8 * ENUM_MEMBER_MAX_LOCAL)
string.memset(member_values_buf, 0, 8 * ENUM_MEMBER_MAX_LOCAL)
member_count: int = 0
next_val: int64 = 0
for ci in range(int(cn)):
stmt: ast.AST* = children.get(ci)
if stmt is None: continue
sk: int = stmt.kind()
if sk == ast.ASTKind.AnnAssign:
aa: ast.AnnAssign* = ast.AnnAssign*(stmt)
if aa is None or aa.target is None: continue
# Get the member name
if aa.target.kind() != ast.ASTKind.Name: continue
tgt: ast.Name* = ast.Name*(aa.target)
if tgt.id is None: continue
# [enum spec fail-fast] Members may not be pointer types: rawptr / CRef /
# CTempRef / CAutoPtr / rawptr[...] / str / bytes and unions carrying a pointer. Checked before type resolution.
if self._enum_member_annotation_is_ptr(aa.annotation) == 1:
self.Gen.TypeGenErr.fatal_error(aa, f"enum member '{tgt.id}' cannot be a pointer type (enum values only support integer member types)", "TypeError")
return 0 # unreachable (fatal_error already exited)
# Resolve the member type
member_ty: mlirlite.LLVMType* = self._resolve_enum_member_type(pool, aa.annotation, trans._imported_modules, trans._from_imports)
# [enum spec fail-fast] Member types must be integers: float/void/struct/array have bit
# width 0 and cannot drive "enum width = widest member"; no silent downgrade.
member_bits: int = self.Gen.TypeGenErr.get_llvm_type_bits(member_ty)
if member_bits <= 0:
self.Gen.TypeGenErr.fatal_error(aa, f"the type of enum member '{tgt.id}' must be an integer type (pointers/floats/void/structs/arrays are not allowed), cannot infer enum width", "TypeError")
return 0 # unreachable (fatal_error already exited)
# Resolve the member value
has_value: int = 0
cur_val: int64 = 0
if aa.value is not None:
cur_val = self._extract_int_value(aa.value)
has_value = 1
else: cur_val = next_val
# Next automatic value
next_val = cur_val + 1
# Store
if member_count < ENUM_MEMBER_MAX_LOCAL:
member_names_buf[member_count] = sizet(tgt.id)
member_types_buf[member_count] = sizet(member_ty)
member_values_buf[member_count] = cur_val
member_count += 1
elif sk == ast.ASTKind.Assign:
# Handle Assign-form enum members (DEBUG = 0, INFO = 1, ...). Each Name target is a
# member (A = B = 1 registers both), matching TPC's per-target rule.
ag: ast.Assign* = ast.Assign*(stmt)
if ag is None or ag.targets is None: continue
tgts: list[ast.AST*]* = ag.targets
tn: sizet = len(tgts)
if tn == 0: continue
# Assign has no annotation; default to i32
member_ty_a: mlirlite.LLVMType* = mlirlite.Int32(pool)
# Resolve the member value (all targets in the statement share it; next_val advances once)
cur_val_a: int64 = 0
if ag.value is not None: cur_val_a = self._extract_int_value(ag.value)
else: cur_val_a = next_val
next_val = cur_val_a + 1
# Store per target (non-Name targets are caught by the registration-side fallback check)
for ti in range(int(tn)):
t0: ast.AST* = tgts.get(ti)
if t0 is None or t0.kind() != ast.ASTKind.Name: continue
tgt_a: ast.Name* = ast.Name*(t0)
if tgt_a.id is None: continue
if member_count < ENUM_MEMBER_MAX_LOCAL:
member_names_buf[member_count] = sizet(tgt_a.id)
member_types_buf[member_count] = sizet(member_ty_a)
member_values_buf[member_count] = cur_val_a
member_count += 1
# [fallback check fail-fast] "Visible member names" vs "actually registered members". Any form the
# registrar does not recognize makes visible > registered, so fail-fast instead of silently dropping.
visible_count: int = self._enum_body_visible_names(children)
if visible_count != member_count:
diff_count: int = visible_count - member_count
bad: ast.AST* = self._enum_first_unregistered_node(children)
bad_desc: str = "(cannot locate the specific statement, check the enum body)"
if bad is not None: bad_desc = bad.type_name()
self.Gen.TypeGenErr.fatal_error(bad if bad is not None else cd, f"enum '{class_name}' member registration count mismatch: {visible_count} member names visible in source, " f"but only {member_count} registered ({diff_count} difference would be silently dropped). " f"first unrecognized member form: a statement of shape {bad_desc} -- e.g. comma group with value `A, B = 1`, " f"comma group with annotation `A, B: T`, single name with trailing comma `A,`. " f"expected forms: one `Name` / `Name: T` / `Name = value` per line, " f"or multiple simple identifiers separated by commas (`A, B, C`, trailing comma allowed). " f"fix: rewrite that line as one member per line, or as a plain identifier comma group (remove `= value` / `: T`). " f"(this check compares the number of visible names vs registered names at the registrar, so any unrecognized form hits it, " f"no more members are silently dropped)", "TypeError")
return 0
if member_count == 0:
VLogger.warning(f"{class_name} has no members", "ENUM")
return 0
# Determine the base type: the widest member type in bits
base_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
max_bits: int = 0
for mi in range(int(member_count)):
ty_addr: sizet = member_types_buf[mi]
if ty_addr == 0: continue
mty: mlirlite.LLVMType* = (mlirlite.LLVMType*)(ty_addr)
bits: int = self.Gen.TypeGenErr.get_llvm_type_bits(mty)
if bits > max_bits:
max_bits = bits
base_ty = mty
# Register the enum
entry: GenTypes.EnumEntry* = self.Gen.StmtGenDecl.register_enum(pool, class_name, base_ty)
if entry is None:
VLogger.error("failed to register {class_name}", "ENUM")
return 0
# Add members
for mi in range(int(member_count)):
fname_addr: sizet = member_names_buf[mi]
if fname_addr == 0: continue
mname: str = rawptr(fname_addr)
fty_addr2: sizet = member_types_buf[mi]
if fty_addr2 == 0: continue
mty2: mlirlite.LLVMType* = (mlirlite.LLVMType*)(fty_addr2)
mval: int64 = member_values_buf[mi]
if mname is not None and mty2 is not None: self.Gen.StmtGenDecl.add_enum_member(pool, entry, mname, mval, mty2)
VLogger.info(f"registered {class_name} with {member_count} members, base_bits={max_bits}", "ENUM")
return 0
def _get_type_size(self, ty: mlirlite.LLVMType*) -> int:
"""Compute the byte size of an LLVM type including alignment padding"""
if ty is None: return 0
match ty:
case mlirlite.LLVMType.Int(bits):
return (bits + 7) // 8
case mlirlite.LLVMType.Float(bits):
return bits // 8
case mlirlite.LLVMType.Ptr(pointee):
return 8
case mlirlite.LLVMType.Array(elem_ty, count):
return self._get_type_size(elem_ty) * count
case mlirlite.LLVMType.Struct(fields, fcount, name):
# Compute the struct size with field-alignment padding: each field's alignment is its
# natural alignment and the total is padded to the largest field alignment. packed structs have no padding.
is_packed: int = self.Gen.StructGen.struct_type_is_packed(ty)
total: int = 0
max_align: int = 1
cur: mlirlite.ParamNode* = fields
i: int = 0
while cur is not None and i < fcount:
if cur.Ty is not None:
fty: mlirlite.LLVMType* = (mlirlite.LLVMType*)(cur.Ty)
fsize: int = self._get_type_size(fty)
falign: int = self._get_type_align(fty)
# Align the current offset to the field boundary (packed inserts no padding)
if is_packed == 0 and falign > 0:
rem: int = total % falign
if rem != 0: total += falign - rem
total += fsize
if is_packed == 0 and falign > max_align: max_align = falign
cur = cur.Next
i += 1
# packed has no tail alignment; non-packed totals are padded to the max field alignment
if is_packed != 0: return total
rem2: int = total % max_align
if rem2 != 0: total += max_align - rem2
return total
case _:
return 8
def _get_type_align(self, ty: mlirlite.LLVMType*) -> int:
"""Return the natural alignment of a type in bytes"""
if ty is None: return 1
match ty:
case mlirlite.LLVMType.Int(bits):
if bits <= 8: return 1
elif bits <= 16: return 2
elif bits <= 32: return 4
else: return 8
case mlirlite.LLVMType.Float(bits):
if bits <= 32: return 4
else: return 8
case mlirlite.LLVMType.Ptr(pointee):
return 8
case mlirlite.LLVMType.Array(elem_ty, count):
return self._get_type_align(elem_ty)
case mlirlite.LLVMType.Struct(fields, fcount, name):
# A packed struct is byte-aligned (alignment 1, no padding)
if self.Gen.StructGen.struct_type_is_packed(ty) != 0: return 1
# A struct's alignment is the largest field alignment
max_align: int = 1
cur: mlirlite.ParamNode* = fields
i: int = 0
while cur is not None and i < fcount:
if cur.Ty is not None:
fty: mlirlite.LLVMType* = (mlirlite.LLVMType*)(cur.Ty)
fa: int = self._get_type_align(fty)
if fa > max_align: max_align = fa
cur = cur.Next
i += 1
return max_align
case _:
return 8
def _translate_union_def(self, trans: HT.Translator*, cd: ast.ClassDef*) -> int:
"""Translate a union definition, return 0"""
if cd is None or cd.name is None or trans is None: return 0
pool: memhub.MemBuddy* = trans.Pool
class_name: str = cd.name
# Namespace isolation: mark visible in the current file (before the existing check)
self.Gen.StructGen.add_visible_struct(pool, class_name)
# Check whether already registered (SHA1 distinguishes cross-module same-name classes)
existing: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_module(class_name, trans.ModuleSha1)
if existing is not None:
# Phase B: register the existing named struct into the current module
if existing.Ty is not None: mlirlite.module_add_named_type(trans.Module, pool, existing.Ty)
# SHA1 was already set by register_struct; no need to set it again
return 0
children: list[ast.AST*]* = cd.children
if children is None:
VLogger.warning("{class_name} has no body", "UNION")
return 0
cn: sizet = len(children)
# Temporary arrays holding field info
field_names_buf: sizet* = pool.alloc(8 * GenTypes.FIELD_MAX)
field_types_buf: sizet* = pool.alloc(8 * GenTypes.FIELD_MAX)
if field_names_buf is None or field_types_buf is None: return 0
string.memset(field_names_buf, 0, 8 * GenTypes.FIELD_MAX)
string.memset(field_types_buf, 0, 8 * GenTypes.FIELD_MAX)
field_count: int = 0
max_size: int = 0
for ci in range(int(cn)):
stmt: ast.AST* = children.get(ci)
if stmt is None: continue
sk: int = stmt.kind()
if sk == ast.ASTKind.AnnAssign:
aa: ast.AnnAssign* = ast.AnnAssign*(stmt)
if aa is None or aa.target is None: continue
# Get the field name
if aa.target.kind() != ast.ASTKind.Name: continue
tgt: ast.Name* = ast.Name*(aa.target)
if tgt.id is None: continue
# Resolve the field type
field_ty: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, aa.annotation, trans._imported_modules, trans._from_imports, trans, 0)
if field_ty is None:
# Phase A-pre (declare_only=1): a dependency class may not be registered yet;
# return 1 so multi-pass convergence kicks in (renum returns 1 = skip this class)
if trans._declare_only == 1: return 1
# Phase B (declare_only=0): a real type resolution error; fail-fast
type_name: str = self.Gen.TypeGenResolve.annotation_to_str(aa.annotation)
self.Gen.TypeGenErr.fatal_error(aa, f"'{type_name}' is not defined", "NameError")
field_ty = mlirlite.Int32(pool)
# Compute the field size and update max_size
fsize: int = self._get_type_size(field_ty)
if fsize > max_size: max_size = fsize
# Store into the array
if field_count < GenTypes.FIELD_MAX:
field_names_buf[field_count] = sizet(tgt.id)
field_types_buf[field_count] = sizet(field_ty)
field_count += 1
if field_count == 0:
VLogger.warning(f"{class_name} has no fields", "UNION")
return 0
if max_size < 1: max_size = 1
# Create the union's LLVM type: Struct([Array(Int8, max_size)]), i.e. { [max_size x i8] }
i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool)
array_ty: mlirlite.LLVMType* = mlirlite.Array(pool, i8_ty, max_size)
# Build the ParamNode list (single field: byte array)
pnode: mlirlite.ParamNode* = pool.alloc(mlirlite.ParamNode.__sizeof__())
if pnode is None: return 0
string.memset(pnode, 0, mlirlite.ParamNode.__sizeof__())
pnode.Ty = array_ty
# Build the named struct type name: "sha1.ClassName" or "ClassName"
union_type_name: str = class_name
if trans.ModuleSha1 is not None:
union_name_buf: str = pool.alloc(64)
if union_name_buf is not None:
viperlib.snprintf(union_name_buf, 64, "%s.%s", trans.ModuleSha1, class_name)
union_type_name = union_name_buf
union_ty: mlirlite.LLVMType* = mlirlite.Struct(pool, pnode, 1, union_type_name)
if union_ty is None:
VLogger.error(f"failed to create type for {class_name}", "UNION")
return 0
# Deep-copy the newly registered union struct_ty to stdlib (survives cross-file reset).
# Needed even under StdlibAlloc=1 (after Phase2 mark2): else reset_to(mark2) leaves entry.Ty dangling.
if trans._declare_only == 0 or self.Gen.StructGen.get_stdlib_alloc() != 0:
union_copy: mlirlite.LLVMType* = mlirlite.DeepCopyType(CompilerFactory.factory.PermPool, union_ty)
if union_copy is not None: union_ty = union_copy
# Register the named struct into the module
mlirlite.module_add_named_type(trans.Module, pool, union_ty)
# Register into HandlesStruct (SHA1 is set directly at registration to avoid picking the wrong entry)
entry: GenTypes.StructEntry* = self.Gen.StructGen.register_struct(pool, class_name, union_ty, trans.ModuleSha1)
if entry is None:
VLogger.error(f"failed to register {class_name}", "UNION")
return 0
# Mark visible in the current file (namespace isolation)
self.Gen.StructGen.add_visible_struct(pool, class_name)
# Mark as a union (using entry directly to avoid cross-module same-name lookup errors)
entry.IsUnion = 1
# Add field info (all fields have Index=0 because they share offset 0)
for fi in range(int(field_count)):
fname_addr: sizet = field_names_buf[fi]
if fname_addr == 0: continue
fname: str = rawptr(fname_addr)
fty_addr2: sizet = field_types_buf[fi]
if fty_addr2 == 0: continue
fty2: mlirlite.LLVMType* = (mlirlite.LLVMType*)(fty_addr2)
if fname is not None and fty2 is not None: self.Gen.StructGen.add_field(pool, entry, fname, fty2, None, None)
VLogger.info(f"registered {class_name} with {field_count} fields, max_size={max_size}", "UNION")
return 0
def _get_annotation_field_size(self, pool: memhub.MemBuddy*, annotation: ast.AST*) -> int:
"""Compute the annotation type's byte size for REnum layout"""
if annotation is None: return 8
k: int = annotation.kind()
# Union type A | B -> pointer size 8
if k == ast.ASTKind.BinOp: return 8
# Name(id) or Attribute(attr) -> resolve the type name
type_name: str = None
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(annotation)
if nm is not None: type_name = nm.id
elif k == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(annotation)
if at is not None: type_name = at.attr
if type_name is None: return 8
# [stage 3] Unified sizing: library typedefs (BYTE/DWORD etc.) resolve through normal library
# analysis; the hardcoded size table below is only a fallback for t/c core primitives.
dyn_sz: int = self.Gen.TypeGenResolve.get_type_size_bytes(pool, type_name, None, None)
if dyn_sz > 0: return dyn_sz
# Builtin short names
if string.strcmp(type_name, "sizet") == 0 or string.strcmp(type_name, "size_t") == 0: return 8
if string.strcmp(type_name, "int8") == 0 or string.strcmp(type_name, "uint8") == 0: return 1
if string.strcmp(type_name, "uchar") == 0: return 1
if string.strcmp(type_name, "int16") == 0 or string.strcmp(type_name, "uint16") == 0: return 2
if string.strcmp(type_name, "short") == 0 or string.strcmp(type_name, "ushort") == 0: return 2
if string.strcmp(type_name, "int32") == 0 or string.strcmp(type_name, "uint32") == 0: return 4
if string.strcmp(type_name, "int") == 0 or string.strcmp(type_name, "uint") == 0: return 4
if string.strcmp(type_name, "long") == 0 or string.strcmp(type_name, "ulong") == 0: return 4
if string.strcmp(type_name, "int64") == 0 or string.strcmp(type_name, "uint64") == 0: return 8
if string.strcmp(type_name, "longlong") == 0 or string.strcmp(type_name, "ulonglong") == 0: return 8
if string.strcmp(type_name, "ptrdifft") == 0: return 8
# Builtin types
if string.strcmp(type_name, "int") == 0: return 4
if string.strcmp(type_name, "CInt") == 0: return 4
if string.strcmp(type_name, "CInt8T") == 0: return 1
if string.strcmp(type_name, "CInt16T") == 0: return 2
if string.strcmp(type_name, "CInt32T") == 0: return 4
if string.strcmp(type_name, "CInt64T") == 0: return 8
if string.strcmp(type_name, "CChar") == 0: return 1
if string.strcmp(type_name, "CDouble") == 0: return 8
if string.strcmp(type_name, "CFloat") == 0: return 4
if string.strcmp(type_name, "CPtr") == 0: return 8
if string.strcmp(type_name, "CSizeT") == 0: return 8
if string.strcmp(type_name, "str") == 0: return 8
if string.strcmp(type_name, "bytes") == 0: return 8
if string.strcmp(type_name, "bool") == 0: return 1
# Registered struct type
struct_ty: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type(type_name)
if struct_ty is not None:
sz: int = self._get_type_size(struct_ty)
if sz > 0: return sz
# Unregistered type -> default pointer size 8 (may be self- or forward-referenced)
return 8
def _translate_renum_def(self, trans: HT.Translator*, cd: ast.ClassDef*) -> int:
"""Translate an REnum class definition, return 0"""
if cd is None or cd.name is None or trans is None: return 0
pool: memhub.MemBuddy* = trans.Pool
class_name: str = cd.name
# Namespace isolation: mark visible in the current file
self.Gen.StructGen.add_visible_struct(pool, class_name)
# Check whether already registered (SHA1 distinguishes cross-module same-name classes)
existing: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_module(class_name, trans.ModuleSha1)
if existing is not None:
# existing only guarantees the struct entry, not the REnum semantics (variant table): the entry
# lives in StructTable across phases, but the variant table is a per-Generator field reset each time.
if self._is_renum_class_name(class_name) != 0:
if existing.Ty is not None: mlirlite.module_add_named_type(trans.Module, pool, existing.Ty)
return 0
# Variant table missing: neutralize the old entry so find_struct_by_module no longer matches,
# then fall through to full registration to fix both the layout and the variant table.
if existing.Name is not None: existing.Name[0] = '\0'
existing.Ty = None
existing.FieldCount = 0
children: list[ast.AST*]* = cd.children
if children is None:
# [D32 fail-fast] Previously only warned and returned: the variant table was silently unregistered,
# so _is_renum_class_name stayed 0 and variant construction lost its tag. No silent skip here.
self.Gen.TypeGenErr.fatal_error(cd, f"REnum class '{class_name}' has no class body (children is None), cannot register the variant table", "TypeError")
return 0
cn: sizet = len(children)
# At most 32 variants per class, 16 fields per variant; variant_field_sizes[i][j] = variant i's field j size.
variant_field_sizes: sizet* = pool.alloc(8 * 64 * 16)
if variant_field_sizes is None: return 0
string.memset(variant_field_sizes, 0, 8 * 64 * 16)
variant_count: int = 0
max_field_count: int = 0 # max field count across all variants
for ci in range(int(cn)):
stmt: ast.AST* = children.get(ci)
if stmt is None: continue
sk: int = stmt.kind()
if sk == ast.ASTKind.ClassDef:
# Nested variant class
variant_cd: ast.ClassDef* = ast.ClassDef*(stmt)
if variant_cd is None or variant_cd.children is None:
variant_count += 1
continue
variant_children: list[ast.AST*]* = variant_cd.children
vcn: sizet = len(variant_children)
field_idx: int = 0
for vi in range(int(vcn)):
vstmt: ast.AST* = variant_children.get(vi)
if vstmt is None: continue
vk: int = vstmt.kind()
if vk != ast.ASTKind.AnnAssign: continue
vaa: ast.AnnAssign* = ast.AnnAssign*(vstmt)
if vaa is None or vaa.target is None or vaa.annotation is None: continue
if vaa.target.kind() != ast.ASTKind.Name: continue
# Extract the field name
vnm: ast.Name* = ast.Name*(vaa.target)
# Compute the field size
fsize: int = self._get_annotation_field_size(pool, vaa.annotation)
if variant_count < 32 and field_idx < 16:
# variant_field_sizes[variant_count][field_idx] = fsize
offset: sizet = variant_count * 16 + field_idx
variant_field_sizes[offset] = sizet(fsize)
# Register the variant field-name alias directly, resolving its actual type for match
# binding. Self-referencing annotations fail to resolve here and fall back to the slot type.
if vnm is not None and vnm.id is not None:
mem_ty_l: mlirlite.LLVMType* = None
if trans is not None: mem_ty_l = self.Gen.TypeGenResolve.resolve_annotation_type(pool, vaa.annotation, trans._imported_modules, trans._from_imports, trans, 0)
self._register_renum_field_alias(pool, class_name, vnm.id, field_idx + 1, mem_ty_l, variant_cd.name)
field_idx += 1
if field_idx > max_field_count: max_field_count = field_idx
variant_count += 1
elif sk == ast.ASTKind.Assign:
# Assign-form payload-less variants (e.g. `Empty = 1`): same rule as TPC, still a variant (occupies
# a slot) with no fields. Tags are assigned by _register_renum_variants (explicit values first).
ag_v: ast.Assign* = ast.Assign*(stmt)
if ag_v is None or ag_v.targets is None: continue
tgts_v: list[ast.AST*]* = ag_v.targets
if len(tgts_v) == 0: continue
t0_v: ast.AST* = tgts_v.get(0)
if t0_v is None or t0_v.kind() != ast.ASTKind.Name: continue
tgt_v: ast.Name* = ast.Name*(t0_v)
if tgt_v.id is None: continue
variant_count += 1
if variant_count == 0:
# [D32 fail-fast] Same as above: variant_count 0 previously returned silently, skipping registration.
# For `class X(t.REnum)` the count must be > 0; 0 means the AST variant classes were lost.
self.Gen.TypeGenErr.fatal_error(cd, f"REnum class '{class_name}' has 0 variants (nested variant classes lost), cannot register the variant table", "TypeError")
return 0
# Compute the max field size for each position.
# Position 0 = __tag (i32, 4 bytes); positions 1+ = variant fields.
total_positions: int = max_field_count + 1 # +1 for tag
if total_positions < 2:
total_positions = 2 # at least {i32, i32}
position_max_sizes: sizet* = pool.alloc(8 * total_positions)
if position_max_sizes is None: return 0
string.memset(position_max_sizes, 0, 8 * total_positions)
# Position 0 = tag = 4 bytes
position_max_sizes[0] = sizet(4)
# Positions 1+: take the max field size across all variants at each position
for pos in range(1, int(total_positions)):
pos_max: sizet = 0
for vi in range(int(variant_count)):
if vi >= 64:
break # variant_field_sizes 64-class slot cap; guards against out-of-bounds reads
field_pos: int = pos - 1 # variant fields start at position 0
if field_pos >= 16:
continue # beyond the 16-field-per-variant cap: not written (memset 0), skip
offset2: sizet = vi * 16 + field_pos
fsize_val: sizet = variant_field_sizes[offset2]
if fsize_val > pos_max: pos_max = fsize_val
position_max_sizes[pos] = sizet(pos_max)
# Build the ParamNode list
i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
i64_ty: mlirlite.LLVMType* = mlirlite.Int64(pool)
first_node: mlirlite.ParamNode* = None
tail_node: mlirlite.ParamNode* = None
for pos in range(int(total_positions)):
psize: int = int(position_max_sizes[pos])
field_ty: mlirlite.LLVMType* = i32_ty
if psize > 4: field_ty = i64_ty
pnode: mlirlite.ParamNode* = mlirlite.new_param_node(pool, field_ty)
if pnode is None: continue
tail_node = mlirlite.param_list_append(first_node, tail_node, pnode)
if first_node is None: first_node = tail_node
if first_node is None:
VLogger.error(f"failed to build field list for {class_name}", "RENUM")
return 0
# Build the named struct type name: "sha1.ClassName" or "ClassName"
renum_type_name: str = class_name
if trans.ModuleSha1 is not None:
renum_name_buf: str = pool.alloc(64)
if renum_name_buf is not None:
viperlib.snprintf(renum_name_buf, 64, "%s.%s", trans.ModuleSha1, class_name)
renum_type_name = renum_name_buf
renum_ty: mlirlite.LLVMType* = mlirlite.Struct(pool, first_node, total_positions, renum_type_name)
if renum_ty is None:
VLogger.error(f"failed to create type for {class_name}", "RENUM")
return 0
# Deep-copy the newly registered REnum struct_ty to stdlib (survives cross-file reset and keeps the
# same pointer as entry.Ty so find_struct_by_type still matches); required under StdlibAlloc=1 too.
if trans._declare_only == 0 or self.Gen.StructGen.get_stdlib_alloc() != 0:
renum_copy: mlirlite.LLVMType* = mlirlite.DeepCopyType(CompilerFactory.factory.PermPool, renum_ty)
if renum_copy is not None: renum_ty = renum_copy
# Register the named struct into the module
mlirlite.module_add_named_type(trans.Module, pool, renum_ty)
# Register into HandlesStruct.
# A forward-declared placeholder for THIS class may have been created *during the variant walk*
# above: a variant field like `Pointee: LLVMType*` is `BinOp(LLVMType | rawptr)` (parser's
# _make_ptr_type expands the C-style suffix into a union, NOT into `rawptr[...]`), so resolving it
# resolves the class name and calls ensure_forward_class. register_struct de-duplicates by Ty
# pointer, so it would add a SECOND same-named entry and get_struct_type/find_struct (first match
# wins) would keep returning the opaque placeholder -> __sizeof__() == 0 at the first use
# ("registered but size 0, field chain unfilled"). Fill the placeholder in place instead, which is
# exactly what ensure_forward_class documents ("the definition side fills the same Ty in place").
entry: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_module(class_name, trans.ModuleSha1)
if entry is not None and entry.IsForwardDecl != 0:
entry.Ty = renum_ty
entry.IsForwardDecl = 0
else:
entry = self.Gen.StructGen.register_struct(pool, class_name, renum_ty, trans.ModuleSha1)
if entry is None:
VLogger.error(f"failed to register {class_name}", "RENUM")
return 0
# Add the __tag field (position 0, i32)
self.Gen.StructGen.add_field(pool, entry, "__tag", i32_ty, None, None)
# Add payload fields (positions 1+, i32 or i64)
for pos in range(1, int(total_positions)):
psize2: int = int(position_max_sizes[pos])
payload_ty: mlirlite.LLVMType* = i32_ty
if psize2 > 4: payload_ty = i64_ty
field_name_buf: str = pool.alloc(16)
if field_name_buf is not None:
viperlib.snprintf(field_name_buf, 16, "_p%d", pos)
self.Gen.StructGen.add_field(pool, entry, field_name_buf, payload_ty, None, None)
# Register variant names into the global variant table (translate_call detects REnum construction)
self._register_renum_variants(pool, class_name, cd, variant_count)
# Variant field-name aliases were registered during the variant walk, which runs before register_struct;
# self-referencing annotations left a 0 type slot, so refill it now (see _refill_renum_field_types).
self._refill_renum_field_types(trans, pool, class_name, cd)
VLogger.info(f"registered {class_name} with {variant_count} variants, {total_positions} positions", "RENUM")
return 0
def _refill_renum_field_types(self, trans: HT.Translator*, pool: memhub.MemBuddy*, class_name: str, cd: ast.ClassDef*):
"""After REnum registration, re-resolve variant field types and backfill only aliases whose type slot is still 0."""
if trans is None or pool is None or class_name is None or cd is None: return
children: list[ast.AST*]* = cd.children
if children is None: return
cn: sizet = len(children)
for ci in range(int(cn)):
stmt: ast.AST* = children.get(ci)
if stmt is None or stmt.kind() != ast.ASTKind.ClassDef: continue
variant_cd: ast.ClassDef* = ast.ClassDef*(stmt)
if variant_cd is None or variant_cd.name is None or variant_cd.children is None: continue
vchildren: list[ast.AST*]* = variant_cd.children
vcn: sizet = len(vchildren)
field_idx: int = 0
for vi in range(int(vcn)):
vstmt: ast.AST* = vchildren.get(vi)
if vstmt is None: continue
if vstmt.kind() != ast.ASTKind.AnnAssign: continue
vaa: ast.AnnAssign* = ast.AnnAssign*(vstmt)
if vaa is None or vaa.target is None or vaa.annotation is None:
# Same rule as the first pass: an invalid AnnAssign does not occupy a payload slot
continue
if vaa.target.kind() != ast.ASTKind.Name: continue
vnm: ast.Name* = ast.Name*(vaa.target)
if vnm.id is None:
# Same rule as the first pass: a valid AnnAssign still occupies a payload slot
field_idx += 1
continue
mem_ty_l: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, vaa.annotation, trans._imported_modules, trans._from_imports, trans, 0)
# Resolved -> trigger the alias dedupe path (backfill when the type slot is 0; skip a non-zero slot)
if mem_ty_l is not None: self._register_renum_field_alias(pool, class_name, vnm.id, field_idx + 1, mem_ty_l, variant_cd.name)
field_idx += 1
def _register_renum_variants(self, pool: memhub.MemBuddy*, class_name: str, cd: ast.ClassDef*, variant_count: int):
"""Register an REnum class's variant names and tag values into the global table"""
if pool is None or class_name is None or cd is None: return
if variant_count == 0: return
children: list[ast.AST*]* = cd.children
if children is None: return
# Lazily initialize the tables on first registration. They must be stdlib-allocated: if pool-allocated
# they would be reclaimed by reset_to(mark1) and later matches would read freed memory and crash.
if self.RenumClassNames is None:
self.RenumClassNames = CompilerFactory.factory.PermPool.alloc(8 * RENUM_CLASS_MAX)
self.RenumClassVariantCounts = CompilerFactory.factory.PermPool.alloc(8 * RENUM_CLASS_MAX)
self.RenumVariantNames = CompilerFactory.factory.PermPool.alloc(8 * RENUM_CLASS_MAX * RENUM_VARIANT_PER_CLASS)
self.RenumVariantTags = CompilerFactory.factory.PermPool.alloc(8 * RENUM_CLASS_MAX * RENUM_VARIANT_PER_CLASS)
if (self.RenumClassNames is None or self.RenumClassVariantCounts is None or self.RenumVariantNames is None or self.RenumVariantTags is None):
# A None table means the perm-pool request failed; returning here left the table empty while the
# caller still reported "registered", so every REnum case in match dispatch was silently lost.
self.RenumClassNames = None
self.Gen.TypeGenErr.fatal_error(cd, f"failed to allocate REnum variant table (class='{class_name}'): PermPool returned NULL, variant table unavailable", "InternalCompilerError")
return
string.memset(self.RenumClassNames, 0, 8 * RENUM_CLASS_MAX)
string.memset(self.RenumClassVariantCounts, 0, 8 * RENUM_CLASS_MAX)
string.memset(self.RenumVariantNames, 0, 8 * RENUM_CLASS_MAX * RENUM_VARIANT_PER_CLASS)
string.memset(self.RenumVariantTags, 0, 8 * RENUM_CLASS_MAX * RENUM_VARIANT_PER_CLASS)
self.RenumClassCount = 0
# [D30] Register the pointers into the compile-time factory singleton (same lifetime as struct
# entries); InitClassGen restores them for new Generators instead of zeroing them.
CompilerFactory.factory.RenumClassNames = self.RenumClassNames
CompilerFactory.factory.RenumClassVariantCounts = self.RenumClassVariantCounts
CompilerFactory.factory.RenumVariantNames = self.RenumVariantNames
CompilerFactory.factory.RenumVariantTags = self.RenumVariantTags
CompilerFactory.factory.RenumClassCount = self.RenumClassCount
#rcv_chk: uint64 = uint64(self.RenumClassVariantCounts)
# VLogger.error(f"[RENUM-FAIL] RCV={rcv_chk} abnormal (perm_pool address expected after lazy init)", "RENUM")
# Check whether already registered (avoid duplicate registration)
for i in range(int(self.RenumClassCount)):
existing: str = str(self.RenumClassNames[i])
if existing is not None and string.strcmp(existing, class_name) == 0: return
# Table full
if self.RenumClassCount >= RENUM_CLASS_MAX:
# Silently dropping the class made its later `match` lose every case; a
# real limit breach must be reported instead.
self.Gen.TypeGenErr.fatal_error(cd, f"REnum class count exceeds the limit {RENUM_CLASS_MAX}: cannot register class='{class_name}'", "InternalCompilerError")
return
# Register the class name (strcpy into perm_pool: after each file's 1a reset the AST raw pointer
# dangles, and strcmp in _lookup_renum_variant/_is_renum_class_name would read poisoned memory).
idx: int = self.RenumClassCount
cn_len_rv: sizet = string.strlen(class_name)
cn_buf_rv: str = CompilerFactory.factory.PermPool.alloc(cn_len_rv + 1)
if cn_buf_rv is None:
self.Gen.TypeGenErr.fatal_error(cd, f"failed to allocate REnum class name copy (class='{class_name}'): PermPool returned NULL", "InternalCompilerError")
return
string.strcpy(cn_buf_rv, class_name)
self.RenumClassNames[idx] = sizet(cn_buf_rv)
self.RenumClassVariantCounts[idx] = variant_count
# Walk children and register variant names + tags. Two forms: `class Some: ...` (payload variant,
# tags numbered from max(explicit)+1) and `Empty = 1` (payload-less, tag = explicit integer value).
cn: sizet = len(children)
# First pass: find the upper bound of Assign variants' explicit integer values (stays -1 if none)
max_explicit: int = -1
for ci in range(int(cn)):
stmt_p: ast.AST* = children.get(ci)
if stmt_p is None or stmt_p.kind() != ast.ASTKind.Assign: continue
ag_p: ast.Assign* = ast.Assign*(stmt_p)
if ag_p is None or ag_p.targets is None or ag_p.value is None: continue
tgts_p: list[ast.AST*]* = ag_p.targets
if len(tgts_p) == 0: continue
t0_p: ast.AST* = tgts_p.get(0)
if t0_p is None or t0_p.kind() != ast.ASTKind.Name: continue
if ag_p.value.kind() != ast.ASTKind.Constant: continue
ev_v: int = int(self._extract_int_value(ag_p.value))
if ev_v > max_explicit: max_explicit = ev_v
# Second pass: write the table in declaration order. slot is the contiguous table index (0..count-1),
# while the cell stores the tag; they are no longer equal, so a single counter cannot serve both.
slot: int = 0
next_class_tag: int = max_explicit + 1
variant_index: int = 0
for ci in range(int(cn)):
if slot >= variant_count: break
if slot >= RENUM_VARIANT_PER_CLASS:
# Fixed 32 slots per class: exceeding it would overflow into the next class's slots; report fail-fast
VLogger.error(f"REnum variant count exceeds the limit {RENUM_VARIANT_PER_CLASS}: {class_name}", "RENUM")
break
stmt: ast.AST* = children.get(ci)
if stmt is None: continue
sk_rv: int = stmt.kind()
vname_ptr: sizet = 0
tag_val: int = 0
if sk_rv == ast.ASTKind.ClassDef:
vcd: ast.ClassDef* = ast.ClassDef*(stmt)
if vcd is None or vcd.name is None: continue
vname_ptr = sizet(vcd.name)
tag_val = next_class_tag
next_class_tag += 1
elif sk_rv == ast.ASTKind.Assign:
# Assign-form payload-less variant (same filter as _translate_renum_def's first pass)
ag_rv: ast.Assign* = ast.Assign*(stmt)
if ag_rv is None or ag_rv.targets is None: continue
tgts_rv: list[ast.AST*]* = ag_rv.targets
if len(tgts_rv) == 0: continue
t0_rv: ast.AST* = tgts_rv.get(0)
if t0_rv is None or t0_rv.kind() != ast.ASTKind.Name: continue
tgt_rv: ast.Name* = ast.Name*(t0_rv)
if tgt_rv.id is None: continue
vname_ptr = sizet(tgt_rv.id)
if ag_rv.value is not None and ag_rv.value.kind() == ast.ASTKind.Constant: tag_val = int(self._extract_int_value(ag_rv.value))
else:
# No explicit value: use a separate counter (distinct from the class-variant counter, as
# TPC's variant_index) so it cannot collide with next_class_tag.
tag_val = variant_index
variant_index += 1
else: continue
name_offset: sizet = idx * RENUM_VARIANT_PER_CLASS + slot
# The variant name is also strcpy'd into perm_pool (the AST name dangles after each file's 1a reset)
vname_src: str = str(vname_ptr)
vn_len_rv: sizet = string.strlen(vname_src)
vn_buf_rv: str = CompilerFactory.factory.PermPool.alloc(vn_len_rv + 1)
if vn_buf_rv is None:
# Breaking here registered a partial variant table: the class would
# keep some variants and silently lose every later one.
self.Gen.TypeGenErr.fatal_error(cd, f"failed to allocate REnum variant name copy (class='{class_name}'): PermPool returned NULL", "InternalCompilerError")
break
string.strcpy(vn_buf_rv, vname_src)
self.RenumVariantNames[name_offset] = sizet(vn_buf_rv)
self.RenumVariantTags[name_offset] = tag_val
slot += 1
self.RenumClassCount = idx + 1
# [D30] Write the count back to the factory singleton (new Generators restore from it; otherwise they
# would see a stale count and miss subsequently registered variants).
CompilerFactory.factory.RenumClassCount = self.RenumClassCount
def _lookup_renum_variant(self, variant_name: str) -> str:
"""Find the REnum class name owning a variant name; None if absent"""
if variant_name is None or self.RenumClassNames is None: return None
for i in range(int(self.RenumClassCount)):
vcount: int = int(self.RenumClassVariantCounts[i])
for j in range(int(vcount)):
name_offset: sizet = i * RENUM_VARIANT_PER_CLASS + j
vname: str = str(self.RenumVariantNames[name_offset])
if vname is not None and string.strcmp(vname, variant_name) == 0:
cls_nm: str = str(self.RenumClassNames[i])
return cls_nm
return None
def RenumTableDump(self) -> str:
"""Diagnostic variant-table summary to tell 'class never registered' from 'class present but variant missing'."""
buf: str = CompilerFactory.factory.PermPool.alloc(256)
if buf is None: return "-"
buf[0] = '\0'
cls_first: str = "-"
v_first: str = "-"
n_first_v: int = 0
if self.RenumClassNames is not None and self.RenumClassCount > 0:
cls_first = str(self.RenumClassNames[0])
if self.RenumVariantNames is not None and self.RenumClassVariantCounts is not None:
n_first_v = int(self.RenumClassVariantCounts[0])
if n_first_v > 0: v_first = str(self.RenumVariantNames[0])
viperlib.snprintf(buf, 256, "count=%d first=%s vcount=%d vfirst=%s", self.RenumClassCount, cls_first, n_first_v, v_first)
return buf
def _is_renum_by_entry(self, class_name: str) -> int:
"""Variant-table-free REnum test: a struct definition carrying a `__tag` field is an REnum, so match dispatch keeps variant bindings even if the table is missing."""
if class_name is None: return 0
st: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type(class_name)
if st is None: return 0
if self.Gen.StructGen.lookup_field(st, "__tag") is None: return 0
return 1
def _is_renum_class_name(self, class_name: str) -> int:
"""Check whether a name is a registered REnum class, 1=yes / 0=no"""
if class_name is None or self.RenumClassNames is None: return 0
for i in range(int(self.RenumClassCount)):
existing: str = str(self.RenumClassNames[i])
if existing is not None and string.strcmp(existing, class_name) == 0: return 1
return 0
def _lookup_renum_variant_in_class(self, class_name: str, variant_name: str) -> int:
"""Find a variant in a given REnum class, return its tag value or -1"""
if class_name is None or variant_name is None or self.RenumClassNames is None: return -1
for i in range(int(self.RenumClassCount)):
existing: str = str(self.RenumClassNames[i])
if existing is None or string.strcmp(existing, class_name) != 0: continue
vcount: int = int(self.RenumClassVariantCounts[i])
for j in range(int(vcount)):
name_offset: sizet = i * RENUM_VARIANT_PER_CLASS + j
vname: str = str(self.RenumVariantNames[name_offset])
if vname is not None and string.strcmp(vname, variant_name) == 0: return int(self.RenumVariantTags[name_offset])
return -1
return -1
def _register_renum_field_alias(self, pool: memhub.MemBuddy*, class_name: str, field_name: str, payload_idx: int, field_ty: mlirlite.LLVMType* = None, variant_name: str = None):
"""Register an REnum variant field name alias in the global table; optional field type and variant name aid match binding."""
if pool is None or class_name is None or field_name is None: return
if payload_idx < 1: return
# Lazily initialize the tables (perm_pool: the tables themselves survive each file's 1a reset)
if self.RenumAliasClass is None:
alias_pool_rfa: memhub.MemBuddy* = CompilerFactory.factory.PermPool
self.RenumAliasClass = alias_pool_rfa.alloc(8 * RENUM_ALIAS_MAX)
self.RenumAliasField = alias_pool_rfa.alloc(8 * RENUM_ALIAS_MAX)
self.RenumAliasIndex = alias_pool_rfa.alloc(8 * RENUM_ALIAS_MAX)
self.RenumAliasTy = alias_pool_rfa.alloc(8 * RENUM_ALIAS_MAX)
self.RenumAliasVariant = alias_pool_rfa.alloc(8 * RENUM_ALIAS_MAX)
if (self.RenumAliasClass is None or self.RenumAliasField is None or self.RenumAliasIndex is None or self.RenumAliasTy is None or self.RenumAliasVariant is None):
# Same as the variant table: a silent bail left the alias table empty,
# so match bindings later failed with an unrelated NameError.
self.RenumAliasClass = None
self.Gen.TypeGenErr.fatal_error(None, f"failed to allocate REnum field alias table (class='{class_name}'): PermPool returned NULL", "InternalCompilerError")
return
string.memset(self.RenumAliasClass, 0, 8 * RENUM_ALIAS_MAX)
string.memset(self.RenumAliasField, 0, 8 * RENUM_ALIAS_MAX)
string.memset(self.RenumAliasIndex, 0, 8 * RENUM_ALIAS_MAX)
string.memset(self.RenumAliasTy, 0, 8 * RENUM_ALIAS_MAX)
string.memset(self.RenumAliasVariant, 0, 8 * RENUM_ALIAS_MAX)
self.RenumAliasCount = 0
# [D30] Same as above: register the alias table pointers into the factory singleton to survive Generators
CompilerFactory.factory.RenumAliasClass = self.RenumAliasClass
CompilerFactory.factory.RenumAliasField = self.RenumAliasField
CompilerFactory.factory.RenumAliasIndex = self.RenumAliasIndex
CompilerFactory.factory.RenumAliasTy = self.RenumAliasTy
CompilerFactory.factory.RenumAliasVariant = self.RenumAliasVariant
CompilerFactory.factory.RenumAliasCount = self.RenumAliasCount
if self.RenumAliasCount >= RENUM_ALIAS_MAX:
# Dropping the record silently made the variant field bind to a wrong-typed
# slot (or fail with NameError) instead of reporting the limit breach.
self.Gen.TypeGenErr.fatal_error(None, f"REnum field alias count exceeds the limit {RENUM_ALIAS_MAX}: cannot register {class_name}.{field_name}", "InternalCompilerError")
return
# Dedupe check (pointer-free): the class slot holds a perm_pool string copy compared with strcmp,
# so it no longer relies on pointer equality (registration uses AST pointers, lookup uses pool copies).
for i in range(int(self.RenumAliasCount)):
cls_existing_ptr: str = rawptr(self.RenumAliasClass[i])
if cls_existing_ptr is not None and string.strcmp(cls_existing_ptr, class_name) == 0:
fld_existing_ptr: str = rawptr(self.RenumAliasField[i])
if fld_existing_ptr is not None and string.strcmp(fld_existing_ptr, field_name) == 0:
# **变体名也必须一致**:同一 REnum 的不同变体可有**同名字段**
# (LLVMType.Int.Bits 与 LLVMType.Float.Bits)。旧判据只比 (class, field),
# 第二个同名字段被当成重复记录直接丢弃 ⇒ _lookup_renum_member_ty(class,
# variant, idx) 查不到该记录 ⇒ match 绑定退化为按**物理槽类型**读取
# (max-payload 槽是 i8*/i64)⇒ 赋给 i32 时误报"隐式窄化禁止"(假阳性)。
_vn_existing: str = rawptr(self.RenumAliasVariant[i])
_vn_same: int = 0
if variant_name is None:
_vn_same = 1 if _vn_existing is None else 0
elif _vn_existing is not None:
_vn_same = 1 if string.strcmp(_vn_existing, variant_name) == 0 else 0
if _vn_same == 0:
continue
# Existing record: if the first pass left the type slot 0 (self-referencing field, see
# _refill_renum_field_types), backfill the real type now; a non-zero slot is left as is.
if self.RenumAliasTy[i] == 0 and field_ty is not None:
ty_copy_rf2: mlirlite.LLVMType* = mlirlite.DeepCopyType(CompilerFactory.factory.PermPool, field_ty)
if ty_copy_rf2 is not None: self.RenumAliasTy[i] = sizet(ty_copy_rf2)
return
idx: int = self.RenumAliasCount
# class_name is also strcpy'd into perm_pool (pointer-free: any class_name string from the lookup
# side matches via strcmp, so no _update_renum_alias_class_ptr is needed).
cls_len_rfa: sizet = string.strlen(class_name)
cls_buf_rfa: str = CompilerFactory.factory.PermPool.alloc(cls_len_rfa + 1)
if cls_buf_rfa is None: return
string.strcpy(cls_buf_rfa, class_name)
self.RenumAliasClass[idx] = sizet(cls_buf_rfa)
# field_name stored as a raw AST pointer dangles after each file's 1a reset; since
# _lookup_renum_field_alias compares with strcmp, copying into perm_pool does not affect lookup.
fn_len_rfa: sizet = string.strlen(field_name)
fn_buf_rfa: str = CompilerFactory.factory.PermPool.alloc(fn_len_rfa + 1)
if fn_buf_rfa is None: return
string.strcpy(fn_buf_rfa, field_name)
self.RenumAliasField[idx] = sizet(fn_buf_rfa)
self.RenumAliasIndex[idx] = sizet(payload_idx)
# Deep-copy the member type into perm_pool (pool types dangle after each file's 1a reset; on an
# unresolved type such as a self reference, store 0 and fall back to the slot type when binding).
self.RenumAliasTy[idx] = 0
if field_ty is not None:
ty_copy_rfa: mlirlite.LLVMType* = mlirlite.DeepCopyType(CompilerFactory.factory.PermPool, field_ty)
if ty_copy_rfa is not None: self.RenumAliasTy[idx] = sizet(ty_copy_rfa)
# The variant name is also strcpy'd into perm_pool (variant_cd.name dangles after the 1a reset)
self.RenumAliasVariant[idx] = 0
if variant_name is not None:
vn_len_rfa: sizet = string.strlen(variant_name)
vn_buf_rfa: str = CompilerFactory.factory.PermPool.alloc(vn_len_rfa + 1)
if vn_buf_rfa is not None:
string.strcpy(vn_buf_rfa, variant_name)
self.RenumAliasVariant[idx] = sizet(vn_buf_rfa)
self.RenumAliasCount = idx + 1
# [D30] Write the count back to the factory singleton (new Generators restore from it)
CompilerFactory.factory.RenumAliasCount = self.RenumAliasCount
def _lookup_renum_field_alias(self, class_name: str, field_name: str) -> int:
"""Look up an REnum variant field name's payload index, return it or -1 (class/field compared by content)."""
if class_name is None or field_name is None or self.RenumAliasClass is None: return -1
for i in range(int(self.RenumAliasCount)):
cls_existing_ptr: str = rawptr(self.RenumAliasClass[i])
if cls_existing_ptr is not None and string.strcmp(cls_existing_ptr, class_name) == 0:
# field_name is compared by strcmp content (pointers differ, content is the same)
fld_existing_ptr: str = rawptr(self.RenumAliasField[i])
if fld_existing_ptr is not None and string.strcmp(fld_existing_ptr, field_name) == 0: return int(self.RenumAliasIndex[i])
return -1
def _lookup_renum_field_alias_ty(self, class_name: str, field_name: str) -> mlirlite.LLVMType*:
"""Look up an REnum variant member's actual LLVM type, or None when the type slot is 0 (binding falls back to the slot type)."""
if class_name is None or field_name is None or self.RenumAliasTy is None: return None
for i in range(int(self.RenumAliasCount)):
cls_existing_ptr: str = rawptr(self.RenumAliasClass[i])
if cls_existing_ptr is not None and string.strcmp(cls_existing_ptr, class_name) == 0:
fld_existing_ptr: str = rawptr(self.RenumAliasField[i])
if fld_existing_ptr is not None and string.strcmp(fld_existing_ptr, field_name) == 0:
if self.RenumAliasTy[i] == 0: return None
return (mlirlite.LLVMType*)(self.RenumAliasTy[i])
return None
def _lookup_renum_member_ty(self, class_name: str, variant_name: str, payload_idx: int) -> mlirlite.LLVMType*:
"""Look up a variant member's actual LLVM type by (class, variant name, payload index) for match binding; None if unresolved."""
if class_name is None or variant_name is None or self.RenumAliasVariant is None: return None
if payload_idx < 1: return None
for i in range(int(self.RenumAliasCount)):
cls_existing_ptr: str = rawptr(self.RenumAliasClass[i])
if cls_existing_ptr is None or string.strcmp(cls_existing_ptr, class_name) != 0: continue
if self.RenumAliasVariant[i] == 0: continue
vnt_ptr: str = rawptr(self.RenumAliasVariant[i])
if vnt_ptr is None or string.strcmp(vnt_ptr, variant_name) != 0: continue
if self.RenumAliasIndex[i] != payload_idx: continue
if self.RenumAliasTy[i] == 0: return None
return (mlirlite.LLVMType*)(self.RenumAliasTy[i])
return None