Files
ViperC/App/lib/core/builtin/ViperBuild.vp
T

1121 lines
42 KiB
Plaintext

"""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
# 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")
_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
# 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 e.kind() == ast.ASTKind.Starred:
st: ast.Starred* = ast.Starred*(e)
if st is None or st.value is None: return 0
if _EvalList(st.value, 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
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, queue other modules for BFS."""
global _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")
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 _HandleAttrAssign(tgt: ast.Attribute*, vnode: ast.AST*) -> int:
"""Handle proj.<field> = <constant expression>."""
if tgt is None or tgt.attr is None or tgt.value is None: return 0
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 _HandleBuildCall(cl: ast.Call*) -> int:
"""Handle a proj.build(triple=, datalayout=, selfmachine=) call."""
global _c_triple, _c_datalayout, _c_selfmachine, _c_has_build
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
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
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: rc = _HandleAssign(ast.Assign*(child))
elif kd == ast.ASTKind.AnnAssign: rc = _HandleAnnAssign(ast.AnnAssign*(child))
elif kd == ast.ASTKind.Expr: rc = _HandleExprStmt(ast.Expr*(child))
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
global _c_triple, _c_datalayout, _c_selfmachine, _c_has_project, _c_has_build
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
_c_output = None
_c_triple = None
_c_datalayout = None
_c_selfmachine = 0
_c_has_project = 0
_c_has_build = 0
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
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)
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
# 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