Files
ViperC/App/lib/core/builtin/ViperBuild.vp
T
TermiNexus 9c3ebc8a2c fix(ffilib): 为 runtime CDLL thunk 创建 entry block 并移除失败静默返回
- new_builder 的 CurBlock 初值为 None,_emit 会静默丢弃指令,导致 thunk 生成空函数体(mlir-opt: empty block)
- 补 create_block + position_at_end,随后再把全部静默 return None 改为 fatal_error
- 移除两处 [ffiprobe] 调试探针
2026-10-24 23:57:09 +08:00

1580 lines
62 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Compiler-side implementation of the `viperbuild` built-in macro library (--src Pass 0)."""
from stdint import *
from win32.win32base import *
import memhub
import string
import stdlib
import ast
import win32.win32file as win32file
import lib.Projectrans.Utils as Utils
import lib.Projectrans.Config as Config
import lib.core.VLogger as VLogger
import lib.core.IncludesScanner as IncludesScanner
import stdio
# kernel32!GetEnvironmentVariableA (DWORD(LPCSTR, LPSTR, DWORD)) - declared here, same as includes/traceback.vp;
# win32base.vp omits it but kernel32 is linked anyway; only for selfmachine host detection.
extern def GetEnvironmentVariableA(lpName: LPCSTR, lpBuffer: CHARPTR, nSize: DWORD) -> DWORD
# Path/source buffer caps (same convention as main.vp's SRC_BUF_SIZE)
define VB_PATH_CAP = 1024
define VB_SRC_CAP = 1048576
# Scratch pool size (BFS working set + constant tables; not part of main-pool reset)
define VB_SCRATCH = 8388608
# Visited cap (cycle defense; fatal if the closure greatly exceeds it)
define VB_MAX_VISIT = 4096
# Element separator for list constants: the char code of newline (source paths/flags never contain newlines).
# Must store the code then char(): in Viper 'c' literals are single-char strings (i8*), not assignable to a char slot.
define VB_SEP_CODE = 10
# Module state
_scratch: memhub.MemBuddy* = None
_entry_dir: str = None # entry file dir (default source; normalized to "." for bare filenames)
_search_dirs: str = None # closure search roots (space-separated): entry dir first, then "."
_work: list[str]* = None # BFS pending file paths
_visited: list[str]* = None # processed file paths
_pending: list[str]* = None # import module names collected in one file (pending BFS)
_proj_vars: list[str]* = None # variable names bound to a vb.Project(...) instance
_alias: str = None # local alias of viperbuild (import viperbuild as vb -> "vb")
# ffilib 的本地别名(import ffilib / import ffilib as F);ffilib.CDLL(...) 是纯构建配置
_ffi_alias: str = None
# ffilib.CDLL 句柄表(两个平行列表):变量名 / runtime(1=运行时加载, 0=链接期)
# 供 `extern <lib> name(...)` 校验句柄来源与选择行为。
_ffi_handles_names: list[str]* = None
_ffi_handles_rt: list[int]* = None
# 句柄对应的库路径(runtime=True 时 thunk 直接用绝对路径 LoadLibraryA)
_ffi_handles_path: list[str]* = None
_ctor_name: str = None # from viperbuild import Project as P -> "P"
# Per-file constant tables (valid only while scanning that file; pool-allocated)
_f_str_names: list[str]* = None
_f_str_vals: list[str]* = None
_f_list_names: list[str]* = None
_f_list_vals: list[str]* = None
# Constant-eval output slot
_g_out_str: str = None
# Extracted config (all strings stdlib.malloc)
_c_name: str = None
_c_version: str = None
_c_source: str = None
_c_buildroot: str = None
_c_includes: str = None
_c_compiler: str = None
_c_linker: str = None
_c_static_libs: str = None
_c_dynamic_libs: str = None
_c_output: str = None
_c_triple: str = None
_c_datalayout: str = None
_c_selfmachine: int = 0
_c_has_project: int = 0
_c_has_build: int = 0
# proj.advanced.* 高级开关(等价 CLI 的 --asan / --traceback)
_c_adv_asan: int = 0
_c_adv_traceback: int = 0
# proj.clean.* 细粒度清理(等价 --clean 的子集;build_dir = 整个 __vpcbuild__)
_c_clean_all: int = 0
_c_clean_output: int = 0
_c_clean_temp: int = 0
_c_clean_build_dir: int = 0
_c_clean_includes: int = 0
# build(run=, args=):编译后直接运行(args 非空时隐含 run)
_c_run: int = 0
_c_run_args: str = None
# Whether `import viperbuild` / `from viperbuild import ...` appeared in the closure (diagnostics only)
_saw_viperbuild: int = 0
# Whether applied successfully (for main.vp to decide viperbuild's linker.output)
_applied: int = 0
# Basic helpers
def _DupStd(s: str) -> str:
"""Deep-copy a string with stdlib.malloc so it survives reset_to."""
if s is None: return None
n: sizet = string.strlen(s)
buf: str = stdlib.malloc(n + 1)
if buf is None: return None
string.strcpy(buf, s)
return buf
def _DupScratch(s: str) -> str:
"""Deep-copy a string into the scratch pool, unaffected by main-pool reset_to."""
if s is None or _scratch is None: return None
n: sizet = string.strlen(s)
buf: str = _scratch.alloc(n + 1)
if buf is None: return None
string.strcpy(buf, s)
return buf
def _Join2(a: str, b: str) -> str:
"""Join a and b with a slash; returns b when a is empty."""
if a is None: return b
if b is None: return a
al: sizet = string.strlen(a)
if al == 0: return b
bl: sizet = string.strlen(b)
buf: str = stdlib.malloc(al + 1 + bl + 1)
if buf is None: return None
string.strcpy(buf, a)
buf[al] = '/'
string.strcpy(buf + al + 1, b)
return buf
def _DirOf(path: str) -> str:
"""Return the directory part of path (no trailing separator); empty string if none."""
if path is None: return None
n: sizet = string.strlen(path)
i: sizet = n
while i > 0:
i -= 1
ch: char = path[i]
if ch == '/' or ch == '\\':
buf: str = stdlib.malloc(i + 1)
if buf is None: return None
string.strncpy(buf, path, i)
buf[i] = '\0'
return buf
return _DupStd("")
def _BaseNameOf(path: str) -> str:
"""Return the file name of path, including its extension."""
if path is None: return None
n: sizet = string.strlen(path)
start: sizet = 0
for i in range(int(n)):
ch: char = path[i]
if ch == '/' or ch == '\\': start = i + 1
return _DupStd(path + start)
def _IsDir(path: str) -> int:
"""Whether path is an existing directory; 1=yes."""
if path is None: return 0
attr: ULONG = win32file.GetFileAttributesA(path)
if attr == 0xFFFFFFFF: return 0
if (attr & win32file.FILE_ATTRIBUTE_DIRECTORY) != 0: return 1
return 0
def _HasPrefix(s: str, prefix: str) -> int:
"""Whether s starts with prefix; 1=yes (None/empty/"." counts as true)."""
if s is None: return 0
if prefix is None: return 1
if string.strcmp(prefix, ".") == 0: return 1
if string.strncmp(s, "./", 2) == 0: s = s + 2
if string.strncmp(prefix, "./", 2) == 0: prefix = prefix + 2
pl: sizet = string.strlen(prefix)
if pl == 0: return 1
sl: sizet = string.strlen(s)
if sl < pl: return 0
if string.strncmp(s, prefix, pl) == 0: return 1
return 0
# Constant tables
def _FindInList(names: list[str]*, name: str) -> int:
"""Linear-search names for name; return its index, or -1 if not found."""
if names is None or name is None: return -1
n: sizet = names.__len__()
for i in range(int(n)):
s: str = names.get(i)
if s is not None:
if string.strcmp(s, name) == 0: return int(i)
return -1
def _LookupStrConst(name: str) -> int:
"""Resolve a name to a string constant in _g_out_str; 0=failure."""
global _g_out_str
idx: int = _FindInList(_f_str_names, name)
if idx < 0: return 0
v: str = _f_str_vals.get(sizet(idx))
if v is None: return 0
_g_out_str = v
return 1
def _LookupListConst(name: str, out: list[str]*) -> int:
"""Resolve a name to a list constant, split into out; 0=failure."""
idx: int = _FindInList(_f_list_names, name)
if idx < 0: return 0
v: str = _f_list_vals.get(sizet(idx))
if v is None: return 0
return _SplitInto(out, v)
def _SplitInto(out: list[str]*, joined: str) -> int:
"""Split a VB_SEP_CODE-joined string into out; empty string means empty list."""
if out is None or joined is None: return 0
n: sizet = string.strlen(joined)
if n == 0: return 1
start: sizet = 0
for i in range(int(n + 1)):
if i == n or joined[i] == char(VB_SEP_CODE):
seg: str = stdlib.malloc(i - start + 1)
if seg is None: return 0
string.strncpy(seg, joined + start, i - start)
seg[i - start] = '\0'
out.append(_DupScratch(seg))
stdlib.free(seg)
start = i + 1
return 1
def _JoinSep(lst: list[str]*) -> str:
"""Join a list[str] with VB_SEP_CODE (pool-allocated); None for an empty list."""
if lst is None: return None
n: sizet = lst.__len__()
if n == 0: return None
# Measure total length first
total: sizet = 0
i: sizet = 0
while i < n:
s: str = lst.get(i)
if s is not None: total += string.strlen(s) + 1
i += 1
if total == 0: return None
buf: str = _scratch.alloc(total + 1)
if buf is None: return None
pos: sizet = 0
for i in range(int(n)):
s = lst.get(i)
if s is not None:
sl: sizet = string.strlen(s)
if pos > 0:
buf[pos] = char(VB_SEP_CODE)
pos += 1
string.strcpy(buf + pos, s)
pos += sl
buf[pos] = '\0'
return buf
def _JoinSpace(lst: list[str]*) -> str:
"""Join a list[str] with spaces (stdlib-allocated); None for an empty list."""
if lst is None: return None
n: sizet = lst.__len__()
if n == 0: return None
total: sizet = 0
i: sizet = 0
while i < n:
s: str = lst.get(i)
if s is not None: total += string.strlen(s) + 1
i += 1
if total == 0: return None
buf: str = stdlib.malloc(total + 1)
if buf is None: return None
pos: sizet = 0
for i in range(int(n)):
s = lst.get(i)
if s is not None:
sl: sizet = string.strlen(s)
if pos > 0:
buf[pos] = ' '
pos += 1
string.strcpy(buf + pos, s)
pos += sl
buf[pos] = '\0'
return buf
def _FirstSpaceTok(joined: str) -> str:
"""Return the first space-separated token (stdlib-allocated); None if empty."""
if joined is None: return None
n: sizet = string.strlen(joined)
if n == 0: return None
sp: sizet = n
for i in range(int(n)):
if joined[i] == ' ':
sp = i
break
seg: str = stdlib.malloc(sp + 1)
if seg is None: return None
string.strncpy(seg, joined, sp)
seg[sp] = '\0'
return seg
# Constant expression evaluation
def _EvalStr(node: ast.AST*) -> int:
"""Evaluate a string constant expression into _g_out_str; 0=failure."""
global _g_out_str
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Constant:
cn: ast.Constant* = ast.Constant*(node)
if cn is None or cn.const_kind != ast.CONST_STR: return 0
_g_out_str = cn.str_val
return 1
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm is None or nm.id is None: return 0
return _LookupStrConst(nm.id)
if k == ast.ASTKind.BinOp:
bop: ast.BinOp* = ast.BinOp*(node)
if bop is None or bop.op != ast.OpKind.Add: return 0
if _EvalStr(bop.left) == 0: return 0
lv: str = _g_out_str
if _EvalStr(bop.right) == 0: return 0
rv: str = _g_out_str
_g_out_str = _ConcatStd(lv, rv)
return 1
return 0
def _ConcatStd(a: str, b: str) -> str:
"""Concatenate two strings with stdlib allocation."""
if a is None: return b
if b is None: return a
al: sizet = string.strlen(a)
bl: sizet = string.strlen(b)
buf: str = stdlib.malloc(al + bl + 1)
if buf is None: return None
string.strcpy(buf, a)
string.strcpy(buf + al, b)
return buf
def _EvalList(node: ast.AST*, out: list[str]*) -> int:
"""Evaluate a list[str] constant expression into out; 0=failure."""
if node is None or out is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm is None or nm.id is None: return 0
return _LookupListConst(nm.id, out)
if k == ast.ASTKind.List:
lst: ast.List* = ast.List*(node)
if lst is None or lst.elts is None: return 0
n: sizet = lst.elts.__len__()
for i in range(int(n)):
e: ast.AST* = lst.elts.get(i)
if e is None: return 0
if _IsListUnpack(e) != 0:
inner: ast.AST* = _ListUnpackOperand(e)
if inner is None: return 0
if _EvalList(inner, out) == 0: return 0
else:
if _EvalStr(e) == 0: return 0
out.append(_DupScratch(_g_out_str))
return 1
if k == ast.ASTKind.BinOp:
bop: ast.BinOp* = ast.BinOp*(node)
if bop is None or bop.op != ast.OpKind.Add: return 0
if _EvalList(bop.left, out) == 0: return 0
if _EvalList(bop.right, out) == 0: return 0
return 1
return 0
# Macro-list unpacking: `[*COMMON_FLAGS]` / `A + [*B]`.
# The parser has no Starred node in list displays: a leading `*` in an element position is
# parsed as a deref UnaryOp (expression-position `*x` is pointer deref). Inside a compile-time
# macro list the only meaningful interpretation is unpacking, so both shapes are accepted here.
# Keep in sync with _ListUnpackOperand.
def _IsListUnpack(node: ast.AST*) -> int:
"""Whether a list element is an unpacking form (`*X`); 1=yes / 0=no."""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Starred: return 1
if k == ast.ASTKind.UnaryOp:
uo: ast.UnaryOp* = ast.UnaryOp*(node)
if uo is not None and uo.op == ast.OpKind.Deref: return 1
return 0
def _ListUnpackOperand(node: ast.AST*) -> ast.AST*:
"""Operand of an unpacking element (`*X` -> X); None when node is not an unpacking form."""
if _IsListUnpack(node) == 0: return None
if node.kind() == ast.ASTKind.Starred:
st: ast.Starred* = ast.Starred*(node)
if st is None: return None
return st.value
uo: ast.UnaryOp* = ast.UnaryOp*(node)
if uo is None: return None
return uo.operand
def _EvalBool(node: ast.AST*) -> int:
"""Evaluate a bool constant; -1 means it is not a bool constant."""
if node is None: return -1
if node.kind() != ast.ASTKind.Constant: return -1
cn: ast.Constant* = ast.Constant*(node)
if cn is None or cn.const_kind != ast.CONST_BOOL: return -1
if cn.int_val != 0: return 1
return 0
# Macro-reference detection (reading is forbidden)
def _IsProjRef(node: ast.AST*) -> int:
"""Whether a node directly references a Project macro object; 1=yes."""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm is None or nm.id is None: return 0
if _FindInList(_proj_vars, nm.id) >= 0: return 1
return 0
if k == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(node)
if at is None or at.value is None: return 0
return _IsProjRef(at.value)
return 0
# Single-file scan
def _ResetFileTables(pool: memhub.MemBuddy*) -> int:
"""Rebuild the per-file constant tables (pool-allocated)."""
global _f_str_names, _f_str_vals, _f_list_names, _f_list_vals
_f_str_names = list[str](pool, 8)
_f_str_vals = list[str](pool, 8)
_f_list_names = list[str](pool, 8)
_f_list_vals = list[str](pool, 8)
if _f_str_names is None or _f_str_vals is None: return 1
if _f_list_names is None or _f_list_vals is None: return 1
return 0
def _AddPending(mod_name: str) -> None:
"""Append a module name to the BFS queue (deep-copied into the scratch pool)."""
if mod_name is None or _pending is None: return
if string.strlen(mod_name) == 0: return
_pending.append(_DupScratch(mod_name))
def _HandleImport(stmt: ast.Import*) -> int:
"""Handle `import X [as Y]`: detect viperbuild/ffilib, queue other modules for BFS."""
global _alias, _ffi_alias, _saw_viperbuild
if stmt is None or stmt.names is None: return 0
n: sizet = stmt.names.__len__()
for i in range(int(n)):
al: ast.Alias* = ast.Alias*(stmt.names.get(i))
if al is not None and al.name is not None:
if string.strcmp(al.name, "viperbuild") == 0:
_saw_viperbuild = 1
if al.asname is not None: _alias = _DupScratch(al.asname)
else: _alias = _DupScratch("viperbuild")
elif string.strcmp(al.name, "ffilib") == 0:
# 宏库:不参与 BFS,只记别名(CDLL 语句由 _HandleCDLL 抽取)。
# _ffilib 的依赖只在真正出现 runtime=True 的 CDLL 时才加(见 _RegisterFfiHandle),
# 纯链接期(runtime=False)的工程不需要编译这个运行时库。
if al.asname is not None: _ffi_alias = _DupScratch(al.asname)
else: _ffi_alias = _DupScratch("ffilib")
else: _AddPending(al.name)
return 0
def _HandleImportFrom(stmt: ast.ImportFrom*) -> int:
"""Handle `from X import Y`: detect viperbuild's Project, queue other modules."""
global _ctor_name, _saw_viperbuild
if stmt is None: return 0
if stmt.level != 0:
# Relative imports are not part of the local closure (no package context: skip)
return 0
if stmt.module is None: return 0
if string.strcmp(stmt.module, "viperbuild") != 0:
_AddPending(stmt.module)
return 0
_saw_viperbuild = 1
if stmt.names is None: return 0
n: sizet = stmt.names.__len__()
for i in range(int(n)):
al: ast.Alias* = ast.Alias*(stmt.names.get(i))
if al is not None and al.name is not None:
if string.strcmp(al.name, "Project") == 0:
if al.asname is not None: _ctor_name = _DupScratch(al.asname)
else: _ctor_name = _DupScratch("Project")
return 0
def _IsProjectCtor(vnode: ast.AST*) -> int:
"""Whether an expression is a vb.Project(...) or Project(...) call; 1=yes."""
if vnode is None: return 0
if vnode.kind() != ast.ASTKind.Call: return 0
cl: ast.Call* = ast.Call*(vnode)
if cl is None or cl.func is None: return 0
fk: int = cl.func.kind()
if fk == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(cl.func)
if at is None or at.attr is None or at.value is None: return 0
if at.value.kind() != ast.ASTKind.Name: return 0
bn: ast.Name* = ast.Name*(at.value)
if bn is None or bn.id is None or _alias is None: return 0
if string.strcmp(bn.id, _alias) != 0: return 0
if string.strcmp(at.attr, "Project") == 0: return 1
return 0
if fk == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(cl.func)
if nm is None or nm.id is None or _ctor_name is None: return 0
if string.strcmp(nm.id, _ctor_name) == 0: return 1
return 0
def _ApplyCtorArgs(cl: ast.Call*) -> int:
"""Extract name/version from Project(...) args into _c_name / _c_version."""
global _c_name, _c_version
if cl is None: return 0
if cl.args is not None:
an: sizet = cl.args.__len__()
if an > 2:
VLogger.error("viperbuild: Project(...) accepts at most 2 positional arguments (name, version)", "viperbuild")
return 1
if an >= 1:
if _EvalStr(cl.args.get(0)) == 0:
VLogger.error("viperbuild: Project(name) must be a string constant", "viperbuild")
return 1
_c_name = _DupStd(_g_out_str)
if an >= 2:
if _EvalStr(cl.args.get(1)) == 0:
VLogger.error("viperbuild: Project(version) must be a string constant", "viperbuild")
return 1
_c_version = _DupStd(_g_out_str)
if cl.keywords is None: return 0
n: sizet = cl.keywords.__len__()
for i in range(int(n)):
kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i))
if kw is not None and kw.arg is not None and kw.value is not None:
if string.strcmp(kw.arg, "name") == 0:
if _EvalStr(kw.value) == 0:
VLogger.error("viperbuild: Project(name=...) must be a string constant", "viperbuild")
return 1
_c_name = _DupStd(_g_out_str)
elif string.strcmp(kw.arg, "version") == 0:
if _EvalStr(kw.value) == 0:
VLogger.error("viperbuild: Project(version=...) must be a string constant", "viperbuild")
return 1
_c_version = _DupStd(_g_out_str)
else:
VLogger.error(f"viperbuild: unsupported keyword {kw.arg} for Project(...)", "viperbuild")
return 1
return 0
def _SetField(fname: str, vnode: ast.AST*) -> int:
"""Apply a proj.<fname> assignment or config(<fname>=...) keyword; 0=success."""
global _c_source, _c_buildroot, _c_includes, _c_compiler, _c_linker
global _c_static_libs, _c_dynamic_libs, _c_output, _c_name, _c_version
if fname is None or vnode is None: return 1
# ---- String fields ----
if string.strcmp(fname, "source") == 0:
if _EvalStr(vnode) == 0:
VLogger.error("viperbuild: source must be a string constant expression", "viperbuild")
return 1
_c_source = _DupStd(_g_out_str)
return 0
if string.strcmp(fname, "buildroot") == 0:
if _EvalStr(vnode) == 0:
VLogger.error("viperbuild: buildroot must be a string constant expression", "viperbuild")
return 1
_c_buildroot = _DupStd(_g_out_str)
return 0
if string.strcmp(fname, "output") == 0:
if _EvalStr(vnode) == 0:
VLogger.error("viperbuild: output must be a string constant expression", "viperbuild")
return 1
_c_output = _DupStd(_g_out_str)
return 0
if string.strcmp(fname, "name") == 0:
if _EvalStr(vnode) == 0:
VLogger.error("viperbuild: name must be a string constant expression", "viperbuild")
return 1
_c_name = _DupStd(_g_out_str)
return 0
if string.strcmp(fname, "version") == 0:
if _EvalStr(vnode) == 0:
VLogger.error("viperbuild: version must be a string constant expression", "viperbuild")
return 1
_c_version = _DupStd(_g_out_str)
return 0
# ---- List fields ----
if string.strcmp(fname, "includes") == 0: return _SetListField(0, vnode)
if string.strcmp(fname, "compiler") == 0: return _SetListField(1, vnode)
if string.strcmp(fname, "linker") == 0: return _SetListField(2, vnode)
if string.strcmp(fname, "static_libs") == 0: return _SetListField(3, vnode)
if string.strcmp(fname, "dynamic_libs") == 0: return _SetListField(4, vnode)
VLogger.error(f"viperbuild: unknown field proj.{fname}", "viperbuild")
return 1
def _SetListField(slot: int, vnode: ast.AST*) -> int:
"""Assign a list field (0=includes, 1=compiler, 2=linker, 3=static_libs, 4=dynamic_libs)."""
global _c_includes, _c_compiler, _c_linker, _c_static_libs, _c_dynamic_libs
if _scratch is None: return 1
tmp: list[str]* = list[str](_scratch, 8)
if tmp is None: return 1
if _EvalList(vnode, tmp) == 0:
VLogger.error("viperbuild: list fields must be list[str] constant expressions", "viperbuild")
return 1
joined: str = _JoinSpace(tmp)
if slot == 0: _c_includes = joined
elif slot == 1: _c_compiler = joined
elif slot == 2: _c_linker = joined
elif slot == 3: _c_static_libs = joined
else: _c_dynamic_libs = joined
return 0
def _HandleAssign(stmt: ast.Assign*) -> int:
"""Handle an assignment: constant, Project instance, or proj field."""
if stmt is None or stmt.targets is None or stmt.value is None: return 0
if stmt.targets.__len__() != 1: return 0
tgt: ast.AST* = stmt.targets.get(0)
if tgt is None: return 0
tk: int = tgt.kind()
if tk == ast.ASTKind.Name: return _HandleNameBind(ast.Name*(tgt), stmt.value)
if tk == ast.ASTKind.Attribute: return _HandleAttrAssign(ast.Attribute*(tgt), stmt.value)
return 0
def _HandleAnnAssign(stmt: ast.AnnAssign*) -> int:
"""Handle an annotated assignment (including define-desugared AnnAssign)."""
if stmt is None or stmt.target is None or stmt.value is None: return 0
if stmt.target.kind() != ast.ASTKind.Name: return 0
return _HandleNameBind(ast.Name*(stmt.target), stmt.value)
def _HandleNameBind(tgt: ast.Name*, vnode: ast.AST*) -> int:
"""Handle NAME = value: Project binding, string constant, or list constant."""
global _c_has_project
if tgt is None or vnode is None or tgt.id is None: return 0
# Reading a Project macro object on the RHS is fatal (reading forbidden)
if _IsProjRef(vnode) != 0:
VLogger.error(f"viperbuild: reading the Project macro object is forbidden ({tgt.id} = proj ...)", "viperbuild")
return 1
# Project instance binding
if _IsProjectCtor(vnode) != 0:
if _c_has_project != 0:
VLogger.error("viperbuild: only one Project object is allowed per dependency closure", "viperbuild")
return 1
_c_has_project = 1
_proj_vars.append(_DupScratch(tgt.id))
cl: ast.Call* = ast.Call*(vnode)
return _ApplyCtorArgs(cl)
# String constant
if _EvalStr(vnode) != 0:
_f_str_names.append(tgt.id)
_f_str_vals.append(_DupScratch(_g_out_str))
return 0
# List constant
if _scratch is not None:
tmp: list[str]* = list[str](_scratch, 8)
if tmp is not None:
if _EvalList(vnode, tmp) != 0:
_f_list_names.append(tgt.id)
_f_list_vals.append(_JoinSep(tmp))
return 0
tmp.clear()
return 0
def _SetAdvancedField(fname: str, vnode: ast.AST*) -> int:
"""Apply proj.advanced.<fname> = <bool constant>; 0=success."""
global _c_adv_asan, _c_adv_traceback
if fname is None or vnode is None: return 1
b: int = _EvalBool(vnode)
if b < 0:
VLogger.error(f"viperbuild: proj.advanced.{fname} must be a boolean constant (True/False)", "viperbuild")
return 1
if string.strcmp(fname, "asan") == 0:
_c_adv_asan = b
return 0
if string.strcmp(fname, "traceback") == 0:
_c_adv_traceback = b
return 0
VLogger.error(f"viperbuild: unknown field proj.advanced.{fname} (supported: asan / traceback)", "viperbuild")
return 1
def _SetCleanField(fname: str, vnode: ast.AST*) -> int:
"""Apply proj.clean.<fname> = <bool constant>; 0=success."""
global _c_clean_all, _c_clean_output, _c_clean_temp, _c_clean_build_dir, _c_clean_includes
if fname is None or vnode is None: return 1
b: int = _EvalBool(vnode)
if b < 0:
VLogger.error(f"viperbuild: proj.clean.{fname} must be a boolean constant (True/False)", "viperbuild")
return 1
if string.strcmp(fname, "all") == 0:
_c_clean_all = b
return 0
if string.strcmp(fname, "output") == 0:
_c_clean_output = b
return 0
if string.strcmp(fname, "temp") == 0:
_c_clean_temp = b
return 0
if string.strcmp(fname, "build_dir") == 0:
_c_clean_build_dir = b
return 0
if string.strcmp(fname, "includes") == 0:
_c_clean_includes = b
return 0
VLogger.error(f"viperbuild: unknown field proj.clean.{fname} (supported: all / output / temp / build_dir / includes)", "viperbuild")
return 1
def _HandleAttrAssign(tgt: ast.Attribute*, vnode: ast.AST*) -> int:
"""Handle proj.<field> = <const>, proj.advanced.<field> / proj.clean.<field> = <bool>."""
if tgt is None or tgt.attr is None or tgt.value is None: return 0
# Nested form: proj.advanced.{asan,traceback} / proj.clean.{output,temp,build_dir,includes}
if tgt.value.kind() == ast.ASTKind.Attribute:
mid: ast.Attribute* = ast.Attribute*(tgt.value)
if mid is None or mid.attr is None or mid.value is None: return 0
if mid.value.kind() != ast.ASTKind.Name: return 0
mn: ast.Name* = ast.Name*(mid.value)
if mn is None or mn.id is None: return 0
if _FindInList(_proj_vars, mn.id) < 0: return 0
if string.strcmp(mid.attr, "advanced") != 0 and string.strcmp(mid.attr, "clean") != 0:
VLogger.error(f"viperbuild: unknown nested field proj.{mid.attr}.{tgt.attr}", "viperbuild")
return 1
if _IsProjRef(vnode) != 0:
VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild")
return 1
if string.strcmp(mid.attr, "clean") == 0: return _SetCleanField(tgt.attr, vnode)
return _SetAdvancedField(tgt.attr, vnode)
if tgt.value.kind() != ast.ASTKind.Name: return 0
bn: ast.Name* = ast.Name*(tgt.value)
if bn is None or bn.id is None: return 0
if _FindInList(_proj_vars, bn.id) < 0: return 0
if _IsProjRef(vnode) != 0:
VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild")
return 1
return _SetField(tgt.attr, vnode)
def _HandleConfigCall(cl: ast.Call*) -> int:
"""Handle a proj.config(kw=...) call."""
if cl is None or cl.keywords is None: return 0
n: sizet = cl.keywords.__len__()
for i in range(int(n)):
kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i))
if kw is not None and kw.arg is not None and kw.value is not None:
if _SetField(kw.arg, kw.value) != 0: return 1
return 0
def _JoinRunArgs(lst: list[str]*) -> str:
"""Join run args for CreateProcess: elements containing blanks are wrapped in double quotes.
Embedded double quotes are rejected (fail-fast) — Windows argv quoting rules would make the
actual argv ambiguous, and macro config should not silently alter the user's arguments.
"""
if lst is None: return None
n: sizet = len(lst)
if n == 0: return None
total: sizet = 0
for i in range(int(n)):
s: str = lst.get(sizet(i))
if s is None: continue
total += string.strlen(s) + 3
buf: str = stdlib.malloc(total + 1)
if buf is None: return None
pos: sizet = 0
for i in range(int(n)):
s = lst.get(sizet(i))
if s is None: continue
sl: sizet = string.strlen(s)
need_quote: int = 0
j: sizet = 0
while j < sl:
if s[j] == ' ' or s[j] == '\t':
need_quote = 1
if s[j] == '"':
stdlib.free(buf)
return None
j += 1
if pos > 0:
buf[pos] = ' '
pos += 1
if need_quote != 0:
buf[pos] = '"'
pos += 1
string.strcpy(buf + pos, s)
pos += sl
if need_quote != 0:
buf[pos] = '"'
pos += 1
buf[pos] = '\0'
return buf
def _HandleBuildCall(cl: ast.Call*) -> int:
"""Handle a proj.build(triple=, datalayout=, selfmachine=, run=, args=) call."""
global _c_triple, _c_datalayout, _c_selfmachine, _c_has_build, _c_run, _c_run_args
if cl is None: return 1
if _c_has_build != 0:
VLogger.error("viperbuild: proj.build() may be called only once", "viperbuild")
return 1
_c_has_build = 1
if cl.keywords is None: return 0
n: sizet = cl.keywords.__len__()
for i in range(int(n)):
kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i))
if kw is not None and kw.arg is not None and kw.value is not None:
if string.strcmp(kw.arg, "triple") == 0:
if _EvalStr(kw.value) == 0:
VLogger.error("viperbuild: build(triple=...) must be a string constant", "viperbuild")
return 1
_c_triple = _DupStd(_g_out_str)
elif string.strcmp(kw.arg, "datalayout") == 0:
if _EvalStr(kw.value) == 0:
VLogger.error("viperbuild: build(datalayout=...) must be a string constant", "viperbuild")
return 1
_c_datalayout = _DupStd(_g_out_str)
elif string.strcmp(kw.arg, "selfmachine") == 0:
b: int = _EvalBool(kw.value)
if b < 0:
VLogger.error("viperbuild: build(selfmachine=...) must be a boolean constant", "viperbuild")
return 1
_c_selfmachine = b
elif string.strcmp(kw.arg, "run") == 0:
rb: int = _EvalBool(kw.value)
if rb < 0:
VLogger.error("viperbuild: build(run=...) must be a boolean constant", "viperbuild")
return 1
_c_run = rb
elif string.strcmp(kw.arg, "args") == 0:
if _scratch is None: return 1
tmp_args: list[str]* = list[str](_scratch, 8)
if tmp_args is None: return 1
if _EvalList(kw.value, tmp_args) == 0:
VLogger.error("viperbuild: build(args=...) must be a list[str] constant expression", "viperbuild")
return 1
joined_args: str = _JoinRunArgs(tmp_args)
# None + non-empty list = element rejected (embedded quote); empty list is legal.
if joined_args is None and len(tmp_args) > 0:
VLogger.error("viperbuild: build(args=...) has an element that cannot be passed through (embedded double quote); quote it in code or avoid it", "viperbuild")
return 1
_c_run_args = joined_args
# Non-empty args imply run: `build(args=["--a"])` == run the exe with --a.
if _c_run_args is not None and string.strlen(_c_run_args) > 0: _c_run = 1
else:
VLogger.error(f"viperbuild: unsupported keyword {kw.arg} for build(...)", "viperbuild")
return 1
return 0
def _CallArgsHaveProjRef(cl: ast.Call*) -> int:
"""Whether any call argument references a Project macro object; 1=yes."""
if cl is None: return 0
if cl.args is not None:
n: sizet = cl.args.__len__()
for i in range(int(n)):
if _IsProjRef(cl.args.get(i)) != 0: return 1
if cl.keywords is not None:
n = cl.keywords.__len__()
for i in range(int(n)):
kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i))
if kw is not None and kw.value is not None:
if _IsProjRef(kw.value) != 0: return 1
return 0
def _HandleExprStmt(stmt: ast.Expr*) -> int:
"""Handle an expression statement: proj.config/build calls, else check reads."""
if stmt is None or stmt.value is None: return 0
v: ast.AST* = stmt.value
if v.kind() != ast.ASTKind.Call: return 0
cl: ast.Call* = ast.Call*(v)
if cl is None or cl.func is None: return 0
# Non-Project method call: if an argument reads the macro object -> fatal (reading forbidden)
if cl.func.kind() != ast.ASTKind.Attribute:
if _CallArgsHaveProjRef(cl) != 0:
VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild")
return 1
return 0
at: ast.Attribute* = ast.Attribute*(cl.func)
if at is None or at.attr is None or at.value is None: return 0
if at.value.kind() != ast.ASTKind.Name:
if _CallArgsHaveProjRef(cl) != 0:
VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild")
return 1
return 0
bn: ast.Name* = ast.Name*(at.value)
if bn is None or bn.id is None: return 0
if _FindInList(_proj_vars, bn.id) < 0:
if _CallArgsHaveProjRef(cl) != 0:
VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild")
return 1
return 0
if string.strcmp(at.attr, "config") == 0: return _HandleConfigCall(cl)
if string.strcmp(at.attr, "build") == 0: return _HandleBuildCall(cl)
VLogger.error(f"viperbuild: unsupported method {at.attr}() on Project object", "viperbuild")
return 1
# ---------------- ffilib.CDLL(纯构建配置) ----------------
# _BoolConst - 布尔字面量 -> 1(True) / 0(False) / -1(不是布尔常量)
def _BoolConst(node: ast.AST*) -> int:
"""布尔常量求值;非布尔常量返回 -1"""
if node is None or node.kind() != ast.ASTKind.Constant: return -1
c: ast.Constant* = ast.Constant*(node)
if c is None or c.const_kind != ast.CONST_BOOL: return -1
if c.int_val != 0: return 1
return 0
# _AppendLibPath - 把路径追加到 static_libs(slot=0) / dynamic_libs(slot=1) 的空格拼接串
def _AppendLibPath(slot: int, path: str) -> int:
"""追加库路径;0=成功"""
global _c_static_libs, _c_dynamic_libs
if path is None: return 1
cur: str = _c_static_libs
if slot == 1: cur = _c_dynamic_libs
pl: sizet = string.strlen(path)
if pl == 0: return 0
ol: sizet = 0
if cur is not None: ol = string.strlen(cur)
buf: str = stdlib.malloc(ol + pl + 2)
if buf is None: return 1
pos: sizet = 0
if ol > 0:
string.strcpy(buf, cur)
buf[ol] = ' '
pos = ol + 1
string.strcpy(buf + pos, path)
if slot == 1: _c_dynamic_libs = buf
else: _c_static_libs = buf
return 0
# _IsCDLLCall - node 是否是 ffilib.CDLL(...)(别名/全名都认)
def _IsCDLLCall(node: ast.AST*) -> int:
"""是 ffilib.CDLL 调用时返回 1"""
if node is None or node.kind() != ast.ASTKind.Call: return 0
cl: ast.Call* = ast.Call*(node)
if cl is None or cl.func is None: return 0
if cl.func.kind() != ast.ASTKind.Attribute: return 0
at: ast.Attribute* = ast.Attribute*(cl.func)
if at is None or at.attr is None or string.strcmp(at.attr, "CDLL") != 0: return 0
if at.value is None or at.value.kind() != ast.ASTKind.Name: return 0
nm: ast.Name* = ast.Name*(at.value)
if nm.id is None: return 0
if _ffi_alias is not None and string.strcmp(nm.id, _ffi_alias) == 0: return 1
if string.strcmp(nm.id, "ffilib") == 0: return 1
return 0
# _RegisterFfiHandle - 登记 CDLL 句柄(变量名 -> runtime + 库路径)
def _RegisterFfiHandle(handle: str, runtime: int, path: str) -> None:
"""登记 ffilib.CDLL 绑定的变量名、runtime 标志与库路径;句柄名为 None 时忽略"""
global _ffi_handles_names, _ffi_handles_rt, _ffi_handles_path
if handle is None or _ffi_handles_names is None or _ffi_handles_rt is None or _ffi_handles_path is None: return
nm: str = _DupScratch(handle)
if nm is None: return
pth: str = _DupScratch(path)
_ffi_handles_names.append(nm)
_ffi_handles_rt.append(runtime)
_ffi_handles_path.append(pth)
# runtime=True:thunk 运行期要调 _ffilib.Load/Sym,把该运行时库加入依赖闭包
# (按需可达性扫描只收录被 import 的模块,否则 _resolve_module_sha1("_ffilib") 为 None)。
if runtime != 0: _AddPending("_ffilib")
# _AssignTargetName - 取 `x = ...` 的赋值目标名;非简单 Name 时返回 None
def _AssignTargetName(asn: ast.Assign*) -> str:
"""赋值目标名;多目标/非 Name 返回 None"""
if asn is None or asn.targets is None: return None
if len(asn.targets) != 1: return None
t: ast.AST* = asn.targets.get(0)
if t is None or t.kind() != ast.ASTKind.Name: return None
tn: ast.Name* = ast.Name*(t)
if tn is None: return None
return tn.id
# _HandleCDLL - 按实参形态写入静态/动态库表;list 回退形式本期 fail-fast
def _HandleCDLL(node: ast.AST*, handle: str) -> int:
"""抽取 ffilib.CDLL(...):0=成功"""
if node is None or node.kind() != ast.ASTKind.Call: return 0
call: ast.Call* = ast.Call*(node)
args: list[ast.AST*]* = call.args
if args is None or len(args) < 1:
VLogger.error("ffilib: CDLL(...) requires at least one argument", "ffilib")
return 1
a0: ast.AST* = args.get(0)
if a0 is None: return 1
# 1) tag 形式:Project.StaticLib(path) / Project.DynamicLib(path)
if a0.kind() == ast.ASTKind.Call:
tag: ast.Call* = ast.Call*(a0)
is_static: int = 0
is_dynamic: int = 0
if tag is not None and tag.func is not None and tag.func.kind() == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(tag.func)
if at is not None and at.attr is not None:
if string.strcmp(at.attr, "StaticLib") == 0: is_static = 1
elif string.strcmp(at.attr, "DynamicLib") == 0: is_dynamic = 1
if is_static == 0 and is_dynamic == 0:
VLogger.error("ffilib: CDLL(tag) expects Project.StaticLib(path) or Project.DynamicLib(path)", "ffilib")
return 1
if tag.args is None or len(tag.args) < 1:
VLogger.error("ffilib: StaticLib/DynamicLib requires a path argument", "ffilib")
return 1
p0: ast.AST* = tag.args.get(0)
if p0 is None or p0.kind() != ast.ASTKind.Constant:
VLogger.error("ffilib: library path must be a string constant", "ffilib")
return 1
c0: ast.Constant* = ast.Constant*(p0)
if c0 is None or c0.const_kind != ast.CONST_STR:
VLogger.error("ffilib: library path must be a string constant", "ffilib")
return 1
if is_static != 0:
rc_t: int = _AppendLibPath(0, c0.str_val)
if rc_t == 0: _RegisterFfiHandle(handle, 0, c0.str_val)
return rc_t
rc_td: int = _AppendLibPath(1, c0.str_val)
if rc_td == 0: _RegisterFfiHandle(handle, 1, c0.str_val)
return rc_td
# 2) 回退列表(预留):需要 _ffilib 运行时库,本期编译期拦截
if a0.kind() == ast.ASTKind.List:
VLogger.error("ffilib: CDLL(list[str], runtime) fallback-list form is not implemented yet (requires the _ffilib runtime library)", "ffilib")
return 1
# 3) 字符串 + runtime(位置参数或 runtime= 关键字)
if a0.kind() != ast.ASTKind.Constant:
VLogger.error("ffilib: CDLL(path, runtime) path must be a string constant", "ffilib")
return 1
c1: ast.Constant* = ast.Constant*(a0)
if c1 is None or c1.const_kind != ast.CONST_STR:
VLogger.error("ffilib: CDLL path must be a string constant", "ffilib")
return 1
rt: int = -1
if len(args) >= 2: rt = _BoolConst(args.get(1))
if rt < 0 and call.keywords is not None:
kn: sizet = len(call.keywords)
for ki in range(int(kn)):
kw: ast.Keyword* = ast.Keyword*(call.keywords.get(ki))
if kw is None or kw.arg is None: continue
if string.strcmp(kw.arg, "runtime") == 0:
rt = _BoolConst(kw.value)
break
if rt < 0:
VLogger.error("ffilib: CDLL(path) needs a constant runtime=True/False, or use Project.StaticLib/DynamicLib", "ffilib")
return 1
if rt == 1:
# runtime=True:不进链接输入(运行期由 thunk 用绝对路径 LoadLibraryA),
# 只登记句柄与路径;否则会同时生成导入表依赖,与 thunk 重复。
_RegisterFfiHandle(handle, 1, c1.str_val)
return 0
rc_s: int = _AppendLibPath(0, c1.str_val)
if rc_s == 0: _RegisterFfiHandle(handle, 0, c1.str_val)
return rc_s
# IsCdlLDeclCall - 供 Gen 层判断"这条模块级赋值是 ffilib.CDLL 声明"(纯编译期配置,不能生成全局)
def IsCdlLDeclCall(node: ast.AST*) -> int:
"""node 是 ffilib.CDLL(...) 调用时返回 1"""
return _IsCDLLCall(node)
# HasRuntimeFfiHandle - 是否存在 runtime=True 的 CDLL 句柄(决定是否要引入 _ffilib 依赖)
def HasRuntimeFfiHandle() -> int:
"""至少一个句柄 runtime=1 时返回 1"""
global _ffi_handles_rt
if _ffi_handles_rt is None: return 0
n: sizet = _ffi_handles_rt.__len__()
for i in range(int(n)):
if _ffi_handles_rt.get(i) != 0: return 1
return 0
# FfiHandlePath - 查询 extern <lib> name(...) 的库路径(runtime=True 的 thunk 用它 LoadLibraryA)
def FfiHandlePath(name: str) -> str:
"""库路径;未登记返回 None"""
global _ffi_handles_names, _ffi_handles_path
if name is None or _ffi_handles_names is None or _ffi_handles_path is None: return None
n: sizet = _ffi_handles_names.__len__()
for i in range(int(n)):
nm: str = _ffi_handles_names.get(i)
if nm is not None and string.strcmp(nm, name) == 0: return _ffi_handles_path.get(i)
return None
# FfiHandleRuntime - 查询 extern <lib> name(...) 的库句柄标志
def FfiHandleRuntime(name: str) -> int:
"""1=运行时加载 / 0=链接期 / -1=未登记(不是 ffilib.CDLL 绑定的变量)"""
global _ffi_handles_names, _ffi_handles_rt
if name is None or _ffi_handles_names is None or _ffi_handles_rt is None: return -1
n: sizet = _ffi_handles_names.__len__()
for i in range(int(n)):
nm: str = _ffi_handles_names.get(i)
if nm is not None and string.strcmp(nm, name) == 0: return _ffi_handles_rt.get(i)
return -1
def _ScanFile(pool: memhub.MemBuddy*, path: str) -> int:
"""Parse and scan one .vp file; 0=ok, 1=failed, 2=skippable (unreadable/not .vp)."""
if path is None: return 2
sl: sizet = string.strlen(path)
if sl == 0: return 2
if Utils.is_vp_at(path, 0, sl) == 0:
# Not .vp: excluded from the closure (.vpi/.vpnci invisible to the compiler)
return 2
src_buf: bytes = Utils.read_file_to_buf(path, VB_SRC_CAP)
if src_buf is None:
VLogger.warning(f"viperbuild: cannot read dependency file {path}", "viperbuild")
return 2
if _ResetFileTables(pool) != 0:
stdlib.free(src_buf)
VLogger.error("viperbuild: constant table allocation failed", "viperbuild")
return 1
mark: sizet = pool.mark()
lx: ast.Lexer* = ast.new_lexer(pool)
if lx is None:
pool.reset_to(mark)
stdlib.free(src_buf)
VLogger.error("viperbuild: lexer allocation failed", "viperbuild")
return 1
ast._lexer_init(lx, src_buf, pool)
tokens: ast.Token* = ast.tokenize(lx)
if tokens is None:
pool.reset_to(mark)
stdlib.free(src_buf)
VLogger.error(f"viperbuild: tokenization failed {path}", "viperbuild")
return 1
tree: ast.AST* = ast.parse_tokens(pool, tokens)
if tree is None:
pool.reset_to(mark)
stdlib.free(src_buf)
VLogger.error(f"viperbuild: parsing failed {path}", "viperbuild")
return 1
body: list[ast.AST*]* = tree.children
rc: int = 0
if body is not None:
cn: sizet = len(body)
for ci in range(int(cn)):
child: ast.AST* = body.get(ci)
if child is not None:
kd: int = child.kind()
if kd == ast.ASTKind.Import: _HandleImport(ast.Import*(child))
elif kd == ast.ASTKind.ImportFrom: _HandleImportFrom(ast.ImportFrom*(child))
elif kd == ast.ASTKind.Assign:
asn_f: ast.Assign* = ast.Assign*(child)
if asn_f is not None and _IsCDLLCall(asn_f.value) != 0: rc = _HandleCDLL(asn_f.value, _AssignTargetName(asn_f))
else: rc = _HandleAssign(asn_f)
elif kd == ast.ASTKind.AnnAssign: rc = _HandleAnnAssign(ast.AnnAssign*(child))
elif kd == ast.ASTKind.Expr:
ex_f: ast.Expr* = ast.Expr*(child)
if ex_f is not None and _IsCDLLCall(ex_f.value) != 0: rc = _HandleCDLL(ex_f.value, None)
else: rc = _HandleExprStmt(ex_f)
if rc != 0: break
pool.reset_to(mark)
stdlib.free(src_buf)
return rc
# Dependency closure BFS
def _ResolveModuleToFile(module_name: str) -> str:
"""Resolve a module name to a local project file path; None if not found."""
if module_name is None or _search_dirs is None: return None
if string.strlen(module_name) == 0: return None
out_abs: rawptr = stdlib.malloc(8)
out_rel: rawptr = stdlib.malloc(8)
out_inc: rawptr = stdlib.malloc(8)
if out_abs is None or out_rel is None or out_inc is None:
if out_abs is not None: stdlib.free(out_abs)
if out_rel is not None: stdlib.free(out_rel)
if out_inc is not None: stdlib.free(out_inc)
return None
abs_slot: sizet* = (sizet*)(out_abs)
rel_slot: sizet* = (sizet*)(out_rel)
inc_slot: sizet* = (sizet*)(out_inc)
abs_slot[0] = 0
rel_slot[0] = 0
inc_slot[0] = 0
rcode: int = IncludesScanner.ResolveModuleToPath(_search_dirs, module_name, out_abs, out_rel, out_inc)
result: str = None
if rcode == 0 and abs_slot[0] != 0:
abs_path: str = rawptr(abs_slot[0])
if abs_path is not None: result = _DupScratch(abs_path)
if abs_slot[0] != 0: stdlib.free(rawptr(abs_slot[0]))
if rel_slot[0] != 0: stdlib.free(rawptr(rel_slot[0]))
if inc_slot[0] != 0: stdlib.free(rawptr(inc_slot[0]))
stdlib.free(out_abs)
stdlib.free(out_rel)
stdlib.free(out_inc)
return result
def _Enqueue(path: str) -> None:
"""Add a file path to the BFS queue (deduped against worklist and visited)."""
if path is None or _work is None: return
if _FindInList(_visited, path) >= 0: return
if _FindInList(_work, path) >= 0: return
_work.append(_DupScratch(path))
def _RunBfs(pool: memhub.MemBuddy*, entry_path: str) -> int:
"""Expand the dependency closure from the entry file; 0=success."""
global _pending
_work.append(_DupScratch(entry_path))
while _work.__len__() > 0:
cur: str = _work.pop()
if cur is None: continue
if _FindInList(_visited, cur) >= 0: continue
if _visited.__len__() >= VB_MAX_VISIT:
VLogger.error(f"viperbuild: dependency closure exceeds {VB_MAX_VISIT} files, likely a cycle", "viperbuild")
return 1
_visited.append(_DupScratch(cur))
_pending = list[str](_scratch, 8)
if _pending is None:
VLogger.error("viperbuild: BFS queue allocation failed", "viperbuild")
return 1
rc: int = _ScanFile(pool, cur)
if rc == 1: return 1
# Resolve module names collected from this file
pn: sizet = _pending.__len__()
for pi in range(int(pn)):
mod: str = _pending.get(pi)
if mod is not None:
dep: str = _ResolveModuleToFile(mod)
if dep is not None: _Enqueue(dep)
return 0
# Defaults / validation / apply
def _DetectHost() -> int:
"""Detect the host triple/datalayout via PROCESSOR_ARCHITECTURE; 0=success."""
global _c_triple, _c_datalayout
buf: CHARPTR = stdlib.malloc(64)
if buf is None:
VLogger.error("viperbuild: selfmachine probe buffer allocation failed", "viperbuild")
return 1
buf[0] = char(0)
n: DWORD = GetEnvironmentVariableA("PROCESSOR_ARCHITECTURE", buf, DWORD(64))
if n == 0 or n >= DWORD(64):
stdlib.free(buf)
VLogger.error("viperbuild: selfmachine probe failed (PROCESSOR_ARCHITECTURE unavailable)", "viperbuild")
return 1
buf[int(n)] = char(0)
# "AMD64\0" (A=65 M=77 D=68 6=54 4=52)
is_amd64: int = 0
if buf[0] == char(65) and buf[1] == char(77) and buf[2] == char(68) and buf[3] == char(54) and buf[4] == char(52) and buf[5] == char(0): is_amd64 = 1
if is_amd64 != 0:
_c_triple = "x86_64-pc-windows-gnu"
_c_datalayout = "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
stdlib.free(buf)
return 0
stdlib.free(buf)
VLogger.error("viperbuild: selfmachine does not support the host architecture yet (currently only AMD64)", "viperbuild")
return 1
def _ProbeIncludes() -> str:
"""Probe up to 3 levels above the exe dir for an includes directory; else None."""
exe_buf: bytes = stdlib.malloc(VB_PATH_CAP)
if exe_buf is None: return None
exe_buf[0] = '\0'
n_exe: ULONG = win32file.GetModuleFileNameA(None, exe_buf, ULONG(VB_PATH_CAP))
if n_exe <= 0:
stdlib.free(exe_buf)
return None
el: int = int(n_exe)
if el >= VB_PATH_CAP: el = VB_PATH_CAP - 1
exe_buf[el] = '\0'
exe_dir: str = _DirOf(exe_buf)
stdlib.free(exe_buf)
if exe_dir is None: return None
for k in range(int(3 + 1)):
base: str = _DupStd(exe_dir)
for j in range(int(k)):
base = _Join2(base, "..")
cand: str = _Join2(base, "includes")
if _IsDir(cand) != 0: return cand
return None
def _DeriveOutputFromEntry(entry_path: str) -> str:
"""Derive the default output name from the entry file name."""
base: str = _BaseNameOf(entry_path)
if base is None: return "app.exe"
n: sizet = string.strlen(base)
cut: sizet = n
for i in range(int(n)):
if base[i] == '.':
cut = i
break
head: str = stdlib.malloc(cut + 5)
if head is None: return "app.exe"
string.strncpy(head, base, cut)
head[cut] = '.'
head[cut + 1] = 'e'
head[cut + 2] = 'x'
head[cut + 3] = 'e'
head[cut + 4] = '\0'
return head
def _FinalizeCfg(entry_path: str) -> int:
"""Fill defaults, validate and write back into Config; 0=success."""
global _c_source, _c_buildroot, _c_includes, _c_compiler, _c_linker
global _c_static_libs, _c_dynamic_libs, _c_output, _c_triple, _c_datalayout
global _c_selfmachine
if _c_has_project == 0:
VLogger.error("viperbuild: no vb.Project(...) definition found in the dependency closure", "viperbuild")
return 1
if _c_has_build == 0:
VLogger.error("viperbuild: proj.build(...) was not called, cannot determine the build target", "viperbuild")
return 1
# ---- Defaults ----
if _c_source is None: _c_source = _DupStd(_entry_dir)
if _c_buildroot is None: _c_buildroot = _Join2(_c_source, "__vpcbuild__")
if _c_includes is None:
probed: str = _ProbeIncludes()
if probed is None:
VLogger.error("viperbuild: could not probe an includes directory (no includes up to 0..3 levels above the exe directory)", "viperbuild")
return 1
_c_includes = probed
if _c_compiler is None: _c_compiler = _DupStd("llc -filetype=obj -relocation-model=pic")
if _c_linker is None:
# Built-in default = the verified link args from this repo's ViperC/project.vpj.
# Do NOT add -lgcc: msys64/mingw64 + lld has no libgcc and fails with "unable to find library -lgcc".
_c_linker = _DupStd("clang++ -fuse-ld=lld -lmsvcrt -lucrt -lpthread -lmingwex -lkernel32 -Wl,--stack,0x1000000 -Wl,--no-insert-timestamp -Wl,--export-all-symbols -g")
if _c_output is None: _c_output = _DeriveOutputFromEntry(entry_path)
# ---- Entry file must be under source ----
if _HasPrefix(entry_path, _c_source) == 0:
VLogger.error(f"viperbuild: entry file is not under the source directory\n entry: {entry_path}\n source: {_c_source}", "viperbuild")
return 1
# ---- Target platform ----
explicit_triple: int = 0
if _c_triple is not None or _c_datalayout is not None: explicit_triple = 1
if _c_selfmachine != 0 and explicit_triple != 0:
VLogger.error("viperbuild: build(selfmachine=True) is mutually exclusive with an explicit triple/datalayout", "viperbuild")
return 1
if explicit_triple != 0:
if _c_triple is None or _c_datalayout is None:
VLogger.error("viperbuild: build() triple and datalayout must be given as a pair", "viperbuild")
return 1
else:
# Neither given, or selfmachine=True: detect the host at runtime
if _DetectHost() != 0: return 1
# ---- Write back to Config ----
Config.ProjectName = _c_name
Config.ProjectVersion = _c_version
Config.SourceDir = _c_source
Config.BuildDir = _c_buildroot
Config.IncludesDirs = _c_includes
Config.IncludesDir = _FirstSpaceTok(_c_includes)
if Config.IncludesDir is None: Config.IncludesDir = _c_includes
Config.TargetTriple = _c_triple
Config.TargetDataLayout = _c_datalayout
Config.LinkerStaticLibs = _c_static_libs
Config.LinkerDynamicLibs = _c_dynamic_libs
Config.LinkerOutput = _c_output
Config.Sha1SliceLevel = 1
Config.StrictMode = 1
_SplitToolCmd(_c_compiler, 0)
_SplitToolCmd(_c_linker, 1)
Config.resolve_paths(_c_source)
return 0
def _SplitToolCmd(joined: str, which: int) -> None:
"""Split a space-joined tool command into Config Cmd/Flags (0=compiler, 1=linker)."""
if joined is None: return
n: sizet = string.strlen(joined)
sp: sizet = n
for i in range(int(n)):
if joined[i] == ' ':
sp = i
break
cmd: str = stdlib.malloc(sp + 1)
if cmd is not None:
string.strncpy(cmd, joined, sp)
cmd[sp] = '\0'
flags: str = None
if sp < n: flags = _DupStd(joined + sp + 1)
if which == 0:
Config.CompilerCmd = cmd
Config.CompilerFlags = flags
else:
Config.LinkerCmd = cmd
Config.LinkerFlags = flags
# Entry point
def _ResetAll() -> None:
"""Reset module state before each DiscoverAndApply call."""
global _alias, _ctor_name, _g_out_str, _applied, _entry_dir, _search_dirs
global _c_name, _c_version, _c_source, _c_buildroot, _c_includes
global _c_compiler, _c_linker, _c_static_libs, _c_dynamic_libs, _c_output, _ffi_alias
global _ffi_handles_names, _ffi_handles_rt, _ffi_handles_path
global _c_triple, _c_datalayout, _c_selfmachine, _c_has_project, _c_has_build
global _c_adv_asan, _c_adv_traceback
global _c_clean_all, _c_clean_output, _c_clean_temp, _c_clean_build_dir, _c_clean_includes
global _c_run, _c_run_args
global _saw_viperbuild
_alias = None
_ctor_name = None
_g_out_str = None
_applied = 0
_entry_dir = None
_search_dirs = None
_saw_viperbuild = 0
_c_name = None
_c_version = None
_c_source = None
_c_buildroot = None
_c_includes = None
_c_compiler = None
_c_linker = None
_c_static_libs = None
_c_dynamic_libs = None
_ffi_alias = None
_ffi_handles_names = None
_ffi_handles_rt = None
_ffi_handles_path = None
_c_output = None
_c_triple = None
_c_datalayout = None
_c_selfmachine = 0
_c_has_project = 0
_c_has_build = 0
_c_adv_asan = 0
_c_adv_traceback = 0
_c_clean_all = 0
_c_clean_output = 0
_c_clean_temp = 0
_c_clean_build_dir = 0
_c_clean_includes = 0
_c_run = 0
_c_run_args = None
def DiscoverAndApply(mb: memhub.MemBuddy*, entry_path: str) -> int:
"""Pass 0 for --src: discover and apply viperbuild config; 0=success, else failed."""
global _scratch, _work, _visited, _pending, _proj_vars, _entry_dir, _applied
global _search_dirs
global _ffi_handles_path, _ffi_handles_names, _ffi_handles_rt
if mb is None or entry_path is None:
VLogger.error("viperbuild: argument is null", "viperbuild")
return 1
_ResetAll()
# Entry dir (default source + closure search root)
_entry_dir = _DirOf(entry_path)
if _entry_dir is None:
VLogger.error(f"viperbuild: cannot resolve the entry file directory {entry_path}", "viperbuild")
return 1
# Bare filename (no separator) -> _DirOf returns ""; must normalize to ".", otherwise
# ResolveModuleToPath("") tries no candidate and same-dir deps (often the viperbuild config) never resolve.
if string.strlen(_entry_dir) == 0: _entry_dir = _DupStd(".")
# Closure search roots: entry dir first, then "." (the config module may live elsewhere)
_search_dirs = _ConcatStd(_DupStd(_entry_dir), _DupStd(" ."))
# Scratch pool (BFS working set; not affected by main-pool reset)
arena: bytes = stdlib.malloc(VB_SCRATCH)
if arena is None:
VLogger.error("viperbuild: scratch memory allocation failed", "viperbuild")
return 1
# Heap-allocate the manager: `_scratch` is a module-level global, so a frame-local
# `MemBuddy` temp would hand out a dangling address once this frame returns (LifetimeError).
# `hlife` is explicit so NLL frame-demotion cannot apply.
_scratch = new heap hlife memhub.MemBuddy(arena, VB_SCRATCH, 0, 0)
if _scratch is None:
VLogger.error("viperbuild: scratch pool initialization failed", "viperbuild")
return 1
_work = list[str](_scratch, 8)
_visited = list[str](_scratch, 8)
_pending = list[str](_scratch, 8)
_proj_vars = list[str](_scratch, 8)
_ffi_handles_names = list[str](_scratch, 8)
_ffi_handles_rt = list[int](_scratch, 8)
_ffi_handles_path = list[str](_scratch, 8)
if _work is None or _visited is None or _pending is None or _proj_vars is None:
VLogger.error("viperbuild: working set allocation failed", "viperbuild")
return 1
if _ffi_handles_names is None or _ffi_handles_rt is None or _ffi_handles_path is None:
VLogger.error("viperbuild: ffilib handle table allocation failed", "viperbuild")
return 1
# Keyword/operator tables must be built before the main-pool reset window (else freed -> dangling crash)
ast._init_tables(mb)
if _RunBfs(mb, entry_path) != 0: return 1
if _proj_vars.__len__() == 0:
if _saw_viperbuild == 0: VLogger.error(f"viperbuild: `import viperbuild` not found in the entry file dependency closure (entry: {entry_path}; scanned {_visited.__len__()} files)", "viperbuild")
else: VLogger.error(f"viperbuild: found `import viperbuild` but the dependency closure has no `proj = vb.Project(...)` binding (entry: {entry_path})", "viperbuild")
return 1
if _FinalizeCfg(entry_path) != 0: return 1
_applied = 1
return 0
def IsApplied() -> int:
"""Whether viperbuild config was applied in this process; 1=yes."""
return _applied
def AdvancedAsan() -> int:
"""Whether viperbuild requested ASan mode (proj.advanced.asan = True); 1=yes / 0=no."""
return _c_adv_asan
def AdvancedTraceback() -> int:
"""Whether viperbuild requested traceback mode (proj.advanced.traceback = True); 1=yes / 0=no."""
return _c_adv_traceback
def CleanAll() -> int:
"""Whether viperbuild requested a full clean (proj.clean.all: buildroot + includes); 1=yes / 0=no."""
return _c_clean_all
def CleanOutput() -> int:
"""Whether viperbuild requested cleaning the output dir (proj.clean.output); 1=yes / 0=no."""
return _c_clean_output
def CleanTemp() -> int:
"""Whether viperbuild requested cleaning the temp dir (proj.clean.temp); 1=yes / 0=no."""
return _c_clean_temp
def CleanBuildDir() -> int:
"""Whether viperbuild requested cleaning the whole build dir (proj.clean.build_dir); 1=yes / 0=no."""
return _c_clean_build_dir
def CleanIncludes() -> int:
"""Whether viperbuild requested cleaning the includes cache (proj.clean.includes); 1=yes / 0=no."""
return _c_clean_includes
def RunAfterBuild() -> int:
"""Whether viperbuild requested running the exe after a successful build; 1=yes / 0=no."""
return _c_run
def RunArgs() -> str:
"""Space-joined run arguments for build(args=[...]) (already quoted where needed); None = none."""
return _c_run_args