Files
ViperC/App/lib/core/Gen/Expr/ExprGenAttr.vp
T
TermiNexus 508428bbc9 fix(viperc): 浮点常量按目标类型/声明宽度直接出 f32,不再先出 double
- 浮点字面量在 f32 目标(float(1.5) / x: float = 1.5 / f32 形参)下直接物化 f32 常量,
  负号折叠(-c)同口径;去掉旧的「f64 常量 + fptrunc」
- CDefine 浮点条目记录宽度种类(1=f64 / 2=f32):define PI: float = ... 的使用处
  直接出 f32 常量,与 f32 字面量混算不再被提升成 double(现场 33 处 cvtss2sd)
- 本模块注册 / pyi·vpi 跨模块重解析 / xmod 缓存三处都按注解裁决宽度,保持跨模块一致
2026-10-15 18:35:48 +08:00

1150 lines
86 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
import t, c
from stdint import *
import ast
import mlirlite
import memhub
import string
import stdio
import stdlib
import viperlib
import lib.core.Gen.Translator as HT
import lib.Stub.StubMerger.StubMergerStore as StubMergerStore
import lib.Stub.StubMerger.StubMergerBuild as StubMergerBuild
import lib.core.VLogger as VLogger
import lib.core.Gen.Expr.ExprGenBase as ExprGenBase
import lib.core.Gen.GenApi as GenApi
# ============================================================
# ExprGenAttr - Attribute access / global references
novirtual class ExprGenAttr(ExprGenBase.ExprGenBase):
"""Attribute access and global references."""
# find_global_in_module - find a global variable by name in the module's global list:
# first an exact match, then a suffix match ".name" (e.g. "_mbuddy" → @"sha1._mbuddy").
def find_global_in_module(self, mod: mlirlite.LLVMModule*, name: str) -> mlirlite.GlobalVariable*:
"""Find a global variable by name in the module's global list."""
if mod is None or name is None: return None
name_len: sizet = string.strlen(name)
cur: mlirlite.GlobalVariable* = mod.GlobalHead
# first pass: exact match
while cur is not None:
if cur.Name is not None:
if string.strcmp(cur.Name, name) == 0: return cur
cur = cur.Next
# second pass: suffix match (.name)
cur = mod.GlobalHead
while cur is not None:
if cur.Name is not None:
cur_len: sizet = string.strlen(cur.Name)
if cur_len > name_len + 1:
suffix_start: sizet = cur_len - name_len
if cur.Name[suffix_start - 1] == '.':
Matched: int = 1
for i in range(int(name_len)):
if cur.Name[suffix_start + i] != name[i]:
Matched = 0
break
if Matched == 1: return cur
cur = cur.Next
return None
# make_global_ref - create a global variable reference (@name, type ty*); used for
# cross-module global access when the stub is not injected (linked via external global).
def make_global_ref(self, pool: memhub.MemBuddy*, name: str, ty: mlirlite.LLVMType*) -> mlirlite.Value*:
"""Create a global variable reference named @name with type ty*."""
if name is None or ty is None: return None
ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, ty)
ref_name: str = pool.alloc(96)
if ref_name is None: return None
# LLVM unquoted identifiers cannot start with a digit: a SHA1-prefixed global name
# ("sha1.name") must be quoted @"sha1.name", else llc: "expected instruction opcode".
if self.Gen.ClassGenVTable._vtable_name_needs_quote(name) != 0: viperlib.snprintf(ref_name, 96, "@\"%s\"", name)
else: viperlib.snprintf(ref_name, 96, "@%s", name)
return mlirlite.SSAValue(pool, ptr_ty, ref_name)
# _resolve_module_attribute_global - resolve a module-attribute access to a global variable
# reference: look up the current module first, else create a forward reference @attr_name.
def _is_t_type_object_name(self, name: str) -> int:
"""Check whether name is a t.py type-object name (CPtr/CInt/CDouble, ...)."""
if name is None: return 0
# type object names in t.py (PascalCase, initial uppercase)
if string.strcmp(name, "CPtr") == 0: return 1
if string.strcmp(name, "CInt") == 0: return 1
if string.strcmp(name, "CChar") == 0: return 1
if string.strcmp(name, "CShort") == 0: return 1
if string.strcmp(name, "CLong") == 0: return 1
if string.strcmp(name, "CLongLong") == 0: return 1
if string.strcmp(name, "CDouble") == 0: return 1
if string.strcmp(name, "CFloat") == 0: return 1
if string.strcmp(name, "CBool") == 0: return 1
if string.strcmp(name, "CVoid") == 0: return 1
if string.strcmp(name, "CInt8T") == 0: return 1
if string.strcmp(name, "CInt16T") == 0: return 1
if string.strcmp(name, "CInt32T") == 0: return 1
if string.strcmp(name, "CInt64T") == 0: return 1
if string.strcmp(name, "CUInt8T") == 0: return 1
if string.strcmp(name, "CUInt16T") == 0: return 1
if string.strcmp(name, "CUInt32T") == 0: return 1
if string.strcmp(name, "CUInt64T") == 0: return 1
if string.strcmp(name, "CUnsigned") == 0: return 1
if string.strcmp(name, "CUnsignedInt") == 0: return 1
if string.strcmp(name, "CUnsignedChar") == 0: return 1
if string.strcmp(name, "CUnsignedShort") == 0: return 1
if string.strcmp(name, "CUnsignedLong") == 0: return 1
if string.strcmp(name, "CUnsignedLongLong") == 0: return 1
if string.strcmp(name, "CSizeT") == 0: return 1
if string.strcmp(name, "CIntPtrT") == 0: return 1
if string.strcmp(name, "CUIntPtrT") == 0: return 1
if string.strcmp(name, "CPtrDiffT") == 0: return 1
if string.strcmp(name, "CType") == 0: return 1
if string.strcmp(name, "CDefine") == 0: return 1
if string.strcmp(name, "CStatic") == 0: return 1
if string.strcmp(name, "CExport") == 0: return 1
if string.strcmp(name, "CArray") == 0: return 1
if string.strcmp(name, "CTypedef") == 0: return 1
if string.strcmp(name, "CNoVTable") == 0: return 1
if string.strcmp(name, "CRef") == 0: return 1
if string.strcmp(name, "CTempRef") == 0: return 1
if string.strcmp(name, "CAutoPtr") == 0: return 1
# Byte-order markers (t.BigEndian / t.LittleEndian): decorator markers used as
# `X | t.BigEndian`; recognized here so they are not taken for an unregistered class.
if string.strcmp(name, "BigEndian") == 0: return 1
if string.strcmp(name, "LittleEndian") == 0: return 1
return 0
def _attr_module_path_str(self, pool: memhub.MemBuddy*, node: ast.AST*) -> str:
"""Convert a module-reference AST into a dotted module path; None for non-module forms."""
if node is None: return None
k: int = node.kind()
if k == ast.ASTKind.Name:
nm_m: ast.Name* = ast.Name*(node)
if nm_m.id is not None: return nm_m.id
return None
if k == ast.ASTKind.Attribute:
at_m: ast.Attribute* = ast.Attribute*(node)
if at_m.attr is None: return None
base_m: str = self._attr_module_path_str(pool, at_m.value)
if base_m is None: return None
blen: sizet = string.strlen(base_m)
alen: sizet = string.strlen(at_m.attr)
buf_m: str = pool.alloc(blen + alen + 2)
if buf_m is None: return None
viperlib.snprintf(buf_m, blen + alen + 2, "%s.%s", base_m, at_m.attr)
return buf_m
return None
# _CheckStaticCrossModule - reject cross-module `mod.name` access to static symbols
# (static = internal linkage); kept separate from translate_attribute to protect its inference.
def _CheckStaticCrossModule(self, pool: memhub.MemBuddy*, trans: HT.Translator*, at: ast.Attribute*) -> int:
if at is None or at.value is None or trans is None: return 0
if trans.ModuleSha1 is None or trans._imported_modules is None: return 0
# fast shortcut: zero cost when the project has no static symbols (no sha1/table scan)
if self.Gen.CallGenBase.StaticCount <= 0: return 0
cs_path: str = self._attr_module_path_str(pool, at.value)
if cs_path is None: return 0
# resolve import alias (import X as Y → look up the original module name, else sha1 misses)
if trans._from_imports is not None:
cs_orig: str = self.Gen.ImportsGen.lookup_from_import(trans._from_imports, cs_path, 0)
if cs_orig is not None:
cs_len: sizet = 0
while cs_orig[cs_len] != ' ' and cs_orig[cs_len] != '\0' and cs_orig[cs_len] != ':': cs_len += 1
if cs_len > 0:
cs_buf: str = pool.alloc(cs_len + 1)
if cs_buf is not None:
string.strncpy(cs_buf, cs_orig, cs_len)
cs_buf[cs_len] = '\0'
cs_path = cs_buf
if self.Gen.ImportsGen.is_module_imported(trans._imported_modules, cs_path) == 0: return 0
cs_sha1: str = self._lookup_module_sha1(cs_path)
if cs_sha1 is None: return 0
if string.strncmp(cs_sha1, trans.ModuleSha1, 16) == 0: return 0
if self.Gen.CallGenBase.IsStaticSymbol(cs_sha1, at.attr) == 0: return 0
self.Gen.TypeGenErr.fatal_error(at, f"'{cs_path}.{at.attr}' is a static symbol, cross-module use is forbidden (static is only visible inside its defining module)", "NameError")
return 0
def _resolve_module_attribute_global(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, at: ast.Attribute*, ty_hint: mlirlite.LLVMType*) -> mlirlite.Value*:
"""Resolve a module attribute access to a global variable reference for cross-module use."""
if at is None or at.value is None or at.attr is None: return None
if trans is None or trans._imported_modules is None: return None
# extract the module path: Name → "sys"; nested Attribute → "os._win32" (the latter
# matters for writes like `os._win32._mbuddy = v`, not just Name modules).
mod_path_w: str = self._attr_module_path_str(pool, at.value)
if mod_path_w is None: return None
# resolve aliases (import lib.core.Gen.Generator as GenGenerator) via from_imports, else
# sha1 lookup fails and the global gets an undefined SHA1-prefixed symbol at link time.
if trans._from_imports is not None:
orig_mw: str = self.Gen.ImportsGen.lookup_from_import(trans._from_imports, mod_path_w, 0)
if orig_mw is not None:
oml_a: sizet = 0
while orig_mw[oml_a] != ' ' and orig_mw[oml_a] != '\0' and orig_mw[oml_a] != ':': oml_a += 1
if oml_a > 0:
om_buf_a: str = pool.alloc(oml_a + 1)
if om_buf_a is not None:
string.strncpy(om_buf_a, orig_mw, oml_a)
om_buf_a[oml_a] = '\0'
mod_path_w = om_buf_a
# check whether it is an imported module
if self.Gen.ImportsGen.is_module_imported(trans._imported_modules, mod_path_w) == 0: return None
# note: static cross-module interception is done in translate_attribute's _CheckStaticCrossModule.
# Look up the global preferring an exact "target-module SHA1 prefix + attr" match, else the bare name.
gv_q: str = self.Gen.FuncGenBase._mangle_name_with_sha1(self._lookup_module_sha1(mod_path_w), at.attr)
gv: mlirlite.GlobalVariable* = self.find_global_in_module(mod, gv_q)
# CExport globals (defined with a bare name like @_sched_ptr): when the mangled name is not
# found, fall back to the bare exact name, else an undefined SHA1-prefixed symbol results.
if gv is None: gv = self.find_global_in_module(mod, at.attr)
if gv is not None and gv.Ty is not None:
# build the reference from gv.Name (full name, matching the definition when SHA1-prefixed),
# avoiding an undefined symbol for a bare @_mbuddy under per-module prefixed definitions
return self.make_global_ref(pool, gv.Name, gv.Ty)
# A module function used as a value (`return serial.puts`, `cb = serial.puts`): the symbol
# is already declared in this module as `llvm.func @<sha1>.puts`, so manufacturing an
# external global under the same name makes mlir-opt reject the module with "redefinition
# of symbol named '<sha1>.puts'". Hand back the function symbol itself instead: a Value
# whose text is the reference `@<mangled>` typed Ptr(Func), the same shape the same-module
# `&f` branch (ExprGenOps.translate_unaryop) produces -- the caller must not load it.
if gv_q is not None:
fn_ma: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, at.attr)
if fn_ma is not None:
fname_ma: str = mlirlite.function_get_name(fn_ma)
# exact match only: a bare suffix hit could be another module's same-name function
if fname_ma is not None and string.strcmp(fname_ma, gv_q) == 0:
fn_ty_ma: mlirlite.LLVMType* = self.Gen.CallGenBase._FuncTyFromFunction(pool, fn_ma)
if fn_ty_ma is not None:
fnlen_ma: sizet = string.strlen(fname_ma)
fbuf_ma: str = pool.alloc(fnlen_ma + 4)
if fbuf_ma is not None:
if self.Gen.ClassGenVTable._vtable_name_needs_quote(fname_ma) != 0: viperlib.snprintf(fbuf_ma, fnlen_ma + 4, "@\"%s\"", fname_ma)
else: viperlib.snprintf(fbuf_ma, fnlen_ma + 4, "@%s", fname_ma)
return mlirlite.SSAValue(pool, mlirlite.Ptr(pool, fn_ty_ma), fbuf_ma)
# not found: create an external global declaration and add it to the module; otherwise only
# an @_argc reference is emitted with no declaration (LLVM: "use of undefined value").
use_ty: mlirlite.LLVMType* = ty_hint
if use_ty is None: use_ty = mlirlite.Int8(pool)
# ensure cross-module struct types in use_ty have an opaque declaration
mlirlite.module_ensure_opaque_for_type(mod, pool, use_ty)
# special case: type objects in t.py (CPtr/CInt/CDouble etc.). t.py is not compiled but
# these objects are referenced at runtime, so emit a linkonce_odr definition with null init.
is_t_type_obj: int = 0
if string.strcmp(mod_path_w, "t") == 0: is_t_type_obj = self._is_t_type_object_name(at.attr)
# a class name is not a module-level global: registered classes/structs/REnums are always
# handled by the class/REnum paths, never manufactured into an external global here.
if is_t_type_obj == 0 and self.Gen.StructGen.find_struct(at.attr) is not None: return None
# fallback declaration name: t type objects (t.py uncompiled, no sha1) keep the bare
# linkonce_odr name; other module attributes carry the target module's SHA1 prefix.
gv_name_ba: str = at.attr
if is_t_type_obj == 0: gv_name_ba = self.Gen.FuncGenBase._mangle_name_with_sha1(self._lookup_module_sha1(mod_path_w), at.attr)
ext_gv: mlirlite.GlobalVariable* = mlirlite.new_global_variable(pool, gv_name_ba, use_ty)
if ext_gv is not None:
if is_t_type_obj != 0:
# t.CPtr etc.: create a definition with a null initializer (linkonce_odr)
ext_gv.Linkage = "linkonce_odr"
# initializer text for i8* null
null_init: bytes = pool.alloc(8)
if null_init is not None:
null_init[0] = 'n'
null_init[1] = 'u'
null_init[2] = 'l'
null_init[3] = 'l'
null_init[4] = '\0'
ext_gv.Initializer = null_init
else: ext_gv.Initializer = None
else:
# regular module attribute: external declaration (definition provided elsewhere)
ext_gv.Linkage = "external"
ext_gv.Initializer = None
mlirlite.module_add_global(mod, ext_gv)
return self.make_global_ref(pool, gv_name_ba, use_ty)
def _lookup_module_sha1(self, mod_name: str) -> str:
"""Look up a module name's SHA1 via StubMergerStore, returning None when not found."""
return StubMergerStore.LookupSha1ByModName(mod_name)
# translate_attribute - translate Attribute(value, attr, ctx) i.e. obj.field by priority: enum member,
# module attribute (sys._mbuddy → global), then struct field (GEP + load). Name bases skip the struct load.
# _get_cderef_attr_base - base pointer of a c.Deref(ptr).attr access (like *p.a → p->a): return the ptr
# itself, not the loaded struct value; None for non-c.Deref forms (_deref_if_ptr_ptr resolves the level).
def _get_cderef_attr_base(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, at_value: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Return the ptr itself for a dereferenced attribute base (`c.Deref(ptr).attr` / `(*ptr).attr`), else None (no load)."""
if at_value is None: return None
# New syntax form: `(*ptr).attr` is AST UnaryOp(OpKind.Deref) — identical semantics to the legacy
# `c.Deref(ptr).attr`. Without this branch the attribute write path sees a *loaded struct value*
# as the base and fails with "base=非指针类型,无法做属性写入"; the read path silently does an
# extra struct load. Both must GEP the pointer itself (R4 migration of `c.Deref` -> `*`).
if at_value.kind() == ast.ASTKind.UnaryOp:
uo_cd: ast.UnaryOp* = ast.UnaryOp*(at_value)
if uo_cd.op != ast.OpKind.Deref or uo_cd.operand is None: return None
inner_uo: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, uo_cd.operand, None, 0, trans)
if inner_uo is not None and self.is_ptr_type(inner_uo.Ty) != 0: return inner_uo
return None
if at_value.kind() != ast.ASTKind.Call: return None
cl_cd: ast.Call* = ast.Call*(at_value)
if cl_cd.func is None or cl_cd.func.kind() != ast.ASTKind.Attribute: return None
f_cd: ast.Attribute* = ast.Attribute*(cl_cd.func)
if f_cd.attr is None or string.strcmp(f_cd.attr, "Deref") != 0: return None
if f_cd.value is None or f_cd.value.kind() != ast.ASTKind.Name: return None
nm_cd: ast.Name* = ast.Name*(f_cd.value)
if nm_cd.id is None or string.strcmp(nm_cd.id, "c") != 0: return None
if cl_cd.args is None or len(cl_cd.args) < 1: return None
inner_cd: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cl_cd.args.get(0), None, 0, trans)
if inner_cd is not None and self.is_ptr_type(inner_cd.Ty) != 0: return inner_cd
return None
def translate_attribute(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Translate an attribute access and return the loaded field value."""
if node is None or builder is None: return None
at: ast.Attribute* = ast.Attribute*(node)
if at is None or at.value is None or at.attr is None:
# fast-fail: a missing AST field is a parser bug; report the specific missing field
diag_buf_an: bytes = stdlib.malloc(256)
if diag_buf_an is not None:
missing_an: str = "?"
if at is not None:
if at.value is None: missing_an = "value"
elif at.attr is None: missing_an = "attr"
viperlib.snprintf(diag_buf_an, 256, "attribute access failed: AST node field missing (%s sha1=%s:%d)", missing_an, trans.ModuleSha1 if trans is not None and trans.ModuleSha1 is not None else "?", at.lineno if at is not None and at.lineno is not None else 0)
VLogger.error(diag_buf_an, "ATTR")
return None
# static symbol (cross-module use forbidden): `mod.name` hitting another module's static
# symbol is fail-fast; the logic stays in _CheckStaticCrossModule, never inlined here.
self._CheckStaticCrossModule(pool, trans, at)
# magic attribute __doc__: consult the docstring registry; a hit inlines a string constant, a miss
# returns null. Supports local and cross-module symbols (func / Class / Class.method / mod.func...).
if string.strcmp(at.attr, "__doc__") == 0:
doc_mod_buf: bytes = pool.alloc(64)
doc_sym_buf: bytes = pool.alloc(256)
if doc_mod_buf is None or doc_sym_buf is None: return None
doc_mod_buf[0] = '\0'
doc_sym_buf[0] = '\0'
if at.value is not None and at.value.kind() == ast.ASTKind.Name:
nm_doc: ast.Name* = ast.Name*(at.value)
if nm_doc.id is not None: self.Gen.ExprGenCls._resolve_doc_symbol_name(trans, nm_doc.id, doc_mod_buf, doc_sym_buf)
elif at.value is not None and at.value.kind() == ast.ASTKind.Attribute: self.Gen.ExprGenCls._resolve_doc_symbol_attr(trans, ast.Attribute*(at.value), doc_mod_buf, doc_sym_buf)
doc_val: str = None
if doc_mod_buf[0] != '\0' and doc_sym_buf[0] != '\0': doc_val = self.Gen.ExprGenCls.lookup_docstring(doc_mod_buf, doc_sym_buf)
if doc_val is None:
i8_ty_doc: mlirlite.LLVMType* = mlirlite.Int8(pool)
if i8_ty_doc is None: return None
i8_ptr_ty_doc: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_doc)
if i8_ptr_ty_doc is None: return None
return mlirlite.ConstNull(pool, i8_ptr_ty_doc, "null")
return self.Gen.ExprGenConst.create_global_string(builder, pool, mod, doc_val, trans)
# enum member access: EnumName.MemberName → constant value, when value is a Name whose id
# is a registered enum, look up the member and return its constant.
if at.value.kind() == ast.ASTKind.Name:
enum_nm: ast.Name* = ast.Name*(at.value)
if enum_nm.id is not None:
if self.Gen.StmtGenDecl.is_enum_class(enum_nm.id) == 1:
member: GenTypes.EnumMember* = self.Gen.StmtGenDecl.lookup_enum_member(enum_nm.id, at.attr)
if member is not None:
base_ty: mlirlite.LLVMType* = self.Gen.StmtGenDecl.get_enum_base_type(enum_nm.id)
if base_ty is not None:
# emit a numeric string as the constant name (LLVM IR prints numeric values)
name_buf: str = pool.alloc(32)
if name_buf is not None: viperlib.snprintf(name_buf, 32, "%lld", member.Value)
return mlirlite.ConstInt(pool, base_ty, member.Value, name_buf)
return mlirlite.const_int32(pool, member.Value)
# fast-fail: enum member not found
VLogger.error(f"enum member not found: {enum_nm.id}.{at.attr}", "ATTR")
return None
# nested enum member access: Module.EnumName.MemberName → constant, when value is an Attribute at any
# module depth whose attr is a registered enum. Only the innermost attr is the enum class name.
if at.value.kind() == ast.ASTKind.Attribute:
inner_at_e: ast.Attribute* = ast.Attribute*(at.value)
if inner_at_e.attr is not None:
if self.Gen.StmtGenDecl.is_enum_class(inner_at_e.attr) == 1:
member_e: GenTypes.EnumMember* = self.Gen.StmtGenDecl.lookup_enum_member( inner_at_e.attr, at.attr)
if member_e is not None:
base_ty_e: mlirlite.LLVMType* = self.Gen.StmtGenDecl.get_enum_base_type(inner_at_e.attr)
if base_ty_e is not None:
name_buf_e: str = pool.alloc(32)
if name_buf_e is not None: viperlib.snprintf(name_buf_e, 32, "%lld", member_e.Value)
return mlirlite.ConstInt(pool, base_ty_e, member_e.Value, name_buf_e)
return mlirlite.const_int32(pool, member_e.Value)
# fast-fail: nested enum member not found
dbg_ee: GenTypes.EnumEntry* = self.Gen.StmtGenDecl.find_enum(inner_at_e.attr)
dbg_mc: int = -1
if dbg_ee is not None: dbg_mc = dbg_ee.MemberCount
VLogger.error(f"nested enum member not found: {inner_at_e.attr}.{at.attr} (enum_members={dbg_mc})", "ATTR")
return None
# REnum variant reference as a value: ClassName.Variant / Module.ClassName.Variant → the variant tag
# constant (matching _lookup_renum_variant_in_class); factory names return -1 and pass through.
renum_cls_rv: str = None
if at.value.kind() == ast.ASTKind.Name:
rn_nm_rv: ast.Name* = ast.Name*(at.value)
if rn_nm_rv.id is not None: renum_cls_rv = rn_nm_rv.id
elif at.value.kind() == ast.ASTKind.Attribute:
rn_at_rv: ast.Attribute* = ast.Attribute*(at.value)
if rn_at_rv.attr is not None: renum_cls_rv = rn_at_rv.attr
if renum_cls_rv is not None and self.Gen.ClassGenEnum._is_renum_class_name(renum_cls_rv) != 0:
rv_tag: int = self.Gen.ClassGenEnum._lookup_renum_variant_in_class(renum_cls_rv, at.attr)
if rv_tag >= 0:
name_buf_rv: str = pool.alloc(32)
if name_buf_rv is not None:
viperlib.snprintf(name_buf_rv, 32, "%d", rv_tag)
return mlirlite.ConstInt(pool, mlirlite.Int32(pool), rv_tag, name_buf_rv)
return mlirlite.ConstInt(pool, mlirlite.Int32(pool), rv_tag, "0")
# FakeDuck: shadow-struct attribute read (per-instance fields of a str variable): for a
# Name variable with a {var}__meta__ shadow (created by fd2), GEP the field and load it.
if at.value.kind() == ast.ASTKind.Name and trans is not None:
fd_nm: ast.Name* = ast.Name*(at.value)
if fd_nm.id is not None:
# pooled persistent buffer (an f-string stack buffer dies on return)
fd_meta_buf: str = pool.alloc(256)
if fd_meta_buf is not None: viperlib.snprintf(fd_meta_buf, 256, "%s__meta__", fd_nm.id)
fd_meta_v: mlirlite.Value* = self.Gen.VarGen.lookup_current(trans.SymTab, fd_meta_buf)
if fd_meta_v is not None:
fd_sty: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type("_str")
if fd_sty is not None:
fd_fe: GenTypes.FieldEntry* = self.Gen.StructGen.lookup_field(fd_sty, at.attr)
if fd_fe is not None and fd_fe.Ty is not None:
fd_fp: mlirlite.Value* = mlirlite.build_gep_struct(builder, fd_sty, fd_fe.Ty, fd_meta_v, fd_fe.Index)
if fd_fp is not None: return mlirlite.build_load(builder, fd_fe.Ty, fd_fp)
# module attribute access (read path): sys._mbuddy → load global @_mbuddy; when obj is an
# imported module name (and not an ordinary variable), find/create the global and load it.
if at.value.kind() == ast.ASTKind.Name and trans is not None:
nm_ma: ast.Name* = ast.Name*(at.value)
if nm_ma.id is not None:
# first check whether it is an ordinary variable (higher priority than module attr)
ma_is_var: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm_ma.id)
if ma_is_var is None:
# first check whether it is a CDefine compile-time constant (e.g. ast.CONST_INT /
# win32base.INVALID_HANDLE_VALUE); such constants require a cross-module .vp/.vpi search
cdef_ma_val: int64 = self.Gen.TypeGenCDef.lookup_cdefine_constant(at.attr)
cd_local: int = self.Gen.TypeGenCDef.is_cdefine_found()
if cd_local == 0:
# determine the real module name: prefer resolving the alias from _from_imports,
# whose entries look like "local_name:real_module" (e.g. win32base:win32.win32base)
real_mod_name: str = nm_ma.id
if trans._from_imports is not None:
resolved_mod: str = self.Gen.ImportsGen.lookup_from_import(trans._from_imports, nm_ma.id, 0)
if resolved_mod is not None:
# copy the module name into a new buffer (truncate at space, null, or ':')
rm_len: sizet = 0
while resolved_mod[rm_len] != ' ' and resolved_mod[rm_len] != '\0' and resolved_mod[rm_len] != ':': rm_len += 1
rm_buf: str = pool.alloc(rm_len + 1)
if rm_buf is not None:
for rm_i in range(int(rm_len)): rm_buf[rm_i] = resolved_mod[rm_i]
rm_buf[rm_len] = '\0'
real_mod_name = rm_buf
# even when _from_imports is None (e.g. a plain `import ast`), still attempt the
# cross-module CDefine lookup, else ast.CONST_INT is misread as @CONST_INT (undefined).
if real_mod_name is not None:
ma_fi_len: sizet = string.strlen(at.attr) + string.strlen(real_mod_name) + 2
ma_fi: str = pool.alloc(ma_fi_len)
if ma_fi is not None:
viperlib.snprintf(ma_fi, ma_fi_len, "%s:%s", at.attr, real_mod_name)
cdef_ma_val = self.Gen.ExprGenCdef._lookup_cross_module_cdefine(pool, at.attr, ma_fi)
if self.Gen.TypeGenCDef.is_cdefine_found() != 0:
# cross-module string CDefine (e.g. pyzlib.ZLIB_VERSION = "1.3.2")
# → string global constant (test_pyzlib.vp: ver = pyzlib.ZLIB_VERSION)
if self.Gen.TypeGenCDef.is_cdefine_str() != 0:
cdef_str_v: str = self.Gen.TypeGenCDef.lookup_cdefine_string(at.attr)
if cdef_str_v is not None: return self.Gen.ExprGenConst.create_global_string(builder, pool, mod, cdef_str_v, trans)
# cross-module float CDefine (e.g. vipermath.U_M_PI) → float constant
dchk_ma: double = self.Gen.TypeGenCDef.lookup_cdefine_double(at.attr)
# f32 声明的常量直接出 f32 常量(否则 f32 字面量会被提升成 double)
if self.Gen.TypeGenCDef.is_cdefine_double() != 0:
if self.Gen.TypeGenCDef.is_cdefine_float32() != 0: return mlirlite.ConstFloat(pool, mlirlite.Float(pool), dchk_ma)
return mlirlite.ConstFloat(pool, mlirlite.Double(pool), dchk_ma)
return self.Gen.ExprGenCdef._const_int_for_cdefine(pool, at.attr, cdef_ma_val)
# check whether nm_ma.id.at.attr is an imported submodule (e.g. win32.win32file);
# for a submodule do not create a global, let the nested attribute path handle it
is_submod: int = 0
if trans._imported_modules is not None:
submod_len: sizet = string.strlen(nm_ma.id) + string.strlen(at.attr) + 2
submod_buf: str = pool.alloc(submod_len)
if submod_buf is not None:
viperlib.snprintf(submod_buf, submod_len, "%s.%s", nm_ma.id, at.attr)
if self.Gen.ImportsGen.is_module_imported(trans._imported_modules, submod_buf) == 1: is_submod = 1
# also check from_imports: the at.attr alias maps to the real module win32.win32file
if is_submod == 0 and trans._from_imports is not None:
fi_mod_raw: str = self.Gen.ImportsGen.lookup_from_import(trans._from_imports, at.attr, 0)
if fi_mod_raw is not None:
# copy the module name into a new buffer (truncate at space, null, or ':')
fm_len: sizet = 0
while fi_mod_raw[fm_len] != ' ' and fi_mod_raw[fm_len] != '\0' and fi_mod_raw[fm_len] != ':': fm_len += 1
fi_mod: str = pool.alloc(fm_len + 1)
if fi_mod is not None:
for fm_i in range(int(fm_len)): fi_mod[fm_i] = fi_mod_raw[fm_i]
fi_mod[fm_len] = '\0'
# check whether fi_mod starts with "nm_ma.id." (e.g. win32.win32file with win32.)
prefix_len: sizet = string.strlen(nm_ma.id)
fi_mod_len: sizet = string.strlen(fi_mod)
if fi_mod_len > prefix_len + 1:
match_prefix: int = 1
for pi in range(int(prefix_len)):
if fi_mod[pi] != nm_ma.id[pi]:
match_prefix = 0
break
if match_prefix == 1 and fi_mod[prefix_len] == '.': is_submod = 1
if is_submod != 0:
return None # it is a submodule, hand it to the nested attribute path
# special case: t.ASM_DESCR is the inline-asm constraint namespace, not a CDefine; its
# .XXX is handled by _asm_resolve_constraint, so return None for the nested path to handle.
if string.strcmp(nm_ma.id, "t") == 0 and string.strcmp(at.attr, "ASM_DESCR") == 0: return None
# ALL_CAPS module attribute that is not a CDefine: load it as a cross-module global when the
# module defines a same-named global (e.g. PA_DEPS_MAX_STR); else fail-fast (undefined value).
if self.Gen.ExprGenCdef._is_cdefine_name_pattern(at.attr) != 0:
if self.find_global_in_module(mod, at.attr) is None:
self.Gen.TypeGenErr.fatal_error(at, f"cross-module CDefine not found: '{nm_ma.id}.{at.attr}' (CDefine cross-module lookup failed)", "TypeError")
# hit a cross-module global → fall through to the cross-module global load path
# not a CDefine: use i8* as the type hint (module-level variables usually hold pointers)
i8_ptr_hint: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool))
mod_gv_ref: mlirlite.Value* = self._resolve_module_attribute_global(pool, mod, trans, at, i8_ptr_hint)
if mod_gv_ref is not None and mod_gv_ref.Ty is not None:
load_ty_mod: mlirlite.LLVMType* = mod_gv_ref.Ty.Pointee
if load_ty_mod is not None:
# A module function used as a value is already the address (Ptr(Func),
# the symbol reference itself): loading it would read the function body,
# so return the reference unchanged.
match load_ty_mod:
case mlirlite.LLVMType.Func(_, _, _):
return mod_gv_ref
case mlirlite.LLVMType.Array(_, _):
# module-level global array (e.g. zdef.zdeflate_len_base: CArray[T, N]):
# decay to the base address per C semantics; loading [N x T] cannot be GEP base.
return mod_gv_ref
case _:
pass
return mlirlite.build_load(builder, load_ty_mod, mod_gv_ref)
# nested module attribute access (win32.win32file.GENERIC_READ → global @GENERIC_READ): when
# at.value is an Attribute resolving to an imported submodule, look up at.attr as a global constant
if at.value.kind() == ast.ASTKind.Attribute and trans is not None:
inner_at: ast.Attribute* = ast.Attribute*(at.value)
if inner_at.value is not None and inner_at.value.kind() == ast.ASTKind.Name:
inner_nm: ast.Name* = ast.Name*(inner_at.value)
if inner_nm.id is not None and inner_at.attr is not None:
if self.Gen.VarGen.lookup_var(trans.SymTab, inner_nm.id) is None:
# build the qualified module name: "win32.win32file"
qual_len: sizet = string.strlen(inner_nm.id) + string.strlen(inner_at.attr) + 2
qual_buf: str = pool.alloc(qual_len)
if qual_buf is not None:
viperlib.snprintf(qual_buf, qual_len, "%s.%s", inner_nm.id, inner_at.attr)
# module knownness: an explicit import (win32.win32file) or the global module
# registry (pkg.X registered via `from . import X` in __init__.vp, e.g. intr.idt)
mod_known_n: int = self.Gen.ImportsGen.is_module_imported(trans._imported_modules, qual_buf)
if mod_known_n == 0 and self._lookup_module_sha1(qual_buf) is not None:
# known submodule not explicitly imported: inject its .vpi declarations first, else
# find_global_in_module misses its CExport global (@idt) (main.vp intr.idt.idt[32]).
StubMergerBuild.LoadDepVpiIntoModule(pool, mod, qual_buf, trans._from_imports, self.Gen.TypeGenBase.get_temp_dir(), trans)
mod_known_n = 1
if mod_known_n != 0:
# look up the global at.attr with an exact qualified-module SHA1 prefix
# match (avoiding suffix collision); fall back to the bare name if sha1 unknown
gv_nested_q: str = self.Gen.FuncGenBase._mangle_name_with_sha1(self._lookup_module_sha1(qual_buf), at.attr)
gv_nested: mlirlite.GlobalVariable* = self.find_global_in_module(mod, gv_nested_q)
# CExport global (defined with a bare name like @idt): fall back to the
# bare exact lookup when the mangled name misses (matching the read path)
if gv_nested is None: gv_nested = self.find_global_in_module(mod, at.attr)
if gv_nested is not None and gv_nested.Ty is not None:
gv_ref_nested: mlirlite.Value* = self.make_global_ref(pool, gv_nested.Name, gv_nested.Ty)
if gv_ref_nested is not None and gv_ref_nested.Ty is not None:
load_ty_nested: mlirlite.LLVMType* = gv_ref_nested.Ty.Pointee
if load_ty_nested is not None:
# module-level global array (e.g. idt.idt: CArray[T, N]) returns
# the base-address pointer per C semantics; indexing/address keeps GEP
match load_ty_nested:
case mlirlite.LLVMType.Array(_, _):
return gv_ref_nested
case _:
pass
return mlirlite.build_load(builder, load_ty_nested, gv_ref_nested)
# global not found: try a CDefine constant lookup; for nested module attributes
# like win32.win32base.INVALID_HANDLE_VALUE, resolve the CDefine from the .vpi
cdef_val_nested: int64 = self.Gen.TypeGenCDef.lookup_cdefine_constant(at.attr)
if self.Gen.TypeGenCDef.is_cdefine_found() == 0:
# build a temporary from_imports "attr:module" for the cross-module lookup
tmp_fi_len: sizet = string.strlen(at.attr) + string.strlen(qual_buf) + 2
tmp_fi: str = pool.alloc(tmp_fi_len)
if tmp_fi is not None:
viperlib.snprintf(tmp_fi, tmp_fi_len, "%s:%s", at.attr, qual_buf)
cdef_val_nested = self.Gen.ExprGenCdef._lookup_cross_module_cdefine(pool, at.attr, tmp_fi)
if self.Gen.TypeGenCDef.is_cdefine_found() != 0:
# cross-module float CDefine → float constant (f32 声明直接出 f32)
dchk_nst: double = self.Gen.TypeGenCDef.lookup_cdefine_double(at.attr)
if self.Gen.TypeGenCDef.is_cdefine_double() != 0:
if self.Gen.TypeGenCDef.is_cdefine_float32() != 0: return mlirlite.ConstFloat(pool, mlirlite.Float(pool), dchk_nst)
return mlirlite.ConstFloat(pool, mlirlite.Double(pool), dchk_nst)
return self.Gen.ExprGenCdef._const_int_for_cdefine(pool, at.attr, cdef_val_nested)
# CDefine not found: fail-fast and stop compiling (no more undefined global
# references, which would make llc report "use of undefined value").
if self.Gen.ExprGenCdef._is_cdefine_name_pattern(at.attr) != 0:
self.Gen.TypeGenErr.fatal_error(at, f"nested attribute cross-module CDefine not found: '{qual_buf}.{at.attr}' (CDefine cross-module lookup failed)", "TypeError")
return None
# for a Name obj, look up the alloca directly (no struct load); nonlocal variables (hoisted
# into the function) read their shared heap value from env, avoiding a cross-function alloca ref.
obj_ptr: mlirlite.Value* = None
if at.value.kind() == ast.ASTKind.Name and trans is not None:
nm: ast.Name* = ast.Name*(at.value)
if nm.id is not None:
if GenApi.is_nonlocal_name(trans, nm.id) != 0:
raw_nl_a: mlirlite.Value* = self.Gen.StmtGenDecl._load_raw_var_ptr(trans, nm.id)
if raw_nl_a is not None:
nl_ty_a: mlirlite.LLVMType* = GenApi.get_nonlocal_type(trans, nm.id)
if nl_ty_a is not None:
# object is the pointer: Struct/Array use the heap address directly (bitcast,
# no load); pointer types (e.g. Ptr(Point)) load the object pointer from the heap
match nl_ty_a:
case mlirlite.LLVMType.Struct(_, _, _):
obj_ptr = mlirlite.build_bitcast(builder, raw_nl_a, mlirlite.Ptr(pool, nl_ty_a))
case mlirlite.LLVMType.Array(_, _):
obj_ptr = mlirlite.build_bitcast(builder, raw_nl_a, mlirlite.Ptr(pool, nl_ty_a))
case _:
nl_val_ptr_a: mlirlite.Value* = mlirlite.build_bitcast(builder, raw_nl_a, mlirlite.Ptr(pool, nl_ty_a))
if nl_val_ptr_a is not None: obj_ptr = mlirlite.build_load(builder, nl_ty_a, nl_val_ptr_a)
else: obj_ptr = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id)
# non-Name path: translate the object value (which loads)
if obj_ptr is None:
# c.Deref(ptr).attr: the base is the pointer itself, not the loaded struct value
# (c.Deref(p).a means (*p).a, so GEP p directly)
cd_base_rd: mlirlite.Value* = self._get_cderef_attr_base(builder, pool, mod, at.value, trans)
if cd_base_rd is not None: obj_ptr = cd_base_rd
elif at.value.kind() == ast.ASTKind.Subscript:
# subscript-base attribute access (e.g. pts[0].x): list[T]/OOP __getitem__ classes use
# translate_value; plain struct-pointer arrays use get_subscript_ptr (GEP the element address).
if self._sub_base_uses_getitem(pool, mod, trans, at.value) != 0: obj_ptr = self.Gen.ExprGen.translate_value(builder, pool, mod, at.value, None, 0, trans)
else: obj_ptr = self.Gen.ExprGenName.get_subscript_ptr(builder, pool, mod, at.value, trans)
elif at.value.kind() == ast.ASTKind.Attribute:
# nested attribute base (e.g. r.origin.x): take the inner field address (recursive
# get_attribute_ptr, address semantics); pointer fields (X*) are loaded by _deref_if_ptr_ptr
obj_ptr = self.get_attribute_ptr(builder, pool, mod, at.value, trans)
if obj_ptr is None: obj_ptr = self.Gen.ExprGen.translate_value(builder, pool, mod, at.value, None, 0, trans)
else: obj_ptr = self.Gen.ExprGen.translate_value(builder, pool, mod, at.value, None, 0, trans)
if obj_ptr is None or obj_ptr.Ty is None: return None
# if obj_ptr is Ptr(Ptr(Struct)) (the alloca of an X* variable), load to deref and get Ptr(Struct)
obj_ptr = self._deref_if_ptr_ptr(builder, obj_ptr)
# rawptr[t.CVolatile | T] base (volatile pointee): loads of fields (p.field) through this pointer carry
# volatile. It is 1 only when the base resolves to a VarEntry (Name) / FieldEntry (Attribute), else 0.
vol_ba_rd: int = self.base_pointee_is_volatile(pool, trans, at.value)
# look up the struct type info; obj_ptr's type should be Ptr(Struct(...))
match obj_ptr.Ty:
case mlirlite.LLVMType.Ptr(struct_ty):
# ensure the cross-module struct definition is available in this module (for field GEP)
self.Gen.StructGen.ensure_struct_def_in_module(pool, mod, struct_ty)
# look up the field index and type
field_info: GenTypes.FieldEntry* = self.Gen.StructGen.lookup_field(struct_ty, at.attr)
# REnum variant field lookup: when lookup_field fails, try the REnum alias. REnum struct
# fields are named _p1/_p2/..., but source uses variant field names (e.g. ty.Pointee).
if field_info is None:
renum_se: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(struct_ty)
if renum_se is not None and renum_se.Name is not None:
is_rn: int = self.Gen.ClassGenEnum._is_renum_class_name(renum_se.Name)
if is_rn == 1:
alias_idx: int = self.Gen.ClassGenEnum._lookup_renum_field_alias(renum_se.Name, at.attr)
if alias_idx >= 1:
alias_fe: GenTypes.FieldEntry* = self.Gen.StructGen._get_field_entry(renum_se, alias_idx)
if alias_fe is not None and alias_fe.Ty is not None:
alias_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, struct_ty, alias_fe.Ty, obj_ptr, alias_idx)
if alias_ptr is not None:
# REnum variant field read by its declared type (matching match binding): the slot is i32/i64,
# so bitcast the slot address then load the declared type (no info loss) to avoid an int→ptr error.
decl_ty_rn: mlirlite.LLVMType* = self.Gen.ClassGenEnum._lookup_renum_field_alias_ty(renum_se.Name, at.attr)
if decl_ty_rn is not None:
decl_ptr_rn: mlirlite.LLVMType* = mlirlite.Ptr(pool, decl_ty_rn)
if decl_ptr_rn is not None:
cast_ptr_rn: mlirlite.Value* = mlirlite.build_bitcast(builder, alias_ptr, decl_ptr_rn)
if cast_ptr_rn is not None:
rn_alias_val: mlirlite.Value* = mlirlite.build_load(builder, decl_ty_rn, cast_ptr_rn)
# BigEndian REnum payload alias: un-swap the load (pairing the write side).
return self.apply_byte_order_bswap(pool, builder, rn_alias_val, alias_fe.ByteOrder)
rn_slot_val: mlirlite.Value* = mlirlite.build_load(builder, alias_fe.Ty, alias_ptr, vol_ba_rd)
# BigEndian REnum slot: un-swap the load (pairing the write side).
return self.apply_byte_order_bswap(pool, builder, rn_slot_val, alias_fe.ByteOrder)
# property getter fallback: when obj.attr is a @property/@x.getter method, emit a call
# (try before reporting a field-not-found error, to avoid a misleading message)
if field_info is None and at.value is not None and trans is not None:
prop_cls_fb: str = self.Gen.StructGen.get_class_name_by_type(pool, struct_ty)
prop_val: mlirlite.Value* = self.Gen.CallGenProp._try_property_getter_attr(pool, builder, mod, at, obj_ptr, trans, prop_cls_fb)
if prop_val is not None: return prop_val
# on-demand filter: when struct_ty is i8 (simplified union), look up by class name via
# AnnotClassName (base class name from a Name var annotation or an Attribute field annotation).
if field_info is None:
base_class: str = None
if at.value.kind() == ast.ASTKind.Name and trans is not None:
nm_fb: ast.Name* = ast.Name*(at.value)
if nm_fb.id is not None:
var_entry: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_fb.id)
if var_entry is not None: base_class = self.Gen.VarGen.var_annot_class_name(var_entry)
elif at.value.kind() == ast.ASTKind.Attribute:
# allow_module_global=1: the base may be "module name.global variable" (Mod.g, e.g.
# GenApi._g_gen); without it the receiver class cannot be resolved and lookup_field fails.
base_class = self._resolve_attr_field_class(pool, trans, at.value, 1)
elif at.value.kind() == ast.ASTKind.Call:
# pointer-cast base (`(ast.Call*(x)).IsVolatile`): `T*(x)` yields the union-simplified i8*,
# so extract the class name from the cast annotation (see _base_class_from_cast_base).
base_class = self._base_class_from_cast_base(pool, trans, at.value)
if base_class is not None:
cur_sha1: str = trans.ModuleSha1
field_info = self.Gen.StructGen.lookup_field_by_class(base_class, at.attr, cur_sha1, at, trans._from_imports)
# REnum variant field alias fallback: a variant field name (e.g. LLVMType.Pointee → the
# _p2 slot) is not a normal field; lookup_field_by_class misses it when struct_ty is i8.
if field_info is None and self.Gen.ClassGenEnum._is_renum_class_name(base_class) != 0:
rn_alias: int = self.Gen.ClassGenEnum._lookup_renum_field_alias(base_class, at.attr)
if rn_alias >= 1:
rn_se: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(base_class)
if rn_se is not None:
rn_fe: GenTypes.FieldEntry* = self.Gen.StructGen._get_field_entry(rn_se, rn_alias)
if rn_fe is not None and rn_fe.Ty is not None: field_info = rn_fe
# on-demand filter succeeded: bitcast obj_ptr to the AnnotClassName struct type;
# the original struct_ty may be i8 (X* simplified to Ptr(i8)), so GEP needs the real type.
if field_info is not None:
annot_se: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(base_class)
if annot_se is not None and annot_se.Ty is not None:
annot_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, annot_se.Ty)
casted_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, obj_ptr, annot_ptr_ty)
if casted_ptr is not None:
obj_ptr = casted_ptr
struct_ty = annot_se.Ty
# after the on-demand filter updates struct_ty, re-ensure the annotated
# struct definition is available (the original struct_ty may be i8)
self.Gen.StructGen.ensure_struct_def_in_module(pool, mod, annot_se.Ty)
if field_info is not None:
field_ty: mlirlite.LLVMType* = field_info.Ty
# t.CVolatile field: loads through the object carry the volatile qualifier
field_vol_rd: int = self.Gen.StructGen.field_is_volatile(field_info)
# merge "the field's pointee is volatile": when the base is rawptr[t.CVolatile|T],
# field loads through it must also be volatile (OR of both flags)
if vol_ba_rd != 0: field_vol_rd = 1
# union: bitcast obj_ptr to field_ty* then load
if self.Gen.StructGen.is_union_by_type(struct_ty) == 1:
field_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, field_ty)
casted: mlirlite.Value* = mlirlite.build_bitcast(builder, obj_ptr, field_ptr_ty)
if casted is not None:
union_val_rd: mlirlite.Value* = mlirlite.build_load(builder, field_ty, casted, field_vol_rd)
# BigEndian field: the slot holds the byte-swapped pattern, so un-swap the load.
return self.apply_byte_order_bswap(pool, builder, union_val_rd, field_info.ByteOrder)
return None
# normal struct: GEP + load
field_idx: int = field_info.Index
field_ptr: mlirlite.Value* = mlirlite.build_gep_struct(builder, struct_ty, field_ty, obj_ptr, field_idx)
if field_ptr is not None:
# [FIX 2026-08-31] read an REnum variant field by its declared type (matching the main
# alias fallback): bitcast the _pN slot address, then load by declared type (no info loss).
rn_rd_se: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(struct_ty)
if rn_rd_se is not None and rn_rd_se.Name is not None and self.Gen.ClassGenEnum._is_renum_class_name(rn_rd_se.Name) != 0:
decl_ty_rd: mlirlite.LLVMType* = self.Gen.ClassGenEnum._lookup_renum_field_alias_ty(rn_rd_se.Name, at.attr)
if decl_ty_rd is not None and self.is_array_type(decl_ty_rd) == 0:
decl_ptr_rd: mlirlite.LLVMType* = mlirlite.Ptr(pool, decl_ty_rd)
if decl_ptr_rd is not None:
cast_ptr_rd: mlirlite.Value* = mlirlite.build_bitcast(builder, field_ptr, decl_ptr_rd)
if cast_ptr_rd is not None:
rn_val_rd: mlirlite.Value* = mlirlite.build_load(builder, decl_ty_rd, cast_ptr_rd, field_vol_rd)
# BigEndian REnum payload slot: un-swap the load, pairing with the write side.
return self.apply_byte_order_bswap(pool, builder, rn_val_rd, field_info.ByteOrder)
# an array-typed field cannot be loaded as an SSA value; return the field pointer for
# later indexing: self.state[0] → GEP the state field → GEP the array element
if self.is_array_type(field_ty) != 0: return field_ptr
loaded_val: mlirlite.Value* = mlirlite.build_load(builder, field_ty, field_ptr, field_vol_rd)
# BigEndian field: the memory slot holds the byte-swapped pattern, so un-swap the
# loaded value; a no-op for non-16/32/64-bit ints (pointers/aggregates unaffected).
loaded_val = self.apply_byte_order_bswap(pool, builder, loaded_val, field_info.ByteOrder)
# a union-typed field (e.g. Token*) is compiled as i8*; when AnnotClassName points to a
# concrete struct type, bitcast the loaded pointer (value→pointer bitcast would be invalid).
fc_annot: str = self.Gen.StructGen.get_field_annot_class_name(field_info)
if loaded_val is not None and fc_annot is not None and self.is_ptr_type(field_ty) != 0:
annot_struct: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(fc_annot)
if annot_struct is not None and annot_struct.Ty is not None:
# ensure the target struct has a declaration here (its AnnotClassName may name an
# unimported class); Ptr(Ty) lets the opaque scan declare the struct name itself.
mlirlite.module_ensure_opaque_for_type(mod, pool, mlirlite.Ptr(pool, annot_struct.Ty))
annot_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, annot_struct.Ty)
return mlirlite.build_bitcast(builder, loaded_val, annot_ptr_ty)
return loaded_val
# field not found: may be Phase A deps not ready (definition-side module uncompiled, an
# empty vpi stub class with FieldCount=0, e.g. LARGE_INTEGER) → fall through to the real error
if trans is not None:
diag_annot: str = None
diag_var_nm: str = None
if at.value is not None and at.value.kind() == ast.ASTKind.Name:
dnm: ast.Name* = ast.Name*(at.value)
diag_var_nm = dnm.id
if dnm.id is not None:
dve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, dnm.id)
if dve is not None: diag_annot = self.Gen.VarGen.var_annot_class_name(dve)
diag_var_txt: str = "?"
if diag_var_nm is not None: diag_var_txt = diag_var_nm
diag_annot_txt: str = "None"
if diag_annot is not None: diag_annot_txt = diag_annot
# receiver chain path: `var=?` means the receiver is not a local variable, so spell out the
# full chain (e.g. "GenApi._g_gen.TypeGenCDef") to tell a global chain from a field chain.
diag_base_txt: str = "?"
if at.value is not None:
dbg_bp: str = self._attr_module_path_str(pool, at.value)
if dbg_bp is not None: diag_base_txt = dbg_bp
sha1_attr: str = "?"
if trans.ModuleSha1 is not None: sha1_attr = trans.ModuleSha1
self.Gen.TypeGenErr.fatal_error(at, f"attribute access failed: .{at.attr} (var={diag_var_txt} annot={diag_annot_txt} base={diag_base_txt} sha1={sha1_attr})", "TypeError")
return None
case _:
# fast-fail: attribute access on a non-pointer type (e.g. i32.x) aborts immediately via a
# Python traceback, rather than silently returning None (which loses the error site)
attr_nm_ap: str = "?"
if at is not None and at.attr is not None: attr_nm_ap = at.attr
sha1_ap: str = "?"
if trans is not None and trans.ModuleSha1 is not None: sha1_ap = trans.ModuleSha1
ln_ap: int = 0
if at is not None and at.lineno is not None: ln_ap = at.lineno
self.Gen.TypeGenErr.fatal_error(at, f"attribute access failed: .{attr_nm_ap} (base=non-pointer type, cannot do attribute access sha1={sha1_ap}:{ln_ap})", "TypeError")
# explicit return after fatal_error: the final match arm must be terminated, else falling off
# the end is misjudged by the function tail as fail-fast
return None
# get_attribute_ptr - get the field pointer for an attribute access (no value load, for assignment);
# for a Name obj, use the alloca pointer directly (no struct load).
def _resolve_attr_field_class(self, pool: memhub.MemBuddy*, trans: HT.Translator*, attr_node: ast.AST*, allow_module_global: int = 0) -> str:
"""Resolve the annotation class name of the final field in an attribute chain (a.b.c)."""
if attr_node is None or trans is None: return None
k: int = attr_node.kind()
if k != ast.ASTKind.Attribute: return None
at: ast.Attribute* = ast.Attribute*(attr_node)
if at is None or at.attr is None or at.value is None: return None
# recursively resolve the base's annotation class name (Name → var annotation; Attribute → field annotation)
parent_class: str = None
if at.value.kind() == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(at.value)
if nm.id is None: return None
var_entry: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)
if var_entry is None:
if allow_module_global != 0:
# the base itself is "module name.global variable" (Mod.g, e.g. GenApi._g_gen): its class name
# comes from the target module global's element type (else None, keeping the caller's semantics).
return self._resolve_module_global_class_name(pool, trans, nm.id, at.attr)
return None
parent_class = self.Gen.VarGen.var_annot_class_name(var_entry)
elif at.value.kind() == ast.ASTKind.Attribute: parent_class = self._resolve_attr_field_class(pool, trans, at.value, allow_module_global)
elif at.value.kind() == ast.ASTKind.Call:
# pointer-cast base (the recursive parent of `(T*(x)).f.g`): extract the class name from the
# cast annotation (see the _base_class_from_cast_base header)
parent_class = self._base_class_from_cast_base(pool, trans, at.value)
else: return None
if parent_class is None: return None
# look up the at.attr field in the parent class and return its annotation class name
cur_sha1: str = None
if trans is not None: cur_sha1 = trans.ModuleSha1
lfi_pc: str = None
if trans is not None: lfi_pc = trans._from_imports
fe: GenTypes.FieldEntry* = self.Gen.StructGen.lookup_field_by_class(parent_class, at.attr, cur_sha1, at, lfi_pc)
if fe is None: return None
return self.Gen.StructGen.get_field_annot_class_name(fe)
# _base_class_from_cast_base - bare class name of a pointer-cast base `T*(x)` (parsed as `(T*)(x)`, whose
# union-with-t.CPtr type is i8*), extracted from the BinOp annotation; bare name matches TPC's convention.
def _base_class_from_cast_base(self, pool: memhub.MemBuddy*, trans: HT.Translator*, node: ast.AST*) -> str:
"""Return the bare class name of a T*(x) cast base, or None for other forms."""
if node is None or trans is None: return None
if node.kind() != ast.ASTKind.Call: return None
cl_cb: ast.Call* = ast.Call*(node)
if cl_cb.func is None or cl_cb.func.kind() != ast.ASTKind.BinOp: return None
bo_cb: ast.BinOp* = ast.BinOp*(cl_cb.func)
if bo_cb.op != ast.OpKind.BitOr: return None
# when one side of the union is the t.CPtr marker (the nested union of `T*` / `T**`), take the
# other side; otherwise (a hand-written `X | Y`) take the left side, matching parser _make_ptr_type
tgt_cb: ast.AST* = bo_cb.left
if self.Gen.TypeGenResolve._is_cptr_marker(bo_cb.left, trans._imported_modules) != 0: tgt_cb = bo_cb.right
if tgt_cb is None: return None
k_cb: int = tgt_cb.kind()
if k_cb == ast.ASTKind.Attribute:
at_cb: ast.Attribute* = ast.Attribute*(tgt_cb)
return at_cb.attr
if k_cb == ast.ASTKind.Name:
nm_cb: ast.Name* = ast.Name*(tgt_cb)
return nm_cb.id
return None
# _resolve_module_global_class_name - struct class name of a "module name.global variable" Mod.g: taken
# from the target module global's element type (lookup_var_entry is None for such a base); else None.
def _resolve_module_global_class_name(self, pool: memhub.MemBuddy*, trans: HT.Translator*, mod_name: str, global_name: str) -> str:
"""Resolve the element type class name of a module-qualified global Mod.g, else None."""
if pool is None or trans is None or mod_name is None or global_name is None: return None
mod: mlirlite.LLVMModule* = trans.Module
if mod is None or trans._imported_modules is None: return None
# module path: resolve the import alias (same truncation as _resolve_module_attribute_global)
mod_path: str = mod_name
if trans._from_imports is not None:
orig_mw: str = self.Gen.ImportsGen.lookup_from_import(trans._from_imports, mod_name, 0)
if orig_mw is not None:
oml_mg: sizet = 0
while orig_mw[oml_mg] != ' ' and orig_mw[oml_mg] != '\0' and orig_mw[oml_mg] != ':': oml_mg += 1
if oml_mg > 0:
om_buf_mg: str = pool.alloc(oml_mg + 1)
if om_buf_mg is not None:
string.strncpy(om_buf_mg, orig_mw, oml_mg)
om_buf_mg[oml_mg] = '\0'
mod_path = om_buf_mg
if self.Gen.ImportsGen.is_module_imported(trans._imported_modules, mod_path) == 0: return None
# global location: SHA1-prefixed name (matching the definition-side mangling) → bare-name fallback (CExport)
gv: mlirlite.GlobalVariable* = self.find_global_in_module(mod, self.Gen.FuncGenBase._mangle_name_with_sha1(self._lookup_module_sha1(mod_path), global_name))
if gv is None: gv = self.find_global_in_module(mod, global_name)
if gv is None or gv.Ty is None: return None
if self.is_ptr_type(gv.Ty) == 0: return None
# the global's type is "pointer to element type" (@"<sha1>._g_gen" : %Generator*) ⇒ take the class
# name from its element type; it may still be a pointer (e.g. %Generator**) ⇒ unwrap level by level.
elem_ty: mlirlite.LLVMType* = gv.Ty.Pointee
level_mg: int = 0
while elem_ty is not None and level_mg < 3:
cls_mg: str = self.Gen.StructGen.get_class_name_by_type(pool, elem_ty)
if cls_mg is not None: return cls_mg
if self.is_ptr_type(elem_ty) == 0: return None
elem_ty = elem_ty.Pointee
level_mg += 1
return None
def attr_field_is_volatile(self, pool: memhub.MemBuddy*, trans: HT.Translator*, attr_node: ast.AST*) -> int:
"""Return 1 if the obj.field field is volatile, 0 otherwise."""
if attr_node is None or trans is None: return 0
if attr_node.kind() != ast.ASTKind.Attribute: return 0
at: ast.Attribute* = ast.Attribute*(attr_node)
if at is None or at.attr is None or at.value is None: return 0
parent_class: str = None
if at.value.kind() == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(at.value)
if nm.id is None: return 0
ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)
if ve is None: return 0
parent_class = self.Gen.VarGen.var_annot_class_name(ve)
elif at.value.kind() == ast.ASTKind.Attribute: parent_class = self._resolve_attr_field_class(pool, trans, at.value)
if parent_class is None: return 0
fe: GenTypes.FieldEntry* = self.Gen.StructGen.lookup_field_by_class(parent_class, at.attr, trans.ModuleSha1, at, trans._from_imports)
return self.Gen.StructGen.field_is_volatile(fe)
# pointee volatile (rawptr[t.CVolatile | T], i.e. C's volatile T*): resolves a base to a
# VarEntry/FieldEntry so that loads/stores through p[i] / p.field / *p carry the volatile qualifier.
def _resolve_attr_field_entry(self, pool: memhub.MemBuddy*, trans: HT.Translator*, attr_node: ast.AST*) -> GenTypes.FieldEntry*:
"""Return the FieldEntry for an obj.field access via a non-fatal read-only resolution."""
if attr_node is None or trans is None: return None
if attr_node.kind() != ast.ASTKind.Attribute: return None
at: ast.Attribute* = ast.Attribute*(attr_node)
if at is None or at.attr is None or at.value is None: return None
if at.value.kind() != ast.ASTKind.Name: return None
nm: ast.Name* = ast.Name*(at.value)
if nm.id is None: return None
ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)
if ve is None: return None
cls_nm: str = self.Gen.VarGen.var_annot_class_name(ve)
if cls_nm is None: return None
# take the bare class name (before the dot): only a bare-name struct lookup, avoiding fatal module-qualified parsing
bare: str = cls_nm
dot: str = self.Gen.StructGen._last_dot_outside_brackets(cls_nm)
if dot is not None: bare = dot + 1
if bare is None: return None
st: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(bare)
if st is None: return None
for fi in range(int(st.FieldCount)):
fe_u: GenTypes.FieldEntry* = self.Gen.StructGen._get_field_entry(st, fi)
if fe_u is not None and fe_u.Name is not None and string.strcmp(fe_u.Name, at.attr) == 0: return fe_u
return None
def attr_field_pointee_is_volatile(self, pool: memhub.MemBuddy*, trans: HT.Translator*, attr_node: ast.AST*) -> int:
"""Return 1 if the pointee of the obj.field field is volatile, 0 otherwise."""
fe: GenTypes.FieldEntry* = self._resolve_attr_field_entry(pool, trans, attr_node)
return self.Gen.StructGen.field_pointee_is_volatile(fe)
def attr_field_is_const(self, pool: memhub.MemBuddy*, trans: HT.Translator*, attr_node: ast.AST*) -> int:
"""Return 1 if the obj.field field itself is a const object, 0 otherwise."""
fe: GenTypes.FieldEntry* = self._resolve_attr_field_entry(pool, trans, attr_node)
return self.Gen.StructGen.field_is_const(fe)
# attr_field_byte_order - the field's byte order (0=native / 1=BigEndian / 2=LittleEndian) for the
# write path, where get_attribute_ptr resolves the pointer but not the FieldEntry. Mirrors
# attr_field_is_volatile/attr_field_is_const (same _resolve_attr_field_entry resolution: only a
# Name base resolves; a nested/global receiver yields 0 -> no swap, never a wrong swap).
def attr_field_byte_order(self, pool: memhub.MemBuddy*, trans: HT.Translator*, attr_node: ast.AST*) -> int:
"""Return the obj.field field's byte order (0 = native/unknown)."""
fe: GenTypes.FieldEntry* = self._resolve_attr_field_entry(pool, trans, attr_node)
if fe is None: return 0
return fe.ByteOrder
def attr_field_decl_src(self, pool: memhub.MemBuddy*, trans: HT.Translator*, attr_node: ast.AST*) -> str:
"""Return the field annotation source name for const diagnostics, or None."""
fe: GenTypes.FieldEntry* = self._resolve_attr_field_entry(pool, trans, attr_node)
return self.Gen.StructGen.get_field_annot_class_name(fe)
def base_pointee_is_volatile(self, pool: memhub.MemBuddy*, trans: HT.Translator*, base_node: ast.AST*) -> int:
"""Return 1 if the pointer base's pointee is volatile, 0 otherwise."""
if base_node is None or trans is None: return 0
if base_node.kind() == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(base_node)
if nm.id is None: return 0
ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)
return self.Gen.VarGen.var_pointee_is_volatile(ve)
if base_node.kind() == ast.ASTKind.Attribute: return self.attr_field_pointee_is_volatile(pool, trans, base_node)
return 0
def get_attribute_ptr(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Return the field pointer for an attribute access (used for assignment lhs)."""
if node is None or builder is None: return None
at: ast.Attribute* = ast.Attribute*(node)
if at is None or at.value is None or at.attr is None: return None
# for a Name obj, look up the alloca directly (no struct load); nonlocal variables (hoisted
# into the function) read their shared heap value from env, avoiding a cross-function alloca ref.
obj_ptr: mlirlite.Value* = None
if at.value.kind() == ast.ASTKind.Name and trans is not None:
nm: ast.Name* = ast.Name*(at.value)
if nm.id is not None:
if GenApi.is_nonlocal_name(trans, nm.id) != 0:
raw_nl_a: mlirlite.Value* = self.Gen.StmtGenDecl._load_raw_var_ptr(trans, nm.id)
if raw_nl_a is not None:
nl_ty_a: mlirlite.LLVMType* = GenApi.get_nonlocal_type(trans, nm.id)
if nl_ty_a is not None:
# object is the pointer: Struct/Array use the heap address directly (bitcast,
# no load); pointer types (e.g. Ptr(Point)) load the object pointer from the heap
match nl_ty_a:
case mlirlite.LLVMType.Struct(_, _, _):
obj_ptr = mlirlite.build_bitcast(builder, raw_nl_a, mlirlite.Ptr(pool, nl_ty_a))
case mlirlite.LLVMType.Array(_, _):
obj_ptr = mlirlite.build_bitcast(builder, raw_nl_a, mlirlite.Ptr(pool, nl_ty_a))
case _:
nl_val_ptr_a: mlirlite.Value* = mlirlite.build_bitcast(builder, raw_nl_a, mlirlite.Ptr(pool, nl_ty_a))
if nl_val_ptr_a is not None: obj_ptr = mlirlite.build_load(builder, nl_ty_a, nl_val_ptr_a)
else: obj_ptr = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id)
# module attribute access (write path): sys._mbuddy = mb / os._win32._mbuddy = v → store to the module
# global. When obj is an imported module name (Name or nested Attribute path), find/create the global.
if obj_ptr is None and trans is not None:
mod_path_c: str = self._attr_module_path_str(pool, at.value)
if mod_path_c is not None:
# a CDefine constant is not writable: report FATAL instead of creating an undefined global
# (only a single-layer Name module can be precisely identified as a CDefine)
if at.value.kind() == ast.ASTKind.Name:
nm_wma: ast.Name* = ast.Name*(at.value)
if nm_wma.id is not None:
if self.Gen.ExprGenCdef._is_cdefine_name_pattern(at.attr) != 0:
err_buf_wma: str = pool.alloc(256)
if err_buf_wma is not None:
viperlib.snprintf(err_buf_wma, 256, "[CD] FATAL: Cannot assign to CDefine constant '%s.%s'\n", nm_wma.id, at.attr)
VLogger.error(err_buf_wma, "CD")
return None
# use i8* as the type hint (module-level variables usually hold pointers)
i8_ptr_w: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool))
wma_ref: mlirlite.Value* = self._resolve_module_attribute_global(pool, mod, trans, at, i8_ptr_w)
if wma_ref is not None: return wma_ref
# non-Name path: translate the object value (which loads)
if obj_ptr is None:
# c.Deref(ptr).attr write: the base is the pointer itself, not the loaded struct value
# (c.Deref(p).a = val means (*p).a = val, so GEP p directly)
cd_base_wr: mlirlite.Value* = self._get_cderef_attr_base(builder, pool, mod, at.value, trans)
if cd_base_wr is not None: obj_ptr = cd_base_wr
elif at.value.kind() == ast.ASTKind.Subscript:
# subscript-base attribute write (e.g. pts[0].x = val): list[T]/OOP __getitem__ classes use
# translate_value; plain struct-pointer arrays use get_subscript_ptr (GEP the element address).
if self._sub_base_uses_getitem(pool, mod, trans, at.value) != 0: obj_ptr = self.Gen.ExprGen.translate_value(builder, pool, mod, at.value, None, 0, trans)
else: obj_ptr = self.Gen.ExprGenName.get_subscript_ptr(builder, pool, mod, at.value, trans)
elif at.value.kind() == ast.ASTKind.Attribute:
# nested attribute base write (e.g. r.origin.x = val): take the inner field address
# (recursive get_attribute_ptr, address semantics); pointer fields are loaded by _deref_if_ptr_ptr
obj_ptr = self.get_attribute_ptr(builder, pool, mod, at.value, trans)
if obj_ptr is None: obj_ptr = self.Gen.ExprGen.translate_value(builder, pool, mod, at.value, None, 0, trans)
else: obj_ptr = self.Gen.ExprGen.translate_value(builder, pool, mod, at.value, None, 0, trans)
if obj_ptr is None or obj_ptr.Ty is None: return None
# if obj_ptr is Ptr(Ptr(Struct)) (the alloca of an X* variable), load to deref and get Ptr(Struct)
obj_ptr = self._deref_if_ptr_ptr(builder, obj_ptr)
match obj_ptr.Ty:
case mlirlite.LLVMType.Ptr(struct_ty):
# ensure the cross-module struct definition is available in this module (for field GEP);
# as in translate_attribute, this avoids a GEP error on opaque types
self.Gen.StructGen.ensure_struct_def_in_module(pool, mod, struct_ty)
field_info: GenTypes.FieldEntry* = self.Gen.StructGen.lookup_field(struct_ty, at.attr)
# on-demand filter: when struct_ty is i8 (simplified union), resolve the base class name (Name →
# var annotation; Attribute → recursive field annotation, e.g. lx.tokens_tail.next: Lexer → Token).
if field_info is None:
base_class: str = None
if at.value.kind() == ast.ASTKind.Name and trans is not None:
nm_fb: ast.Name* = ast.Name*(at.value)
if nm_fb.id is not None:
var_entry: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_fb.id)
if var_entry is not None: base_class = self.Gen.VarGen.var_annot_class_name(var_entry)
elif at.value.kind() == ast.ASTKind.Attribute:
# allow_module_global=1, as in translate_attribute (the base may be "module name.global
# variable", e.g. GenApi._g_gen.TypeGenCDef)
base_class = self._resolve_attr_field_class(pool, trans, at.value, 1)
elif at.value.kind() == ast.ASTKind.Call:
# pointer-cast base (`(ast.Call*(vinner)).IsVolatile = 1`): `T*(x)` yields the union-simplified
# i8*, so extract the class name from the cast annotation (see _base_class_from_cast_base).
base_class = self._base_class_from_cast_base(pool, trans, at.value)
if base_class is not None:
cur_sha1: str = None
lfi_gp: str = None
if trans is not None:
cur_sha1 = trans.ModuleSha1
lfi_gp = trans._from_imports
field_info = self.Gen.StructGen.lookup_field_by_class(base_class, at.attr, cur_sha1, at, lfi_gp)
# REnum variant field alias fallback (as in translate_attribute): the assignment path's
# on-demand filter also needs it, since the main alias search is skipped when struct_ty is i8.
if field_info is None and self.Gen.ClassGenEnum._is_renum_class_name(base_class) != 0:
rn_alias_g: int = self.Gen.ClassGenEnum._lookup_renum_field_alias(base_class, at.attr)
if rn_alias_g >= 1:
rn_se_g: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(base_class)
if rn_se_g is not None:
rn_fe_g: GenTypes.FieldEntry* = self.Gen.StructGen._get_field_entry(rn_se_g, rn_alias_g)
if rn_fe_g is not None and rn_fe_g.Ty is not None: field_info = rn_fe_g
# on-demand filter succeeded: bitcast obj_ptr to the AnnotClassName struct type;
# the original struct_ty may be i8 (X* simplified to Ptr(i8)), so GEP needs the real type.
if field_info is not None:
annot_se: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(base_class)
if annot_se is not None and annot_se.Ty is not None:
annot_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, annot_se.Ty)
casted_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, obj_ptr, annot_ptr_ty)
if casted_ptr is not None:
obj_ptr = casted_ptr
struct_ty = annot_se.Ty
# after the on-demand filter updates struct_ty, re-ensure the annotation struct
# definition is available in this module
self.Gen.StructGen.ensure_struct_def_in_module(pool, mod, annot_se.Ty)
if field_info is not None:
# union: bitcast obj_ptr to field_ty* (field pointer for the store)
if self.Gen.StructGen.is_union_by_type(struct_ty) == 1:
field_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, field_info.Ty)
return mlirlite.build_bitcast(builder, obj_ptr, field_ptr_ty)
# normal struct: GEP to the field pointer
return mlirlite.build_gep_struct(builder, struct_ty, field_info.Ty, obj_ptr, field_info.Index)
# field not found: may be Phase A deps not ready (definition-side module uncompiled, an
# empty vpi stub class with FieldCount=0, e.g. SECURITY_ATTRIBUTES) → fall through to the real error
if trans is not None:
diag_annot_g: str = None
diag_var_nm_g: str = None
if at.value is not None and at.value.kind() == ast.ASTKind.Name:
dnm_g: ast.Name* = ast.Name*(at.value)
diag_var_nm_g = dnm_g.id
if dnm_g.id is not None:
dve_g: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, dnm_g.id)
if dve_g is not None: diag_annot_g = self.Gen.VarGen.var_annot_class_name(dve_g)
diag_var_txt_g: str = "?"
if diag_var_nm_g is not None: diag_var_txt_g = diag_var_nm_g
diag_annot_txt_g: str = "None"
if diag_annot_g is not None: diag_annot_txt_g = diag_annot_g
# receiver chain path (the read-side completion in translate_attribute): `var=?` means the
# receiver is not a local variable, so spell out the full form (Mod.g.F[.F]).
diag_base_txt_g: str = "?"
if at.value is not None:
dbg_bp_g: str = self._attr_module_path_str(pool, at.value)
if dbg_bp_g is not None: diag_base_txt_g = dbg_bp_g
sha1_attr_g: str = "?"
if trans.ModuleSha1 is not None: sha1_attr_g = trans.ModuleSha1
self.Gen.TypeGenErr.fatal_error(at, f"attribute write failed: .{at.attr} (var={diag_var_txt_g} annot={diag_annot_txt_g} base={diag_base_txt_g} sha1={sha1_attr_g})", "TypeError")
return None
case _:
# fast-fail: attribute write on a non-pointer type (e.g. i32.x = ...) aborts immediately via a
# Python traceback, rather than silently returning None (which loses the error site)
attr_nm_gp: str = "?"
if at is not None and at.attr is not None: attr_nm_gp = at.attr
sha1_gp: str = "?"
if trans is not None and trans.ModuleSha1 is not None: sha1_gp = trans.ModuleSha1
ln_gp: int = 0
if at is not None and at.lineno is not None: ln_gp = at.lineno
self.Gen.TypeGenErr.fatal_error(at, f"attribute write failed: .{attr_nm_gp} (base=non-pointer type, cannot do attribute write sha1={sha1_gp}:{ln_gp})", "TypeError")
# explicit return after fatal_error: the final match arm must be terminated
return None