Files
ViperC/App/lib/Phase/BuildPipeline.vp
T
TermiNexus 36d23d2f42 feat(build): 实现 Qt 运行时依赖部署(runtime.qt6_path / qt6_dlls / qt6_plugins)
背景:project.vpj 里的 runtime 块此前只是声明(编译器不读取),Qr 程序运行时报 Could not find the Qt platform plugin "windows"。

实现:链接成功后(Phase2 的 PhaseC 成功点)调用 BuildPipeline.deploy_runtime_deps(exe_path)——把 qt6_dlls 各项从 <qt6_path>/bin 复制到 exe 同目录;把新增的可选字段 qt6_plugins 各项从 <qt6_path>/plugins 复制到 <exe 目录>/<相对路径>(父目录不存在则 mkdir)。Config.vp 新增 Qt6Path/Qt6Dlls/Qt6Plugins 读取(口径同 LinkerStaticLibs)。

fail-fast:声明了但源文件不存在 / 复制失败 / mkdir 失败 => VLogger.error 并中止构建;三字段全未声明则整段跳过。

验证:重编 SUCCESS(210.11s);ViperMNIST 产物含 7 个 Qt6*.dll + platforms/qwindows.dll,实跑 stderr 无插件报错;ViperAIChat / ViperQtHelloWorld 同样 OK 并完成部署。
2026-10-07 09:08:35 +08:00

2522 lines
113 KiB
Plaintext

import lib.core.Gen.GenApi as GenApi
import t, c
from stdint import *
import stdio
import string
import stdlib
import memhub
import viperlib
import vthreading
import w32.fileio as fileio
import w32.win32file
import w32.win32base
import w32.win32process as win32process
import w32.win32sync as win32sync
import subprocess
import viperlib
import ast
import os as oslib
import shutil
import lib.core.VLogger as VLogger
import lib.core.Gen.Translator as HandlesTranslator
import lib.core.Gen.DebugInfoGen as DebugInfoGen
import lib.Stub.StubMerger as StubMerger
import lib.Stub.StubGen.Converter as StubConverter
import lib.Projectrans.Config as Config
import lib.core.Gen.Generator as GenGenerator
import lib.core.Gen.Expr.ExprGenCls as ExprGenCls
import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect
# BuildPipeline - compile pipeline: write .ll to temp, run llc (.ll -> .obj),
# then link with clang++ (.obj -> .exe).
# global mbuddy pointer
_mbuddy: memhub.MemBuddy*
# ASan mode (--asan): instrument .ll via opt -passes=asan; the ASan runtime is
# declared explicitly in project.vpj's linker.static_libs / dynamic_libs (TPV injects nothing).
_asan_mode: bool = 0
# Traceback mode (--traceback): force-build includes/traceback.vp, inject the startup
# Install() ctor, keep frame pointers, link dbghelp, then export <exe>.stackmap.
_traceback_mode: bool = 0
# --phase2command xxx.bat: do not run llc/link; write the llc (incl. ASan opt) and clang
# link commands to the given .bat path for manual execution. None = run normally (default).
_phase2_cmd_path: str = None
# whether the bat header (@echo off) has been written; set to 1 on first write
_phase2_bat_init: int = 0
# MLIR lowering step (fixed, no backend switch): mlirlite emits MLIR but opt/llc accept
# only LLVM IR, so run mlir-opt (validate) then mlir-translate --mlir-to-llvmir first.
MLIR_OPT_CMD: str = "mlir-opt"
MLIR_TRANSLATE_CMD: str = "mlir-translate"
def _ir_sibling_path(ir_path: str, suffix: str) -> str:
"""Derive a sibling path from <...>.ll: strip the .ll suffix and append suffix; mbuddy-allocated."""
if ir_path is None or suffix is None: return None
n: sizet = string.strlen(ir_path)
if n < 3: return None
suf_len: sizet = string.strlen(suffix)
buf: str = _mbuddy.alloc(n + suf_len + 1)
if buf is None: return None
string.strncpy(buf, ir_path, n - 3)
buf[n - 3] = '\0'
string.strcat(buf, suffix)
return buf
# source buffer size (1MB)
define SRC_BUF_SIZE = 1048576 # 2^20 = 1MB
# .vpi stub buffer size (256KB, same as Phase1). Use a compile-time define: a module-level
# global with this name would emit a second strong symbol and llvm-link reports multiply defined.
define PYI_BUF_SIZE = 262144
# _llc_frame_ptr_flag - llc extra flag for --traceback: " -frame-pointer=all" while
# enabled (leading space), "" otherwise so the llc command stays byte-identical.
def _llc_frame_ptr_flag() -> str:
if _traceback_mode != 0: return " -frame-pointer=all"
return ""
# _append_traceback_dbghelp - append -ldbghelp for --traceback (StackWalk64 etc. live in
# dbghelp); appended so libraries follow the objects, else the link has undefined symbols.
def _append_traceback_dbghelp(linker_flags: str) -> str:
if _traceback_mode == 0 or linker_flags is None: return linker_flags
need: sizet = string.strlen(linker_flags) + 16
buf: str = _mbuddy.alloc(need)
if buf is None:
VLogger.error("--traceback: failed to allocate -ldbghelp linker argument buffer, aborting compilation", "TRACEBACK")
return linker_flags
viperlib.snprintf(buf, need, "%s -ldbghelp", linker_flags)
return buf
# --traceback call-site sidecar (<sha1>.vpcall): the compiler accumulates F/C records per
# module; WriteCallSidecar writes and registers them, WriteStackMap reads them after link.
define TB_VPCALL_MAX = 8192
_tb_vpcall_buf: bytes = None # TB_VPCALL_MAX * 8: one char* per slot (stdlib-allocated, one-shot per process)
_tb_vpcall_n: int = 0
def _tb_register_vpcall(path: str) -> int:
"""Register a written .vpcall path in the global table for WriteStackMap to read."""
global _tb_vpcall_buf, _tb_vpcall_n
if path is None: return 0
if _tb_vpcall_buf is None:
_tb_vpcall_buf = stdlib.malloc(sizet(TB_VPCALL_MAX) * 8)
if _tb_vpcall_buf is None:
VLogger.error("--traceback: failed to allocate .vpcall path table, aborting compilation", "TRACEBACK")
return 1
_tb_vpcall_n = 0
if _tb_vpcall_n >= TB_VPCALL_MAX:
VLogger.error("--traceback: .vpcall path table overflow (module count exceeds limit), aborting compilation", "TRACEBACK")
return 1
slot: str* = (str*)(uint64(_tb_vpcall_buf) + sizet(_tb_vpcall_n) * 8)
slot[0] = path
_tb_vpcall_n += 1
return 0
def WriteCallSidecar(temp_dir: str, sha1: str, text: str) -> int:
"""--traceback: write this module's call-site text to <temp_dir>/<slice>/<sha1>.vpcall; None text skips."""
if temp_dir is None or sha1 is None or text is None: return 0
vpath: str = StubMergerCollect._sliced_path(temp_dir, string.strlen(temp_dir), sha1, "vpcall")
if vpath is None:
VLogger.error(f"--traceback: failed to build .vpcall path (sha1={sha1}), aborting compilation", "TRACEBACK")
return 1
vf: fileio.File* = fileio.File(vpath, fileio.MODE.W)
if vf.closed:
VLogger.error(f"--traceback: failed to write .vpcall (cannot open {vpath}), aborting compilation", "TRACEBACK")
stdlib.free(vpath)
return 1
vf.write(text, string.strlen(text))
vf.close()
return _tb_register_vpcall(vpath)
# _tb_basename_is_traceback - whether this source file is includes/traceback.vp
def _tb_basename_is_traceback(file_path: str) -> int:
if file_path is None: return 0
flen: sizet = string.strlen(file_path)
if flen < 12: return 0
if flen > 12:
prev: char = file_path[flen - 13]
if prev != '/' and prev != '\\': return 0
return 1 if string.strcmp(file_path + (flen - 12), "traceback.vp") == 0 else 0
# TranslateFileGetTrans - translate one file and return a Translator (caller dumps IR).
# File -> AST -> .vpi -> LLVM IR; includes use loose scoping, user files strict (local + imports).
def TranslateFileGetTrans(mb: memhub.MemBuddy*, file_path: str, sha1_val: str, current_package: str = None, declare_only: int = 0, temp_dir: str = None, pre_allow_dep_inject: int = 0) -> HandlesTranslator.Translator*:
"""Translate one file and return a Translator (caller dumps IR); None on failure."""
if file_path is None: return None
f: fileio.File* = fileio.File(file_path, fileio.MODE.R)
if f.closed: return None
# read_all's capacity includes the terminator (reads cap-1 bytes max) and silently
# truncates longer files; reject by size first, else parsing gets confusing syntax errors.
if f.size() >= SRC_BUF_SIZE:
VLogger.error(f"source file exceeds {SRC_BUF_SIZE} byte limit, refusing to read: {file_path}", "fileio")
f.close()
return None
src_buf: bytes = stdlib.malloc(SRC_BUF_SIZE)
if src_buf is None:
f.close()
return None
bytes_read: LONG = f.read_all(src_buf, SRC_BUF_SIZE)
f.close()
if bytes_read <= 0:
stdlib.free(src_buf)
return None
if bytes_read < SRC_BUF_SIZE: src_buf[bytes_read] = 0
lx: ast.Lexer* = ast.new_lexer(mb)
ast._lexer_init(lx, src_buf, mb)
tokens: ast.Token* = ast.tokenize(lx)
tree: ast.AST* = ast.parse_tokens(mb, tokens)
# AST is independent after parse; free the source buffer (avoid a 1MB/file leak)
stdlib.free(src_buf)
if tree is None:
# fail-fast location: print a Python-style parse-error traceback (like fatal_error):
# Traceback (most recent call last) / File "<path>", line N / SyntaxError: <message>
GenApi.dump_parse_traceback(file_path)
err_line: int = ast.get_parse_err_line()
err_col: int = ast.get_parse_err_col()
VLogger.error(f"parse failed: {file_path} (line {err_line} col {err_col})", "PARSE")
return None
# Generate the .vpi stub from the AST before tr.translate (same as Phase1); it lets
# other modules query function signatures and CTypedef definitions.
if temp_dir is not None:
pyi_buf: bytes = stdlib.malloc(PYI_BUF_SIZE)
if pyi_buf is not None:
pyi_buf[0] = 0
# Pass file_path as rel_path: vpi-time fatal_error must blame this source file,
# and the absolute path lets _read_source_line open and print the source line.
pyi_pos: sizet = StubConverter._GeneratePyiFromAst(mb, tree, file_path, pyi_buf, PYI_BUF_SIZE)
if pyi_pos > 0:
# write {sha1}.vpi into temp_dir (sliced path)
td_len_vpi: sizet = len(temp_dir)
pyi_path: str = StubMergerCollect._sliced_path(temp_dir, td_len_vpi, sha1_val, "vpi")
if pyi_path is not None:
pf: fileio.File* = fileio.File(pyi_path, fileio.MODE.W)
if not pf.closed:
pf.write(pyi_buf, pyi_pos)
pf.close()
stdlib.free(pyi_path)
stdlib.free(pyi_buf)
tr: HandlesTranslator.Translator* = mb.alloc(HandlesTranslator.Translator.__sizeof__())
tr.__before_init__()
tr.__init__()
tr.ModuleSha1 = sha1_val
tr.CurrentPackage = current_package
tr._declare_only = declare_only
# App pre-registration (Phase A-pre) switch: when 1, import handling + dependency vpi
# injection also run in the declare phase. includes pre-registration must pass 0.
tr._pre_allow_dep_inject = pre_allow_dep_inject
# P2: reuse the global gen singleton (created by App/main.vp); create it as a fallback
# when unregistered (defensive for direct-call paths).
gen: GenGenerator.Generator* = GenGenerator._g_gen
if gen is None:
gen = mb.alloc(GenGenerator.Generator.__sizeof__())
if gen is None:
VLogger.error("Generator alloc failed", "GEN")
return None
gen.__before_init__()
gen.__init__(mb)
GenGenerator._g_gen = gen
# P2-P5: inject the Handles module-level singletons (delegated functions use local
# _g_gen to avoid package/class name ambiguity from in-body imports).
GenApi._g_gen = gen
# P6: inject singletons for statement/assign/var/import/translate-driver modules
# set the current file name (for error reporting)
GenApi.set_current_file_type(file_path)
# --traceback: mirror the switch into the Gen-layer DWARF emitter. 0 => every DI entry
# point is a no-op and the IR stays byte-identical to the pre-DWARF output.
DebugInfoGen.SetEnabled(_traceback_mode)
# also set it for HandlesImports (import SHA1 checks need it; a circular dep blocks reading HandlesType)
GenApi.set_current_file_imports(file_path)
# clear the call stack (reset before each file translation)
GenApi.clear_trace()
# module switch: clear the CDefine constant table to isolate each module's constants
GenApi.clear_cdefine_constants()
# module switch: clear the CTypedef table to isolate each module's typedefs
GenApi.clear_ctypedefs()
# namespace isolation: includes loose (all visible), user files strict (local + imports only)
strict_mode: int = 1
if string.strstr(file_path, "includes") is not None: strict_mode = 0
GenApi.reset_visible_structs(mb, strict_mode)
# Extract docstrings before tr.translate (pre-registration registers cross-module __doc__;
# must run before translate recycles the AST pool blocks, which crashes under ASan).
if gen is not None: gen.ExprGenCls.extract_module_docstrings(tree, sha1_val)
ret: int = tr.translate(tree)
if ret != 0:
VLogger.error(f"file translation failed: {file_path}", "Translate")
return None
# --traceback: for includes/traceback.vp, inject the startup Install() ctor into its IR.
# The injector lives in the Gen layer, so no new cross-module mlirlite dependency.
if _traceback_mode != 0 and _tb_basename_is_traceback(file_path) != 0: HandlesTranslator.InjectTracebackInstallCtor(tr)
return tr
class BuildResult:
Success: int
OutputPath: str
ErrorMsg: str
def __new__(self) -> t.CPtr:
return t.CPtr(_mbuddy.alloc(BuildResult.__sizeof__()))
def __init__(self):
self.Success = 0
self.OutputPath = None
self.ErrorMsg = None
def ensure_dir(path: str) -> int:
"""Recursively create a directory (like mkdir -p); an existing directory counts as success."""
if path is None: return 1
path_len: sizet = len(path)
if path_len == 0: return 0
# Hot path (PROF: ensure_dir was 4.5%): if the final dir exists, return success early,
# saving an alloc + N+1 CreateDirectoryA calls. Semantics unchanged (exists == success).
attr: ULONG = w32.win32file.GetFileAttributesA(path)
if attr != 0xFFFFFFFF and (attr & w32.win32file.FILE_ATTRIBUTE_DIRECTORY) != 0: return 0
# copy the path into a writable buffer (for stepwise truncation)
buf: bytes = _mbuddy.alloc(path_len + 1)
if buf is None: return 1
string.strcpy(buf, path)
# Temporarily truncate at each separator and create every directory level.
# CreateDirectoryA returns 0 when the dir exists (failure); ignore it.
plen: int = path_len
for i in range(plen):
ch: int = buf[i]
if ch == 47 or ch == 92: # '/' = 47, '\\' = 92
saved: int = ch
buf[i] = '\0'
w32.win32file.CreateDirectoryA(buf, None)
buf[i] = saved
# create the final directory
w32.win32file.CreateDirectoryA(buf, None)
return 0
# build_sliced_path - build a sliced path and ensure its directory exists. Fans out into
# SHA1-prefix subdirs per Config.Sha1SliceLevel; returns the full path (mbuddy) or None.
def build_sliced_path(base_dir: str, sha1: str, ext: str) -> str:
"""Build a sliced path and ensure its directory exists."""
if base_dir is None or sha1 is None or ext is None: return None
subdir: str = Config.slice_subdir(sha1, Config.Sha1SliceLevel)
base_len: sizet = len(base_dir)
sha1_len: sizet = len(sha1)
ext_len: sizet = len(ext)
if subdir is not None:
sub_len: sizet = len(subdir)
# ensure_dir({base_dir}/{subdir})
dir_path: str = _mbuddy.alloc(base_len + sub_len + 2)
if dir_path is not None:
viperlib.snprintf(dir_path, base_len + sub_len + 2, "%s/%s", base_dir, subdir)
ensure_dir(dir_path)
# file path: {base_dir}/{subdir}/{sha1}.{ext}
path_len: sizet = base_len + sub_len + sha1_len + ext_len + 4
path: str = _mbuddy.alloc(path_len)
if path is None: return None
viperlib.snprintf(path, path_len, "%s/%s/%s.%s", base_dir, subdir, sha1, ext)
return path
else:
# no slicing: {base_dir}/{sha1}.{ext}
path_len = base_len + sha1_len + ext_len + 3
path = _mbuddy.alloc(path_len)
if path is None: return None
viperlib.snprintf(path, path_len, "%s/%s.%s", base_dir, sha1, ext)
return path
def write_ir_to_file(ir_buf: bytes, ir_len: sizet, output_dir: str, module_name: str) -> int:
"""Write the IR buffer to a .ll file; 0 on success, non-zero on failure."""
if ir_buf is None or output_dir is None or module_name is None: return 1
# build sliced path: output_dir/{sha1 prefix}/{module_name}.ll
path: str = build_sliced_path(output_dir, module_name, "ll")
if path is None: return 1
# open the file for writing
f: fileio.File* = fileio.File(path, fileio.MODE.W)
if f.closed: return 1
written: int64 = f.write(ir_buf, ir_len)
f.close()
if written < 0: return 1
return 0
def compile_ll_to_obj(ir_path: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str) -> int:
"""Compile a .ll file to .obj via llc; 0 on success, non-zero on failure."""
if ir_path is None or cc_cmd is None or output_dir is None or module_name is None: return 1
# build sliced .obj path: output_dir/{sha1 prefix}/{module_name}.obj
obj_path: str = build_sliced_path(output_dir, module_name, "obj")
if obj_path is None: return 1
# ASan mode: run opt -passes=asan before llc. -asan-globals=false disables global
# instrumentation: TPV addresses globals directly and ASan redzones would shift them.
compile_input: str = ir_path
# ---- MLIR lowering (fixed, no switch): mlir-opt (validate) -> mlir-translate ----
# Must run before ASan opt and llc, replacing compile_input with <...>.ll.local.
ll_local_path: str = _ir_sibling_path(ir_path, ".ll.local")
mlir_opt_out: str = _ir_sibling_path(ir_path, ".mlir.opt")
if ll_local_path is None or mlir_opt_out is None: return 1
mo_len: sizet = len(MLIR_OPT_CMD) + len(ir_path) + len(mlir_opt_out) + 16
cmd_mo: str = _mbuddy.alloc(mo_len)
if cmd_mo is None: return 1
viperlib.snprintf(cmd_mo, mo_len, "%s %s -o %s", MLIR_OPT_CMD, ir_path, mlir_opt_out)
mt_len: sizet = len(MLIR_TRANSLATE_CMD) + len(mlir_opt_out) + len(ll_local_path) + 40
cmd_mt: str = _mbuddy.alloc(mt_len)
if cmd_mt is None: return 1
viperlib.snprintf(cmd_mt, mt_len, "%s --mlir-to-llvmir %s -o %s", MLIR_TRANSLATE_CMD, mlir_opt_out, ll_local_path)
if _phase2_cmd_path is not None:
# write the command only (must precede the ASan/llc commands below)
_phase2_write_cmd(cmd_mo)
_phase2_write_cmd(cmd_mt)
else:
mo_rc: int = _run_cmd_wait(cmd_mo)
if mo_rc != 0:
VLogger.error(f"mlir-opt failed (module={module_name}, exit={mo_rc}, cmd={cmd_mo})", "MLIR")
return 1
mt_rc: int = _run_cmd_wait(cmd_mt)
if mt_rc != 0:
VLogger.error(f"mlir-translate failed (module={module_name}, exit={mt_rc}, cmd={cmd_mt})", "MLIR")
return 1
compile_input = ll_local_path
# --phase2command mode: do not run opt/llc; write the opt (ASan) + llc commands to the bat
if _phase2_cmd_path is not None:
p2_opt_cmd: str = None
if _asan_mode != 0:
p2_asan: str = build_sliced_path(output_dir, module_name, "asan.ll")
if p2_asan is not None:
p2_olen: sizet = len(p2_asan) + len(compile_input) + 192
p2_opt_cmd = _mbuddy.alloc(p2_olen)
if p2_opt_cmd is not None:
viperlib.snprintf(p2_opt_cmd, p2_olen, "opt -passes=forceattrs,asan -force-attribute=sanitize_address -asan-globals=false -o %s %s", p2_asan, compile_input)
compile_input = p2_asan
llc_fp: str = _llc_frame_ptr_flag()
p2_clen: sizet = len(cc_cmd) + len(cc_flags) + len(llc_fp) + len(obj_path) + len(compile_input) + 64
p2_cmd: bytes = _mbuddy.alloc(p2_clen)
if p2_cmd is not None:
viperlib.snprintf(p2_cmd, p2_clen, "%s %s%s -o %s %s", cc_cmd, cc_flags, llc_fp, obj_path, compile_input)
if p2_opt_cmd is not None: _phase2_write_cmd(p2_opt_cmd)
if p2_cmd is not None: _phase2_write_cmd(p2_cmd)
return 0
if _asan_mode != 0:
asan_path: str = build_sliced_path(output_dir, module_name, "asan.ll")
if asan_path is None: return 1
opt_cmd_len: sizet = len(asan_path) + len(compile_input) + 192
opt_cmd: bytes = _mbuddy.alloc(opt_cmd_len)
if opt_cmd is None: return 1
viperlib.snprintf(opt_cmd, opt_cmd_len, "opt -passes=forceattrs,asan -force-attribute=sanitize_address -asan-globals=false -o %s %s", asan_path, compile_input)
opt_ret: int = stdlib.system(opt_cmd)
if opt_ret != 0:
VLogger.error(f"opt ASan instrumentation failed (module={module_name})", "ASAN")
return 1
compile_input = asan_path
# command: llc -filetype=obj -o {obj_path} ir_path
llc_fp2: str = _llc_frame_ptr_flag()
cmd_len: sizet = len(cc_cmd) + len(cc_flags) + len(llc_fp2) + len(obj_path) + len(compile_input) + 64
cmd: bytes = _mbuddy.alloc(cmd_len)
if cmd is None: return 1
viperlib.snprintf(cmd, cmd_len, "%s %s%s -o %s %s", cc_cmd, cc_flags, llc_fp2, obj_path, compile_input)
# Launch llc directly via CreateProcessA (avoids cmd/c hanging under asan); the child
# inherits the console. On failure, log module sha1 + exit code + full command.
sys_ret: int = _run_cmd_wait(cmd)
if sys_ret != 0:
VLogger.error(f"compilation failed (module={module_name}, exit={sys_ret}, cmd={cmd})", "LLC")
return 1
return 0
# llc parallel compile (Phase B, async threads): tasks/commands use stdlib.malloc (main pool mb
# gets reset); the worker never allocates from MemBuddy. Max threads = os.cpu_count() (16 here).
define LLC_MAX_THREADS = 16
# LlcTask.FailStage: which step failed (0=none, 1=mlir-opt, 2=mlir-translate, 3=ASan opt,
# 4=llc). Without this the main thread misreported mlir-opt/translate failures as llc failures.
define LLC_STAGE_NONE = 0
define LLC_STAGE_MLIR_OPT = 1
define LLC_STAGE_MLIR_TRANSLATE = 2
define LLC_STAGE_ASAN_OPT = 3
define LLC_STAGE_LLC = 4
class LlcTask:
"""Async llc task: command strings (stdlib.malloc), module SHA1 and result."""
OptCmd: bytes # ASan instrumentation command (None if not ASan)
LlcCmd: bytes # llc command
ModuleSha1: bytes # module SHA1 (for error reporting)
Result: int # worker-written: 0 success, non-zero failure
# ExitCode is appended at the end so existing offsets are unchanged; all construction
# sites use __sizeof__() and memset the whole record, so offsets need no update.
ExitCode: int # worker-written: exit code of the failing command (0 on success)
# MLIR-backend lowering commands (None if not MLIR); also appended at the end.
MlirOptCmd: bytes # mlir-opt validation command
MlirTranslateCmd: bytes # mlir-translate lowering command
# failure stage (LLC_STAGE_*, see the constants above); appended at the end.
FailStage: int # worker-written: which step failed
def init_llc_task(task: LlcTask*, ir_path: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str) -> int:
"""Fill an llc task (all stdlib.malloc, kept alive for the thread's run); 0 on success."""
if task is None or ir_path is None or output_dir is None or module_name is None: return 1
string.memset(task, 0, LlcTask.__sizeof__())
task.Result = -1
obj_path: str = build_sliced_path(output_dir, module_name, "obj")
if obj_path is None: return 1
# module SHA1
sl: sizet = len(module_name)
task.ModuleSha1 = stdlib.malloc(sl + 1)
if task.ModuleSha1 is None: return 1
string.strcpy(task.ModuleSha1, module_name)
# llc input choice: ASan mode must feed the instrumented .asan.ll, same as the serial
# compile_ll_to_obj path (else llc ignores the instrumentation and ASan is half-blind).
compile_input: str = ir_path
# MLIR lowering (fixed): the worker runs mlir-opt (validate) -> mlir-translate first, so
# the later ASan OptCmd / LlcCmd input must be <...>.ll.local.
ll_local_path: str = _ir_sibling_path(ir_path, ".ll.local")
mlir_opt_out: str = _ir_sibling_path(ir_path, ".mlir.opt")
if ll_local_path is None or mlir_opt_out is None: return 1
mo_len: sizet = len(MLIR_OPT_CMD) + len(ir_path) + len(mlir_opt_out) + 16
task.MlirOptCmd = stdlib.malloc(mo_len)
if task.MlirOptCmd is None: return 1
viperlib.snprintf(task.MlirOptCmd, mo_len, "%s %s -o %s", MLIR_OPT_CMD, ir_path, mlir_opt_out)
mt_len: sizet = len(MLIR_TRANSLATE_CMD) + len(mlir_opt_out) + len(ll_local_path) + 40
task.MlirTranslateCmd = stdlib.malloc(mt_len)
if task.MlirTranslateCmd is None: return 1
viperlib.snprintf(task.MlirTranslateCmd, mt_len, "%s --mlir-to-llvmir %s -o %s", MLIR_TRANSLATE_CMD, mlir_opt_out, ll_local_path)
compile_input = ll_local_path
# ASan mode: opt instrumentation command (the worker runs MLIR lowering/opt before llc,
# so LlcCmd's input must point at asan_path; failure sets Result).
if _asan_mode != 0:
asan_path: str = build_sliced_path(output_dir, module_name, "asan.ll")
if asan_path is None: return 1
opt_len: sizet = len(asan_path) + len(compile_input) + 192
task.OptCmd = stdlib.malloc(opt_len)
if task.OptCmd is None: return 1
viperlib.snprintf(task.OptCmd, opt_len, "opt -passes=forceattrs,asan -force-attribute=sanitize_address" " -asan-globals=false -o %s %s", asan_path, compile_input)
compile_input = asan_path
# llc command (input = compile_input: .ll normally, .asan.ll under ASan)
llc_fp3: str = _llc_frame_ptr_flag()
cmd_len: sizet = len(cc_cmd) + len(cc_flags) + len(llc_fp3) + len(obj_path) + len(compile_input) + 64
task.LlcCmd = stdlib.malloc(cmd_len)
if task.LlcCmd is None: return 1
viperlib.snprintf(task.LlcCmd, cmd_len, "%s %s%s -o %s %s", cc_cmd, cc_flags, llc_fp3, obj_path, compile_input)
return 0
def free_llc_task(task: LlcTask*):
"""Free the task's command strings; the record itself is a slot in the task array."""
if task is None: return
if task.OptCmd is not None:
stdlib.free(task.OptCmd)
task.OptCmd = None
if task.LlcCmd is not None:
stdlib.free(task.LlcCmd)
task.LlcCmd = None
if task.MlirOptCmd is not None:
stdlib.free(task.MlirOptCmd)
task.MlirOptCmd = None
if task.MlirTranslateCmd is not None:
stdlib.free(task.MlirTranslateCmd)
task.MlirTranslateCmd = None
if task.ModuleSha1 is not None:
stdlib.free(task.ModuleSha1)
task.ModuleSha1 = None
def _phase2_write_cmd(cmd: str) -> None:
"""--phase2command mode: append the command to the bat file without executing it."""
global _phase2_bat_init
if _phase2_cmd_path is None or cmd is None: return
f: fileio.File* = fileio.File(_phase2_cmd_path, fileio.MODE.A)
if f.closed:
VLogger.error("phase2command: failed to open bat", "phase2cmd")
return
if _phase2_bat_init == 0:
_phase2_bat_init = 1
hdr1: str = "@echo off\r\n"
hdr2: str = "rem TransPyV --phase2command generated commands\r\n"
f.write(hdr1, string.strlen(hdr1))
f.write(hdr2, string.strlen(hdr2))
f.write(cmd, string.strlen(cmd))
f.write("\r\n", 2)
f.close()
def _run_cmd_wait(cmd: str) -> int:
"""Run a command synchronously via CreateProcessA and return its exit code."""
if cmd is None: return -1
cl_len: sizet = len(cmd)
cmd_line: bytes = stdlib.malloc(cl_len + 1)
if cmd_line is None: return -1
string.strcpy(cmd_line, cmd)
sa: w32.win32base.SECURITY_ATTRIBUTES = w32.win32base.SECURITY_ATTRIBUTES()
si: win32process.STARTUPINFOA = win32process.STARTUPINFOA()
pi: win32process.PROCESS_INFORMATION = win32process.PROCESS_INFORMATION()
# T() only allocas, no memset: unset STARTUPINFOA fields are stack garbage, so concurrent
# threads get random dwFlags and CreateProcessA uses junk stdio handles. Clear si/sa/pi.
string.memset(&si, 0, win32process.STARTUPINFOA.__sizeof__())
string.memset(&sa, 0, w32.win32base.SECURITY_ATTRIBUTES.__sizeof__())
string.memset(&pi, 0, win32process.PROCESS_INFORMATION.__sizeof__())
sa.nLength = w32.win32base.SECURITY_ATTRIBUTES.__sizeof__()
sa.lpSecurityDescriptor = t.CPtr(0)
sa.bInheritHandle = 1
si.cb = win32process.STARTUPINFOA.__sizeof__()
# create_flags=0: the child inherits the parent console (output shown, like system);
# si.dwFlags stays 0 after memset (no STARTF_USESTDHANDLES, no std handles).
ok: int = win32process.CreateProcessA(t.CPtr(0), cmd_line, t.CPtr(0), t.CPtr(0), 1, 0, t.CPtr(0), t.CPtr(0), &si, &pi)
# CreateProcessA failure diagnostics: read GetLastError immediately (later calls overwrite
# it). Use warning, not error (error would sys.exit in a worker thread).
if ok == 0:
cp_err: uint32 = w32.win32base.GetLastError()
VLogger.warning(f"CreateProcessA failed (GetLastError={cp_err}) external command not started: cmd={cmd}", "LLC")
stdlib.free(cmd_line)
if ok == 0: return -1
win32sync.WaitForSingleObject(pi.hProcess, w32.win32base.INFINITE)
exit_code: uint32 = 0
win32process.GetExitCodeProcess(pi.hProcess, &exit_code)
w32.win32base.CloseHandle(pi.hProcess)
w32.win32base.CloseHandle(pi.hThread)
return int(exit_code)
def _llc_task_worker(arg: t.CPtr) -> t.CPtr:
"""llc thread entry: only calls _run_cmd_wait, no pool allocation (thread-safe)."""
task: LlcTask* = (LlcTask*)(arg)
if task is None: return t.CPtr(0)
# MLIR backend: validate (mlir-opt) -> lower (mlir-translate), then ASan opt -> llc
if task.MlirOptCmd is not None:
mo_rc: int = _run_cmd_wait(task.MlirOptCmd)
if mo_rc != 0:
task.ExitCode = mo_rc
task.FailStage = LLC_STAGE_MLIR_OPT
task.Result = 1
return t.CPtr(0)
if task.MlirTranslateCmd is not None:
mt_rc: int = _run_cmd_wait(task.MlirTranslateCmd)
if mt_rc != 0:
task.ExitCode = mt_rc
task.FailStage = LLC_STAGE_MLIR_TRANSLATE
task.Result = 1
return t.CPtr(0)
if task.OptCmd is not None:
opt_rc: int = _run_cmd_wait(task.OptCmd)
if opt_rc != 0:
task.ExitCode = opt_rc
task.FailStage = LLC_STAGE_ASAN_OPT
task.Result = 1
return t.CPtr(0)
llc_rc: int = _run_cmd_wait(task.LlcCmd)
task.ExitCode = llc_rc
if llc_rc != 0:
task.FailStage = LLC_STAGE_LLC
task.Result = 1
else:
task.FailStage = LLC_STAGE_NONE
task.Result = 0
return t.CPtr(0)
def run_llc_parallel(tasks: bytes, task_count: int, max_threads: int) -> int:
"""Run llc in parallel: batches of max_threads threads, join all then check results."""
if tasks is None or task_count <= 0: return 0
# --phase2command mode: do not spawn threads; write all opt (ASan) + llc commands to the bat
if _phase2_cmd_path is not None:
for pi2 in range(task_count):
tsk_p: LlcTask* = (LlcTask*)(uint64(tasks) + sizet(pi2) * LlcTask.__sizeof__())
if tsk_p is None: continue
if tsk_p.MlirOptCmd is not None: _phase2_write_cmd(tsk_p.MlirOptCmd)
if tsk_p.MlirTranslateCmd is not None: _phase2_write_cmd(tsk_p.MlirTranslateCmd)
if tsk_p.OptCmd is not None: _phase2_write_cmd(tsk_p.OptCmd)
if tsk_p.LlcCmd is not None: _phase2_write_cmd(tsk_p.LlcCmd)
VLogger.info(f"phase2command mode: {task_count} llc commands written to {_phase2_cmd_path}", "phase2cmd")
return 0
if max_threads <= 0: max_threads = 1
# Thread-handle array: list[str] (str=bytes=i8*, explicit elem_size=8) stores handles;
# list[t.CPtr] append crashes reading page zero. Grab the handle right after start().
handles: list[str]* = list[str](_mbuddy, 8)
if handles is None:
VLogger.error("run_llc_parallel: failed to allocate thread handle array", "PhaseB")
return 1
slot_size: sizet = LlcTask.__sizeof__()
ret: int = 0
# Batched execution: batch_start steps by max_threads (each batch spawn+join+close).
for batch_start in range(0, task_count, max_threads):
batch_n: int = max_threads
if batch_start + batch_n > task_count: batch_n = task_count - batch_start
# spawn this batch
for j in range(batch_n):
tsk: LlcTask* = (LlcTask*)(uint64(tasks) + sizet(batch_start + j) * slot_size)
th: vthreading.Thread* = vthreading.Thread(_llc_task_worker, t.CPtr(tsk))
th.start()
# Read the handle right after start() (the Thread object's stack slot may be
# reused; reading self.handle at join could see a cleared/overwritten value).
handles.append(t.CPtr(th.handle))
for j in range(batch_n):
h_j: t.CPtr = t.CPtr(handles.get(batch_start + j))
wr: uint32 = win32sync.WaitForSingleObject(h_j, 600000)
if wr != 0: VLogger.error("llc thread join timeout/failure (not finished within 600 seconds)", "PhaseB")
w32.win32base.CloseHandle(h_j)
# check this batch's results
for j in range(batch_n):
tsk2: LlcTask* = (LlcTask*)(uint64(tasks) + sizet(batch_start + j) * slot_size)
if tsk2 is not None and tsk2.Result != 0:
# Failure: use FailStage to pinpoint the step and print that step's command.
# Previously everything printed the LlcCmd as "llc failed", misdirecting MLIR failures.
st: int = tsk2.FailStage
stage_name: str = "unknown stage (worker did not finish)"
stage_cmd: bytes = tsk2.LlcCmd
if st == LLC_STAGE_MLIR_OPT:
stage_name = "mlir-opt validation"
stage_cmd = tsk2.MlirOptCmd
elif st == LLC_STAGE_MLIR_TRANSLATE:
stage_name = "mlir-translate lowering"
stage_cmd = tsk2.MlirTranslateCmd
elif st == LLC_STAGE_ASAN_OPT:
stage_name = "opt ASan instrumentation"
stage_cmd = tsk2.OptCmd
elif st == LLC_STAGE_LLC:
stage_name = "llc compile"
stage_cmd = tsk2.LlcCmd
VLogger.error(f"{stage_name} failed: {(tsk2.ModuleSha1 if tsk2.ModuleSha1 is not None else "?")} (exit={tsk2.ExitCode} cmd={(stage_cmd if stage_cmd is not None else "?")})", "LLC")
ret = 1
return ret
def _CollectObjFilesImpl(base_dir: str, bd_len: sizet, out_buf: bytes, out_pos: sizet, out_size: sizet) -> sizet:
"""Recursively scan base_dir, collect .obj paths into out_buf, return the new out_pos."""
find_data: w32.win32file.WIN32_FIND_DATAA* = _mbuddy.alloc(w32.win32file.WIN32_FIND_DATAA.__sizeof__())
if find_data is None:
VLogger.warning("_CollectObjFilesImpl failed to allocate find_data, fewer .obj collected", "collect")
return out_pos
string.memset(find_data, 0, w32.win32file.WIN32_FIND_DATAA.__sizeof__())
pattern: bytes = _mbuddy.alloc(bd_len + 8)
if pattern is None:
VLogger.warning("_CollectObjFilesImpl failed to allocate pattern, fewer .obj collected", "collect")
return out_pos
viperlib.snprintf(pattern, bd_len + 8, "%s/*", base_dir)
handle: w32.win32base.HANDLE = w32.win32file.FindFirstFileA(pattern, find_data)
_mbuddy.free(pattern)
if handle == w32.win32base.INVALID_HANDLE_VALUE:
VLogger.warning("_CollectObjFilesImpl cannot open directory, fewer .obj collected", "collect")
return out_pos
while True:
fname: str = find_data.cFileName
if fname is not None:
fname_len: sizet = len(fname)
if fname_len > 0:
# skip . and ..
is_dot: int = 0
if fname_len == 1 and fname[0] == '.': is_dot = 1
elif fname_len == 2 and fname[0] == '.' and fname[1] == '.': is_dot = 1
if is_dot == 0:
attrs: ULONG = find_data.dwFileAttributes
is_dir: int = 0
if (attrs & w32.win32file.FILE_ATTRIBUTE_DIRECTORY) != 0: is_dir = 1
if is_dir != 0:
# recurse into the subdirectory
sub_dir: bytes = _mbuddy.alloc(bd_len + fname_len + 2)
if sub_dir is not None:
viperlib.snprintf(sub_dir, bd_len + fname_len + 2, "%s/%s", base_dir, fname)
sub_len: sizet = len(sub_dir)
out_pos = _CollectObjFilesImpl(sub_dir, sub_len, out_buf, out_pos, out_size)
_mbuddy.free(sub_dir)
else:
# check whether this is a .obj file
if fname_len > 4 and fname[fname_len - 4] == '.' and fname[fname_len - 3] == 'o' and fname[fname_len - 2] == 'b' and fname[fname_len - 1] == 'j':
need: sizet = bd_len + 1 + fname_len + 2
if out_pos + need >= out_size: break
if out_pos > 0:
out_buf[out_pos] = ' '
out_pos += 1
viperlib.snprintf(out_buf + out_pos, need, "%s/%s", base_dir, fname)
out_pos += bd_len + 1 + fname_len
if w32.win32file.FindNextFileA(handle, find_data) == 0: break
w32.win32file.FindClose(handle)
return out_pos
def collect_obj_files(includes_binary_dir: str, out_buf: bytes, out_size: sizet) -> sizet:
"""Recursively scan includes_binary_dir, collect all .obj paths into out_buf; bytes written."""
if includes_binary_dir is None or out_buf is None or out_size == 0: return 0
out_buf[0] = '\0'
dir_len: sizet = len(includes_binary_dir)
out_pos: sizet = _CollectObjFilesImpl(includes_binary_dir, dir_len, out_buf, 0, out_size)
out_buf[out_pos] = '\0'
return out_pos
def _append_host_export_implib(linker_flags: str, implib_path: str) -> str:
"""Host-export mode: append --out-implib=<abs path> when linker_flags has --export-all-symbols."""
if linker_flags is None or implib_path is None: return linker_flags
if string.strstr(linker_flags, "--export-all-symbols") is None: return linker_flags
extra: sizet = string.strlen(" -Wl,--out-implib=") + string.strlen(implib_path) + 1
buf: str = _mbuddy.alloc(string.strlen(linker_flags) + extra)
if buf is None:
VLogger.warning("host export: failed to allocate import lib argument buffer (fragments cannot import host symbols)", "link")
return linker_flags
viperlib.snprintf(buf, string.strlen(linker_flags) + extra, "%s -Wl,--out-implib=%s", linker_flags, implib_path)
VLogger.info(f"host export: produced import lib {implib_path}", "link")
return buf
def link_obj_to_exe(output_dir: str, module_name: str, linker_cmd: str, linker_flags: str, linker_output: str, includes_binary_dir: str, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> int:
"""Link .obj files into an .exe via clang++; 0 on success, non-zero on failure."""
if output_dir is None or linker_cmd is None or module_name is None: return 1
# build sliced .obj path: output_dir/{sha1 prefix}/{module_name}.obj
obj_path: str = build_sliced_path(output_dir, module_name, "obj")
if obj_path is None: return 1
# Host-exe export (when linker.flags has --export-all-symbols) also emits an import lib
# {output_dir}/{linker_output}.dll.a; REPL fragments link it to import the host.
if linker_output is not None:
implib_len: sizet = string.strlen(output_dir) + string.strlen(linker_output) + 8
implib_path: str = _mbuddy.alloc(implib_len)
if implib_path is not None:
viperlib.snprintf(implib_path, implib_len, "%s/%s.dll.a", output_dir, linker_output)
linker_flags = _append_host_export_implib(linker_flags, implib_path)
# --traceback: link dbghelp (after the host-export rewrite, so both survive)
linker_flags = _append_traceback_dbghelp(linker_flags)
# collect .obj paths from includes.binary
EXTRA_BUF_SIZE: sizet = 32768
extra_objs: bytes = _mbuddy.alloc(EXTRA_BUF_SIZE)
if extra_objs is None: return 1
extra_len: sizet = 0
if includes_binary_dir is not None:
extra_len = collect_obj_files(includes_binary_dir, extra_objs, EXTRA_BUF_SIZE)
if extra_len > 0: VLogger.info(f"appended {extra_len} bytes of includes.binary .obj files", "link")
else: VLogger.warning(f"includes.binary has no .obj files: {includes_binary_dir}", "link")
# Command: clang++ {obj_path} extra_objs [libs] -o {...}/{linker_output} linker_flags. Objects
# and explicit libs must precede -l flags. The ASan runtime is declared in project.vpj, not hardcoded.
use_flags: str = linker_flags
static_libs: str = linker_static_libs
if static_libs is None: static_libs = ""
if linker_dynamic_libs is not None and len(linker_dynamic_libs) > 0:
# Merge static + dynamic into one space-separated string (Viper str += would emit
# invalid add i8* IR, so build it with snprintf).
merged_sl: str = _mbuddy.alloc(len(static_libs) + len(linker_dynamic_libs) + 2)
if merged_sl is not None:
if len(static_libs) > 0:
viperlib.snprintf(merged_sl, len(static_libs) + len(linker_dynamic_libs) + 2, "%s %s", static_libs, linker_dynamic_libs)
else:
viperlib.snprintf(merged_sl, len(static_libs) + len(linker_dynamic_libs) + 2, "%s", linker_dynamic_libs)
static_libs = merged_sl
sl_len: sizet = len(static_libs)
cmd_len: sizet = len(linker_cmd) + len(obj_path) + len(use_flags) + len(output_dir) + len(linker_output) + extra_len + sl_len + 128
cmd: bytes = _mbuddy.alloc(cmd_len)
if cmd is None: return 1
if sl_len > 0 and extra_len > 0:
viperlib.snprintf(cmd, cmd_len, "%s %s %s %s -o %s/%s %s", linker_cmd, obj_path, extra_objs, static_libs, output_dir, linker_output, use_flags)
elif sl_len > 0:
viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s/%s %s", linker_cmd, obj_path, static_libs, output_dir, linker_output, use_flags)
elif extra_len > 0:
viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s/%s %s", linker_cmd, obj_path, extra_objs, output_dir, linker_output, use_flags)
else:
viperlib.snprintf(cmd, cmd_len, "%s %s -o %s/%s %s", linker_cmd, obj_path, output_dir, linker_output, use_flags)
# --phase2command mode: do not link; write the command to the bat
if _phase2_cmd_path is not None:
_phase2_write_cmd(cmd)
return 0
result: subprocess.CompletedProcess* = subprocess.run(cmd, True, True)
if result is None:
VLogger.error("subprocess.run returned None", "link")
return 1
if result.returncode != 0:
VLogger.error(f"link failed, return code: {result.returncode}", "link")
VLogger.error(f"command: {cmd}", "link")
# Show linker error output.
# Note: subprocess merges stderr into stdout on Windows, so result.stderr is always None.
if result.stdout is not None: VLogger.error(result.stdout, "link")
else: VLogger.error("no output captured", "link")
if result.stderr is not None: VLogger.error(result.stderr, "link")
return 1
return 0
def asm_to_obj(asm_path: str, sha1_val: str, output_dir: str, triple: str) -> int:
"""Assemble a .s source into .obj with llvm-mc on the sliced path; 0 on success, non-zero on failure."""
if asm_path is None or sha1_val is None or output_dir is None: return 1
obj_path: str = build_sliced_path(output_dir, sha1_val, "obj")
if obj_path is None: return 1
trip: str = triple
if trip is None or trip[0] == '\0': trip = "x86_64-none-elf"
cmd_len: sizet = len("llvm-mc -triple=") + len(trip) + len(" -filetype=obj -o ") + len(obj_path) + len(asm_path) + 64
cmd: bytes = _mbuddy.alloc(cmd_len)
if cmd is None: return 1
viperlib.snprintf(cmd, cmd_len, "llvm-mc -triple=%s -filetype=obj -o %s %s", trip, obj_path, asm_path)
# --phase2command mode: write the command to the bat (same as llc/link)
if _phase2_cmd_path is not None:
_phase2_write_cmd(cmd)
return 0
result: subprocess.CompletedProcess* = subprocess.run(cmd, True, True)
if result is None:
VLogger.error("llvm-mc subprocess.run returned None", "asm")
return 1
if result.returncode != 0:
if result.stdout is not None: stdio.printf("%s\n", result.stdout)
if result.stderr is not None: stdio.printf("%s\n", result.stderr)
VLogger.error(f"llvm-mc assembly failed, return code: {result.returncode}, command: {cmd}", "asm")
return 1
return 0
def compile_module_to_obj(ir_buf: bytes, ir_len: sizet, temp_dir: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str) -> int:
"""Compile IR to .obj without linking; 0 on success, non-zero on failure."""
if ir_buf is None or temp_dir is None or output_dir is None or module_name is None: return 1
# Step 1: write the .ll file
ret: int = write_ir_to_file(ir_buf, ir_len, temp_dir, module_name)
if ret != 0:
VLogger.error(f"failed to write .ll: {module_name}", "compile")
return 1
# Step 2: build the sliced .ll path and compile -> .obj
ir_path: str = build_sliced_path(temp_dir, module_name, "ll")
if ir_path is None: return 1
ret = compile_ll_to_obj(ir_path, output_dir, module_name, cc_cmd, cc_flags)
if ret != 0:
VLogger.error(f"llc compilation failed: {module_name}", "compile")
return 1
return 0
def link_objs_to_exe(obj_paths: str, obj_paths_len: sizet, linker_cmd: str, linker_flags: str, linker_output: str, includes_binary_dir: str, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> int:
"""Link multiple .obj files into an .exe; 0 on success, non-zero on failure."""
if obj_paths is None or obj_paths_len == 0 or linker_cmd is None: return 1
# Dedup .obj paths: the same module's obj can be added by several collection stages, and
# a path appearing twice causes duplicate symbols. Keep the first occurrence, skip the rest.
if obj_paths_len > 0:
dedup: bytes = _mbuddy.alloc(obj_paths_len + 1)
if dedup is not None:
dedup[0] = '\0'
dpos: sizet = 0
dsrc_i: sizet = 0
while dsrc_i < obj_paths_len:
# skip leading spaces
while dsrc_i < obj_paths_len and obj_paths[dsrc_i] == ' ': dsrc_i += 1
if dsrc_i >= obj_paths_len: break
dstart: sizet = dsrc_i
while dsrc_i < obj_paths_len and obj_paths[dsrc_i] != ' ': dsrc_i += 1
dtlen: sizet = dsrc_i - dstart
# check whether dedup already has this token
ddup: int = 0
dj: sizet = 0
while dj < dpos:
while dj < dpos and dedup[dj] == ' ': dj += 1
if dj >= dpos: break
ds2: sizet = dj
while dj < dpos and dedup[dj] != ' ': dj += 1
dt2len: sizet = dj - ds2
if dt2len == dtlen:
deq: int = 1
for dk in range(dtlen):
if dedup[ds2 + dk] != obj_paths[dstart + dk]:
deq = 0
break
if deq != 0:
ddup = 1
break
if ddup == 0:
if dpos > 0:
dedup[dpos] = ' '
dpos += 1
string.memcpy(dedup + dpos, obj_paths + dstart, dtlen)
dpos += dtlen
dedup[dpos] = '\0'
if dpos > 0 and dpos != obj_paths_len:
obj_paths = dedup
obj_paths_len = dpos
# collect .obj paths from includes.binary
EXTRA_BUF_SIZE: sizet = 32768
extra_objs: bytes = _mbuddy.alloc(EXTRA_BUF_SIZE)
if extra_objs is None: return 1
extra_len: sizet = 0
if includes_binary_dir is not None: extra_len = collect_obj_files(includes_binary_dir, extra_objs, EXTRA_BUF_SIZE)
# Command: clang++ obj_paths extra_objs -o linker_output linker_flags. The ASan runtime
# comes from project.vpj, not TPV. Host export: import lib is <linker_output>.dll.a.
if linker_output is not None:
implib_len2: sizet = string.strlen(linker_output) + 8
implib_path2: str = _mbuddy.alloc(implib_len2)
if implib_path2 is not None:
viperlib.snprintf(implib_path2, implib_len2, "%s.dll.a", linker_output)
linker_flags = _append_host_export_implib(linker_flags, implib_path2)
# --traceback: link dbghelp (after the host-export rewrite)
linker_flags = _append_traceback_dbghelp(linker_flags)
use_flags: str = linker_flags
static_libs2: str = linker_static_libs
if static_libs2 is None: static_libs2 = ""
if linker_dynamic_libs is not None and len(linker_dynamic_libs) > 0:
# merge static + dynamic (Viper str += emits invalid IR; use snprintf)
merged_sl2: str = _mbuddy.alloc(len(static_libs2) + len(linker_dynamic_libs) + 2)
if merged_sl2 is not None:
if len(static_libs2) > 0:
viperlib.snprintf(merged_sl2, len(static_libs2) + len(linker_dynamic_libs) + 2, "%s %s", static_libs2, linker_dynamic_libs)
else:
viperlib.snprintf(merged_sl2, len(static_libs2) + len(linker_dynamic_libs) + 2, "%s", linker_dynamic_libs)
static_libs2 = merged_sl2
sl2_len: sizet = len(static_libs2)
cmd_len: sizet = len(linker_cmd) + obj_paths_len + len(use_flags) + len(linker_output) + extra_len + sl2_len + 128
cmd: bytes = _mbuddy.alloc(cmd_len)
if cmd is None: return 1
# linker.static_libs explicit static libs go after the .obj files and before -o
if sl2_len > 0 and extra_len > 0:
viperlib.snprintf(cmd, cmd_len, "%s %s %s %s -o %s %s", linker_cmd, obj_paths, extra_objs, static_libs2, linker_output, use_flags)
elif sl2_len > 0:
viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s %s", linker_cmd, obj_paths, static_libs2, linker_output, use_flags)
elif extra_len > 0:
viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s %s", linker_cmd, obj_paths, extra_objs, linker_output, use_flags)
else:
viperlib.snprintf(cmd, cmd_len, "%s %s -o %s %s", linker_cmd, obj_paths, linker_output, use_flags)
# Capture clang++ stdout/stderr via subprocess.run (stdlib.system could not capture errors like
# undefined reference). --phase2command mode: do not link; write the command to the bat.
if _phase2_cmd_path is not None:
_phase2_write_cmd(cmd)
return 0
result: subprocess.CompletedProcess* = subprocess.run(cmd, True, True)
if result is None:
VLogger.error("subprocess.run returned None", "link")
return 1
if result.returncode != 0:
# First print clang++'s full error output (incl. undefined reference) with stdio.printf;
# it must precede VLogger.error, which calls sys.exit(1) and terminates the process.
if result.stdout is not None: stdio.printf("=== clang++ link error output ===\n%s\n", result.stdout)
if result.stderr is not None:
stdio.printf("=== clang++ stderr ===\n%s\n", result.stderr)
VLogger.error(f"link failed, return code: {result.returncode}, command: {cmd}", "link")
return 1
return 0
# --traceback: <exe>.stackmap sidecar writer (address -> source line). After a successful link,
# llvm-dwarfdump parses the PE's DWARF; addresses are normalised to RVAs and written out.
# row record: u64 rva | u64 file_path | u32 line | u32 pad | u64 func_name (nullable).
# func_name is only carried by our .vpcall rows; DWARF rows write 0 and fall back to the subprogram table.
define TB_ROW_SIZE = 32
define TB_ROW_OFF_FILE = 8
define TB_ROW_OFF_LINE = 16
define TB_ROW_OFF_FUNC = 24
# func record: u64 low_rva | u64 high_rva | u64 name
define TB_FUNC_SIZE = 24
# dir record: u32 idx | u32 pad | u64 name
define TB_DIR_SIZE = 16
# file record: u32 idx | u32 dir_index | u64 name | u64 resolved_path
define TB_FILE_SIZE = 32
# initial capacities for the (growable) row / func tables
define TB_ROW_CAP0 = 65536
define TB_FUNC_CAP0 = 65536
# fixed per-CU table capacities (exceeding either aborts the build)
define TB_DIR_CAP = 4096
define TB_FILE_CAP = 65536
class TbPeLayout:
Ok: int
ImageBase: uint64
SizeOfImage: uint64
TextRva: uint64
def __new__(self) -> t.CPtr:
return t.CPtr(_mbuddy.alloc(TbPeLayout.__sizeof__()))
def __init__(self):
self.Ok = 0
self.ImageBase = 0
self.SizeOfImage = 0
self.TextRva = 0
class TbDump:
RowBuf: bytes
RowCap: int
RowCount: int
RawRowCount: int
FuncBuf: bytes
FuncCap: int
FuncCount: int
def __new__(self) -> t.CPtr:
return t.CPtr(_mbuddy.alloc(TbDump.__sizeof__()))
def __init__(self):
self.RowBuf = None
self.RowCap = 0
self.RowCount = 0
self.RawRowCount = 0
self.FuncBuf = None
self.FuncCap = 0
self.FuncCount = 0
class TbPool:
Pool: bytes
PoolLen: int
Offs: bytes
OffCount: int
def __new__(self) -> t.CPtr:
return t.CPtr(_mbuddy.alloc(TbPool.__sizeof__()))
def __init__(self):
self.Pool = None
self.PoolLen = 0
self.Offs = None
self.OffCount = 0
# ---- little-endian byte helpers (no struct.pack in Viper) ----
def _tb_u8(buf: bytes, off: sizet) -> uint32:
return uint32(int(buf[off]) & 0xFF)
def _tb_rd_u16(buf: bytes, off: sizet) -> uint32:
return _tb_u8(buf, off) | (_tb_u8(buf, off + 1) << 8)
def _tb_rd_u32(buf: bytes, off: sizet) -> uint32:
b0: uint32 = _tb_u8(buf, off)
b1: uint32 = _tb_u8(buf, off + 1)
b2: uint32 = _tb_u8(buf, off + 2)
b3: uint32 = _tb_u8(buf, off + 3)
return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
def _tb_rd_u64(buf: bytes, off: sizet) -> uint64:
lo: uint64 = uint64(_tb_rd_u32(buf, off))
hi: uint64 = uint64(_tb_rd_u32(buf, off + 4))
return lo | (hi << 32)
def _tb_wr_u32(buf: bytes, off: sizet, v: uint64):
buf[off] = char(int(v & 0xFF))
buf[off + 1] = char(int((v >> 8) & 0xFF))
buf[off + 2] = char(int((v >> 16) & 0xFF))
buf[off + 3] = char(int((v >> 24) & 0xFF))
def _tb_wr_u64(buf: bytes, off: sizet, v: uint64):
_tb_wr_u32(buf, off, v & 0xFFFFFFFF)
_tb_wr_u32(buf, off + 4, (v >> 32) & 0xFFFFFFFF)
# ---- tiny string helpers (no re module) ----
def _tb_cc(s: str, i: sizet) -> int:
"""Byte value at s[i] as 0..255 (chars in DWARF text are ASCII)."""
if s is None: return 0
return int(s[i]) & 0xFF
def _tb_contains(s: str, sub: str) -> int:
if s is None or sub is None: return 0
return 1 if string.strstr(s, sub) is not None else 0
def _tb_startswith(s: str, pfx: str) -> int:
if s is None or pfx is None: return 0
pl: sizet = string.strlen(pfx)
if string.strlen(s) < pl: return 0
return 1 if string.strncmp(s, pfx, pl) == 0 else 0
def _tb_ltrim(s: str) -> str:
if s is None: return None
i: sizet = 0
while _tb_cc(s, i) == 32 or _tb_cc(s, i) == 9: i += 1
return s + i
def _tb_len_trimmed(s: str) -> sizet:
if s is None: return 0
n: sizet = string.strlen(s)
while n > 0:
c: int = _tb_cc(s, n - 1)
if c == 32 or c == 9 or c == 13: n -= 1
else: break
return n
def _tb_dup_span(mb: memhub.MemBuddy*, src: str, n: sizet) -> str:
"""Copy src[0..n) into a fresh NUL-terminated string."""
if src is None: return None
out: str = mb.alloc(n + 1)
if out is None: return None
string.strncpy(out, src, n)
out[n] = '\0'
return out
def _tb_unquote(mb: memhub.MemBuddy*, v: str) -> str:
"""Trim trailing spaces and strip one pair of surrounding quotes (TPC _Unquote)."""
if v is None: return None
n: sizet = _tb_len_trimmed(v)
if n >= 2 and _tb_cc(v, 0) == 34 and _tb_cc(v, n - 1) == 34: return _tb_dup_span(mb, v + 1, n - 2)
return _tb_dup_span(mb, v, n)
def _tb_is_abs_path(s: str) -> int:
if s is None: return 0
c0: int = _tb_cc(s, 0)
if c0 == 0: return 0
if c0 == 47 or c0 == 92: return 1
if _tb_cc(s, 1) == 58: return 1
return 0
def _tb_join(mb: memhub.MemBuddy*, base: str, name: str) -> str:
"""Join base and name with a separator unless base already ends in one."""
if base is None or string.strlen(base) == 0: return name
if name is None: return base
bl: sizet = string.strlen(base)
nl: sizet = string.strlen(name)
buf: str = mb.alloc(bl + nl + 2)
if buf is None: return name
last: int = _tb_cc(base, bl - 1)
if last == 47 or last == 92: viperlib.snprintf(buf, bl + nl + 2, "%s%s", base, name)
else: viperlib.snprintf(buf, bl + nl + 2, "%s/%s", base, name)
return buf
def _tb_parse_dec(s: str) -> int:
"""Parse a leading decimal integer, skipping leading blanks (dwarfdump right-aligns indices)."""
v: int = 0
i: sizet = 0
while _tb_cc(s, i) == 32 or _tb_cc(s, i) == 9: i += 1
while _tb_cc(s, i) >= 48 and _tb_cc(s, i) <= 57:
v = v * 10 + (_tb_cc(s, i) - 48)
i += 1
return v
def _tb_bracket_idx(s: str) -> int:
"""Parse the N in '[N]' (first bracket group of the line)."""
i: sizet = 0
while _tb_cc(s, i) != 0:
if _tb_cc(s, i) == 91: return _tb_parse_dec(s + i + 1)
i += 1
return -1
def _tb_after_bracket(s: str) -> str:
i: sizet = 0
while _tb_cc(s, i) != 0:
if _tb_cc(s, i) == 93: return s + i + 1
i += 1
return None
def _tb_value_after_sep(s: str) -> str:
"""Return the text after the first '=' or ':' (left-trimmed), or None."""
if s is None: return None
i: sizet = 0
while _tb_cc(s, i) != 0:
c: int = _tb_cc(s, i)
if c == 61 or c == 58: return _tb_ltrim(s + i + 1)
i += 1
return None
def _tb_hex_digit(ch: int) -> int:
if ch >= 48 and ch <= 57: return ch - 48
if ch >= 97 and ch <= 102: return ch - 87
if ch >= 65 and ch <= 70: return ch - 55
return -1
def _tb_hex_in_parens(s: str, key: str) -> uint64:
"""Parse the 0x value inside the parentheses right after key; 0 when the form is absent."""
p: str = string.strstr(s, key)
if p is None: return 0
p = p + string.strlen(key)
while _tb_cc(p, 0) == 32 or _tb_cc(p, 0) == 9: p = p + 1
if _tb_cc(p, 0) != 40: return 0
p = p + 1
while _tb_cc(p, 0) == 32: p = p + 1
if _tb_cc(p, 0) != 48 or _tb_cc(p, 1) != 120: return 0
q: str = p + 2
v: uint64 = 0
i: sizet = 0
hd: int = _tb_hex_digit(_tb_cc(q, 0))
while hd >= 0:
v = (v << 4) | uint64(hd)
i += 1
hd = _tb_hex_digit(_tb_cc(q, i))
return v
def _tb_quoted_after(mb: memhub.MemBuddy*, s: str, key: str) -> str:
"""Extract the first double-quoted string following key (DW_AT_name ("..."))."""
p: str = string.strstr(s, key)
if p is None: return None
p = p + string.strlen(key)
q: str = string.strchr(p, 40)
if q is None: return None
q = q + 1
while _tb_cc(q, 0) == 32: q = q + 1
if _tb_cc(q, 0) != 34: return None
q = q + 1
start: str = q
n: sizet = 0
while _tb_cc(q, n) != 0:
c: int = _tb_cc(q, n)
if c == 92:
if _tb_cc(q, n + 1) == 0: break
n += 2
continue
if c == 34: break
n += 1
return _tb_dup_span(mb, start, n)
def _tb_dir_lookup(dirs: bytes, count: int, idx: int) -> str:
for i in range(count):
if int(_tb_rd_u32(dirs, sizet(i) * TB_DIR_SIZE)) == idx: return rawptr(_tb_rd_u64(dirs, sizet(i) * TB_DIR_SIZE + 8))
return None
def _tb_file_rec(files: bytes, count: int, idx: int) -> int:
for i in range(count):
if int(_tb_rd_u32(files, sizet(i) * TB_FILE_SIZE)) == idx: return i
return -1
def _tb_resolve_file(mb: memhub.MemBuddy*, files: bytes, fcount: int, dirs: bytes, dcount: int, frec: int) -> str:
"""Resolve a file-table entry to a path (absolute name wins, else dir join)."""
roff: sizet = sizet(frec) * TB_FILE_SIZE
cached: uint64 = _tb_rd_u64(files, roff + 24)
if cached != 0: return rawptr(cached)
name: str = rawptr(_tb_rd_u64(files, roff + 8))
path: str = name
if name is not None and _tb_is_abs_path(name) == 0:
didx: int = int(_tb_rd_u32(files, roff + 4))
base: str = _tb_dir_lookup(dirs, dcount, didx)
if base is not None and string.strlen(base) > 0:
if _tb_is_abs_path(base) == 0:
root: str = _tb_dir_lookup(dirs, dcount, 0)
if root is not None and _tb_is_abs_path(root) != 0: base = _tb_join(mb, root, base)
path = _tb_join(mb, base, name)
if path is None or string.strlen(path) == 0: path = "??"
_tb_wr_u64(files, roff + 24, uint64(path))
return path
def _tb_grow_rec(mb: memhub.MemBuddy*, buf: bytes, old_cap: int, rec_size: int, new_cap: int) -> bytes:
nb: bytes = mb.alloc(sizet(new_cap) * sizet(rec_size))
if nb is None: return None
string.memcpy(nb, buf, sizet(old_cap) * sizet(rec_size))
mb.free(buf)
return nb
def _tb_emit_row(mb: memhub.MemBuddy*, d: TbDump*, addr: uint64, path: str, line: int, fname: str = None) -> int:
if d is None: return 1
if d.RowCount >= d.RowCap:
ncap: int = d.RowCap * 2
nb: bytes = _tb_grow_rec(mb, d.RowBuf, d.RowCap, TB_ROW_SIZE, ncap)
if nb is None:
VLogger.error("--traceback: failed to grow DWARF line record table, aborting compilation", "TRACEBACK")
return 1
d.RowBuf = nb
d.RowCap = ncap
off: sizet = sizet(d.RowCount) * TB_ROW_SIZE
_tb_wr_u64(d.RowBuf, off, addr)
_tb_wr_u64(d.RowBuf, off + TB_ROW_OFF_FILE, uint64(path))
_tb_wr_u32(d.RowBuf, off + TB_ROW_OFF_LINE, uint64(line))
fv: uint64 = 0
if fname is not None: fv = uint64(fname)
_tb_wr_u64(d.RowBuf, off + TB_ROW_OFF_FUNC, fv)
d.RowCount = d.RowCount + 1
d.RawRowCount = d.RawRowCount + 1
return 0
def _tb_emit_func(mb: memhub.MemBuddy*, d: TbDump*, low: uint64, high: uint64, name: str) -> int:
if d is None: return 1
if d.FuncCount >= d.FuncCap:
ncap: int = d.FuncCap * 2
nb: bytes = _tb_grow_rec(mb, d.FuncBuf, d.FuncCap, TB_FUNC_SIZE, ncap)
if nb is None:
VLogger.error("--traceback: failed to grow DWARF subprogram table, aborting compilation", "TRACEBACK")
return 1
d.FuncBuf = nb
d.FuncCap = ncap
off: sizet = sizet(d.FuncCount) * TB_FUNC_SIZE
_tb_wr_u64(d.FuncBuf, off, low)
_tb_wr_u64(d.FuncBuf, off + 8, high)
_tb_wr_u64(d.FuncBuf, off + 16, uint64(name))
d.FuncCount = d.FuncCount + 1
return 0
def _tb_func_name_for(fbuf: bytes, fcount: int, rva: uint64) -> str:
"""bisect_right over the (sorted) func low_pc table, then range check."""
if fbuf is None or fcount <= 0: return None
lo: int = 0
hi: int = fcount
while lo < hi:
mid: int = lo + (hi - lo) / 2
if _tb_rd_u64(fbuf, sizet(mid) * TB_FUNC_SIZE) <= rva: lo = mid + 1
else: hi = mid
k: int = lo - 1
if k < 0: return None
low: uint64 = _tb_rd_u64(fbuf, sizet(k) * TB_FUNC_SIZE)
high: uint64 = _tb_rd_u64(fbuf, sizet(k) * TB_FUNC_SIZE + 8)
if low <= rva and rva < high: return rawptr(_tb_rd_u64(fbuf, sizet(k) * TB_FUNC_SIZE + 16))
return None
def _tb_intern(p: TbPool*, s: str) -> uint64:
"""Intern s into the pool (linear scan; keeps offset 0 as the empty sentinel)."""
if p is None or s is None: return 0
for k in range(p.OffCount):
o: uint64 = _tb_rd_u64(p.Offs, sizet(k) * 8)
if string.strcmp(p.Pool + sizet(o), s) == 0: return o
off: uint64 = uint64(p.PoolLen)
n: sizet = string.strlen(s)
string.memcpy(p.Pool + sizet(off), s, n)
p.Pool[sizet(off) + n] = '\0'
p.PoolLen = p.PoolLen + int(n) + 1
_tb_wr_u64(p.Offs, sizet(p.OffCount) * 8, off)
p.OffCount = p.OffCount + 1
return off
def _tb_read_pe_layout(mb: memhub.MemBuddy*, exe_path: str) -> TbPeLayout*:
"""Read (ImageBase, SizeOfImage, .text RVA) from the PE headers."""
lay: TbPeLayout* = TbPeLayout()
if lay is None: return None
f: fileio.File* = fileio.File(exe_path, fileio.MODE.R)
if f.closed: return lay
HEAD: sizet = 4096
head: bytes = mb.alloc(HEAD)
if head is None:
f.close()
return lay
got: LONG = f.read_all(head, HEAD)
f.close()
if got < 0x40: return lay
if _tb_cc(head, 0) != 77 or _tb_cc(head, 1) != 90: return lay
e_lfanew: uint64 = uint64(_tb_rd_u32(head, 0x3C))
if e_lfanew + 24 > uint64(got): return lay
if _tb_cc(head, sizet(e_lfanew)) != 80 or _tb_cc(head, sizet(e_lfanew) + 1) != 69: return lay
coff: sizet = sizet(e_lfanew) + 4
nsec: uint64 = uint64(_tb_rd_u16(head, coff + 2))
optsz: uint64 = uint64(_tb_rd_u16(head, coff + 16))
opt: sizet = coff + 20
if uint64(opt) + 32 > uint64(got): return lay
magic: uint64 = uint64(_tb_rd_u16(head, opt))
img_base: uint64 = 0
if magic == 0x20B: img_base = _tb_rd_u64(head, opt + 24)
elif magic == 0x10B: img_base = uint64(_tb_rd_u32(head, opt + 28))
else: return lay
size_img: uint64 = uint64(_tb_rd_u32(head, opt + 56))
sec_off: sizet = opt + sizet(optsz)
text_rva: uint64 = 0x1000
min_rva: uint64 = 0xFFFFFFFF
found_text: int = 0
have_sec: int = 0
for si in range(nsec):
base: sizet = sec_off + sizet(si) * 40
if uint64(base) + 40 > uint64(got): break
va: uint64 = uint64(_tb_rd_u32(head, base + 12))
have_sec = 1
if va < min_rva: min_rva = va
if _tb_cc(head, base) == 46 and _tb_cc(head, base + 1) == 116 and _tb_cc(head, base + 2) == 101 and _tb_cc(head, base + 3) == 120 and _tb_cc(head, base + 4) == 116:
text_rva = va
found_text = 1
if found_text == 0 and have_sec != 0 and min_rva != 0xFFFFFFFF: text_rva = min_rva
lay.ImageBase = img_base
lay.SizeOfImage = size_img
lay.TextRva = text_rva
lay.Ok = 1
return lay
def _tb_parse_dump(mb: memhub.MemBuddy*, dump: str) -> TbDump*:
"""Parse llvm-dwarfdump text into a row table (addr, path, line) and a func table."""
d: TbDump* = TbDump()
if d is None: return None
LINE_CAP: sizet = 8192
d.RowBuf = mb.alloc(sizet(TB_ROW_CAP0) * TB_ROW_SIZE)
d.RowCap = TB_ROW_CAP0
d.FuncBuf = mb.alloc(sizet(TB_FUNC_CAP0) * TB_FUNC_SIZE)
d.FuncCap = TB_FUNC_CAP0
dirs: bytes = mb.alloc(sizet(TB_DIR_CAP) * TB_DIR_SIZE)
files: bytes = mb.alloc(sizet(TB_FILE_CAP) * TB_FILE_SIZE)
linebuf: bytes = mb.alloc(LINE_CAP)
if d.RowBuf is None or d.FuncBuf is None or dirs is None or files is None or linebuf is None:
VLogger.error("--traceback: failed to allocate dwarfdump parse buffer, aborting compilation", "TRACEBACK")
return None
in_line: int = 0
dir_count: int = 0
file_count: int = 0
pkind: int = 0 # 0=none, 1=file, 2=dir
pidx: int = 0
pname: str = None
pdir: int = 0
in_prog: int = 0
cur_low: uint64 = 0
cur_high: uint64 = 0
cur_has_low: int = 0
cur_has_high: int = 0
cname: str = None
dlen: sizet = string.strlen(dump)
pos: sizet = 0
while pos < dlen:
ls: sizet = pos
while pos < dlen and _tb_cc(dump, pos) != 10: pos += 1
le: sizet = pos
if pos < dlen: pos += 1
if le > ls and _tb_cc(dump, le - 1) == 13: le -= 1
ll: sizet = le - ls
if ll == 0: continue
if ll >= LINE_CAP: ll = LINE_CAP - 1
string.strncpy(linebuf, dump + ls, ll)
linebuf[ll] = '\0'
st: str = _tb_ltrim(linebuf)
# ---- line-table section gate ----
if _tb_contains(st, "debug_line contents") != 0:
in_line = 1
continue
if _tb_contains(st, "contents:") != 0 and _tb_contains(st, "debug_") != 0:
in_line = 0
continue
if in_line != 0:
if _tb_startswith(st, "debug_line[") != 0:
# new CU: commit pending file, reset both tables
if pkind == 1 and pname is not None and file_count < TB_FILE_CAP:
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx))
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0))
file_count += 1
pkind = 0
pname = None
pdir = 0
dir_count = 0
file_count = 0
continue
if _tb_startswith(st, "file_names[") != 0:
if pkind == 1 and pname is not None and file_count < TB_FILE_CAP:
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx))
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0))
file_count += 1
pname = None
pdir = 0
pkind = 1
pidx = _tb_bracket_idx(st)
continue
if _tb_startswith(st, "directory_names[") != 0 or _tb_startswith(st, "include_directories[") != 0:
if pkind == 1 and pname is not None and file_count < TB_FILE_CAP:
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx))
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0))
file_count += 1
pname = None
pdir = 0
pidx = _tb_bracket_idx(st)
val: str = _tb_value_after_sep(_tb_after_bracket(st))
if val is not None and _tb_len_trimmed(val) > 0:
if dir_count < TB_DIR_CAP:
_tb_wr_u32(dirs, sizet(dir_count) * TB_DIR_SIZE, uint64(pidx))
_tb_wr_u64(dirs, sizet(dir_count) * TB_DIR_SIZE + 8, uint64(_tb_unquote(mb, val)))
dir_count += 1
pkind = 0
else: pkind = 2
continue
if _tb_startswith(st, "name:") != 0 and pkind != 0:
val2: str = _tb_unquote(mb, _tb_ltrim(st + 5))
if pkind == 1: pname = val2
else:
if dir_count < TB_DIR_CAP and val2 is not None:
_tb_wr_u32(dirs, sizet(dir_count) * TB_DIR_SIZE, uint64(pidx))
_tb_wr_u64(dirs, sizet(dir_count) * TB_DIR_SIZE + 8, uint64(val2))
dir_count += 1
pkind = 0
continue
if _tb_startswith(st, "dir_index:") != 0 and pkind == 1:
# "dir_index:" is 10 chars with the colon; st + 9 would point at the colon and
# always parse 0 (losing dir indices). Use st + 10.
pdir = _tb_parse_dec(_tb_ltrim(st + 10))
continue
if _tb_startswith(st, "Address") != 0 and _tb_contains(st, "Column") != 0:
if pkind == 1 and pname is not None and file_count < TB_FILE_CAP:
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx))
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0))
file_count += 1
pkind = 0
pname = None
pdir = 0
continue
if _tb_cc(linebuf, 0) == 48 and _tb_cc(linebuf, 1) == 120:
if pkind == 1 and pname is not None and file_count < TB_FILE_CAP:
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx))
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0))
file_count += 1
pkind = 0
pname = None
pdir = 0
ri: sizet = 2
addr: uint64 = 0
hd: int = _tb_hex_digit(_tb_cc(linebuf, ri))
while hd >= 0:
addr = (addr << 4) | uint64(hd)
ri += 1
hd = _tb_hex_digit(_tb_cc(linebuf, ri))
while ri < ll and (_tb_cc(linebuf, ri) == 32 or _tb_cc(linebuf, ri) == 9): ri += 1
lno: int = 0
while ri < ll and _tb_cc(linebuf, ri) >= 48 and _tb_cc(linebuf, ri) <= 57:
lno = lno * 10 + (_tb_cc(linebuf, ri) - 48)
ri += 1
while ri < ll and (_tb_cc(linebuf, ri) == 32 or _tb_cc(linebuf, ri) == 9): ri += 1
while ri < ll and _tb_cc(linebuf, ri) >= 48 and _tb_cc(linebuf, ri) <= 57: ri += 1
while ri < ll and (_tb_cc(linebuf, ri) == 32 or _tb_cc(linebuf, ri) == 9): ri += 1
fidx: int = 0
while ri < ll and _tb_cc(linebuf, ri) >= 48 and _tb_cc(linebuf, ri) <= 57:
fidx = fidx * 10 + (_tb_cc(linebuf, ri) - 48)
ri += 1
if lno > 0:
frec: int = _tb_file_rec(files, file_count, fidx)
fpath: str = None
if frec >= 0: fpath = _tb_resolve_file(mb, files, file_count, dirs, dir_count, frec)
if fpath is None: fpath = "??"
if _tb_emit_row(mb, d, addr, fpath, lno) != 0: return None
continue
# ---- DWARF debug_info subprogram parsing ----
if _tb_contains(linebuf, "DW_TAG_") != 0:
if in_prog != 0 and cur_has_low != 0 and cname is not None and string.strlen(cname) > 0:
hi: uint64 = cur_low + 1
if cur_has_high != 0:
hi = cur_high
if hi <= cur_low: hi = cur_low + cur_high
if hi > cur_low:
if _tb_emit_func(mb, d, cur_low, hi, cname) != 0: return None
in_prog = 0
cur_low = 0
cur_high = 0
cur_has_low = 0
cur_has_high = 0
cname = None
if _tb_contains(linebuf, "DW_TAG_subprogram") != 0: in_prog = 1
continue
if in_prog == 0: continue
if _tb_contains(linebuf, "DW_AT_low_pc") != 0:
cur_low = _tb_hex_in_parens(linebuf, "DW_AT_low_pc")
cur_has_low = 1
continue
if _tb_contains(linebuf, "DW_AT_high_pc") != 0:
cur_high = _tb_hex_in_parens(linebuf, "DW_AT_high_pc")
cur_has_high = 1
continue
if _tb_contains(linebuf, "DW_AT_name") != 0 and cname is None:
cname = _tb_quoted_after(mb, linebuf, "DW_AT_name")
continue
# trailing subprogram
if in_prog != 0 and cur_has_low != 0 and cname is not None and string.strlen(cname) > 0:
hi2: uint64 = cur_low + 1
if cur_has_high != 0:
hi2 = cur_high
if hi2 <= cur_low: hi2 = cur_low + cur_high
if hi2 > cur_low:
if _tb_emit_func(mb, d, cur_low, hi2, cname) != 0: return None
return d
# --traceback call-site normalisation: .vpcall + llvm-objdump. The MLIR backend emits no DWARF
# for our .vp functions, so each direct callq's return address is mapped back to file/line/function.
# call record: u64 caller | u64 callee | u64 file | u32 line | u32 next (next in group, NONE=tail)
define TB_CR_SIZE = 32
define TB_CR_OFF_CALLER = 0
define TB_CR_OFF_CALLEE = 8
define TB_CR_OFF_FILE = 16
define TB_CR_OFF_LINE = 24
define TB_CR_OFF_NEXT = 28
# function record: u64 sym | u64 file | u32 defline | u32 pad
define TB_FR_SIZE = 24
define TB_FR_OFF_SYM = 0
define TB_FR_OFF_FILE = 8
define TB_FR_OFF_DEFLINE = 16
# group hash slots (keyed by (caller,callee); open addressing + linear probing):
# u64 caller | u64 callee | u32 used | u32 head | u32 tail | u32 cursor
define TB_GRP_SIZE = 32
define TB_GRP_OFF_CALLER = 0
define TB_GRP_OFF_CALLEE = 8
define TB_GRP_OFF_USED = 16
define TB_GRP_OFF_HEAD = 20
define TB_GRP_OFF_TAIL = 24
define TB_GRP_OFF_CURSOR = 28
# invalid-index sentinel (all-ones u32)
TB_IDX_NONE: uint64 = 0xFFFFFFFF
class TbCallParse:
Cr: bytes # array of TB_CR_SIZE records
CrCap: int
CrN: int
Fr: bytes # array of TB_FR_SIZE records
FrCap: int
FrN: int
Grp: bytes # array of TB_GRP_SIZE slots
GrpCap: int
def __new__(self) -> t.CPtr:
return t.CPtr(_mbuddy.alloc(TbCallParse.__sizeof__()))
def __init__(self):
self.Cr = None
self.CrCap = 0
self.CrN = 0
self.Fr = None
self.FrCap = 0
self.FrN = 0
self.Grp = None
self.GrpCap = 0
def _tb_next_pow2(n: int) -> int:
p: int = 16
while p < n: p = p * 2
return p
def _tb_strhash(s: str) -> uint64:
"""FNV-1a hash (32-bit offset basis, 64-bit prime) used only for table addressing."""
h: uint64 = 0x811C9DC5
if s is None: return h
i: sizet = 0
while _tb_cc(s, i) != 0:
h = (h ^ uint64(_tb_cc(s, i))) * 0x100000001B3
i += 1
return h
def _tb_grp_get(grp: bytes, cap: int, caller: str, callee: str, do_insert: int) -> int:
"""Look up a slot by (caller, callee); insert on miss when do_insert=1; slot or -1."""
if grp is None or cap <= 0 or caller is None or callee is None: return -1
mask: uint64 = uint64(cap - 1)
hv: uint64 = (_tb_strhash(caller) * 0x100000001B3 + _tb_strhash(callee)) & mask
for i in range(cap):
slot: int = int((hv + uint64(i)) & mask)
off: sizet = sizet(slot) * TB_GRP_SIZE
if uint64(_tb_rd_u32(grp, off + TB_GRP_OFF_USED)) == 0:
if do_insert == 0: return -1
_tb_wr_u32(grp, off + TB_GRP_OFF_USED, uint64(1))
_tb_wr_u64(grp, off + TB_GRP_OFF_CALLER, uint64(caller))
_tb_wr_u64(grp, off + TB_GRP_OFF_CALLEE, uint64(callee))
_tb_wr_u32(grp, off + TB_GRP_OFF_HEAD, uint64(TB_IDX_NONE))
_tb_wr_u32(grp, off + TB_GRP_OFF_TAIL, uint64(TB_IDX_NONE))
_tb_wr_u32(grp, off + TB_GRP_OFF_CURSOR, uint64(TB_IDX_NONE))
return slot
sc: str = rawptr(_tb_rd_u64(grp, off + TB_GRP_OFF_CALLER))
sk: str = rawptr(_tb_rd_u64(grp, off + TB_GRP_OFF_CALLEE))
if sc is not None and sk is not None and string.strcmp(sc, caller) == 0 and string.strcmp(sk, callee) == 0: return slot
return -1
def _tb_cp_init(mb: memhub.MemBuddy*) -> TbCallParse*:
cp: TbCallParse* = TbCallParse()
if cp is None: return None
cp.CrCap = 1024
cp.FrCap = 1024
cp.Cr = mb.alloc(sizet(cp.CrCap) * TB_CR_SIZE)
cp.Fr = mb.alloc(sizet(cp.FrCap) * TB_FR_SIZE)
if cp.Cr is None or cp.Fr is None: return None
cp.CrN = 0
cp.FrN = 0
return cp
def _tb_cp_add_cr(mb: memhub.MemBuddy*, cp: TbCallParse*, caller: str, callee: str, file: str, line: int) -> int:
if cp.CrN >= cp.CrCap:
ncap: int = cp.CrCap * 2
nb: bytes = _tb_grow_rec(mb, cp.Cr, cp.CrCap, TB_CR_SIZE, ncap)
if nb is None:
VLogger.error("--traceback: failed to grow call record table, aborting compilation", "TRACEBACK")
return 1
cp.Cr = nb
cp.CrCap = ncap
off: sizet = sizet(cp.CrN) * TB_CR_SIZE
_tb_wr_u64(cp.Cr, off + TB_CR_OFF_CALLER, uint64(caller))
_tb_wr_u64(cp.Cr, off + TB_CR_OFF_CALLEE, uint64(callee))
_tb_wr_u64(cp.Cr, off + TB_CR_OFF_FILE, uint64(file))
_tb_wr_u32(cp.Cr, off + TB_CR_OFF_LINE, uint64(line))
_tb_wr_u32(cp.Cr, off + TB_CR_OFF_NEXT, uint64(TB_IDX_NONE))
cp.CrN = cp.CrN + 1
return 0
def _tb_cp_add_fr(mb: memhub.MemBuddy*, cp: TbCallParse*, sym: str, file: str, defline: int) -> int:
if cp.FrN >= cp.FrCap:
ncap: int = cp.FrCap * 2
nb: bytes = _tb_grow_rec(mb, cp.Fr, cp.FrCap, TB_FR_SIZE, ncap)
if nb is None:
VLogger.error("--traceback: failed to grow function record table, aborting compilation", "TRACEBACK")
return 1
cp.Fr = nb
cp.FrCap = ncap
off: sizet = sizet(cp.FrN) * TB_FR_SIZE
_tb_wr_u64(cp.Fr, off + TB_FR_OFF_SYM, uint64(sym))
_tb_wr_u64(cp.Fr, off + TB_FR_OFF_FILE, uint64(file))
_tb_wr_u32(cp.Fr, off + TB_FR_OFF_DEFLINE, uint64(defline))
cp.FrN = cp.FrN + 1
return 0
def _tb_read_whole(mb: memhub.MemBuddy*, path: str) -> str:
"""Read the whole file into a NUL-terminated string; None on failure."""
f: fileio.File* = fileio.File(path, fileio.MODE.R)
if f.closed: return None
sz: LONGLONG = f.size()
if sz < 0:
f.close()
return None
buf: str = mb.alloc(sizet(sz) + 2)
if buf is None:
f.close()
return None
got: LONG = f.read_all(buf, ULONG(sz) + ULONG(2))
f.close()
if got < 0: return None
return buf
def _tb_parse_f_line(mb: memhub.MemBuddy*, s: str, a: sizet, b: sizet, cp: TbCallParse*) -> int:
"""Parse an F record line with symbol, defline and file fields (tab-separated)."""
p: sizet = a
while p < b and _tb_cc(s, p) != 9: p += 1
if p >= b: return 0
p += 1
f2: sizet = p
while f2 < b and _tb_cc(s, f2) != 9: f2 += 1
if f2 >= b: return 0
sym: str = _tb_dup_span(mb, s + p, f2 - p)
p = f2 + 1
f3: sizet = p
while f3 < b and _tb_cc(s, f3) != 9: f3 += 1
if f3 >= b: return 0
defline: int = _tb_parse_dec(s + p)
p = f3 + 1
file: str = _tb_dup_span(mb, s + p, b - p)
if sym is None or file is None: return 0
return _tb_cp_add_fr(mb, cp, sym, file, defline)
def _tb_parse_c_line(mb: memhub.MemBuddy*, s: str, a: sizet, b: sizet, cp: TbCallParse*) -> int:
"""Parse a C record line with caller, callee, line and file fields (tab-separated)."""
p: sizet = a
while p < b and _tb_cc(s, p) != 9: p += 1
if p >= b: return 0
p += 1
e2: sizet = p
while e2 < b and _tb_cc(s, e2) != 9: e2 += 1
if e2 >= b: return 0
caller: str = _tb_dup_span(mb, s + p, e2 - p)
p = e2 + 1
e3: sizet = p
while e3 < b and _tb_cc(s, e3) != 9: e3 += 1
if e3 >= b: return 0
callee: str = _tb_dup_span(mb, s + p, e3 - p)
p = e3 + 1
e4: sizet = p
while e4 < b and _tb_cc(s, e4) != 9: e4 += 1
if e4 >= b: return 0
line: int = _tb_parse_dec(s + p)
p = e4 + 1
file: str = _tb_dup_span(mb, s + p, b - p)
if caller is None or callee is None or file is None: return 0
return _tb_cp_add_cr(mb, cp, caller, callee, file, line)
def _tb_parse_vpcall_file(mb: memhub.MemBuddy*, path: str, cp: TbCallParse*) -> int:
whole: str = _tb_read_whole(mb, path)
if whole is None:
VLogger.error(f"--traceback: failed to read .vpcall: {path}, aborting compilation", "TRACEBACK")
return 1
n: sizet = string.strlen(whole)
pos: sizet = 0
while pos < n:
ls: sizet = pos
while pos < n and _tb_cc(whole, pos) != 10: pos += 1
le: sizet = pos
if pos < n: pos += 1
if le > ls and _tb_cc(whole, le - 1) == 13: le -= 1
if le > ls:
typ: int = _tb_cc(whole, ls)
if typ == 70:
if _tb_parse_f_line(mb, whole, ls, le, cp) != 0: return 1
elif typ == 67:
if _tb_parse_c_line(mb, whole, ls, le, cp) != 0: return 1
return 0
def _tb_load_vpcalls(mb: memhub.MemBuddy*, cp: TbCallParse*) -> int:
if _tb_vpcall_buf is None or _tb_vpcall_n <= 0: return 0
for k in range(_tb_vpcall_n):
slot: str* = (str*)(uint64(_tb_vpcall_buf) + sizet(k) * 8)
path: str = slot[0]
if path is not None:
if _tb_parse_vpcall_file(mb, path, cp) != 0: return 1
return 0
def _tb_build_groups(mb: memhub.MemBuddy*, cp: TbCallParse*) -> int:
"""Chain all C records into group lists keyed by (caller, callee) (head/tail/cursor)."""
if cp.CrN <= 0: return 0
cp.GrpCap = _tb_next_pow2(cp.CrN * 2 + 16)
cp.Grp = mb.alloc(sizet(cp.GrpCap) * TB_GRP_SIZE)
if cp.Grp is None:
VLogger.error("--traceback: failed to allocate call-site group table, aborting compilation", "TRACEBACK")
return 1
string.memset(cp.Grp, 0, sizet(cp.GrpCap) * TB_GRP_SIZE)
for i in range(cp.CrN):
off: sizet = sizet(i) * TB_CR_SIZE
caller: str = rawptr(_tb_rd_u64(cp.Cr, off + TB_CR_OFF_CALLER))
callee: str = rawptr(_tb_rd_u64(cp.Cr, off + TB_CR_OFF_CALLEE))
slot: int = _tb_grp_get(cp.Grp, cp.GrpCap, caller, callee, 1)
if slot < 0:
VLogger.error("--traceback: call-site group hash table overflow, aborting compilation", "TRACEBACK")
return 1
goff: sizet = sizet(slot) * TB_GRP_SIZE
tailv: uint64 = uint64(_tb_rd_u32(cp.Grp, goff + TB_GRP_OFF_TAIL))
if tailv == uint64(TB_IDX_NONE):
_tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_HEAD, uint64(i))
_tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_CURSOR, uint64(i))
else:
toff: sizet = sizet(int(tailv)) * TB_CR_SIZE
_tb_wr_u32(cp.Cr, toff + TB_CR_OFF_NEXT, uint64(i))
_tb_wr_u32(cp.Cr, off + TB_CR_OFF_NEXT, uint64(TB_IDX_NONE))
_tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_TAIL, uint64(i))
return 0
def _tb_emit_call_row(mb: memhub.MemBuddy*, d: TbDump*, cp: TbCallParse*, img_lo: uint64, caller: str, callee: str, ret_va: uint64) -> int:
"""Emit a row for a return address from the next unconsumed C record of (caller, callee)."""
if caller is None or callee is None or cp.Grp is None or ret_va < img_lo: return 0
slot: int = _tb_grp_get(cp.Grp, cp.GrpCap, caller, callee, 0)
if slot < 0: return 0
goff: sizet = sizet(slot) * TB_GRP_SIZE
curv: uint64 = uint64(_tb_rd_u32(cp.Grp, goff + TB_GRP_OFF_CURSOR))
if curv == uint64(TB_IDX_NONE): return 0
ci: int = int(curv)
coff: sizet = sizet(ci) * TB_CR_SIZE
file: str = rawptr(_tb_rd_u64(cp.Cr, coff + TB_CR_OFF_FILE))
line: int = int(_tb_rd_u32(cp.Cr, coff + TB_CR_OFF_LINE))
nxt: uint64 = uint64(_tb_rd_u32(cp.Cr, coff + TB_CR_OFF_NEXT))
_tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_CURSOR, nxt)
return _tb_emit_row(mb, d, ret_va, file, line, caller)
def _tb_emit_entry_row(mb: memhub.MemBuddy*, d: TbDump*, cp: TbCallParse*, img_lo: uint64, addr: uint64, sym: str) -> int:
"""Emit a fallback function-entry row for an objdump label sym (file/line from the F record)."""
if sym is None or cp.Fr is None or cp.FrN <= 0 or addr < img_lo: return 0
for i in range(cp.FrN):
off: sizet = sizet(i) * TB_FR_SIZE
s: str = rawptr(_tb_rd_u64(cp.Fr, off + TB_FR_OFF_SYM))
if s is not None and string.strcmp(s, sym) == 0:
file: str = rawptr(_tb_rd_u64(cp.Fr, off + TB_FR_OFF_FILE))
dl: int = int(_tb_rd_u32(cp.Fr, off + TB_FR_OFF_DEFLINE))
return _tb_emit_row(mb, d, addr, file, dl, sym)
return 0
def _tb_hex_span(s: str, a: sizet, b: sizet) -> uint64:
v: uint64 = 0
i: sizet = a
while i < b:
hd: int = _tb_hex_digit(_tb_cc(s, i))
if hd < 0: break
v = (v << 4) | uint64(hd)
i += 1
return v
def _tb_run_objdump(mb: memhub.MemBuddy*, exe_path: str) -> str:
cl: sizet = string.strlen(exe_path) + 64
cmd: bytes = mb.alloc(cl)
if cmd is None:
VLogger.error("--traceback: failed to allocate objdump command buffer, aborting compilation", "TRACEBACK")
return None
viperlib.snprintf(cmd, cl, "llvm-objdump -d \"%s\"", exe_path)
res: subprocess.CompletedProcess* = subprocess.run(cmd, True, True)
if res is None or res.returncode != 0:
rc: int = -1
if res is not None: rc = res.returncode
VLogger.error(f"--traceback: failed to run llvm-objdump (return code {rc}; ensure LLVM tools are in PATH), aborting compilation", "TRACEBACK")
return None
dump: str = res.stdout
if dump is None:
VLogger.error("--traceback: llvm-objdump produced no output, aborting compilation", "TRACEBACK")
return None
return dump
def _tb_scan_objdump(mb: memhub.MemBuddy*, dump: str, img_lo: uint64, d: TbDump*, cp: TbCallParse*) -> int:
"""Parse llvm-objdump -d text, match C/F records and write our rows into d."""
dlen: sizet = string.strlen(dump)
pos: sizet = 0
in_text: int = 0
cur_func: str = None
prev_is_call: int = 0
prev_callee: str = None
prev_func: str = None
while pos < dlen:
ls: sizet = pos
while pos < dlen and _tb_cc(dump, pos) != 10: pos += 1
le: sizet = pos
if pos < dlen: pos += 1
if le > ls and _tb_cc(dump, le - 1) == 13: le -= 1
if le <= ls: continue
if _tb_startswith(dump + ls, "Disassembly of section") != 0:
# section change: a pending callq at the previous section's end has no valid return address
prev_is_call = 0
if _tb_contains(dump + ls, ".text") != 0: in_text = 1
else: in_text = 0
continue
if in_text == 0: continue
c0: int = _tb_cc(dump, ls)
is_hex0: int = 0
if (c0 >= 48 and c0 <= 57) or (c0 >= 97 and c0 <= 102) or (c0 >= 65 and c0 <= 70): is_hex0 = 1
if is_hex0 == 0: continue
# first space
sp: sizet = ls
while sp < le and _tb_cc(dump, sp) != 32: sp += 1
if sp + 1 < le and _tb_cc(dump, sp + 1) == 60:
# ---- label line `ADDR <name>:` ----
laddr: uint64 = _tb_hex_span(dump, ls, sp)
if prev_is_call != 0:
_tb_emit_call_row(mb, d, cp, img_lo, prev_func, prev_callee, laddr)
prev_is_call = 0
lb: sizet = sp + 2
j: sizet = lb
while j < le and _tb_cc(dump, j) != 62: j += 1
cur_func = _tb_dup_span(mb, dump + lb, j - lb)
if cur_func is not None: _tb_emit_entry_row(mb, d, cp, img_lo, laddr, cur_func)
continue
# ---- instruction line `ADDR: bytes mnemonic operands` ----
k: sizet = ls
while k < le and _tb_cc(dump, k) != 58: k += 1
if k >= le: continue
iaddr: uint64 = _tb_hex_span(dump, ls, k)
if prev_is_call != 0:
_tb_emit_call_row(mb, d, cp, img_lo, prev_func, prev_callee, iaddr)
prev_is_call = 0
# skip byte tokens (exactly 2 hex chars) and locate the mnemonic start
p: sizet = k + 1
while p < le and (_tb_cc(dump, p) == 32 or _tb_cc(dump, p) == 9): p += 1
ms: sizet = p
while p < le:
ts: sizet = p
while p < le and _tb_cc(dump, p) != 32 and _tb_cc(dump, p) != 9: p += 1
tl: sizet = p - ts
ishex: int = 0
if tl == 2 and _tb_hex_digit(_tb_cc(dump, ts)) >= 0 and _tb_hex_digit(_tb_cc(dump, ts + 1)) >= 0: ishex = 1
ms = ts
if ishex == 0: break
while p < le and (_tb_cc(dump, p) == 32 or _tb_cc(dump, p) == 9): p += 1
me: sizet = p
mlen: sizet = me - ms
is_call: int = 0
if mlen == 5 and string.strncmp(dump + ms, "callq", 5) == 0: is_call = 1
elif mlen == 4 and string.strncmp(dump + ms, "call", 4) == 0: is_call = 1
if is_call != 0:
os: sizet = me
while os < le and (_tb_cc(dump, os) == 32 or _tb_cc(dump, os) == 9): os += 1
if os + 1 < le and _tb_cc(dump, os) == 48 and _tb_cc(dump, os + 1) == 120:
# direct call: extract `<callee>`
ab: sizet = os
while ab < le and _tb_cc(dump, ab) != 60: ab += 1
if ab < le:
cb: sizet = ab + 1
ce: sizet = cb
while ce < le and _tb_cc(dump, ce) != 62: ce += 1
xe: sizet = cb
while xe < ce and _tb_cc(dump, xe) != 43: xe += 1
prev_callee = _tb_dup_span(mb, dump + cb, xe - cb)
prev_func = cur_func
prev_is_call = 1
return 0
def WriteStackMap(mb: memhub.MemBuddy*, exe_path: str) -> int:
"""--traceback: export <exe>.stackmap after a successful link. 0 = ok."""
if mb is None or exe_path is None:
VLogger.error("--traceback: WriteStackMap argument is null, aborting compilation", "TRACEBACK")
return 1
lay: TbPeLayout* = _tb_read_pe_layout(mb, exe_path)
if lay is None or lay.Ok == 0:
VLogger.error(f"--traceback: only Windows PE output is supported, but {exe_path} is not a valid PE (required by --traceback)", "TRACEBACK")
return 1
size_of_image: uint64 = lay.SizeOfImage
if size_of_image == 0:
VLogger.error(f"--traceback: read PE.SizeOfImage is not positive ({size_of_image}), cannot bound RVA range (required by --traceback)", "TRACEBACK")
return 1
img_lo: uint64 = lay.ImageBase
img_hi: uint64 = lay.ImageBase + size_of_image
text_rva: uint64 = lay.TextRva
# ---- 1) run llvm-dwarfdump (bare name: CreateProcessA resolves it via PATH) ----
cmd_len: sizet = string.strlen(exe_path) + 64
cmd: bytes = mb.alloc(cmd_len)
if cmd is None:
VLogger.error("--traceback: failed to allocate dwarfdump command buffer, aborting compilation", "TRACEBACK")
return 1
viperlib.snprintf(cmd, cmd_len, "llvm-dwarfdump --debug-line --debug-info \"%s\"", exe_path)
res: subprocess.CompletedProcess* = subprocess.run(cmd, True, True)
if res is None or res.returncode != 0:
rc: int = -1
if res is not None: rc = res.returncode
VLogger.error(f"--traceback: failed to run llvm-dwarfdump (return code {rc}; ensure LLVM tools are in PATH), aborting compilation", "TRACEBACK")
return 1
dump: str = res.stdout
if dump is None:
VLogger.error("--traceback: llvm-dwarfdump produced no output, aborting compilation", "TRACEBACK")
return 1
d: TbDump* = _tb_parse_dump(mb, dump)
if d is None: return 1
# ---- 1b) our call-site rows via .vpcall + llvm-objdump ----
# DWARF rows only cover the linked mingw CRT; .vpcall + objdump add return-address -> call-site rows.
if _tb_vpcall_n > 0:
cp: TbCallParse* = _tb_cp_init(mb)
if cp is None:
VLogger.error("--traceback: failed to allocate call-site parse state, aborting compilation", "TRACEBACK")
return 1
if _tb_load_vpcalls(mb, cp) != 0: return 1
if _tb_build_groups(mb, cp) != 0: return 1
odump: str = _tb_run_objdump(mb, exe_path)
if odump is None: return 1
our_before: int = d.RowCount
if _tb_scan_objdump(mb, odump, img_lo, d, cp) != 0: return 1
our_added: int = d.RowCount - our_before
if our_added > 0: VLogger.success(f"--traceback: from {_tb_vpcall_n} .vpcall files generated {our_added} own call-site records (F={cp.FrN} functions, C={cp.CrN} calls)", "TRACEBACK")
else: VLogger.warning(f"--traceback: read {_tb_vpcall_n} .vpcall files (F={cp.FrN}, C={cp.CrN}) but matched no objdump call sites -- own frame line numbers still missing, falling back to DWARF", "TRACEBACK")
else: VLogger.warning("--traceback: no .vpcall registered (own modules have no call-site records), falling back to DWARF behavior", "TRACEBACK")
if d.RawRowCount == 0:
VLogger.error(f"--traceback: could not parse any line records from {exe_path} (DWARF .debug_line is empty and there are no own .vpcall records; required by --traceback)", "TRACEBACK")
return 1
# ---- 2) rows: normalise to RVA, same rva keeps the largest line ----
n: int = d.RowCount
dropped_rows: int = 0
kept: int = 0
i: int = 0
while i < n:
a: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE)
if a < img_lo or a >= img_hi:
dropped_rows += 1
i += 1
continue
if kept != i:
_tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE, a - img_lo)
_tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE + TB_ROW_OFF_FILE, _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE))
_tb_wr_u32(d.RowBuf, sizet(kept) * TB_ROW_SIZE + TB_ROW_OFF_LINE, uint64(_tb_rd_u32(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_LINE)))
_tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE + TB_ROW_OFF_FUNC, _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC))
else: _tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE, a - img_lo)
kept += 1
i += 1
tmp: bytes = mb.alloc(TB_ROW_SIZE)
if tmp is None:
VLogger.error("--traceback: failed to allocate line record sort buffer, aborting compilation", "TRACEBACK")
return 1
si2: int = 1
while si2 < kept:
string.memcpy(tmp, d.RowBuf + sizet(si2) * TB_ROW_SIZE, TB_ROW_SIZE)
sj: int = si2
while sj > 0:
if _tb_rd_u64(d.RowBuf, sizet(sj - 1) * TB_ROW_SIZE) <= _tb_rd_u64(tmp, 0): break
string.memcpy(d.RowBuf + sizet(sj) * TB_ROW_SIZE, d.RowBuf + sizet(sj - 1) * TB_ROW_SIZE, TB_ROW_SIZE)
sj -= 1
string.memcpy(d.RowBuf + sizet(sj) * TB_ROW_SIZE, tmp, TB_ROW_SIZE)
si2 += 1
uniq: int = 0
i = 0
while i < kept:
rv: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE)
bl: uint64 = uint64(_tb_rd_u32(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_LINE))
bp: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE)
bf: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC)
j: int = i + 1
while j < kept and _tb_rd_u64(d.RowBuf, sizet(j) * TB_ROW_SIZE) == rv:
lj: uint64 = uint64(_tb_rd_u32(d.RowBuf, sizet(j) * TB_ROW_SIZE + TB_ROW_OFF_LINE))
if lj > bl:
bl = lj
bp = _tb_rd_u64(d.RowBuf, sizet(j) * TB_ROW_SIZE + TB_ROW_OFF_FILE)
bf = _tb_rd_u64(d.RowBuf, sizet(j) * TB_ROW_SIZE + TB_ROW_OFF_FUNC)
j += 1
if uniq != i:
_tb_wr_u64(d.RowBuf, sizet(uniq) * TB_ROW_SIZE, rv)
_tb_wr_u64(d.RowBuf, sizet(uniq) * TB_ROW_SIZE + TB_ROW_OFF_FILE, bp)
_tb_wr_u32(d.RowBuf, sizet(uniq) * TB_ROW_SIZE + TB_ROW_OFF_LINE, bl)
_tb_wr_u64(d.RowBuf, sizet(uniq) * TB_ROW_SIZE + TB_ROW_OFF_FUNC, bf)
uniq += 1
i = j
if dropped_rows > 0: VLogger.warning(f"--traceback: dropped {dropped_rows} DWARF line records that cannot be normalised (address not in image VA range, mostly COMDAT dedup copies)", "TRACEBACK")
if uniq == 0:
VLogger.error(f"--traceback: all line records from {exe_path} cannot be normalised to runtime RVA range, aborting compilation", "TRACEBACK")
return 1
# ---- 3) subprograms: normalise + sort by low_rva ----
fcount: int = d.FuncCount
fkept: int = 0
dropped_funcs: int = 0
fi: int = 0
while fi < fcount:
low: uint64 = _tb_rd_u64(d.FuncBuf, sizet(fi) * TB_FUNC_SIZE)
if low < img_lo or low >= img_hi:
dropped_funcs += 1
fi += 1
continue
high: uint64 = _tb_rd_u64(d.FuncBuf, sizet(fi) * TB_FUNC_SIZE + 8)
lo_rva: uint64 = low - img_lo
hi_rva: uint64 = high - img_lo
if hi_rva <= lo_rva: hi_rva = lo_rva + 1
if hi_rva > size_of_image: hi_rva = size_of_image
_tb_wr_u64(d.FuncBuf, sizet(fkept) * TB_FUNC_SIZE, lo_rva)
_tb_wr_u64(d.FuncBuf, sizet(fkept) * TB_FUNC_SIZE + 8, hi_rva)
_tb_wr_u64(d.FuncBuf, sizet(fkept) * TB_FUNC_SIZE + 16, _tb_rd_u64(d.FuncBuf, sizet(fi) * TB_FUNC_SIZE + 16))
fkept += 1
fi += 1
ftmp: bytes = mb.alloc(TB_FUNC_SIZE)
if ftmp is None:
VLogger.error("--traceback: failed to allocate subprogram sort buffer, aborting compilation", "TRACEBACK")
return 1
fj: int = 1
while fj < fkept:
string.memcpy(ftmp, d.FuncBuf + sizet(fj) * TB_FUNC_SIZE, TB_FUNC_SIZE)
fk: int = fj
while fk > 0:
if _tb_rd_u64(d.FuncBuf, sizet(fk - 1) * TB_FUNC_SIZE) <= _tb_rd_u64(ftmp, 0): break
string.memcpy(d.FuncBuf + sizet(fk) * TB_FUNC_SIZE, d.FuncBuf + sizet(fk - 1) * TB_FUNC_SIZE, TB_FUNC_SIZE)
fk -= 1
string.memcpy(d.FuncBuf + sizet(fk) * TB_FUNC_SIZE, ftmp, TB_FUNC_SIZE)
fj += 1
if dropped_funcs > 0: VLogger.warning(f"--traceback: dropped {dropped_funcs} DWARF subprograms that cannot be normalised (low_pc not in image VA range)", "TRACEBACK")
# ---- 4) string pool: exact upper bound, then intern (offset 0 = sentinel) ----
pool_need: sizet = 1
i = 0
while i < uniq:
pth: str = rawptr(_tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE))
pool_need += string.strlen(pth) + 1
i += 1
# our rows carry the function name directly (string pool); DWARF rows take it from the subprogram table.
i = 0
while i < uniq:
fp: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC)
if fp != 0:
fns: str = rawptr(fp)
if fns is not None: pool_need += string.strlen(fns) + 1
i += 1
i = 0
while i < fkept:
nm0: str = rawptr(_tb_rd_u64(d.FuncBuf, sizet(i) * TB_FUNC_SIZE + 16))
if nm0 is not None: pool_need += string.strlen(nm0) + 1
i += 1
p: TbPool* = TbPool()
if p is None:
VLogger.error("--traceback: failed to allocate string pool object, aborting compilation", "TRACEBACK")
return 1
p.Pool = mb.alloc(pool_need)
p.Offs = mb.alloc((sizet(uniq) * 2 + sizet(fkept)) * 8 + 64)
if p.Pool is None or p.Offs is None:
VLogger.error("--traceback: failed to allocate string pool buffer, aborting compilation", "TRACEBACK")
return 1
p.Pool[0] = '\0'
p.PoolLen = 1
_tb_wr_u64(p.Offs, 0, uint64(0))
p.OffCount = 1
# file_off / func_off per unique row (u32 each)
ro: bytes = mb.alloc(sizet(uniq) * 8 + 8)
if ro is None:
VLogger.error("--traceback: failed to allocate record offset buffer, aborting compilation", "TRACEBACK")
return 1
for i in range(uniq):
pth2: str = rawptr(_tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE))
foff: uint64 = _tb_intern(p, pth2)
noff: uint64 = 0
rv2: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE)
fp2: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC)
nm2: str = None
if fp2 != 0: nm2 = rawptr(fp2)
else: nm2 = _tb_func_name_for(d.FuncBuf, fkept, rv2)
if nm2 is not None: noff = _tb_intern(p, nm2)
_tb_wr_u32(ro, sizet(i) * 8, foff)
_tb_wr_u32(ro, sizet(i) * 8 + 4, noff)
# ---- 5) self-checks (mirror the runtime LoadMapFile validation) ----
prev_rva: uint64 = 0
outside: int = 0
plausible: int = 0
ci: int = 0
while ci < uniq:
cv: uint64 = _tb_rd_u64(d.RowBuf, sizet(ci) * TB_ROW_SIZE)
if ci > 0 and cv <= prev_rva:
VLogger.error(f"--traceback self-check failed: record RVAs not strictly ascending (record {ci} would break runtime binary search), aborting compilation", "TRACEBACK")
return 1
prev_rva = cv
if cv >= size_of_image: outside += 1
if cv >= text_rva and cv < size_of_image: plausible += 1
ci += 1
if outside > 0:
VLogger.error(f"--traceback self-check failed: {outside} records have RVA outside [0, SizeOfImage=0x{size_of_image:x}), aborting compilation", "TRACEBACK")
return 1
if plausible <= 0:
VLogger.error("--traceback self-check failed: no record RVA falls in the range possible at runtime (RVA basis likely wrong), aborting compilation", "TRACEBACK")
return 1
# file_off must be >= 1: offset 0 is the empty-string sentinel and the runtime
# rejects a whole map whose record file_off is 0.
for ci in range(uniq):
fo: uint64 = uint64(_tb_rd_u32(ro, sizet(ci) * 8))
if fo == 0:
VLogger.error("--traceback self-check failed: a record has file_off==0 (empty path; runtime would reject that sidecar file), aborting compilation", "TRACEBACK")
return 1
# ---- 6) serialise and write <exe>.stackmap ----
total: sizet = 20 + sizet(uniq) * 20 + sizet(p.PoolLen)
out: bytes = mb.alloc(total)
if out is None:
VLogger.error("--traceback: failed to allocate sidecar output buffer, aborting compilation", "TRACEBACK")
return 1
out[0] = 'T'
out[1] = 'P'
out[2] = 'C'
out[3] = 'S'
_tb_wr_u32(out, 4, uint64(1))
_tb_wr_u32(out, 8, uint64(uniq))
_tb_wr_u32(out, 12, uint64(p.OffCount))
_tb_wr_u32(out, 16, uint64(p.PoolLen))
for ri2 in range(uniq):
roff2: sizet = 20 + sizet(ri2) * 20
rv3: uint64 = _tb_rd_u64(d.RowBuf, sizet(ri2) * TB_ROW_SIZE)
ln3: uint64 = uint64(_tb_rd_u32(d.RowBuf, sizet(ri2) * TB_ROW_SIZE + TB_ROW_OFF_LINE))
_tb_wr_u64(out, roff2, rv3)
_tb_wr_u32(out, roff2 + 8, uint64(_tb_rd_u32(ro, sizet(ri2) * 8)))
_tb_wr_u32(out, roff2 + 12, ln3)
_tb_wr_u32(out, roff2 + 16, uint64(_tb_rd_u32(ro, sizet(ri2) * 8 + 4)))
string.memcpy(out + 20 + sizet(uniq) * 20, p.Pool, sizet(p.PoolLen))
map_len: sizet = string.strlen(exe_path) + 16
map_path: str = mb.alloc(map_len)
if map_path is None:
VLogger.error("--traceback: failed to allocate sidecar path buffer, aborting compilation", "TRACEBACK")
return 1
viperlib.snprintf(map_path, map_len, "%s.stackmap", exe_path)
mf: fileio.File* = fileio.File(map_path, fileio.MODE.W)
if mf.closed:
VLogger.error(f"--traceback: failed to write sidecar file (cannot open {map_path}), aborting compilation", "TRACEBACK")
return 1
mf.write(out, total)
mf.close()
VLogger.success(f"--traceback: wrote address-to-source-line sidecar {map_path} ({uniq} records, {total} bytes)", "TRACEBACK")
return 0
def run_pipeline(ir_buf: bytes, ir_len: sizet, temp_dir: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str, linker_cmd: str, linker_flags: str, linker_output: str, includes_binary_dir: str, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> BuildResult*:
"""Run the full compile pipeline; returns a BuildResult object."""
result: BuildResult* = BuildResult()
if result is None: return None
# Step 0: ensure temp/output dirs exist (auto-create to avoid write failures)
ensure_dir(temp_dir)
ensure_dir(output_dir)
# Step 1: write the .ll file
ret: int = write_ir_to_file(ir_buf, ir_len, temp_dir, module_name)
if ret != 0:
result.Success = 0
result.ErrorMsg = "failed to write .ll file"
return result
# Step 2: llc-compile .ll -> .obj.
# build the sliced .ll file path
ir_path: str = build_sliced_path(temp_dir, module_name, "ll")
if ir_path is None:
result.Success = 0
result.ErrorMsg = "memory allocation failed"
return result
ret = compile_ll_to_obj(ir_path, output_dir, module_name, cc_cmd, cc_flags)
if ret != 0:
result.Success = 0
result.ErrorMsg = "llc compilation failed"
return result
# Step 3: clang++ link .obj -> .exe (plus includes.binary prebuilt .obj and static/dynamic libs)
ret = link_obj_to_exe(output_dir, module_name, linker_cmd, linker_flags, linker_output, includes_binary_dir, linker_static_libs, linker_dynamic_libs)
if ret != 0:
result.Success = 0
result.ErrorMsg = "link failed"
return result
result.Success = 1
return result
# =============================================================================
# Runtime dependency deployment (project.vpj runtime.*)
# After a successful link, copy the declared runtime deps next to the exe.
# Optional: no-op unless runtime.qt6_path / qt6_dlls / qt6_plugins are declared.
# Fail-fast: a declared source that is missing, or a failed copy/dir-create, aborts the build.
# =============================================================================
def _deploy_dir_of(path: str) -> str:
"""Return the directory portion of path (text before the last / or backslash); None if no separator."""
if path is None: return None
plen: sizet = string.strlen(path)
i: sizet = plen
while i > 0:
if path[i - 1] == '/' or path[i - 1] == '\\': break
i -= 1
if i == 0: return None
buf: str = _mbuddy.alloc(i)
if buf is None: return None
string.strncpy(buf, path, i - 1)
buf[i - 1] = '\0'
return buf
def _deploy_join(base: str, name: str) -> str:
"""Join base and name with '/'; mbuddy-allocated."""
if base is None or name is None: return None
bl: sizet = string.strlen(base)
nl: sizet = string.strlen(name)
buf: str = _mbuddy.alloc(bl + nl + 2)
if buf is None: return None
viperlib.snprintf(buf, bl + nl + 2, "%s/%s", base, name)
return buf
def _deploy_token(s: str, start: sizet, end: sizet) -> str:
"""Copy s[start:end] into an mbuddy-allocated NUL-terminated string; None on OOM."""
n: sizet = end - start
buf: str = _mbuddy.alloc(n + 1)
if buf is None: return None
string.strncpy(buf, s + start, n)
buf[n] = '\0'
return buf
def deploy_runtime_deps(exe_path: str) -> int:
"""Deploy declared runtime dependencies (project.vpj runtime.*) next to the exe; 0=ok, 1=fail.
No-op when runtime.qt6_path / qt6_dlls / qt6_plugins are undeclared. Fail-fast otherwise."""
if exe_path is None: return 0
qt6_path: str = Config.Qt6Path
if qt6_path is None: return 0
dlls: str = Config.Qt6Dlls
plugins: str = Config.Qt6Plugins
if dlls is None and plugins is None: return 0
exe_dir: str = _deploy_dir_of(exe_path)
if exe_dir is None:
VLogger.error(f"runtime deploy: cannot determine exe directory from {exe_path}", "runtime")
return 1
bin_dir: str = _deploy_join(qt6_path, "bin")
plug_root: str = _deploy_join(qt6_path, "plugins")
# runtime.qt6_dlls: <qt6_path>/bin/<dll> -> <exe_dir>/<dll>
if dlls is not None:
total: sizet = string.strlen(dlls)
i: sizet = 0
while i < total:
while i < total and dlls[i] == ' ': i += 1
if i >= total: break
start: sizet = i
while i < total and dlls[i] != ' ': i += 1
if i > start:
name: str = _deploy_token(dlls, start, i)
if name is None:
VLogger.error("runtime deploy: DLL name allocation failed (OOM)", "runtime")
return 1
src: str = _deploy_join(bin_dir, name)
dst: str = _deploy_join(exe_dir, name)
if not oslib.isfile(src):
VLogger.error(f"runtime deploy: declared DLL not found: {src}", "runtime")
return 1
if shutil.copyfile(src, dst) != 0:
VLogger.error(f"runtime deploy: failed to copy {src} -> {dst}", "runtime")
return 1
VLogger.info(f"runtime deploy: {name}", "runtime")
# runtime.qt6_plugins: <qt6_path>/plugins/<rel> -> <exe_dir>/<rel> (ensure parent dir)
if plugins is not None:
total_p: sizet = string.strlen(plugins)
ip: sizet = 0
while ip < total_p:
while ip < total_p and plugins[ip] == ' ': ip += 1
if ip >= total_p: break
start_p: sizet = ip
while ip < total_p and plugins[ip] != ' ': ip += 1
if ip > start_p:
rel: str = _deploy_token(plugins, start_p, ip)
if rel is None:
VLogger.error("runtime deploy: plugin path allocation failed (OOM)", "runtime")
return 1
src_p: str = _deploy_join(plug_root, rel)
dst_p: str = _deploy_join(exe_dir, rel)
parent: str = _deploy_dir_of(dst_p)
if parent is not None:
if not oslib.isdir(parent):
if oslib.mkdir(parent) != 0:
VLogger.error(f"runtime deploy: failed to create dir {parent}", "runtime")
return 1
if not oslib.isfile(src_p):
VLogger.error(f"runtime deploy: declared plugin not found: {src_p}", "runtime")
return 1
if shutil.copyfile(src_p, dst_p) != 0:
VLogger.error(f"runtime deploy: failed to copy {src_p} -> {dst_p}", "runtime")
return 1
VLogger.info(f"runtime deploy: {rel}", "runtime")
return 0