from stdint import * import ast import mlirlite import memhub import string import viperlib import lib.core.Gen.Translator as HT import lib.core.Gen.GenApi as GenApi import lib.core.Gen.GenTypes as GenTypes from . import CallGenBase # CallGenCBuiltin - c.*/asm/llvmir builtins # Constants (kept consistent with TPC-side HandlesExprCall) define _CEXPORT_MAX = 256 define ASM_MAX_OPERANDS = 16 # Clobber (~{...}) capacity is decoupled from the operand limit: vipersimd x8 # needs 21 entries, so ASM_MAX_OPERANDS=16 silently truncated it to 16. define ASM_MAX_CLOBBERS = 32 # ASM_TEXT_BUF_SIZE holds the whole inline-asm template (a kernel switch statement # template is already 2245 bytes). Overflow is fail-fast in _asm_append_cstr, never # silently truncated: a cut-in-half template used to surface as a bogus llc parse # error (e.g. a dangling `mov` with its operands chopped off). define ASM_TEXT_BUF_SIZE = 8192 # Constraint buffer base size; the real capacity scales with operand/clobber count. define ASM_CONST_BUF_SIZE = 512 define ASM_ARGS_BUF_SIZE = 512 define ASM_LINE_BUF_SIZE = 4096 define LLVMIR_MAX_OPS = 16 # Raw LLVM IR templates (c.LLVMIR) are multi-instruction text, not one-liners. define LLVMIR_TEMPLATE_BUF = 4096 # CallGenCBuiltin - c.*/asm/llvmir builtins novirtual class CallGenCBuiltin(CallGenBase.CallGenBase): """Translate c.* builtins, inline asm and raw LLVM IR.""" def translate_c_deref(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator* = None) -> mlirlite.Value*: """Load *ptr; the read width is decided solely by the pointee type.""" if cl is None: return None cargs: list[ast.AST*]* = cl.args if cargs is None or len(cargs) < 1: return None ptr_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cargs.get(0), None, 0, trans) if ptr_val is None: return None # rawptr[t.CVolatile | T] arg: the deref load in c.Deref(p) is volatile. # 1 only when the arg resolves to VarEntry/FieldEntry, otherwise 0. vol_deref: int = self.Gen.ExprGenAttr.base_pointee_is_volatile(pool, trans, cargs.get(0)) # Read width is decided solely by the pointer type: load one pointee # element. i8 pointee always reads 1 byte; wider reads must cast first. if self.Gen.ExprGenBase.is_ptr_type(ptr_val.Ty) != 0: pointee: mlirlite.LLVMType* = ptr_val.Ty.Pointee if pointee is not None: return mlirlite.build_load(builder, pointee, ptr_val, vol_deref) # Non-pointer arg means degraded type inference: fail-fast, never read as i8. self.Gen.TypeGenErr.fatal_error(cl, "c.Deref argument is not a pointer type (type inference degraded)", "TypeError") return None def translate_c_derefas(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator* = None) -> mlirlite.Value*: """Store val to ptr after a bitcast; the single-arg form reads *ptr.""" if cl is None: return None cargs: list[ast.AST*]* = cl.args if cargs is None or len(cargs) < 1: return None # Single-arg form is a deref read: previously len<2 returned None and # fell through to an undefined call @DerefAs, so reuse translate_c_deref. if len(cargs) < 2: return self.translate_c_deref(pool, builder, mod, cl, trans) ptr_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cargs.get(0), None, 0, trans) val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cargs.get(1), None, 0, trans) if ptr_val is None or val is None: return None # When both sides point to the same struct type, copy the struct value # instead of the pointer, or a REnum variant tag would stay 0x0. if self.Gen.ExprGenBase.is_ptr_type(ptr_val.Ty) != 0 and self.Gen.ExprGenBase.is_ptr_type(val.Ty) != 0: ptr_pointee: mlirlite.LLVMType* = ptr_val.Ty.Pointee val_pointee: mlirlite.LLVMType* = val.Ty.Pointee if ptr_pointee is not None and val_pointee is not None: # ptr_pointee is a registered struct type (checked via find_struct_by_type) if self.Gen.StructGen.find_struct_by_type(ptr_pointee) is not None: if ptr_pointee is val_pointee: # Same struct type: load the value then store it (struct copy) loaded: mlirlite.Value* = mlirlite.build_load(builder, val_pointee, val) if loaded is not None: # bitcast ptr to a struct pointer, then store struct_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, ptr_pointee) casted_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, ptr_val, struct_ptr_ty) if casted_ptr is not None: mlirlite.build_store(builder, loaded, casted_ptr) return val # bitcast ptr to val.Ty* and store val_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, val.Ty) casted_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, ptr_val, val_ptr_ty) if casted_ptr is None: return None mlirlite.build_store(builder, val, casted_ptr) return val def translate_c_set(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator* = None) -> mlirlite.Value*: """Assign b to the lvalue a.""" if cl is None: return None cargs: list[ast.AST*]* = cl.args if cargs is None or len(cargs) < 2: return None a_node: ast.AST* = cargs.get(0) b_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cargs.get(1), None, 0, trans) if b_val is None: return None # Form 0: c.Set(*ptr, b) -> *ptr = b. In an argument list a leading '*' is # parsed as Starred (unpacking), so the new-syntax deref lvalue arrives as # Starred(value=ptr) (UnaryOp/Deref also accepted). The old c.Deref(ptr) # spelling is Form 1; without either the store is silently dropped. ptr_expr: ast.AST* = None if a_node is not None and a_node.kind() == ast.ASTKind.Starred: st_a: ast.Starred* = ast.Starred*(a_node) if st_a.value is not None: ptr_expr = st_a.value elif a_node is not None and a_node.kind() == ast.ASTKind.UnaryOp: uo_a: ast.UnaryOp* = ast.UnaryOp*(a_node) if uo_a.op == ast.OpKind.Deref and uo_a.operand is not None: ptr_expr = uo_a.operand if ptr_expr is not None: ptr_a: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, ptr_expr, None, 0, trans) if ptr_a is None or ptr_a.Ty is None or self.Gen.ExprGenBase.is_ptr_type(ptr_a.Ty) == 0: self.Gen.TypeGenErr.fatal_error(a_node, "c.Set dereference target must be a pointer type (type inference degraded)", "TypeError") pointee_a: mlirlite.LLVMType* = ptr_a.Ty.Pointee if pointee_a is None: self.Gen.TypeGenErr.fatal_error(a_node, "c.Set dereference target pointee type missing", "TypeError") sv_a: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, b_val, pointee_a, a_node, trans) if sv_a is not None: mlirlite.build_store(builder, sv_a, ptr_a) return b_val # Form 1: c.Set(c.Deref(ptr), b) -> *ptr = b if a_node is not None and a_node.kind() == ast.ASTKind.Call: d_call: ast.Call* = ast.Call*(a_node) if d_call.func is not None and d_call.func.kind() == ast.ASTKind.Attribute: d_at: ast.Attribute* = ast.Attribute*(d_call.func) if d_at.attr is not None and string.strcmp(d_at.attr, "Deref") == 0: d_args: list[ast.AST*]* = d_call.args if d_args is not None and len(d_args) >= 1: ptr_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, d_args.get(0), None, 0, trans) if ptr_val is not None: # Store width follows the lvalue pointer's element type: # coerce v to T first. Untyped (i8) pointers keep old behavior. store_ptr: mlirlite.Value* = ptr_val store_val: mlirlite.Value* = b_val typed_elem: int = 0 elem_ty: mlirlite.LLVMType* = None if ptr_val.Ty is not None: elem_ty = ptr_val.Ty.Pointee if elem_ty is not None: eb: int = self.Gen.ExprGenBase.get_llvm_type_bits(elem_ty) if eb != 0 and eb != 8: typed_elem = 1 elif self.Gen.ExprGenBase.get_llvm_float_bits(elem_ty) != 0: typed_elem = 1 elif self.Gen.ExprGenBase.is_ptr_type(elem_ty) != 0: typed_elem = 1 elif self.Gen.ExprGenBase.is_array_type(elem_ty) != 0: typed_elem = 1 if typed_elem != 0: store_val = self.Gen.ExprGenBase.coerce_to_type(builder, b_val, elem_ty, a_node) else: val_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, b_val.Ty) casted_ptr: mlirlite.Value* = mlirlite.build_bitcast(builder, ptr_val, val_ptr_ty) if casted_ptr is not None: store_ptr = casted_ptr if store_ptr is not None and store_val is not None: mlirlite.build_store(builder, store_val, store_ptr) return b_val # Other Call forms cannot be an lvalue return b_val # Form 2: assign to a Name variable if a_node is not None and a_node.kind() == ast.ASTKind.Name and trans is not None: nm_set: ast.Name* = ast.Name*(a_node) if nm_set.id is not None: if GenApi.is_global_name(trans, nm_set.id) != 0: mod_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm_set.id) if mod_alloca is not None: tg_ty: mlirlite.LLVMType* = None if mod_alloca.Ty is not None: tg_ty = mod_alloca.Ty.Pointee sv: mlirlite.Value* = b_val if tg_ty is not None: sv = self.Gen.ExprGenBase.coerce_to_type(builder, b_val, tg_ty, a_node) mlirlite.build_store(builder, sv, mod_alloca) return b_val if GenApi.is_nonlocal_name(trans, nm_set.id) != 0: nl_ptr: mlirlite.Value* = self.Gen.StmtGenDecl.get_nonlocal_var_ptr(trans, nm_set.id) if nl_ptr is not None: nl_ty: mlirlite.LLVMType* = None if nl_ptr.Ty is not None: nl_ty = nl_ptr.Ty.Pointee sv2: mlirlite.Value* = b_val if nl_ty is not None: sv2 = self.Gen.ExprGenBase.coerce_to_type(builder, b_val, nl_ty, a_node) mlirlite.build_store(builder, sv2, nl_ptr) return b_val alloca: mlirlite.Value* = self.Gen.VarGen.get_or_create_sym(trans.SymTab, pool, builder, nm_set.id, b_val.Ty) if alloca is not None: ac_ty: mlirlite.LLVMType* = None if alloca.Ty is not None: ac_ty = alloca.Ty.Pointee sv3: mlirlite.Value* = b_val if ac_ty is not None: sv3 = self.Gen.ExprGenBase.coerce_to_type(builder, b_val, ac_ty, a_node) mlirlite.build_store(builder, sv3, alloca) return b_val return b_val # Form 3: assign to an Attribute field if a_node is not None and a_node.kind() == ast.ASTKind.Attribute: field_ptr: mlirlite.Value* = self.Gen.ExprGenAttr.get_attribute_ptr(builder, pool, mod, a_node, trans) if field_ptr is not None: fd_ty: mlirlite.LLVMType* = None if field_ptr.Ty is not None: fd_ty = field_ptr.Ty.Pointee sv4: mlirlite.Value* = b_val if fd_ty is not None: sv4 = self.Gen.ExprGenBase.coerce_to_type(builder, b_val, fd_ty, a_node) mlirlite.build_store(builder, sv4, field_ptr) return b_val return b_val def translate_c_addr(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator* = None) -> mlirlite.Value*: """Return the variable's alloca pointer without loading it.""" if cl is None: return None cargs: list[ast.AST*]* = cl.args if cargs is None or len(cargs) < 1: return None arg0: ast.AST* = cargs.get(0) if arg0 is None: return None # Name form: c.Addr(var) returns the variable's alloca (no load) if arg0.kind() == ast.ASTKind.Name: nm: ast.Name* = ast.Name*(arg0) if nm.id is None: return None if trans is not None: # self/cls object refs: c.Addr(self) returns the object pointer # itself (Ptr(Struct)), not the address of the pointer variable. if string.strcmp(nm.id, "self") == 0 or string.strcmp(nm.id, "cls") == 0: self_entry: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) if self_entry is not None and self_entry.Alloca is not None and self_entry.Alloca.Ty is not None: match self_entry.Alloca.Ty: case mlirlite.LLVMType.Ptr(inner1): match inner1: case mlirlite.LLVMType.Ptr(inner2): obj_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, inner2) return mlirlite.build_load(builder, obj_ty, self_entry.Alloca) case _: pass case _: pass # nonlocal vars: read the object pointer from env (load_nonlocal_var); # lookup_var must not hit the parent frame's alloca across functions. if trans is not None: if GenApi.is_nonlocal_name(trans, nm.id) != 0: nl_addr: mlirlite.Value* = self.Gen.StmtGenDecl.load_nonlocal_var(trans, nm.id) if nl_addr is not None: return nl_addr return_var: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) # REPL session var addressing: c.Addr(x) yields the ctx slot address. # Runs after local lookup and before module globals/functions. if return_var is None and trans is not None and trans.ReplCtx is not None: sess_addr: mlirlite.Value* = self.Gen.VarGen.repl_try_addr(trans, nm.id, arg0) if sess_addr is not None: return sess_addr if return_var is None and mod is not None: # Module global c.Addr(g) returns the global pointer; wrap via make_global_ref into a quoted Ptr(Ty) so ptrtoint renders correctly. gv_ref_nm: mlirlite.GlobalVariable* = self.Gen.ExprGenAttr.find_global_in_module(mod, nm.id) if gv_ref_nm is not None and gv_ref_nm.Name is not None and gv_ref_nm.Ty is not None: return_var = self.Gen.ExprGenAttr.make_global_ref(pool, gv_ref_nm.Name, gv_ref_nm.Ty) # Module function address: c.Addr(fn) yields the symbol itself. # Previously it returned None and fell through to an undefined @Addr. if return_var is None and mod is not None: f_addr_ref: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, nm.id) if f_addr_ref is not None: f_addr_name: str = mlirlite.function_get_name(f_addr_ref) if f_addr_name is not None: # Param chains and ParamNode chains are unrelated classes: # rebuild via _FuncTyFromFunction. fn_ty_ca: mlirlite.LLVMType* = self.Gen.CallGenBase._FuncTyFromFunction(pool, f_addr_ref) # Quote the function reference name when needed (sha1-prefixed # names start with a digit). fnlen_ca: sizet = string.strlen(f_addr_name) needs_q_ca: int = self.Gen.ClassGenVTable._vtable_name_needs_quote(f_addr_name) faddr_buf_ca: str = pool.alloc(fnlen_ca + 4) if faddr_buf_ca is not None and fn_ty_ca is not None: if needs_q_ca != 0: viperlib.snprintf(faddr_buf_ca, fnlen_ca + 4, "@\"%s\"", f_addr_name) else: viperlib.snprintf(faddr_buf_ca, fnlen_ca + 4, "@%s", f_addr_name) return_var = mlirlite.SSAValue(pool, mlirlite.Ptr(pool, fn_ty_ca), faddr_buf_ca) # Closure var: c.Addr(name) returns the closure object pointer (one load); # the closure alloca is i8** holding {fn_ptr, env_ptr}. if return_var is not None and return_var.Ty is not None: if self.Gen.CallGenLeafKit.is_closure_name(nm.id) != 0: i8_ty_cb: mlirlite.LLVMType* = mlirlite.Int8(pool) i8_ptr_ty_cb: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_cb) return mlirlite.build_load(builder, i8_ptr_ty_cb, return_var) if return_var is not None and return_var.Ty is not None: # Object-pointer variable (alloca is Ptr(Ptr(Struct))): c.Addr(v) # returns the object pointer itself (one load), like self/cls. match return_var.Ty: case mlirlite.LLVMType.Ptr(addr_inner1): match addr_inner1: case mlirlite.LLVMType.Ptr(addr_inner2): match addr_inner2: case mlirlite.LLVMType.Struct(_, _, _): addr_obj_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, addr_inner2) return mlirlite.build_load(builder, addr_obj_ty, return_var) case _: pass case _: pass case _: pass return return_var # Attribute form: c.Addr(self.field) takes the field address (GEP, no load), # used by lock ops like atom.__atomic_test_and_set(c.Addr(self.lock_val)). if arg0.kind() == ast.ASTKind.Attribute: if trans is None: return None at_addr: ast.Attribute* = ast.Attribute*(arg0) if at_addr is None or at_addr.attr is None or at_addr.value is None: return None # Cross-module function address c.Addr(serial.puts) yields the symbol; do not fall back to a global or it collides with the injected declare. if at_addr.value.kind() == ast.ASTKind.Name and trans is not None and trans._imported_modules is not None: mod_nm2: ast.Name* = ast.Name*(at_addr.value) if mod_nm2.id is not None: if self.Gen.ImportsGen.is_module_imported(trans._imported_modules, mod_nm2.id) != 0: f_ref2: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, at_addr.attr) if f_ref2 is not None: fname2: str = mlirlite.function_get_name(f_ref2) if fname2 is not None: # Build an SSAValue with @ name (Ptr(FuncTy)): returning a Function node formats as void; rebuild the type from RetTy+Params (Params is a GSList, use function_get_param_head). # Same: rebuild the ParamNode chain from the Function's Param chain. fn_ty2: mlirlite.LLVMType* = self.Gen.CallGenBase._FuncTyFromFunction(pool, f_ref2) # Quote the function reference name when needed # (sha1-prefixed names start with a digit). fnlen2: sizet = string.strlen(fname2) needs_q2: int = self.Gen.ClassGenVTable._vtable_name_needs_quote(fname2) faddr_buf2: str = pool.alloc(fnlen2 + 4) if faddr_buf2 is not None and fn_ty2 is not None: if needs_q2 != 0: viperlib.snprintf(faddr_buf2, fnlen2 + 4, "@\"%s\"", fname2) else: viperlib.snprintf(faddr_buf2, fnlen2 + 4, "@%s", fname2) return mlirlite.SSAValue(pool, mlirlite.Ptr(pool, fn_ty2), faddr_buf2) else: # Module-qualified global c.Addr(gdt.gp) yields the symbol; wrap via make_global_ref into a quoted Ptr(Ty) (a raw GlobalVariable renders "ptrtoint void"). g_ref2: mlirlite.GlobalVariable* = self.Gen.ExprGenAttr.find_global_in_module(mod, at_addr.attr) if g_ref2 is not None and g_ref2.Name is not None and g_ref2.Ty is not None: return self.Gen.ExprGenAttr.make_global_ref(pool, g_ref2.Name, g_ref2.Ty) # Resolve the base (Name var / Subscript array element) to an object pointer obj_ptr: mlirlite.Value* = None struct_ty: mlirlite.LLVMType* = None if at_addr.value.kind() == ast.ASTKind.Subscript: # c.Addr(s.threads[0].rsp): base is an array element, so GEP to the # element first, then to the field; Name-only base used to fail. sub_base: ast.Subscript* = ast.Subscript*(at_addr.value) if sub_base.value is None or sub_base.slice is None: return None bval: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, sub_base.value, None, 0, trans) bidx: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, sub_base.slice, None, 0, trans) if bval is None or bidx is None or bval.Ty is None: return None bpointee: mlirlite.LLVMType* = None if self.Gen.ExprGenBase.is_ptr_type(bval.Ty) != 0: bpointee = bval.Ty.Pointee if bpointee is not None: match bpointee: case mlirlite.LLVMType.Array(bet, _): obj_ptr = mlirlite.build_gep_array(builder, bpointee, bet, bval, bidx) struct_ty = bet case _: return None else: return None elif at_addr.value.kind() == ast.ASTKind.Name: self_nm: ast.Name* = ast.Name*(at_addr.value) if self_nm.id is None: return None self_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, self_nm.id) if self_alloca is None or self_alloca.Ty is None: return None # Extract the struct type from the self type (Ptr(Struct) or Ptr(Ptr(Struct))) self_is_ptr_ptr: int = 0 match self_alloca.Ty: case mlirlite.LLVMType.Ptr(inner1): match inner1: case mlirlite.LLVMType.Ptr(inner2): struct_ty = inner2 self_is_ptr_ptr = 1 case _: struct_ty = inner1 case _: return None # When self_alloca is Ptr(Ptr(Struct)), load it into a Ptr(Struct) object pointer obj_ptr = self_alloca if self_is_ptr_ptr == 1: self_obj_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, struct_ty) obj_ptr = mlirlite.build_load(builder, self_obj_ty, self_alloca) else: return None if obj_ptr is None or struct_ty is None: return None # Look up the field fe_addr: GenTypes.FieldEntry* = self.Gen.StructGen.lookup_field(struct_ty, at_addr.attr) if fe_addr is None: return None # GEP to the field address return mlirlite.build_gep_struct(builder, struct_ty, fe_addr.Ty, obj_ptr, fe_addr.Index) # Subscript form: c.Addr(arr[i]) yields the element address (GEP, no load), # via get_subscript_ptr which handles nested indices and ptr/str* forms. if arg0.kind() == ast.ASTKind.Subscript: if trans is None: return None elem_ptr_ca: mlirlite.Value* = self.Gen.ExprGenName.get_subscript_ptr(builder, pool, mod, arg0, trans) if elem_ptr_ca is not None: return elem_ptr_ca # Fallback: single-level array base GEP (when get_subscript_ptr misses) sub_addr: ast.Subscript* = ast.Subscript*(arg0) if sub_addr is None or sub_addr.value is None or sub_addr.slice is None: return None arr_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, sub_addr.value, None, 0, trans) if arr_val is None or arr_val.Ty is None: return None idx_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, sub_addr.slice, None, 0, trans) if idx_val is None: return None # Index zext is handled inside mlirlite.build_gep/build_gep_array (single source of truth). arr_pointee_ca: mlirlite.LLVMType* = None if self.Gen.ExprGenBase.is_ptr_type(arr_val.Ty) != 0: arr_pointee_ca = arr_val.Ty.Pointee if arr_pointee_ca is not None: match arr_pointee_ca: case mlirlite.LLVMType.Array(arr_et_ca, _): return mlirlite.build_gep_array(builder, arr_pointee_ca, arr_et_ca, arr_val, idx_val) case _: # Scalar/pointer elements (e.g. string a[4]) use a simple GEP; # a scalar pointee previously fell through to an undefined @Addr. return mlirlite.build_gep(builder, arr_pointee_ca, arr_val, idx_val) return None # Call form: c.Addr(expr) evaluates expr into a temp entry alloca and # returns its address, e.g. tt: Coroutine = c.Addr(task()). if arg0.kind() == ast.ASTKind.Call: call_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg0, None, 0, trans) if call_val is None or call_val.Ty is None: return None tmp_slot: mlirlite.Value* = mlirlite.build_alloca_at_entry(builder, call_val.Ty) if tmp_slot is None: return None mlirlite.build_store(builder, call_val, tmp_slot) return tmp_slot # Other arg0 forms fail-fast: c.Addr is a compile-time instruction and must # never fall through to a runtime call @Addr. self.Gen.TypeGenErr.fatal_error(cl, f"c.Addr unsupported operand form (kind={arg0.kind()}); c.Addr only supports variable/global/function/array element/field", "TypeError") return None def translate_c_load(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator* = None) -> mlirlite.Value*: """Load *b and store it into *a.""" if cl is None: return None cargs: list[ast.AST*]* = cl.args if cargs is None or len(cargs) < 2: return None dst_ptr: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cargs.get(0), None, 0, trans) src_ptr: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cargs.get(1), None, 0, trans) if dst_ptr is None or src_ptr is None: return None # Determine the pointee type (prefer src's pointee, fall back to dst's) pointee: mlirlite.LLVMType* = None if src_ptr.Ty is not None: pointee = src_ptr.Ty.Pointee if pointee is None and dst_ptr.Ty is not None: pointee = dst_ptr.Ty.Pointee if pointee is None: return None # bitcast src to pointee* and load target_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, pointee) src_cast: mlirlite.Value* = mlirlite.build_bitcast(builder, src_ptr, target_ptr_ty) if src_cast is None: return None loaded: mlirlite.Value* = mlirlite.build_load(builder, pointee, src_cast) if loaded is None: return None # bitcast dst to pointee* and store dst_cast: mlirlite.Value* = mlirlite.build_bitcast(builder, dst_ptr, target_ptr_ty) if dst_cast is None: return None mlirlite.build_store(builder, loaded, dst_cast) return loaded def _asm_append_cstr(self, dst: str, dst_size: sizet, src: str, what: str = "inline asm"): """Append the C string src to the end of dst; fail-fast when it does not fit. Silent truncation corrupts the generated text (a template cut in the middle of an instruction yields a bogus llc/verifier error much later), so overflow is reported here at the real cause. """ if dst is None or src is None: return dlen: sizet = string.strlen(dst) slen: sizet = string.strlen(src) # Capacity includes the terminating NUL. if dlen + slen + 1 > dst_size: self.Gen.TypeGenErr.fatal_error(None, f"{what} text does not fit the fixed buffer: {dlen} byte(s) already used + {slen} byte(s) to append + 1 NUL > capacity {dst_size} byte(s); shorten or split the template (silent truncation would generate broken code)", "TypeError") return i: sizet = 0 while i < slen: dst[dlen + i] = src[i] i += 1 dst[dlen + i] = '\0' def _asm_mangle_global_symbols(self, pool: memhub.MemBuddy*, asm_text: str, asm_size: sizet, mod: mlirlite.LLVMModule*) -> str: """Fail-fast when inline asm references a SHA1-mangled module global by bare name.""" if asm_text is None or mod is None: return asm_text src_len: sizet = string.strlen(asm_text) if src_len == 0: return asm_text out: str = pool.alloc(asm_size * 2) if out is None: return asm_text out[0] = '\0' out_pos: sizet = 0 i: sizet = 0 while i < src_len: matched: int = 0 cur: mlirlite.GlobalVariable* = mod.GlobalHead while cur is not None: gname: str = cur.Name if gname is not None and string.strchr(gname, 46) is not None: dot_g: str = string.strchr(gname, 46) bare_g: str = dot_g + 1 bare_len: sizet = string.strlen(bare_g) if bare_len > 0 and i + bare_len <= src_len: if string.strncmp(asm_text + i, bare_g, bare_len) == 0: # Word boundary: neighbors are not identifier chars (letter/digit/_) prev_ok: int = 1 next_ok: int = 1 if i > 0: prev_ch: char = asm_text[i - 1] if (prev_ch >= 'a' and prev_ch <= 'z') or (prev_ch >= 'A' and prev_ch <= 'Z') or (prev_ch >= '0' and prev_ch <= '9') or prev_ch == '_': prev_ok = 0 if i + bare_len < src_len: next_ch: char = asm_text[i + bare_len] if (next_ch >= 'a' and next_ch <= 'z') or (next_ch >= 'A' and next_ch <= 'Z') or (next_ch >= '0' and next_ch <= '9') or next_ch == '_': next_ok = 0 if prev_ok != 0 and next_ok != 0: # fail-fast: an asm template referencing a SHA1-mangled # global cannot be used in LLVM MC; mark it t.CExport. self.Gen.TypeGenErr.fatal_error(None, f"Global symbol '{bare_g}' referenced by the asm template has a SHA1 mangling prefix ({gname}); inline assembly must reference bare-name symbols, so mark that global definition as t.CExport", "TypeError") return asm_text cur = cur.Next if matched == 0: # The output buffer is 2x the input, so this cannot trigger for a plain copy; # keep it fail-fast so a future longer rewrite (mangled expansion) can never # silently drop characters. if out_pos + 1 >= asm_size * 2: self.Gen.TypeGenErr.fatal_error(None, f"inline asm text overflows the symbol-mangling buffer (capacity {asm_size * 2} byte(s)); shorten the template", "TypeError") return asm_text out[out_pos] = asm_text[i] out_pos += 1 i += 1 out[out_pos] = '\0' return out def _asm_escape_for_ir(self, pool: memhub.MemBuddy*, src: str) -> str: """Escape asm text for an LLVM IR string literal.""" if src is None: return None slen: sizet = string.strlen(src) buf: str = pool.alloc(slen * 4 + 8) if buf is None: return None pos: sizet = 0 for ch in src: if ch == '\n': buf[pos] = '\\' pos += 1 buf[pos] = '0' pos += 1 buf[pos] = 'A' pos += 1 elif ch == '"': buf[pos] = '\\' pos += 1 buf[pos] = '2' pos += 1 buf[pos] = '2' pos += 1 elif ch == '\\': buf[pos] = '\\' pos += 1 buf[pos] = '5' pos += 1 buf[pos] = 'C' pos += 1 else: buf[pos] = ch pos += 1 buf[pos] = '\0' return buf def _resolve_register(self, attr_name: str) -> str: """Map an ASM_DESCR attribute name to its constraint string.""" if attr_name is None: return "" # Clobber constraints if string.strcmp(attr_name, "CLOBBER_MEMORY") == 0: return "memory" if string.strcmp(attr_name, "CLOBBER_CC") == 0: return "cc" if string.strcmp(attr_name, "CLOBBER_RAX") == 0: return "rax" if string.strcmp(attr_name, "CLOBBER_RBX") == 0: return "rbx" if string.strcmp(attr_name, "CLOBBER_RCX") == 0: return "rcx" if string.strcmp(attr_name, "CLOBBER_RDX") == 0: return "rdx" if string.strcmp(attr_name, "CLOBBER_RSI") == 0: return "rsi" if string.strcmp(attr_name, "CLOBBER_RDI") == 0: return "rdi" if string.strcmp(attr_name, "CLOBBER_RBP") == 0: return "rbp" if string.strcmp(attr_name, "CLOBBER_RSP") == 0: return "rsp" if string.strcmp(attr_name, "CLOBBER_R8") == 0: return "r8" if string.strcmp(attr_name, "CLOBBER_R9") == 0: return "r9" if string.strcmp(attr_name, "CLOBBER_R10") == 0: return "r10" if string.strcmp(attr_name, "CLOBBER_R11") == 0: return "r11" if string.strcmp(attr_name, "CLOBBER_R12") == 0: return "r12" if string.strcmp(attr_name, "CLOBBER_R13") == 0: return "r13" if string.strcmp(attr_name, "CLOBBER_R14") == 0: return "r14" if string.strcmp(attr_name, "CLOBBER_R15") == 0: return "r15" if string.strcmp(attr_name, "CLOBBER_EAX") == 0: return "eax" if string.strcmp(attr_name, "CLOBBER_EBX") == 0: return "ebx" if string.strcmp(attr_name, "CLOBBER_ECX") == 0: return "ecx" if string.strcmp(attr_name, "CLOBBER_EDX") == 0: return "edx" if string.strcmp(attr_name, "CLOBBER_ESI") == 0: return "esi" if string.strcmp(attr_name, "CLOBBER_EDI") == 0: return "edi" if string.strcmp(attr_name, "CLOBBER_DX") == 0: return "dx" if string.strcmp(attr_name, "CLOBBER_AX") == 0: return "ax" if string.strcmp(attr_name, "CLOBBER_CX") == 0: return "cx" if string.strcmp(attr_name, "CLOBBER_SI") == 0: return "si" if string.strcmp(attr_name, "CLOBBER_DI") == 0: return "di" # 8-bit register clobbers (aligned with the t.py constant set): a missing AL # dropped ~{al} on `rep stosb` and let LLVM clobber the page address in RAX. if string.strcmp(attr_name, "CLOBBER_AL") == 0: return "al" if string.strcmp(attr_name, "CLOBBER_AH") == 0: return "ah" if string.strcmp(attr_name, "CLOBBER_BL") == 0: return "bl" if string.strcmp(attr_name, "CLOBBER_BH") == 0: return "bh" if string.strcmp(attr_name, "CLOBBER_CL") == 0: return "cl" if string.strcmp(attr_name, "CLOBBER_CH") == 0: return "ch" if string.strcmp(attr_name, "CLOBBER_DL") == 0: return "dl" if string.strcmp(attr_name, "CLOBBER_DH") == 0: return "dh" # Output constraints if string.strcmp(attr_name, "OUTPUT_REG") == 0: return "=r" if string.strcmp(attr_name, "OUTPUT_MEM") == 0: return "=m" if string.strcmp(attr_name, "OUTPUT_EAX") == 0: return "=a" if string.strcmp(attr_name, "OUTPUT_EBX") == 0: return "=b" if string.strcmp(attr_name, "OUTPUT_ECX") == 0: return "=c" if string.strcmp(attr_name, "OUTPUT_EDX") == 0: return "=d" if string.strcmp(attr_name, "OUTPUT_ESI") == 0: return "=S" if string.strcmp(attr_name, "OUTPUT_EDI") == 0: return "=D" if string.strcmp(attr_name, "OUTPUT_RAX") == 0: return "=a" if string.strcmp(attr_name, "OUTPUT_RBX") == 0: return "=b" if string.strcmp(attr_name, "OUTPUT_RCX") == 0: return "=c" if string.strcmp(attr_name, "OUTPUT_RDX") == 0: return "=d" if string.strcmp(attr_name, "OUTPUT_RSI") == 0: return "=S" if string.strcmp(attr_name, "OUTPUT_RDI") == 0: return "=D" # Input/register constraints if string.strcmp(attr_name, "REG_ANY") == 0: return "r" if string.strcmp(attr_name, "REG_EAX") == 0: return "a" if string.strcmp(attr_name, "REG_EBX") == 0: return "b" if string.strcmp(attr_name, "REG_ECX") == 0: return "c" if string.strcmp(attr_name, "REG_EDX") == 0: return "d" if string.strcmp(attr_name, "REG_ESI") == 0: return "S" if string.strcmp(attr_name, "REG_EDI") == 0: return "D" if string.strcmp(attr_name, "REG_RAX") == 0: return "a" if string.strcmp(attr_name, "REG_RBX") == 0: return "b" if string.strcmp(attr_name, "REG_RCX") == 0: return "c" if string.strcmp(attr_name, "REG_RDX") == 0: return "d" if string.strcmp(attr_name, "REG_RSI") == 0: return "S" if string.strcmp(attr_name, "REG_RDI") == 0: return "D" if string.strcmp(attr_name, "INPUT_REG") == 0: return "r" if string.strcmp(attr_name, "INPUT_MEM") == 0: return "m" if string.strcmp(attr_name, "INPUT_EAX") == 0: return "a" if string.strcmp(attr_name, "INPUT_EBX") == 0: return "b" if string.strcmp(attr_name, "INPUT_ECX") == 0: return "c" if string.strcmp(attr_name, "INPUT_EDX") == 0: return "d" if string.strcmp(attr_name, "INPUT_ESI") == 0: return "S" if string.strcmp(attr_name, "INPUT_EDI") == 0: return "D" if string.strcmp(attr_name, "INPUT_RAX") == 0: return "a" if string.strcmp(attr_name, "INPUT_RBX") == 0: return "b" if string.strcmp(attr_name, "INPUT_RCX") == 0: return "c" if string.strcmp(attr_name, "INPUT_RDX") == 0: return "d" if string.strcmp(attr_name, "INPUT_RSI") == 0: return "S" if string.strcmp(attr_name, "INPUT_RDI") == 0: return "D" return "" def _resolve_asm_descr(self, pool: memhub.MemBuddy*, node: ast.AST*) -> str: """Resolve a constraint expression recursively into its constraint string.""" if node is None: return "" k: int = node.kind() # BinOp with BitOr: concatenate left and right if k == ast.ASTKind.BinOp: bop: ast.BinOp* = ast.BinOp*(node) if bop.op == ast.OpKind.BitOr: left_str: str = self._resolve_asm_descr(pool, bop.left) right_str: str = self._resolve_asm_descr(pool, bop.right) llen: sizet = string.strlen(left_str) rlen: sizet = string.strlen(right_str) buf: str = pool.alloc(llen + rlen + 1) if buf is None: return "" string.strcpy(buf, left_str) for i in range(int(rlen)): buf[llen + i] = right_str[i] buf[llen + rlen] = '\0' return buf return "" # Attribute: ASM_DESCR.XXX if k == ast.ASTKind.Attribute: at: ast.Attribute* = ast.Attribute*(node) if at.value is not None and at.value.kind() == ast.ASTKind.Name: mod_nm2: ast.Name* = ast.Name*(at.value) if mod_nm2.id is not None and string.strcmp(mod_nm2.id, "ASM_DESCR") == 0: return self._resolve_register(at.attr) return "" # 裸枚举名(不带 ASM_DESCR. / t. 前缀):`op=[CLOBBER_MEMORY, CLOBBER_RAX]`。 # 只有确实解析成 ASM_DESCR 成员时才接受(_resolve_register 对未知名给不出非空串), # 否则返回空串交由调用方按"未知约束"处理,避免把普通变量名误当约束串。 if k == ast.ASTKind.Name: nm_b: ast.Name* = ast.Name*(node) if nm_b.id is not None: rs_b: str = self._resolve_register(nm_b.id) if rs_b is not None and rs_b[0] != '\0': return rs_b return "" # Constant: string constant if k == ast.ASTKind.Constant: cn: ast.Constant* = ast.Constant*(node) if cn.const_kind == ast.CONST_STR: return cn.str_val return "" return "" def _asm_get_operand(self, base: GenTypes.AsmOperand*, idx: int) -> GenTypes.AsmOperand*: """Return a pointer to operand idx in the operand array.""" entry_size: sizet = GenTypes.AsmOperand.__sizeof__() entry_addr: uint64 = uint64(base) + idx * entry_size return GenTypes.AsmOperand*(entry_addr) def _asm_is_c_call(self, node: ast.Call*) -> str: """Return the canonical name for an operand-marker call, else None.""" if node is None or node.func is None: return None if node.func.kind() == ast.ASTKind.Name: nm: ast.Name* = ast.Name*(node.func) if nm.id is not None: if string.strcmp(nm.id, "asminp") == 0: return "AsmInp" if string.strcmp(nm.id, "asmout") == 0: return "AsmOut" if string.strcmp(nm.id, "irinp") == 0: return "LInp" if string.strcmp(nm.id, "irout") == 0: return "LOut" return None return None def _asm_name_is_mangled(self, name: str) -> int: """Return 1 when the LLVM name carries a SHA1-mangled prefix.""" if name is None: return 0 n: str = name nlen: sizet = string.strlen(n) # Strip the '@"..."' wrapper (reference name) if nlen >= 3 and n[0] == '@' and n[1] == '"' and n[nlen - 1] == '"': n = n + 2 elif nlen >= 1 and n[0] == '@': n = n + 1 if GenApi.sha1_prefix_len(n) != 0: return 1 return 0 def _asm_add_operand(self, operands: GenTypes.AsmOperand*, count: int, val: mlirlite.Value*, constraint: str, is_output: int) -> int: """Append an operand, dedup by value and direction, and return the new count.""" # fail-fast: the operand array is fixed at ASM_MAX_OPERANDS slots; dropping the # excess operand here would silently change the generated constraint/argument lists. if count >= ASM_MAX_OPERANDS: self.Gen.TypeGenErr.fatal_error(None, f"inline asm has more than {ASM_MAX_OPERANDS} operands (compiler limit); split the asm block into several ones", "TypeError") return count # fail-fast: AsmInp/AsmOut operands must not carry SHA1-mangled symbols; # LLVM MC cannot parse digit-leading names, so mark them t.CExport. if val is not None and val.Name is not None: if self._asm_name_is_mangled(val.Name) != 0: self.Gen.TypeGenErr.fatal_error(None, f"__asm__ operand (AsmInp/AsmOut) does not allow a symbol with a SHA1 mangling prefix '{val.Name}'; mark the corresponding function/global definition as t.CExport (bare name)", "TypeError") if val is not None: for ei in range(int(count)): eop: GenTypes.AsmOperand* = self._asm_get_operand(operands, ei) if eop is not None and eop.Value == val and eop.IsOutput == is_output: return count op: GenTypes.AsmOperand* = self._asm_get_operand(operands, count) op.Value = val # Default constraint: AsmInp(reg) with one arg has no descr, causing an # llc mismatch. Input defaults to "r", output to "=r". if constraint is None or string.strlen(constraint) == 0: if is_output != 0: constraint = "=r" else: constraint = "r" op.Constraint = constraint op.IsOutput = is_output return count + 1 def translate_c_asm(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator* = None) -> mlirlite.Value*: """Translate an __asm__ block and return its result Value or None.""" if cl is None: return None cargs: list[ast.AST*]* = cl.args if cargs is None or len(cargs) < 1: return None # Allocate the operand array operand_size: sizet = GenTypes.AsmOperand.__sizeof__() operands: GenTypes.AsmOperand* = pool.alloc(operand_size * ASM_MAX_OPERANDS) if operands is None: return None string.memset(operands, 0, operand_size * ASM_MAX_OPERANDS) # Clobber name array: 8 bytes/slot, capacity ASM_MAX_CLOBBERS, decoupled from # the operand limit; a single-level rawptr buffer with slots cast to str*. clobbers: rawptr = pool.alloc(8 * ASM_MAX_CLOBBERS) if clobbers is None: return None string.memset(clobbers, 0, 8 * ASM_MAX_CLOBBERS) # Allocate the asm text buffer asm_text: str = pool.alloc(ASM_TEXT_BUF_SIZE) if asm_text is None: return None asm_text[0] = '\0' operand_count: int = 0 clobber_count: int = 0 fv_count: int = 0 # Cache the first output's element type at collection time (like the Python # reference), avoiding an unstable re-read that left ret as Void. out_ret_ty_first: mlirlite.LLVMType* = None # Cache all output element types in order (multi-output like vrandom's # val+ok), taken from the alloca pointee at collection time. out_ptys: sizet* = pool.alloc(8 * ASM_MAX_OPERANDS) out_pty_n: int = 0 if out_ptys is not None: string.memset(out_ptys, 0, 8 * ASM_MAX_OPERANDS) # fv index array: records each FormattedValue's original operand index fv_orig_indices: int* = pool.alloc(4 * ASM_MAX_OPERANDS) if fv_orig_indices is None: return None string.memset(fv_orig_indices, 0, 4 * ASM_MAX_OPERANDS) # 1. Parse the asm template (JoinedStr or Constant) - first pass: collect # operands only; $N placeholders are numbered after reordering. tmpl_node: ast.AST* = cargs.get(0) if tmpl_node is not None: tk: int = tmpl_node.kind() if tk == ast.ASTKind.JoinedStr: js: ast.JoinedStr* = ast.JoinedStr*(tmpl_node) jvalues: list[ast.AST*]* = js.values if jvalues is not None: jvlen: sizet = len(jvalues) for vi in range(int(jvlen)): part: ast.AST* = jvalues.get(vi) if part is None: continue pk: int = part.kind() if pk == ast.ASTKind.FormattedValue: fv: ast.FormattedValue* = ast.FormattedValue*(part) fexpr: ast.AST* = fv.value if fexpr is None or fexpr.kind() != ast.ASTKind.Call: continue fcall: ast.Call* = ast.Call*(fexpr) attr_nm: str = self._asm_is_c_call(fcall) if attr_nm is None: continue is_out: int = 0 if string.strcmp(attr_nm, "AsmOut") == 0: is_out = 1 elif string.strcmp(attr_nm, "AsmInp") == 0: is_out = 0 else: continue fargs: list[ast.AST*]* = fcall.args if fargs is None or len(fargs) < 1: continue val_node: ast.AST* = fargs.get(0) cons_str: str = "" if len(fargs) >= 2: cons_str = self._resolve_asm_descr(pool, fargs.get(1)) if is_out != 0: # Output: get the variable alloca (Name local/global or Attribute field address) if val_node is None: continue alloca: mlirlite.Value* = None if val_node.kind() == ast.ASTKind.Name: nm: ast.Name* = ast.Name*(val_node) if nm.id is None: continue alloca = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) if alloca is None: # Module-level global (lookup_var only sees locals): # fall back to make_global_ref into a quoted Ptr(Ty). gv_asm: mlirlite.GlobalVariable* = self.Gen.ExprGenAttr.find_global_in_module(mod, nm.id) if gv_asm is not None and gv_asm.Name is not None and gv_asm.Ty is not None: alloca = self.Gen.ExprGenAttr.make_global_ref(pool, gv_asm.Name, gv_asm.Ty) elif val_node.kind() == ast.ASTKind.Attribute: # AsmOut(p.pml4_root): GEP to the field address, like # translate_c_addr's Attribute branch; skipping it left a void operand. at_a: ast.Attribute* = ast.Attribute*(val_node) if at_a.attr is not None and at_a.value is not None and at_a.value.kind() == ast.ASTKind.Name: base_nm_a: ast.Name* = ast.Name*(at_a.value) if base_nm_a.id is not None: base_alloca_a: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, base_nm_a.id) if base_alloca_a is not None and base_alloca_a.Ty is not None: struct_ty_a: mlirlite.LLVMType* = None self_is_ptr_ptr_a: int = 0 match base_alloca_a.Ty: case mlirlite.LLVMType.Ptr(inner1): match inner1: case mlirlite.LLVMType.Ptr(inner2): struct_ty_a = inner2 self_is_ptr_ptr_a = 1 case _: struct_ty_a = inner1 case _: pass if struct_ty_a is not None: fe_a: GenTypes.FieldEntry* = self.Gen.StructGen.lookup_field(struct_ty_a, at_a.attr) if fe_a is not None: obj_ptr_a: mlirlite.Value* = base_alloca_a if self_is_ptr_ptr_a == 1: obj_ptr_a = mlirlite.build_load(builder, mlirlite.Ptr(pool, struct_ty_a), base_alloca_a) if obj_ptr_a is not None: alloca = mlirlite.build_gep_struct(builder, struct_ty_a, fe_a.Ty, obj_ptr_a, fe_a.Index) if alloca is None: continue operand_count = self._asm_add_operand(operands, operand_count, alloca, cons_str, 1) if out_ret_ty_first is None and alloca is not None and alloca.Ty is not None and alloca.Ty.Pointee != 0: out_ret_ty_first = alloca.Ty.Pointee if out_ptys is not None and out_pty_n < ASM_MAX_OPERANDS and alloca is not None and alloca.Ty is not None and alloca.Ty.Pointee != 0: out_ptys[out_pty_n] = uint64(alloca.Ty.Pointee) out_pty_n += 1 else: # Input: translate the value ival: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, val_node, None, 0, trans) if ival is None: continue operand_count = self._asm_add_operand(operands, operand_count, ival, cons_str, 0) # Record the fv's original operand index if fv_count < ASM_MAX_OPERANDS: fv_orig_indices[fv_count] = operand_count - 1 fv_count += 1 # 2. Parse keyword args out=/inp=/op=/clobber= keywords: list[ast.AST*]* = cl.keywords if keywords is not None: kwlen: sizet = len(keywords) for ki in range(int(kwlen)): kw: ast.Keyword* = ast.Keyword*(keywords.get(ki)) if kw is None or kw.arg is None or kw.value is None: continue kwk: int = kw.value.kind() if string.strcmp(kw.arg, "out") == 0: # Output operand list if kwk != ast.ASTKind.List: continue out_lst: ast.List* = ast.List*(kw.value) out_elts: list[ast.AST*]* = out_lst.elts if out_elts is None: continue oelen: sizet = len(out_elts) for oi in range(int(oelen)): oelt: ast.AST* = out_elts.get(oi) if oelt is None or oelt.kind() != ast.ASTKind.Call: continue ocall: ast.Call* = ast.Call*(oelt) oattr: str = self._asm_is_c_call(ocall) if oattr is None or string.strcmp(oattr, "AsmOut") != 0: continue oargs: list[ast.AST*]* = ocall.args if oargs is None or len(oargs) < 1: continue oval_node: ast.AST* = oargs.get(0) ocons: str = "" if len(oargs) >= 2: ocons = self._resolve_asm_descr(pool, oargs.get(1)) if oval_node is None or oval_node.kind() != ast.ASTKind.Name: continue onm: ast.Name* = ast.Name*(oval_node) if onm.id is None: continue oalloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, onm.id) if oalloca is None: continue operand_count = self._asm_add_operand(operands, operand_count, oalloca, ocons, 1) if out_ret_ty_first is None and oalloca is not None and oalloca.Ty is not None and oalloca.Ty.Pointee != 0: out_ret_ty_first = oalloca.Ty.Pointee if out_ptys is not None and out_pty_n < ASM_MAX_OPERANDS and oalloca is not None and oalloca.Ty is not None and oalloca.Ty.Pointee != 0: out_ptys[out_pty_n] = uint64(oalloca.Ty.Pointee) out_pty_n += 1 elif string.strcmp(kw.arg, "inp") == 0 or string.strcmp(kw.arg, "inputs") == 0: # Input operand list if kwk != ast.ASTKind.List: continue in_lst: ast.List* = ast.List*(kw.value) in_elts: list[ast.AST*]* = in_lst.elts if in_elts is None: continue ielen: sizet = len(in_elts) for ii in range(int(ielen)): ielt: ast.AST* = in_elts.get(ii) if ielt is None or ielt.kind() != ast.ASTKind.Call: continue icall: ast.Call* = ast.Call*(ielt) iattr: str = self._asm_is_c_call(icall) if iattr is None or string.strcmp(iattr, "AsmInp") != 0: continue iargs: list[ast.AST*]* = icall.args if iargs is None or len(iargs) < 1: continue ival_node: ast.AST* = iargs.get(0) icons: str = "" if len(iargs) >= 2: icons = self._resolve_asm_descr(pool, iargs.get(1)) ival2: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, ival_node, None, 0, trans) if ival2 is None: continue operand_count = self._asm_add_operand(operands, operand_count, ival2, icons, 0) elif string.strcmp(kw.arg, "op") == 0 or string.strcmp(kw.arg, "clobber") == 0: # Clobber list if kwk != ast.ASTKind.List: continue cl_lst: ast.List* = ast.List*(kw.value) cl_elts: list[ast.AST*]* = cl_lst.elts if cl_elts is None: continue clen: sizet = len(cl_elts) for ci in range(int(clen)): celt: ast.AST* = cl_elts.get(ci) if celt is None: continue cstr: str = self._resolve_asm_descr(pool, celt) if cstr is None or cstr[0] == '\0': continue # fail-fast: the clobber slot array is fixed at ASM_MAX_CLOBBERS; # dropping the excess would silently weaken the clobber list. if clobber_count >= ASM_MAX_CLOBBERS: self.Gen.TypeGenErr.fatal_error(celt, f"inline asm has more than {ASM_MAX_CLOBBERS} clobbers (compiler limit); remove duplicates or split the asm block", "TypeError") return None cl_addr: uint64 = uint64(clobbers) + clobber_count * 8 cl_ptr: str* = (str*)(cl_addr) cl_ptr[0] = cstr clobber_count += 1 # 3. Parse the second positional arg as the clobber list if len(cargs) >= 2: pos_node: ast.AST* = cargs.get(1) if pos_node is not None and pos_node.kind() == ast.ASTKind.List: pos_lst: ast.List* = ast.List*(pos_node) pos_elts: list[ast.AST*]* = pos_lst.elts if pos_elts is not None: plen: sizet = len(pos_elts) for pi in range(int(plen)): pelt: ast.AST* = pos_elts.get(pi) if pelt is None: continue pstr: str = self._resolve_asm_descr(pool, pelt) if pstr is None or pstr[0] == '\0': continue if clobber_count >= ASM_MAX_CLOBBERS: self.Gen.TypeGenErr.fatal_error(pelt, f"inline asm has more than {ASM_MAX_CLOBBERS} clobbers (compiler limit); remove duplicates or split the asm block", "TypeError") return None pcl_addr: uint64 = uint64(clobbers) + clobber_count * 8 pcl_ptr: str* = (str*)(pcl_addr) pcl_ptr[0] = pstr clobber_count += 1 # 4. Count outputs/inputs and determine the return type output_count: int = 0 input_count: int = 0 first_output_idx: int = -1 for oi2 in range(int(operand_count)): op2: GenTypes.AsmOperand* = self._asm_get_operand(operands, oi2) if op2.IsOutput != 0: if first_output_idx < 0: first_output_idx = oi2 output_count += 1 else: input_count += 1 # Determine the return type. Multiple outputs return an anonymous struct # {ty1, ty2, ...}; LLVM asm requires output constraints == struct elements. ret_ty: mlirlite.LLVMType* = mlirlite.Void(pool) if output_count > 0 and first_output_idx >= 0: if output_count > 1: # The multi-output struct field list is a ParamNode chain (not # Function*); annotating it as Function* broke implicit conversion. out_pn_head: mlirlite.ParamNode* = None out_pn_last: mlirlite.ParamNode* = None out_pn_n: int = 0 out_seq: int = 0 for oi5 in range(int(operand_count)): op5: GenTypes.AsmOperand* = self._asm_get_operand(operands, oi5) if op5 is None or op5.IsOutput == 0: continue pty5: mlirlite.LLVMType* = None # Prefer the type cached in order at collection, then fall back to reading the slot if out_ptys is not None and out_seq < out_pty_n: pty5 = mlirlite.LLVMType*(out_ptys[out_seq]) elif op5.Value is not None and op5.Value.Ty is not None and op5.Value.Ty.Pointee != 0: pty5 = op5.Value.Ty.Pointee out_seq += 1 if pty5 is None: continue pn5: mlirlite.ParamNode* = mlirlite.new_param_node(pool, pty5) if pn5 is not None: if out_pn_head is None: out_pn_head = pn5 else: out_pn_last.Next = pn5 out_pn_last = pn5 out_pn_n += 1 if out_pn_head is not None and out_pn_n > 0: ret_ty = mlirlite.Struct(pool, out_pn_head, out_pn_n, None) else: self.Gen.TypeGenErr.fatal_error(cl, "__asm__ output operand type missing (variable alloca type degraded)", "TypeError") ret_ty = mlirlite.Int32(pool) else: out_op: GenTypes.AsmOperand* = self._asm_get_operand(operands, first_output_idx) out_pt: mlirlite.LLVMType* = None # Prefer the element type cached at collection, then fall back to reading the slot if out_ret_ty_first is not None: out_pt = out_ret_ty_first elif out_op is not None and out_op.Value is not None and out_op.Value.Ty is not None and out_op.Value.Ty.Pointee != 0: out_pt = out_op.Value.Ty.Pointee if out_pt is not None: ret_ty = out_pt else: self.Gen.TypeGenErr.fatal_error(cl, "__asm__ output operand type missing (variable alloca type degraded)", "TypeError") ret_ty = mlirlite.Int32(pool) # Determine void directly from output_count to avoid match type issues is_void: int = 1 if output_count > 0: is_void = 0 # [FIX] Outputs exist but ret is still Void: rescan all output operands # before emitting, otherwise a named void asm reaches llc. Else fail-fast. if is_void == 0: match ret_ty: case mlirlite.LLVMType.Void(): ret_re: mlirlite.LLVMType* = None for re_i in range(int(operand_count)): re_op: GenTypes.AsmOperand* = self._asm_get_operand(operands, re_i) if re_op is None or re_op.IsOutput == 0: continue if re_op.Value is not None and re_op.Value.Ty is not None and re_op.Value.Ty.Pointee != 0: ret_re = re_op.Value.Ty.Pointee break if ret_re is None: self.Gen.TypeGenErr.fatal_error(cl, "__asm__ output operand type missing (variable alloca type degraded)", "TypeError") ret_ty = ret_re case _: pass # 4.5 Reorder operands: outputs first, inputs after (LLVM IR requirement). # new_order maps new->orig; orig_to_new maps orig->new for $N numbering. new_order: int* = pool.alloc(4 * (operand_count + 1)) orig_to_new: int* = pool.alloc(4 * (operand_count + 1)) if new_order is None or orig_to_new is None: return None new_idx: int = 0 # Outputs first for oi3 in range(int(operand_count)): op3: GenTypes.AsmOperand* = self._asm_get_operand(operands, oi3) if op3.IsOutput != 0: new_order[new_idx] = oi3 new_idx += 1 # Then inputs for oi4 in range(int(operand_count)): op4: GenTypes.AsmOperand* = self._asm_get_operand(operands, oi4) if op4.IsOutput == 0: new_order[new_idx] = oi4 new_idx += 1 # Build the orig -> new mapping for ni2 in range(int(operand_count)): orig_to_new[new_order[ni2]] = ni2 # 4.6 Generate asm text (second f-string pass, $N from reordered indices). # The project uses Intel syntax via the IR inteldialect keyword. if tmpl_node is not None: tk2: int = tmpl_node.kind() if tk2 == ast.ASTKind.JoinedStr: js2: ast.JoinedStr* = ast.JoinedStr*(tmpl_node) jvalues2: list[ast.AST*]* = js2.values if jvalues2 is not None: jvlen2: sizet = len(jvalues2) fv_fi: int = 0 for vi2 in range(int(jvlen2)): part2: ast.AST* = jvalues2.get(vi2) if part2 is None: continue pk2: int = part2.kind() if pk2 == ast.ASTKind.Constant: cn3: ast.Constant* = ast.Constant*(part2) if cn3.const_kind == ast.CONST_STR and cn3.str_val is not None: self._asm_append_cstr(asm_text, ASM_TEXT_BUF_SIZE, cn3.str_val) elif pk2 == ast.ASTKind.FormattedValue: if fv_fi < fv_count: orig_idx2: int = fv_orig_indices[fv_fi] new_idx2: int = orig_to_new[orig_idx2] ph_buf2: str = pool.alloc(16) if ph_buf2 is not None: viperlib.snprintf(ph_buf2, 16, "$%d", new_idx2) self._asm_append_cstr(asm_text, ASM_TEXT_BUF_SIZE, ph_buf2) fv_fi += 1 elif tk2 == ast.ASTKind.Constant: cn4: ast.Constant* = ast.Constant*(tmpl_node) if cn4.const_kind == ast.CONST_STR and cn4.str_val is not None: self._asm_append_cstr(asm_text, ASM_TEXT_BUF_SIZE, cn4.str_val) # 5. Build the constraint string in reordered order (outputs first). # Capacity scales with the operand/clobber count: a 32-entry clobber list alone is # ~300 bytes, so the old fixed 512-byte cap could silently cut it. Overflow of this # (or any asm text buffer) is fail-fast in _asm_append_cstr. const_cap: sizet = ASM_CONST_BUF_SIZE + sizet(operand_count + clobber_count + 1) * 64 const_buf: str = pool.alloc(const_cap) if const_buf is None: return None const_buf[0] = '\0' first_const: int = 1 for ci2 in range(int(operand_count)): orig_i: int = new_order[ci2] cop: GenTypes.AsmOperand* = self._asm_get_operand(operands, orig_i) cons_val: str = cop.Constraint if cons_val is None: cons_val = "r" if first_const == 0: self._asm_append_cstr(const_buf, const_cap, ",") first_const = 0 if cop.IsOutput != 0: # Output constraint: ensure it starts with = if cons_val[0] != '=': self._asm_append_cstr(const_buf, const_cap, "=") self._asm_append_cstr(const_buf, const_cap, cons_val) else: self._asm_append_cstr(const_buf, const_cap, cons_val) # Append clobber constraints ~{name} for cidx in range(int(clobber_count)): cl_addr2: uint64 = uint64(clobbers) + cidx * 8 cname: str = (str*)(cl_addr2)[0] if cname is None or cname[0] == '\0': continue if first_const == 0: self._asm_append_cstr(const_buf, const_cap, ",") first_const = 0 self._asm_append_cstr(const_buf, const_cap, "~{") self._asm_append_cstr(const_buf, const_cap, cname) self._asm_append_cstr(const_buf, const_cap, "}") # 6. Build the argument list text (input operands only): each arg is type text # (TypePrint is given a 4096-byte buffer below) plus the value name, so size the # buffer from that worst case instead of underestimating it (544 bytes/operand # silently cut wide inlined named structs). asm_arg_cap: sizet = sizet(operand_count + 1) * 4224 + 64 args_buf: str = pool.alloc(asm_arg_cap) if args_buf is None: return None args_buf[0] = '\0' first_arg: int = 1 for ai3 in range(int(operand_count)): orig_i2: int = new_order[ai3] aop: GenTypes.AsmOperand* = self._asm_get_operand(operands, orig_i2) if aop.IsOutput != 0: continue aval: mlirlite.Value* = aop.Value if aval is None or aval.Ty is None or aval.Name is None: continue if first_arg == 0: self._asm_append_cstr(args_buf, asm_arg_cap, ", ") first_arg = 0 # Type text ty_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_CALLGENCBUILTIN_1150) if ty_buf is not None: ty_buf[0] = '\0' mlirlite.TypePrint(ty_buf, 4096, aval.Ty, pool) self._asm_append_cstr(args_buf, asm_arg_cap, ty_buf) self._asm_append_cstr(args_buf, asm_arg_cap, " ") self._asm_append_cstr(args_buf, asm_arg_cap, aval.Name) # 7. Escape the asm text. First replace bare symbols with mangled names # (lea rax, [rip + gdt] -> [rip + 054f5e56cbefcaa4.gdt]). mangled_asm: str = self._asm_mangle_global_symbols(pool, asm_text, ASM_TEXT_BUF_SIZE, mod) if mangled_asm is None: mangled_asm = asm_text escaped_asm: str = self._asm_escape_for_ir(pool, mangled_asm) if escaped_asm is None: return None # 8. Generate and emit the IR line # The line buffer is sized after ret_ty_buf is ready (see line_cap below) ssa_name: str = None if is_void == 0: ssa_name = pool.alloc(16) if ssa_name is not None: viperlib.snprintf(ssa_name, 16, "%%%d", builder.Counter) builder.Counter += 1 # Get the return type text ret_ty_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_CALLGENCBUILTIN_1176) if ret_ty_buf is not None: ret_ty_buf[0] = '\0' mlirlite.TypePrint(ret_ty_buf, 4096, ret_ty, pool) # Size the line buffer by actual content (type text can be 512 bytes; asm/constraints may be long) line_cap: sizet = 512 + string.strlen(escaped_asm) + string.strlen(args_buf) if const_buf is not None: line_cap += string.strlen(const_buf) if ret_ty_buf is not None: line_cap += string.strlen(ret_ty_buf) line: str = pool.alloc(line_cap) if line is None: return None if is_void == 1: viperlib.snprintf(line, line_cap, "call %s asm sideeffect inteldialect \"%s\", \"%s\"(%s)", ret_ty_buf, escaped_asm, const_buf, args_buf) else: viperlib.snprintf(line, line_cap, "%s = call %s asm sideeffect inteldialect \"%s\", \"%s\"(%s)", ssa_name, ret_ty_buf, escaped_asm, const_buf, args_buf) # Emit the IR into the current block mlirlite.block_append_text(pool, builder.CurBlock, line) # 9. Handle outputs: store results into the output variables if is_void == 0 and first_output_idx >= 0: result_val: mlirlite.Value* = mlirlite.SSAValue(pool, ret_ty, ssa_name) out_emit_idx: int = 0 for oi_out in range(int(operand_count)): orig_o: int = new_order[oi_out] oo: GenTypes.AsmOperand* = self._asm_get_operand(operands, orig_o) if oo is None or oo.IsOutput == 0: continue if output_count > 1: # Multi-output: extractvalue index out_emit_idx, then store if oo.Value is not None and oo.Value.Ty is not None and oo.Value.Ty.Pointee != 0: ev_name: str = pool.alloc(32) # Size the line from its actual parts: ret_ty_buf alone can be ~4096 # bytes, so the old fixed cap could cut the instruction in half. ev_cap: sizet = ASM_LINE_BUF_SIZE + 64 if ret_ty_buf is not None: ev_cap += string.strlen(ret_ty_buf) if ssa_name is not None: ev_cap += string.strlen(ssa_name) ev_line: str = pool.alloc(ev_cap) if ev_name is not None and ev_line is not None: # ssa_name is "%N"; build a unique "%asm.N.e{i}" (letter- # initial) so %N.e{i} is not parsed as id plus a bad suffix. viperlib.snprintf(ev_name, 32, "%%asm.%s.e%d", ssa_name + 1, out_emit_idx) viperlib.snprintf(ev_line, ev_cap, "%s = extractvalue %s %s, %d", ev_name, ret_ty_buf, ssa_name, out_emit_idx) mlirlite.block_append_text(pool, builder.CurBlock, ev_line) ev_val: mlirlite.Value* = mlirlite.SSAValue(pool, oo.Value.Ty.Pointee, ev_name) mlirlite.build_store(builder, ev_val, oo.Value) else: if oo.Value is not None: mlirlite.build_store(builder, result_val, oo.Value) out_emit_idx += 1 return result_val return None def translate_c_llvmir(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator* = None) -> mlirlite.Value*: """Inline raw LLVM IR text into the current basic block; returns None.""" if cl is None or builder is None or pool is None or trans is None: return None cargs: list[ast.AST*]* = cl.args if cargs is None or len(cargs) < 1: self.Gen.TypeGenErr.fatal_error(cl, "c.LLVMIR missing template argument", "TypeError") # LOut writeback records (alloca pointer + register name), up to LLVMIR_MAX_OPS out_alloca: sizet* = pool.alloc(8 * LLVMIR_MAX_OPS) out_regname: sizet* = pool.alloc(8 * LLVMIR_MAX_OPS) if out_alloca is None or out_regname is None: return None string.memset(out_alloca, 0, 8 * LLVMIR_MAX_OPS) string.memset(out_regname, 0, 8 * LLVMIR_MAX_OPS) out_count: int = 0 # Template text buffer text: str = pool.alloc(LLVMIR_TEMPLATE_BUF) if text is None: return None text[0] = '\0' # 1. Parse the IR template (JoinedStr or Constant), replacing LInp/LOut/exprs with value text first_arg: ast.AST* = cargs.get(0) if first_arg is None: return None tk: int = first_arg.kind() if tk == ast.ASTKind.JoinedStr: js: ast.JoinedStr* = ast.JoinedStr*(first_arg) jvalues: list[ast.AST*]* = js.values if jvalues is not None: jvlen: sizet = len(jvalues) for vi in range(int(jvlen)): part: ast.AST* = jvalues.get(vi) if part is None: continue pk: int = part.kind() if pk == ast.ASTKind.Constant: # String literal fragment cn: ast.Constant* = ast.Constant*(part) if cn.str_val is not None: self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, cn.str_val, "c.LLVMIR") elif pk == ast.ASTKind.FormattedValue: fv: ast.FormattedValue* = ast.FormattedValue*(part) fexpr: ast.AST* = fv.value if fexpr is None: continue # Detect c.LInp / c.LOut if fexpr.kind() == ast.ASTKind.Call: fcall: ast.Call* = ast.Call*(fexpr) attr_nm: str = self._asm_is_c_call(fcall) if attr_nm is not None: fargs: list[ast.AST*]* = fcall.args if fargs is None or len(fargs) < 1: continue val_node: ast.AST* = fargs.get(0) if string.strcmp(attr_nm, "LInp") == 0: # Input operand: translate to an LLVM value reference text (%x or constant) inp_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, val_node, None, 0, trans) if inp_val is not None and inp_val.Name is not None: self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, inp_val.Name, "c.LLVMIR") elif string.strcmp(attr_nm, "LOut") == 0: # Output operand: allocate a unique SSA register # %llvmirN and record the writeback to the alloca. if val_node is not None and val_node.kind() == ast.ASTKind.Name: nm: ast.Name* = ast.Name*(val_node) if nm.id is not None: alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) if alloca is None or out_count >= LLVMIR_MAX_OPS: self.Gen.TypeGenErr.fatal_error(fcall, f"c.LOut variable '{nm.id}' is undefined or the output count exceeds {LLVMIR_MAX_OPS}", "TypeError") self.Gen.CallGenBase.LLVMIRRegSeq += 1 reg_buf: str = pool.alloc(32) if reg_buf is not None: viperlib.snprintf(reg_buf, 32, "%%llvmir_%d", self.Gen.CallGenBase.LLVMIRRegSeq) self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, reg_buf, "c.LLVMIR") out_alloca[out_count] = sizet(alloca) out_regname[out_count] = sizet(reg_buf) out_count += 1 continue # Non-LInp/LOut f-string expression: translate and embed the value text ival: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, fexpr, None, 0, trans) if ival is not None and ival.Name is not None: self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, ival.Name, "c.LLVMIR") elif tk == ast.ASTKind.Constant: cn2: ast.Constant* = ast.Constant*(first_arg) if cn2.str_val is not None: # A bare strcpy is unbounded and could overflow the pool block, so # use the same bounded append as the JoinedStr branch. self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, cn2.str_val, "c.LLVMIR") # 2. Embed the template text directly (no instruction parsing) + LOut writeback mlirlite.block_append_text(pool, builder.CurBlock, text) # LOut writeback: store %llvmirN, * %var oi: int for oi in range(int(out_count)): oa: mlirlite.Value* = mlirlite.Value*(out_alloca[oi]) rn: str = str(out_regname[oi]) if oa is None or oa.Ty is None or oa.Ty.Pointee == 0 or rn is None: continue reg_val: mlirlite.Value* = mlirlite.new_value(pool) if reg_val is None: continue reg_val.Ty = oa.Ty.Pointee reg_val.Name = rn reg_val.IsConst = 0 mlirlite.build_store(builder, reg_val, oa) return None # _apply_struct_defaults - apply default values to struct fields, after # zeroinitializer and before explicit args (which later overwrite defaults).