Files
ViperC/App/lib/Stub/StubGen/Converter.vp
T
TermiNexus cd68bb62ac feat(gen): M2.3 v3 跨模块逃逸摘要 + 全局赋值判据双保险
【v3 跨模块摘要】Converter._PyiFunction 在每个 def 签名行尾写 `# esc=<mask> ret=<ret>`(仅非零时写);VpiReader 新增逃逸摘要缓存(键 {sha1}.{Class}.{method} / {sha1}.{func},值打包 mask|ret<<32),与返回注解缓存同一次加载解析,并加"模块已加载"备忘 + 键前缀过滤避免拖慢编译;StructGenMixin 抽出 compute_escape_summary 供登记与落盘共用;CallGenBase.lookup_escape_via_vpi 按本类 + MRO 祖先模块 sha1 查询,方法/构造器/模块级函数三处调用点在本地表未命中时回退查 vpi。

【修复】Converter._PyiEnsureEscScratch 补 global(漏写导致全局仍 NULL,box 算成 0+520 → ASan WRITE on 0x208);函数内写"本文件已声明的模块全局"且无 global/nonlocal 时必须 fail-fast——原判据只看 SymTab.Root.Vars,部分时序看不到模块全局而静默建局部影子;新增 FileVarNames(register_file_funcs 预扫收集顶层变量名)+ is_file_var() 兜底,普通赋值与注解式重声明两处都接上。
2026-10-08 17:28:06 +08:00

963 lines
53 KiB
Plaintext

import lib.core.Gen.GenApi as GenApi
import lib.core.VLogger as VLogger
import t, c
from stdint import *
import ast
import string
import memhub
import viperlib
import stdlib
import sys
# ============================================================
# StubGen.Converter - .vpi stub file generator
# _eval_binop_int / _eval_binop_valid - recursively evaluate BinOp constant values.
# Handles BitOr/BitAnd/Add/Sub and nested Name refs to registered CDefine constants.
def _eval_binop_valid(node: ast.AST*) -> int:
"""Return 1 if a BinOp node is fully computable from constants or registered CDefines"""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Constant:
cn: ast.Constant* = ast.Constant*(node)
if cn.const_kind == ast.CONST_INT: return 1
return 0
if k == ast.ASTKind.UnaryOp:
uop: ast.UnaryOp* = ast.UnaryOp*(node)
if uop.operand is not None: return _eval_binop_valid(uop.operand)
return 0
if k == ast.ASTKind.BinOp:
bop: ast.BinOp* = ast.BinOp*(node)
if bop.left is not None and bop.right is not None:
if _eval_binop_valid(bop.left) != 0:
if _eval_binop_valid(bop.right) != 0: return 1
return 0
if k == ast.ASTKind.Name:
# Name reference: check whether it is a registered CDefine constant
nm: ast.Name* = ast.Name*(node)
if nm.id is not None:
# Look up in the local CDefine table
val: int = _lookup_cdefine_for_pyi(nm.id)
if _pyi_cdefine_found() != 0: return 1
return 0
return 0
def _eval_binop_int(node: ast.AST*) -> int64:
"""Recursively evaluate a BinOp node as a 64-bit integer value"""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Constant:
cn: ast.Constant* = ast.Constant*(node)
if cn.const_kind == ast.CONST_INT: return cn.int_val
return 0
if k == ast.ASTKind.UnaryOp:
uop: ast.UnaryOp* = ast.UnaryOp*(node)
if uop.op == ast.OpKind.USub and uop.operand is not None: return -_eval_binop_int(uop.operand)
if uop.op == ast.OpKind.UAdd and uop.operand is not None: return _eval_binop_int(uop.operand)
if uop.op == ast.OpKind.Invert and uop.operand is not None: return ~_eval_binop_int(uop.operand)
return 0
if k == ast.ASTKind.BinOp:
bop: ast.BinOp* = ast.BinOp*(node)
if bop.left is None or bop.right is None: return 0
lv: int64 = _eval_binop_int(bop.left)
rv: int64 = _eval_binop_int(bop.right)
if bop.op == ast.OpKind.BitOr: return lv | rv
if bop.op == ast.OpKind.BitAnd: return lv & rv
if bop.op == ast.OpKind.BitXor: return lv ^ rv
if bop.op == ast.OpKind.Add: return lv + rv
if bop.op == ast.OpKind.Sub: return lv - rv
if bop.op == ast.OpKind.Mult: return lv * rv
if bop.op == ast.OpKind.Div: return lv // rv
if bop.op == ast.OpKind.FloorDiv: return lv // rv
if bop.op == ast.OpKind.Mod: return lv % rv
if bop.op == ast.OpKind.LShift: return lv << rv
if bop.op == ast.OpKind.RShift: return lv >> rv
return 0
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm.id is not None: return _lookup_cdefine_for_pyi(nm.id)
return 0
return 0
# Forward declaration for CDefine lookup (real impl in HandlesType);
# weak refs avoid circular dependencies.
_g_pyi_cdefine_found: int = 0
# Per-file CDefine value cache (vpi only): vpi runs before the translate phase, so
# _GeneratePyiFromAst registers in order; C arrays dodge list.get misdispatch.
_g_pyi_cdef_names: str* = None
_g_pyi_cdef_vals: int64* = None
_g_pyi_cdef_count: int = 0
def _lookup_cdefine_for_pyi(name: str) -> int64:
"""Look up a constant value from this file's CDefine cache or the local CDefine table"""
global _g_pyi_cdefine_found
# Look up this file's CDefine cache first (globals are empty during vpi gen)
if _g_pyi_cdef_names is not None and _g_pyi_cdef_vals is not None:
# Bound loop by _g_pyi_cdef_count; never len()/.get on i8** (len falls back
# to strlen, .get misdispatches to HashTable.get) -- index via t.CPtr(base + i*8).
pn: sizet = sizet(_g_pyi_cdef_count)
for pi in range(pn):
entry_pc: str* = (str*)(uint64(_g_pyi_cdef_names) + pi * 8)
pnm: str = entry_pc[0]
if pnm is not None and string.strcmp(pnm, name) == 0:
_g_pyi_cdefine_found = 1
return _g_pyi_cdef_vals[pi]
val: int64 = GenApi.lookup_cdefine_constant(name)
if GenApi.is_cdefine_found() != 0:
_g_pyi_cdefine_found = 1
return val
_g_pyi_cdefine_found = 0
return 0
def _eval_cdefine_value_cache(node: ast.AST*) -> int64:
"""Evaluate a CDefine value for preprocessing registration (Constant/UnaryOp/BinOp/Call/Name)"""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Constant:
cn_c: ast.Constant* = ast.Constant*(node)
if cn_c.const_kind == ast.CONST_INT: return cn_c.int_val
if cn_c.const_kind == ast.CONST_BOOL: return cn_c.int_val
# STR/FLOAT/NONE do not participate in integer BinOp refs (registering 0 is harmless)
return 0
if k == ast.ASTKind.UnaryOp: return _eval_binop_int(node)
if k == ast.ASTKind.BinOp: return _eval_binop_int(node)
if k == ast.ASTKind.Call:
cl_c: ast.Call* = ast.Call*(node)
if cl_c.args is not None and len(cl_c.args) > 0: return _eval_cdefine_value_cache(cl_c.args.get(0))
return 0
if k == ast.ASTKind.Name:
nm_c: ast.Name* = ast.Name*(node)
if nm_c.id is not None: return _lookup_cdefine_for_pyi(nm_c.id)
return 0
return 0
def _pyi_cdefine_found() -> int:
"""Return the result status of the last _lookup_cdefine_for_pyi call"""
return _g_pyi_cdefine_found
# ============================================================
# _PyiEmit - append text to buf at pos, returning the new pos (truncates on overflow)
def _PyiEmit(buf: bytes, buf_size: sizet, pos: sizet, text: str) -> sizet:
"""Append text to buf at pos and return the new pos"""
if buf is None or text is None or pos >= buf_size: return pos
tlen: sizet = string.strlen(text)
if tlen == 0: return pos
remain: sizet = buf_size - pos
if tlen >= remain: tlen = remain - 1
if tlen > 0: string.memcpy(buf + pos, text, tlen)
return pos + tlen
# ============================================================
# _PyiIndent - append level*4 spaces to buf at pos
def _PyiIndent(buf: bytes, buf_size: sizet, pos: sizet, level: int) -> sizet:
"""Append level*4 spaces to buf at pos and return the new pos"""
for i in range(level):
pos = _PyiEmit(buf, buf_size, pos, " ")
return pos
# _PyiBinOpOpText - emit the " OP " text for a binary operator.
# Kept byte-identical to TPC's BinOp operator table; unknown ops fail fast.
def _PyiBinOpOpText(buf: bytes, buf_size: sizet, pos: sizet, op: int, node: ast.AST*) -> sizet:
"""Emit the " OP " text for a binary operator; unknown operators fail fast"""
if op == ast.OpKind.BitOr: return _PyiEmit(buf, buf_size, pos, " | ")
if op == ast.OpKind.Add: return _PyiEmit(buf, buf_size, pos, " + ")
if op == ast.OpKind.Sub: return _PyiEmit(buf, buf_size, pos, " - ")
if op == ast.OpKind.Mult: return _PyiEmit(buf, buf_size, pos, " * ")
if op == ast.OpKind.Div: return _PyiEmit(buf, buf_size, pos, " / ")
if op == ast.OpKind.Mod: return _PyiEmit(buf, buf_size, pos, " % ")
if op == ast.OpKind.BitAnd: return _PyiEmit(buf, buf_size, pos, " & ")
if op == ast.OpKind.RShift: return _PyiEmit(buf, buf_size, pos, " >> ")
GenApi.fatal_error(node, "unknown vpi type-annotation node type", "TypeError")
return pos # unreachable (fatal_error already exited)
# _PyiTypeStr - recursively render an AST annotation node into buf at pos.
# Support set matches TPC; TPV additionally accepts a bare Tuple.
def _PyiTypeStr(buf: bytes, buf_size: sizet, pos: sizet, annot: ast.AST*, depth: int = 0) -> sizet:
"""Recursively render a type annotation node into buf and return the new pos"""
# Recursion guard: a self-referential AST would recurse until stack exhaustion.
# Fail fast instead of silently falling back.
if depth > 64:
VLogger.error("vpi type annotation recursion too deep (AST cycle or nesting over 64 levels)", "StubGen")
sys.exit(1)
if annot is None: return _PyiEmit(buf, buf_size, pos, "int")
k: int = annot.kind()
# Name: simple type name (e.g. int, str, MyClass)
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(annot)
if nm.id is not None: return _PyiEmit(buf, buf_size, pos, nm.id)
return _PyiEmit(buf, buf_size, pos, "int")
# Attribute: attribute reference (e.g. int, memhub.MemBuddy)
if k == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(annot)
pos = _PyiTypeStr(buf, buf_size, pos, at.value, depth + 1)
pos = _PyiEmit(buf, buf_size, pos, ".")
if at.attr is not None: pos = _PyiEmit(buf, buf_size, pos, at.attr)
return pos
# Subscript: subscripted type (e.g. list[int], t.CArray[elem, count])
if k == ast.ASTKind.Subscript:
sub: ast.Subscript* = ast.Subscript*(annot)
pos = _PyiTypeStr(buf, buf_size, pos, sub.value, depth + 1)
pos = _PyiEmit(buf, buf_size, pos, "[")
pos = _PyiTypeStr(buf, buf_size, pos, sub.slice, depth + 1)
pos = _PyiEmit(buf, buf_size, pos, "]")
return pos
# BinOp: union types and CArray count expressions (MAX_ORDER + 1,
# 2 * ZHUFF_MAX_CODES); operator text via _PyiBinOpOpText, byte-identical to TPC.
if k == ast.ASTKind.BinOp:
bop: ast.BinOp* = ast.BinOp*(annot)
pos = _PyiTypeStr(buf, buf_size, pos, bop.left, depth + 1)
pos = _PyiBinOpOpText(buf, buf_size, pos, bop.op, annot)
pos = _PyiTypeStr(buf, buf_size, pos, bop.right, depth + 1)
return pos
# UnaryOp: count expressions (-1 / ~0) and macro args (not ct.isptr(x)).
# Not/Invert/USub match TPC; other unary ops fail fast.
if k == ast.ASTKind.UnaryOp:
uop_t: ast.UnaryOp* = ast.UnaryOp*(annot)
if uop_t.op == ast.OpKind.Not: pos = _PyiEmit(buf, buf_size, pos, "not ")
elif uop_t.op == ast.OpKind.Invert: pos = _PyiEmit(buf, buf_size, pos, "~")
elif uop_t.op == ast.OpKind.USub: pos = _PyiEmit(buf, buf_size, pos, "-")
else:
GenApi.fatal_error(annot, "unknown vpi type-annotation node type", "TypeError")
return pos # unreachable (fatal_error already exited)
return _PyiTypeStr(buf, buf_size, pos, uop_t.operand, depth + 1)
# Compare: comparison expressions (MAX_SIZE > 512 / val == 1).
# == != < <= > >= match TPC; unknown ops emit " ? ".
if k == ast.ASTKind.Compare:
cmp_t: ast.Compare* = ast.Compare*(annot)
pos = _PyiTypeStr(buf, buf_size, pos, cmp_t.left, depth + 1)
ops_ct: list[int]* = (list[int]*)(cmp_t.ops)
cmps_ct: list[ast.AST*]* = cmp_t.comparators
if ops_ct is not None:
on_ct: sizet = len(ops_ct)
for oi_ct in range(on_ct):
oid_ct: int = ops_ct.get(oi_ct)
if oid_ct == ast.OpKind.Eq: pos = _PyiEmit(buf, buf_size, pos, " == ")
elif oid_ct == ast.OpKind.Ne: pos = _PyiEmit(buf, buf_size, pos, " != ")
elif oid_ct == ast.OpKind.Lt: pos = _PyiEmit(buf, buf_size, pos, " < ")
elif oid_ct == ast.OpKind.Le: pos = _PyiEmit(buf, buf_size, pos, " <= ")
elif oid_ct == ast.OpKind.Gt: pos = _PyiEmit(buf, buf_size, pos, " > ")
elif oid_ct == ast.OpKind.Ge: pos = _PyiEmit(buf, buf_size, pos, " >= ")
else: pos = _PyiEmit(buf, buf_size, pos, " ? ")
# TPC indexes comparators[i] by ops; guard the length here so a
# malformed AST (unequal ops/comparators) cannot overrun.
if cmps_ct is not None and oi_ct < len(cmps_ct): pos = _PyiTypeStr(buf, buf_size, pos, cmps_ct.get(oi_ct), depth + 1)
return pos
# BoolOp: boolean ops (ENABLED and MAX_SIZE > 0 / A or B).
# Matches TPC: and -> " and ", or -> " or ".
if k == ast.ASTKind.BoolOp:
bool_t: ast.BoolOp* = ast.BoolOp*(annot)
vals_bt: list[ast.AST*]* = bool_t.values
if vals_bt is not None:
vn_bt: sizet = len(vals_bt)
for vi_bt in range(vn_bt):
if vi_bt > 0:
if bool_t.op == ast.OpKind.Or: pos = _PyiEmit(buf, buf_size, pos, " or ")
else: pos = _PyiEmit(buf, buf_size, pos, " and ")
pos = _PyiTypeStr(buf, buf_size, pos, vals_bt.get(vi_bt), depth + 1)
return pos
# Constant: string forward refs (e.g. 'MyClass'), None or numeric constants
if k == ast.ASTKind.Constant:
cn: ast.Constant* = ast.Constant*(annot)
if cn.const_kind == ast.CONST_STR:
pos = _PyiEmit(buf, buf_size, pos, "'")
if cn.str_val is not None: pos = _PyiEmit(buf, buf_size, pos, cn.str_val)
pos = _PyiEmit(buf, buf_size, pos, "'")
return pos
if cn.const_kind == ast.CONST_NONE: return _PyiEmit(buf, buf_size, pos, "None")
# Numeric constants (e.g. the count in t.CArray[uint32, 64]): emit the value
# itself; falling back to int makes stub loading raise TypeError.
if cn.const_kind == ast.CONST_INT or cn.const_kind == ast.CONST_CHAR:
num_buf_ct: str = stdlib.malloc(24)
if num_buf_ct is not None:
viperlib.snprintf(num_buf_ct, 24, "%d", cn.int_val)
pos = _PyiEmit(buf, buf_size, pos, num_buf_ct)
stdlib.free(num_buf_ct)
return pos
return _PyiEmit(buf, buf_size, pos, "int")
# List: function pointer parameter table (e.g. the inner [t.CPtr, ULONG] of
# t.Callable[[t.CPtr, ULONG], BOOL]); must emit "[a, b]" or it fails fast.
if k == ast.ASTKind.List:
lst: ast.List* = ast.List*(annot)
pos = _PyiEmit(buf, buf_size, pos, "[")
if lst.elts is not None:
lst_elts: list[ast.AST*]* = lst.elts
ln: sizet = len(lst_elts)
for li in range(ln):
if li > 0: pos = _PyiEmit(buf, buf_size, pos, ", ")
le: ast.AST* = lst_elts.get(li)
pos = _PyiTypeStr(buf, buf_size, pos, le, depth + 1)
pos = _PyiEmit(buf, buf_size, pos, "]")
return pos
# Tuple: multi-argument subscript (e.g. Dict[str, int])
if k == ast.ASTKind.Tuple:
tup: ast.Tuple* = ast.Tuple*(annot)
if tup.elts is not None:
tup_elts: list[ast.AST*]* = tup.elts
tn: sizet = len(tup_elts)
for ti in range(tn):
if ti > 0: pos = _PyiEmit(buf, buf_size, pos, ", ")
el: ast.AST* = tup_elts.get(ti)
pos = _PyiTypeStr(buf, buf_size, pos, el, depth + 1)
return pos
# Call: call expression (used by decorators such as @t.NoVTable())
if k == ast.ASTKind.Call:
cl: ast.Call* = ast.Call*(annot)
pos = _PyiTypeStr(buf, buf_size, pos, cl.func, depth + 1)
pos = _PyiEmit(buf, buf_size, pos, "(")
if cl.args is not None:
cl_args: list[ast.AST*]* = cl.args
can: sizet = len(cl_args)
for cai in range(can):
if cai > 0: pos = _PyiEmit(buf, buf_size, pos, ", ")
ca: ast.AST* = cl_args.get(cai)
pos = _PyiTypeStr(buf, buf_size, pos, ca, depth + 1)
pos = _PyiEmit(buf, buf_size, pos, ")")
return pos
# Unknown node type: fail fast rather than silently writing "int" and producing a wrong signature.
GenApi.fatal_error(annot, "unknown vpi type-annotation node type", "TypeError")
return _PyiEmit(buf, buf_size, pos, "int") # unreachable (fatal_error already exited)
# _PyiDefaultStr - render a default-value expression for '= <default>' in vpi.
# Stubs must keep defaults so VpiReader can register the default-parameter table.
def _PyiDefaultStr(buf: bytes, buf_size: sizet, pos: sizet, node: ast.AST*, depth: int = 0) -> sizet:
"""Render a default-value expression into buf and return the new pos"""
# Recursion guard: fail fast on a self-referential AST (same as _PyiTypeStr)
if depth > 64:
VLogger.error("vpi default-value expression recursion too deep (AST cycle or nesting over 64 levels)", "StubGen")
sys.exit(1)
if node is None: return pos
k: int = node.kind()
# Constant: emit numeric/string/bool/None literals directly
if k == ast.ASTKind.Constant:
cn: ast.Constant* = ast.Constant*(node)
if cn.const_kind == ast.CONST_INT or cn.const_kind == ast.CONST_CHAR:
num_buf_dv: str = stdlib.malloc(24)
if num_buf_dv is not None:
viperlib.snprintf(num_buf_dv, 24, "%d", cn.int_val)
pos = _PyiEmit(buf, buf_size, pos, num_buf_dv)
stdlib.free(num_buf_dv)
return pos
if cn.const_kind == ast.CONST_STR:
pos = _PyiEmit(buf, buf_size, pos, "\"")
if cn.str_val is not None: pos = _PyiEmit(buf, buf_size, pos, cn.str_val)
return _PyiEmit(buf, buf_size, pos, "\"")
if cn.const_kind == ast.CONST_BOOL:
if cn.int_val != 0: return _PyiEmit(buf, buf_size, pos, "True")
return _PyiEmit(buf, buf_size, pos, "False")
if cn.const_kind == ast.CONST_NONE: return _PyiEmit(buf, buf_size, pos, "None")
return pos
# Name: CDefine constant name (e.g. share: ULONG = SHARE_READ)
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm.id is not None: return _PyiEmit(buf, buf_size, pos, nm.id)
return pos
# UnaryOp: unary operation (e.g. -1, ~0)
if k == ast.ASTKind.UnaryOp:
uop: ast.UnaryOp* = ast.UnaryOp*(node)
if uop.op == ast.OpKind.USub: pos = _PyiEmit(buf, buf_size, pos, "-")
elif uop.op == ast.OpKind.UAdd: pos = _PyiEmit(buf, buf_size, pos, "+")
elif uop.op == ast.OpKind.Invert: pos = _PyiEmit(buf, buf_size, pos, "~")
else: return pos
if uop.operand is not None: return _PyiDefaultStr(buf, buf_size, pos, uop.operand, depth + 1)
return pos
# Other complex expressions (BinOp etc.): emit nothing; caller enforces fail-fast
return pos
# _PyiRejectCReturnDecorator - fail if @c.CReturn appears in the decorator list.
# The decorator was removed; a stale stub must be regenerated. 0 = none found.
def _PyiRejectCReturnDecorator(node: ast.AST*, decorators: list[ast.AST*]*) -> int:
"""Fail fast on a @c.CReturn decorator (returns 0 when absent)"""
if decorators is None: return 0
dn: sizet = len(decorators)
di: sizet = 0
while di < dn:
dnode: ast.AST* = decorators.get(di)
di += 1
if dnode is None: continue
# Handles both @c.CReturn(...) (Call with func=Attribute) and bare @c.CReturn
dattr: ast.Attribute* = None
if dnode.kind() == ast.ASTKind.Call:
dcall: ast.Call* = ast.Call*(dnode)
if dcall.func is not None and dcall.func.kind() == ast.ASTKind.Attribute: dattr = ast.Attribute*(dcall.func)
elif dnode.kind() == ast.ASTKind.Attribute: dattr = ast.Attribute*(dnode)
if dattr is None or dattr.attr is None: continue
if string.strcmp(dattr.attr, "CReturn") == 0:
GenApi.fatal_error(node, "vpi still contains `@c.CReturn` (removed): regenerate the .vpi using a return annotation `-> tuple[ty1, ty2, ...]` (you may add --clean)", "TypeError")
return 1
return 0
# ============================================================
# 逃逸摘要计算用的复用临时区(74 槽:gset[0]=global 计数、gset[1..65)=名哈希、box=gset+65)。
# 只分配一次,避免每个 def 都分配;布局与 StructGen 的 EscScratch 完全一致。
_g_pyi_esc_scratch: sizet* = None
def _PyiEnsureEscScratch() -> int:
"""惰性分配摘要临时区;1=可用 / 0=失败"""
# 必须声明 global:否则赋值会落到**局部**,调用方读到的仍是 NULL 全局,
# box 指针算成 0+520 → ASan: WRITE on 0x208(本文件它处同款写法见 _g_pyi_cdefine_found)。
global _g_pyi_esc_scratch
if _g_pyi_esc_scratch is not None: return 1
_g_pyi_esc_scratch = sizet*(stdlib.malloc(74 * 8))
if _g_pyi_esc_scratch is None: return 0
string.memset(_g_pyi_esc_scratch, 0, 74 * 8)
return 1
# _PyiFunction - emit a function signature (def name(...) -> Ret: pass)
def _PyiFunction(buf: bytes, buf_size: sizet, pos: sizet, fn: ast.FunctionDef*, indent_level: int, class_name: str) -> sizet:
"""Emit a function signature into buf and return the new pos"""
# Decorators
if fn.decorator_list is not None:
fn_decorator_list: list[ast.AST*]* = fn.decorator_list
dn: sizet = len(fn_decorator_list)
for di in range(dn):
dec: ast.AST* = fn_decorator_list.get(di)
if dec is not None:
pos = _PyiIndent(buf, buf_size, pos, indent_level)
pos = _PyiEmit(buf, buf_size, pos, "@")
pos = _PyiTypeStr(buf, buf_size, pos, dec)
pos = _PyiEmit(buf, buf_size, pos, "\n")
pos = _PyiIndent(buf, buf_size, pos, indent_level)
pos = _PyiEmit(buf, buf_size, pos, "def ")
if fn.name is not None: pos = _PyiEmit(buf, buf_size, pos, fn.name)
pos = _PyiEmit(buf, buf_size, pos, "(")
# Parameter list
args_node: ast.AST* = fn.args
if args_node is not None:
args: ast.Arguments* = ast.Arguments*(args_node)
first: int = 1
# Positional parameters
if args.args is not None:
args_args: list[ast.AST*]* = args.args
an: sizet = len(args_args)
# Defaults right-align to args tail: defaults[i] maps to args[an - dn + i];
# emit '= <default>' and declare arg_defaults inline to avoid .get misdispatch.
arg_defaults: list[ast.AST*]* = args.defaults
dn: sizet = 0
if arg_defaults is not None: dn = len(arg_defaults)
ai: sizet = 0
while ai < an:
if first == 0: pos = _PyiEmit(buf, buf_size, pos, ", ")
first = 0
ag: ast.Arg* = ast.Arg*(args_args.get(ai))
if ag.arg is not None: pos = _PyiEmit(buf, buf_size, pos, ag.arg)
if ag.annotation is not None:
pos = _PyiEmit(buf, buf_size, pos, ": ")
pos = _PyiTypeStr(buf, buf_size, pos, ag.annotation)
if arg_defaults is not None and an - dn <= ai:
di: sizet = ai - (an - dn)
dnode: ast.AST* = arg_defaults.get(di)
if dnode is not None:
pos = _PyiEmit(buf, buf_size, pos, " = ")
pos = _PyiDefaultStr(buf, buf_size, pos, dnode)
ai += 1
# *args
if args.vararg is not None:
if first == 0: pos = _PyiEmit(buf, buf_size, pos, ", ")
first = 0
pos = _PyiEmit(buf, buf_size, pos, "*")
va: ast.Arg* = ast.Arg*(args.vararg)
if va.arg is not None: pos = _PyiEmit(buf, buf_size, pos, va.arg)
# **kwargs
if args.kwarg is not None:
if first == 0: pos = _PyiEmit(buf, buf_size, pos, ", ")
first = 0
pos = _PyiEmit(buf, buf_size, pos, "**")
kw: ast.Arg* = ast.Arg*(args.kwarg)
if kw.arg is not None: pos = _PyiEmit(buf, buf_size, pos, kw.arg)
pos = _PyiEmit(buf, buf_size, pos, ")")
# Return type
if fn.returns is not None:
pos = _PyiEmit(buf, buf_size, pos, " -> ")
pos = _PyiTypeStr(buf, buf_size, pos, fn.returns)
else:
# Without a `->` annotation, @c.CReturn is removed: fail fast to regenerate vpi
_PyiRejectCReturnDecorator(fn, fn.decorator_list)
pos = _PyiEmit(buf, buf_size, pos, ": pass")
# v3 跨模块逃逸摘要:把形参逃逸掩码与返回借用位写成**同一行**的行尾注释,供 VpiReader
# 解析后在别的模块的调用点查询(跨模块摘要表按文件重置,只能靠 vpi 传递)。
# 只在真有逃逸/返回借用时写,保持 vpi 干净:无标记 = 无摘要 = 调用点保守放行。
if _PyiEnsureEscScratch() != 0:
esc_box_c: sizet* = sizet*(uint64(_g_pyi_esc_scratch) + 520)
if GenApi.compute_escape_summary(None, fn, _g_pyi_esc_scratch, esc_box_c) == 0:
if esc_box_c[0] != 0 or esc_box_c[1] != 0:
tmp_c: bytes = stdlib.malloc(64)
if tmp_c is not None:
viperlib.snprintf(tmp_c, 64, " # esc=%d ret=%d", int(esc_box_c[0]), int(esc_box_c[1]))
pos = _PyiEmit(buf, buf_size, pos, tmp_c)
stdlib.free(tmp_c)
pos = _PyiEmit(buf, buf_size, pos, "\n")
return pos
# _PyiClass - emit a class definition (fields and method stubs).
def _PyiClass(buf: bytes, buf_size: sizet, pos: sizet, cls: ast.ClassDef*, indent_level: int) -> sizet:
"""Emit a class definition into buf and return the new pos"""
# Decorators
if cls.decorator_list is not None:
cls_decorator_list: list[ast.AST*]* = cls.decorator_list
dcn: sizet = len(cls_decorator_list)
for dci in range(dcn):
ddec: ast.AST* = cls_decorator_list.get(dci)
if ddec is not None:
pos = _PyiIndent(buf, buf_size, pos, indent_level)
pos = _PyiEmit(buf, buf_size, pos, "@")
pos = _PyiTypeStr(buf, buf_size, pos, ddec)
pos = _PyiEmit(buf, buf_size, pos, "\n")
pos = _PyiIndent(buf, buf_size, pos, indent_level)
pos = _PyiEmit(buf, buf_size, pos, "class ")
if cls.name is not None: pos = _PyiEmit(buf, buf_size, pos, cls.name)
# PEP 695 type parameters (class Foo[T]:)
if cls.type_params is not None:
# ClassDef.type_params is really rawptr[list[str]] (a list of name strings),
# not list[rawptr[AST]] (that is decorator_list's type); align with the rest of the repo.
cls_type_params: list[str]* = cls.type_params
tpn: sizet = len(cls_type_params)
if tpn > 0:
pos = _PyiEmit(buf, buf_size, pos, "[")
for tpi in range(tpn):
if tpi > 0: pos = _PyiEmit(buf, buf_size, pos, ", ")
tpname: str = cls_type_params.get(tpi)
if tpname is not None: pos = _PyiEmit(buf, buf_size, pos, tpname)
pos = _PyiEmit(buf, buf_size, pos, "]")
# Base classes
if cls.bases is not None:
cls_bases: list[ast.AST*]* = cls.bases
bn: sizet = len(cls_bases)
if bn > 0:
pos = _PyiEmit(buf, buf_size, pos, "(")
for bi in range(bn):
if bi > 0: pos = _PyiEmit(buf, buf_size, pos, ", ")
base: ast.AST* = cls_bases.get(bi)
pos = _PyiTypeStr(buf, buf_size, pos, base)
pos = _PyiEmit(buf, buf_size, pos, ")")
pos = _PyiEmit(buf, buf_size, pos, ":\n")
# Class members
has_member: int = 0
if cls.children is not None:
cls_children: list[ast.AST*]* = cls.children
cn: sizet = len(cls_children)
ci: sizet = 0
while ci < cn:
stmt: ast.AST* = cls_children.get(ci)
if stmt is not None:
sk: int = stmt.kind()
# Methods
if sk == ast.ASTKind.FunctionDef:
mfn: ast.FunctionDef* = ast.FunctionDef*(stmt)
# __init__ method: first extract self.x: Type attributes
if mfn.name is not None and string.strcmp(mfn.name, "__init__") == 0:
if mfn.children is not None:
mfn_children: list[ast.AST*]* = mfn.children
icn: sizet = len(mfn_children)
ici: sizet = 0
while ici < icn:
ibody: ast.AST* = mfn_children.get(ici)
if ibody is not None and ibody.kind() == ast.ASTKind.AnnAssign:
iaa: ast.AnnAssign* = ast.AnnAssign*(ibody)
if iaa.target is not None and iaa.target.kind() == ast.ASTKind.Attribute:
iat: ast.Attribute* = ast.Attribute*(iaa.target)
# Check whether target.value is Name("self")
if iat.value is not None and iat.value.kind() == ast.ASTKind.Name:
iavn: ast.Name* = ast.Name*(iat.value)
if iavn.id is None or string.strcmp(iavn.id, "self") != 0:
ici += 1
continue
pos = _PyiIndent(buf, buf_size, pos, indent_level + 1)
if iat.attr is not None: pos = _PyiEmit(buf, buf_size, pos, iat.attr)
pos = _PyiEmit(buf, buf_size, pos, ": ")
pos = _PyiTypeStr(buf, buf_size, pos, iaa.annotation)
pos = _PyiEmit(buf, buf_size, pos, "\n")
has_member = 1
ici += 1
pos = _PyiFunction(buf, buf_size, pos, mfn, indent_level + 1, cls.name)
has_member = 1
# Annotated attributes
elif sk == ast.ASTKind.AnnAssign:
aa: ast.AnnAssign* = ast.AnnAssign*(stmt)
if aa.target is not None and aa.target.kind() == ast.ASTKind.Name:
tn: ast.Name* = ast.Name*(aa.target)
pos = _PyiIndent(buf, buf_size, pos, indent_level + 1)
if tn.id is not None: pos = _PyiEmit(buf, buf_size, pos, tn.id)
pos = _PyiEmit(buf, buf_size, pos, ": ")
pos = _PyiTypeStr(buf, buf_size, pos, aa.annotation)
pos = _PyiEmit(buf, buf_size, pos, "\n")
has_member = 1
# Nested classes
elif sk == ast.ASTKind.ClassDef:
ncls: ast.ClassDef* = ast.ClassDef*(stmt)
pos = _PyiClass(buf, buf_size, pos, ncls, indent_level + 1)
has_member = 1
ci += 1
# Emit pass for an empty class
if has_member == 0:
pos = _PyiIndent(buf, buf_size, pos, indent_level + 1)
pos = _PyiEmit(buf, buf_size, pos, "pass\n")
return pos
# _GeneratePyiFromAst - walk the AST Module and emit .vpi stub content into buf.
# Returns bytes written (0 on failure); rel_path should be absolute for error attribution.
def _GeneratePyiFromAst(mb: memhub.MemBuddy*, tree: ast.AST*, rel_path: str, buf: bytes, buf_size: sizet) -> sizet:
"""Generate .vpi stub content from an AST into buf"""
if tree is None or buf is None or buf_size == 0: return 0
# Error attribution: point "current file" at the file being stubbed. Callers must pass
# an absolute path (fatal_error opens it directly). Set once at entry to cover all branches.
if rel_path is not None: GenApi.set_current_file_type(rel_path)
pos: sizet = 0
# Pre-pass: register this file's CDefines into the _g_pyi cache in declaration order
# (vpi runs before the translate phase; without this, BinOp Name refs degrade to 0).
global _g_pyi_cdef_names, _g_pyi_cdef_vals, _g_pyi_cdef_count
if tree.children is not None:
pre_children: list[ast.AST*]* = tree.children
pcn: sizet = len(pre_children)
# Pass 1: count t.CDefine declarations (C arrays need a fixed size first)
_g_pyi_cdef_count = 0
for pci0 in range(pcn):
stmt_pc0: ast.AST* = pre_children.get(pci0)
if stmt_pc0 is not None and stmt_pc0.kind() == ast.ASTKind.AnnAssign:
aa_pc0: ast.AnnAssign* = ast.AnnAssign*(stmt_pc0)
if aa_pc0.target is not None and aa_pc0.target.kind() == ast.ASTKind.Name and aa_pc0.annotation is not None:
tgt_pc0: ast.Name* = ast.Name*(aa_pc0.target)
if tgt_pc0.id is not None and aa_pc0.annotation.kind() == ast.ASTKind.Attribute:
ann_pc0: ast.Attribute* = ast.Attribute*(aa_pc0.annotation)
if ann_pc0.attr is not None and string.strcmp(ann_pc0.attr, "CDefine") == 0: _g_pyi_cdef_count += 1
if _g_pyi_cdef_count > 0:
_g_pyi_cdef_names = (str*)(mb.alloc(sizet(_g_pyi_cdef_count) * 8))
_g_pyi_cdef_vals = (int64*)(mb.alloc(sizet(_g_pyi_cdef_count) * 8))
# Pass 2: register in declaration order (BinOp Name refs depend on earlier entries)
pci: sizet = 0
cdi: int = 0
for pci in range(pcn):
stmt_pc: ast.AST* = pre_children.get(pci)
if stmt_pc is not None and stmt_pc.kind() == ast.ASTKind.AnnAssign:
aa_pc: ast.AnnAssign* = ast.AnnAssign*(stmt_pc)
if aa_pc.target is not None and aa_pc.target.kind() == ast.ASTKind.Name and aa_pc.annotation is not None:
tgt_pc: ast.Name* = ast.Name*(aa_pc.target)
if tgt_pc.id is not None and aa_pc.annotation.kind() == ast.ASTKind.Attribute:
ann_pc: ast.Attribute* = ast.Attribute*(aa_pc.annotation)
if ann_pc.attr is not None and string.strcmp(ann_pc.attr, "CDefine") == 0:
if _g_pyi_cdef_names is not None and _g_pyi_cdef_vals is not None:
slot_pc: str* = (str*)(uint64(_g_pyi_cdef_names) + cdi * 8)
slot_pc[0] = tgt_pc.id
_g_pyi_cdef_vals[cdi] = _eval_cdefine_value_cache(aa_pc.value)
cdi += 1
# File header
pos = _PyiEmit(buf, buf_size, pos, "\"\"\"Auto-generated Python stub file\"\"\"\n\n")
if tree.children is None:
if pos < buf_size: buf[pos] = 0
return pos
tree_children: list[ast.AST*]* = tree.children
n: sizet = len(tree_children)
# Pass 1: detect import t/c
has_import_t: int = 0
has_import_c: int = 0
for i in range(n):
stmt: ast.AST* = tree_children.get(i)
if stmt is not None and stmt.kind() == ast.ASTKind.Import:
imp: ast.Import* = ast.Import*(stmt)
if imp.names is not None:
imp_names: list[ast.AST*]* = imp.names
an: sizet = len(imp_names)
for ai in range(an):
al: ast.Alias* = ast.Alias*(imp_names.get(ai))
if al is not None and al.name is not None:
if string.strcmp(al.name, "t") == 0: has_import_t = 1
elif string.strcmp(al.name, "c") == 0: has_import_c = 1
# Add missing import t/c
if has_import_t == 0 and has_import_c == 0: pos = _PyiEmit(buf, buf_size, pos, "import t, c\n\n")
elif has_import_t == 0: pos = _PyiEmit(buf, buf_size, pos, "import t\n\n")
elif has_import_c == 0: pos = _PyiEmit(buf, buf_size, pos, "import c\n\n")
else: pos = _PyiEmit(buf, buf_size, pos, "\n")
# Pass 2: emit declarations
for i in range(n):
stmt = tree_children.get(i)
if stmt is not None:
k: int = stmt.kind()
# Import statement
if k == ast.ASTKind.Import:
imp2: ast.Import* = ast.Import*(stmt)
pos = _PyiEmit(buf, buf_size, pos, "import ")
# fail-fast: an empty alias list would write a broken `import ` line and
# lose all names, breaking package re-export registration.
if imp2.names is None or len(imp2.names) == 0:
GenApi.fatal_error(stmt, "vpi generation: the import statement alias list is empty (expected >=1 alias) -- AST construction/parse anomaly", "NameError")
if imp2.names is not None:
imp2_names: list[ast.AST*]* = imp2.names
an2: sizet = len(imp2_names)
for ai2 in range(an2):
if ai2 > 0: pos = _PyiEmit(buf, buf_size, pos, ", ")
al2: ast.Alias* = ast.Alias*(imp2_names.get(ai2))
# fail-fast: never silently write "import , ". Import names are the
# only input for package re-export registration. Report the two causes separately.
if al2 is None:
GenApi.fatal_error(stmt, "vpi generation: import alias **node pointer is null** (Alias(...) construction produced no object: " "pool.alloc failed or __init__ was not run) -- losing the import name breaks package re-export registration", "NameError")
if al2.name is None or al2.name[0] == '\0':
GenApi.fatal_error(stmt, "vpi generation: the import alias node exists but **name is empty** (_copy_str returned None / " "__init__ did not set name) -- losing the import name breaks package re-export registration", "NameError")
pos = _PyiEmit(buf, buf_size, pos, al2.name)
if al2.asname is not None and string.strlen(al2.asname) > 0:
pos = _PyiEmit(buf, buf_size, pos, " as ")
pos = _PyiEmit(buf, buf_size, pos, al2.asname)
pos = _PyiEmit(buf, buf_size, pos, "\n")
# ImportFrom statement
elif k == ast.ASTKind.ImportFrom:
ifr: ast.ImportFrom* = ast.ImportFrom*(stmt)
pos = _PyiEmit(buf, buf_size, pos, "from ")
lvl: int = ifr.level
while lvl > 0:
pos = _PyiEmit(buf, buf_size, pos, ".")
lvl -= 1
if ifr.module is not None: pos = _PyiEmit(buf, buf_size, pos, ifr.module)
pos = _PyiEmit(buf, buf_size, pos, " import ")
# fail-fast: same as Import; `from X import *` uses Alias("*"). An empty
# list or empty name means a broken AST and would break re-export registration.
if ifr.names is None or len(ifr.names) == 0:
GenApi.fatal_error(stmt, "vpi generation: the from-import statement alias list is empty (expected >=1 alias, including star)" "-- AST construction/parse anomaly", "NameError")
if ifr.names is not None:
ifr_names: list[ast.AST*]* = ifr.names
an3: sizet = len(ifr_names)
for ai3 in range(an3):
if ai3 > 0: pos = _PyiEmit(buf, buf_size, pos, ", ")
al3: ast.Alias* = ast.Alias*(ifr_names.get(ai3))
# fail-fast: same as Import (report the two causes separately).
if al3 is None:
GenApi.fatal_error(stmt, "vpi generation: from-import alias **node pointer is null** (Alias(...) construction produced no object: " "pool.alloc failed or __init__ was not run) -- package re-export registration will therefore fail", "NameError")
if al3.name is None or al3.name[0] == '\0':
GenApi.fatal_error(stmt, "vpi generation: the from-import alias node exists but **name is empty** (_copy_str returned None / " "__init__ did not set name) -- package re-export registration will therefore fail", "NameError")
pos = _PyiEmit(buf, buf_size, pos, al3.name)
if al3.asname is not None and string.strlen(al3.asname) > 0:
pos = _PyiEmit(buf, buf_size, pos, " as ")
pos = _PyiEmit(buf, buf_size, pos, al3.asname)
pos = _PyiEmit(buf, buf_size, pos, "\n")
# Function definition (_PyiFunction already ends with a newline)
elif k == ast.ASTKind.FunctionDef:
fn: ast.FunctionDef* = ast.FunctionDef*(stmt)
# Generic function templates (def f[T]) emit no vpi: the signature has an
# unbound T. Specializations are monomorphized per call site, not here.
if fn.type_params is not None: continue
pos = _PyiFunction(buf, buf_size, pos, fn, 0, "")
# Class definition
elif k == ast.ASTKind.ClassDef:
cls: ast.ClassDef* = ast.ClassDef*(stmt)
pos = _PyiClass(buf, buf_size, pos, cls, 0)
# Annotated variable declaration
elif k == ast.ASTKind.AnnAssign:
aa: ast.AnnAssign* = ast.AnnAssign*(stmt)
if aa.target is not None and aa.target.kind() == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(aa.target)
if nm.id is not None:
pos = _PyiEmit(buf, buf_size, pos, nm.id)
pos = _PyiEmit(buf, buf_size, pos, ": ")
pos = _PyiTypeStr(buf, buf_size, pos, aa.annotation)
# If there is a value (e.g. a CDefine constant) emit = value.
# CTypedef is special: its value is a type expression, rendered by _PyiTypeStr.
is_ctypedef_ann: int = 0
if aa.annotation is not None and aa.annotation.kind() == ast.ASTKind.Attribute:
ctd_at: ast.Attribute* = ast.Attribute*(aa.annotation)
if ctd_at.attr is not None and string.strcmp(ctd_at.attr, "CTypedef") == 0:
if ctd_at.value is not None and ctd_at.value.kind() == ast.ASTKind.Name:
ctd_mod: ast.Name* = ast.Name*(ctd_at.value)
if ctd_mod.id is not None and string.strcmp(ctd_mod.id, "t") == 0: is_ctypedef_ann = 1
if is_ctypedef_ann != 0 and aa.value is not None:
pos = _PyiEmit(buf, buf_size, pos, " = ")
pos = _PyiTypeStr(buf, buf_size, pos, aa.value)
elif aa.value is not None and aa.value.kind() == ast.ASTKind.Constant:
cv: ast.Constant* = ast.Constant*(aa.value)
num_buf: bytes = mb.alloc(32)
if cv.const_kind == ast.CONST_INT and num_buf is not None:
viperlib.snprintf(num_buf, 32, " = %lld", cv.int_val)
pos = _PyiEmit(buf, buf_size, pos, num_buf)
elif cv.const_kind == ast.CONST_FLOAT:
# Float CDefines (e.g. numpy pi/e) must emit a value or read-back
# degrades to a global pointer; %.17g round-trips double exactly.
fbuf_c: bytes = mb.alloc(64)
if fbuf_c is not None:
viperlib.snprintf(fbuf_c, 64, " = %.17g", cv.float_val)
pos = _PyiEmit(buf, buf_size, pos, fbuf_c)
elif cv.const_kind == ast.CONST_BOOL:
if cv.int_val != 0: pos = _PyiEmit(buf, buf_size, pos, " = True")
else: pos = _PyiEmit(buf, buf_size, pos, " = False")
elif cv.const_kind == ast.CONST_STR:
# String CDefines (e.g. pyzlib.ZLIB_VERSION) must emit a value so the stub is self-contained
sbuf_c: bytes = mb.alloc(string.strlen(cv.str_val) + 6)
if sbuf_c is not None:
viperlib.snprintf(sbuf_c, string.strlen(cv.str_val) + 6, " = \"%s\"", cv.str_val)
pos = _PyiEmit(buf, buf_size, pos, sbuf_c)
elif cv.const_kind == ast.CONST_NONE: pos = _PyiEmit(buf, buf_size, pos, " = None")
elif aa.value is not None and aa.value.kind() == ast.ASTKind.UnaryOp:
# Negative constant (e.g. INVALID_HANDLE_VALUE = -1):
# UnaryOp(USub, Constant(1)) -> -1
uop_cv: ast.UnaryOp* = ast.UnaryOp*(aa.value)
num_buf_u: bytes = mb.alloc(32)
if uop_cv.op == ast.OpKind.USub and uop_cv.operand is not None and uop_cv.operand.kind() == ast.ASTKind.Constant:
uop_const: ast.Constant* = ast.Constant*(uop_cv.operand)
if uop_const.const_kind == ast.CONST_INT and num_buf_u is not None:
neg_val: int64 = -uop_const.int_val
viperlib.snprintf(num_buf_u, 32, " = %lld", neg_val)
pos = _PyiEmit(buf, buf_size, pos, num_buf_u)
elif aa.value is not None and aa.value.kind() == ast.ASTKind.Call:
# Type-constructor constant (e.g. t.CUnsignedLong(-11)):
# extract and emit the integer value of the first argument.
call_cv: ast.Call* = ast.Call*(aa.value)
if call_cv.args is not None:
call_args_cv: list[ast.AST*]* = call_cv.args
if len(call_args_cv) > 0:
first_arg: ast.AST* = call_args_cv.get(0)
if first_arg is not None:
arg_kind: int = first_arg.kind()
if arg_kind == ast.ASTKind.Constant:
ca_cv: ast.Constant* = ast.Constant*(first_arg)
num_buf_c: bytes = mb.alloc(32)
if ca_cv.const_kind == ast.CONST_INT and num_buf_c is not None:
viperlib.snprintf(num_buf_c, 32, " = %lld", ca_cv.int_val)
pos = _PyiEmit(buf, buf_size, pos, num_buf_c)
elif ca_cv.const_kind == ast.CONST_FLOAT:
# Call-wrapped float constant (e.g. float(3.14159...)): emit the
# value or read-back degrades to 0.0; %.17g round-trips double.
fbuf_cc: bytes = mb.alloc(64)
if fbuf_cc is not None:
viperlib.snprintf(fbuf_cc, 64, " = %.17g", ca_cv.float_val)
pos = _PyiEmit(buf, buf_size, pos, fbuf_cc)
elif arg_kind == ast.ASTKind.UnaryOp:
uop_ca: ast.UnaryOp* = ast.UnaryOp*(first_arg)
num_buf_cu: bytes = mb.alloc(32)
if uop_ca.op == ast.OpKind.USub and uop_ca.operand is not None and uop_ca.operand.kind() == ast.ASTKind.Constant:
uoc: ast.Constant* = ast.Constant*(uop_ca.operand)
if uoc.const_kind == ast.CONST_INT and num_buf_cu is not None:
neg_c_val: int64 = -uoc.int_val
viperlib.snprintf(num_buf_cu, 32, " = %lld", neg_c_val)
pos = _PyiEmit(buf, buf_size, pos, num_buf_cu)
elif uoc.const_kind == ast.CONST_FLOAT:
# Negative Call-wrapped float (e.g. float(-1.5)):
# same as CONST_FLOAT, %.17g round-trips exactly.
neg_cf: double = -uoc.float_val
fbuf_cu: bytes = mb.alloc(64)
if fbuf_cu is not None:
viperlib.snprintf(fbuf_cu, 64, " = %.17g", neg_cf)
pos = _PyiEmit(buf, buf_size, pos, fbuf_cu)
elif arg_kind == ast.ASTKind.BinOp:
# BinOp inside a Call (e.g. t.CUnsignedLong(A | B)):
# recursively evaluate the BinOp value.
binop_val_bo: int64 = _eval_binop_int(first_arg)
num_buf_bo: bytes = mb.alloc(32)
if (binop_val_bo != 0 or _eval_binop_valid(first_arg) != 0) and num_buf_bo is not None:
viperlib.snprintf(num_buf_bo, 32, " = %lld", binop_val_bo)
pos = _PyiEmit(buf, buf_size, pos, num_buf_bo)
elif aa.value is not None and aa.value.kind() == ast.ASTKind.BinOp:
# BinOp constant (e.g. FOREGROUND_COMBO = FOREGROUND_RED | FOREGROUND_GREEN):
# recursively evaluate and emit it.
binop_val_av: int64 = _eval_binop_int(aa.value)
if binop_val_av != 0 or _eval_binop_valid(aa.value) != 0:
num_buf_b: bytes = mb.alloc(32)
if num_buf_b is not None:
viperlib.snprintf(num_buf_b, 32, " = %lld", binop_val_av)
pos = _PyiEmit(buf, buf_size, pos, num_buf_b)
elif aa.value is not None and aa.value.kind() == ast.ASTKind.Name:
# Name reference constant (e.g. ALIAS = OTHER_CONST):
# emit the referenced name for the consumer to resolve locally.
name_val: ast.Name* = ast.Name*(aa.value)
if name_val.id is not None:
pos = _PyiEmit(buf, buf_size, pos, " = ")
pos = _PyiEmit(buf, buf_size, pos, name_val.id)
pos = _PyiEmit(buf, buf_size, pos, "\n")
# Global assignment (no annotation) - infer type from the Constant value
elif k == ast.ASTKind.Assign:
asg: ast.Assign* = ast.Assign*(stmt)
if asg.targets is not None and asg.value is not None:
asg_targets: list[ast.AST*]* = asg.targets
atn: sizet = len(asg_targets)
if atn == 1:
atg: ast.AST* = asg_targets.get(0)
if atg is not None and atg.kind() == ast.ASTKind.Name:
agn: ast.Name* = ast.Name*(atg)
aval: ast.AST* = asg.value
avk: int = aval.kind()
inferred: int = 0
if avk == ast.ASTKind.Constant:
acv: ast.Constant* = ast.Constant*(aval)
if acv.const_kind == ast.CONST_INT: inferred = 1
elif acv.const_kind == ast.CONST_FLOAT: inferred = 2
elif acv.const_kind == ast.CONST_STR: inferred = 3
elif acv.const_kind == ast.CONST_BOOL: inferred = 4
elif acv.const_kind == ast.CONST_NONE: inferred = 5
if inferred > 0 and agn.id is not None:
pos = _PyiEmit(buf, buf_size, pos, agn.id)
pos = _PyiEmit(buf, buf_size, pos, ": ")
if inferred == 1: pos = _PyiEmit(buf, buf_size, pos, "int")
elif inferred == 2: pos = _PyiEmit(buf, buf_size, pos, "double")
elif inferred == 3: pos = _PyiEmit(buf, buf_size, pos, "str")
elif inferred == 4: pos = _PyiEmit(buf, buf_size, pos, "bool")
elif inferred == 5: pos = _PyiEmit(buf, buf_size, pos, "None")
pos = _PyiEmit(buf, buf_size, pos, "\n")
# NUL terminator
if pos < buf_size: buf[pos] = 0
else: buf[buf_size - 1] = 0
return pos