- add VpiStub: fixed-layout blob written next to every .vpi (dual write at all writers, Ensure() backfills a missing or stale companion on cache reuse) - serve arity/signature queries for top-level functions from the blob, with a silent fallback to the text scanner (methods, varargs, duplicates, corrupt file) - prefer the injected declare over the text arity in the cross-module check - CDefine/type text indexes and in-place SSA renumbering and bulk appends - WIP: char* conversion in CallGenBase and CallGenLeafKit
3048 lines
136 KiB
Plaintext
3048 lines
136 KiB
Plaintext
import lib.core.Gen.GenApi as GenApi
|
|
from stdint import *
|
|
import stdio
|
|
import string
|
|
import stdlib
|
|
import memhub
|
|
import viperlib
|
|
import vthreading
|
|
import io
|
|
import win32.win32file
|
|
import win32.win32base
|
|
import win32.win32process as win32process
|
|
import win32.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.Stub.StubGen.VpiStub as VpiStub
|
|
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 (VPC 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"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PhaseB backend
|
|
#
|
|
# ircore.dll (IRCore) compiles one MLIR module to an object file in-process: parse +
|
|
# verify + LLVM IR translation + codegen in a single FFI call. That replaces the three
|
|
# child processes (mlir-opt / mlir-translate / llc).
|
|
#
|
|
# The legacy command flow is kept intact for rollback (see compile_ll_to_obj,
|
|
# init_llc_task, _llc_task_worker and run_llc_parallel): set _phaseb_backend to
|
|
# PHASEB_BACKEND_LEGACY and PhaseB runs exactly the old commands again. Modules that
|
|
# need the external tools (ASan instrumentation, raw-IR merging via llvm-link/opt,
|
|
# --phase2command, --traceback frame pointers) automatically take the legacy path.
|
|
# ---------------------------------------------------------------------------
|
|
define PHASEB_BACKEND_IRCORE = 1
|
|
define PHASEB_BACKEND_LEGACY = 0
|
|
|
|
_phaseb_backend: int = PHASEB_BACKEND_IRCORE
|
|
|
|
# IRCore FFI (C ABI, ircore.dll). Loaded at runtime with LoadLibraryA instead of the import
|
|
# table: the compiler must still start (and PhaseB must still work) when the DLL or the LLVM
|
|
# build it needs is absent -- in that case PhaseB falls back to the legacy command flow.
|
|
#
|
|
# The signature of ircore_mlir_to_obj:
|
|
# int ircore_mlir_to_obj(mlir, mlir_len, out_obj, out_obj_len, out_err, out_err_len)
|
|
# Returned out_obj/out_err buffers are owned by the DLL; release with ircore_free.
|
|
_ircore_lib: win32.win32base.HANDLE = None
|
|
_ircore_ready: int = 0
|
|
# ircore_mlir_to_obj_opts(mlir, len, triple, cpu, features, opt, reloc, flags, ...):
|
|
# flags carry ASan instrumentation / frame pointers, so one entry point covers every mode.
|
|
_ircore_raw_fn: callable[[char*, sizet, char*, sizet, char*, char*, char*, int, int, int, char**, sizet*, char**, sizet*], int] = None
|
|
_ircore_free_fn: callable[[char*], int] = None
|
|
|
|
# IRCORE_FLAG_* (see IRCore/irffi.h)
|
|
define IRCORE_FLAG_ASAN = 1
|
|
define IRCORE_FLAG_FRAME_POINTER = 2
|
|
define IRCORE_FLAG_INLINE = 4
|
|
define IRCORE_OPT_DEFAULT_VALUE = 2
|
|
define IRCORE_RELOC_PIC_VALUE = 2
|
|
|
|
# ircore.dll imports the exact LLVM build it was compiled with (MSYS2 clang64 22.1.x).
|
|
# When another LLVM build (e.g. MSYS2 mingw64's) comes first on PATH the load fails with
|
|
# 0xC0000139; point the loader at this directory and retry once.
|
|
define IRCORE_LLVM_DLL_DIR = "D:\\msys64\\clang64\\bin"
|
|
|
|
extern def SetDllDirectoryA(lpPathName: LPCSTR) -> BOOL
|
|
|
|
|
|
def _ircore_load() -> int:
|
|
"""Load ircore.dll and resolve its entry points; 1 = available, 0 = not available."""
|
|
global _ircore_lib, _ircore_raw_fn, _ircore_free_fn
|
|
if _ircore_lib is not None: return 1
|
|
h: win32.win32base.HANDLE = win32.win32base.LoadLibraryA("ircore.dll")
|
|
if h is None:
|
|
# Retry with clang64's bin directory ahead of PATH (the LLVM build ircore.dll needs).
|
|
SetDllDirectoryA(IRCORE_LLVM_DLL_DIR)
|
|
h = win32.win32base.LoadLibraryA("ircore.dll")
|
|
if h is None:
|
|
return 0
|
|
p_raw: win32.win32base.VOIDPTR = win32.win32base.GetProcAddress(h, "ircore_mlir_to_obj_raw")
|
|
p_free: win32.win32base.VOIDPTR = win32.win32base.GetProcAddress(h, "ircore_free")
|
|
if p_raw is None or p_free is None:
|
|
win32.win32base.FreeLibrary(h)
|
|
return 0
|
|
_ircore_raw_fn = p_raw
|
|
_ircore_free_fn = p_free
|
|
_ircore_lib = h
|
|
return 1
|
|
|
|
|
|
def _phaseb_ircore_active() -> int:
|
|
"""1 when PhaseB should use the in-process ircore backend; switches to legacy on failure."""
|
|
global _phaseb_backend, _ircore_ready
|
|
if _phaseb_backend != PHASEB_BACKEND_IRCORE: return 0
|
|
if _ircore_ready != 0: return 1
|
|
if _ircore_load() == 0:
|
|
_phaseb_backend = PHASEB_BACKEND_LEGACY
|
|
VLogger.warning("ircore.dll is unavailable (or its LLVM runtime could not be loaded); PhaseB falls back to the legacy mlir-opt/mlir-translate/llc flow (3 child processes per module) -- put ircore.dll next to viperc.exe (or on PATH) to use the in-process backend", "PhaseB")
|
|
return 0
|
|
_ircore_ready = 1
|
|
return 1
|
|
|
|
# Raw-IR support (__llvmir__/__mlir__): raw LLVM function bodies are split into <sha1>.raw.ll
|
|
# and merged with the lowered module; always-inline then removes the call boundary so the raw
|
|
# body behaves like inline IR again.
|
|
LLVM_LINK_CMD: str = "llvm-link"
|
|
INLINE_PASSES: str = "always-inline,globaldce"
|
|
define RAW_ENVELOPE_PREFIX = "//__VPC_RAWIR__ "
|
|
|
|
|
|
def _parse_raw_envelope(ir_buf: bytes, ir_len: sizet, inline_out: int*, raw_len_out: sizet*) -> sizet:
|
|
"""Parse the FULL-artifact envelope written by mlirlite.
|
|
|
|
Format: "<prefix><inline 0|1> <raw byte length>\n<raw LLVM text><MLIR module>".
|
|
Returns the raw section's start offset (0 = no envelope) and fills the flag/length outputs.
|
|
"""
|
|
if inline_out is not None: inline_out[0] = 0
|
|
if raw_len_out is not None: raw_len_out[0] = 0
|
|
if ir_buf is None: return 0
|
|
plen: sizet = string.strlen(RAW_ENVELOPE_PREFIX)
|
|
if ir_len <= plen + 3: return 0
|
|
if string.strncmp(ir_buf, RAW_ENVELOPE_PREFIX, plen) != 0: return 0
|
|
i: sizet = plen
|
|
while i < ir_len and ir_buf[i] == ' ': i += 1
|
|
if i >= ir_len or (ir_buf[i] != '0' and ir_buf[i] != '1'): return 0
|
|
inl: int = 0
|
|
if ir_buf[i] == '1': inl = 1
|
|
i += 1
|
|
while i < ir_len and ir_buf[i] == ' ': i += 1
|
|
rl: sizet = 0
|
|
digits: int = 0
|
|
while i < ir_len and ir_buf[i] >= '0' and ir_buf[i] <= '9':
|
|
rl = rl * 10 + sizet(int(ir_buf[i]) - 48)
|
|
i += 1
|
|
digits += 1
|
|
if digits == 0: return 0
|
|
while i < ir_len and ir_buf[i] == '\r': i += 1
|
|
if i >= ir_len or ir_buf[i] != '\n': return 0
|
|
if inline_out is not None: inline_out[0] = inl
|
|
if raw_len_out is not None: raw_len_out[0] = rl
|
|
return i + 1
|
|
|
|
|
|
|
|
def _file_exists(path: str) -> int:
|
|
"""1 when path exists (opens it for reading and closes), else 0."""
|
|
if path is None: return 0
|
|
f: io.File* = io.File(path, io.MODE.R)
|
|
if f.closed: return 0
|
|
f.close()
|
|
return 1
|
|
|
|
|
|
# ResolveSpendPath - --spend-file: absolute paths stay as written; relative ones (including
|
|
# ../x.txt) join the output exe directory so the built program writes the documented file
|
|
# no matter which directory it is started from.
|
|
def ResolveSpendPath(output_dir: str, path: str) -> str:
|
|
if path is None or output_dir is None: return path
|
|
if path[0] == '/' or path[0] == '\\': return path
|
|
if len(path) >= 2 and path[1] == ':': return path
|
|
cap: sizet = len(output_dir) + len(path) + 2
|
|
buf: str = stdlib.malloc(cap)
|
|
if buf is None: return path
|
|
viperlib.snprintf(buf, cap, "%s/%s", output_dir, path)
|
|
return buf
|
|
|
|
|
|
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: io.File* = io.File(vpath, io.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: io.File* = io.File(file_path, io.MODE.R)
|
|
# Fail-fast: a path that cannot be opened (e.g. an App source resolved against the wrong root)
|
|
# used to return None silently, and the caller only reported "cannot be translated".
|
|
if f.closed: VLogger.error(f"cannot open source file: {file_path}", "Translate")
|
|
# 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()
|
|
VLogger.error(f"source buffer allocation failed (OOM): {file_path}", "Translate")
|
|
bytes_read: LONG = f.read_all(src_buf, SRC_BUF_SIZE)
|
|
f.close()
|
|
if bytes_read <= 0:
|
|
stdlib.free(src_buf)
|
|
VLogger.error(f"source file read failed or empty: {file_path}", "Translate")
|
|
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 TypedefT 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: io.File* = io.File(pyi_path, io.MODE.W)
|
|
if not pf.closed:
|
|
pf.write(pyi_buf, pyi_pos)
|
|
pf.close()
|
|
VpiStub.Emit(pyi_path, pyi_buf, pyi_pos, sha1_val)
|
|
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 DefineT constant table to isolate each module's constants
|
|
GenApi.clear_cdefine_constants()
|
|
# module switch: clear the TypedefT 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) -> rawptr:
|
|
return rawptr(_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 = win32.win32file.GetFileAttributesA(path)
|
|
if attr != 0xFFFFFFFF and (attr & win32.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 = int(path_len)
|
|
for i in range(int(plen)):
|
|
ch: int = buf[i]
|
|
if ch == 47 or ch == 92: # '/' = 47, '\\' = 92
|
|
saved: char = char(ch)
|
|
buf[i] = '\0'
|
|
win32.win32file.CreateDirectoryA(buf, None)
|
|
buf[i] = saved
|
|
|
|
# create the final directory
|
|
win32.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 <sha1>.ll; the envelope's raw LLVM text becomes <sha1>.raw.ll."""
|
|
if ir_buf is None or output_dir is None or module_name is None: return 1
|
|
|
|
# FULL artifacts carry a length-prefixed envelope (see _parse_raw_envelope); STUB/TEXT
|
|
# artifacts have none and are written verbatim.
|
|
inline_flag: int = 0
|
|
raw_len: sizet = 0
|
|
raw_start: sizet = _parse_raw_envelope(ir_buf, ir_len, &inline_flag, &raw_len)
|
|
if raw_start > ir_len: raw_start = ir_len
|
|
if raw_len > ir_len - raw_start: raw_len = ir_len - raw_start
|
|
mlir_start: sizet = raw_start
|
|
if raw_len > 0: mlir_start = raw_start + raw_len
|
|
if mlir_start > ir_len: mlir_start = ir_len
|
|
mlir_len: sizet = ir_len - mlir_start
|
|
|
|
# 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: io.File* = io.File(path, io.MODE.W)
|
|
if f.closed: return 1
|
|
|
|
written: int64 = f.write(ir_buf + mlir_start, mlir_len)
|
|
f.close()
|
|
if written < 0: return 1
|
|
|
|
if raw_len > 0:
|
|
raw_path: str = build_sliced_path(output_dir, module_name, "raw.ll")
|
|
if raw_path is None: return 1
|
|
rf: io.File* = io.File(raw_path, io.MODE.W)
|
|
if rf.closed: return 1
|
|
rw: int64 = rf.write(ir_buf + raw_start, raw_len)
|
|
rf.close()
|
|
if rw < 0: return 1
|
|
|
|
# Inline-pass marker: raw LLVM functions and/or a raw-MLIR body (__mlir__) are present, so
|
|
# the object must go through opt -passes=always-inline,globaldce before llc.
|
|
if inline_flag != 0 or raw_len > 0:
|
|
flag_path: str = build_sliced_path(output_dir, module_name, "inline")
|
|
if flag_path is None: return 1
|
|
ff: io.File* = io.File(flag_path, io.MODE.W)
|
|
if ff.closed: return 1
|
|
ff.close()
|
|
return 0
|
|
|
|
|
|
# _ircore_compile_module - PhaseB through ircore.dll: one in-process MLIR -> object call
|
|
# replaces mlir-opt + mlir-translate + llc. Returns 0 on success, non-zero on failure.
|
|
# The worker context: only stdlib.malloc buffers, no MemBuddy allocation.
|
|
# _ircore_sibling - "<ir_path without .ll><suffix>", stdlib-allocated: worker threads must
|
|
# not touch MemBuddy (_ir_sibling_path allocates from it).
|
|
def _ircore_sibling(ir_path: str, suffix: str) -> str:
|
|
if ir_path is None or suffix is None: return None
|
|
n: sizet = len(ir_path)
|
|
if n < 3: return None
|
|
sl: sizet = len(suffix)
|
|
out: str = stdlib.malloc(n + sl + 1)
|
|
if out is None: return None
|
|
string.strncpy(out, ir_path, n - 3)
|
|
out[n - 3] = '\0'
|
|
string.strcat(out, suffix)
|
|
return out
|
|
|
|
|
|
# _ircore_read_whole - read a whole file into a stdlib buffer; 1 = ok (caller frees buf[0]).
|
|
def _ircore_read_whole(path: str, buf: bytes*, n: sizet*) -> int:
|
|
if buf is not None: buf[0] = None
|
|
if n is not None: n[0] = 0
|
|
f: io.File* = io.File(path, io.MODE.R)
|
|
if f.closed: return 0
|
|
sz: LONGLONG = f.size()
|
|
if sz <= 0:
|
|
f.close()
|
|
return 0
|
|
# read_all() keeps the last byte for the NUL, so ask for sz+1 and expect sz bytes.
|
|
b: bytes = stdlib.malloc(sizet(sz) + 1)
|
|
if b is None:
|
|
f.close()
|
|
return 0
|
|
got: LONG = f.read_all(b, sizet(sz) + 1)
|
|
f.close()
|
|
if got != LONG(sz):
|
|
stdlib.free(b)
|
|
return 0
|
|
buf[0] = b
|
|
n[0] = sizet(sz)
|
|
return 1
|
|
|
|
|
|
def _ircore_compile_module(ir_path: str, obj_path: str, module_name: str) -> int:
|
|
if ir_path is None or obj_path is None: return -1
|
|
|
|
src: bytes = None
|
|
src_len: sizet = 0
|
|
if _ircore_read_whole(ir_path, &src, &src_len) == 0:
|
|
VLogger.warning(f"ircore: cannot read module {ir_path}", "PhaseB")
|
|
return -1
|
|
|
|
# Raw IR sidecars (__llvmir__): the DLL links them and runs always-inline,globaldce, so the
|
|
# legacy llvm-link + opt steps disappear. The .inline marker asks for that pass run alone.
|
|
raw_buf: bytes = None
|
|
raw_len: sizet = 0
|
|
flags: int = 0
|
|
if _asan_mode != 0: flags = flags | IRCORE_FLAG_ASAN
|
|
if _traceback_mode != 0: flags = flags | IRCORE_FLAG_FRAME_POINTER
|
|
raw_path: str = _ircore_sibling(ir_path, ".raw.ll")
|
|
if raw_path is not None:
|
|
if _file_exists(raw_path) != 0: _ircore_read_whole(raw_path, &raw_buf, &raw_len)
|
|
stdlib.free(raw_path)
|
|
inline_path: str = _ircore_sibling(ir_path, ".inline")
|
|
if inline_path is not None:
|
|
if _file_exists(inline_path) != 0: flags = flags | IRCORE_FLAG_INLINE
|
|
stdlib.free(inline_path)
|
|
|
|
out_obj: char* = None
|
|
out_len: sizet = 0
|
|
err: char* = None
|
|
err_len: sizet = 0
|
|
rc: int = _ircore_raw_fn(src, src_len, raw_buf, raw_len, None, None, None,
|
|
IRCORE_OPT_DEFAULT_VALUE, IRCORE_RELOC_PIC_VALUE, flags,
|
|
&out_obj, &out_len, &err, &err_len)
|
|
stdlib.free(src)
|
|
if raw_buf is not None: stdlib.free(raw_buf)
|
|
|
|
if rc != 0:
|
|
# The DLL's diagnostic is the authoritative message (MLIR verifier or LLVM codegen).
|
|
if err is not None:
|
|
VLogger.warning(f"ircore MLIR->obj failed (module={module_name}): {err}", "PhaseB")
|
|
_ircore_free_fn(err)
|
|
else:
|
|
VLogger.warning(f"ircore MLIR->obj failed (module={module_name}): no diagnostic", "PhaseB")
|
|
if out_obj is not None: _ircore_free_fn(out_obj)
|
|
return 1
|
|
|
|
wf: io.File* = io.File(obj_path, io.MODE.W)
|
|
if wf.closed:
|
|
VLogger.warning(f"ircore: cannot create object {obj_path}", "PhaseB")
|
|
_ircore_free_fn(out_obj)
|
|
return -1
|
|
wr: LONG = wf.write(out_obj, out_len)
|
|
wf.close()
|
|
_ircore_free_fn(out_obj)
|
|
if wr != LONG(out_len):
|
|
VLogger.warning(f"ircore: short write on {obj_path} ({wr}/{out_len})", "PhaseB")
|
|
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
|
|
|
|
# IRCore backend: one in-process call for plain modules. ASan instrumentation, raw-IR
|
|
# merging (llvm-link/opt), --traceback frame pointers and --phase2command still need the
|
|
# external tools, so they fall through to the legacy command flow below (kept intact).
|
|
if _phaseb_ircore_active() != 0 and _phase2_cmd_path is None:
|
|
return _ircore_compile_module(ir_path, obj_path, module_name)
|
|
|
|
# ASan mode: run opt -passes=asan before llc. -asan-globals=false disables global
|
|
# instrumentation: VPC 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 the final LLVM IR file.
|
|
ll_local_path: str = _ir_sibling_path(ir_path, ".ll.local")
|
|
mlir_opt_out: str = _ir_sibling_path(ir_path, ".mlir.opt")
|
|
raw_path: str = _ir_sibling_path(ir_path, ".raw.ll")
|
|
inline_flag: str = _ir_sibling_path(ir_path, ".inline")
|
|
ll_pre_path: str = _ir_sibling_path(ir_path, ".ll.pre")
|
|
ll_link_path: str = _ir_sibling_path(ir_path, ".ll.link")
|
|
ll_inline_path: str = _ir_sibling_path(ir_path, ".ll.inline")
|
|
if ll_local_path is None or mlir_opt_out is None or raw_path is None or inline_flag is None \
|
|
or ll_pre_path is None or ll_link_path is None or ll_inline_path is None: return 1
|
|
has_raw: int = _file_exists(raw_path)
|
|
translate_out: str = ll_local_path
|
|
if has_raw != 0: translate_out = ll_pre_path
|
|
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(translate_out) + 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, translate_out)
|
|
|
|
# Raw LLVM functions (__llvmir__): merge the sidecar into the lowered module.
|
|
cmd_link: str = None
|
|
if has_raw != 0:
|
|
lk_len: sizet = len(LLVM_LINK_CMD) + len(ll_pre_path) + len(raw_path) + len(ll_link_path) + 32
|
|
cmd_link = _mbuddy.alloc(lk_len)
|
|
if cmd_link is None: return 1
|
|
viperlib.snprintf(cmd_link, lk_len, "%s %s %s -S -o %s", LLVM_LINK_CMD, ll_pre_path, raw_path, ll_link_path)
|
|
|
|
# Always-inline pass: restores inline semantics for raw functions/blocks (raw LLVM bodies and
|
|
# raw MLIR bodies both become ordinary calls first).
|
|
need_inline: int = 0
|
|
if has_raw != 0: need_inline = 1
|
|
elif _file_exists(inline_flag) != 0: need_inline = 1
|
|
cmd_inline: str = None
|
|
inline_src: str = ll_local_path
|
|
if has_raw != 0: inline_src = ll_link_path
|
|
if need_inline != 0:
|
|
in_len: sizet = len("opt -passes=") + len(INLINE_PASSES) + len(inline_src) + len(ll_inline_path) + 32
|
|
cmd_inline = _mbuddy.alloc(in_len)
|
|
if cmd_inline is None: return 1
|
|
viperlib.snprintf(cmd_inline, in_len, "opt -passes=%s -S %s -o %s", INLINE_PASSES, inline_src, ll_inline_path)
|
|
|
|
if _phase2_cmd_path is not None:
|
|
# write the commands only (must precede the ASan/llc commands below)
|
|
_phase2_write_cmd(cmd_mo)
|
|
_phase2_write_cmd(cmd_mt)
|
|
if cmd_link is not None: _phase2_write_cmd(cmd_link)
|
|
if cmd_inline is not None: _phase2_write_cmd(cmd_inline)
|
|
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
|
|
if cmd_link is not None:
|
|
lk_rc: int = _run_cmd_wait(cmd_link)
|
|
if lk_rc != 0:
|
|
VLogger.error(f"llvm-link failed for raw LLVM IR (module={module_name}, exit={lk_rc}, cmd={cmd_link})", "RAWIR")
|
|
return 1
|
|
if cmd_inline is not None:
|
|
inl_rc: int = _run_cmd_wait(cmd_inline)
|
|
if inl_rc != 0:
|
|
VLogger.error(f"opt always-inline failed for raw IR (module={module_name}, exit={inl_rc}, cmd={cmd_inline})", "RAWIR")
|
|
return 1
|
|
if need_inline != 0: compile_input = ll_inline_path
|
|
else: 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
|
|
define LLC_STAGE_RAW_LINK = 5
|
|
define LLC_STAGE_RAW_INLINE = 6
|
|
define LLC_STAGE_IRCORE = 7
|
|
|
|
|
|
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
|
|
# Raw-IR support (__llvmir__/__mlir__); appended at the end, None when the module has none.
|
|
RawLinkCmd: bytes # llvm-link merge of <sha1>.raw.ll into the lowered module
|
|
RawInlineCmd: bytes # opt -passes=always-inline,globaldce (raw body -> inline)
|
|
# IRCore backend (PHASEB_BACKEND_IRCORE): when UseIrcore=1 the worker ignores every
|
|
# command field above and compiles IrPath -> ObjPath in-process. The commands stay
|
|
# allocated (error reporting / dead legacy path) and are never executed.
|
|
UseIrcore: int
|
|
IrPath: bytes
|
|
ObjPath: bytes
|
|
|
|
|
|
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 the final LLVM IR file.
|
|
ll_local_path: str = _ir_sibling_path(ir_path, ".ll.local")
|
|
mlir_opt_out: str = _ir_sibling_path(ir_path, ".mlir.opt")
|
|
raw_path: str = _ir_sibling_path(ir_path, ".raw.ll")
|
|
inline_flag: str = _ir_sibling_path(ir_path, ".inline")
|
|
ll_pre_path: str = _ir_sibling_path(ir_path, ".ll.pre")
|
|
ll_link_path: str = _ir_sibling_path(ir_path, ".ll.link")
|
|
ll_inline_path: str = _ir_sibling_path(ir_path, ".ll.inline")
|
|
if ll_local_path is None or mlir_opt_out is None or raw_path is None or inline_flag is None \
|
|
or ll_pre_path is None or ll_link_path is None or ll_inline_path is None: return 1
|
|
has_raw: int = _file_exists(raw_path)
|
|
translate_out: str = ll_local_path
|
|
if has_raw != 0: translate_out = ll_pre_path
|
|
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(translate_out) + 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, translate_out)
|
|
compile_input = translate_out
|
|
# Raw LLVM functions (__llvmir__): merge the sidecar into the lowered module.
|
|
if has_raw != 0:
|
|
lk_len: sizet = len(LLVM_LINK_CMD) + len(ll_pre_path) + len(raw_path) + len(ll_link_path) + 32
|
|
task.RawLinkCmd = stdlib.malloc(lk_len)
|
|
if task.RawLinkCmd is None: return 1
|
|
viperlib.snprintf(task.RawLinkCmd, lk_len, "%s %s %s -S -o %s", LLVM_LINK_CMD, ll_pre_path, raw_path, ll_link_path)
|
|
compile_input = ll_link_path
|
|
# Always-inline pass: restores inline semantics for raw functions/blocks.
|
|
need_inline: int = 0
|
|
if has_raw != 0: need_inline = 1
|
|
elif _file_exists(inline_flag) != 0: need_inline = 1
|
|
if need_inline != 0:
|
|
in_len: sizet = len("opt -passes=") + len(INLINE_PASSES) + len(compile_input) + len(ll_inline_path) + 32
|
|
task.RawInlineCmd = stdlib.malloc(in_len)
|
|
if task.RawInlineCmd is None: return 1
|
|
viperlib.snprintf(task.RawInlineCmd, in_len, "opt -passes=%s -S %s -o %s", INLINE_PASSES, compile_input, ll_inline_path)
|
|
compile_input = ll_inline_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)
|
|
|
|
# IRCore backend: plain modules compile in-process (the commands above stay allocated for
|
|
# the legacy path / reporting but are not run). ASan instrumentation, raw-IR merging and
|
|
# --phase2command keep the external tools.
|
|
if _phaseb_ircore_active() != 0 and _phase2_cmd_path is None:
|
|
task.UseIrcore = 1
|
|
task.IrPath = stdlib.malloc(len(ir_path) + 1)
|
|
if task.IrPath is None: return 1
|
|
string.strcpy(task.IrPath, ir_path)
|
|
task.ObjPath = stdlib.malloc(len(obj_path) + 1)
|
|
if task.ObjPath is None: return 1
|
|
string.strcpy(task.ObjPath, obj_path)
|
|
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.RawLinkCmd is not None:
|
|
stdlib.free(task.RawLinkCmd)
|
|
task.RawLinkCmd = None
|
|
if task.RawInlineCmd is not None:
|
|
stdlib.free(task.RawInlineCmd)
|
|
task.RawInlineCmd = None
|
|
if task.ModuleSha1 is not None:
|
|
stdlib.free(task.ModuleSha1)
|
|
task.ModuleSha1 = None
|
|
if task.IrPath is not None:
|
|
stdlib.free(task.IrPath)
|
|
task.IrPath = None
|
|
if task.ObjPath is not None:
|
|
stdlib.free(task.ObjPath)
|
|
task.ObjPath = 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: io.File* = io.File(_phase2_cmd_path, io.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 ViperC --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: win32.win32base.SECURITY_ATTRIBUTES = win32.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, win32.win32base.SECURITY_ATTRIBUTES.__sizeof__())
|
|
string.memset(&pi, 0, win32process.PROCESS_INFORMATION.__sizeof__())
|
|
sa.nLength = win32.win32base.SECURITY_ATTRIBUTES.__sizeof__()
|
|
sa.lpSecurityDescriptor = None
|
|
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(None, cmd_line, None, None, 1, 0, None, None, &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 = win32.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, win32.win32base.INFINITE)
|
|
exit_code: uint32 = 0
|
|
win32process.GetExitCodeProcess(pi.hProcess, &exit_code)
|
|
win32.win32base.CloseHandle(pi.hProcess)
|
|
win32.win32base.CloseHandle(pi.hThread)
|
|
return int(exit_code)
|
|
|
|
|
|
def _llc_task_worker(arg: rawptr) -> rawptr:
|
|
"""llc thread entry: only calls _run_cmd_wait, no pool allocation (thread-safe)."""
|
|
task: LlcTask* = (LlcTask*)(arg)
|
|
if task is None: return None
|
|
# IRCore backend: one in-process call replaces the whole command sequence below.
|
|
if task.UseIrcore != 0:
|
|
ir_rc: int = _ircore_compile_module(task.IrPath, task.ObjPath, task.ModuleSha1)
|
|
task.ExitCode = ir_rc
|
|
if ir_rc != 0:
|
|
task.FailStage = LLC_STAGE_IRCORE
|
|
task.Result = 1
|
|
else:
|
|
task.FailStage = LLC_STAGE_NONE
|
|
task.Result = 0
|
|
return None
|
|
# Legacy backend (rollback path): 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 None
|
|
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 None
|
|
if task.RawLinkCmd is not None:
|
|
rl_rc: int = _run_cmd_wait(task.RawLinkCmd)
|
|
if rl_rc != 0:
|
|
task.ExitCode = rl_rc
|
|
task.FailStage = LLC_STAGE_RAW_LINK
|
|
task.Result = 1
|
|
return None
|
|
if task.RawInlineCmd is not None:
|
|
ri_rc: int = _run_cmd_wait(task.RawInlineCmd)
|
|
if ri_rc != 0:
|
|
task.ExitCode = ri_rc
|
|
task.FailStage = LLC_STAGE_RAW_INLINE
|
|
task.Result = 1
|
|
return None
|
|
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 None
|
|
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 None
|
|
|
|
|
|
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(int(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.RawLinkCmd is not None: _phase2_write_cmd(tsk_p.RawLinkCmd)
|
|
if tsk_p.RawInlineCmd is not None: _phase2_write_cmd(tsk_p.RawInlineCmd)
|
|
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[rawptr] 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, int(task_count), int(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(int(batch_n)):
|
|
tsk: LlcTask* = (LlcTask*)(uint64(tasks) + sizet(batch_start + j) * slot_size)
|
|
th: vthreading.Thread* = vthreading.Thread(_llc_task_worker, rawptr(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(rawptr(th.handle))
|
|
for j in range(int(batch_n)):
|
|
h_j: rawptr = rawptr(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")
|
|
win32.win32base.CloseHandle(h_j)
|
|
# check this batch's results
|
|
for j in range(int(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_RAW_LINK:
|
|
stage_name = "llvm-link raw IR merge"
|
|
stage_cmd = tsk2.RawLinkCmd
|
|
elif st == LLC_STAGE_RAW_INLINE:
|
|
stage_name = "opt always-inline (raw IR)"
|
|
stage_cmd = tsk2.RawInlineCmd
|
|
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
|
|
elif st == LLC_STAGE_IRCORE:
|
|
stage_name = "ircore MLIR->obj (in-process)"
|
|
stage_cmd = tsk2.IrPath
|
|
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: win32.win32file.WIN32_FIND_DATAA* = _mbuddy.alloc(win32.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, win32.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: win32.win32base.HANDLE = win32.win32file.FindFirstFileA(pattern, find_data)
|
|
_mbuddy.free(pattern)
|
|
if handle == win32.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 & win32.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 win32.win32file.FindNextFileA(handle, find_data) == 0: break
|
|
|
|
win32.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(int(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 VPC. 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) -> rawptr:
|
|
return rawptr(_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) -> rawptr:
|
|
return rawptr(_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) -> rawptr:
|
|
return rawptr(_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:
|
|
# Assemble via memcpy instead of u32 widening + shifts: the manual form left 0xFFFFFFFF in
|
|
# the high half (a stale register/slot), which then looked like a wild pointer in the
|
|
# stackmap tables. memcpy is exact and little-endian on x86-64.
|
|
v: uint64 = 0
|
|
string.memcpy(&v, buf + off, 8)
|
|
return v
|
|
|
|
|
|
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):
|
|
string.memcpy(buf + off, &v, 8)
|
|
|
|
|
|
def _tb_wr_ptr(buf: bytes, off: sizet, p: str):
|
|
"""Store a string pointer into a byte table without a ptr->int cast.
|
|
|
|
Do NOT write `memcpy(buf + off, &p, 8)`: passing `&p` (i8**) to a `rawptr` parameter is
|
|
currently compiled as a dereference (it copies the string's first 8 bytes instead of the
|
|
pointer value), so go through a typed slot instead.
|
|
"""
|
|
slot: str* = str*(buf + off)
|
|
slot[0] = p
|
|
|
|
|
|
# ---- 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_str_ok(s: str) -> int:
|
|
"""Return 1 when s is a plausible user-space string pointer (not 0, -1 or garbage).
|
|
|
|
DWARF/objdump text is parsed into pointer-bearing tables; a single bad slot would later be
|
|
dereferenced by _tb_cc/strlen and crash the compiler with ASan "unknown address 0xffff...".
|
|
Callers use this to reject such a slot at the boundary instead of storing it.
|
|
"""
|
|
if s is None: return 0
|
|
v: uint64 = uint64(s)
|
|
return 1 if v >= 65536 and v <= 0x7FFFFFFFFFFF else 0
|
|
|
|
|
|
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_contains_span(s: str, ls: sizet, le: sizet, sub: str) -> int:
|
|
"""Span-aware substring test: only s[ls, le) is searched.
|
|
A tail-wide strstr would both scan the whole remaining objdump text (hundreds of MB) and
|
|
match a substring belonging to a LATER line (e.g. ".text" found past a ".rdata" header)."""
|
|
if s is None or sub is None: return 0
|
|
sl: sizet = string.strlen(sub)
|
|
if sl == 0 or le < ls or le - ls < sl: return 0
|
|
i: sizet = ls
|
|
while i + sl <= le:
|
|
if string.strncmp(s + i, sub, sl) == 0: return 1
|
|
i += 1
|
|
return 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_startswith_span(s: str, ls: sizet, le: sizet, pfx: str) -> int:
|
|
"""Span-aware prefix test: only s[ls, le) is considered, never the tail.
|
|
_tb_startswith on `s + ls` would run strlen over the WHOLE remaining text; llvm-objdump -d
|
|
output is hundreds of MB for a large exe, so a per-line strlen makes the whole scan O(n^2)
|
|
and the compilation appears to hang (it spins inside strlen for hours)."""
|
|
if s is None or pfx is None: return 0
|
|
if le < ls: return 0
|
|
pl: sizet = string.strlen(pfx)
|
|
if le - ls < pl: return 0
|
|
return 1 if string.strncmp(s + ls, 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 = 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 = 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(int(count)):
|
|
if int(_tb_rd_u32(dirs, sizet(i) * TB_DIR_SIZE)) == idx:
|
|
p: uint64 = _tb_rd_u64(dirs, sizet(i) * TB_DIR_SIZE + 8)
|
|
# fail-fast: a wild name pointer would crash inside _tb_is_abs_path/strlen later.
|
|
if p < 65536 or p > 0x7FFFFFFFFFFF:
|
|
VLogger.error(f"--traceback: internal error: DWARF directory record {i} has an invalid name pointer ({p})", "TRACEBACK")
|
|
return None
|
|
return rawptr(p)
|
|
return None
|
|
|
|
|
|
def _tb_file_rec(files: bytes, count: int, idx: int) -> int:
|
|
for i in range(int(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:
|
|
# fail-fast: a wild cached path would crash inside strlen/is_abs_path below; report the
|
|
# record that carries it together with the table state instead.
|
|
if cached < 65536 or cached > 0x7FFFFFFFFFFF:
|
|
VLogger.error(f"--traceback: internal error: DWARF file record {frec} has an invalid cached path pointer ({cached})", "TRACEBACK")
|
|
return None
|
|
return rawptr(cached)
|
|
nptr: uint64 = _tb_rd_u64(files, roff + 8)
|
|
if nptr < 65536 or nptr > 0x7FFFFFFFFFFF:
|
|
ridx: uint64 = uint64(_tb_rd_u32(files, roff))
|
|
rdir: uint64 = uint64(_tb_rd_u32(files, roff + 4))
|
|
VLogger.error(f"--traceback: internal error: DWARF file record {frec} has an invalid name pointer ({nptr}); record idx={ridx} dir_index={rdir}, records in table={fcount}", "TRACEBACK")
|
|
return None
|
|
name: str = rawptr(nptr)
|
|
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_ptr(files, roff + 24, 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
|
|
# Every row must carry a file path. A caller that could not resolve one passes None (or a
|
|
# wild sentinel); storing that as-is makes the later strlen() sizing pass fault. Normalise to
|
|
# the stackmap's existing "??" convention (the DWARF path already does the same).
|
|
if _tb_str_ok(path) == 0: path = "??"
|
|
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_ptr(d.RowBuf, off + TB_ROW_OFF_FILE, path)
|
|
_tb_wr_u32(d.RowBuf, off + TB_ROW_OFF_LINE, uint64(line))
|
|
if _tb_str_ok(fname) != 0: _tb_wr_ptr(d.RowBuf, off + TB_ROW_OFF_FUNC, fname)
|
|
else: _tb_wr_u64(d.RowBuf, off + TB_ROW_OFF_FUNC, 0)
|
|
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_ptr(d.FuncBuf, off + 16, 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(int(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: io.File* = io.File(exe_path, io.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(int(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 _tb_str_ok(pname) != 0 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_ptr(files, sizet(file_count) * TB_FILE_SIZE + 8, 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 _tb_str_ok(pname) != 0 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_ptr(files, sizet(file_count) * TB_FILE_SIZE + 8, 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 _tb_str_ok(pname) != 0 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_ptr(files, sizet(file_count) * TB_FILE_SIZE + 8, 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:
|
|
dname: str = _tb_unquote(mb, val)
|
|
if _tb_str_ok(dname) != 0:
|
|
_tb_wr_u32(dirs, sizet(dir_count) * TB_DIR_SIZE, uint64(pidx))
|
|
_tb_wr_ptr(dirs, sizet(dir_count) * TB_DIR_SIZE + 8, dname)
|
|
dir_count += 1
|
|
else:
|
|
VLogger.warning("--traceback: ignoring a DWARF directory entry whose name could not be copied", "TRACEBACK")
|
|
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 _tb_str_ok(val2) != 0:
|
|
_tb_wr_u32(dirs, sizet(dir_count) * TB_DIR_SIZE, uint64(pidx))
|
|
_tb_wr_ptr(dirs, sizet(dir_count) * TB_DIR_SIZE + 8, 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 _tb_str_ok(pname) != 0 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_ptr(files, sizet(file_count) * TB_FILE_SIZE + 8, 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 _tb_str_ok(pname) != 0 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_ptr(files, sizet(file_count) * TB_FILE_SIZE + 8, 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 _tb_str_ok(cname) != 0 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) -> rawptr:
|
|
return rawptr(_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(int(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_ptr(grp, off + TB_GRP_OFF_CALLER, caller)
|
|
_tb_wr_ptr(grp, off + TB_GRP_OFF_CALLEE, 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_ptr(cp.Cr, off + TB_CR_OFF_CALLER, caller)
|
|
_tb_wr_ptr(cp.Cr, off + TB_CR_OFF_CALLEE, callee)
|
|
_tb_wr_ptr(cp.Cr, off + TB_CR_OFF_FILE, 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_ptr(cp.Fr, off + TB_FR_OFF_SYM, sym)
|
|
_tb_wr_ptr(cp.Fr, off + TB_FR_OFF_FILE, 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: io.File* = io.File(path, io.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
|
|
# 容量全程�?sizet(与 sz 同宽):旧写�?ULONG(sz) + ULONG(2) 会在 sz >= 4GiB 时静默回�?
|
|
got: LONG = f.read_all(buf, sizet(sz) + 2)
|
|
f.close()
|
|
if got < 0: return None
|
|
# NUL-terminate at the actually read length. The pool buffer is NOT zero-filled (MemBuddy's
|
|
# diagnostic fill is 0xCD), so without this the caller's strlen() walks into uninitialised
|
|
# bytes and can run past the end of the arena.
|
|
buf[got] = 0
|
|
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 = int(_tb_parse_dec(s + p))
|
|
p = f3 + 1
|
|
file: str = _tb_dup_span(mb, s + p, b - p)
|
|
if _tb_str_ok(sym) == 0 or _tb_str_ok(file) == 0: 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 = int(_tb_parse_dec(s + p))
|
|
p = e4 + 1
|
|
file: str = _tb_dup_span(mb, s + p, b - p)
|
|
if _tb_str_ok(caller) == 0 or _tb_str_ok(callee) == 0 or _tb_str_ok(file) == 0: 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(int(_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(int(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 _tb_str_ok(caller) == 0 or _tb_str_ok(callee) == 0 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 _tb_str_ok(sym) == 0 or cp.Fr is None or cp.FrN <= 0 or addr < img_lo: return 0
|
|
for i in range(int(cp.FrN)):
|
|
off: sizet = sizet(i) * TB_FR_SIZE
|
|
s: str = rawptr(_tb_rd_u64(cp.Fr, off + TB_FR_OFF_SYM))
|
|
if _tb_str_ok(s) != 0 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_span(dump, ls, le, "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_span(dump, ls, le, ".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 _tb_str_ok(cur_func) != 0: _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 = 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 = 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:
|
|
fptr: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE)
|
|
# fail-fast: Windows x64 user addresses stay below 0x7FFF_FFFF_FFFF; anything else is a
|
|
# wild pointer (0, -1 or an uninitialised slot). Report the record instead of letting
|
|
# strlen walk off into unmapped memory (ASan access-violation at 0xffffffffffffffff).
|
|
if fptr < 65536 or fptr > 0x7FFFFFFFFFFF:
|
|
VLogger.error(f"--traceback: internal error: line record {i} carries an invalid file-path pointer ({fptr})", "TRACEBACK")
|
|
return 1
|
|
pth: str = rawptr(fptr)
|
|
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:
|
|
if fp > 0x7FFFFFFFFFFF:
|
|
VLogger.error(f"--traceback: internal error: line record {i} carries an invalid function-name pointer ({fp})", "TRACEBACK")
|
|
return 1
|
|
fns: str = rawptr(fp)
|
|
pool_need += string.strlen(fns) + 1
|
|
i += 1
|
|
i = 0
|
|
while i < fkept:
|
|
nmf: uint64 = _tb_rd_u64(d.FuncBuf, sizet(i) * TB_FUNC_SIZE + 16)
|
|
if nmf != 0:
|
|
if nmf > 0x7FFFFFFFFFFF:
|
|
VLogger.error(f"--traceback: internal error: subprogram record {i} carries an invalid name pointer ({nmf})", "TRACEBACK")
|
|
return 1
|
|
nm0: str = rawptr(nmf)
|
|
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(int(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(int(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(int(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: io.File* = io.File(map_path, io.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 |