Files
ViperC/App/lib/core/Gen/Call/CallGenLeafKit.vp
T

1203 lines
66 KiB
Plaintext

import lib.core.Gen.GenTypes as GenTypes
import t, c
from stdint import *
import ast
import mlirlite
import memhub
import string
import stdlib
import viperlib
import lib.core.VLogger as VLogger
import lib.core.Gen.GenApi as GenApi
import lib.core.Gen.Translator as HT
import lib.core.CompilerFactory as CompilerFactory
import lib.core.Gen.Type.TypeSpec as TSpec
from . import CallGenCBuiltin
from . import CallGenBase
# ============================================================
# CallGenLeafKit - CallGenMixin split 3/9: EH/isinstance/closures/print/func table
# ============================================================
# Constants
define _CEXPORT_MAX = 256
define ASM_MAX_OPERANDS = 16
define ASM_TEXT_BUF_SIZE = 2048
define ASM_CONST_BUF_SIZE = 512
define ASM_ARGS_BUF_SIZE = 512
define ASM_LINE_BUF_SIZE = 4096
define LLVMIR_MAX_OPS = 16
define LLVMIR_TEMPLATE_BUF = 512
# Global function-table name index (name -> slot, open addressing / linear probing):
# capacity 16384 = 2 * GLOBAL_FUNC_MAX; slots store (index + 1), 0 = empty.
define GLOBAL_FUNC_IDX_CAP = 16384
# index hash constants (32-bit FNV-1a, same pattern as StructGenMixin)
define GFN_FNV_OFFSET = 2166136261
define GFN_FNV_PRIME = 16777619
define GFN_FNV_MASK = 4294967295
def _gfn_hash(name: str) -> uint64:
"""Function name to 64-bit hash (32-bit FNV-1a, byte-wise until NUL)"""
h: uint64 = uint64(GFN_FNV_OFFSET)
if name is None: return h
i: sizet = 0
while name[i] != '\0':
h = (h ^ uint64(int(name[i]))) * uint64(GFN_FNV_PRIME)
h = h & uint64(GFN_FNV_MASK)
i += 1
return h
def _gfn_idx_slot(idx_base: sizet*, si: sizet) -> sizet*:
"""Return the si-th index slot (8 bytes; stores slot index + 1, 0 = empty)"""
return (sizet*)(uint64(idx_base) + uint64(si) * 8)
def _gfn_idx_find(funcs: GenTypes.FuncEntry*, idx_base: sizet*, cap: int, name: str) -> int:
"""Probe a name in the global func-name index; return the matching slot (>=0) or -1"""
if funcs is None or idx_base is None or cap <= 0 or name is None: return -1
h: uint64 = _gfn_hash(name)
mask: sizet = sizet(cap - 1)
si: sizet = sizet(h) & mask
entry_size: sizet = GenTypes.FuncEntry.__sizeof__()
for i in range(int(cap)):
slot: sizet* = _gfn_idx_slot(idx_base, si)
sv: sizet = slot[0]
if sv == 0: return -1
se: int = int(sv) - 1
entry_addr: uint64 = uint64(funcs) + se * entry_size
entry: GenTypes.FuncEntry* = GenTypes.FuncEntry*(entry_addr)
if entry is not None and entry.Name is not None:
if string.strcmp(entry.Name, name) == 0: return se
si = (si + 1) & mask
return -1
def _gfn_idx_insert(idx_base: sizet*, cap: int, slot_index: int, name: str) -> int:
"""Insert (name -> slot_index) into the index; 0 = success, 1 = failure"""
if idx_base is None or cap <= 0 or name is None: return 1
h: uint64 = _gfn_hash(name)
mask: sizet = sizet(cap - 1)
si: sizet = sizet(h) & mask
for i in range(int(cap)):
slot: sizet* = _gfn_idx_slot(idx_base, si)
if slot[0] == 0:
slot[0] = sizet(slot_index + 1)
return 0
si = (si + 1) & mask
return 1
# ============================================================
# CallGenLeafKit - EH/isinstance/closures/print/func table
novirtual class CallGenLeafKit(CallGenCBuiltin.CallGenCBuiltin):
"""EH guard + isinstance + closures + print + global func table (extends CBuiltin)"""
# _func_has_eh_params - whether the callee signature has the try-except EH output
# params (%__eh_msg_out__/__eh_code_out__), injected for functions containing raise.
def _func_has_eh_params(self, func: mlirlite.Function*) -> int:
"""Return 1 if the function takes an EH out-parameter, else 0"""
if func is None: return 0
cur_p: mlirlite.Param* = mlirlite.function_get_param_head(func)
while cur_p is not None:
pn: str = mlirlite.param_get_name(cur_p)
if pn is not None and string.strcmp(pn, "%__eh_msg_out__") == 0: return 1
cur_p = mlirlite.param_get_next(cur_p)
return 0
# _emit_eh_guarded_call - caller-side try-except EH check (aligned with TPC
# HandlesExprCall). Reuses an existing msg/code alloca or out-param, else a local null alloca.
# ============================================================
def _emit_eh_guarded_call(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, func: mlirlite.Function*, call_name: str, head: mlirlite.Value*, count: int, ret_ty: mlirlite.LLVMType*, is_variadic: int) -> mlirlite.Value*:
"""Call a function with try-except EH detection; returns the call result (positioned at ok block)"""
if trans is None or self._func_has_eh_params(func) == 0: return mlirlite.build_call(builder, call_name, head, count, ret_ty, is_variadic)
# ---- 1. determine output pointers ----
i8_ty_g: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty_g: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_g)
i32_ty_g: mlirlite.LLVMType* = mlirlite.Int32(pool)
if i8_ty_g is None or i8_ptr_ty_g is None or i32_ty_g is None: return mlirlite.build_call(builder, call_name, head, count, ret_ty, is_variadic)
msg_ptr: mlirlite.Value* = None
code_ptr: mlirlite.Value* = None
if trans._eh_msg_ptr is not None and trans._eh_code_ptr is not None:
msg_ptr = trans._eh_msg_ptr
code_ptr = trans._eh_code_ptr
elif trans._cur_func_has_eh != 0 and trans._eh_msg_out_param is not None and trans._eh_code_out_param is not None:
msg_ptr = trans._eh_msg_out_param
code_ptr = trans._eh_code_out_param
if msg_ptr is None:
msg_ptr = self.Gen.VarGen._alloca_at_entry(builder, i8_ptr_ty_g)
code_ptr = self.Gen.VarGen._alloca_at_entry(builder, i32_ty_g)
# ---- 2. clear before the call ----
null_msg_g: mlirlite.Value* = mlirlite.ConstNull(pool, i8_ptr_ty_g, "null")
zero_code_g: mlirlite.Value* = mlirlite.const_int32(pool, 0)
if null_msg_g is not None: mlirlite.build_store(builder, null_msg_g, msg_ptr)
if zero_code_g is not None: mlirlite.build_store(builder, zero_code_g, code_ptr)
# ---- 3. append args (tail of list + count+2) ----
msg_ptr.Next = code_ptr
code_ptr.Next = None
if head is None: head = msg_ptr
else:
tail_g: mlirlite.Value* = head
while tail_g.Next is not None: tail_g = tail_g.Next
tail_g.Next = msg_ptr
count = count + 2
# ---- 4. call + check ----
result: mlirlite.Value* = mlirlite.build_call(builder, call_name, head, count, ret_ty, is_variadic)
cur_func_g: mlirlite.Function* = trans._cur_func
if cur_func_g is None or null_msg_g is None: return result
msg_val_g: mlirlite.Value* = mlirlite.build_load(builder, i8_ptr_ty_g, msg_ptr)
if msg_val_g is None: return result
is_err_g: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, msg_val_g, null_msg_g)
if is_err_g is None: return result
lc_g: int = trans._label_counter
trans._label_counter = lc_g + 1
bb_buf_g: str = pool.alloc(64)
if bb_buf_g is None: return result
viperlib.snprintf(bb_buf_g, 64, "eh.err.%d", lc_g)
err_bb_g: mlirlite.BasicBlock* = mlirlite.create_block(pool, cur_func_g, bb_buf_g)
viperlib.snprintf(bb_buf_g, 64, "eh.ok.%d", lc_g)
ok_bb_g: mlirlite.BasicBlock* = mlirlite.create_block(pool, cur_func_g, bb_buf_g)
if err_bb_g is None or ok_bb_g is None: return result
mlirlite.build_cond_br(builder, is_err_g, err_bb_g, ok_bb_g)
# err block
mlirlite.position_at_end(builder, err_bb_g)
if trans._eh_dispatch_bb is not None:
# inside try: the exception is already in try's alloca (code_ptr);
# branch to DispatchBB to hand off to the except chain.
mlirlite.build_br(builder, trans._eh_dispatch_bb)
else:
# not in try: the exception is in this function's out-param (reused) or a
# local alloca; just return the default and let the caller check our out-param.
if mlirlite.builder_cur_block_is_terminated(builder) == 0:
ret_ty_g2: mlirlite.LLVMType* = mlirlite.function_get_ret_ty(cur_func_g)
if ret_ty_g2 is not None:
match ret_ty_g2:
case mlirlite.LLVMType.Void():
mlirlite.build_ret_void(builder)
case mlirlite.LLVMType.Ptr(_pg):
nv_g: mlirlite.Value* = mlirlite.ConstNull(pool, ret_ty_g2, "null")
if nv_g is not None: mlirlite.build_ret(builder, nv_g)
case mlirlite.LLVMType.Float(_fg):
zf_g: mlirlite.Value* = mlirlite.ConstFloat(pool, ret_ty_g2, 0.0)
if zf_g is not None: mlirlite.build_ret(builder, zf_g)
case _:
zv_g: mlirlite.Value* = mlirlite.ConstZero(pool, ret_ty_g2)
if zv_g is not None: mlirlite.build_ret(builder, zv_g)
# ok block: continue
mlirlite.position_at_end(builder, ok_bb_g)
return result
# isinstance - runtime vtable subclass-set comparison: precompute the class's known
# subclasses via reverse BFS, then compare obj->vtable against their vtable addresses.
def _translate_isinstance(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, obj_node: ast.AST*, cls_node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*:
"""isinstance(obj, Cls): runtime vtable subclass-set comparison; returns i1"""
if obj_node is None or cls_node is None:
# missing args is a call error: fail fast, never hide it by returning True
self.Gen.TypeGenErr.fatal_error(cls_node, "isinstance() argument type inference failed", "TypeError")
obj_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, obj_node, None, 0, trans)
if obj_val is None or obj_val.Ty is None:
# obj type inference failed: fail fast, never hide it by returning True
self.Gen.TypeGenErr.fatal_error(cls_node, "isinstance() argument type inference failed", "TypeError")
# normalize the object pointer: load through a pointer-to-pointer (var alloca / Class*)
obj_ptr: mlirlite.Value* = obj_val
match obj_val.Ty:
case mlirlite.LLVMType.Ptr(inner_ty):
match inner_ty:
case mlirlite.LLVMType.Ptr(_):
loaded: mlirlite.Value* = mlirlite.build_load(builder, inner_ty, obj_val)
if loaded is not None: obj_ptr = loaded
case _:
pass
obj_is_ptr: int = self.Gen.ExprGenBase.is_ptr_type(obj_ptr.Ty)
i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty)
i8_ptr_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ptr_ty)
# read the object vtable pointer lazily (once, at the first CVTable comparison)
vtable_ptr: mlirlite.Value* = None
vtable_ready: int = 0
res: mlirlite.Value* = None
cls_names: sizet* = self._extract_isinstance_class_names(pool, cls_node)
if cls_names is None:
# class-name extraction failed: fail fast, never hide it by returning True
self.Gen.TypeGenErr.fatal_error(cls_node, "isinstance() argument type inference failed", "TypeError")
ci: int = 0
while cls_names[ci] != 0:
cn: str = rawptr(cls_names[ci])
if cn is not None:
if self.Gen.StructGen.is_novtable_by_name(cn) != 0 or self.Gen.StructGen.has_vtable_by_name(cn) == 0:
# NoVTable / unknown class -> compile-time check (obj static-type MRO)
st: int = self._isinstance_compile_time(pool, obj_ptr, cn)
c1: mlirlite.Value* = mlirlite.const_int(pool, 1, 1 if st > 0 else 0)
if res is None: res = c1
else: res = mlirlite.build_or(builder, res, c1)
elif obj_is_ptr != 0:
# CVTable -> runtime vtable compare (read the object header vtable pointer)
if vtable_ready == 0:
vtable_slot: mlirlite.Value* = mlirlite.build_bitcast(builder, obj_ptr, i8_ptr_ptr_ty)
if vtable_slot is not None: vtable_ptr = mlirlite.build_load(builder, i8_ptr_ty, vtable_slot)
vtable_ready = 1
if vtable_ptr is not None:
subs: sizet* = self._collect_subclass_vtable_classes(pool, cn)
if subs is not None:
k: int = 0
while subs[k] != 0:
scn: str = rawptr(subs[k])
if scn is not None:
sub_i8: mlirlite.Value* = self._make_vtable_ref_i8(pool, builder, mod, scn, trans)
if sub_i8 is not None:
cmp: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_EQ, vtable_ptr, sub_i8)
if cmp is not None:
if res is None: res = cmp
else: res = mlirlite.build_or(builder, res, cmp)
k += 1
ci += 1
if res is None:
# no comparison produced (obj non-pointer, all classes CVTable): fail fast
self.Gen.TypeGenErr.fatal_error(cls_node, "isinstance() argument type inference failed", "TypeError")
return res
def _extract_isinstance_class_names(self, pool: memhub.MemBuddy*, node: ast.AST*) -> sizet*:
"""Extract the class-name array from isinstance's second argument (Name/Attribute/Tuple)"""
arr: sizet* = pool.alloc(8 * 16)
if arr is None: return None
# zero the whole array: pool memory may be non-zero, so uninitialized slots would
# be dereferenced as class-name pointers by the while cls_names[ci]!=0 loop.
string.memset(arr, 0, 8 * 16)
arr[0] = 0
if node is None: return arr
count: int = 0
k: int = node.kind()
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm.id is not None: arr[0] = sizet(nm.id)
elif k == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(node)
if at.attr is not None: arr[0] = sizet(at.attr)
elif k == ast.ASTKind.Tuple:
tp: ast.Tuple* = ast.Tuple*(node)
elts: list[ast.AST*]* = tp.elts
if elts is not None:
n: sizet = len(elts)
for i in range(int(n)):
if count >= 15: break
elt: ast.AST* = elts.get(i)
if elt is None: continue
ek: int = elt.kind()
if ek == ast.ASTKind.Name:
enm: ast.Name* = ast.Name*(elt)
if enm.id is not None:
arr[count] = sizet(enm.id)
count += 1
elif ek == ast.ASTKind.Attribute:
eat: ast.Attribute* = ast.Attribute*(elt)
if eat.attr is not None:
arr[count] = sizet(eat.attr)
count += 1
arr[count] = 0
return arr
def _collect_subclass_vtable_classes(self, pool: memhub.MemBuddy*, class_name: str) -> sizet*:
"""Reverse BFS collect the target class and all subclasses (0-terminated array)"""
if class_name is None: return None
names: sizet* = pool.alloc(8 * (CallGenBase.ISINSTANCE_CLASS_MAX + 1))
queue: sizet* = pool.alloc(8 * CallGenBase.ISINSTANCE_CLASS_MAX)
visited: sizet* = pool.alloc(8 * CallGenBase.ISINSTANCE_CLASS_MAX)
if names is None or queue is None or visited is None: return None
string.memset(names, 0, 8 * (CallGenBase.ISINSTANCE_CLASS_MAX + 1))
string.memset(queue, 0, 8 * CallGenBase.ISINSTANCE_CLASS_MAX)
string.memset(visited, 0, 8 * CallGenBase.ISINSTANCE_CLASS_MAX)
qcount: int = 1
outcount: int = 0
vcount: int = 0
queue[0] = sizet(class_name) # uncertain
while qcount > 0:
qcount -= 1
cur_addr: sizet = queue[qcount]
cur: str = rawptr(cur_addr)
if cur is None: continue
dup: int = 0
for vi in range(int(vcount)):
va: sizet = visited[vi]
if va != 0:
vn: str = rawptr(va)
if vn is not None and string.strcmp(vn, cur) == 0:
dup = 1
break
if dup != 0: continue
if vcount < CallGenBase.ISINSTANCE_CLASS_MAX:
visited[vcount] = sizet(cur)
vcount += 1
centry: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(cur)
if centry is not None and centry.HasVTable != 0:
if outcount < CallGenBase.ISINSTANCE_CLASS_MAX:
names[outcount] = sizet(cur)
outcount += 1
# enqueue subclasses: full scan over registered structs whose ParentNames
# contain cur (by ordinal, not by name -- do not switch to find_struct).
scnt: int = self.Gen.StructGen.get_struct_count()
for i in range(int(scnt)):
e2: GenTypes.StructEntry* = self.Gen.StructGen._get_struct_entry(i)
if e2 is None or e2.Name is None: continue
if e2.ParentCount > 0 and e2.ParentNames is not None:
parr: sizet* = (sizet*)(e2.ParentNames)
for j in range(int(e2.ParentCount)):
pa: sizet = parr[j]
if pa != 0:
pn: str = rawptr(pa)
if pn is not None and string.strcmp(pn, cur) == 0:
if qcount < CallGenBase.ISINSTANCE_CLASS_MAX:
queue[qcount] = sizet(e2.Name)
qcount += 1
break
names[outcount] = 0
return names
def _make_vtable_ref_i8(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, class_name: str, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Build a class vtable address reference (i8*): defined here directly, external cross-module"""
if class_name is None: return None
i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty)
buf: str = pool.alloc(256)
if buf is None: return None
viperlib.snprintf(buf, 256, "%s.vtable", class_name)
vn: str = rawptr(buf)
sha1: str = self.Gen.StructGen.get_struct_sha1(class_name)
if sha1 is None:
if trans is None: return None
sha1 = trans.ModuleSha1
if sha1 is None: return None
mangled: str = self.Gen.FuncGenBase._mangle_name_with_sha1(sha1, vn)
ref_name: str = pool.alloc(256)
if ref_name is None: return None
# vtable names carry the sha1 dot prefix, so always use the @"..." quoted form
viperlib.snprintf(ref_name, 256, "@\"%s\"", mangled)
gv: mlirlite.GlobalVariable* = self.Gen.ExprGenAttr.find_global_in_module(mod, mangled)
ref_ty: mlirlite.LLVMType* = None
if gv is not None:
# vtable defined in this module (constant {i8* x N}) -> reference type Ptr(struct)
ref_ty = mlirlite.Ptr(pool, gv.Ty)
else:
# cross-module: declare @mangled = external global i8*, reference type i8**
gv2: mlirlite.GlobalVariable* = mlirlite.new_global_variable(pool, mangled, i8_ptr_ty)
if gv2 is None: return None
mlirlite.module_add_global(mod, gv2)
ref_ty = mlirlite.Ptr(pool, i8_ptr_ty)
ref: mlirlite.Value* = mlirlite.SSAValue(pool, ref_ty, ref_name)
if ref is None: return None
return mlirlite.build_bitcast(builder, ref, i8_ptr_ty)
def _isinstance_compile_time(self, pool: memhub.MemBuddy*, obj_ptr: mlirlite.Value*, class_name: str) -> int:
"""Static check: whether obj's static type equals class_name or lists it in its MRO"""
if obj_ptr is None or obj_ptr.Ty is None or class_name is None: return 0
match obj_ptr.Ty:
case mlirlite.LLVMType.Ptr(obj_struct_ty):
sname: str = self.Gen.StructGen.get_class_name_by_type(pool, obj_struct_ty)
if sname is not None:
if string.strcmp(sname, class_name) == 0: return 1
mro: sizet* = self.Gen.StructGen.get_mro_chain(pool, sname)
if mro is not None:
k: int = 0
while mro[k] != 0:
mn: str = rawptr(mro[k])
if mn is not None and string.strcmp(mn, class_name) == 0: return 1
k += 1
case _:
pass
return 0
# P4 cluster 5+6 migrated methods (bodies from HandlesExprCall.vp: closure call /
# function pointer / printf / print / stdarg sugar / varargs promotion / func tables).
def translate_closure_call(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, closure_alloca: mlirlite.Value*, cargs: list[ast.AST*]*, can: sizet, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Indirect call through a closure pointer (supports parameterized lambdas)"""
if builder is None or closure_alloca is None: return None
i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty)
i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
# 1. load the closure pointer (i8*)
closure_ptr: mlirlite.Value* = mlirlite.build_load(builder, i8_ptr_ty, closure_alloca)
if closure_ptr is None: return None
# 2. load fn_ptr (offset 0): bitcast to i8**, load i8*
fn_ptr_addr: mlirlite.Value* = mlirlite.build_bitcast(builder, closure_ptr, mlirlite.Ptr(pool, i8_ptr_ty))
if fn_ptr_addr is None: return None
fn_raw: mlirlite.Value* = mlirlite.build_load(builder, i8_ptr_ty, fn_ptr_addr)
if fn_raw is None: return None
# 3. load env_ptr (offset 8): GEP i8 closure 8; load i8*
eight_val: mlirlite.Value* = mlirlite.const_int32(pool, 8)
env_addr: mlirlite.Value* = mlirlite.build_gep(builder, i8_ty, closure_ptr, eight_val)
if env_addr is None: return None
env_ptr: mlirlite.Value* = mlirlite.build_load(builder, i8_ptr_ty, env_addr)
if env_ptr is None: return None
# 4. build args: env head + arg tail (translate args for a parameterized lambda)
total_args: int = 1
arg_head: mlirlite.Value* = env_ptr
arg_tail: mlirlite.Value* = env_ptr
param_node: mlirlite.ParamNode* = mlirlite.new_param_node(pool, i8_ptr_ty)
pn_tail: mlirlite.ParamNode* = param_node
if can > 0 and cargs is not None and trans is not None:
for ai in range(int(can)):
arg: ast.AST* = cargs.get(ai)
if arg is None: continue
arg_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg, None, 0, trans)
if arg_val is None or arg_val.Ty is None: continue
arg_val.Next = None
if arg_tail is not None: arg_tail.Next = arg_val
arg_tail = arg_val
pn_new: mlirlite.ParamNode* = mlirlite.new_param_node(pool, arg_val.Ty)
if pn_new is not None:
pn_tail.Next = pn_new
pn_tail = pn_new
total_args += 1
if total_args == 1: env_ptr.Next = None
# 5. bitcast fn_raw to the function-pointer type i32(i8*, [args...])*
func_ty: mlirlite.LLVMType* = mlirlite.Func(pool, i32_ty, param_node, total_args)
func_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, func_ty)
fn_typed: mlirlite.Value* = mlirlite.build_bitcast(builder, fn_raw, func_ptr_ty)
if fn_typed is None: return None
# 6. indirect call: call i32 %fn_typed(env, args...)
return mlirlite.build_call_indirect(builder, fn_typed, arg_head, total_args, i32_ty)
def _is_func_ptr_alloca(self, alloca: mlirlite.Value*) -> int:
"""Return whether an alloca is a function-pointer variable (type Ptr(Ptr(i8))); 1 = yes"""
if alloca is None or alloca.Ty is None: return 0
if self.Gen.ExprGenBase.is_ptr_type(alloca.Ty) == 0: return 0
pointee: mlirlite.LLVMType* = alloca.Ty.Pointee
if pointee is None or self.Gen.ExprGenBase.is_ptr_type(pointee) == 0: return 0
inner: mlirlite.LLVMType* = pointee.Pointee
if inner is None or self.Gen.ExprGenBase.is_ptr_type(inner) != 0: return 0
return 1
def _ensure_closure_names(self) -> int:
"""Ensure the closure-name table is allocated; returns 0 on failure"""
if self.ClosureNames is not None: return 1
self.ClosureNames = CompilerFactory.factory.PermPool.alloc(64 * CallGenBase.MAX_CLOSURE_NAMES)
if self.ClosureNames is None: return 0
string.memset(self.ClosureNames, 0, 64 * CallGenBase.MAX_CLOSURE_NAMES)
return 1
def register_closure_name(self, name: str) -> int:
"""Register a nested function (closure variable) name; returns 0 on success"""
if name is None: return 1
if self._ensure_closure_names() == 0: return 1
if self.ClosureCount >= CallGenBase.MAX_CLOSURE_NAMES: return 1
idx: sizet = sizet(self.ClosureCount) * 64
string.strncpy(self.ClosureNames + idx, name, 63)
self.ClosureCount += 1
return 0
def is_closure_name(self, name: str) -> int:
"""Return whether name is registered as a closure variable name (1 = yes, 0 = no)"""
if name is None or self.ClosureNames is None: return 0
i: sizet
for i in range(int(self.ClosureCount)):
if string.strcmp(self.ClosureNames + i * 64, name) == 0: return 1
return 0
def _ensure_funcptr_sigs(self) -> int:
"""Ensure the function-pointer signature table is allocated (lazy stdlib.malloc)"""
if self.FuncptrNames is not None: return 1
self.FuncptrNames = CompilerFactory.factory.PermPool.alloc(64 * CallGenBase.MAX_FUNC_PTR_SIGS)
self.FuncptrRets = CompilerFactory.factory.PermPool.alloc(8 * CallGenBase.MAX_FUNC_PTR_SIGS)
if self.FuncptrNames is None or self.FuncptrRets is None: return 0
string.memset(self.FuncptrNames, 0, 64 * CallGenBase.MAX_FUNC_PTR_SIGS)
return 1
def _func_ptr_sig_fatal(self, pool: memhub.MemBuddy*, node: ast.AST*, name: str, ret_ty: mlirlite.LLVMType*, reason: str) -> None:
"""Fail compilation when a function-pointer signature registration cannot be honored"""
cap: int = CallGenBase.MAX_FUNC_PTR_SIGS
ret_str: str = "?"
if ret_ty is not None:
typed_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_LEAFKIT_539)
if typed_buf is not None:
mlirlite.TypePrint(typed_buf, 4096, ret_ty, pool)
if typed_buf[0] != '\0': ret_str = typed_buf
msg_buf: str = pool.alloc(1024)
if msg_buf is not None:
viperlib.snprintf(msg_buf, 1024, "Function pointer signature table registration failed: variable '%s' (return type %s) -- %s. " "Current limit MAX_FUNC_PTR_SIGS=%d, %d entries registered. " "Duplicate registration with the same name and return type hits in place and does not occupy a new slot; " "if more function pointer variables are really needed, raise the " "MAX_FUNC_PTR_SIGS (the single repo-wide source; changing this one place takes effect everywhere)", name, ret_str, reason, cap, self.FuncptrCount)
self.Gen.TypeGenErr.fatal_error(node, msg_buf, "TypeError")
return
self.Gen.TypeGenErr.fatal_error(node, "Function pointer signature table registration failed (table full or variable name conflict)", "TypeError")
def register_func_ptr_sig(self, pool: memhub.MemBuddy*, name: str, ret_ty: mlirlite.LLVMType*, node: ast.AST* = None) -> int:
"""Register a function-pointer variable's return type; 0 = success, 1 = failed (already reported)"""
if name is None or ret_ty is None: return 1
if self._ensure_funcptr_sigs() == 0:
self._func_ptr_sig_fatal(pool, node, name, ret_ty, "Signature table allocation failed (PermPool out of memory)")
return 1
# idempotence/conflict check: linear scan of registered bare names (cap 64, cheap)
i: sizet
for i in range(int(self.FuncptrCount)):
idx_hit: sizet = sizet(i) * 64
if string.strcmp(self.FuncptrNames + idx_hit, name) == 0:
prev_ret: mlirlite.LLVMType* = (mlirlite.LLVMType*)(self.FuncptrRets[i])
if prev_ret is not None and self.Gen.AssignGen._stg_ty_equal(pool, prev_ret, ret_ty) != 0: return 0
self._func_ptr_sig_fatal(pool, node, name, ret_ty, "This name is already registered with a different return type")
return 1
if self.FuncptrCount >= CallGenBase.MAX_FUNC_PTR_SIGS:
self._func_ptr_sig_fatal(pool, node, name, ret_ty, "Signature table is full")
return 1
idx: sizet = sizet(self.FuncptrCount) * 64
# slot is 64 bytes fixed; use strncpy clamping like register_closure_name
string.strncpy(self.FuncptrNames + idx, name, 63)
# ret_ty is a caller pool-allocated LLVMType that dangles after the per-file reset;
# deep-copy into perm_pool (lookup_func_ptr_ret reads it across files).
ret_copy_fp: mlirlite.LLVMType* = mlirlite.DeepCopyType(CompilerFactory.factory.PermPool, ret_ty)
if ret_copy_fp is not None: self.FuncptrRets[self.FuncptrCount] = uint64(ret_copy_fp)
else: self.FuncptrRets[self.FuncptrCount] = uint64(ret_ty)
self.FuncptrCount += 1
return 0
def lookup_func_ptr_ret(self, name: str) -> mlirlite.LLVMType*:
"""Look up a function-pointer variable's return type; None = unregistered (defaults to i32)"""
if name is None or self.FuncptrNames is None: return None
for i in range(int(self.FuncptrCount)):
idx: sizet = sizet(i) * 64
if string.strcmp(self.FuncptrNames + idx, name) == 0: return (mlirlite.LLVMType*)(self.FuncptrRets[i])
return None
def _translate_func_ptr_call(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, fp_alloca: mlirlite.Value*, func_name: str, cargs: list[ast.AST*]*, can: sizet, trans: HT.Translator*) -> mlirlite.Value*:
"""Indirect call through a function-pointer variable; returns the result Value"""
if builder is None or fp_alloca is None: return None
i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty)
i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
# 1. load the function address (i8*)
fn_i8: mlirlite.Value* = mlirlite.build_load(builder, i8_ptr_ty, fp_alloca)
if fn_i8 is None: return None
# 2. translate args and link them into a Value list
head: mlirlite.Value* = None
tail: mlirlite.Value* = None
arg_count: int = 0
ai: sizet
for ai in range(int(can)):
arg: ast.AST* = cargs.get(ai)
if arg is not None:
arg_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg, None, 0, trans)
if arg_val is not None:
arg_val.Next = None
if head is None: head = arg_val
else: tail.Next = arg_val
tail = arg_val
arg_count += 1
# 3. build the signature from arg types + registered ret type (missing = fail fast)
ret_ty: mlirlite.LLVMType* = self.lookup_func_ptr_ret(func_name)
if ret_ty is None:
self.Gen.TypeGenErr.fatal_error(None, f"Return type of function pointer variable '{func_name}' is not registered (t.Callable annotation missing)", "TypeError")
ret_ty = i32_ty
fp_first: mlirlite.ParamNode* = None
fp_prev: mlirlite.ParamNode* = None
cur_v: mlirlite.Value* = head
for pi in range(int(arg_count)):
if cur_v is None: break
cur_ty: mlirlite.LLVMType* = mlirlite.value_get_ty(cur_v)
pn2: mlirlite.ParamNode* = mlirlite.new_param_node(pool, cur_ty)
if pn2 is not None:
if fp_first is None: fp_first = pn2
if fp_prev is not None: mlirlite.paramnode_set_next(fp_prev, pn2)
fp_prev = pn2
cur_v = mlirlite.value_get_next(cur_v)
func_ty: mlirlite.LLVMType* = mlirlite.Func(pool, ret_ty, fp_first, arg_count)
if func_ty is None: return None
func_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, func_ty)
# 4. bitcast i8* to the function-pointer type and call indirectly
fn_typed: mlirlite.Value* = mlirlite.build_bitcast(builder, fn_i8, func_ptr_ty)
if fn_typed is None: return None
return mlirlite.build_call_indirect(builder, fn_typed, head, arg_count, ret_ty)
def translate_printf_call(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator* = None) -> None:
"""Translate a printf call (special-cases the format string)"""
cargs: list[ast.AST*]* = cl.args
if cargs is None: return
can: sizet = len(cargs)
if can == 0: return
arg0: ast.AST* = cargs.get(0)
if arg0 is None: return
fmt_str: str = None
arg0_kind: int = arg0.kind()
if arg0_kind == ast.ASTKind.Constant:
cn: ast.Constant* = ast.Constant*(arg0)
if cn.const_kind == ast.CONST_STR: fmt_str = cn.str_val
if fmt_str is None: return
fmt_ptr: mlirlite.Value* = self.Gen.ExprGenConst.create_global_string(builder, pool, mod, fmt_str, trans)
if fmt_ptr is None: return
i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
# translate extra args and link them
prev: mlirlite.Value* = fmt_ptr
for ai in range(1, int(can)):
arg: ast.AST* = cargs.get(ai)
if arg is not None:
arg_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg, None, 0, trans)
if arg_val is not None:
# C ABI integer promotion: i8/i16 -> i32 (required by printf varargs)
arg_bits: int = self.Gen.ExprGenBase.get_llvm_type_bits(arg_val.Ty)
if arg_bits != 0 and arg_bits < 32: arg_val = self.Gen.ExprGenBase.coerce_to_type(builder, arg_val, i32_ty, arg)
# C ABI float promotion: float -> double (required by printf varargs)
arg_fbits: int = self.Gen.ExprGenBase.get_llvm_float_bits(arg_val.Ty)
if arg_fbits != 0 and arg_fbits < 64:
double_ty: mlirlite.LLVMType* = mlirlite.Double(pool)
arg_val = self.Gen.ExprGenBase.coerce_to_type(builder, arg_val, double_ty, arg)
arg_val.Next = None
prev.Next = arg_val
prev = arg_val
prev.Next = None
# is_variadic=1: emit a call with an explicit function type (e.g. call i32 (i8*, ...)),
# so llc treats it as variadic and generates correct Win64 ABI code.
mlirlite.build_call(builder, "printf", fmt_ptr, can, i32_ty, 1)
def translate_print_call(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator* = None) -> None:
"""Translate a print call (type-aware formatting + trailing newline)"""
cargs: list[ast.AST*]* = cl.args
if cargs is None: return
FMT_BUF_SIZE: sizet = 256
fmt_buf: bytes = stdlib.malloc(FMT_BUF_SIZE)
if fmt_buf is None: return
fmt_buf[0] = '\0'
i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
i64_ty: mlirlite.LLVMType* = mlirlite.Int64(pool)
dbl_ty: mlirlite.LLVMType* = mlirlite.Double(pool)
can: sizet = len(cargs)
arg_head: mlirlite.Value* = None
arg_tail: mlirlite.Value* = None
arg_count: int = 1 # the format string is printf arg 1
for ai in range(int(can)):
arg: ast.AST* = cargs.get(ai)
if arg is None: continue
arg_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg, None, 0, trans)
if arg_val is None or arg_val.Ty is None:
# fail fast: a failed print-arg translation must abort, never write a
# misleading format string that hides a compiler bug as a data problem.
VLogger.error(f"print argument translation failed (expression returned None): [arg={ai}]", "PRINT")
self.Gen.TypeGenErr.fatal_error(arg, "print argument translation failed (expression returned None)", "TypeError")
return
# object defines __str__ -> print its i8* return via %s (TPC-aligned)
str_ov_pr: mlirlite.Value* = self.Gen.ExprGenOps._try_named_dunder(pool, builder, mod, arg_val, "__str__", trans)
if str_ov_pr is not None: arg_val = str_ov_pr
# array name (e.g. print(l), l: t.CArray[char, N]): use the array alloca address
# per decay semantics -- char array -> %s, other element types -> 0x%llx.
array_ptr: mlirlite.Value* = None
array_elem_bits: int = 0
if arg.kind() == ast.ASTKind.Name and trans is not None:
anm: ast.Name* = ast.Name*(arg)
if anm.id is not None:
arr_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, anm.id)
if arr_alloca is not None and arr_alloca.Ty is not None and self.Gen.ExprGenBase.is_ptr_type(arr_alloca.Ty) != 0:
arr_pointee: mlirlite.LLVMType* = arr_alloca.Ty.Pointee
if arr_pointee is not None:
match arr_pointee:
case mlirlite.LLVMType.Array(arr_et, arr_n):
array_ptr = arr_alloca
array_elem_bits = self.Gen.ExprGenBase.get_llvm_type_bits(arr_et)
case _:
pass
token: str = None
chain_arg: int = 1 # 1 = this token consumes an arg, 0 = text only
arg_bits: int = self.Gen.ExprGenBase.get_llvm_type_bits(arg_val.Ty)
arg_fbits: int = self.Gen.ExprGenBase.get_llvm_float_bits(arg_val.Ty)
if array_ptr is not None:
# array decay: char array -> %s (bitcast [N x i8]* to i8*); else 0x%llx
if array_elem_bits == 8:
i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool))
token = "%s"
arg_val = mlirlite.build_bitcast(builder, array_ptr, i8_ptr_ty)
else:
token = "0x%llx"
arg_val = mlirlite.build_ptrtoint(builder, array_ptr, i64_ty)
elif self.Gen.ExprGenBase.is_ptr_type(arg_val.Ty) != 0:
# i8* (str/bytes) -> %s; other pointers -> 0x%llx
pointee: mlirlite.LLVMType* = arg_val.Ty.Pointee
if pointee is not None and self.Gen.ExprGenBase.get_llvm_type_bits(pointee) == 8: token = "%s"
else:
token = "0x%llx"
arg_val = mlirlite.build_ptrtoint(builder, arg_val, i64_ty)
elif arg_bits != 0:
if arg_bits == 1:
token = "%d"
arg_val = mlirlite.build_zext(builder, arg_val, i32_ty)
elif arg_bits == 8:
token = "%d"
arg_val = mlirlite.build_sext(builder, arg_val, i32_ty)
elif arg_bits == 16:
token = "%d"
arg_val = mlirlite.build_sext(builder, arg_val, i32_ty)
elif arg_bits == 32: token = "%d"
else: token = "%lld"
elif arg_fbits != 0:
token = "%f"
if arg_fbits < 64: arg_val = mlirlite.build_fpext(builder, arg_val, dbl_ty)
else:
# struct value / array / function -> "(unknown)", no arg passed
token = "(unknown)"
chain_arg = 0
# append the format token
if string.strlen(fmt_buf) < FMT_BUF_SIZE - 16: viperlib.snprintf(fmt_buf + string.strlen(fmt_buf), 16, "%s", token)
# link the argument
if chain_arg != 0:
arg_val.Next = None
if arg_tail is None: arg_head = arg_val
else: arg_tail.Next = arg_val
arg_tail = arg_val
arg_count += 1
# append the default newline (Python print end="\n")
if string.strlen(fmt_buf) < FMT_BUF_SIZE - 2: viperlib.snprintf(fmt_buf + string.strlen(fmt_buf), 2, "\n")
fmt_ptr: mlirlite.Value* = self.Gen.ExprGenConst.create_global_string(builder, pool, mod, fmt_buf, trans)
stdlib.free(fmt_buf)
if fmt_ptr is None: return
fmt_ptr.Next = arg_head
# is_variadic=1: with an explicit function type (C ABI varargs call)
mlirlite.build_call(builder, "printf", fmt_ptr, arg_count, i32_ty, 1)
def init_func_table(self, pool: memhub.MemBuddy*, max_count: int) -> GenTypes.FuncEntry*:
"""Allocate and zero a func table array"""
size: sizet = max_count * GenTypes.FuncEntry.__sizeof__()
funcs_ptr: GenTypes.FuncEntry* = pool.alloc(size)
if funcs_ptr is not None: string.memset(funcs_ptr, 0, size)
return funcs_ptr
def find_func_in_table(self, funcs: GenTypes.FuncEntry*, func_count: int, name: str, owner_sha1: str = None) -> mlirlite.Function*:
"""Look up a function by name in a func table, preferring this module's entry when owner_sha1 is set"""
if name is None or funcs is None: return None
entry_size: sizet = GenTypes.FuncEntry.__sizeof__()
# global func table + no owner: equivalent to "first same-name entry's Func", so use
# the name-index fast path; otherwise fall through to the linear scan below.
gfuncs_match: int = 0
if owner_sha1 is None and self.GlobalFuncs is not None:
if uint64(funcs) == uint64(self.GlobalFuncs) and func_count == self.GlobalFuncCount: gfuncs_match = 1
if gfuncs_match != 0 and self.GlobalFuncNameIdxBuilt == self.GlobalFuncCount and self.GlobalFuncNameIdx is not None and self.GlobalFuncNameIdxCap > 0:
fi_t: int = _gfn_idx_find(self.GlobalFuncs, self.GlobalFuncNameIdx, self.GlobalFuncNameIdxCap, name)
if fi_t >= 0:
fe_t: GenTypes.FuncEntry* = GenTypes.FuncEntry*((uint64(self.GlobalFuncs) + fi_t * entry_size))
if fe_t is not None: return fe_t.Func
# index trusted => a miss means "no such name"; the global table is append-only and
# never rewrites names. Otherwise fall back to the linear scan below.
return None
# Single pass: record the first same-bare-name entry as fallback; return this module's entry early.
fallback_entry: GenTypes.FuncEntry* = None
for i in range(int(func_count)):
entry_addr: uint64 = uint64(funcs) + i * entry_size
entry: GenTypes.FuncEntry* = GenTypes.FuncEntry*(entry_addr)
if entry is not None and entry.Name is not None:
# fast path: equal names imply equal first chars, so skip strcmp when the
# first chars differ (avoids a function call per table slot).
if entry.Name[0] == name[0] and string.strcmp(entry.Name, name) == 0:
if fallback_entry is None: fallback_entry = entry
if entry.Func is not None and owner_sha1 is not None:
fn_nm_o: str = mlirlite.function_get_name(entry.Func)
if fn_nm_o is not None:
if string.strncmp(fn_nm_o, owner_sha1, 16) == 0 and fn_nm_o[16] == '.': return entry.Func
if fallback_entry is not None: return fallback_entry.Func
return None
def add_func_to_table(self, funcs: GenTypes.FuncEntry*, func_count: int, name: str, func: mlirlite.Function*, max_count: int, defaults: t.CPtr = None, default_count: int = 0, param_count: int = 0, pool: memhub.MemBuddy* = None, def_sha1: str = None) -> int:
"""Add a function to a func table; returns 0 on success"""
if name is None or func is None or funcs is None: return 1
if func_count >= max_count: return 1
entry_size: sizet = GenTypes.FuncEntry.__sizeof__()
entry_addr: uint64 = uint64(funcs) + func_count * entry_size
entry: GenTypes.FuncEntry* = GenTypes.FuncEntry*(entry_addr)
entry.Name = name
entry.Func = func
entry.Defaults = defaults
entry.DefaultCount = default_count
entry.ParamCount = param_count
# [S3 dual-write] Spec is filled by update_global_func_spec after params are added
entry.Spec = None
# also register into the global func table (for cross-module vtable init lookup)
self._register_global_func(name, func, pool)
# also register into the global defaults table (to fill defaults on cross-module calls)
if default_count > 0: self._register_global_defaults(name, defaults, default_count, param_count, def_sha1)
return 0
def _init_global_func_table(self) -> int:
"""Initialize the global func table (stdlib.malloc, lives for the whole program)"""
if self.GlobalFuncs is not None: return 1
size: sizet = CallGenBase.GLOBAL_FUNC_MAX * GenTypes.FuncEntry.__sizeof__()
self.GlobalFuncs = CompilerFactory.factory.PermPool.alloc(size)
if self.GlobalFuncs is None: return 0
string.memset(self.GlobalFuncs, 0, size)
self.GlobalFuncCount = 0
# name index (PermPool, same lifetime as the table). On alloc failure keep it
# untrusted (Built=-1) and fall back to linear scans; init still succeeds.
idx_bytes: sizet = sizet(GLOBAL_FUNC_IDX_CAP) * 8
idx_buf: sizet* = CompilerFactory.factory.PermPool.alloc(idx_bytes)
if idx_buf is None:
self.GlobalFuncNameIdx = None
self.GlobalFuncNameIdxCap = 0
self.GlobalFuncNameIdxBuilt = -1
else:
string.memset(idx_buf, 0, idx_bytes)
self.GlobalFuncNameIdx = idx_buf
self.GlobalFuncNameIdxCap = GLOBAL_FUNC_IDX_CAP
self.GlobalFuncNameIdxBuilt = 0
return 1
def _register_global_func(self, name: str, func: mlirlite.Function*, pool: memhub.MemBuddy* = None) -> int:
"""Register a function in the global table, skipping duplicates; returns 0 on success"""
if name is None or func is None: return 1
if self._init_global_func_table() == 0: return 1
# skip if already present (avoid duplicates)
if self.find_func_in_table(self.GlobalFuncs, self.GlobalFuncCount, name) is not None: return 0
if self.GlobalFuncCount >= CallGenBase.GLOBAL_FUNC_MAX: return 1
# write directly into the global table (no add_func_to_table, to avoid recursion)
entry_size: sizet = GenTypes.FuncEntry.__sizeof__()
entry_addr: uint64 = uint64(self.GlobalFuncs) + self.GlobalFuncCount * entry_size
entry: GenTypes.FuncEntry* = GenTypes.FuncEntry*(entry_addr)
# deep-copy Name into stdlib: the global table outlives files, and pool names would
# dangle after the per-file reset_to(mark1) (strcmp crash after find_func_global).
name_len: sizet = string.strlen(name)
name_copy: str = CompilerFactory.factory.PermPool.alloc(name_len + 1)
if name_copy is None: return 1
string.strcpy(name_copy, name)
entry.Name = name_copy
entry.Func = func
# [S3 dual-write] Spec is filled later by update_global_func_spec (params not added yet)
entry.Spec = None
# Spec is not built here: params are added after add_func_to_table, so the caller
# must call update_global_func_spec to complete it.
self.GlobalFuncCount += 1
# sync the name index (slot = new entry index) only if it was trusted before the
# insert (Built==Count-1) and the insert succeeded; otherwise mark untrusted.
if self.GlobalFuncNameIdxBuilt == self.GlobalFuncCount - 1 and _gfn_idx_insert(self.GlobalFuncNameIdx, self.GlobalFuncNameIdxCap, self.GlobalFuncCount - 1, name_copy) == 0:
self.GlobalFuncNameIdxBuilt = self.GlobalFuncCount
else: self.GlobalFuncNameIdxBuilt = -1
return 0
def update_global_func_spec(self, name: str, func: mlirlite.Function*) -> int:
"""Update a global table entry's Spec after all its params are added"""
if name is None or func is None: return 1
entry: GenTypes.FuncEntry* = self.find_func_entry_global(name)
if entry is None: return 1
entry.Spec = self._build_func_spec(func)
return 0
# _build_func_spec: build the full-signature TypeSpec (Kind=TS_FUNC) from a function after all
# params are added; unlike SpecFromLLVMType's Func branch it carries func.IsVarArg.
def _build_func_spec(self, func: mlirlite.Function*) -> TSpec.TypeSpec*:
"""Build the full-signature TypeSpec from a function; returns None on failure"""
if func is None: return None
arena: TSpec.TypeSpecArena* = self.Gen.TypeGenBase.ensure_spec_arena()
if arena is None: return None
ret_s: TSpec.TypeSpec* = TSpec.SpecFromLLVMType(arena, mlirlite.function_get_ret_ty(func))
if ret_s is None: return None
# pass 1: count
cnt: int = 0
cur: mlirlite.Param* = mlirlite.function_get_param_head(func)
while cur is not None and cnt < 256:
cnt += 1
cur = mlirlite.param_get_next(cur)
# pass 2: build each Spec and write into a contiguous param array
params_arr: sizet* = None
if cnt > 0:
params_arr = CompilerFactory.factory.PermPool.alloc(8 * cnt)
if params_arr is None: return None
cur = mlirlite.function_get_param_head(func)
i: int = 0
while cur is not None and i < cnt:
ps: TSpec.TypeSpec* = TSpec.SpecFromLLVMType(arena, mlirlite.param_get_ty(cur))
if ps is None: return None
slot: sizet* = (sizet*)(uint64(params_arr) + uint64(i * 8))
if slot is None: return None
slot[0] = sizet(ps)
cur = mlirlite.param_get_next(cur)
i += 1
return TSpec.NewTypeFunc(arena, ret_s, params_arr, cnt, func.IsVarArg)
def find_func_global(self, name: str) -> mlirlite.Function*:
"""Look up a name in the global func table; returns a Function pointer or None"""
if name is None or self.GlobalFuncs is None: return None
return self.find_func_in_table(self.GlobalFuncs, self.GlobalFuncCount, name)
def find_func_entry_global(self, name: str) -> GenTypes.FuncEntry*:
"""Look up a name in the global func table; returns the full FuncEntry (with Spec) or None"""
if name is None or self.GlobalFuncs is None: return None
entry_size: sizet = GenTypes.FuncEntry.__sizeof__()
# name-index fast path: when trusted, one probe returns the first same-name entry;
# a miss means none exists. If untrusted, fall back to the linear scan below.
if self.GlobalFuncNameIdxBuilt == self.GlobalFuncCount and self.GlobalFuncNameIdx is not None and self.GlobalFuncNameIdxCap > 0:
gi: int = _gfn_idx_find(self.GlobalFuncs, self.GlobalFuncNameIdx, self.GlobalFuncNameIdxCap, name)
if gi >= 0:
entry_g: GenTypes.FuncEntry* = GenTypes.FuncEntry*(uint64(self.GlobalFuncs) + gi * entry_size)
return entry_g
return None
for i in range(int(self.GlobalFuncCount)):
entry_addr: uint64 = uint64(self.GlobalFuncs) + i * entry_size
entry: GenTypes.FuncEntry* = GenTypes.FuncEntry*(entry_addr)
if entry.Name is not None:
# fast path: skip strcmp when the first chars differ (equal names imply
# equal first chars).
if entry.Name[0] == name[0] and string.strcmp(entry.Name, name) == 0: return entry
return None
def _init_global_defaults_table(self) -> int:
"""Initialize the global default-argument table"""
if self.GlobalDefaults is not None: return 1
size: sizet = CallGenBase.GLOBAL_DEFAULTS_MAX * GenTypes.FuncDefaultsEntry.__sizeof__()
self.GlobalDefaults = CompilerFactory.factory.PermPool.alloc(size)
if self.GlobalDefaults is None: return 0
string.memset(self.GlobalDefaults, 0, size)
self.GlobalDefaultsCount = 0
return 1
def _register_global_defaults(self, name: str, defaults: t.CPtr, default_count: int, param_count: int, def_sha1: str = None) -> int:
"""Register a function's default arguments in the global table; returns 0 on success"""
if name is None or defaults is None: return 1
if self._init_global_defaults_table() == 0: return 1
# qualified key = the defining module's SHA1: one entry per (name, module) removes
# cross-module same-name collisions. def_sha1 comes from the injection path.
cur_sha: str = def_sha1
if cur_sha is None and self.Gen is not None and self.Gen.TypeGenBase is not None: cur_sha = self.Gen.TypeGenBase.CurrentModuleSha1
# skip if the same name already exists for this module
if self.find_func_defaults(name, cur_sha) is not None: return 0
if self.GlobalDefaultsCount >= CallGenBase.GLOBAL_DEFAULTS_MAX: return 1
entry_size: sizet = GenTypes.FuncDefaultsEntry.__sizeof__()
entry_addr: uint64 = uint64(self.GlobalDefaults) + self.GlobalDefaultsCount * entry_size
entry: GenTypes.FuncDefaultsEntry* = GenTypes.FuncDefaultsEntry*(entry_addr)
# deep-copy Name into stdlib (the global table outlives files)
nm_len: sizet = string.strlen(name)
nm_copy: str = CompilerFactory.factory.PermPool.alloc(nm_len + 1)
if nm_copy is None: return 1
string.strcpy(nm_copy, name)
entry.Name = nm_copy
entry.ModuleSha1 = None
if cur_sha is not None:
sh_len: sizet = string.strlen(cur_sha)
sh_copy: str = CompilerFactory.factory.PermPool.alloc(sh_len + 1)
if sh_copy is not None:
string.strcpy(sh_copy, cur_sha)
entry.ModuleSha1 = sh_copy
entry.Defaults = defaults
entry.DefaultCount = default_count
entry.ParamCount = param_count
# range guard: on an abnormal default_count (corrupt vpi tree) skip snapshot
# extraction to avoid a huge alloc / out-of-bounds write into perm_pool.
if default_count > 128:
entry.Defaults = None
entry.DefaultCount = 0
return 0
# extract the constant snapshot into stdlib
if default_count > 0:
kinds: int* = CompilerFactory.factory.PermPool.alloc(default_count * 4)
ints: int64* = CompilerFactory.factory.PermPool.alloc(default_count * 8)
strs: sizet* = CompilerFactory.factory.PermPool.alloc(default_count * 8)
if kinds is not None and ints is not None and strs is not None:
string.memset(kinds, 0, default_count * 4)
string.memset(ints, 0, default_count * 8)
string.memset(strs, 0, default_count * 8)
dl: list[ast.AST*]* = (list[ast.AST*] | t.CPtr)(uint64(defaults))
di: int
for di in range(int(default_count)):
dnode: ast.AST* = dl.get(di)
if dnode is not None and dnode.kind() == ast.ASTKind.Constant:
dcn: ast.Constant* = ast.Constant*(dnode)
kinds[di] = dcn.const_kind
ints[di] = dcn.int_val
if dcn.const_kind == ast.CONST_STR and dcn.str_val is not None:
slen: sizet = string.strlen(dcn.str_val)
scopy: str = CompilerFactory.factory.PermPool.alloc(slen + 1)
if scopy is not None:
string.strcpy(scopy, dcn.str_val)
strs[di] = sizet(scopy)
elif dnode is not None and dnode.kind() == ast.ASTKind.Name:
# CDefine-valued default (e.g. share: ULONG = SHARE_READ): evaluate the
# CDefine at registration; only registered int CDefines are supported.
dnm: ast.Name* = ast.Name*(dnode)
if dnm.id is not None:
cdef_v: int64 = self.Gen.TypeGenCDef.lookup_cdefine_constant(dnm.id)
if self.Gen.TypeGenCDef.is_cdefine_found() != 0:
kinds[di] = ast.CONST_INT
ints[di] = cdef_v
elif dnode is not None and dnode.kind() == ast.ASTKind.UnaryOp:
# unary-literal default (e.g. ex3: int = -1): AST is UnaryOp over a
# Constant; evaluate only when the operand is an int Constant.
du: ast.UnaryOp* = ast.UnaryOp*(dnode)
if du.operand is not None and du.operand.kind() == ast.ASTKind.Constant:
doc: ast.Constant* = ast.Constant*(du.operand)
if doc.const_kind == ast.CONST_INT:
if du.op == ast.OpKind.USub:
kinds[di] = ast.CONST_INT
ints[di] = -doc.int_val
elif du.op == ast.OpKind.UAdd:
kinds[di] = ast.CONST_INT
ints[di] = doc.int_val
elif du.op == ast.OpKind.Invert:
kinds[di] = ast.CONST_INT
ints[di] = ~doc.int_val
entry.ConstKinds = kinds
entry.ConstInts = ints
entry.ConstStrs = strs
self.GlobalDefaultsCount += 1
return 0
def find_func_defaults(self, name: str, module_sha1: str = None) -> GenTypes.FuncDefaultsEntry*:
"""Find default-argument info by name and optional defining-module SHA1, or None"""
if name is None or self.GlobalDefaults is None: return None
entry_size: sizet = GenTypes.FuncDefaultsEntry.__sizeof__()
for i in range(int(self.GlobalDefaultsCount)):
entry_addr: uint64 = uint64(self.GlobalDefaults) + i * entry_size
entry: GenTypes.FuncDefaultsEntry* = GenTypes.FuncDefaultsEntry*(entry_addr)
if entry.Name is None: continue
if string.strcmp(entry.Name, name) != 0: continue
if module_sha1 is not None:
if entry.ModuleSha1 is None: continue
if string.strcmp(entry.ModuleSha1, module_sha1) != 0: continue
return entry
return None
def _build_const_from_snapshot(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, gd_entry: GenTypes.FuncDefaultsEntry*, di: int, trans: HT.Translator*, builder: mlirlite.IRBuilder*) -> mlirlite.Value*:
"""Rebuild the di-th default-value constant IR from the snapshot"""
if gd_entry is None or gd_entry.ConstKinds is None or gd_entry.ConstInts is None: return None
if di < 0 or di >= gd_entry.DefaultCount: return None
kind: int = gd_entry.ConstKinds[di]
ival: int64 = gd_entry.ConstInts[di]
if kind == ast.CONST_INT:
if ival > 2147483647 or ival < -2147483648: return mlirlite.const_int64(pool, ival)
return mlirlite.const_int32(pool, ival)
elif kind == ast.CONST_BOOL:
if ival != 0: return mlirlite.const_int32(pool, 1)
return mlirlite.const_int32(pool, 0)
elif kind == ast.CONST_NONE:
i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty)
return mlirlite.ConstNull(pool, i8_ptr_ty, "null")
elif kind == ast.CONST_STR:
if gd_entry.ConstStrs is not None and gd_entry.ConstStrs[di] != 0:
sv: str = rawptr(gd_entry.ConstStrs[di])
return self.Gen.ExprGenConst.create_global_string(builder, pool, mod, sv, trans)
# return None for unreconstructable values (CONST_FLOAT / complex expr); caller falls back to AST
return None
def find_func_entry_in_table(self, funcs: GenTypes.FuncEntry*, func_count: int, name: str) -> GenTypes.FuncEntry*:
"""Look up a name in a func table; returns the full FuncEntry or None"""
if name is None or funcs is None: return None
entry_size: sizet = GenTypes.FuncEntry.__sizeof__()
for i in range(int(func_count)):
entry_addr: uint64 = uint64(funcs) + i * entry_size
entry: GenTypes.FuncEntry* = GenTypes.FuncEntry*(entry_addr)
if entry.Name is not None:
# fast path: skip strcmp when the first chars differ (equal names imply equal first chars)
if entry.Name[0] == name[0] and string.strcmp(entry.Name, name) == 0: return entry
return None
def find_func_entry_in_table_owned(self, funcs: GenTypes.FuncEntry*, func_count: int, name: str, owner_sha1: str) -> GenTypes.FuncEntry*:
"""Find a func table entry by bare name and defining-module SHA1, or None"""
if name is None or funcs is None: return None
entry_size: sizet = GenTypes.FuncEntry.__sizeof__()
for i in range(int(func_count)):
entry_addr: uint64 = uint64(funcs) + i * entry_size
entry: GenTypes.FuncEntry* = GenTypes.FuncEntry*(entry_addr)
if entry is None or entry.Name is None: continue
# fast path: skip strcmp when the first chars differ (hot: this compares the
# whole table by bare name + defining-module SHA1).
if entry.Name[0] != name[0] or string.strcmp(entry.Name, name) != 0: continue
if owner_sha1 is None: return entry
if entry.Func is not None:
fn_nm_o: str = mlirlite.function_get_name(entry.Func)
if fn_nm_o is not None:
if string.strncmp(fn_nm_o, owner_sha1, 16) == 0 and fn_nm_o[16] == '.': return entry
return None
def NewExprCallHandle(self, pool: memhub.MemBuddy*, trans: HT.Translator*) -> ExprCallHandle*:
h: ExprCallHandle* = pool.alloc(ExprCallHandle.__sizeof__())
if h is None: return None
string.memset(h, 0, ExprCallHandle.__sizeof__())
h.Trans = trans
return h
# ============================================================
# ExprCallHandle - function-call handler (moved from HandlesExprCall); delegates to GenApi.
novirtual class ExprCallHandle(HT.Mixin):
"""Function-call handler: inherits the mixin to get the Trans back-pointer"""
def __init__(self, trans: HT.Translator*):
self.Trans = trans
# ============================================================
# HandleCall - translate a function call, returning the call result Value
def HandleCall(self, node: ast.AST*) -> mlirlite.Value*:
"""Translate a function call using shared state from self.Trans"""
return GenApi.translate_call(self.Trans.Pool, self.Trans._cur_builder, self.Trans.Module, node, self.Trans._funcs, self.Trans._func_count, self.Trans)
# ============================================================
# HandlePrintfCall - translate a printf call
def HandlePrintfCall(self, cl: ast.Call*):
"""Translate a printf call using shared state from self.Trans"""
GenApi.translate_printf_call(self.Trans.Pool, self.Trans._cur_builder, self.Trans.Module, cl, self.Trans)
# ============================================================
# HandlePrintCall - translate a print call
def HandlePrintCall(self, cl: ast.Call*):
"""Translate a print call (type-aware formatting + trailing newline)"""
GenApi.translate_print_call(self.Trans.Pool, self.Trans._cur_builder, self.Trans.Module, cl, self.Trans)