diff --git a/1.vp b/1.vp new file mode 100644 index 0000000..99db2d9 --- /dev/null +++ b/1.vp @@ -0,0 +1,8 @@ +def a(l: int): + pass + +def main2() -> int*: + a(1) + # 1024000000000000000000 + return a + diff --git a/App/lib/Phase/BuildPipeline.vp b/App/lib/Phase/BuildPipeline.vp index 8297615..c746f4c 100644 --- a/App/lib/Phase/BuildPipeline.vp +++ b/App/lib/Phase/BuildPipeline.vp @@ -1,2918 +1,2904 @@ -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 .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 .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: " \n". - 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 (.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 //.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 "", line N / SyntaxError: - 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 - # Per-file strict NAFTAG opt-in (`from __future__ import __obs__`): detect it before the - # vpi pass so stub generation and translation agree on the mode. - GenApi.set_strict_obs(GenApi.detect_strict_obs(tree)) - # 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 - tr.StrictOBS = GenApi.strict_obs() - # 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 - - -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 .ll; the envelope's raw LLVM text becomes .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 - "", 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 .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= 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 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 .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: .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 :` ---- - 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 `` - 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 .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 .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 - - -# ============================================================================= -# 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: /bin/ -> / - 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: /plugins/ -> / (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") - +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 sys +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 .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: rawptr = 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" + +# Link commands longer than this are moved into a clang++ response file (@file). The Windows +# command runner caps a command line at 8192 chars (includes/subprocess CMDLINE_MAX) and refuses +# longer ones without running them; a self-build with ~110 object files exceeds that. +define LINK_RSP_THRESHOLD = 7000 + + +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: rawptr = sys.load_library("ircore.dll") + if h is None: + sys.set_dll_directory(IRCORE_LLVM_DLL_DIR) + h = sys.load_library("ircore.dll") + if h is None: return 0 + p_raw: rawptr = sys.symbol(h, "ircore_mlir_to_obj_raw") + p_free: rawptr = sys.symbol(h, "ircore_free") + if p_raw is None or p_free is None: + sys.close_library(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 .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: " \n". + 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 (.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 //.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 "", line N / SyntaxError: + 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 + # Per-file strict NAFTAG opt-in (`from __future__ import __obs__`): detect it before the + # vpi pass so stub generation and translation agree on the mode. + GenApi.set_strict_obs(GenApi.detect_strict_obs(tree)) + # 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 + tr.StrictOBS = GenApi.strict_obs() + # 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 + + +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). + if oslib.isdir(path): 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' + oslib.mkdir(buf) + buf[i] = saved + + # create the final directory + oslib.mkdir(buf) + 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 .ll; the envelope's raw LLVM text becomes .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 - "", 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 subprocess.run (no shell: 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 .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 and return its exit code.""" + if cmd is None: return -1 + res: subprocess.CompletedProcess* = subprocess.run(cmd, False, False) + if res is None: return -1 + return res.returncode + + +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: BOOL = vthreading.wait_handle(h_j, 600000) + if wr == 0: VLogger.error("llc thread join timeout/failure (not finished within 600 seconds)", "PhaseB") + vthreading.close_handle(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.""" + reader: oslib.DirReader* = oslib.scandir(base_dir) + if reader is None: + VLogger.warning("_CollectObjFilesImpl cannot open directory, fewer .obj collected", "collect") + return out_pos + entry: oslib.DirEntry = oslib.DirEntry() + while True: + rr: int = oslib.readdir(reader, &entry) + if rr == 0: break + if rr < 0: break + fname: str = entry.name + fname_len: sizet = len(fname) + if fname_len == 0: continue + if entry.is_dir: + 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: + 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 + oslib.closedir(reader) + 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= 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 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 + + +# _link_write_rsp - write a clang++ response file (@file) for an over-long link command. +# Backslashes become forward slashes: clang's response-file reader treats `\` as an escape +# character on Windows, so a raw `D:\dir\x.obj` would lose its separators and fail to open. +def _link_write_rsp(path: str, args: str) -> int: + """Write args to a clang++ response file; 1 = success / 0 = failure.""" + if path is None or args is None: return 0 + f: io.File* = io.File(path, io.MODE.W) + if f.closed: return 0 + n: sizet = string.strlen(args) + conv: bytes = _mbuddy.alloc(n + 1) + if conv is None: + f.close() + return 0 + i: sizet = 0 + while i < n: + ch: char = args[i] + if ch == '\\': ch = '/' + conv[i] = ch + i += 1 + conv[n] = '\0' + f.write(conv, n) + f.close() + return 1 + + +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 .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) + + # Response file: the Windows command runner rejects a command line longer than 8192 chars + # (includes/subprocess CMDLINE_MAX), so move the argument list to `@.rsp` and keep the + # invoked command short. This is what lets the ~110-object self-build link at all. + if string.strlen(cmd) >= LINK_RSP_THRESHOLD and linker_output is not None: + rsp_path_len: sizet = string.strlen(linker_output) + 8 + rsp_path: str = _mbuddy.alloc(rsp_path_len) + if rsp_path is not None: + viperlib.snprintf(rsp_path, rsp_path_len, "%s.rsp", linker_output) + args_off: sizet = string.strlen(linker_cmd) + while cmd[args_off] == ' ': args_off += 1 + if _link_write_rsp(rsp_path, cmd + args_off) != 0: + short_len: sizet = string.strlen(linker_cmd) + string.strlen(rsp_path) + 4 + short_cmd: bytes = _mbuddy.alloc(short_len) + if short_cmd is not None: + viperlib.snprintf(short_cmd, short_len, "%s @%s", linker_cmd, rsp_path) + cmd = short_cmd + + # 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: .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 :` ---- + 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 `` + 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 .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: the OS loader 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 .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 + + +# ============================================================================= +# 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: /bin/ -> / + 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: /plugins/ -> / (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 \ No newline at end of file diff --git a/App/lib/Phase/Phase1/Phase1Main.vp b/App/lib/Phase/Phase1/Phase1Main.vp index b824c08..1120d2d 100644 --- a/App/lib/Phase/Phase1/Phase1Main.vp +++ b/App/lib/Phase/Phase1/Phase1Main.vp @@ -1,10 +1,10 @@ from stdint import * import string import stdlib +import sys import memhub import viperlib import io -import win32.win32base import ast import mlirlite import lib.core.VLogger as VLogger @@ -233,7 +233,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg # On-demand dependency scan (App-first): seed the queue from App imports, then BFS-resolve # reachable includes only (read + SHA1 + import parse); unreferenced ones are never read. - t_scan0: uint64 = win32.win32base.GetTickCount64() + t_scan0: uint64 = sys.monotonic_ms() reachable_set: str = None reachable_count: int = 0 use_reachable: int = 0 @@ -257,7 +257,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg else: VLogger.warning("SourceDir unavailable; cannot build the dependency graph; skipping Phase1", "Phase1") return 0 - t_scan1: uint64 = win32.win32base.GetTickCount64() + t_scan1: uint64 = sys.monotonic_ms() VLogger.info(f"dependency-graph scan total time: {t_scan1 - t_scan0} ms (reachable {reachable_count} includes)", "Phase1") # Build the reachable FileEntry list from the Sha1Store; 1a/1b-pre/1b iterate result @@ -348,7 +348,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg # Iterate files: topo order preferred, falling back to alphabetical iter_count: int = int(topo_count if topo_count > 0 else result.Count) # 1a total-time start (struct/enum/union registration) - t_a0: uint64 = win32.win32base.GetTickCount64() + t_a0: uint64 = sys.monotonic_ms() for i in range(int(iter_count)): # Per-file mark: free this file's AST after translating (registered data is in perm_pool) p1a_mark: sizet = mb.mark() @@ -382,7 +382,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg continue # Per-file timer start (read + parse, to locate 1a stalls) - t_fa0: uint64 = win32.win32base.GetTickCount64() + t_fa0: uint64 = sys.monotonic_ms() # Read file contents file_path_r: str = entry_r.Path @@ -427,7 +427,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg continue # Timer 1: read + parse done - t_fa1: uint64 = win32.win32base.GetTickCount64() + t_fa1: uint64 = sys.monotonic_ms() # Create Translator with _declare_only=1 (register struct/enum/union) tr_r: HandlesTranslator.Translator* = HandlesTranslator.Translator() @@ -446,7 +446,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg GenApi.reset_visible_structs(mb, 0) ret_r: int = tr_r.translate(tree_r) # Timer 2: translate(declare=1) returned - t_fa2: uint64 = win32.win32base.GetTickCount64() + t_fa2: uint64 = sys.monotonic_ms() if ret_r != 0: p1a_fl += 1 else: p1a_reg += 1 @@ -471,7 +471,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg mb.reset_to(p1a_mark) # 1a total time (struct/enum/union registration, incl. per-file read+parse+declaration) - t_a1: uint64 = win32.win32base.GetTickCount64() + t_a1: uint64 = sys.monotonic_ms() VLogger.info(f"1a total time: {t_a1-t_a0} ms (registering struct/enum, reg={p1a_reg} skp={p1a_skp})", "Phase1") if p1a_fl > 0: @@ -498,7 +498,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg p1bpre_failed: int = 0 dir_len_pv: sizet = string.strlen(temp_dir) # 1b-pre total-time start (vpi pre-generation) - t_pv0: uint64 = win32.win32base.GetTickCount64() + t_pv0: uint64 = sys.monotonic_ms() for i_pv in range(int(result.Count)): entry_addr_pv: uint64 = uint64(result.Entries) + i_pv * entry_size entry_pv: IncludesScanner.FileEntry* = IncludesScanner.FileEntry*(entry_addr_pv) @@ -596,7 +596,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg mb.reset_to(mark1) # 1b-pre total time (vpi pre-generation) - t_pv1: uint64 = win32.win32base.GetTickCount64() + t_pv1: uint64 = sys.monotonic_ms() VLogger.info(f"1b-pre total time: {t_pv1-t_pv0} ms (vpi pre-generation)", "Phase1") if p1bpre_failed > 0: @@ -609,7 +609,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg skipped: int = 0 failed: int = 0 # 1b total-time start (full translation) - t_b0: uint64 = win32.win32base.GetTickCount64() + t_b0: uint64 = sys.monotonic_ms() loop_n_b: int = int(result.Count) if topo_count > 0: loop_n_b = topo_count @@ -732,7 +732,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg stdlib.free(stub_path) # Per-file timer start: read (parse/translate/dump segments, matching Phase2 instrumentation) - t_f0: uint64 = win32.win32base.GetTickCount64() + t_f0: uint64 = sys.monotonic_ms() # Read file contents file_path: str = entry.Path @@ -786,7 +786,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg continue # Timer 1: read + lexer/tokenize/parse done - t_f1: uint64 = win32.win32base.GetTickCount64() + t_f1: uint64 = sys.monotonic_ms() # Register TypedefT by scanning source text: AST string fields (attr/id) point into src_buf # and dangle across phases (reading attr faults), so match "NAME: TypedefT = TYPE" lines. @@ -842,7 +842,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg ret: int = tr.translate(tree) # Timer 2: translate returned - t_f2: uint64 = win32.win32base.GetTickCount64() + t_f2: uint64 = sys.monotonic_ms() # Diagnostic: translate returned normally (if it hangs inside, this line is not printed, # distinguishing an internal hang from a later dump hang). if ret == 0: VLogger.info(f"translation complete (translate returned): {file_path}", "Phase1") @@ -937,7 +937,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg stdlib.free(vpcall_text_p1) # Timer 3: dump (stub+text) done; print the per-file three-segment timing - t_f3: uint64 = win32.win32base.GetTickCount64() + t_f3: uint64 = sys.monotonic_ms() VLogger.info(f"[{i+1}/{loop_n_b}] translating: {file_path} (parse={t_f1-t_f0} ms, translate={t_f2-t_f1} ms, dump={t_f3-t_f2} ms, total={t_f3-t_f0} ms)", "Phase1") # save dependencies(_imported_modules) for Phase B @@ -971,7 +971,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg if sc_rc != 0: VLogger.error(f"memory corruption (free list, rc={sc_rc}): {file_path}", "Phase1") # 1b total time (full translation + dump) - t_b1: uint64 = win32.win32base.GetTickCount64() + t_b1: uint64 = sys.monotonic_ms() VLogger.info(f"1b total time: {t_b1-t_b0} ms (full translation, trans={translated} skip={skipped} fail={failed})", "Phase1") # Release the topo-sort resources used by Phase 1b diff --git a/App/lib/Phase/Phase2/__init__.vp b/App/lib/Phase/Phase2/__init__.vp index efe67e5..0a297f9 100644 --- a/App/lib/Phase/Phase2/__init__.vp +++ b/App/lib/Phase/Phase2/__init__.vp @@ -1,1563 +1,1519 @@ -import lib.core.Gen.GenApi as GenApi -from stdint import * -import stdio -import string -import stdlib -import memhub -import viperlib -import io -import win32.win32file -import win32.win32base -import sys -import ast -import mlirlite -import subprocess -import argparse -import lib.core.VLogger as VLogger -import lib.core.Gen.Translator as HandlesTranslator -import lib.Phase.BuildPipeline as BuildPipeline -import lib.core.IncludesScanner as IncludesScanner -import lib.core.CompilerFactory as CompilerFactory -import lib.core.builtin.ViperBuild as ViperBuild -import lib.Stub.StubMerger as StubMerger -import lib.Projectrans.Utils as Utils -import lib.Projectrans.Config as Config -import lib.Stub.StubMerger.StubMergerBuild as StubMergerBuild -import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect -import lib.Stub.StubMerger.StubMergerIR as StubMergerIR -import lib.Stub.StubMerger.StubMergerStore as StubMergerStore -import lib.Stub.StubMerger.StubMergerTopo as StubMergerTopo -import lib.Stub.StubGen.Converter as StubConverter -import lib.Stub.StubGen.VpiReader as VpiReader -import lib.Stub.StubGen.VpiStub as VpiStub - -# Global mbuddy pointer -_mbuddy: memhub.MemBuddy* - -# Source buffer size (1MB) -define SRC_BUF_SIZE = 1048576 - -# Max source files (recursive scanning grows the count; too small truncates the list -> undefined symbol) -define MAX_SRC_FILES = 128 - -novirtual class SrcFileEntry: - """Source file entry""" - Path: str # full path - Sha1: str # first 16 chars of SHA1 - -# _ScanDirForSources - recursively collect .vp or .s files into entries. -# want_asm=0: .vp sources, want_asm=1: .s assembly sources. -def _ScanDirForSources(mb: memhub.MemBuddy*, dir_path: str, entries: SrcFileEntry*, entry_size: sizet, file_count: int, max_files: int, want_asm: int) -> int: - if dir_path is None or entries is None: return file_count - if file_count >= max_files: return file_count - - dir_len: sizet = string.strlen(dir_path) - pattern: bytes = stdlib.malloc(dir_len + 16) - if pattern is None: return file_count - viperlib.snprintf(pattern, dir_len + 16, "%s/*", dir_path) - - find_data_size: sizet = win32.win32file.WIN32_FIND_DATAA.__sizeof__() - find_data: win32.win32file.WIN32_FIND_DATAA* = stdlib.malloc(find_data_size + 16) - if find_data is None: - stdlib.free(pattern) - return file_count - string.memset(find_data, 0, find_data_size + 16) - - handle: win32.win32base.HANDLE = win32.win32file.FindFirstFileA(pattern, find_data) - if handle == win32.win32base.INVALID_HANDLE_VALUE: - stdlib.free(pattern) - stdlib.free(find_data) - return file_count - - while True: - fname: str = find_data.cFileName - if fname is None: - if win32.win32file.FindNextFileA(handle, find_data) == 0: break - continue - fname_len: sizet = string.strlen(fname) - if fname_len <= 0: - if win32.win32file.FindNextFileA(handle, find_data) == 0: break - continue - 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: - if win32.win32file.FindNextFileA(handle, find_data) == 0: break - continue - attrs: ULONG = find_data.dwFileAttributes - is_dir: int = 0 - if (attrs & win32.win32file.FILE_ATTRIBUTE_DIRECTORY) != 0: is_dir = 1 - if is_dir != 0: - sub_dir: bytes = Utils.JoinPath(dir_path, fname) - if sub_dir is not None: - file_count = _ScanDirForSources(mb, sub_dir, entries, entry_size, file_count, max_files, want_asm) - stdlib.free(sub_dir) - if win32.win32file.FindNextFileA(handle, find_data) == 0: break - continue - - is_match: int = 0 - if want_asm == 0: - if fname_len > 3 and Utils.is_vp_at(fname, 0, fname_len) != 0: is_match = 1 - else: - if fname_len > 2 and fname[fname_len - 2] == '.' and fname[fname_len - 1] == 's': is_match = 1 - if is_match != 0 and file_count < max_files: - full_path: bytes = Utils.JoinPath(dir_path, fname) - if full_path is not None: - if want_asm == 0: - sbuf: bytes = Utils.read_file_to_buf(full_path, SRC_BUF_SIZE) - if sbuf is None: - if win32.win32file.FindNextFileA(handle, find_data) == 0: break - continue - sha1_val: str = IncludesScanner.compute_file_sha1(mb, full_path) - if sha1_val is not None: - ea: uint64 = uint64(entries) + file_count * entry_size - ent: SrcFileEntry* = (SrcFileEntry*)(ea) - if ent is not None: - ent.Path = full_path - ent.Sha1 = sha1_val - file_count += 1 - VLogger.info(f"{fname} (sha1={sha1_val})", "project") - stdlib.free(sbuf) - else: - sha1_val_a: str = IncludesScanner.compute_file_sha1(mb, full_path) - if sha1_val_a is not None: - ea_a: uint64 = uint64(entries) + file_count * entry_size - ent_a: SrcFileEntry* = (SrcFileEntry*)(ea_a) - if ent_a is not None: - ent_a.Path = full_path - ent_a.Sha1 = sha1_val_a - file_count += 1 - VLogger.info(f"{fname} (asm sha1={sha1_val_a})", "project") - - if win32.win32file.FindNextFileA(handle, find_data) == 0: break - - win32.win32file.FindClose(handle) - stdlib.free(pattern) - stdlib.free(find_data) - return file_count - - -# _ScanDirForPyFiles - collect Viper source files. -def _ScanDirForPyFiles(mb: memhub.MemBuddy*, dir_path: str, entries: SrcFileEntry*, entry_size: sizet, file_count: int, max_files: int) -> int: - """Recursively scan a directory and collect .vp files into entries.""" - return _ScanDirForSources(mb, dir_path, entries, entry_size, file_count, max_files, 0) - - -# _ScanDirForAsmFiles - collect assembly sources. -def _ScanDirForAsmFiles(mb: memhub.MemBuddy*, dir_path: str, entries: SrcFileEntry*, entry_size: sizet, file_count: int, max_files: int) -> int: - """Recursively scan a directory and collect .s files into entries.""" - return _ScanDirForSources(mb, dir_path, entries, entry_size, file_count, max_files, 1) - - -# _PreRegisterAppGenericTemplates - pre-register App generic class templates before mark2. -# The generic tables live in the pool; registering after mark2 would be reclaimed by reset_to, corrupting later passes. -def _PreRegisterAppGenericTemplates(mb: memhub.MemBuddy*, entries: SrcFileEntry*, file_count: int, entry_size: sizet) -> int: - """Pre-register App source generic class templates before mark2, return the processed count""" - if mb is None or entries is None or file_count <= 0: return 0 - handled: int = 0 - for i in range(int(file_count)): - ea_pa: uint64 = uint64(entries) + i * entry_size - ent_pa: SrcFileEntry* = (SrcFileEntry*)(ea_pa) - if ent_pa is None or ent_pa.Path is None or ent_pa.Sha1 is None: continue - # Read the file - f_pa: io.File* = io.File(ent_pa.Path, io.MODE.R) - if f_pa.closed: continue - # read_all's capacity includes the terminator (reads at most cap-1 bytes); longer files are truncated - if f_pa.size() >= SRC_BUF_SIZE: - VLogger.error(f"source file exceeds {SRC_BUF_SIZE} byte limit, refusing to read: {ent_pa.Path}", "PhaseA") - f_pa.close() - continue - src_buf_pa: bytes = stdlib.malloc(SRC_BUF_SIZE) - if src_buf_pa is None: - f_pa.close() - continue - bytes_read_pa: LONG = f_pa.read_all(src_buf_pa, SRC_BUF_SIZE) - f_pa.close() - if bytes_read_pa <= 0: - stdlib.free(src_buf_pa) - continue - if bytes_read_pa < SRC_BUF_SIZE: src_buf_pa[bytes_read_pa] = 0 - # Parse the AST only (perm pool keeps template nodes alive across the mark2 reset) - perm_pa: memhub.MemBuddy* = CompilerFactory.factory.PermPool - if perm_pa is None: perm_pa = mb - lx_pa: ast.Lexer* = ast.new_lexer(perm_pa) - if lx_pa is None: - stdlib.free(src_buf_pa) - continue - ast._lexer_init(lx_pa, src_buf_pa, perm_pa) - tokens_pa: ast.Token* = ast.tokenize(lx_pa) - tree_pa: ast.AST* = ast.parse_tokens(perm_pa, tokens_pa) - if tree_pa is None: - stdlib.free(src_buf_pa) - continue - # Walk top-level ClassDef/FunctionDef and register generic templates (type_params != None) - ch_pa: list[ast.AST*]* = tree_pa.children - if ch_pa is not None: - cn_pa: sizet = len(ch_pa) - for ci_pa in range(int(cn_pa)): - child_pa: ast.AST* = ch_pa.get(ci_pa) - if child_pa is None: continue - ck_pa: int = child_pa.kind() - if ck_pa == ast.ASTKind.ClassDef: - cd_pa: ast.ClassDef* = ast.ClassDef*(child_pa) - if cd_pa is None or cd_pa.name is None: continue - tp_pa: list[str]* = cd_pa.type_params - if tp_pa is None: continue - GenApi.set_current_file_type(ent_pa.Path) - GenApi._register_generic_template(perm_pa, cd_pa, ent_pa.Sha1) - handled += 1 - elif ck_pa == ast.ASTKind.FunctionDef: - fd_pa: ast.FunctionDef* = ast.FunctionDef*(child_pa) - if fd_pa is None or fd_pa.name is None: continue - tf_pa: list[str]* = fd_pa.type_params - if tf_pa is None: continue - GenApi.set_current_file_type(ent_pa.Path) - GenApi._register_generic_func_template(perm_pa, fd_pa, ent_pa.Sha1) - handled += 1 - stdlib.free(src_buf_pa) - if handled > 0: VLogger.info(f"pre-registered {handled} App generic templates before mark2 (classes + functions)", "PhaseA") - return handled - - -# Phase A convergence-failure dependency-graph diagnostics. -# Each file's deps and missing details are persisted to a stdlib table (survives reset) and dumped on failure. -PA_DEPS_MAX_STR: sizet = 1024 -PA_MISS_MAX_STR: sizet = 768 -PA_GRAPH_DEPTH_MAX: int = 3 # missing-chain drill-down depth (counts from layer 1) -PA_GRAPH_OK_PROBE: int = 2 # ready nodes are probed 1 more level (to find hidden misses) -_pa_diag_max: sizet = 0 -_pa_diag_count: int = 0 -_pa_diag_sha1: bytes = None -_pa_diag_deps: bytes = None -_pa_diag_miss: bytes = None - - -def _PhaseADiagInit(max_count: sizet) -> None: - """Allocate the persistent Phase A convergence-failure diagnostic table""" - global _pa_diag_max, _pa_diag_count, _pa_diag_sha1, _pa_diag_deps, _pa_diag_miss - _pa_diag_max = max_count - _pa_diag_count = 0 - _pa_diag_sha1 = stdlib.malloc(max_count * 17) - _pa_diag_deps = stdlib.malloc(max_count * PA_DEPS_MAX_STR) - _pa_diag_miss = stdlib.malloc(max_count * PA_MISS_MAX_STR) - if _pa_diag_sha1 is not None: string.memset(_pa_diag_sha1, 0, max_count * 17) - if _pa_diag_deps is not None: string.memset(_pa_diag_deps, 0, max_count * PA_DEPS_MAX_STR) - if _pa_diag_miss is not None: string.memset(_pa_diag_miss, 0, max_count * PA_MISS_MAX_STR) - - -def _PhaseADiagFree() -> None: - """Free the persistent diagnostic table""" - global _pa_diag_max, _pa_diag_count, _pa_diag_sha1, _pa_diag_deps, _pa_diag_miss - if _pa_diag_sha1 is not None: - stdlib.free(_pa_diag_sha1) - _pa_diag_sha1 = None - if _pa_diag_deps is not None: - stdlib.free(_pa_diag_deps) - _pa_diag_deps = None - if _pa_diag_miss is not None: - stdlib.free(_pa_diag_miss) - _pa_diag_miss = None - _pa_diag_count = 0 - _pa_diag_max = 0 - - -def _PhaseADiagRecord(sha1: str, deps: str, miss: str) -> None: - """Record or update one file's diagnostics: sha1 to deps plus missing detail""" - global _pa_diag_count - if _pa_diag_sha1 is None or _pa_diag_deps is None or sha1 is None: return - if string.strlen(sha1) < 16: return - for i in range(int(_pa_diag_count)): - es: bytes = _pa_diag_sha1 + sizet(i) * 17 - if string.strcmp(es, sha1) == 0: - if miss is not None and _pa_diag_miss is not None: - maddr: bytes = _pa_diag_miss + sizet(i) * PA_MISS_MAX_STR - string.strncpy(maddr, miss, PA_MISS_MAX_STR - 1) - maddr[PA_MISS_MAX_STR - 1] = 0 - return - if _pa_diag_count >= _pa_diag_max: return - saddr: bytes = _pa_diag_sha1 + sizet(_pa_diag_count) * 17 - string.strncpy(saddr, sha1, 16) - saddr[16] = 0 - daddr: bytes = _pa_diag_deps + sizet(_pa_diag_count) * PA_DEPS_MAX_STR - if deps is not None: string.strncpy(daddr, deps, PA_DEPS_MAX_STR - 1) - daddr[PA_DEPS_MAX_STR - 1] = 0 - if miss is not None and _pa_diag_miss is not None: - maddr2: bytes = _pa_diag_miss + sizet(_pa_diag_count) * PA_MISS_MAX_STR - string.strncpy(maddr2, miss, PA_MISS_MAX_STR - 1) - maddr2[PA_MISS_MAX_STR - 1] = 0 - _pa_diag_count += 1 - - -def _PhaseADiagDepsLookup(sha1: str) -> str: - """Look up the persisted deps string by sha1; caller frees the copy, None if absent""" - if _pa_diag_sha1 is None or _pa_diag_deps is None or sha1 is None: return None - for i in range(int(_pa_diag_count)): - es: bytes = _pa_diag_sha1 + sizet(i) * 17 - if string.strcmp(es, sha1) == 0: - src: bytes = _pa_diag_deps + sizet(i) * PA_DEPS_MAX_STR - ln: sizet = string.strlen(src) - out: bytes = stdlib.malloc(ln + 1) - if out is None: return None - string.strcpy(out, src) - return out - return None - - -def _PhaseADiagMissLookup(sha1: str) -> str: - """Look up the persisted missing detail by sha1; caller frees the copy, None if absent""" - if _pa_diag_sha1 is None or _pa_diag_miss is None or sha1 is None: return None - for i in range(int(_pa_diag_count)): - es: bytes = _pa_diag_sha1 + sizet(i) * 17 - if string.strcmp(es, sha1) == 0: - src: bytes = _pa_diag_miss + sizet(i) * PA_MISS_MAX_STR - ln: sizet = string.strlen(src) - if ln == 0: return None - out: bytes = stdlib.malloc(ln + 1) - if out is None: return None - string.strcpy(out, src) - return out - return None - - -def _PhaseATextLLExists(temp_dir: str, td_len: sizet, sha1: str) -> int: - """Probe whether {temp_dir}/{sha1 first 2 chars}/{sha1}.text.ll exists, 1=yes""" - if temp_dir is None or td_len == 0 or sha1 is None or string.strlen(sha1) < 2: return 0 - tpath: str = StubMergerCollect._sliced_path(temp_dir, td_len, sha1, "text.ll") - if tpath is None: return 0 - tf: io.File* = io.File(tpath, io.MODE.R) - exists: int = 0 - if not tf.closed: - tf.close() - exists = 1 - stdlib.free(tpath) - return exists - - -def _PhaseAResolveDepSha1(mod_name: str) -> str: - """Resolve a dependency module name to SHA1 (exact then suffix), None if absent""" - if mod_name is None: return None - s1: str = StubMergerStore.LookupSha1ByModName(mod_name) - if s1 is not None: return s1 - return StubMergerStore.LookupSha1ByModNameSuffix(mod_name) - - -def _PhaseADumpNode(temp_dir: str, td_len: sizet, sha1: str, depth: int, prefix: bytes) -> None: - """Recursively print the dependency subgraph; [MISS]=missing text.ll, [OK]=ready""" - deps: bytes = None - if sha1 is not None: deps = _PhaseADiagDepsLookup(sha1) - if deps is None and sha1 is not None: - # Fallback: read {temp_dir}/{prefix}/{sha1}.deps.txt (Phase1 already wrote it for includes) - dpath: str = StubMergerCollect._sliced_path(temp_dir, td_len, sha1, "deps.txt") - if dpath is not None: - df: io.File* = io.File(dpath, io.MODE.R) - if not df.closed: - deps = stdlib.malloc(PA_DEPS_MAX_STR) - if deps is not None: - rl: int64 = df.read_all(deps, PA_DEPS_MAX_STR) - if rl < 0: rl = 0 - # read_all's size arg is the buffer capacity (incl. terminator): it reads at most MAX-1 - deps[rl] = 0 - df.close() - stdlib.free(dpath) - if deps is None or deps[0] == 0: - if deps is not None: stdlib.free(deps) - return - dl: sizet = string.strlen(deps) - token_count: int = 0 - pos: sizet = 0 - while pos < dl: - while pos < dl and deps[pos] == ' ': pos += 1 - if pos >= dl: break - token_count += 1 - while pos < dl and deps[pos] != ' ': pos += 1 - pos = 0 - idx: int = 0 - while pos < dl: - while pos < dl and deps[pos] == ' ': pos += 1 - if pos >= dl: break - ts: sizet = pos - while pos < dl and deps[pos] != ' ': pos += 1 - tl: sizet = pos - ts - nm: bytes = stdlib.malloc(tl + 1) - if nm is not None: - string.strncpy(nm, deps + ts, tl) - nm[tl] = 0 - is_last: int = 0 - if idx + 1 >= token_count: is_last = 1 - dep_sha1: str = _PhaseAResolveDepSha1(nm) - txt_ok: int = 0 - if dep_sha1 is not None: txt_ok = _PhaseATextLLExists(temp_dir, td_len, dep_sha1) - # Branch characters are inlined (don't return string literals: VPC would return a dangling - # stack pointer for a literal -> printf %s crashes and the graph output is swallowed) - br_1: str = "|-- " - br_2: str = "`-- " - if dep_sha1 is None: - if is_last != 0: stdio.printf("%s%s%s (unresolved)\n", prefix, br_2, nm) - else: stdio.printf("%s%s%s (unresolved)\n", prefix, br_1, nm) - elif txt_ok != 0: - if is_last != 0: stdio.printf("%s%s%s [OK]\n", prefix, br_2, nm) - else: stdio.printf("%s%s%s [OK]\n", prefix, br_1, nm) - else: - if is_last != 0: stdio.printf("%s%s%s [MISS] text.ll=%s\n", prefix, br_2, nm, dep_sha1) - else: stdio.printf("%s%s%s [MISS] text.ll=%s\n", prefix, br_1, nm, dep_sha1) - # Drill down: MISS follows the whole chain (to PA_GRAPH_DEPTH_MAX); OK probes 1 level only - can_expand: int = 0 - if dep_sha1 is not None: - if txt_ok == 0: - if depth + 1 < PA_GRAPH_DEPTH_MAX: can_expand = 1 - else: - if depth + 1 < PA_GRAPH_OK_PROBE: can_expand = 1 - if can_expand != 0: - # +5: strcat appends "| "/" " (4 chars) + NUL; strlen+4 would overflow 1 byte and - # corrupt heap metadata, crashing later malloc/free and aborting the graph output - npl: sizet = string.strlen(prefix) + 5 - new_prefix: bytes = stdlib.malloc(npl) - if new_prefix is not None: - string.strcpy(new_prefix, prefix) - if is_last != 0: string.strcat(new_prefix, " ") - else: string.strcat(new_prefix, "| ") - _PhaseADumpNode(temp_dir, td_len, dep_sha1, depth + 1, new_prefix) - stdlib.free(new_prefix) - stdlib.free(nm) - idx += 1 - stdlib.free(deps) - - -def _PhaseADumpFailGraph(temp_dir: str, td_len: sizet, path: str, sha1: str) -> None: - """Convergence-failure diagnostics: pending detail plus dependency tree. - Flush after every printf so the trace survives a later crash.""" - stdio.printf("[PhaseA-Fail] ==============================================================\n") - stdio.fflush(None) - stdio.printf("[PhaseA-Fail] FAIL TO TRANSLATE: %s [sha1=%s]\n", path, sha1) - stdio.fflush(None) - miss: bytes = _PhaseADiagMissLookup(sha1) - if miss is not None: - if miss[0] != 0: stdio.printf("[PhaseA-Fail] pending reason: %s\n", miss) - stdlib.free(miss) - stdio.fflush(None) - stdio.printf("[PhaseA-Fail] dep graph ([MISS]=missing text.ll, [OK]=ready, depth<=%d):\n", PA_GRAPH_DEPTH_MAX) - stdio.fflush(None) - root_prefix: bytes = stdlib.malloc(1) - if root_prefix is not None: - root_prefix[0] = 0 - _PhaseADumpNode(temp_dir, td_len, sha1, 0, root_prefix) - stdlib.free(root_prefix) - stdio.fflush(None) - - -def RunMultiFileProject(mb: memhub.MemBuddy*, source_dir: str, temp_dir: str, output_dir: str, cc_cmd: str, cc_flags: str, linker_cmd: str, linker_flags: str, linker_output: str, includes_binary_dir: str, includes_dir: str, do_phase1: int, do_phase2: int, log: VLogger.Logger*, args: argparse.ParsedArgs*, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> int: - """Compile a multi-file project; return 0 on success, non-zero on failure""" - if source_dir is None: - VLogger.error("source_dir is empty", "project") - return 1 - - VLogger.info(f"multi-file project compilation: {source_dir}", "project") - - # === 1. Recursively scan .vp/.py files under source_dir (including subdirectories) === - entry_size: sizet = SrcFileEntry.__sizeof__() - entries: SrcFileEntry* = stdlib.malloc(MAX_SRC_FILES * entry_size) - if entries is None: return 1 - string.memset(entries, 0, MAX_SRC_FILES * entry_size) - - file_count: int = _ScanDirForPyFiles(mb, source_dir, entries, entry_size, 0, MAX_SRC_FILES) - VLogger.info(f"{file_count} source files in total", "project") - - # Scan .s assembly files (separate array; not translated in Phase A, assembled by llvm-mc after Phase B) - asm_entries: SrcFileEntry* = stdlib.malloc(MAX_SRC_FILES * entry_size) - if asm_entries is None: - stdlib.free(entries) - return 1 - string.memset(asm_entries, 0, MAX_SRC_FILES * entry_size) - asm_file_count: int = _ScanDirForAsmFiles(mb, source_dir, asm_entries, entry_size, 0, MAX_SRC_FILES) - if asm_file_count > 0: VLogger.info(f"{asm_file_count} assembly files (assembled by llvm-mc and linked)", "project") - - if file_count == 0: - stdlib.free(entries) - stdlib.free(asm_entries) - return 1 - - # Initialize the AST tables - ast._init_tables(mb) - - # Pre-register App generic class templates before mark2: the tables/nodes are pool-allocated, so they - # must be registered before mark2 to survive reset_to(mark2); otherwise later passes hit dangling memory. - _PreRegisterAppGenericTemplates(mb, entries, file_count, entry_size) - - # OOM cleanup (mirrors Phase1's mark/reset pattern): without reclamation the AST/IR of all includes - # pile up in the pool. mark2 lets each file's transient data be freed by reset_to after translation. - mark2: sizet = mb.mark() - # struct/enum entries registered after mark2 use stdlib allocation (survive reset, as in Phase1) - GenApi.set_stdlib_alloc(1) - - # Ensure the temp/output directories exist - BuildPipeline.ensure_dir(temp_dir) - BuildPipeline.ensure_dir(output_dir) - - # === 0.5 Fill Sha1Store on demand from the dependency graph (removes the Phase1 dependency) === - # If Sha1Store is empty, scan the dependency graph (App-first BFS) so later "compile missing includes" works. - if includes_dir is not None: - if StubMergerStore.GetSha1StoreCount() == 0: - rs_05: bytes = stdlib.malloc(StubMerger.MAX_INCLUDES_SHA1 * 17) - if rs_05 is None: VLogger.error("reachable_set allocation failed, cannot fill Sha1Store", "project") - else: - string.memset(rs_05, 0, StubMerger.MAX_INCLUDES_SHA1 * 17) - rc_05: int = StubMergerBuild._BuildReachableSha1Set(mb, source_dir, temp_dir, rs_05, None, includes_dir) - if rc_05 < 0: VLogger.error("dependency graph scan failed, cannot fill Sha1Store", "project") - elif rc_05 > 0: VLogger.info(f"Phase2 dependency graph filled Sha1Store on demand: {rc_05} entries", "project") - stdlib.free(rs_05) - - # Append App source SHA1s to _sha1_map.txt (human-readable only; format {sha1}:{rel_path}) and to the - # in-memory store (AppendToSha1MapStore) for cross-module DefineT lookups. - src_dir_len_am: sizet = string.strlen(source_dir) - # _sha1_map.txt 落在构建目录(temp 的上一级),方便直接按 SHA1 反查源文件 - map_path_am: bytes = StubMergerCollect.Sha1MapPath(mb, temp_dir) - if map_path_am is not None: - mf: io.File* = io.File(map_path_am, io.MODE.A) - line_am: bytes = stdlib.malloc(512) - # First fill the in-memory store (no file I/O), then write _sha1_map.txt - app_filled: int = 0 - for i in range(int(file_count)): - ea_am: uint64 = uint64(entries) + i * entry_size - ent_am: SrcFileEntry* = (SrcFileEntry*)(ea_am) - if ent_am is None or ent_am.Path is None or ent_am.Sha1 is None: continue - # Compute the relative path (strip the source_dir prefix, keep the directory structure) - p_str: str = ent_am.Path - p_len: sizet = string.strlen(p_str) - rel_path_am: str = p_str - if p_len > src_dir_len_am + 1: - if string.strncmp(p_str, source_dir, src_dir_len_am) == 0: rel_path_am = p_str + src_dir_len_am + 1 - # Also append to the in-memory store (for cross-module DefineT lookups) - # rel_path_am looks like "lib/core/StubMerger.vp", matching _sha1_map.txt - if StubMergerStore.AppendToSha1MapStore(ent_am.Sha1, rel_path_am) != 0: VLogger.error(f"AppendToSha1MapStore failed: {rel_path_am}", "project") - app_filled += 1 - # Write _sha1_map.txt (human-readable output) - if not mf.closed and line_am is not None: - viperlib.snprintf(line_am, 512, "%s:%s\n", ent_am.Sha1, rel_path_am) - ll_am: sizet = string.strlen(line_am) - mf.write(line_am, ll_am) - if line_am is not None: stdlib.free(line_am) - if not mf.closed: - mf.close() - VLogger.info(f"appended {app_filled} App files to _sha1_map.txt and the in-memory store", "project") - stdlib.free(map_path_am) - - # The module SHA1 map is filled into the StubMerger global store by PopulateSha1MapStore + - # AppendToSha1MapStore; HandlesExprCall._lookup_module_sha1 queries it directly, no local copy needed. - - - # === Phase A-pre-vpi: pre-generate .vpi stubs for all App sources === - # Deps' .vpi must exist before their dependents translate; pre-generating avoids missing dep declarations. - td_len_vpa: sizet = string.strlen(temp_dir) - # Reuse BuildPipeline's DefineT (proven to be non-zero: the same buffer size generates ViperBuild.vp's - # vpi in Phase A). A large literal stored in a local used to come through as 0 here, and - # _GeneratePyiFromAst then returns immediately (buf_size == 0) -> every App .vpi silently missing. - PYI_BUF_SIZE_P2: sizet = BuildPipeline.PYI_BUF_SIZE - vpi_failed_p2: int = 0 - vpi_written_p2: int = 0 - vpi_skipped_p2: int = 0 - for i_vp2 in range(int(file_count)): - ea_vp2: uint64 = uint64(entries) + i_vp2 * entry_size - ent_vp2: SrcFileEntry* = (SrcFileEntry*)(ea_vp2) - if ent_vp2 is None or ent_vp2.Path is None or ent_vp2.Sha1 is None: - vpi_failed_p2 += 1 - VLogger.error("[Phase A-pre-vpi] invalid source entry (Path/Sha1 missing), skipping", "PhaseA") - continue - # Skip if the vpi already exists (sha1 is content-based, so a same-named vpi is valid; cross-run cache) - vpi_path_vp2: str = StubMergerCollect._sliced_path(temp_dir, td_len_vpa, ent_vp2.Sha1, "vpi") - if vpi_path_vp2 is None: - vpi_failed_p2 += 1 - VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | failed to build vpi path", "PhaseA") - continue - vf_vp2: io.File* = io.File(vpi_path_vp2, io.MODE.R) - if not vf_vp2.closed: - vf_vp2.close() - VpiStub.Ensure(vpi_path_vp2, ent_vp2.Sha1) - vpi_skipped_p2 += 1 - stdlib.free(vpi_path_vp2) - continue - stdlib.free(vpi_path_vp2) - # Read the source -> parse the AST -> generate the vpi (rel_path = ent_vp2.Path: vpi content does - # not depend on it; it is only used for fatal_error attribution, pointing at this source file) - f_vp2: io.File* = io.File(ent_vp2.Path, io.MODE.R) - if f_vp2.closed: - vpi_failed_p2 += 1 - VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | cannot open source file", "PhaseA") - continue - # read_all's capacity includes the terminator (reads at most cap-1 bytes); longer files are truncated - if f_vp2.size() >= SRC_BUF_SIZE: - vpi_failed_p2 += 1 - VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | source file exceeds {SRC_BUF_SIZE} byte limit", "PhaseA") - f_vp2.close() - continue - src_buf_vp2: bytes = stdlib.malloc(SRC_BUF_SIZE) - if src_buf_vp2 is None: - f_vp2.close() - vpi_failed_p2 += 1 - VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | source buffer allocation failed (OOM)", "PhaseA") - continue - bytes_read_vp2: LONG = f_vp2.read_all(src_buf_vp2, SRC_BUF_SIZE) - f_vp2.close() - if bytes_read_vp2 <= 0: - stdlib.free(src_buf_vp2) - vpi_failed_p2 += 1 - VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | file read failed or empty", "PhaseA") - continue - if bytes_read_vp2 < SRC_BUF_SIZE: src_buf_vp2[bytes_read_vp2] = 0 - lx_vp2: ast.Lexer* = ast.new_lexer(mb) - if lx_vp2 is None: - stdlib.free(src_buf_vp2) - vpi_failed_p2 += 1 - VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | lexer allocation failed (OOM)", "PhaseA") - continue - ast._lexer_init(lx_vp2, src_buf_vp2, mb) - tokens_vp2: ast.Token* = ast.tokenize(lx_vp2) - tree_vp2: ast.AST* = ast.parse_tokens(mb, tokens_vp2) - if tree_vp2 is None: - stdlib.free(src_buf_vp2) - vpi_failed_p2 += 1 - # Parse failure: inline the line/column + reason in the log line (same source as the full - # Python-style traceback below), since "AST parse failed" alone gives no location or cause. - VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | AST parse failed (line {ast.get_parse_err_line()} col {ast.get_parse_err_col()}: {ast.get_parse_err_msg()})", "PhaseA") - GenApi.dump_parse_traceback(ent_vp2.Path) - continue - pyi_buf_vp2: bytes = stdlib.malloc(PYI_BUF_SIZE_P2) - if pyi_buf_vp2 is not None: - # 文件级名字窗口(FileFuncNames 等)由 _GeneratePyiFromAst 入口统一 reset+重建, - # 不再在此处重复调用;契约见 StructGenMixin.reset_file_name_window。 - pyi_pos_vp2: sizet = StubConverter._GeneratePyiFromAst(mb, tree_vp2, ent_vp2.Path, pyi_buf_vp2, PYI_BUF_SIZE_P2) - if pyi_pos_vp2 == 0: - # The generator always emits at least a newline once buf_size > 1; 0 means the - # buffer size collapsed (or the generator failed) and no .vpi would be produced — - # every later cross-module lookup for this module would then fail. - vpi_failed_p2 += 1 - VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | vpi generation produced nothing (buf_size={PYI_BUF_SIZE_P2})", "PhaseA") - if pyi_pos_vp2 > 0: - pyi_path_vp2: str = StubMergerCollect._sliced_path(temp_dir, td_len_vpa, ent_vp2.Sha1, "vpi") - if pyi_path_vp2 is not None: - pf_vp2: io.File* = io.File(pyi_path_vp2, io.MODE.W) - if pf_vp2.closed: - # Silent here used to mean "no App .vpi at all" and every cross-module - # return-type lookup later failed. Report the exact path instead. - vpi_failed_p2 += 1 - VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | cannot open the vpi for writing: {pyi_path_vp2}", "PhaseA") - pf_vp2.write(pyi_buf_vp2, pyi_pos_vp2) - pf_vp2.close() - VpiStub.Emit(pyi_path_vp2, pyi_buf_vp2, pyi_pos_vp2, ent_vp2.Sha1) - vpi_written_p2 += 1 - stdlib.free(pyi_path_vp2) - stdlib.free(pyi_buf_vp2) - else: - vpi_failed_p2 += 1 - VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | pyi buffer allocation failed (OOM)", "PhaseA") - stdlib.free(src_buf_vp2) - # OOM cleanup: free this file's AST (the vpi is already on disk; persistent data is stdlib-allocated) - mb.reset_to(mark2) - VLogger.info(f"[Phase A-pre-vpi] generated={vpi_written_p2} skipped_existing={vpi_skipped_p2} failed={vpi_failed_p2} total={file_count}", "PhaseA") - if vpi_failed_p2 > 0: - VLogger.error(f"Phase A-pre-vpi: {vpi_failed_p2} files failed vpi pre-generation (see the per-file [Phase A-pre-vpi] logs above for failing files and causes)", "PhaseA") - return 1 - - - # === 2. Phase A: generate stub + text for each file === - # app_deps_buf collects App files' direct deps (space-separated) as Phase B+'s worklist (65536, matching StubMerger). - APP_DEPS_BUF_SIZE: sizet = 65536 - app_deps_buf: bytes = None - app_deps_len: sizet = 0 - if do_phase1 != 0: - log.banner("Phase A: generate stub + text") - # Initialize the Phase A convergence-failure diagnostic table (stdlib, survives reset) - _PhaseADiagInit(sizet(file_count) + StubMerger.MAX_INCLUDES) - - # 枚举全量预扫(最早的一次):所有已落盘 .vpi 里的 `enum X:` 先登记进 EnumTable, - # 再进入任何预注册/翻译。否则依赖 stub 里的 `types.FRESULT` 会在枚举模块的 vpi - # 被注入之前被当成类名 ensure_forward_class,占位结构体常驻进程级 StructTable, - # 之后所有模块的 `FRESULT` 都解析成空结构体(llvm.call 返回 - # !llvm.struct<"...FRESULT", ()>,函数本体却是 i32 -> mlir-opt: llvm.return 类型不匹配)。 - preenum_early: int = VpiReader.PreRegisterVpiEnums(mb, temp_dir) - VLogger.info(f"enum pre-scan (early): {preenum_early} enums registered", "PhaseA") - - # === Phase A-pre-inc: pre-register includes structs + fill deps === - # Must run before App pre-registration (multi-pass resolves cross-file inheritance) and fills PopulateIncludesDeps. - if includes_dir is not None and includes_binary_dir is not None: - store_count_inc_pre: int = StubMergerStore.GetSha1StoreCount() - if store_count_inc_pre > 0: - store_sha1_inc_pre: bytes = StubMergerStore.GetSha1StoreArrPtr() - store_rel_inc_pre: bytes = StubMergerStore.GetSha1StoreRelArrPtr() - store_incdir_inc_pre: bytes = StubMergerStore.GetSha1StoreIncdirArrPtr() - deps_filled_pre: int = 0 - PHASE_A_INC_MAX_PASSES: int = 3 - # Track includes that fail pre-registration: after all passes, any failure aborts the build - # (no silent downgrade). App files must be skipped here; Phase A-pre handles them. - inc_fail_flags: bytes = stdlib.malloc(store_count_inc_pre) - if inc_fail_flags is not None: string.memset(inc_fail_flags, 1, store_count_inc_pre) - for pass_inc_i in range(int(PHASE_A_INC_MAX_PASSES)): - struct_count_before_inc: int = GenApi.get_struct_count() - for si_inc in range(int(store_count_inc_pre)): - inc_sha1_pre: str = store_sha1_inc_pre + sizet(si_inc) * 17 - inc_rel_pre: str = store_rel_inc_pre + sizet(si_inc) * StubMerger.MAX_REL_PATH_LEN - if inc_rel_pre is None or inc_rel_pre[0] == '\0': continue - # Skip App source files (main.vp etc.): pre-registered by Phase A-pre, not a failure - is_app_file_inc: int = 0 - for i_app_inc in range(int(file_count)): - ea_app_inc: uint64 = uint64(entries) + i_app_inc * entry_size - ent_app_inc: SrcFileEntry* = (SrcFileEntry*)(ea_app_inc) - if ent_app_inc is not None and ent_app_inc.Sha1 is not None and string.strcmp(ent_app_inc.Sha1, inc_sha1_pre) == 0: - is_app_file_inc = 1 - break - if is_app_file_inc != 0 and inc_fail_flags is not None: - inc_fail_flags[si_inc] = 0 - continue - rel_len_pre: sizet = string.strlen(inc_rel_pre) - # Empty IncDir marks an App source (AppendToSha1MapStore / _BuildReachableSha1Set): - # its rel path is source_dir-relative, so resolving it against includes_dir opens a - # non-existent file and the translation fails with no diagnostic. App declarations - # are pre-registered by Phase A-pre below (against source_dir) — skip them here. - incdir_pre: str = store_incdir_inc_pre + sizet(si_inc) * StubMerger.MAX_INC_DIR_LEN - if incdir_pre is None or incdir_pre[0] == '\0': - if inc_fail_flags is not None: inc_fail_flags[si_inc] = 0 - continue - incdir_len_pre: sizet = string.strlen(incdir_pre) - full_path_pre: bytes = stdlib.malloc(incdir_len_pre + rel_len_pre + 2) - if full_path_pre is None: continue - viperlib.snprintf(full_path_pre, incdir_len_pre + rel_len_pre + 2, "%s/%s", incdir_pre, inc_rel_pre) - pkg_inc_pre: str = GenApi.compute_package_from_relpath(mb, inc_rel_pre) - # Pass temp_dir so the include's .vpi is materialized here: Phase A resolves - # cross-module method return types (and escape contracts) from the dependency's - # .vpi, and in a build without Phase1 artifacts none exists yet — the lookup then - # fails hard ("method return type cannot be obtained precisely"). - tr_inc_pre: HandlesTranslator.Translator* = BuildPipeline.TranslateFileGetTrans(mb, full_path_pre, inc_sha1_pre, pkg_inc_pre, 1, temp_dir) - stdlib.free(full_path_pre) - if tr_inc_pre is not None: - # Record this file's deps (for convergence diagnostics; stdlib table survives reset) - _PhaseADiagRecord(inc_sha1_pre, tr_inc_pre._imported_modules, None) - # Pre-registration succeeded: clear the failure flag - if inc_fail_flags is not None: inc_fail_flags[si_inc] = 0 - # Fill deps only on the first pass (PopulateIncludesDeps dedupes internally). - # Must be called before reset_to(mark2) since it deep-copies into stdlib. - if pass_inc_i == 0 and tr_inc_pre._imported_modules is not None and StubMergerStore.PopulateIncludesDeps(inc_sha1_pre, tr_inc_pre._imported_modules) == 0: - deps_filled_pre = deps_filled_pre + 1 - # OOM cleanup: free this includes file's transient pre-registration data - # (AST/tokens/lexer/IR/Handler); stdlib-allocated struct entries are unaffected - mb.reset_to(mark2) - struct_count_after_inc: int = GenApi.get_struct_count() - # Convergence check: no new struct registered this pass -> all classes are registered - if struct_count_after_inc == struct_count_before_inc: break - # Includes still failing after all passes -> abort (no downgrade) - if inc_fail_flags is not None: - for si_inc_f in range(int(store_count_inc_pre)): - if inc_fail_flags[si_inc_f] != 0: - rel_f: str = store_rel_inc_pre + sizet(si_inc_f) * StubMerger.MAX_REL_PATH_LEN - VLogger.error(f"includes pre-registration failed: {rel_f} (file cannot be translated; possible syntax/parse/compile error), aborting compilation", "prePhaseA") - stdlib.free(inc_fail_flags) - VLogger.info(f"includes pre-registration + dependency fill: {deps_filled_pre}/{store_count_inc_pre}", "prePhaseA") - - # === Phase A-pre: pre-register struct/enum/union for all sources === - # Registers declarations only (declare_only=1), no method bodies; multi-pass resolves cross-file ordering. - VLogger.info("pre-registering struct/enum/union...", "PhaseA") - src_dir_len_pre: sizet = string.strlen(source_dir) - PHASE_A_PRE_MAX_PASSES: int = 3 - for pass_i in range(int(PHASE_A_PRE_MAX_PASSES)): - struct_count_before: int = GenApi.get_struct_count() - for i in range(int(file_count)): - ea_pre: uint64 = uint64(entries) + i * entry_size - ent_pre: SrcFileEntry* = (SrcFileEntry*)(ea_pre) - if ent_pre is None or ent_pre.Path is None: continue - pkg_pre: str = None - if string.strlen(ent_pre.Path) > src_dir_len_pre + 1: - rel_path_pre: str = ent_pre.Path + src_dir_len_pre + 1 - pkg_pre = GenApi.compute_package_from_relpath(mb, rel_path_pre) - # App pre-registration passes pre_allow_dep_inject=1: imports and dep vpi injection happen at - # declaration time so field annotations share Phase A's context (else struct layouts diverge). - # temp_dir is passed so every App module's .vpi is materialized here (right after its AST is - # parsed), before Phase A translates anything: a module referenced by an earlier file must - # have its declarations on disk, else the call site cannot resolve its return type. - tr_pre: HandlesTranslator.Translator* = BuildPipeline.TranslateFileGetTrans(mb, ent_pre.Path, ent_pre.Sha1, pkg_pre, 1, temp_dir, 1) - if tr_pre is None: VLogger.error(f"pre-registration failed: {ent_pre.Path}", "PhaseA") - # OOM cleanup: free this App file's transient pre-registration data - mb.reset_to(mark2) - struct_count_after: int = GenApi.get_struct_count() - # Convergence check: no new struct registered this pass -> all classes are registered - if struct_count_after == struct_count_before: break - VLogger.info("pre-registration complete", "PhaseA") - - # 缺省参数全量预扫(TPC 式):所有模块 .vpi 里带缺省值的函数/方法一次性登记进全局 - # defaults 表,避免“谁先翻译谁登记”的时序依赖。stub-only 依赖(如包 mlirlite 的 - # __builder.vp,只产 .vpi 不产 .ll)走不到 FuncGenDef,缺了这一步跨模块少传实参时 - # 会补不出缺省值,直接发出操作数不足的 llvm.call(mlir-opt: incorrect number of - # operands;现场 build_load / build_store)。 - predef_count: int = VpiReader.PreRegisterVpiDefaults(mb, temp_dir) - VLogger.info(f"default-argument pre-scan: {predef_count} functions registered", "PhaseA") - - # 枚举全量预扫(与缺省值同理):所有模块 .vpi 里的 `enum X:` 一次性登记进 EnumTable, - # 消除"谁先注入谁登记"的时序依赖。否则依赖 stub 里的 `types.FRESULT` 会在枚举模块 - # 的 vpi 被注入之前解析,被当成类名 ensure_forward_class;占位结构体常驻进程级 - # StructTable,之后所有模块的 `FRESULT` 都变成空结构体(llvm.call 返回 - # !llvm.struct<"...FRESULT", ()>,而函数本体是 i32 -> mlir-opt: llvm.return 类型不匹配)。 - preenum_count: int = VpiReader.PreRegisterVpiEnums(mb, temp_dir) - VLogger.info(f"enum pre-scan: {preenum_count} enums registered", "PhaseA") - - # Phase A IR buffer: 1MB is not enough, ast/parser.vp's text.ll exceeds 1MB and got truncated -> - # merged IR lost the tail (opt: "end of file in global variable name"). - PHASE_A_IR_SIZE: sizet = 8388608 - # FULL dump = stub + text (same module), with 2x headroom - PHASE_A_FULL_SIZE: sizet = 16777216 - td_len_pa: sizet = string.strlen(temp_dir) - - # Set the temp dir so HandlesExprCall can read dependency modules' text.ll for cross-module - # method return-type lookup (avoids pointer returns being truncated to i32) - GenApi.SetTempDir(temp_dir) - - # app_deps_buf collects App files' direct dependency modules (space-separated) as Phase B+'s initial - # worklist for _BuildReachableSha1Set; declared outside the if, only assigned here. - app_deps_buf = stdlib.malloc(APP_DEPS_BUF_SIZE) - app_deps_len = 0 - if app_deps_buf is not None: app_deps_buf[0] = '\0' - - # Phase A multi-pass retry: with several sources, main.vp may translate before its imports - # (filesystem order); failed files are retried next pass, converging in at most file_count passes. - done_flags: bytes = stdlib.malloc(file_count) - if done_flags is None: - VLogger.error("Phase A done_flags allocation failed", "PhaseA") - stdlib.free(entries) - return 1 - string.memset(done_flags, 0, file_count) - PHASE_A_MAX_PASSES: int = 32 - for pass_a in range(int(PHASE_A_MAX_PASSES)): - completed_this_pass: int = 0 - for i in range(int(file_count)): - if done_flags[i] != 0: continue - ea: uint64 = uint64(entries) + i * entry_size - ent: SrcFileEntry* = (SrcFileEntry*)(ea) - # Timing point 0: start processing this file (translate phase start) - t_pa0: uint64 = win32.win32base.GetTickCount64() - if ent is None or ent.Path is None: - done_flags[i] = 1 - completed_this_pass += 1 - continue - # Compute the package name (the directory part relative to source_dir) - src_dir_len_pa: sizet = string.strlen(source_dir) - pkg_pa: str = None - if string.strlen(ent.Path) > src_dir_len_pa + 1: - rel_path_pa: str = ent.Path + src_dir_len_pa + 1 - pkg_pa = GenApi.compute_package_from_relpath(mb, rel_path_pa) - tr_a: HandlesTranslator.Translator* = BuildPipeline.TranslateFileGetTrans(mb, ent.Path, ent.Sha1, pkg_pa, 0, temp_dir) - # Timing point 1: translate returned - t_pa1: uint64 = win32.win32base.GetTickCount64() - if tr_a is None: - # Translation failed (syntax/parse error): retry next pass; convergence check reports it. - # OOM cleanup: a failed translation also produced AST/tokens, freed immediately. - mb.reset_to(mark2) - continue - # Record this file's deps (for convergence diagnostics; stdlib table survives reset) - _PhaseADiagRecord(ent.Sha1, tr_a._imported_modules, None) - # Collect _imported_modules into app_deps_buf (for Phase B+ on-demand compilation). - # Dependencies were filled early during translation. - if tr_a._imported_modules is not None and app_deps_buf is not None: - # Dedupe per module: several App files importing the same module would otherwise inflate - # app_deps toward 64KB, filling the initial worklist and overflowing BFS. - im_len_a: sizet = string.strlen(tr_a._imported_modules) - im_pos_a: sizet = 0 - while im_pos_a < im_len_a: - im_start_a: sizet = im_pos_a - while im_pos_a < im_len_a and tr_a._imported_modules[im_pos_a] != ' ': im_pos_a += 1 - im_mod_len: sizet = im_pos_a - im_start_a - if im_pos_a < im_len_a: im_pos_a += 1 - if im_mod_len == 0: continue - im_mod_buf: bytes = stdlib.malloc(im_mod_len + 1) - if im_mod_buf is None: continue - string.strncpy(im_mod_buf, tr_a._imported_modules + im_start_a, im_mod_len) - im_mod_buf[im_mod_len] = '\0' - # Skip if already in app_deps (exact word-boundary match) - if app_deps_len > 0 and GenApi.is_module_imported(app_deps_buf, im_mod_buf) != 0: - stdlib.free(im_mod_buf) - continue - if app_deps_len + im_mod_len + 2 < APP_DEPS_BUF_SIZE: - if app_deps_len > 0: - app_deps_buf[app_deps_len] = ' ' - app_deps_len = app_deps_len + 1 - string.strcpy(app_deps_buf + app_deps_len, im_mod_buf) - app_deps_len = app_deps_len + im_mod_len - app_deps_buf[app_deps_len] = '\0' - stdlib.free(im_mod_buf) - # dump stub IR (declarations only) - stub_buf_a: bytes = stdlib.malloc(PHASE_A_IR_SIZE) - if stub_buf_a is None: VLogger.error("stub buffer allocation failed", "PhaseA") - tr_a.dump_ir(stub_buf_a, PHASE_A_IR_SIZE, mlirlite.OUTPUT_STUB) - stub_len_a: sizet = string.strlen(stub_buf_a) - - # Save stub.ll (sliced path: temp_dir/{sha1 prefix}/{sha1}.stub.ll) - stub_path_a: str = StubMergerCollect._sliced_path(temp_dir, td_len_pa, ent.Sha1, "stub.ll") - if stub_path_a is not None: - sf_a: io.File* = io.File(stub_path_a, io.MODE.W) - if not sf_a.closed: - sf_a.write(stub_buf_a, stub_len_a) - sf_a.close() - stdlib.free(stub_path_a) - stdlib.free(stub_buf_a) - - # dump text IR (definitions only) - text_buf_a: bytes = stdlib.malloc(PHASE_A_IR_SIZE) - if text_buf_a is None: VLogger.error("text buffer allocation failed", "PhaseA") - tr_a.dump_ir(text_buf_a, PHASE_A_IR_SIZE, mlirlite.OUTPUT_TEXT) - text_len_a: sizet = string.strlen(text_buf_a) - - # Save text.ll (sliced path) - text_path_a: str = StubMergerCollect._sliced_path(temp_dir, td_len_pa, ent.Sha1, "text.ll") - if text_path_a is not None: - tf_a: io.File* = io.File(text_path_a, io.MODE.W) - if not tf_a.closed: - tf_a.write(text_buf_a, text_len_a) - tf_a.close() - stdlib.free(text_path_a) - stdlib.free(text_buf_a) - - # Save {sha1}.meta (structured sidecar: function list / struct layout / vtable) - StubMergerIR.WriteModuleMeta(tr_a.Module, temp_dir, td_len_pa, ent.Sha1) - - # Save {sha1}.full.ll (in-memory FULL artifact: a self-contained single-module IR) - full_buf_a: bytes = stdlib.malloc(PHASE_A_FULL_SIZE) - if full_buf_a is None: VLogger.error("full.ll buffer allocation failed", "PhaseA") - tr_a.dump_ir(full_buf_a, PHASE_A_FULL_SIZE, mlirlite.OUTPUT_FULL) - full_len_a: sizet = string.strlen(full_buf_a) - full_path_a: str = StubMergerCollect._sliced_path(temp_dir, td_len_pa, ent.Sha1, "full.ll") - if full_path_a is not None: - ffa: io.File* = io.File(full_path_a, io.MODE.W) - if not ffa.closed: - ffa.write(full_buf_a, full_len_a) - ffa.close() - stdlib.free(full_path_a) - stdlib.free(full_buf_a) - - # --traceback: take this module's debug call-site records and write .vpcall. - if BuildPipeline._traceback_mode != 0: - vpcall_text_a: str = mlirlite.DebugTakeText() - if vpcall_text_a is not None: - BuildPipeline.WriteCallSidecar(temp_dir, ent.Sha1, vpcall_text_a) - stdlib.free(vpcall_text_a) - - # Save dependencies (_imported_modules) for Phase B (sliced path) - deps_path_a: str = StubMergerCollect._sliced_path(temp_dir, td_len_pa, ent.Sha1, "deps.txt") - if deps_path_a is not None: - df_a: io.File* = io.File(deps_path_a, io.MODE.W) - if not df_a.closed: - if tr_a._imported_modules is not None: - dl_a: sizet = string.strlen(tr_a._imported_modules) - df_a.write(tr_a._imported_modules, dl_a) - df_a.close() - stdlib.free(deps_path_a) - - # Timing point 2: stub+text dump and write done; print per-file progress (as in Phase1 1b) - t_pa2: uint64 = win32.win32base.GetTickCount64() - VLogger.info(f"[{i+1}/{file_count}] PhaseA: {ent.Path} (translate={t_pa1-t_pa0} ms, dump={t_pa2-t_pa1} ms, total={t_pa2-t_pa0} ms)", "PhaseA") - - done_flags[i] = 1 - completed_this_pass += 1 - # OOM cleanup: stub/text/deps are on disk, so free this file's transient translation data - # (AST/tokens/lexer/IR/Handler); app_deps_buf already deep-copied the deps. - mb.reset_to(mark2) - # Convergence check: stop when all files are done - all_done: int = 1 - for i_ck in range(int(file_count)): - if done_flags[i_ck] == 0: - all_done = 0 - break - if all_done != 0: break - if completed_this_pass == 0: - # No progress: remaining files cannot finish (real compile errors). Abort and list them, each - # with a dependency-graph dump. VLogger.error calls sys.exit(1), so it must come after all output. - td_len_fail: sizet = string.strlen(temp_dir) - for i_err in range(int(file_count)): - if done_flags[i_err] == 0: - ea_err: uint64 = uint64(entries) + i_err * entry_size - ent_err: SrcFileEntry* = (SrcFileEntry*)(ea_err) - if ent_err is not None and ent_err.Path is not None: - if ent_err.Sha1 is not None: _PhaseADumpFailGraph(temp_dir, td_len_fail, ent_err.Path, ent_err.Sha1) - stdlib.free(done_flags) - _PhaseADiagFree() - VLogger.error("Phase A multi-pass convergence failed, aborting compilation", "PhaseA") - stdlib.free(entries) - return 1 - stdlib.free(done_flags) - # Phase A converged: free the diagnostic table (needed only on the failure path) - _PhaseADiagFree() - - if do_phase2 == 0: - VLogger.info("Phase A complete (stub generation only)", "project") - if app_deps_buf is not None: stdlib.free(app_deps_buf) - stdlib.free(entries) - return 0 - - # === 3. Phase B: compile each file to .obj === - if do_phase2 != 0: - log.banner("Phase B: compile .obj") - - # Collect .obj paths (64KB to avoid includes .obj path overflow dropping link symbols). - # reachable_set/reachable_count: filled by Phase B+, used by Phase C for on-demand linking. - reachable_set: bytes = None - reachable_count: int = 0 - OBJ_PATHS_SIZE: sizet = 65536 - obj_paths: bytes = stdlib.malloc(OBJ_PATHS_SIZE) - if obj_paths is None: return 1 - obj_paths[0] = '\0' - obj_pos: sizet = 0 - # main_obj_path: the .obj that defines the user main (placed first at link time so the linker - # does not pick another module's wrapper main) - main_obj_path: bytes = stdlib.malloc(512) - if main_obj_path is None: return 1 - main_obj_path[0] = '\0' - compiled_count: int = 0 - - # IR buffer size (16MB, to hold the in-memory FULL artifact {sha1}.full.ll; FULL = stub + text) - COMBINED_IR_SIZE: sizet = 16777216 - - # llc task table (stdlib.malloc, outlives threads): a contiguous array of LlcTask records; the loop - # only writes .ll + builds tasks, then run_llc_parallel runs llc in parallel batches. - llc_tasks: bytes = stdlib.malloc(sizet(file_count) * BuildPipeline.LlcTask.__sizeof__()) - if llc_tasks is None: - VLogger.error("llc task table allocation failed", "PhaseB") - return 1 - llc_task_count: int = 0 - - for i in range(int(file_count)): - ea: uint64 = uint64(entries) + i * entry_size - ent: SrcFileEntry* = (SrcFileEntry*)(ea) - if ent is None or ent.Path is None or ent.Sha1 is None: continue - # Start of this file's three-step timing (GetTickCount64, ms) - t_pb0: uint64 = win32.win32base.GetTickCount64() - # Read the in-memory FULL artifact {sha1}.full.ll -> complete IR (replaces merging stub + dep stub/text) - combined_ir: bytes = stdlib.malloc(COMBINED_IR_SIZE) - if combined_ir is None: - VLogger.error(f"combined_ir allocation failed: {ent.Path}", "PhaseB") - continue - combined_len: sizet = StubMergerBuild.LoadFullIR(temp_dir, ent.Sha1, combined_ir, COMBINED_IR_SIZE) - if combined_len == 0: - VLogger.error(f"failed to read full.ll: {ent.Path}", "PhaseB") - stdlib.free(combined_ir) - continue - # Timing point 1: full.ll read done - t_pb1: uint64 = win32.win32base.GetTickCount64() - - # Write .ll (llc threads only read this file); llc itself runs parallel in batches after the loop - ret_w: int = BuildPipeline.write_ir_to_file(combined_ir, combined_len, temp_dir, ent.Sha1) - stdlib.free(combined_ir) - if ret_w != 0: - VLogger.error(f"failed to write .ll: {ent.Path}", "PhaseB") - sys.exit(1) - # Timing point 2: .ll write done - t_pb2: uint64 = win32.win32base.GetTickCount64() - - # Build the llc task (command string is stdlib.malloc'd so threads' lifetimes are not reset-reclaimed) - llc_ir_path: str = BuildPipeline.build_sliced_path(temp_dir, ent.Sha1, "ll") - if llc_ir_path is None: - VLogger.error("failed to build llc input path", "PhaseB") - sys.exit(1) - tslot: BuildPipeline.LlcTask* = BuildPipeline.LlcTask*(uint64(llc_tasks) + sizet(llc_task_count) * BuildPipeline.LlcTask.__sizeof__()) - if BuildPipeline.init_llc_task(tslot, llc_ir_path, output_dir, ent.Sha1, cc_cmd, cc_flags) != 0: - VLogger.error(f"failed to build llc task: {ent.Path}", "PhaseB") - sys.exit(1) - # Timing point 3: llc task construction done - t_pb3: uint64 = win32.win32base.GetTickCount64() - llc_task_count += 1 - - compiled_count += 1 - # Show files submitted to llc (with the three timings: merge stub / write .ll / build task) - VLogger.info(f"[{i+1}/{file_count}] llc submitted: {ent.Path} (merge={t_pb1-t_pb0} ms, write={t_pb2-t_pb1} ms, task={t_pb3-t_pb2} ms, total={t_pb3-t_pb0} ms)", "PhaseB") - # Build the .obj path and detect the main module (test_main.vp or main.vp; .py no longer recognized) - od_len: sizet = string.strlen(output_dir) - is_main_mod: int = 0 - if string.strstr(ent.Path, "test_main.vp") is not None: is_main_mod = 1 - elif string.strstr(ent.Path, "main.vp") is not None: is_main_mod = 1 - - # Sliced path: output_dir/{sha1 prefix}/{sha1}.obj - obj_path_sliced: str = StubMergerCollect._sliced_path(output_dir, od_len, ent.Sha1, "obj") - if obj_path_sliced is not None: - op_sliced_len: sizet = string.strlen(obj_path_sliced) - if is_main_mod != 0: - # main module: copy to main_obj_path (so main is first at link time) - if op_sliced_len < 512: string.strcpy(main_obj_path, obj_path_sliced) - else: VLogger.error("main_obj_path buffer too small, aborting compilation", "PhaseB") - else: - # Other modules: append to obj_paths - if obj_pos + op_sliced_len + 2 < OBJ_PATHS_SIZE: - if obj_pos > 0: - obj_paths[obj_pos] = ' ' - obj_pos += 1 - string.strcpy(obj_paths + obj_pos, obj_path_sliced) - obj_pos += op_sliced_len - obj_paths[obj_pos] = '\0' - else: VLogger.error(".obj path buffer too small", "PhaseB") - stdlib.free(obj_path_sliced) - - # Run all llc jobs in parallel (batched at LLC_MAX_THREADS threads) - ret_p: int = BuildPipeline.run_llc_parallel(llc_tasks, llc_task_count, BuildPipeline.LLC_MAX_THREADS) - if ret_p != 0: - VLogger.error("llc compilation failed, aborting compilation", "PhaseB") - sys.exit(1) - - # Free the llc tasks (command strings + records) - ti: int = 0 - while ti < llc_task_count: - tfree: BuildPipeline.LlcTask* = BuildPipeline.LlcTask*(uint64(llc_tasks) + sizet(ti) * BuildPipeline.LlcTask.__sizeof__()) - BuildPipeline.free_llc_task(tfree) - ti += 1 - stdlib.free(llc_tasks) - - # Assemble .s files: llvm-mc -> .obj (isr.s etc., bare symbols matching the .vp ExportT/State stubs). - # The obj is added to obj_paths for Phase C linking. - od_len_as: sizet = string.strlen(output_dir) - asm_compiled: int = 0 - for ai_as in range(int(asm_file_count)): - ea_as: uint64 = uint64(asm_entries) + ai_as * entry_size - ent_as: SrcFileEntry* = (SrcFileEntry*)(ea_as) - if ent_as is None or ent_as.Path is None or ent_as.Sha1 is None: continue - aret_as: int = BuildPipeline.asm_to_obj(ent_as.Path, ent_as.Sha1, output_dir, Config.TargetTriple) - if aret_as != 0: - VLogger.error(f"llvm-mc assembly failed, aborting compilation: {ent_as.Path}", "PhaseB") - sys.exit(1) - obj_path_asm: str = StubMergerCollect._sliced_path(output_dir, od_len_as, ent_as.Sha1, "obj") - if obj_path_asm is not None: - op_len_asm: sizet = string.strlen(obj_path_asm) - if obj_pos + op_len_asm + 2 < OBJ_PATHS_SIZE: - if obj_pos > 0: - obj_paths[obj_pos] = ' ' - obj_pos += 1 - string.strcpy(obj_paths + obj_pos, obj_path_asm) - obj_pos += op_len_asm - obj_paths[obj_pos] = '\0' - stdlib.free(obj_path_asm) - asm_compiled += 1 - VLogger.info(f"[asm {asm_compiled}] assembly complete: {ent_as.Path}", "PhaseB") - - VLogger.success(f"compilation complete: {compiled_count}/{file_count}", "PhaseB") - - if compiled_count == 0: - VLogger.error("no files compiled successfully", "PhaseB") - stdlib.free(entries) - stdlib.free(asm_entries) - return 1 - - # === 3.5 Compile missing includes (on demand via the dependency graph) === - # Only compile includes reached directly/indirectly from App sources (via _BuildReachableSha1Set). - if includes_dir is not None and includes_binary_dir is not None: - inc_compiled: int = 0 - td_len_mi: sizet = string.strlen(temp_dir) - - # Diagnostic log: confirm the Phase B+ entry and Sha1Store state - VLogger.info(f"PhaseB+ entry: store_count={StubMergerStore.GetSha1StoreCount()} includes_binary_dir={includes_binary_dir}", "PhaseB+") - - # Create includes_binary_dir (so .obj files can be written and collect_obj_files can scan them) - BuildPipeline.ensure_dir(includes_binary_dir) - - # declare_only preprocessing was moved before Phase A (Phase A-pre-inc) so includes structs are - # registered when Phase A translates App sources. PopulateIncludesDeps is filled there too. - - # Build the reachable SHA1 set (dependency graph traversal). - # When app_deps_buf is None, fall back to scanning source_dir (non-recursive). - REACHABLE_SET_SIZE: sizet = sizet(StubMerger.MAX_INCLUDES_SHA1) * 17 - reachable_set: bytes = stdlib.malloc(REACHABLE_SET_SIZE) - reachable_count: int = 0 - if reachable_set is not None: - string.memset(reachable_set, 0, REACHABLE_SET_SIZE) - reachable_count = StubMergerBuild._BuildReachableSha1Set(mb, source_dir, temp_dir, reachable_set, app_deps_buf, includes_dir) - VLogger.info(f"reachable includes SHA1: {reachable_count} entries", "PhaseB+") - # fast-fail: reachable-set construction failed (deps not found); never fall back, abort - if reachable_count < 0: - VLogger.error("reachable set construction failed (fast-fail), aborting compilation", "PhaseB+") - return 1 - else: - VLogger.error("reachable_set allocation failed, aborting compilation", "PhaseB+") - return 1 - - # Free app_deps_buf (reachable_set is built, no longer needed) - if app_deps_buf is not None: - stdlib.free(app_deps_buf) - app_deps_buf = None - - # Get the SHA1/module-name arrays from the global store as local copies (bytes) to avoid address - # issues from passing the store's union return (bytes*) across functions; rel/incdir keep pointers. - td_len_pb: sizet = string.strlen(temp_dir) - store_sha1_arr_mi: bytes = stdlib.malloc(StubMerger.MAX_INCLUDES * 17) - store_mod_arr_mi: bytes = stdlib.malloc(sizet(StubMerger.MAX_INCLUDES) * 64) - store_count_mi: int = 0 - if store_sha1_arr_mi is not None and store_mod_arr_mi is not None: - store_count_mi = StubMergerIR._BuildIncludesSha1Map(temp_dir, td_len_pb, store_sha1_arr_mi, store_mod_arr_mi) - store_rel_arr_mi: bytes = StubMergerStore.GetSha1StoreRelArrPtr() - store_incdir_arr_mi: bytes = StubMergerStore.GetSha1StoreIncdirArrPtr() - # 诊断/失败前置:store 为空或拓扑为空时,下面的 include 编译循环会整体跳过, - # 但 PhaseC 仍会把这些 include 的 .obj 塞进链接命令 → 只看到一串 - # "no such file or directory: includes.binary/xx/xxxx.obj"。这里直接给出根因。 - if store_count_mi <= 0: - VLogger.error(f"PhaseB+: Sha1Store is empty (store_count=0) but {reachable_count} include(s) are reachable; the include compile loop would be skipped and every include .obj would be missing at link time (check the Phase1 dependency scan / cache flags)", "PhaseB+") - if store_sha1_arr_mi is not None: stdlib.free(store_sha1_arr_mi) - if store_mod_arr_mi is not None: stdlib.free(store_mod_arr_mi) - return 1 - if store_count_mi > 0: - - # Invalidate cached include objects when the include set or the compiler build changed: - # an .obj embeds the sha1-prefixed symbols of every module it calls, so an object built - # against an older dependency set must never be reused (stale call targets / link errors). - if StubMergerBuild.EnsureObjCacheStamp(includes_binary_dir, store_sha1_arr_mi, store_incdir_arr_mi, store_count_mi) == 0: - VLogger.warning("include object-cache stamp could not be written; cached objects may be reused", "PhaseB+") - - # Topologically sort reachable includes: dependencies are fully translated first. Cross-module - # vtable generation needs dependency functions' TypeStr registered globally (e.g. astaux/base AST). - topo_idx: sizet* = (sizet*)(stdlib.malloc(8 * (store_count_mi + 1))) - topo_count: int = 0 - if topo_idx is None: - VLogger.error("topological sort index allocation failed, aborting compilation", "PhaseB+") - topo_count = 0 - else: - topo_count = StubMergerTopo.BuildTopoSortedIndices(store_count_mi, store_sha1_arr_mi, store_mod_arr_mi, reachable_set, reachable_count, topo_idx) - # 拓扑为空 ⇒ 下面的编译循环整体不执行,但 PhaseC 仍会引用这些 .obj → 链接必然失败。 - # 在这里给根因,而不是让人去看一串 "no such file or directory"。 - if topo_count <= 0: - VLogger.error(f"PhaseB+: topological sort produced 0 entries (store_count={store_count_mi}, reachable={reachable_count}); include .obj files would be missing at link time", "PhaseB+") - return 1 - - for si_mi in range(int(topo_count)): - # Current entry (topological index; reachable modules only) - ti: int = int(topo_idx[si_mi]) - # Get the current entry's SHA1 and rel_path - inc_sha1_mi: str = store_sha1_arr_mi + sizet(ti) * 17 - inc_rel_mi: str = store_rel_arr_mi + sizet(ti) * StubMerger.MAX_REL_PATH_LEN - # Skip App sources: an App file may be scanned as an include and be reachable; compiling - # another .obj into includes_binary would duplicate the output/ .obj's symbols. - app_skip_mi: int = 0 - for ai_mi in range(int(file_count)): - ea_ap: uint64 = uint64(entries) + ai_mi * entry_size - ent_ap: SrcFileEntry* = (SrcFileEntry*)(ea_ap) - if ent_ap is not None and ent_ap.Sha1 is not None: - if string.strcmp(ent_ap.Sha1, inc_sha1_mi) == 0: - app_skip_mi = 1 - break - if app_skip_mi != 0: continue - # Check whether the .obj already exists in includes.binary (sliced subdir) - ibd_len_mi: sizet = string.strlen(includes_binary_dir) - check_pat_mi: str = StubMergerCollect._sliced_path(includes_binary_dir, ibd_len_mi, inc_sha1_mi, "obj") - check_fd_mi: win32.win32file.WIN32_FIND_DATAA* = stdlib.malloc(win32.win32file.WIN32_FIND_DATAA.__sizeof__()) - obj_exists_mi: int = 0 - if check_pat_mi is not None and check_fd_mi is not None: - string.memset(check_fd_mi, 0, win32.win32file.WIN32_FIND_DATAA.__sizeof__()) - check_h_mi: win32.win32base.HANDLE = win32.win32file.FindFirstFileA(check_pat_mi, check_fd_mi) - if check_h_mi != win32.win32base.INVALID_HANDLE_VALUE: - win32.win32file.FindClose(check_h_mi) - obj_exists_mi = 1 - if obj_exists_mi != 0: - if check_pat_mi is not None: stdlib.free(check_pat_mi) - stdlib.free(check_fd_mi) - continue - - # The .obj does not exist; compile it. - # Build the source path: {includes root}/{rel_path} (per-entry IncDir; empty falls back to includes_dir) - rel_path_len_mi: sizet = string.strlen(inc_rel_mi) - incdir_mi: str = store_incdir_arr_mi + sizet(ti) * StubMerger.MAX_INC_DIR_LEN - if incdir_mi is None or incdir_mi[0] == '\0': incdir_mi = includes_dir - inc_dir_len_mi: sizet = string.strlen(incdir_mi) - src_fp_mi: bytes = stdlib.malloc(inc_dir_len_mi + 1 + rel_path_len_mi + 1) - if src_fp_mi is None: - stdlib.free(check_pat_mi) - if check_fd_mi is not None: stdlib.free(check_fd_mi) - VLogger.error("includes source path allocation failed, aborting compilation", "PhaseB+") - viperlib.snprintf(src_fp_mi, inc_dir_len_mi + 1 + rel_path_len_mi + 1, "%s/%s", incdir_mi, inc_rel_mi) - - # Check whether it is a declaration-only file (read {sha1}.meta: no FUNC ... DEF means pure declaration) - is_decl_mi: int = StubMergerIR.IsDeclOnlyModule(temp_dir, td_len_mi, inc_sha1_mi) - - # is_decl_mi: -1 = no meta (Phase1 did not translate it), 1 = declaration-only, 0 = impl. - # Skip only declaration-only (is_decl_mi == 1); -1 and 0 continue to the translation logic. - if is_decl_mi == 1: - stdlib.free(check_pat_mi) - if check_fd_mi is not None: stdlib.free(check_fd_mi) - stdlib.free(src_fp_mi) - continue - - # Try to read the in-memory FULL artifact {sha1}.full.ll (should be produced by Phase1/PhaseA) - inc_combined_mi: bytes = stdlib.malloc(COMBINED_IR_SIZE) - inc_combined_len_mi: sizet = 0 - if inc_combined_mi is not None: inc_combined_len_mi = StubMergerBuild.LoadFullIR(temp_dir, inc_sha1_mi, inc_combined_mi, COMBINED_IR_SIZE) - - # If full.ll is missing, translate the source, save stub/text/meta/full, then retry - if inc_combined_len_mi == 0 and inc_combined_mi is not None: - # Compute the includes file's package name (the directory part relative to includes_dir) - inc_pkg_mi: str = None - if string.strlen(src_fp_mi) > inc_dir_len_mi + 1: - inc_rel_mi2: str = src_fp_mi + inc_dir_len_mi + 1 - inc_pkg_mi = GenApi.compute_package_from_relpath(mb, inc_rel_mi2) - tr_mi: HandlesTranslator.Translator* = BuildPipeline.TranslateFileGetTrans(mb, src_fp_mi, inc_sha1_mi, inc_pkg_mi) - if tr_mi is not None: - # PBP IR buffer: same as PHASE_A_IR_SIZE (parser.vp's text.ll >1MB truncation root cause) - PBP_IR_SIZE: sizet = 8388608 - # FULL dump = stub + text, with 2x headroom - PBP_FULL_SIZE: sizet = 16777216 - # Save stub.ll (sliced path) - inc_stub_buf: bytes = stdlib.malloc(PBP_IR_SIZE) - if inc_stub_buf is not None: - tr_mi.dump_ir(inc_stub_buf, PBP_IR_SIZE, mlirlite.OUTPUT_STUB) - inc_stub_len: sizet = string.strlen(inc_stub_buf) - inc_stub_path: str = StubMergerCollect._sliced_path(temp_dir, td_len_mi, inc_sha1_mi, "stub.ll") - if inc_stub_path is not None: - isf: io.File* = io.File(inc_stub_path, io.MODE.W) - if not isf.closed: - isf.write(inc_stub_buf, inc_stub_len) - isf.close() - stdlib.free(inc_stub_path) - stdlib.free(inc_stub_buf) - # Save text.ll (sliced path) - inc_text_buf: bytes = stdlib.malloc(PBP_IR_SIZE) - if inc_text_buf is not None: - tr_mi.dump_ir(inc_text_buf, PBP_IR_SIZE, mlirlite.OUTPUT_TEXT) - inc_text_len: sizet = string.strlen(inc_text_buf) - inc_text_path: str = StubMergerCollect._sliced_path(temp_dir, td_len_mi, inc_sha1_mi, "text.ll") - if inc_text_path is not None: - itf: io.File* = io.File(inc_text_path, io.MODE.W) - if not itf.closed: - itf.write(inc_text_buf, inc_text_len) - itf.close() - stdlib.free(inc_text_path) - stdlib.free(inc_text_buf) - # Save {sha1}.meta (structured sidecar: function list / struct layout / vtable) - StubMergerIR.WriteModuleMeta(tr_mi.Module, temp_dir, td_len_mi, inc_sha1_mi) - # Save {sha1}.full.ll (in-memory FULL artifact) - inc_full_buf: bytes = stdlib.malloc(PBP_FULL_SIZE) - if inc_full_buf is None: VLogger.error("full.ll buffer allocation failed", "PhaseB+") - tr_mi.dump_ir(inc_full_buf, PBP_FULL_SIZE, mlirlite.OUTPUT_FULL) - inc_full_len: sizet = string.strlen(inc_full_buf) - inc_full_path: str = StubMergerCollect._sliced_path(temp_dir, td_len_mi, inc_sha1_mi, "full.ll") - if inc_full_path is not None: - ifff: io.File* = io.File(inc_full_path, io.MODE.W) - if not ifff.closed: - ifff.write(inc_full_buf, inc_full_len) - ifff.close() - stdlib.free(inc_full_path) - stdlib.free(inc_full_buf) - # --traceback: take this module's debug call-site records and write .vpcall. - if BuildPipeline._traceback_mode != 0: - vpcall_text_mi: str = mlirlite.DebugTakeText() - if vpcall_text_mi is not None: - BuildPipeline.WriteCallSidecar(temp_dir, inc_sha1_mi, vpcall_text_mi) - stdlib.free(vpcall_text_mi) - # Retry reading full.ll - inc_combined_len_mi = StubMergerBuild.LoadFullIR(temp_dir, inc_sha1_mi, inc_combined_mi, COMBINED_IR_SIZE) - if tr_mi is None: - VLogger.error(f"include translation returned no module: {src_fp_mi} (sha1={inc_sha1_mi})", "PhaseB+") - # OOM cleanup: stub/text/meta/full are on disk, free this includes file's transient - # translation data (AST/tokens/lexer/IR/Handler); reads only touch files. - mb.reset_to(mark2) - - if inc_combined_len_mi == 0: - # Diagnostic: check whether full.ll/stub.ll/text.ll exist - diag_full: str = StubMergerCollect._sliced_path(temp_dir, td_len_mi, inc_sha1_mi, "full.ll") - diag_stub: str = StubMergerCollect._sliced_path(temp_dir, td_len_mi, inc_sha1_mi, "stub.ll") - diag_text: str = StubMergerCollect._sliced_path(temp_dir, td_len_mi, inc_sha1_mi, "text.ll") - diag_full_ex: int = 0 - diag_stub_ex: int = 0 - diag_text_ex: int = 0 - if diag_full is not None: - df_diag_f: io.File* = io.File(diag_full, io.MODE.R) - if not df_diag_f.closed: - diag_full_ex = 1 - df_diag_f.close() - stdlib.free(diag_full) - if diag_stub is not None: - df_diag_s: io.File* = io.File(diag_stub, io.MODE.R) - if not df_diag_s.closed: - diag_stub_ex = 1 - df_diag_s.close() - stdlib.free(diag_stub) - if diag_text is not None: - df_diag_t: io.File* = io.File(diag_text, io.MODE.R) - if not df_diag_t.closed: - diag_text_ex = 1 - df_diag_t.close() - stdlib.free(diag_text) - # fail-fast:该 include 既没有 full.ll、翻译也没产出 → 它的 .obj 必然缺失, - # 继续下去只会得到一串链接器 "no such file or directory"。这里直接停下并指出文件。 - VLogger.error(f"failed to read full.ll: {src_fp_mi} (full={diag_full_ex} stub={diag_stub_ex} text={diag_text_ex} is_decl={is_decl_mi}); aborting so the include .obj is not silently missing at link time", "PhaseB+") - if inc_combined_mi is not None: stdlib.free(inc_combined_mi) - stdlib.free(check_pat_mi) - if check_fd_mi is not None: stdlib.free(check_fd_mi) - stdlib.free(src_fp_mi) - return 1 - - # Compile to .obj (output to includes_binary_dir, consistent with the existence check and link collection) - inc_cret_mi: int = BuildPipeline.compile_module_to_obj(inc_combined_mi, inc_combined_len_mi, temp_dir, includes_binary_dir, inc_sha1_mi, cc_cmd, cc_flags) - stdlib.free(inc_combined_mi) - - if inc_cret_mi != 0: - VLogger.error(f"llc compilation failed, aborting compilation: {src_fp_mi}", "PhaseB+") - sys.exit(1) - - inc_compiled += 1 - # Show the compiled includes file - VLogger.info(f"[includes {inc_compiled}] compilation complete: {src_fp_mi}", "PhaseB+") - - # Add to obj_paths (sliced path, taken from includes_binary_dir) - ibd_len_mi2: sizet = string.strlen(includes_binary_dir) - inc_obj_sliced: str = StubMergerCollect._sliced_path(includes_binary_dir, ibd_len_mi2, inc_sha1_mi, "obj") - - if inc_obj_sliced is not None: - inc_obj_len: sizet = string.strlen(inc_obj_sliced) - if obj_pos + inc_obj_len + 2 < OBJ_PATHS_SIZE: - if obj_pos > 0: - obj_paths[obj_pos] = ' ' - obj_pos += 1 - string.strcpy(obj_paths + obj_pos, inc_obj_sliced) - obj_pos += inc_obj_len - obj_paths[obj_pos] = '\0' - else: VLogger.error("includes .obj path buffer overflow, aborting compilation", "PhaseB+") - stdlib.free(inc_obj_sliced) - - # Free this iteration's temporary memory - if check_pat_mi is not None: stdlib.free(check_pat_mi) - - if check_fd_mi is not None: stdlib.free(check_fd_mi) - - stdlib.free(src_fp_mi) - - # Free the topological indices and local map copies - if topo_idx is not None: stdlib.free(topo_idx) - if store_sha1_arr_mi is not None: stdlib.free(store_sha1_arr_mi) - if store_mod_arr_mi is not None: stdlib.free(store_mod_arr_mi) - - VLogger.info(f"compiled missing includes: {inc_compiled} entries", "PhaseB+") - - # Free app_deps_buf (fallback when the Phase B+ block did not run) - if app_deps_buf is not None: - stdlib.free(app_deps_buf) - app_deps_buf = None - - # === 4. Phase C: link all .obj -> .exe === - log.banner("Phase C: link") - - od_len2: sizet = string.strlen(output_dir) - lo_len: sizet = string.strlen(linker_output) - exe_path: bytes = stdlib.malloc(od_len2 + lo_len + 2) - if exe_path is not None: viperlib.snprintf(exe_path, od_len2 + lo_len + 2, "%s/%s", output_dir, linker_output) - else: - exe_path = linker_output - VLogger.error("exe_path allocation failed, aborting compilation", "PhaseC") - - # Build the final .obj path list: main_obj_path first, reachable .objs after - final_obj_paths: bytes = stdlib.malloc(OBJ_PATHS_SIZE + 512) - if final_obj_paths is None: - stdlib.free(entries) - return 1 - final_obj_paths[0] = '\0' - fop_pos: sizet = 0 - # main_obj_path goes first (so the linker prefers the user main) - if main_obj_path[0] != '\0': - mlen: sizet = string.strlen(main_obj_path) - string.strcpy(final_obj_paths, main_obj_path) - fop_pos = mlen - - # Merge the App non-main modules' .obj (obj_paths, space-separated) compiled by Phase B/B+. - # final_obj_paths must include main_obj + reachable includes + App deps, else linking hits undefined symbols. - if obj_pos > 0: - if fop_pos + obj_pos + 1 < OBJ_PATHS_SIZE + 512: - if fop_pos > 0: - final_obj_paths[fop_pos] = ' ' - fop_pos += 1 - string.strcpy(final_obj_paths + fop_pos, obj_paths) - fop_pos += obj_pos - final_obj_paths[fop_pos] = '\0' - - # Link by graph: iterate reachable_set and collect only reachable includes' .obj, - # no longer collecting every .obj under includes.binary. - if reachable_set is not None and reachable_count > 0: - ibd_len_pc: sizet = string.strlen(includes_binary_dir) - td_len_pc: sizet = string.strlen(temp_dir) - ri_pc: int - for ri_pc in range(int(reachable_count)): - rsha1_pc: str = reachable_set + sizet(ri_pc) * 17 - if rsha1_pc[0] == '\0': continue - # Skip App sources: their .obj is already in final_obj_paths; collecting a same-sha1 .obj from includes_binary would duplicate symbols. - app_skip_pc: int = 0 - for ai_pc in range(int(file_count)): - ea_pc: uint64 = uint64(entries) + ai_pc * entry_size - ent_pc: SrcFileEntry* = (SrcFileEntry*)(ea_pc) - if ent_pc is not None and ent_pc.Sha1 is not None: - if string.strcmp(ent_pc.Sha1, rsha1_pc) == 0: - app_skip_pc = 1 - break - if app_skip_pc != 0: continue - obj_path_pc: str = StubMergerCollect._sliced_path(includes_binary_dir, ibd_len_pc, rsha1_pc, "obj") - if obj_path_pc is not None: - # Check whether the .obj file exists - cf_pc: io.File* = io.File(obj_path_pc, io.MODE.R) - if not cf_pc.closed: - cf_pc.close() - # The .obj exists; append to final_obj_paths - op_len_pc: sizet = string.strlen(obj_path_pc) - if fop_pos + op_len_pc + 2 < OBJ_PATHS_SIZE + 512: - if fop_pos > 0: - final_obj_paths[fop_pos] = ' ' - fop_pos += 1 - string.strcpy(final_obj_paths + fop_pos, obj_path_pc) - fop_pos += op_len_pc - final_obj_paths[fop_pos] = '\0' - else: - # .obj 缺失:声明-only 模块本来就不需要 .obj,静默跳过;其余情况无论是否 - # --phase2command,链接都会因缺文件失败 —— 直接报根因并中止,不要拖到链接器。 - if StubMergerIR.IsDeclOnlyModule(temp_dir, td_len_pc, rsha1_pc) != 1: - VLogger.error(f"reachable module .obj missing: {obj_path_pc} (not declaration-only); aborting before link", "PhaseC") - stdlib.free(obj_path_pc) - return 1 - stdlib.free(obj_path_pc) - - final_obj_paths[fop_pos] = '\0' - obj_paths_len: sizet = fop_pos - - # Free reachable_set (Phase C is done with it) - if reachable_set is not None: - stdlib.free(reachable_set) - reachable_set = None - - # Link by graph: pass None for includes_binary_dir to avoid collecting everything - lret: int = BuildPipeline.link_objs_to_exe(final_obj_paths, obj_paths_len, linker_cmd, linker_flags, exe_path, None, linker_static_libs, linker_dynamic_libs) - - if lret == 0: - VLogger.success(f"output: {exe_path}", "PhaseC") - # Deploy declared runtime deps (project.vpj runtime.*) next to the exe; fail-fast on error. - # Skipped under --phase2command (the exe is not linked yet). - if BuildPipeline._phase2_cmd_path is None: - if BuildPipeline.deploy_runtime_deps(exe_path) != 0: - VLogger.error("runtime dependency deployment failed, aborting compilation", "runtime") - stdlib.free(entries) - stdlib.free(asm_entries) - return 1 - # --traceback: export .stackmap (skipped under --phase2command, - # where nothing was actually linked). - if BuildPipeline._traceback_mode != 0 and BuildPipeline._phase2_cmd_path is None: BuildPipeline.WriteStackMap(mb, exe_path) - # --run / viperbuild build(run=True[, args=[...]]): run the linked exe after linking. - # viperbuild 的 args 已在 Pass 0 拼好(含按需加引号),这里只负责拼命令行。 - vb_run_wanted: int = 0 - vb_run_args: str = None - if ViperBuild.IsApplied() != 0: - vb_run_wanted = ViperBuild.RunAfterBuild() - vb_run_args = ViperBuild.RunArgs() - run_wanted: bool = args.get_bool("run") or vb_run_wanted != 0 - if run_wanted and BuildPipeline._phase2_cmd_path is not None: - # phase2command mode: the exe is not linked yet (the command is in the bat); skip --run - VLogger.info("phase2command mode: skipping --run (exe not linked yet)", "phase2cmd") - elif run_wanted: - run_cmd_len: sizet = len(exe_path) + 8 - if vb_run_args is not None: run_cmd_len += len(vb_run_args) + 1 - run_cmd: bytes = stdlib.malloc(run_cmd_len) - if run_cmd is not None: - if vb_run_args is not None and string.strlen(vb_run_args) > 0: - viperlib.snprintf(run_cmd, run_cmd_len, "\"%s\" %s", exe_path, vb_run_args) - else: - viperlib.snprintf(run_cmd, run_cmd_len, "\"%s\"", exe_path) - VLogger.info(f"run: {run_cmd}", "run") - rp: subprocess.CompletedProcess* = subprocess.run(run_cmd, False, False) - if rp is not None: VLogger.info(f"exit code: {rp.returncode}", "run") - stdlib.free(run_cmd) - else: - VLogger.error("link failed", "PhaseC") - stdlib.free(entries) - stdlib.free(asm_entries) - return 1 - - stdlib.free(entries) - stdlib.free(asm_entries) - return 0 +import lib.core.Gen.GenApi as GenApi +from stdint import * +import stdio +import string +import stdlib +import memhub +import viperlib +import io +import os +import sys +import ast +import mlirlite +import subprocess +import argparse +import lib.core.VLogger as VLogger +import lib.core.Gen.Translator as HandlesTranslator +import lib.Phase.BuildPipeline as BuildPipeline +import lib.core.IncludesScanner as IncludesScanner +import lib.core.CompilerFactory as CompilerFactory +import lib.core.builtin.ViperBuild as ViperBuild +import lib.Stub.StubMerger as StubMerger +import lib.Projectrans.Utils as Utils +import lib.Projectrans.Config as Config +import lib.Stub.StubMerger.StubMergerBuild as StubMergerBuild +import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect +import lib.Stub.StubMerger.StubMergerIR as StubMergerIR +import lib.Stub.StubMerger.StubMergerStore as StubMergerStore +import lib.Stub.StubMerger.StubMergerTopo as StubMergerTopo +import lib.Stub.StubGen.Converter as StubConverter +import lib.Stub.StubGen.VpiReader as VpiReader +import lib.Stub.StubGen.VpiStub as VpiStub + +# Global mbuddy pointer +_mbuddy: memhub.MemBuddy* + +# Source buffer size (1MB) +define SRC_BUF_SIZE = 1048576 + +# Max source files (recursive scanning grows the count; too small truncates the list -> undefined symbol) +define MAX_SRC_FILES = 128 + +novirtual class SrcFileEntry: + """Source file entry""" + Path: str # full path + Sha1: str # first 16 chars of SHA1 + +# _ScanDirForSources - recursively collect .vp or .s files into entries. +# want_asm=0: .vp sources, want_asm=1: .s assembly sources. +def _ScanDirForSources(mb: memhub.MemBuddy*, dir_path: str, entries: SrcFileEntry*, entry_size: sizet, file_count: int, max_files: int, want_asm: int) -> int: + if dir_path is None or entries is None: return file_count + if file_count >= max_files: return file_count + + reader: os.DirReader* = os.scandir(dir_path) + if reader is None: return file_count + entry: os.DirEntry = os.DirEntry() + + while True: + rr: int = os.readdir(reader, &entry) + if rr == 0: break + if rr < 0: break + fname: str = entry.name + fname_len: sizet = string.strlen(fname) + if fname_len <= 0: continue + if entry.is_dir: + sub_dir: bytes = Utils.JoinPath(dir_path, fname) + if sub_dir is not None: + file_count = _ScanDirForSources(mb, sub_dir, entries, entry_size, file_count, max_files, want_asm) + stdlib.free(sub_dir) + continue + + is_match: int = 0 + if want_asm == 0: + if fname_len > 3 and Utils.is_vp_at(fname, 0, fname_len) != 0: is_match = 1 + else: + if fname_len > 2 and fname[fname_len - 2] == '.' and fname[fname_len - 1] == 's': is_match = 1 + if is_match != 0 and file_count < max_files: + full_path: bytes = Utils.JoinPath(dir_path, fname) + if full_path is not None: + if want_asm == 0: + sbuf: bytes = Utils.read_file_to_buf(full_path, SRC_BUF_SIZE) + if sbuf is None: continue + sha1_val: str = IncludesScanner.compute_file_sha1(mb, full_path) + if sha1_val is not None: + ea: uint64 = uint64(entries) + file_count * entry_size + ent: SrcFileEntry* = (SrcFileEntry*)(ea) + if ent is not None: + ent.Path = full_path + ent.Sha1 = sha1_val + file_count += 1 + VLogger.info(f"{fname} (sha1={sha1_val})", "project") + stdlib.free(sbuf) + else: + sha1_val_a: str = IncludesScanner.compute_file_sha1(mb, full_path) + if sha1_val_a is not None: + ea_a: uint64 = uint64(entries) + file_count * entry_size + ent_a: SrcFileEntry* = (SrcFileEntry*)(ea_a) + if ent_a is not None: + ent_a.Path = full_path + ent_a.Sha1 = sha1_val_a + file_count += 1 + VLogger.info(f"{fname} (asm sha1={sha1_val_a})", "project") + + os.closedir(reader) + return file_count + + +# _ScanDirForPyFiles - collect Viper source files. +def _ScanDirForPyFiles(mb: memhub.MemBuddy*, dir_path: str, entries: SrcFileEntry*, entry_size: sizet, file_count: int, max_files: int) -> int: + """Recursively scan a directory and collect .vp files into entries.""" + return _ScanDirForSources(mb, dir_path, entries, entry_size, file_count, max_files, 0) + + +# _ScanDirForAsmFiles - collect assembly sources. +def _ScanDirForAsmFiles(mb: memhub.MemBuddy*, dir_path: str, entries: SrcFileEntry*, entry_size: sizet, file_count: int, max_files: int) -> int: + """Recursively scan a directory and collect .s files into entries.""" + return _ScanDirForSources(mb, dir_path, entries, entry_size, file_count, max_files, 1) + + +# _PreRegisterAppGenericTemplates - pre-register App generic class templates before mark2. +# The generic tables live in the pool; registering after mark2 would be reclaimed by reset_to, corrupting later passes. +def _PreRegisterAppGenericTemplates(mb: memhub.MemBuddy*, entries: SrcFileEntry*, file_count: int, entry_size: sizet) -> int: + """Pre-register App source generic class templates before mark2, return the processed count""" + if mb is None or entries is None or file_count <= 0: return 0 + handled: int = 0 + for i in range(int(file_count)): + ea_pa: uint64 = uint64(entries) + i * entry_size + ent_pa: SrcFileEntry* = (SrcFileEntry*)(ea_pa) + if ent_pa is None or ent_pa.Path is None or ent_pa.Sha1 is None: continue + # Read the file + f_pa: io.File* = io.File(ent_pa.Path, io.MODE.R) + if f_pa.closed: continue + # read_all's capacity includes the terminator (reads at most cap-1 bytes); longer files are truncated + if f_pa.size() >= SRC_BUF_SIZE: + VLogger.error(f"source file exceeds {SRC_BUF_SIZE} byte limit, refusing to read: {ent_pa.Path}", "PhaseA") + f_pa.close() + continue + src_buf_pa: bytes = stdlib.malloc(SRC_BUF_SIZE) + if src_buf_pa is None: + f_pa.close() + continue + bytes_read_pa: LONG = f_pa.read_all(src_buf_pa, SRC_BUF_SIZE) + f_pa.close() + if bytes_read_pa <= 0: + stdlib.free(src_buf_pa) + continue + if bytes_read_pa < SRC_BUF_SIZE: src_buf_pa[bytes_read_pa] = 0 + # Parse the AST only (perm pool keeps template nodes alive across the mark2 reset) + perm_pa: memhub.MemBuddy* = CompilerFactory.factory.PermPool + if perm_pa is None: perm_pa = mb + lx_pa: ast.Lexer* = ast.new_lexer(perm_pa) + if lx_pa is None: + stdlib.free(src_buf_pa) + continue + ast._lexer_init(lx_pa, src_buf_pa, perm_pa) + tokens_pa: ast.Token* = ast.tokenize(lx_pa) + tree_pa: ast.AST* = ast.parse_tokens(perm_pa, tokens_pa) + if tree_pa is None: + stdlib.free(src_buf_pa) + continue + # Walk top-level ClassDef/FunctionDef and register generic templates (type_params != None) + ch_pa: list[ast.AST*]* = tree_pa.children + if ch_pa is not None: + cn_pa: sizet = len(ch_pa) + for ci_pa in range(int(cn_pa)): + child_pa: ast.AST* = ch_pa.get(ci_pa) + if child_pa is None: continue + ck_pa: int = child_pa.kind() + if ck_pa == ast.ASTKind.ClassDef: + cd_pa: ast.ClassDef* = ast.ClassDef*(child_pa) + if cd_pa is None or cd_pa.name is None: continue + tp_pa: list[str]* = cd_pa.type_params + if tp_pa is None: continue + GenApi.set_current_file_type(ent_pa.Path) + GenApi._register_generic_template(perm_pa, cd_pa, ent_pa.Sha1) + handled += 1 + elif ck_pa == ast.ASTKind.FunctionDef: + fd_pa: ast.FunctionDef* = ast.FunctionDef*(child_pa) + if fd_pa is None or fd_pa.name is None: continue + tf_pa: list[str]* = fd_pa.type_params + if tf_pa is None: continue + GenApi.set_current_file_type(ent_pa.Path) + GenApi._register_generic_func_template(perm_pa, fd_pa, ent_pa.Sha1) + handled += 1 + stdlib.free(src_buf_pa) + if handled > 0: VLogger.info(f"pre-registered {handled} App generic templates before mark2 (classes + functions)", "PhaseA") + return handled + + +# Phase A convergence-failure dependency-graph diagnostics. +# Each file's deps and missing details are persisted to a stdlib table (survives reset) and dumped on failure. +PA_DEPS_MAX_STR: sizet = 1024 +PA_MISS_MAX_STR: sizet = 768 +PA_GRAPH_DEPTH_MAX: int = 3 # missing-chain drill-down depth (counts from layer 1) +PA_GRAPH_OK_PROBE: int = 2 # ready nodes are probed 1 more level (to find hidden misses) +_pa_diag_max: sizet = 0 +_pa_diag_count: int = 0 +_pa_diag_sha1: bytes = None +_pa_diag_deps: bytes = None +_pa_diag_miss: bytes = None + + +def _PhaseADiagInit(max_count: sizet) -> None: + """Allocate the persistent Phase A convergence-failure diagnostic table""" + global _pa_diag_max, _pa_diag_count, _pa_diag_sha1, _pa_diag_deps, _pa_diag_miss + _pa_diag_max = max_count + _pa_diag_count = 0 + _pa_diag_sha1 = stdlib.malloc(max_count * 17) + _pa_diag_deps = stdlib.malloc(max_count * PA_DEPS_MAX_STR) + _pa_diag_miss = stdlib.malloc(max_count * PA_MISS_MAX_STR) + if _pa_diag_sha1 is not None: string.memset(_pa_diag_sha1, 0, max_count * 17) + if _pa_diag_deps is not None: string.memset(_pa_diag_deps, 0, max_count * PA_DEPS_MAX_STR) + if _pa_diag_miss is not None: string.memset(_pa_diag_miss, 0, max_count * PA_MISS_MAX_STR) + + +def _PhaseADiagFree() -> None: + """Free the persistent diagnostic table""" + global _pa_diag_max, _pa_diag_count, _pa_diag_sha1, _pa_diag_deps, _pa_diag_miss + if _pa_diag_sha1 is not None: + stdlib.free(_pa_diag_sha1) + _pa_diag_sha1 = None + if _pa_diag_deps is not None: + stdlib.free(_pa_diag_deps) + _pa_diag_deps = None + if _pa_diag_miss is not None: + stdlib.free(_pa_diag_miss) + _pa_diag_miss = None + _pa_diag_count = 0 + _pa_diag_max = 0 + + +def _PhaseADiagRecord(sha1: str, deps: str, miss: str) -> None: + """Record or update one file's diagnostics: sha1 to deps plus missing detail""" + global _pa_diag_count + if _pa_diag_sha1 is None or _pa_diag_deps is None or sha1 is None: return + if string.strlen(sha1) < 16: return + for i in range(int(_pa_diag_count)): + es: bytes = _pa_diag_sha1 + sizet(i) * 17 + if string.strcmp(es, sha1) == 0: + if miss is not None and _pa_diag_miss is not None: + maddr: bytes = _pa_diag_miss + sizet(i) * PA_MISS_MAX_STR + string.strncpy(maddr, miss, PA_MISS_MAX_STR - 1) + maddr[PA_MISS_MAX_STR - 1] = 0 + return + if _pa_diag_count >= _pa_diag_max: return + saddr: bytes = _pa_diag_sha1 + sizet(_pa_diag_count) * 17 + string.strncpy(saddr, sha1, 16) + saddr[16] = 0 + daddr: bytes = _pa_diag_deps + sizet(_pa_diag_count) * PA_DEPS_MAX_STR + if deps is not None: string.strncpy(daddr, deps, PA_DEPS_MAX_STR - 1) + daddr[PA_DEPS_MAX_STR - 1] = 0 + if miss is not None and _pa_diag_miss is not None: + maddr2: bytes = _pa_diag_miss + sizet(_pa_diag_count) * PA_MISS_MAX_STR + string.strncpy(maddr2, miss, PA_MISS_MAX_STR - 1) + maddr2[PA_MISS_MAX_STR - 1] = 0 + _pa_diag_count += 1 + + +def _PhaseADiagDepsLookup(sha1: str) -> str: + """Look up the persisted deps string by sha1; caller frees the copy, None if absent""" + if _pa_diag_sha1 is None or _pa_diag_deps is None or sha1 is None: return None + for i in range(int(_pa_diag_count)): + es: bytes = _pa_diag_sha1 + sizet(i) * 17 + if string.strcmp(es, sha1) == 0: + src: bytes = _pa_diag_deps + sizet(i) * PA_DEPS_MAX_STR + ln: sizet = string.strlen(src) + out: bytes = stdlib.malloc(ln + 1) + if out is None: return None + string.strcpy(out, src) + return out + return None + + +def _PhaseADiagMissLookup(sha1: str) -> str: + """Look up the persisted missing detail by sha1; caller frees the copy, None if absent""" + if _pa_diag_sha1 is None or _pa_diag_miss is None or sha1 is None: return None + for i in range(int(_pa_diag_count)): + es: bytes = _pa_diag_sha1 + sizet(i) * 17 + if string.strcmp(es, sha1) == 0: + src: bytes = _pa_diag_miss + sizet(i) * PA_MISS_MAX_STR + ln: sizet = string.strlen(src) + if ln == 0: return None + out: bytes = stdlib.malloc(ln + 1) + if out is None: return None + string.strcpy(out, src) + return out + return None + + +def _PhaseATextLLExists(temp_dir: str, td_len: sizet, sha1: str) -> int: + """Probe whether {temp_dir}/{sha1 first 2 chars}/{sha1}.text.ll exists, 1=yes""" + if temp_dir is None or td_len == 0 or sha1 is None or string.strlen(sha1) < 2: return 0 + tpath: str = StubMergerCollect._sliced_path(temp_dir, td_len, sha1, "text.ll") + if tpath is None: return 0 + tf: io.File* = io.File(tpath, io.MODE.R) + exists: int = 0 + if not tf.closed: + tf.close() + exists = 1 + stdlib.free(tpath) + return exists + + +def _PhaseAResolveDepSha1(mod_name: str) -> str: + """Resolve a dependency module name to SHA1 (exact then suffix), None if absent""" + if mod_name is None: return None + s1: str = StubMergerStore.LookupSha1ByModName(mod_name) + if s1 is not None: return s1 + return StubMergerStore.LookupSha1ByModNameSuffix(mod_name) + + +def _PhaseADumpNode(temp_dir: str, td_len: sizet, sha1: str, depth: int, prefix: bytes) -> None: + """Recursively print the dependency subgraph; [MISS]=missing text.ll, [OK]=ready""" + deps: bytes = None + if sha1 is not None: deps = _PhaseADiagDepsLookup(sha1) + if deps is None and sha1 is not None: + # Fallback: read {temp_dir}/{prefix}/{sha1}.deps.txt (Phase1 already wrote it for includes) + dpath: str = StubMergerCollect._sliced_path(temp_dir, td_len, sha1, "deps.txt") + if dpath is not None: + df: io.File* = io.File(dpath, io.MODE.R) + if not df.closed: + deps = stdlib.malloc(PA_DEPS_MAX_STR) + if deps is not None: + rl: int64 = df.read_all(deps, PA_DEPS_MAX_STR) + if rl < 0: rl = 0 + # read_all's size arg is the buffer capacity (incl. terminator): it reads at most MAX-1 + deps[rl] = 0 + df.close() + stdlib.free(dpath) + if deps is None or deps[0] == 0: + if deps is not None: stdlib.free(deps) + return + dl: sizet = string.strlen(deps) + token_count: int = 0 + pos: sizet = 0 + while pos < dl: + while pos < dl and deps[pos] == ' ': pos += 1 + if pos >= dl: break + token_count += 1 + while pos < dl and deps[pos] != ' ': pos += 1 + pos = 0 + idx: int = 0 + while pos < dl: + while pos < dl and deps[pos] == ' ': pos += 1 + if pos >= dl: break + ts: sizet = pos + while pos < dl and deps[pos] != ' ': pos += 1 + tl: sizet = pos - ts + nm: bytes = stdlib.malloc(tl + 1) + if nm is not None: + string.strncpy(nm, deps + ts, tl) + nm[tl] = 0 + is_last: int = 0 + if idx + 1 >= token_count: is_last = 1 + dep_sha1: str = _PhaseAResolveDepSha1(nm) + txt_ok: int = 0 + if dep_sha1 is not None: txt_ok = _PhaseATextLLExists(temp_dir, td_len, dep_sha1) + # Branch characters are inlined (don't return string literals: VPC would return a dangling + # stack pointer for a literal -> printf %s crashes and the graph output is swallowed) + br_1: str = "|-- " + br_2: str = "`-- " + if dep_sha1 is None: + if is_last != 0: stdio.printf("%s%s%s (unresolved)\n", prefix, br_2, nm) + else: stdio.printf("%s%s%s (unresolved)\n", prefix, br_1, nm) + elif txt_ok != 0: + if is_last != 0: stdio.printf("%s%s%s [OK]\n", prefix, br_2, nm) + else: stdio.printf("%s%s%s [OK]\n", prefix, br_1, nm) + else: + if is_last != 0: stdio.printf("%s%s%s [MISS] text.ll=%s\n", prefix, br_2, nm, dep_sha1) + else: stdio.printf("%s%s%s [MISS] text.ll=%s\n", prefix, br_1, nm, dep_sha1) + # Drill down: MISS follows the whole chain (to PA_GRAPH_DEPTH_MAX); OK probes 1 level only + can_expand: int = 0 + if dep_sha1 is not None: + if txt_ok == 0: + if depth + 1 < PA_GRAPH_DEPTH_MAX: can_expand = 1 + else: + if depth + 1 < PA_GRAPH_OK_PROBE: can_expand = 1 + if can_expand != 0: + # +5: strcat appends "| "/" " (4 chars) + NUL; strlen+4 would overflow 1 byte and + # corrupt heap metadata, crashing later malloc/free and aborting the graph output + npl: sizet = string.strlen(prefix) + 5 + new_prefix: bytes = stdlib.malloc(npl) + if new_prefix is not None: + string.strcpy(new_prefix, prefix) + if is_last != 0: string.strcat(new_prefix, " ") + else: string.strcat(new_prefix, "| ") + _PhaseADumpNode(temp_dir, td_len, dep_sha1, depth + 1, new_prefix) + stdlib.free(new_prefix) + stdlib.free(nm) + idx += 1 + stdlib.free(deps) + + +def _PhaseADumpFailGraph(temp_dir: str, td_len: sizet, path: str, sha1: str) -> None: + """Convergence-failure diagnostics: pending detail plus dependency tree. + Flush after every printf so the trace survives a later crash.""" + stdio.printf("[PhaseA-Fail] ==============================================================\n") + stdio.fflush(None) + stdio.printf("[PhaseA-Fail] FAIL TO TRANSLATE: %s [sha1=%s]\n", path, sha1) + stdio.fflush(None) + miss: bytes = _PhaseADiagMissLookup(sha1) + if miss is not None: + if miss[0] != 0: stdio.printf("[PhaseA-Fail] pending reason: %s\n", miss) + stdlib.free(miss) + stdio.fflush(None) + stdio.printf("[PhaseA-Fail] dep graph ([MISS]=missing text.ll, [OK]=ready, depth<=%d):\n", PA_GRAPH_DEPTH_MAX) + stdio.fflush(None) + root_prefix: bytes = stdlib.malloc(1) + if root_prefix is not None: + root_prefix[0] = 0 + _PhaseADumpNode(temp_dir, td_len, sha1, 0, root_prefix) + stdlib.free(root_prefix) + stdio.fflush(None) + + +def RunMultiFileProject(mb: memhub.MemBuddy*, source_dir: str, temp_dir: str, output_dir: str, cc_cmd: str, cc_flags: str, linker_cmd: str, linker_flags: str, linker_output: str, includes_binary_dir: str, includes_dir: str, do_phase1: int, do_phase2: int, log: VLogger.Logger*, args: argparse.ParsedArgs*, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> int: + """Compile a multi-file project; return 0 on success, non-zero on failure""" + if source_dir is None: + VLogger.error("source_dir is empty", "project") + return 1 + + VLogger.info(f"multi-file project compilation: {source_dir}", "project") + + # === 1. Recursively scan .vp/.py files under source_dir (including subdirectories) === + entry_size: sizet = SrcFileEntry.__sizeof__() + entries: SrcFileEntry* = stdlib.malloc(MAX_SRC_FILES * entry_size) + if entries is None: return 1 + string.memset(entries, 0, MAX_SRC_FILES * entry_size) + + file_count: int = _ScanDirForPyFiles(mb, source_dir, entries, entry_size, 0, MAX_SRC_FILES) + VLogger.info(f"{file_count} source files in total", "project") + + # Scan .s assembly files (separate array; not translated in Phase A, assembled by llvm-mc after Phase B) + asm_entries: SrcFileEntry* = stdlib.malloc(MAX_SRC_FILES * entry_size) + if asm_entries is None: + stdlib.free(entries) + return 1 + string.memset(asm_entries, 0, MAX_SRC_FILES * entry_size) + asm_file_count: int = _ScanDirForAsmFiles(mb, source_dir, asm_entries, entry_size, 0, MAX_SRC_FILES) + if asm_file_count > 0: VLogger.info(f"{asm_file_count} assembly files (assembled by llvm-mc and linked)", "project") + + if file_count == 0: + stdlib.free(entries) + stdlib.free(asm_entries) + return 1 + + # Initialize the AST tables + ast._init_tables(mb) + + # Pre-register App generic class templates before mark2: the tables/nodes are pool-allocated, so they + # must be registered before mark2 to survive reset_to(mark2); otherwise later passes hit dangling memory. + _PreRegisterAppGenericTemplates(mb, entries, file_count, entry_size) + + # OOM cleanup (mirrors Phase1's mark/reset pattern): without reclamation the AST/IR of all includes + # pile up in the pool. mark2 lets each file's transient data be freed by reset_to after translation. + mark2: sizet = mb.mark() + # struct/enum entries registered after mark2 use stdlib allocation (survive reset, as in Phase1) + GenApi.set_stdlib_alloc(1) + + # Ensure the temp/output directories exist + BuildPipeline.ensure_dir(temp_dir) + BuildPipeline.ensure_dir(output_dir) + + # === 0.5 Fill Sha1Store on demand from the dependency graph (removes the Phase1 dependency) === + # If Sha1Store is empty, scan the dependency graph (App-first BFS) so later "compile missing includes" works. + if includes_dir is not None: + if StubMergerStore.GetSha1StoreCount() == 0: + rs_05: bytes = stdlib.malloc(StubMerger.MAX_INCLUDES_SHA1 * 17) + if rs_05 is None: VLogger.error("reachable_set allocation failed, cannot fill Sha1Store", "project") + else: + string.memset(rs_05, 0, StubMerger.MAX_INCLUDES_SHA1 * 17) + rc_05: int = StubMergerBuild._BuildReachableSha1Set(mb, source_dir, temp_dir, rs_05, None, includes_dir) + if rc_05 < 0: VLogger.error("dependency graph scan failed, cannot fill Sha1Store", "project") + elif rc_05 > 0: VLogger.info(f"Phase2 dependency graph filled Sha1Store on demand: {rc_05} entries", "project") + stdlib.free(rs_05) + + # Append App source SHA1s to _sha1_map.txt (human-readable only; format {sha1}:{rel_path}) and to the + # in-memory store (AppendToSha1MapStore) for cross-module DefineT lookups. + src_dir_len_am: sizet = string.strlen(source_dir) + # _sha1_map.txt 落在构建目录(temp 的上一级),方便直接按 SHA1 反查源文件 + map_path_am: bytes = StubMergerCollect.Sha1MapPath(mb, temp_dir) + if map_path_am is not None: + mf: io.File* = io.File(map_path_am, io.MODE.A) + line_am: bytes = stdlib.malloc(512) + # First fill the in-memory store (no file I/O), then write _sha1_map.txt + app_filled: int = 0 + for i in range(int(file_count)): + ea_am: uint64 = uint64(entries) + i * entry_size + ent_am: SrcFileEntry* = (SrcFileEntry*)(ea_am) + if ent_am is None or ent_am.Path is None or ent_am.Sha1 is None: continue + # Compute the relative path (strip the source_dir prefix, keep the directory structure) + p_str: str = ent_am.Path + p_len: sizet = string.strlen(p_str) + rel_path_am: str = p_str + if p_len > src_dir_len_am + 1: + if string.strncmp(p_str, source_dir, src_dir_len_am) == 0: rel_path_am = p_str + src_dir_len_am + 1 + # Also append to the in-memory store (for cross-module DefineT lookups) + # rel_path_am looks like "lib/core/StubMerger.vp", matching _sha1_map.txt + if StubMergerStore.AppendToSha1MapStore(ent_am.Sha1, rel_path_am) != 0: VLogger.error(f"AppendToSha1MapStore failed: {rel_path_am}", "project") + app_filled += 1 + # Write _sha1_map.txt (human-readable output) + if not mf.closed and line_am is not None: + viperlib.snprintf(line_am, 512, "%s:%s\n", ent_am.Sha1, rel_path_am) + ll_am: sizet = string.strlen(line_am) + mf.write(line_am, ll_am) + if line_am is not None: stdlib.free(line_am) + if not mf.closed: + mf.close() + VLogger.info(f"appended {app_filled} App files to _sha1_map.txt and the in-memory store", "project") + stdlib.free(map_path_am) + + # The module SHA1 map is filled into the StubMerger global store by PopulateSha1MapStore + + # AppendToSha1MapStore; HandlesExprCall._lookup_module_sha1 queries it directly, no local copy needed. + + + # === Phase A-pre-vpi: pre-generate .vpi stubs for all App sources === + # Deps' .vpi must exist before their dependents translate; pre-generating avoids missing dep declarations. + td_len_vpa: sizet = string.strlen(temp_dir) + # Reuse BuildPipeline's DefineT (proven to be non-zero: the same buffer size generates ViperBuild.vp's + # vpi in Phase A). A large literal stored in a local used to come through as 0 here, and + # _GeneratePyiFromAst then returns immediately (buf_size == 0) -> every App .vpi silently missing. + PYI_BUF_SIZE_P2: sizet = BuildPipeline.PYI_BUF_SIZE + vpi_failed_p2: int = 0 + vpi_written_p2: int = 0 + vpi_skipped_p2: int = 0 + for i_vp2 in range(int(file_count)): + ea_vp2: uint64 = uint64(entries) + i_vp2 * entry_size + ent_vp2: SrcFileEntry* = (SrcFileEntry*)(ea_vp2) + if ent_vp2 is None or ent_vp2.Path is None or ent_vp2.Sha1 is None: + vpi_failed_p2 += 1 + VLogger.error("[Phase A-pre-vpi] invalid source entry (Path/Sha1 missing), skipping", "PhaseA") + continue + # Skip if the vpi already exists (sha1 is content-based, so a same-named vpi is valid; cross-run cache) + vpi_path_vp2: str = StubMergerCollect._sliced_path(temp_dir, td_len_vpa, ent_vp2.Sha1, "vpi") + if vpi_path_vp2 is None: + vpi_failed_p2 += 1 + VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | failed to build vpi path", "PhaseA") + continue + vf_vp2: io.File* = io.File(vpi_path_vp2, io.MODE.R) + if not vf_vp2.closed: + vf_vp2.close() + VpiStub.Ensure(vpi_path_vp2, ent_vp2.Sha1) + vpi_skipped_p2 += 1 + stdlib.free(vpi_path_vp2) + continue + stdlib.free(vpi_path_vp2) + # Read the source -> parse the AST -> generate the vpi (rel_path = ent_vp2.Path: vpi content does + # not depend on it; it is only used for fatal_error attribution, pointing at this source file) + f_vp2: io.File* = io.File(ent_vp2.Path, io.MODE.R) + if f_vp2.closed: + vpi_failed_p2 += 1 + VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | cannot open source file", "PhaseA") + continue + # read_all's capacity includes the terminator (reads at most cap-1 bytes); longer files are truncated + if f_vp2.size() >= SRC_BUF_SIZE: + vpi_failed_p2 += 1 + VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | source file exceeds {SRC_BUF_SIZE} byte limit", "PhaseA") + f_vp2.close() + continue + src_buf_vp2: bytes = stdlib.malloc(SRC_BUF_SIZE) + if src_buf_vp2 is None: + f_vp2.close() + vpi_failed_p2 += 1 + VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | source buffer allocation failed (OOM)", "PhaseA") + continue + bytes_read_vp2: LONG = f_vp2.read_all(src_buf_vp2, SRC_BUF_SIZE) + f_vp2.close() + if bytes_read_vp2 <= 0: + stdlib.free(src_buf_vp2) + vpi_failed_p2 += 1 + VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | file read failed or empty", "PhaseA") + continue + if bytes_read_vp2 < SRC_BUF_SIZE: src_buf_vp2[bytes_read_vp2] = 0 + lx_vp2: ast.Lexer* = ast.new_lexer(mb) + if lx_vp2 is None: + stdlib.free(src_buf_vp2) + vpi_failed_p2 += 1 + VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | lexer allocation failed (OOM)", "PhaseA") + continue + ast._lexer_init(lx_vp2, src_buf_vp2, mb) + tokens_vp2: ast.Token* = ast.tokenize(lx_vp2) + tree_vp2: ast.AST* = ast.parse_tokens(mb, tokens_vp2) + if tree_vp2 is None: + stdlib.free(src_buf_vp2) + vpi_failed_p2 += 1 + # Parse failure: inline the line/column + reason in the log line (same source as the full + # Python-style traceback below), since "AST parse failed" alone gives no location or cause. + VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | AST parse failed (line {ast.get_parse_err_line()} col {ast.get_parse_err_col()}: {ast.get_parse_err_msg()})", "PhaseA") + GenApi.dump_parse_traceback(ent_vp2.Path) + continue + pyi_buf_vp2: bytes = stdlib.malloc(PYI_BUF_SIZE_P2) + if pyi_buf_vp2 is not None: + # 文件级名字窗口(FileFuncNames 等)由 _GeneratePyiFromAst 入口统一 reset+重建, + # 不再在此处重复调用;契约见 StructGenMixin.reset_file_name_window。 + pyi_pos_vp2: sizet = StubConverter._GeneratePyiFromAst(mb, tree_vp2, ent_vp2.Path, pyi_buf_vp2, PYI_BUF_SIZE_P2) + if pyi_pos_vp2 == 0: + # The generator always emits at least a newline once buf_size > 1; 0 means the + # buffer size collapsed (or the generator failed) and no .vpi would be produced — + # every later cross-module lookup for this module would then fail. + vpi_failed_p2 += 1 + VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | vpi generation produced nothing (buf_size={PYI_BUF_SIZE_P2})", "PhaseA") + if pyi_pos_vp2 > 0: + pyi_path_vp2: str = StubMergerCollect._sliced_path(temp_dir, td_len_vpa, ent_vp2.Sha1, "vpi") + if pyi_path_vp2 is not None: + pf_vp2: io.File* = io.File(pyi_path_vp2, io.MODE.W) + if pf_vp2.closed: + # Silent here used to mean "no App .vpi at all" and every cross-module + # return-type lookup later failed. Report the exact path instead. + vpi_failed_p2 += 1 + VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | cannot open the vpi for writing: {pyi_path_vp2}", "PhaseA") + pf_vp2.write(pyi_buf_vp2, pyi_pos_vp2) + pf_vp2.close() + VpiStub.Emit(pyi_path_vp2, pyi_buf_vp2, pyi_pos_vp2, ent_vp2.Sha1) + vpi_written_p2 += 1 + stdlib.free(pyi_path_vp2) + stdlib.free(pyi_buf_vp2) + else: + vpi_failed_p2 += 1 + VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | pyi buffer allocation failed (OOM)", "PhaseA") + stdlib.free(src_buf_vp2) + # OOM cleanup: free this file's AST (the vpi is already on disk; persistent data is stdlib-allocated) + mb.reset_to(mark2) + VLogger.info(f"[Phase A-pre-vpi] generated={vpi_written_p2} skipped_existing={vpi_skipped_p2} failed={vpi_failed_p2} total={file_count}", "PhaseA") + if vpi_failed_p2 > 0: + VLogger.error(f"Phase A-pre-vpi: {vpi_failed_p2} files failed vpi pre-generation (see the per-file [Phase A-pre-vpi] logs above for failing files and causes)", "PhaseA") + return 1 + + + # === 2. Phase A: generate stub + text for each file === + # app_deps_buf collects App files' direct deps (space-separated) as Phase B+'s worklist (65536, matching StubMerger). + APP_DEPS_BUF_SIZE: sizet = 65536 + app_deps_buf: bytes = None + app_deps_len: sizet = 0 + if do_phase1 != 0: + log.banner("Phase A: generate stub + text") + # Initialize the Phase A convergence-failure diagnostic table (stdlib, survives reset) + _PhaseADiagInit(sizet(file_count) + StubMerger.MAX_INCLUDES) + + # 枚举全量预扫(最早的一次):所有已落盘 .vpi 里的 `enum X:` 先登记进 EnumTable, + # 再进入任何预注册/翻译。否则依赖 stub 里的 `types.FRESULT` 会在枚举模块的 vpi + # 被注入之前被当成类名 ensure_forward_class,占位结构体常驻进程级 StructTable, + # 之后所有模块的 `FRESULT` 都解析成空结构体(llvm.call 返回 + # !llvm.struct<"...FRESULT", ()>,函数本体却是 i32 -> mlir-opt: llvm.return 类型不匹配)。 + preenum_early: int = VpiReader.PreRegisterVpiEnums(mb, temp_dir) + VLogger.info(f"enum pre-scan (early): {preenum_early} enums registered", "PhaseA") + + # === Phase A-pre-inc: pre-register includes structs + fill deps === + # Must run before App pre-registration (multi-pass resolves cross-file inheritance) and fills PopulateIncludesDeps. + if includes_dir is not None and includes_binary_dir is not None: + store_count_inc_pre: int = StubMergerStore.GetSha1StoreCount() + if store_count_inc_pre > 0: + store_sha1_inc_pre: bytes = StubMergerStore.GetSha1StoreArrPtr() + store_rel_inc_pre: bytes = StubMergerStore.GetSha1StoreRelArrPtr() + store_incdir_inc_pre: bytes = StubMergerStore.GetSha1StoreIncdirArrPtr() + deps_filled_pre: int = 0 + PHASE_A_INC_MAX_PASSES: int = 3 + # Track includes that fail pre-registration: after all passes, any failure aborts the build + # (no silent downgrade). App files must be skipped here; Phase A-pre handles them. + inc_fail_flags: bytes = stdlib.malloc(store_count_inc_pre) + if inc_fail_flags is not None: string.memset(inc_fail_flags, 1, store_count_inc_pre) + for pass_inc_i in range(int(PHASE_A_INC_MAX_PASSES)): + struct_count_before_inc: int = GenApi.get_struct_count() + for si_inc in range(int(store_count_inc_pre)): + inc_sha1_pre: str = store_sha1_inc_pre + sizet(si_inc) * 17 + inc_rel_pre: str = store_rel_inc_pre + sizet(si_inc) * StubMerger.MAX_REL_PATH_LEN + if inc_rel_pre is None or inc_rel_pre[0] == '\0': continue + # Skip App source files (main.vp etc.): pre-registered by Phase A-pre, not a failure + is_app_file_inc: int = 0 + for i_app_inc in range(int(file_count)): + ea_app_inc: uint64 = uint64(entries) + i_app_inc * entry_size + ent_app_inc: SrcFileEntry* = (SrcFileEntry*)(ea_app_inc) + if ent_app_inc is not None and ent_app_inc.Sha1 is not None and string.strcmp(ent_app_inc.Sha1, inc_sha1_pre) == 0: + is_app_file_inc = 1 + break + if is_app_file_inc != 0 and inc_fail_flags is not None: + inc_fail_flags[si_inc] = 0 + continue + rel_len_pre: sizet = string.strlen(inc_rel_pre) + # Empty IncDir marks an App source (AppendToSha1MapStore / _BuildReachableSha1Set): + # its rel path is source_dir-relative, so resolving it against includes_dir opens a + # non-existent file and the translation fails with no diagnostic. App declarations + # are pre-registered by Phase A-pre below (against source_dir) — skip them here. + incdir_pre: str = store_incdir_inc_pre + sizet(si_inc) * StubMerger.MAX_INC_DIR_LEN + if incdir_pre is None or incdir_pre[0] == '\0': + if inc_fail_flags is not None: inc_fail_flags[si_inc] = 0 + continue + incdir_len_pre: sizet = string.strlen(incdir_pre) + full_path_pre: bytes = stdlib.malloc(incdir_len_pre + rel_len_pre + 2) + if full_path_pre is None: continue + viperlib.snprintf(full_path_pre, incdir_len_pre + rel_len_pre + 2, "%s/%s", incdir_pre, inc_rel_pre) + pkg_inc_pre: str = GenApi.compute_package_from_relpath(mb, inc_rel_pre) + # Pass temp_dir so the include's .vpi is materialized here: Phase A resolves + # cross-module method return types (and escape contracts) from the dependency's + # .vpi, and in a build without Phase1 artifacts none exists yet — the lookup then + # fails hard ("method return type cannot be obtained precisely"). + tr_inc_pre: HandlesTranslator.Translator* = BuildPipeline.TranslateFileGetTrans(mb, full_path_pre, inc_sha1_pre, pkg_inc_pre, 1, temp_dir) + stdlib.free(full_path_pre) + if tr_inc_pre is not None: + # Record this file's deps (for convergence diagnostics; stdlib table survives reset) + _PhaseADiagRecord(inc_sha1_pre, tr_inc_pre._imported_modules, None) + # Pre-registration succeeded: clear the failure flag + if inc_fail_flags is not None: inc_fail_flags[si_inc] = 0 + # Fill deps only on the first pass (PopulateIncludesDeps dedupes internally). + # Must be called before reset_to(mark2) since it deep-copies into stdlib. + if pass_inc_i == 0 and tr_inc_pre._imported_modules is not None and StubMergerStore.PopulateIncludesDeps(inc_sha1_pre, tr_inc_pre._imported_modules) == 0: + deps_filled_pre = deps_filled_pre + 1 + # OOM cleanup: free this includes file's transient pre-registration data + # (AST/tokens/lexer/IR/Handler); stdlib-allocated struct entries are unaffected + mb.reset_to(mark2) + struct_count_after_inc: int = GenApi.get_struct_count() + # Convergence check: no new struct registered this pass -> all classes are registered + if struct_count_after_inc == struct_count_before_inc: break + # Includes still failing after all passes -> abort (no downgrade) + if inc_fail_flags is not None: + for si_inc_f in range(int(store_count_inc_pre)): + if inc_fail_flags[si_inc_f] != 0: + rel_f: str = store_rel_inc_pre + sizet(si_inc_f) * StubMerger.MAX_REL_PATH_LEN + VLogger.error(f"includes pre-registration failed: {rel_f} (file cannot be translated; possible syntax/parse/compile error), aborting compilation", "prePhaseA") + stdlib.free(inc_fail_flags) + VLogger.info(f"includes pre-registration + dependency fill: {deps_filled_pre}/{store_count_inc_pre}", "prePhaseA") + + # === Phase A-pre: pre-register struct/enum/union for all sources === + # Registers declarations only (declare_only=1), no method bodies; multi-pass resolves cross-file ordering. + VLogger.info("pre-registering struct/enum/union...", "PhaseA") + src_dir_len_pre: sizet = string.strlen(source_dir) + PHASE_A_PRE_MAX_PASSES: int = 3 + for pass_i in range(int(PHASE_A_PRE_MAX_PASSES)): + struct_count_before: int = GenApi.get_struct_count() + for i in range(int(file_count)): + ea_pre: uint64 = uint64(entries) + i * entry_size + ent_pre: SrcFileEntry* = (SrcFileEntry*)(ea_pre) + if ent_pre is None or ent_pre.Path is None: continue + pkg_pre: str = None + if string.strlen(ent_pre.Path) > src_dir_len_pre + 1: + rel_path_pre: str = ent_pre.Path + src_dir_len_pre + 1 + pkg_pre = GenApi.compute_package_from_relpath(mb, rel_path_pre) + # App pre-registration passes pre_allow_dep_inject=1: imports and dep vpi injection happen at + # declaration time so field annotations share Phase A's context (else struct layouts diverge). + # temp_dir is passed so every App module's .vpi is materialized here (right after its AST is + # parsed), before Phase A translates anything: a module referenced by an earlier file must + # have its declarations on disk, else the call site cannot resolve its return type. + tr_pre: HandlesTranslator.Translator* = BuildPipeline.TranslateFileGetTrans(mb, ent_pre.Path, ent_pre.Sha1, pkg_pre, 1, temp_dir, 1) + if tr_pre is None: VLogger.error(f"pre-registration failed: {ent_pre.Path}", "PhaseA") + # OOM cleanup: free this App file's transient pre-registration data + mb.reset_to(mark2) + struct_count_after: int = GenApi.get_struct_count() + # Convergence check: no new struct registered this pass -> all classes are registered + if struct_count_after == struct_count_before: break + VLogger.info("pre-registration complete", "PhaseA") + + # 缺省参数全量预扫(TPC 式):所有模块 .vpi 里带缺省值的函数/方法一次性登记进全局 + # defaults 表,避免“谁先翻译谁登记”的时序依赖。stub-only 依赖(如包 mlirlite 的 + # __builder.vp,只产 .vpi 不产 .ll)走不到 FuncGenDef,缺了这一步跨模块少传实参时 + # 会补不出缺省值,直接发出操作数不足的 llvm.call(mlir-opt: incorrect number of + # operands;现场 build_load / build_store)。 + predef_count: int = VpiReader.PreRegisterVpiDefaults(mb, temp_dir) + VLogger.info(f"default-argument pre-scan: {predef_count} functions registered", "PhaseA") + + # 枚举全量预扫(与缺省值同理):所有模块 .vpi 里的 `enum X:` 一次性登记进 EnumTable, + # 消除"谁先注入谁登记"的时序依赖。否则依赖 stub 里的 `types.FRESULT` 会在枚举模块 + # 的 vpi 被注入之前解析,被当成类名 ensure_forward_class;占位结构体常驻进程级 + # StructTable,之后所有模块的 `FRESULT` 都变成空结构体(llvm.call 返回 + # !llvm.struct<"...FRESULT", ()>,而函数本体是 i32 -> mlir-opt: llvm.return 类型不匹配)。 + preenum_count: int = VpiReader.PreRegisterVpiEnums(mb, temp_dir) + VLogger.info(f"enum pre-scan: {preenum_count} enums registered", "PhaseA") + + # Phase A IR buffer: 1MB is not enough, ast/parser.vp's text.ll exceeds 1MB and got truncated -> + # merged IR lost the tail (opt: "end of file in global variable name"). + PHASE_A_IR_SIZE: sizet = 8388608 + # FULL dump = stub + text (same module), with 2x headroom + PHASE_A_FULL_SIZE: sizet = 16777216 + td_len_pa: sizet = string.strlen(temp_dir) + + # Set the temp dir so HandlesExprCall can read dependency modules' text.ll for cross-module + # method return-type lookup (avoids pointer returns being truncated to i32) + GenApi.SetTempDir(temp_dir) + + # app_deps_buf collects App files' direct dependency modules (space-separated) as Phase B+'s initial + # worklist for _BuildReachableSha1Set; declared outside the if, only assigned here. + app_deps_buf = stdlib.malloc(APP_DEPS_BUF_SIZE) + app_deps_len = 0 + if app_deps_buf is not None: app_deps_buf[0] = '\0' + + # Phase A multi-pass retry: with several sources, main.vp may translate before its imports + # (filesystem order); failed files are retried next pass, converging in at most file_count passes. + done_flags: bytes = stdlib.malloc(file_count) + if done_flags is None: + VLogger.error("Phase A done_flags allocation failed", "PhaseA") + stdlib.free(entries) + return 1 + string.memset(done_flags, 0, file_count) + PHASE_A_MAX_PASSES: int = 32 + for pass_a in range(int(PHASE_A_MAX_PASSES)): + completed_this_pass: int = 0 + for i in range(int(file_count)): + if done_flags[i] != 0: continue + ea: uint64 = uint64(entries) + i * entry_size + ent: SrcFileEntry* = (SrcFileEntry*)(ea) + # Timing point 0: start processing this file (translate phase start) + t_pa0: uint64 = sys.monotonic_ms() + if ent is None or ent.Path is None: + done_flags[i] = 1 + completed_this_pass += 1 + continue + # Compute the package name (the directory part relative to source_dir) + src_dir_len_pa: sizet = string.strlen(source_dir) + pkg_pa: str = None + if string.strlen(ent.Path) > src_dir_len_pa + 1: + rel_path_pa: str = ent.Path + src_dir_len_pa + 1 + pkg_pa = GenApi.compute_package_from_relpath(mb, rel_path_pa) + tr_a: HandlesTranslator.Translator* = BuildPipeline.TranslateFileGetTrans(mb, ent.Path, ent.Sha1, pkg_pa, 0, temp_dir) + # Timing point 1: translate returned + t_pa1: uint64 = sys.monotonic_ms() + if tr_a is None: + # Translation failed (syntax/parse error): retry next pass; convergence check reports it. + # OOM cleanup: a failed translation also produced AST/tokens, freed immediately. + mb.reset_to(mark2) + continue + # Record this file's deps (for convergence diagnostics; stdlib table survives reset) + _PhaseADiagRecord(ent.Sha1, tr_a._imported_modules, None) + # Collect _imported_modules into app_deps_buf (for Phase B+ on-demand compilation). + # Dependencies were filled early during translation. + if tr_a._imported_modules is not None and app_deps_buf is not None: + # Dedupe per module: several App files importing the same module would otherwise inflate + # app_deps toward 64KB, filling the initial worklist and overflowing BFS. + im_len_a: sizet = string.strlen(tr_a._imported_modules) + im_pos_a: sizet = 0 + while im_pos_a < im_len_a: + im_start_a: sizet = im_pos_a + while im_pos_a < im_len_a and tr_a._imported_modules[im_pos_a] != ' ': im_pos_a += 1 + im_mod_len: sizet = im_pos_a - im_start_a + if im_pos_a < im_len_a: im_pos_a += 1 + if im_mod_len == 0: continue + im_mod_buf: bytes = stdlib.malloc(im_mod_len + 1) + if im_mod_buf is None: continue + string.strncpy(im_mod_buf, tr_a._imported_modules + im_start_a, im_mod_len) + im_mod_buf[im_mod_len] = '\0' + # Skip if already in app_deps (exact word-boundary match) + if app_deps_len > 0 and GenApi.is_module_imported(app_deps_buf, im_mod_buf) != 0: + stdlib.free(im_mod_buf) + continue + if app_deps_len + im_mod_len + 2 < APP_DEPS_BUF_SIZE: + if app_deps_len > 0: + app_deps_buf[app_deps_len] = ' ' + app_deps_len = app_deps_len + 1 + string.strcpy(app_deps_buf + app_deps_len, im_mod_buf) + app_deps_len = app_deps_len + im_mod_len + app_deps_buf[app_deps_len] = '\0' + stdlib.free(im_mod_buf) + # dump stub IR (declarations only) + stub_buf_a: bytes = stdlib.malloc(PHASE_A_IR_SIZE) + if stub_buf_a is None: VLogger.error("stub buffer allocation failed", "PhaseA") + tr_a.dump_ir(stub_buf_a, PHASE_A_IR_SIZE, mlirlite.OUTPUT_STUB) + stub_len_a: sizet = string.strlen(stub_buf_a) + + # Save stub.ll (sliced path: temp_dir/{sha1 prefix}/{sha1}.stub.ll) + stub_path_a: str = StubMergerCollect._sliced_path(temp_dir, td_len_pa, ent.Sha1, "stub.ll") + if stub_path_a is not None: + sf_a: io.File* = io.File(stub_path_a, io.MODE.W) + if not sf_a.closed: + sf_a.write(stub_buf_a, stub_len_a) + sf_a.close() + stdlib.free(stub_path_a) + stdlib.free(stub_buf_a) + + # dump text IR (definitions only) + text_buf_a: bytes = stdlib.malloc(PHASE_A_IR_SIZE) + if text_buf_a is None: VLogger.error("text buffer allocation failed", "PhaseA") + tr_a.dump_ir(text_buf_a, PHASE_A_IR_SIZE, mlirlite.OUTPUT_TEXT) + text_len_a: sizet = string.strlen(text_buf_a) + + # Save text.ll (sliced path) + text_path_a: str = StubMergerCollect._sliced_path(temp_dir, td_len_pa, ent.Sha1, "text.ll") + if text_path_a is not None: + tf_a: io.File* = io.File(text_path_a, io.MODE.W) + if not tf_a.closed: + tf_a.write(text_buf_a, text_len_a) + tf_a.close() + stdlib.free(text_path_a) + stdlib.free(text_buf_a) + + # Save {sha1}.meta (structured sidecar: function list / struct layout / vtable) + StubMergerIR.WriteModuleMeta(tr_a.Module, temp_dir, td_len_pa, ent.Sha1) + + # Save {sha1}.full.ll (in-memory FULL artifact: a self-contained single-module IR) + full_buf_a: bytes = stdlib.malloc(PHASE_A_FULL_SIZE) + if full_buf_a is None: VLogger.error("full.ll buffer allocation failed", "PhaseA") + tr_a.dump_ir(full_buf_a, PHASE_A_FULL_SIZE, mlirlite.OUTPUT_FULL) + full_len_a: sizet = string.strlen(full_buf_a) + full_path_a: str = StubMergerCollect._sliced_path(temp_dir, td_len_pa, ent.Sha1, "full.ll") + if full_path_a is not None: + ffa: io.File* = io.File(full_path_a, io.MODE.W) + if not ffa.closed: + ffa.write(full_buf_a, full_len_a) + ffa.close() + stdlib.free(full_path_a) + stdlib.free(full_buf_a) + + # --traceback: take this module's debug call-site records and write .vpcall. + if BuildPipeline._traceback_mode != 0: + vpcall_text_a: str = mlirlite.DebugTakeText() + if vpcall_text_a is not None: + BuildPipeline.WriteCallSidecar(temp_dir, ent.Sha1, vpcall_text_a) + stdlib.free(vpcall_text_a) + + # Save dependencies (_imported_modules) for Phase B (sliced path) + deps_path_a: str = StubMergerCollect._sliced_path(temp_dir, td_len_pa, ent.Sha1, "deps.txt") + if deps_path_a is not None: + df_a: io.File* = io.File(deps_path_a, io.MODE.W) + if not df_a.closed: + if tr_a._imported_modules is not None: + dl_a: sizet = string.strlen(tr_a._imported_modules) + df_a.write(tr_a._imported_modules, dl_a) + df_a.close() + stdlib.free(deps_path_a) + + # Timing point 2: stub+text dump and write done; print per-file progress (as in Phase1 1b) + t_pa2: uint64 = sys.monotonic_ms() + VLogger.info(f"[{i+1}/{file_count}] PhaseA: {ent.Path} (translate={t_pa1-t_pa0} ms, dump={t_pa2-t_pa1} ms, total={t_pa2-t_pa0} ms)", "PhaseA") + + done_flags[i] = 1 + completed_this_pass += 1 + # OOM cleanup: stub/text/deps are on disk, so free this file's transient translation data + # (AST/tokens/lexer/IR/Handler); app_deps_buf already deep-copied the deps. + mb.reset_to(mark2) + # Convergence check: stop when all files are done + all_done: int = 1 + for i_ck in range(int(file_count)): + if done_flags[i_ck] == 0: + all_done = 0 + break + if all_done != 0: break + if completed_this_pass == 0: + # No progress: remaining files cannot finish (real compile errors). Abort and list them, each + # with a dependency-graph dump. VLogger.error calls sys.exit(1), so it must come after all output. + td_len_fail: sizet = string.strlen(temp_dir) + for i_err in range(int(file_count)): + if done_flags[i_err] == 0: + ea_err: uint64 = uint64(entries) + i_err * entry_size + ent_err: SrcFileEntry* = (SrcFileEntry*)(ea_err) + if ent_err is not None and ent_err.Path is not None: + if ent_err.Sha1 is not None: _PhaseADumpFailGraph(temp_dir, td_len_fail, ent_err.Path, ent_err.Sha1) + stdlib.free(done_flags) + _PhaseADiagFree() + VLogger.error("Phase A multi-pass convergence failed, aborting compilation", "PhaseA") + stdlib.free(entries) + return 1 + stdlib.free(done_flags) + # Phase A converged: free the diagnostic table (needed only on the failure path) + _PhaseADiagFree() + + if do_phase2 == 0: + VLogger.info("Phase A complete (stub generation only)", "project") + if app_deps_buf is not None: stdlib.free(app_deps_buf) + stdlib.free(entries) + return 0 + + # === 3. Phase B: compile each file to .obj === + if do_phase2 != 0: + log.banner("Phase B: compile .obj") + + # Collect .obj paths (64KB to avoid includes .obj path overflow dropping link symbols). + # reachable_set/reachable_count: filled by Phase B+, used by Phase C for on-demand linking. + reachable_set: bytes = None + reachable_count: int = 0 + OBJ_PATHS_SIZE: sizet = 65536 + obj_paths: bytes = stdlib.malloc(OBJ_PATHS_SIZE) + if obj_paths is None: return 1 + obj_paths[0] = '\0' + obj_pos: sizet = 0 + # main_obj_path: the .obj that defines the user main (placed first at link time so the linker + # does not pick another module's wrapper main) + main_obj_path: bytes = stdlib.malloc(512) + if main_obj_path is None: return 1 + main_obj_path[0] = '\0' + compiled_count: int = 0 + + # IR buffer size (16MB, to hold the in-memory FULL artifact {sha1}.full.ll; FULL = stub + text) + COMBINED_IR_SIZE: sizet = 16777216 + + # llc task table (stdlib.malloc, outlives threads): a contiguous array of LlcTask records; the loop + # only writes .ll + builds tasks, then run_llc_parallel runs llc in parallel batches. + llc_tasks: bytes = stdlib.malloc(sizet(file_count) * BuildPipeline.LlcTask.__sizeof__()) + if llc_tasks is None: + VLogger.error("llc task table allocation failed", "PhaseB") + return 1 + llc_task_count: int = 0 + + for i in range(int(file_count)): + ea: uint64 = uint64(entries) + i * entry_size + ent: SrcFileEntry* = (SrcFileEntry*)(ea) + if ent is None or ent.Path is None or ent.Sha1 is None: continue + # Start of this file's three-step timing (GetTickCount64, ms) + t_pb0: uint64 = sys.monotonic_ms() + # Read the in-memory FULL artifact {sha1}.full.ll -> complete IR (replaces merging stub + dep stub/text) + combined_ir: bytes = stdlib.malloc(COMBINED_IR_SIZE) + if combined_ir is None: + VLogger.error(f"combined_ir allocation failed: {ent.Path}", "PhaseB") + continue + combined_len: sizet = StubMergerBuild.LoadFullIR(temp_dir, ent.Sha1, combined_ir, COMBINED_IR_SIZE) + if combined_len == 0: + VLogger.error(f"failed to read full.ll: {ent.Path}", "PhaseB") + stdlib.free(combined_ir) + continue + # Timing point 1: full.ll read done + t_pb1: uint64 = sys.monotonic_ms() + + # Write .ll (llc threads only read this file); llc itself runs parallel in batches after the loop + ret_w: int = BuildPipeline.write_ir_to_file(combined_ir, combined_len, temp_dir, ent.Sha1) + stdlib.free(combined_ir) + if ret_w != 0: + VLogger.error(f"failed to write .ll: {ent.Path}", "PhaseB") + sys.exit(1) + # Timing point 2: .ll write done + t_pb2: uint64 = sys.monotonic_ms() + + # Build the llc task (command string is stdlib.malloc'd so threads' lifetimes are not reset-reclaimed) + llc_ir_path: str = BuildPipeline.build_sliced_path(temp_dir, ent.Sha1, "ll") + if llc_ir_path is None: + VLogger.error("failed to build llc input path", "PhaseB") + sys.exit(1) + tslot: BuildPipeline.LlcTask* = BuildPipeline.LlcTask*(uint64(llc_tasks) + sizet(llc_task_count) * BuildPipeline.LlcTask.__sizeof__()) + if BuildPipeline.init_llc_task(tslot, llc_ir_path, output_dir, ent.Sha1, cc_cmd, cc_flags) != 0: + VLogger.error(f"failed to build llc task: {ent.Path}", "PhaseB") + sys.exit(1) + # Timing point 3: llc task construction done + t_pb3: uint64 = sys.monotonic_ms() + llc_task_count += 1 + + compiled_count += 1 + # Show files submitted to llc (with the three timings: merge stub / write .ll / build task) + VLogger.info(f"[{i+1}/{file_count}] llc submitted: {ent.Path} (merge={t_pb1-t_pb0} ms, write={t_pb2-t_pb1} ms, task={t_pb3-t_pb2} ms, total={t_pb3-t_pb0} ms)", "PhaseB") + # Build the .obj path and detect the main module (test_main.vp or main.vp; .py no longer recognized) + od_len: sizet = string.strlen(output_dir) + is_main_mod: int = 0 + if string.strstr(ent.Path, "test_main.vp") is not None: is_main_mod = 1 + elif string.strstr(ent.Path, "main.vp") is not None: is_main_mod = 1 + + # Sliced path: output_dir/{sha1 prefix}/{sha1}.obj + obj_path_sliced: str = StubMergerCollect._sliced_path(output_dir, od_len, ent.Sha1, "obj") + if obj_path_sliced is not None: + op_sliced_len: sizet = string.strlen(obj_path_sliced) + if is_main_mod != 0: + # main module: copy to main_obj_path (so main is first at link time) + if op_sliced_len < 512: string.strcpy(main_obj_path, obj_path_sliced) + else: VLogger.error("main_obj_path buffer too small, aborting compilation", "PhaseB") + else: + # Other modules: append to obj_paths + if obj_pos + op_sliced_len + 2 < OBJ_PATHS_SIZE: + if obj_pos > 0: + obj_paths[obj_pos] = ' ' + obj_pos += 1 + string.strcpy(obj_paths + obj_pos, obj_path_sliced) + obj_pos += op_sliced_len + obj_paths[obj_pos] = '\0' + else: VLogger.error(".obj path buffer too small", "PhaseB") + stdlib.free(obj_path_sliced) + + # Run all llc jobs in parallel (batched at LLC_MAX_THREADS threads) + ret_p: int = BuildPipeline.run_llc_parallel(llc_tasks, llc_task_count, BuildPipeline.LLC_MAX_THREADS) + if ret_p != 0: + VLogger.error("llc compilation failed, aborting compilation", "PhaseB") + sys.exit(1) + + # Free the llc tasks (command strings + records) + ti: int = 0 + while ti < llc_task_count: + tfree: BuildPipeline.LlcTask* = BuildPipeline.LlcTask*(uint64(llc_tasks) + sizet(ti) * BuildPipeline.LlcTask.__sizeof__()) + BuildPipeline.free_llc_task(tfree) + ti += 1 + stdlib.free(llc_tasks) + + # Assemble .s files: llvm-mc -> .obj (isr.s etc., bare symbols matching the .vp ExportT/State stubs). + # The obj is added to obj_paths for Phase C linking. + od_len_as: sizet = string.strlen(output_dir) + asm_compiled: int = 0 + for ai_as in range(int(asm_file_count)): + ea_as: uint64 = uint64(asm_entries) + ai_as * entry_size + ent_as: SrcFileEntry* = (SrcFileEntry*)(ea_as) + if ent_as is None or ent_as.Path is None or ent_as.Sha1 is None: continue + aret_as: int = BuildPipeline.asm_to_obj(ent_as.Path, ent_as.Sha1, output_dir, Config.TargetTriple) + if aret_as != 0: + VLogger.error(f"llvm-mc assembly failed, aborting compilation: {ent_as.Path}", "PhaseB") + sys.exit(1) + obj_path_asm: str = StubMergerCollect._sliced_path(output_dir, od_len_as, ent_as.Sha1, "obj") + if obj_path_asm is not None: + op_len_asm: sizet = string.strlen(obj_path_asm) + if obj_pos + op_len_asm + 2 < OBJ_PATHS_SIZE: + if obj_pos > 0: + obj_paths[obj_pos] = ' ' + obj_pos += 1 + string.strcpy(obj_paths + obj_pos, obj_path_asm) + obj_pos += op_len_asm + obj_paths[obj_pos] = '\0' + stdlib.free(obj_path_asm) + asm_compiled += 1 + VLogger.info(f"[asm {asm_compiled}] assembly complete: {ent_as.Path}", "PhaseB") + + VLogger.success(f"compilation complete: {compiled_count}/{file_count}", "PhaseB") + + if compiled_count == 0: + VLogger.error("no files compiled successfully", "PhaseB") + stdlib.free(entries) + stdlib.free(asm_entries) + return 1 + + # === 3.5 Compile missing includes (on demand via the dependency graph) === + # Only compile includes reached directly/indirectly from App sources (via _BuildReachableSha1Set). + if includes_dir is not None and includes_binary_dir is not None: + inc_compiled: int = 0 + td_len_mi: sizet = string.strlen(temp_dir) + + # Diagnostic log: confirm the Phase B+ entry and Sha1Store state + VLogger.info(f"PhaseB+ entry: store_count={StubMergerStore.GetSha1StoreCount()} includes_binary_dir={includes_binary_dir}", "PhaseB+") + + # Create includes_binary_dir (so .obj files can be written and collect_obj_files can scan them) + BuildPipeline.ensure_dir(includes_binary_dir) + + # declare_only preprocessing was moved before Phase A (Phase A-pre-inc) so includes structs are + # registered when Phase A translates App sources. PopulateIncludesDeps is filled there too. + + # Build the reachable SHA1 set (dependency graph traversal). + # When app_deps_buf is None, fall back to scanning source_dir (non-recursive). + REACHABLE_SET_SIZE: sizet = sizet(StubMerger.MAX_INCLUDES_SHA1) * 17 + reachable_set: bytes = stdlib.malloc(REACHABLE_SET_SIZE) + reachable_count: int = 0 + if reachable_set is not None: + string.memset(reachable_set, 0, REACHABLE_SET_SIZE) + reachable_count = StubMergerBuild._BuildReachableSha1Set(mb, source_dir, temp_dir, reachable_set, app_deps_buf, includes_dir) + VLogger.info(f"reachable includes SHA1: {reachable_count} entries", "PhaseB+") + # fast-fail: reachable-set construction failed (deps not found); never fall back, abort + if reachable_count < 0: + VLogger.error("reachable set construction failed (fast-fail), aborting compilation", "PhaseB+") + return 1 + else: + VLogger.error("reachable_set allocation failed, aborting compilation", "PhaseB+") + return 1 + + # Free app_deps_buf (reachable_set is built, no longer needed) + if app_deps_buf is not None: + stdlib.free(app_deps_buf) + app_deps_buf = None + + # Get the SHA1/module-name arrays from the global store as local copies (bytes) to avoid address + # issues from passing the store's union return (bytes*) across functions; rel/incdir keep pointers. + td_len_pb: sizet = string.strlen(temp_dir) + store_sha1_arr_mi: bytes = stdlib.malloc(StubMerger.MAX_INCLUDES * 17) + store_mod_arr_mi: bytes = stdlib.malloc(sizet(StubMerger.MAX_INCLUDES) * 64) + store_count_mi: int = 0 + if store_sha1_arr_mi is not None and store_mod_arr_mi is not None: + store_count_mi = StubMergerIR._BuildIncludesSha1Map(temp_dir, td_len_pb, store_sha1_arr_mi, store_mod_arr_mi) + store_rel_arr_mi: bytes = StubMergerStore.GetSha1StoreRelArrPtr() + store_incdir_arr_mi: bytes = StubMergerStore.GetSha1StoreIncdirArrPtr() + # 诊断/失败前置:store 为空或拓扑为空时,下面的 include 编译循环会整体跳过, + # 但 PhaseC 仍会把这些 include 的 .obj 塞进链接命令 → 只看到一串 + # "no such file or directory: includes.binary/xx/xxxx.obj"。这里直接给出根因。 + if store_count_mi <= 0: + VLogger.error(f"PhaseB+: Sha1Store is empty (store_count=0) but {reachable_count} include(s) are reachable; the include compile loop would be skipped and every include .obj would be missing at link time (check the Phase1 dependency scan / cache flags)", "PhaseB+") + if store_sha1_arr_mi is not None: stdlib.free(store_sha1_arr_mi) + if store_mod_arr_mi is not None: stdlib.free(store_mod_arr_mi) + return 1 + if store_count_mi > 0: + + # Invalidate cached include objects when the include set or the compiler build changed: + # an .obj embeds the sha1-prefixed symbols of every module it calls, so an object built + # against an older dependency set must never be reused (stale call targets / link errors). + if StubMergerBuild.EnsureObjCacheStamp(includes_binary_dir, store_sha1_arr_mi, store_incdir_arr_mi, store_count_mi) == 0: + VLogger.warning("include object-cache stamp could not be written; cached objects may be reused", "PhaseB+") + + # Topologically sort reachable includes: dependencies are fully translated first. Cross-module + # vtable generation needs dependency functions' TypeStr registered globally (e.g. astaux/base AST). + topo_idx: sizet* = (sizet*)(stdlib.malloc(8 * (store_count_mi + 1))) + topo_count: int = 0 + if topo_idx is None: + VLogger.error("topological sort index allocation failed, aborting compilation", "PhaseB+") + topo_count = 0 + else: + topo_count = StubMergerTopo.BuildTopoSortedIndices(store_count_mi, store_sha1_arr_mi, store_mod_arr_mi, reachable_set, reachable_count, topo_idx) + # 拓扑为空 ⇒ 下面的编译循环整体不执行,但 PhaseC 仍会引用这些 .obj → 链接必然失败。 + # 在这里给根因,而不是让人去看一串 "no such file or directory"。 + if topo_count <= 0: + VLogger.error(f"PhaseB+: topological sort produced 0 entries (store_count={store_count_mi}, reachable={reachable_count}); include .obj files would be missing at link time", "PhaseB+") + return 1 + + for si_mi in range(int(topo_count)): + # Current entry (topological index; reachable modules only) + ti: int = int(topo_idx[si_mi]) + # Get the current entry's SHA1 and rel_path + inc_sha1_mi: str = store_sha1_arr_mi + sizet(ti) * 17 + inc_rel_mi: str = store_rel_arr_mi + sizet(ti) * StubMerger.MAX_REL_PATH_LEN + # Skip App sources: an App file may be scanned as an include and be reachable; compiling + # another .obj into includes_binary would duplicate the output/ .obj's symbols. + app_skip_mi: int = 0 + for ai_mi in range(int(file_count)): + ea_ap: uint64 = uint64(entries) + ai_mi * entry_size + ent_ap: SrcFileEntry* = (SrcFileEntry*)(ea_ap) + if ent_ap is not None and ent_ap.Sha1 is not None: + if string.strcmp(ent_ap.Sha1, inc_sha1_mi) == 0: + app_skip_mi = 1 + break + if app_skip_mi != 0: continue + # Check whether the .obj already exists in includes.binary (sliced subdir) + ibd_len_mi: sizet = string.strlen(includes_binary_dir) + check_pat_mi: str = StubMergerCollect._sliced_path(includes_binary_dir, ibd_len_mi, inc_sha1_mi, "obj") + obj_exists_mi: int = 0 + if check_pat_mi is not None and os.isfile(check_pat_mi): obj_exists_mi = 1 + if obj_exists_mi != 0: + if check_pat_mi is not None: stdlib.free(check_pat_mi) + continue + + # The .obj does not exist; compile it. + # Build the source path: {includes root}/{rel_path} (per-entry IncDir; empty falls back to includes_dir) + rel_path_len_mi: sizet = string.strlen(inc_rel_mi) + incdir_mi: str = store_incdir_arr_mi + sizet(ti) * StubMerger.MAX_INC_DIR_LEN + if incdir_mi is None or incdir_mi[0] == '\0': incdir_mi = includes_dir + inc_dir_len_mi: sizet = string.strlen(incdir_mi) + src_fp_mi: bytes = stdlib.malloc(inc_dir_len_mi + 1 + rel_path_len_mi + 1) + if src_fp_mi is None: + stdlib.free(check_pat_mi) + VLogger.error("includes source path allocation failed, aborting compilation", "PhaseB+") + continue + viperlib.snprintf(src_fp_mi, inc_dir_len_mi + 1 + rel_path_len_mi + 1, "%s/%s", incdir_mi, inc_rel_mi) + + # Check whether it is a declaration-only file (read {sha1}.meta: no FUNC ... DEF means pure declaration) + is_decl_mi: int = StubMergerIR.IsDeclOnlyModule(temp_dir, td_len_mi, inc_sha1_mi) + + # is_decl_mi: -1 = no meta (Phase1 did not translate it), 1 = declaration-only, 0 = impl. + # Skip only declaration-only (is_decl_mi == 1); -1 and 0 continue to the translation logic. + if is_decl_mi == 1: + stdlib.free(check_pat_mi) + stdlib.free(src_fp_mi) + continue + + # Try to read the in-memory FULL artifact {sha1}.full.ll (should be produced by Phase1/PhaseA) + inc_combined_mi: bytes = stdlib.malloc(COMBINED_IR_SIZE) + inc_combined_len_mi: sizet = 0 + if inc_combined_mi is not None: inc_combined_len_mi = StubMergerBuild.LoadFullIR(temp_dir, inc_sha1_mi, inc_combined_mi, COMBINED_IR_SIZE) + + # If full.ll is missing, translate the source, save stub/text/meta/full, then retry + if inc_combined_len_mi == 0 and inc_combined_mi is not None: + # Compute the includes file's package name (the directory part relative to includes_dir) + inc_pkg_mi: str = None + if string.strlen(src_fp_mi) > inc_dir_len_mi + 1: + inc_rel_mi2: str = src_fp_mi + inc_dir_len_mi + 1 + inc_pkg_mi = GenApi.compute_package_from_relpath(mb, inc_rel_mi2) + tr_mi: HandlesTranslator.Translator* = BuildPipeline.TranslateFileGetTrans(mb, src_fp_mi, inc_sha1_mi, inc_pkg_mi) + if tr_mi is not None: + # PBP IR buffer: same as PHASE_A_IR_SIZE (parser.vp's text.ll >1MB truncation root cause) + PBP_IR_SIZE: sizet = 8388608 + # FULL dump = stub + text, with 2x headroom + PBP_FULL_SIZE: sizet = 16777216 + # Save stub.ll (sliced path) + inc_stub_buf: bytes = stdlib.malloc(PBP_IR_SIZE) + if inc_stub_buf is not None: + tr_mi.dump_ir(inc_stub_buf, PBP_IR_SIZE, mlirlite.OUTPUT_STUB) + inc_stub_len: sizet = string.strlen(inc_stub_buf) + inc_stub_path: str = StubMergerCollect._sliced_path(temp_dir, td_len_mi, inc_sha1_mi, "stub.ll") + if inc_stub_path is not None: + isf: io.File* = io.File(inc_stub_path, io.MODE.W) + if not isf.closed: + isf.write(inc_stub_buf, inc_stub_len) + isf.close() + stdlib.free(inc_stub_path) + stdlib.free(inc_stub_buf) + # Save text.ll (sliced path) + inc_text_buf: bytes = stdlib.malloc(PBP_IR_SIZE) + if inc_text_buf is not None: + tr_mi.dump_ir(inc_text_buf, PBP_IR_SIZE, mlirlite.OUTPUT_TEXT) + inc_text_len: sizet = string.strlen(inc_text_buf) + inc_text_path: str = StubMergerCollect._sliced_path(temp_dir, td_len_mi, inc_sha1_mi, "text.ll") + if inc_text_path is not None: + itf: io.File* = io.File(inc_text_path, io.MODE.W) + if not itf.closed: + itf.write(inc_text_buf, inc_text_len) + itf.close() + stdlib.free(inc_text_path) + stdlib.free(inc_text_buf) + # Save {sha1}.meta (structured sidecar: function list / struct layout / vtable) + StubMergerIR.WriteModuleMeta(tr_mi.Module, temp_dir, td_len_mi, inc_sha1_mi) + # Save {sha1}.full.ll (in-memory FULL artifact) + inc_full_buf: bytes = stdlib.malloc(PBP_FULL_SIZE) + if inc_full_buf is None: VLogger.error("full.ll buffer allocation failed", "PhaseB+") + tr_mi.dump_ir(inc_full_buf, PBP_FULL_SIZE, mlirlite.OUTPUT_FULL) + inc_full_len: sizet = string.strlen(inc_full_buf) + inc_full_path: str = StubMergerCollect._sliced_path(temp_dir, td_len_mi, inc_sha1_mi, "full.ll") + if inc_full_path is not None: + ifff: io.File* = io.File(inc_full_path, io.MODE.W) + if not ifff.closed: + ifff.write(inc_full_buf, inc_full_len) + ifff.close() + stdlib.free(inc_full_path) + stdlib.free(inc_full_buf) + # --traceback: take this module's debug call-site records and write .vpcall. + if BuildPipeline._traceback_mode != 0: + vpcall_text_mi: str = mlirlite.DebugTakeText() + if vpcall_text_mi is not None: + BuildPipeline.WriteCallSidecar(temp_dir, inc_sha1_mi, vpcall_text_mi) + stdlib.free(vpcall_text_mi) + # Retry reading full.ll + inc_combined_len_mi = StubMergerBuild.LoadFullIR(temp_dir, inc_sha1_mi, inc_combined_mi, COMBINED_IR_SIZE) + if tr_mi is None: + VLogger.error(f"include translation returned no module: {src_fp_mi} (sha1={inc_sha1_mi})", "PhaseB+") + # OOM cleanup: stub/text/meta/full are on disk, free this includes file's transient + # translation data (AST/tokens/lexer/IR/Handler); reads only touch files. + mb.reset_to(mark2) + + if inc_combined_len_mi == 0: + # Diagnostic: check whether full.ll/stub.ll/text.ll exist + diag_full: str = StubMergerCollect._sliced_path(temp_dir, td_len_mi, inc_sha1_mi, "full.ll") + diag_stub: str = StubMergerCollect._sliced_path(temp_dir, td_len_mi, inc_sha1_mi, "stub.ll") + diag_text: str = StubMergerCollect._sliced_path(temp_dir, td_len_mi, inc_sha1_mi, "text.ll") + diag_full_ex: int = 0 + diag_stub_ex: int = 0 + diag_text_ex: int = 0 + if diag_full is not None: + df_diag_f: io.File* = io.File(diag_full, io.MODE.R) + if not df_diag_f.closed: + diag_full_ex = 1 + df_diag_f.close() + stdlib.free(diag_full) + if diag_stub is not None: + df_diag_s: io.File* = io.File(diag_stub, io.MODE.R) + if not df_diag_s.closed: + diag_stub_ex = 1 + df_diag_s.close() + stdlib.free(diag_stub) + if diag_text is not None: + df_diag_t: io.File* = io.File(diag_text, io.MODE.R) + if not df_diag_t.closed: + diag_text_ex = 1 + df_diag_t.close() + stdlib.free(diag_text) + # fail-fast:该 include 既没有 full.ll、翻译也没产出 → 它的 .obj 必然缺失, + # 继续下去只会得到一串链接器 "no such file or directory"。这里直接停下并指出文件。 + VLogger.error(f"failed to read full.ll: {src_fp_mi} (full={diag_full_ex} stub={diag_stub_ex} text={diag_text_ex} is_decl={is_decl_mi}); aborting so the include .obj is not silently missing at link time", "PhaseB+") + if inc_combined_mi is not None: stdlib.free(inc_combined_mi) + stdlib.free(check_pat_mi) + stdlib.free(src_fp_mi) + return 1 + + # Compile to .obj (output to includes_binary_dir, consistent with the existence check and link collection) + inc_cret_mi: int = BuildPipeline.compile_module_to_obj(inc_combined_mi, inc_combined_len_mi, temp_dir, includes_binary_dir, inc_sha1_mi, cc_cmd, cc_flags) + stdlib.free(inc_combined_mi) + + if inc_cret_mi != 0: + VLogger.error(f"llc compilation failed, aborting compilation: {src_fp_mi}", "PhaseB+") + sys.exit(1) + + inc_compiled += 1 + # Show the compiled includes file + VLogger.info(f"[includes {inc_compiled}] compilation complete: {src_fp_mi}", "PhaseB+") + + # Add to obj_paths (sliced path, taken from includes_binary_dir) + ibd_len_mi2: sizet = string.strlen(includes_binary_dir) + inc_obj_sliced: str = StubMergerCollect._sliced_path(includes_binary_dir, ibd_len_mi2, inc_sha1_mi, "obj") + + if inc_obj_sliced is not None: + inc_obj_len: sizet = string.strlen(inc_obj_sliced) + if obj_pos + inc_obj_len + 2 < OBJ_PATHS_SIZE: + if obj_pos > 0: + obj_paths[obj_pos] = ' ' + obj_pos += 1 + string.strcpy(obj_paths + obj_pos, inc_obj_sliced) + obj_pos += inc_obj_len + obj_paths[obj_pos] = '\0' + else: VLogger.error("includes .obj path buffer overflow, aborting compilation", "PhaseB+") + stdlib.free(inc_obj_sliced) + + # Free this iteration's temporary memory + if check_pat_mi is not None: stdlib.free(check_pat_mi) + + stdlib.free(src_fp_mi) + + # Free the topological indices and local map copies + if topo_idx is not None: stdlib.free(topo_idx) + if store_sha1_arr_mi is not None: stdlib.free(store_sha1_arr_mi) + if store_mod_arr_mi is not None: stdlib.free(store_mod_arr_mi) + + VLogger.info(f"compiled missing includes: {inc_compiled} entries", "PhaseB+") + + # Free app_deps_buf (fallback when the Phase B+ block did not run) + if app_deps_buf is not None: + stdlib.free(app_deps_buf) + app_deps_buf = None + + # === 4. Phase C: link all .obj -> .exe === + log.banner("Phase C: link") + + od_len2: sizet = string.strlen(output_dir) + lo_len: sizet = string.strlen(linker_output) + exe_path: bytes = stdlib.malloc(od_len2 + lo_len + 2) + if exe_path is not None: viperlib.snprintf(exe_path, od_len2 + lo_len + 2, "%s/%s", output_dir, linker_output) + else: + exe_path = linker_output + VLogger.error("exe_path allocation failed, aborting compilation", "PhaseC") + + # Build the final .obj path list: main_obj_path first, reachable .objs after + final_obj_paths: bytes = stdlib.malloc(OBJ_PATHS_SIZE + 512) + if final_obj_paths is None: + stdlib.free(entries) + return 1 + final_obj_paths[0] = '\0' + fop_pos: sizet = 0 + # main_obj_path goes first (so the linker prefers the user main) + if main_obj_path[0] != '\0': + mlen: sizet = string.strlen(main_obj_path) + string.strcpy(final_obj_paths, main_obj_path) + fop_pos = mlen + + # Merge the App non-main modules' .obj (obj_paths, space-separated) compiled by Phase B/B+. + # final_obj_paths must include main_obj + reachable includes + App deps, else linking hits undefined symbols. + if obj_pos > 0: + if fop_pos + obj_pos + 1 < OBJ_PATHS_SIZE + 512: + if fop_pos > 0: + final_obj_paths[fop_pos] = ' ' + fop_pos += 1 + string.strcpy(final_obj_paths + fop_pos, obj_paths) + fop_pos += obj_pos + final_obj_paths[fop_pos] = '\0' + + # Link by graph: iterate reachable_set and collect only reachable includes' .obj, + # no longer collecting every .obj under includes.binary. + if reachable_set is not None and reachable_count > 0: + ibd_len_pc: sizet = string.strlen(includes_binary_dir) + td_len_pc: sizet = string.strlen(temp_dir) + ri_pc: int + for ri_pc in range(int(reachable_count)): + rsha1_pc: str = reachable_set + sizet(ri_pc) * 17 + if rsha1_pc[0] == '\0': continue + # Skip App sources: their .obj is already in final_obj_paths; collecting a same-sha1 .obj from includes_binary would duplicate symbols. + app_skip_pc: int = 0 + for ai_pc in range(int(file_count)): + ea_pc: uint64 = uint64(entries) + ai_pc * entry_size + ent_pc: SrcFileEntry* = (SrcFileEntry*)(ea_pc) + if ent_pc is not None and ent_pc.Sha1 is not None: + if string.strcmp(ent_pc.Sha1, rsha1_pc) == 0: + app_skip_pc = 1 + break + if app_skip_pc != 0: continue + obj_path_pc: str = StubMergerCollect._sliced_path(includes_binary_dir, ibd_len_pc, rsha1_pc, "obj") + if obj_path_pc is not None: + # Check whether the .obj file exists + cf_pc: io.File* = io.File(obj_path_pc, io.MODE.R) + if not cf_pc.closed: + cf_pc.close() + # The .obj exists; append to final_obj_paths + op_len_pc: sizet = string.strlen(obj_path_pc) + if fop_pos + op_len_pc + 2 < OBJ_PATHS_SIZE + 512: + if fop_pos > 0: + final_obj_paths[fop_pos] = ' ' + fop_pos += 1 + string.strcpy(final_obj_paths + fop_pos, obj_path_pc) + fop_pos += op_len_pc + final_obj_paths[fop_pos] = '\0' + else: + # .obj 缺失:声明-only 模块本来就不需要 .obj,静默跳过;其余情况无论是否 + # --phase2command,链接都会因缺文件失败 —— 直接报根因并中止,不要拖到链接器。 + if StubMergerIR.IsDeclOnlyModule(temp_dir, td_len_pc, rsha1_pc) != 1: + VLogger.error(f"reachable module .obj missing: {obj_path_pc} (not declaration-only); aborting before link", "PhaseC") + stdlib.free(obj_path_pc) + return 1 + stdlib.free(obj_path_pc) + + final_obj_paths[fop_pos] = '\0' + obj_paths_len: sizet = fop_pos + + # Free reachable_set (Phase C is done with it) + if reachable_set is not None: + stdlib.free(reachable_set) + reachable_set = None + + # Link by graph: pass None for includes_binary_dir to avoid collecting everything + lret: int = BuildPipeline.link_objs_to_exe(final_obj_paths, obj_paths_len, linker_cmd, linker_flags, exe_path, None, linker_static_libs, linker_dynamic_libs) + + if lret == 0: + VLogger.success(f"output: {exe_path}", "PhaseC") + # Deploy declared runtime deps (project.vpj runtime.*) next to the exe; fail-fast on error. + # Skipped under --phase2command (the exe is not linked yet). + if BuildPipeline._phase2_cmd_path is None: + if BuildPipeline.deploy_runtime_deps(exe_path) != 0: + VLogger.error("runtime dependency deployment failed, aborting compilation", "runtime") + stdlib.free(entries) + stdlib.free(asm_entries) + return 1 + # --traceback: export .stackmap (skipped under --phase2command, + # where nothing was actually linked). + if BuildPipeline._traceback_mode != 0 and BuildPipeline._phase2_cmd_path is None: BuildPipeline.WriteStackMap(mb, exe_path) + # --run / viperbuild build(run=True[, args=[...]]): run the linked exe after linking. + # viperbuild 的 args 已在 Pass 0 拼好(含按需加引号),这里只负责拼命令行。 + vb_run_wanted: int = 0 + vb_run_args: str = None + if ViperBuild.IsApplied() != 0: + vb_run_wanted = ViperBuild.RunAfterBuild() + vb_run_args = ViperBuild.RunArgs() + run_wanted: bool = args.get_bool("run") or vb_run_wanted != 0 + if run_wanted and BuildPipeline._phase2_cmd_path is not None: + # phase2command mode: the exe is not linked yet (the command is in the bat); skip --run + VLogger.info("phase2command mode: skipping --run (exe not linked yet)", "phase2cmd") + elif run_wanted: + run_cmd_len: sizet = len(exe_path) + 8 + if vb_run_args is not None: run_cmd_len += len(vb_run_args) + 1 + run_cmd: bytes = stdlib.malloc(run_cmd_len) + if run_cmd is not None: + if vb_run_args is not None and string.strlen(vb_run_args) > 0: + viperlib.snprintf(run_cmd, run_cmd_len, "\"%s\" %s", exe_path, vb_run_args) + else: + viperlib.snprintf(run_cmd, run_cmd_len, "\"%s\"", exe_path) + VLogger.info(f"run: {run_cmd}", "run") + rp: subprocess.CompletedProcess* = subprocess.run(run_cmd, False, False) + if rp is not None: VLogger.info(f"exit code: {rp.returncode}", "run") + stdlib.free(run_cmd) + else: + VLogger.error("link failed", "PhaseC") + stdlib.free(entries) + stdlib.free(asm_entries) + return 1 + + stdlib.free(entries) + stdlib.free(asm_entries) + return 0 diff --git a/App/lib/Projectrans/Config.vp b/App/lib/Projectrans/Config.vp index 85e9485..447c47a 100644 --- a/App/lib/Projectrans/Config.vp +++ b/App/lib/Projectrans/Config.vp @@ -626,7 +626,14 @@ def get_includes_binary_dir() -> str: # (sha1 subdirectories) while TPC's includes cache at {IncludesDir}.binary/ holds flat # "..pic.obj" files, so sharing one directory would mix the two layouts. def mlir_dir_suffix() -> str: - """Directory isolation suffix for the includes cache of the MLIR backend (always "_mlir").""" + """Directory isolation suffix for the includes cache of the MLIR backend. + + The object format is target-specific, so a Linux-target build uses a separate + cache from the Windows one (otherwise cross-builds would link the wrong objects): + Windows target -> "_mlir" Linux target -> "_mlir_linux" + """ + if TargetTriple is not None and string.strstr(TargetTriple, "linux") is not None: + return "_mlir_linux" return "_mlir" diff --git a/App/lib/Projectrans/Utils.vp b/App/lib/Projectrans/Utils.vp index 2c4f56b..d546211 100644 --- a/App/lib/Projectrans/Utils.vp +++ b/App/lib/Projectrans/Utils.vp @@ -4,9 +4,8 @@ import string import stdlib import viperlib import hashlib -import win32.win32file -import win32.win32base import io +import os # Utils - project utility functions (SHA1 hashing, directory cleanup) @@ -140,50 +139,22 @@ def PathToModuleName(path: str, strip_includes_prefix: int) -> str: def CleanDir(dir_path: str) -> int: """Recursively delete files/subdirs under dir_path, returning the count.""" if dir_path is None: return -1 - dir_len: sizet = string.strlen(dir_path) - pattern: bytes = stdlib.malloc(dir_len + 8) - if pattern is None: return -1 - viperlib.snprintf(pattern, dir_len + 8, "%s/*", dir_path) - - find_data: win32.win32file.WIN32_FIND_DATAA* = stdlib.malloc(win32.win32file.WIN32_FIND_DATAA.__sizeof__()) - if find_data is None: - stdlib.free(pattern) - return -1 - string.memset(find_data, 0, win32.win32file.WIN32_FIND_DATAA.__sizeof__()) - - handle: win32.win32base.HANDLE = win32.win32file.FindFirstFileA(pattern, find_data) - if handle == win32.win32base.INVALID_HANDLE_VALUE: - stdlib.free(pattern) - stdlib.free(find_data) - return 0 - + reader: os.DirReader* = os.scandir(dir_path) + if reader is None: return 0 + entry: os.DirEntry = os.DirEntry() deleted: int = 0 while True: - fname: str = find_data.cFileName - if fname is not None: - # Skip . and .. - if fname[0] == '.': - if fname[1] == '\0': fname = None - elif fname[1] == '.' and fname[2] == '\0': fname = None - if fname is not None: - full_path: bytes = JoinPath(dir_path, fname) - if full_path is not None: - # Check whether this is a directory - is_dir: uint32 = find_data.dwFileAttributes & win32.win32file.FILE_ATTRIBUTE_DIRECTORY - if is_dir != 0: - # Recurse into the subdirectory - sub_deleted: int = CleanDir(full_path) - if sub_deleted > 0: deleted += sub_deleted - # Remove the now-empty subdirectory - win32.win32file.RemoveDirectoryA(full_path) - else: - # Delete the file - if win32.win32file.DeleteFileA(full_path) != 0: deleted += 1 - stdlib.free(full_path) - - if win32.win32file.FindNextFileA(handle, find_data) == 0: break - - win32.win32file.FindClose(handle) - stdlib.free(pattern) - stdlib.free(find_data) + rr: int = os.readdir(reader, &entry) + if rr == 0: break + if rr < 0: break + full_path: bytes = JoinPath(dir_path, entry.name) + if full_path is None: continue + if entry.is_dir: + sub_deleted: int = CleanDir(full_path) + if sub_deleted > 0: deleted += sub_deleted + os.rmdir(full_path) + else: + if os.remove(full_path) == 0: deleted += 1 + stdlib.free(full_path) + os.closedir(reader) return deleted diff --git a/App/lib/Stub/StubGen/Converter.vp b/App/lib/Stub/StubGen/Converter.vp index d32af02..76da74b 100644 --- a/App/lib/Stub/StubGen/Converter.vp +++ b/App/lib/Stub/StubGen/Converter.vp @@ -810,6 +810,25 @@ def _PyiDefaultStr(buf: bytes, buf_size: sizet, pos: sizet, node: ast.AST*, dept pos = _PyiEmit(buf, buf_size, pos, num_buf_dv) stdlib.free(num_buf_dv) return pos + if cn.const_kind == ast.CONST_FLOAT: + # %.17g round-trips an IEEE double exactly (used elsewhere in this file too). Without + # this branch a float default emitted nothing, producing a malformed stub line + # `def f(x: float = )` that crashes the vpi default scan. + num_buf_df: str = stdlib.malloc(64) + if num_buf_df is not None: + viperlib.snprintf(num_buf_df, 64, "%.17g", cn.float_val) + # %.17g prints an integral float as "-1"/"1" (no dot or exponent). The vpi default + # scanner would classify that as an INTEGER and hand an i32 to a float parameter + # (cross-module defaults then read 0.0). Keep the token unambiguously float. + has_frac_df: int = 0 + j_df: sizet = 0 + while num_buf_df[j_df] != 0: + if num_buf_df[j_df] == '.' or num_buf_df[j_df] == 'e' or num_buf_df[j_df] == 'E': has_frac_df = 1 + j_df += 1 + pos = _PyiEmit(buf, buf_size, pos, num_buf_df) + if has_frac_df == 0: pos = _PyiEmit(buf, buf_size, pos, ".0") + stdlib.free(num_buf_df) + return pos if cn.const_kind == ast.CONST_STR: pos = _PyiEmit(buf, buf_size, pos, "\"") if cn.str_val is not None: pos = _PyiEmit(buf, buf_size, pos, cn.str_val) @@ -836,6 +855,32 @@ def _PyiDefaultStr(buf: bytes, buf_size: sizet, pos: sizet, node: ast.AST*, dept if uop.operand is not None: return _PyiDefaultStr(buf, buf_size, pos, uop.operand, depth + 1) return pos + # Call: struct-constructor / call default (e.g. Vec2(1.0, 2.0)). Emitting the expression + # keeps the .vpi parameter list syntactically valid (an empty default used to drop the + # whole parameter and produce an arity mismatch) and lets the default scanner encode the + # literal field values for cross-module reconstruction. + if k == ast.ASTKind.Call: + cc: ast.Call* = ast.Call*(node) + if cc.func is None: return pos + pos = _PyiDefaultStr(buf, buf_size, pos, cc.func, depth + 1) + pos = _PyiEmit(buf, buf_size, pos, "(") + if cc.args is not None: + cargs: list[ast.AST*]* = cc.args + can: sizet = len(cargs) + for cai in range(int(can)): + if cai > 0: pos = _PyiEmit(buf, buf_size, pos, ", ") + pos = _PyiDefaultStr(buf, buf_size, pos, cargs.get(cai), depth + 1) + return _PyiEmit(buf, buf_size, pos, ")") + + # Attribute: module-qualified constant / constructor (e.g. mod.Vec2, mod.SHARE) + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(node) + if at.value is not None: + pos = _PyiDefaultStr(buf, buf_size, pos, at.value, depth + 1) + pos = _PyiEmit(buf, buf_size, pos, ".") + if at.attr is not None: pos = _PyiEmit(buf, buf_size, pos, at.attr) + return pos + # Other complex expressions (BinOp etc.): emit nothing; caller enforces fail-fast return pos diff --git a/App/lib/Stub/StubGen/VpiReader.vp b/App/lib/Stub/StubGen/VpiReader.vp index 15ab667..02ec5e1 100644 --- a/App/lib/Stub/StubGen/VpiReader.vp +++ b/App/lib/Stub/StubGen/VpiReader.vp @@ -456,14 +456,14 @@ def _lookup_vpi_typedef(src_sha1: str, name: str) -> str: def _resolve_vpi_annot_with_typedefs(pool: memhub.MemBuddy*, src_sha1: str, ret_annot: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*: """Resolve a vpi annotation string, following same-module typedef aliases (max 4 levels).""" if ret_annot is None or pool is None: return None - ty: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, ret_annot, i32_ty, i8_ptr_ty) + ty: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, ret_annot, i32_ty, i8_ptr_ty, src_sha1) if ty is not None: return ty cur: str = ret_annot depth: int = 0 while depth < 4: nxt: str = _lookup_vpi_typedef(src_sha1, cur) if nxt is None: return None - ty = _resolve_vpi_ret_annot_str(pool, nxt, i32_ty, i8_ptr_ty) + ty = _resolve_vpi_ret_annot_str(pool, nxt, i32_ty, i8_ptr_ty, src_sha1) if ty is not None: return ty cur = nxt depth += 1 @@ -974,7 +974,7 @@ def _resolve_vpi_param_annot_seg_template(pool: memhub.MemBuddy*, src_sha1: str, if uint64(subst_t) != uint64(seg_buf_t): stdlib.free(seg_buf_t) seg_buf_t = subst_t stdlib.free(tparam_t) - ty_t = _resolve_vpi_ret_annot_str(pool, seg_buf_t, i32_ty, i8_ptr_ty) + ty_t = _resolve_vpi_ret_annot_str(pool, seg_buf_t, i32_ty, i8_ptr_ty, src_sha1) stdlib.free(seg_buf_t) break stdlib.free(base_t) @@ -1011,7 +1011,7 @@ def _repair_declare_params_from_text(pool: memhub.MemBuddy*, func: mlirlite.Func if seg_buf_r is not None: string.strncpy(seg_buf_r, prm_annots + seg_start2, seg_len2) seg_buf_r[seg_len2] = '\0' - rt: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, seg_buf_r, i32_ty, i8_ptr_ty) + rt: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, seg_buf_r, i32_ty, i8_ptr_ty, src_sha1) if rt is not None: param_ty_r = rt stdlib.free(seg_buf_r) mlirlite.add_param(pool, func, param_ty_r, "arg") @@ -1041,7 +1041,7 @@ def _resolve_vpi_param_annot_seg(pool: memhub.MemBuddy*, src_sha1: str, cls_name if seg_buf_p is None: return None string.strncpy(seg_buf_p, prm_annots_p + seg_start_p, seg_len_p) seg_buf_p[seg_len_p] = '\0' - seg_ty_p: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, seg_buf_p, i32_ty, i8_ptr_ty) + seg_ty_p: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, seg_buf_p, i32_ty, i8_ptr_ty, src_sha1) stdlib.free(seg_buf_p) return seg_ty_p @@ -1913,9 +1913,29 @@ def _vpi_struct_or_enum(seg: str) -> mlirlite.LLVMType*: return struct_ty +# _vpi_struct_or_enum_in_module - module-aware variant of _vpi_struct_or_enum. +# A .vpi return/param annotation carries the DEFINING module's bare name (`-> Point`), so a +# bare-name registry lookup returns the first same-named struct from another module and the +# caller gets the wrong layout (cross-module same-name struct returned by value). Resolve by +# (name, src_sha1) first; only fall back to the context-free lookup when the module has no entry. +def _vpi_struct_or_enum_in_module(seg: str, src_sha1: str) -> mlirlite.LLVMType*: + """Module-aware struct/enum lookup for a .vpi annotation segment.""" + if seg is None: return None + if src_sha1 is not None: + se_m: GenTypes.StructEntry* = GenApi.find_struct_by_module(seg, src_sha1) + if se_m is not None: + en_m: mlirlite.LLVMType* = GenApi.get_enum_base_type_in_module(seg, src_sha1) + # struct forward placeholder must not shadow a real enum in the same module + if se_m.IsForwardDecl != 0: return en_m + if se_m.Ty is not None: return se_m.Ty + en_m2: mlirlite.LLVMType* = GenApi.get_enum_base_type_in_module(seg, src_sha1) + if en_m2 is not None: return en_m2 + return _vpi_struct_or_enum(seg) + + # _resolve_single_vpi_type_str - resolve a single type segment to LLVMType. # Pure string parsing, no AST nodes. -def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.LLVMType*: +def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str, src_sha1: str = None) -> mlirlite.LLVMType*: """Resolve a single type segment to LLVMType; None if unrecognized.""" if seg is None or pool is None: return None # Source-syntax descriptors / value references: `const T` / `volatile T` / `&T` / @@ -1930,19 +1950,19 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L if trim_buf is None: return None string.strncpy(trim_buf, seg + seg_sp0, seg_ep0 - seg_sp0) trim_buf[seg_ep0 - seg_sp0] = '\0' - trim_ty: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, trim_buf) + trim_ty: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, trim_buf, src_sha1) stdlib.free(trim_buf) return trim_ty if seg_l0 - seg_sp0 >= 6 and string.strncmp(seg + seg_sp0, "const ", 6) == 0: - return _resolve_single_vpi_type_str(pool, seg + seg_sp0 + 6) + return _resolve_single_vpi_type_str(pool, seg + seg_sp0 + 6, src_sha1) if seg_l0 - seg_sp0 >= 9 and string.strncmp(seg + seg_sp0, "volatile ", 9) == 0: - return _resolve_single_vpi_type_str(pool, seg + seg_sp0 + 9) + return _resolve_single_vpi_type_str(pool, seg + seg_sp0 + 9, src_sha1) if seg_sp0 < seg_l0 and seg[seg_sp0] == '&': # Value-reference parameter ABI: &T / &temp T -> T* (both modes use a pointer). ref_sp1: sizet = seg_sp0 + 1 while ref_sp1 < seg_l0 and (seg[ref_sp1] == ' ' or seg[ref_sp1] == '\t'): ref_sp1 += 1 if seg_l0 - ref_sp1 >= 5 and string.strncmp(seg + ref_sp1, "temp ", 5) == 0: ref_sp1 += 5 - inner_ref: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg + ref_sp1) + inner_ref: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg + ref_sp1, src_sha1) if inner_ref is not None: return mlirlite.Ptr(pool, inner_ref) return None if seg_sp0 < seg_l0 and seg[seg_sp0] == '[': @@ -1950,7 +1970,7 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L arr_rb: sizet = seg_sp0 + 1 while arr_rb < seg_l0 and seg[arr_rb] != ']': arr_rb += 1 if arr_rb < seg_l0: - inner_arr: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg + arr_rb + 1) + inner_arr: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg + arr_rb + 1, src_sha1) if inner_arr is not None: return mlirlite.Ptr(pool, inner_arr) return None # Canonical pointer form is `T*` (also rawptr[T]); rewrite trailing `*` @@ -1978,7 +1998,7 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L star_buf_ps[star_off_ps + star_base_end_ps] = '\0' for star_k_ps in range(int(star_cnt_ps)): string.strcat(star_buf_ps, "]") - star_ty_ps: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, star_buf_ps) + star_ty_ps: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, star_buf_ps, src_sha1) stdlib.free(star_buf_ps) return star_ty_ps i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) @@ -2030,7 +2050,7 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L if inner_mk is not None: string.strncpy(inner_mk, seg + lb_off + 1, inner_len_mk) inner_mk[inner_len_mk] = '\0' - inner_ty_mk: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, inner_mk) + inner_ty_mk: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, inner_mk, src_sha1) stdlib.free(inner_mk) stdlib.free(mk_buf) return inner_ty_mk @@ -2045,7 +2065,7 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L if inner_buf_rp is not None: string.strncpy(inner_buf_rp, seg + 7, inner_len_rp) inner_buf_rp[inner_len_rp] = '\0' - inner_ty_rp: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, inner_buf_rp) + inner_ty_rp: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, inner_buf_rp, src_sha1) stdlib.free(inner_buf_rp) if inner_ty_rp is not None: return mlirlite.Ptr(pool, inner_ty_rp) return None @@ -2059,7 +2079,7 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L # Bare class / enum name (e.g. `-> Lexer*` written in the defining module's own vpi): # mirror _resolve_vpi_annot_direct and consult the struct/enum registries, otherwise # `X*` -> rawptr[X] fails on the inner name and the whole return type is lost. - bare_cls_ty: mlirlite.LLVMType* = _vpi_struct_or_enum(seg) + bare_cls_ty: mlirlite.LLVMType* = _vpi_struct_or_enum_in_module(seg, src_sha1) if bare_cls_ty is not None: return bare_cls_ty return None # module-qualified class name "memhub.MemBuddy" -> use last segment to look up struct @@ -2071,7 +2091,7 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L if mod_struct_ty is not None: return mod_struct_ty return None # bare class name -> global struct registry - bare_struct_ty: mlirlite.LLVMType* = _vpi_struct_or_enum(seg) + bare_struct_ty: mlirlite.LLVMType* = _vpi_struct_or_enum_in_module(seg, src_sha1) if bare_struct_ty is not None: return bare_struct_ty # bare typedef name (e.g. VOIDPTR): TypedefT dynamic table first, on miss fall back to map_builtin_type mapped_bare: mlirlite.LLVMType* = GenApi.resolve_typedef_or_map(pool, seg, None, None) @@ -2081,7 +2101,7 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L # _resolve_vpi_ret_annot_str - resolve a vpi return-annotation string to LLVMType. # Pure string parsing (no AST nodes) to avoid corrupted kind() from cross-module AST reuse. -def _resolve_vpi_ret_annot_str(pool: memhub.MemBuddy*, ret_annot: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*: +def _resolve_vpi_ret_annot_str(pool: memhub.MemBuddy*, ret_annot: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*, src_sha1: str = None) -> mlirlite.LLVMType*: """Resolve a vpi return-annotation string directly to LLVMType without building AST nodes.""" if ret_annot is None or pool is None: return None # `T1 | T2` unions were retired; a stub containing one must be regenerated. @@ -2112,7 +2132,7 @@ def _resolve_vpi_ret_annot_str(pool: memhub.MemBuddy*, ret_annot: str, i32_ty: m if seg_buf_t is None: return None string.strncpy(seg_buf_t, ret_annot + 6 + seg_s_t, seg_len_t) seg_buf_t[seg_len_t] = '\0' - seg_ty_t: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg_buf_t) + seg_ty_t: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg_buf_t, src_sha1) stdlib.free(seg_buf_t) if seg_ty_t is None: return None tup_node_t: mlirlite.ParamNode* = mlirlite.new_param_node(pool, seg_ty_t) @@ -2136,7 +2156,7 @@ def _resolve_vpi_ret_annot_str(pool: memhub.MemBuddy*, ret_annot: str, i32_ty: m # A stub return annotation is a single type. `T*` / `T**` and `const` / `volatile` # prefixes are handled by the shared single-segment resolver. - return _resolve_single_vpi_type_str(pool, ret_annot) + return _resolve_single_vpi_type_str(pool, ret_annot, src_sha1) # VpiReader - .vpi AST parser. Reads function signatures and TypedefT definitions from @@ -2563,7 +2583,195 @@ def _VpiParseIntRange(vbuf: bytes, s: sizet, e: sizet, out_v: int64*) -> int: out_v[0] = v return 1 +# _VpiIsFloatLit - whether vbuf[a:b] is a float literal (optional sign, digits, '.', exponent). +def _VpiIsFloatLit(vbuf: bytes, a: sizet, b: sizet) -> int: + """1 when the span is a plain float literal (1.5 / 1e3 / -2.5e-4), 0 otherwise.""" + if vbuf is None or b <= a: return 0 + i: sizet = a + if vbuf[i] == 0x2D or vbuf[i] == 0x2B: i += 1 + digits: int = 0 + dot: int = 0 + has_frac: int = 0 + while i < b: + chf: uint8 = vbuf[i] + if chf >= 0x30 and chf <= 0x39: digits += 1 + elif chf == 0x2E and dot == 0: + dot = 1 + has_frac = 1 + else: break + i += 1 + if i < b and (vbuf[i] == 0x65 or vbuf[i] == 0x45): + has_frac = 1 + i += 1 + if i < b and (vbuf[i] == 0x2D or vbuf[i] == 0x2B): i += 1 + ed: int = 0 + while i < b: + chfe: uint8 = vbuf[i] + if chfe >= 0x30 and chfe <= 0x39: ed += 1 + else: return 0 + i += 1 + if ed == 0: return 0 + if i != b: return 0 + if digits == 0: return 0 + # A plain digit run is an INTEGER default (handled by _VpiParseIntRange); only a dot or + # exponent makes it a float. Without this guard "0"/"12" would be stored as 0.0 f64 and an + # int parameter would receive an f64 operand (llvm.call type mismatch). + if has_frac == 0: return 0 + return 1 + + # _VpiFillDefaultLit - 把第 di 个缺省值字面量写入快照数组;1=成功(可重建) +# ---- aggregate default literals (struct constructor defaults) ---- +# Encoded token format (one token per struct field, '|' separated): +# i: d: s:: b:<0|1> n:0 +# The call site rebuilds the aggregate from the parameter's struct type, so the .vpi only +# needs the literal values (the constructor name is not used). + + +def _VpiEncPut(dst: bytes, cap: sizet, pos: sizet*, src: char*, n: sizet) -> int: + """Append n bytes of src at dst[pos]; keep dst NUL-terminated. 0 = overflow/argument error.""" + if dst is None or pos is None or src is None: return 0 + if n == 0: return 1 + if pos[0] + n + 1 > cap: return 0 + string.memcpy(dst + pos[0], src, n) + pos[0] += n + dst[pos[0]] = '\0' + return 1 + + +def _VpiEncInt(dst: bytes, cap: sizet, pos: sizet*, v: int64) -> int: + """Append the decimal form of v.""" + tmp: str = stdlib.malloc(32) + if tmp is None: return 0 + viperlib.snprintf(tmp, 32, "%lld", v) + r: int = _VpiEncPut(dst, cap, pos, tmp, string.strlen(tmp)) + stdlib.free(tmp) + return r + + +def _VpiEncodeAggArg(pool: memhub.MemBuddy*, vbuf: bytes, s: sizet, e: sizet, dst: bytes, cap: sizet, pos: sizet*) -> int: + """Encode one scalar constructor argument (already trimmed) into one token.""" + a: sizet = s + b: sizet = e + while a < b and (vbuf[a] == 0x20 or vbuf[a] == 0x09): a += 1 + while b > a and (vbuf[b - 1] == 0x20 or vbuf[b - 1] == 0x09): b -= 1 + if b <= a: return 0 + ch0: uint8 = vbuf[a] + if ch0 == 0x22 or ch0 == 0x27: + if b - a < 2 or vbuf[b - 1] != ch0: return 0 + ci: sizet = a + 1 + while ci < b - 1: + if vbuf[ci] == 0x5C: return 0 # escapes are not supported here + ci += 1 + clen: sizet = b - a - 2 + if _VpiEncPut(dst, cap, pos, "s:", 2) == 0: return 0 + if _VpiEncInt(dst, cap, pos, int64(clen)) == 0: return 0 + if _VpiEncPut(dst, cap, pos, ":", 1) == 0: return 0 + return _VpiEncPut(dst, cap, pos, vbuf + a + 1, clen) + if b - a == 4 and string.strncmp(vbuf + a, "True", 4) == 0: return _VpiEncPut(dst, cap, pos, "b:1", 3) + if b - a == 5 and string.strncmp(vbuf + a, "False", 5) == 0: return _VpiEncPut(dst, cap, pos, "b:0", 3) + if b - a == 4 and string.strncmp(vbuf + a, "None", 4) == 0: return _VpiEncPut(dst, cap, pos, "n:0", 3) + if _VpiIsFloatLit(vbuf, a, b) != 0: + if _VpiEncPut(dst, cap, pos, "d:", 2) == 0: return 0 + return _VpiEncPut(dst, cap, pos, vbuf + a, b - a) + iv: int64* = pool.alloc(8) + if iv is None: return 0 + if _VpiParseIntRange(vbuf, a, b, iv) == 0: return 0 + if _VpiEncPut(dst, cap, pos, "i:", 2) == 0: return 0 + return _VpiEncInt(dst, cap, pos, iv[0]) + + +def _VpiEncodeAggDefault(pool: memhub.MemBuddy*, vbuf: bytes, a0: sizet, b0: sizet, kinds: int*, ints: int64*, strs: sizet*, di: int) -> int: + """Encode `Name(arg, ...)` as an aggregate literal snapshot; 0 = not a flat constructor.""" + if pool is None or vbuf is None or kinds is None or ints is None or strs is None or di < 0: return 0 + a: sizet = a0 + b: sizet = b0 + while a < b and (vbuf[a] == 0x20 or vbuf[a] == 0x09): a += 1 + while b > a and (vbuf[b - 1] == 0x20 or vbuf[b - 1] == 0x09): b -= 1 + if b - a < 3 or vbuf[b - 1] != 0x29: return 0 # ')' + # Constructor name: identifier characters plus '.' qualifier; then '('. + lp: sizet = a + has_name: int = 0 + while lp < b: + ch: uint8 = vbuf[lp] + if ch == 0x28: break + ok_ch: int = 0 + if (ch >= 0x41 and ch <= 0x5A) or (ch >= 0x61 and ch <= 0x7A) or ch == 0x5F: ok_ch = 1 + elif ch >= 0x30 and ch <= 0x39: ok_ch = 1 + elif ch == 0x2E: ok_ch = 1 + elif ch == 0x20 or ch == 0x09: pass + else: return 0 + if ok_ch != 0: has_name = 1 + lp += 1 + if lp >= b - 1 or has_name == 0: return 0 + cap: sizet = 4096 + dst: bytes = CompilerFactory.factory.PermPool.alloc(cap) + if dst is None: return 0 + dst[0] = '\0' + pos: sizet = 0 + ai: sizet = lp + 1 + first: int = 1 + while True: + while ai < b - 1 and (vbuf[ai] == 0x20 or vbuf[ai] == 0x09): ai += 1 + if ai >= b - 1: break + j: sizet = ai + quote: uint8 = 0 + while j < b - 1: + c: uint8 = vbuf[j] + if quote != 0: + if c == 0x5C: j += 1 + elif c == quote: quote = 0 + j += 1 + continue + if c == 0x22 or c == 0x27: quote = c + elif c == 0x28 or c == 0x29: return 0 # nested calls are not supported + elif c == 0x2C: break + j += 1 + if first == 0: + if _VpiEncPut(dst, cap, &pos, "|", 1) == 0: return 0 + if _VpiEncodeAggArg(pool, vbuf, ai, j, dst, cap, &pos) == 0: return 0 + first = 0 + ai = j + 1 + kinds[di] = GenTypes.CONST_AGG_DEFAULT + ints[di] = 0 + strs[di] = sizet(dst) + return 1 + + +def _VpiEncodeNameDefault(pool: memhub.MemBuddy*, vbuf: bytes, a0: sizet, b0: sizet, kinds: int*, ints: int64*, strs: sizet*, di: int) -> int: + """Encode a bare/qualified identifier default (enum member or CDefine) as text. + + The .vpi default pre-scan runs before enums are registered, so the value cannot be resolved + here. Keep the text in the snapshot; the call site resolves it after enum registration. + """ + if pool is None or vbuf is None or kinds is None or ints is None or strs is None or di < 0: return 0 + a: sizet = a0 + b: sizet = b0 + while a < b and (vbuf[a] == 0x20 or vbuf[a] == 0x09): a += 1 + while b > a and (vbuf[b - 1] == 0x20 or vbuf[b - 1] == 0x09): b -= 1 + if b <= a or b - a > 200: return 0 + i: sizet = a + has_id: int = 0 + while i < b: + ch: uint8 = vbuf[i] + ok: int = 0 + if (ch >= 0x41 and ch <= 0x5A) or (ch >= 0x61 and ch <= 0x7A) or ch == 0x5F: ok = 1 + elif ch >= 0x30 and ch <= 0x39: ok = 1 + elif ch == 0x2E: ok = 1 + else: return 0 + if ch != 0x2E: has_id = 1 + i += 1 + if has_id == 0: return 0 + copy: str = CompilerFactory.factory.PermPool.alloc(b - a + 1) + if copy is None: return 0 + string.strncpy(copy, vbuf + a, b - a) + copy[b - a] = '\0' + kinds[di] = GenTypes.CONST_EXPR_TEXT + ints[di] = 0 + strs[di] = sizet(copy) + return 1 + + def _VpiFillDefaultLit(pool: memhub.MemBuddy*, vbuf: bytes, s: sizet, e: sizet, kinds: int*, ints: int64*, strs: sizet*, di: int) -> int: """把第 di 个缺省值字面量写入快照;0=不可重建(字符串转义/复杂表达式)""" if kinds is None or ints is None or strs is None or di < 0: return 0 @@ -2605,13 +2813,36 @@ def _VpiFillDefaultLit(pool: memhub.MemBuddy*, vbuf: bytes, s: sizet, e: sizet, ints[di] = 0 strs[di] = 0 return 1 + # Float literal: store the IEEE-754 bit pattern (int64 slot); the snapshot rebuild + # reinterprets it back to double, then the call-site coercion narrows to the param type. + if _VpiIsFloatLit(vbuf, a, b) != 0: + flen: sizet = b - a + fbuf: bytes = stdlib.malloc(flen + 1) + if fbuf is None: return 0 + string.strncpy(fbuf, vbuf + a, flen) + fbuf[flen] = '\0' + dv: double = string.atof(fbuf) + stdlib.free(fbuf) + uv: uint64 = 0 + *&uv = *&dv + kinds[di] = ast.CONST_FLOAT + ints[di] = int64(uv) + strs[di] = 0 + return 1 + # Struct-constructor literal (e.g. Vec2(1.0, 2.0)): encode the scalar field values so a + # cross-module call site can rebuild the aggregate from its parameter's struct type. + if _VpiEncodeAggDefault(pool, vbuf, a, b, kinds, ints, strs, di) != 0: + return 1 iv: int64* = pool.alloc(8) if iv is None: return 0 - if _VpiParseIntRange(vbuf, a, b, iv) == 0: return 0 - kinds[di] = ast.CONST_INT - ints[di] = iv[0] - strs[di] = 0 - return 1 + if _VpiParseIntRange(vbuf, a, b, iv) != 0: + kinds[di] = ast.CONST_INT + ints[di] = iv[0] + strs[di] = 0 + return 1 + # Identifier default (enum member `Flags.Name_` / CDefine): keep the text; the call site + # resolves it after enum registration (the pre-scan runs before enums exist). + return _VpiEncodeNameDefault(pool, vbuf, a, b, kinds, ints, strs, di) # _VpiParseParamDefaults - 解析参数区间:返回 (参数个数 << 8) | 缺省值个数;-1=有不可重建缺省值 def _VpiParseParamDefaults(pool: memhub.MemBuddy*, vbuf: bytes, ps: sizet, pe: sizet, kinds: int*, ints: int64*, strs: sizet*) -> int: @@ -2718,8 +2949,7 @@ def _ScanVpiDefaults(pool: memhub.MemBuddy*, vbuf: bytes, buf_len: sizet, sha1: stdlib.free(strs) return reg -# 单个 .vpi 的枚举成员上限(与 ENUM_MEMBER_MAX 同量级) -define VPI_ENUM_MEMBER_MAX = 128 +# 单个 .vpi 的枚举成员上限:直接复用 GenTypes.ENUM_MEMBER_MAX(唯一定义点),不再维护本地副本。 # _vpi_slice_copy - 把 buf[a:b] 拷成 NUL 结尾的字符串(分配自 pool) @@ -2746,9 +2976,9 @@ def _ScanVpiEnumsText(pool: memhub.MemBuddy*, vbuf: bytes, vlen: sizet, sha1: st # 类型对象与枚举表都要活到进程结束:统一挂在 PermPool 上(EnumTable 本身就在 PermPool) ty_pool: memhub.MemBuddy* = pool if CompilerFactory.factory.PermPool is not None: ty_pool = CompilerFactory.factory.PermPool - nm_buf: sizet* = pool.alloc(8 * VPI_ENUM_MEMBER_MAX) - ty_buf: sizet* = pool.alloc(8 * VPI_ENUM_MEMBER_MAX) - val_buf: int64* = pool.alloc(8 * VPI_ENUM_MEMBER_MAX) + nm_buf: sizet* = pool.alloc(8 * GenTypes.ENUM_MEMBER_MAX) + ty_buf: sizet* = pool.alloc(8 * GenTypes.ENUM_MEMBER_MAX) + val_buf: int64* = pool.alloc(8 * GenTypes.ENUM_MEMBER_MAX) if nm_buf is None or ty_buf is None or val_buf is None: return 0 reg: int = 0 i: sizet = 0 @@ -2800,7 +3030,7 @@ def _ScanVpiEnumsText(pool: memhub.MemBuddy*, vbuf: bytes, vlen: sizet, sha1: st ty_txt: str = _vpi_slice_copy(pool, vbuf, ts, ty_end) # `State` 是自动编号成员的占位类型(源码端 t.State -> i32) if string.strcmp(ty_txt, "State") != 0: - rty: mlirlite.LLVMType* = _resolve_single_vpi_type_str(ty_pool, ty_txt) + rty: mlirlite.LLVMType* = _resolve_single_vpi_type_str(ty_pool, ty_txt, sha1) rb: int = _vpi_int_ty_bits(rty) # 枚举存储必须可按字节寻址:bool 在定义端按 8 位算 if rb == 1: mty = mlirlite.Int8(ty_pool) @@ -2821,7 +3051,7 @@ def _ScanVpiEnumsText(pool: memhub.MemBuddy*, vbuf: bytes, vlen: sizet, sha1: st if neg != 0: pv = -pv cur = pv next_val = cur + 1 - if cnt < VPI_ENUM_MEMBER_MAX: + if cnt < GenTypes.ENUM_MEMBER_MAX: nm_buf[cnt] = sizet(mname) ty_buf[cnt] = sizet(mty) val_buf[cnt] = cur @@ -3674,7 +3904,7 @@ def _inject_reexport_class_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMMod # _vpi_tattr_name / _vpi_class_has_packed - detect the vpi class packed decorator. -# The stub keeps @c.Attribute(t.attr.packed); struct injection must mirror the packed flag (B5). +# The stub keeps @Attribute(attr.packed); struct injection must mirror the packed flag (B5). def _vpi_tattr_name(node: ast.AST*, attr_name: str) -> int: """Extract the outermost attribute name from t.attr. / t.attr.(args) and compare.""" if node is None: return 0 @@ -4029,7 +4259,7 @@ def _inject_class_struct_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModul existing_c.FieldCount = 0 # 6. register into the current module (emit the type def) + global StructTable + visibility mlirlite.module_add_named_type(mod, pool, struct_ty_c) - # B5: vpi keeps @c.Attribute(t.attr.packed); cross-module injected structs must be packed too + # B5: vpi keeps @Attribute(attr.packed); cross-module injected structs must be packed too # (<{...}>), else the referencing module computes GEPs with a non-packed layout (fields misread). if _vpi_class_has_packed(class_cd.decorator_list) != 0: mlirlite.module_set_named_type_packed(mod, pool, struct_ty_c, 1) # Register under the flat real_name: cross-module `Outer.Inner` lookups query the flat-name @@ -4111,7 +4341,9 @@ def _AggregateParamTy(mod: mlirlite.LLVMModule*, pool: memhub.MemBuddy*, ty: mli pass if is_opaque_ap == 0 and GenApi.find_struct_by_type(ty) is not None: _ensure_full_value_types(mod, pool, ty) - return ty + # Win64: iN for exactly 1/2/4/8 bytes, Ptr(struct) (byref copy) for every other size. + # Non-Windows keeps the struct value and lets the backend apply the platform ABI. + return GenApi.extern_c_param_agg_ty(pool, ty) return mlirlite.Int64(pool) ptr_ty_ap: mlirlite.LLVMType* = mlirlite.Ptr(pool, ty) mlirlite.module_ensure_opaque_for_type(mod, pool, ptr_ty_ap) @@ -4244,7 +4476,7 @@ def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, # parse) and resolve it directly, avoiding corrupted kind() from cross-module AST reuse. ret_ty: mlirlite.LLVMType* = None ret_annot_fd: str = _lookup_vpi_ret_annot(src_sha1, None, fd.name) - if ret_annot_fd is not None: ret_ty = _resolve_vpi_ret_annot_str(pool, ret_annot_fd, i32_ty, i8_ptr_ty) + if ret_annot_fd is not None: ret_ty = _resolve_vpi_ret_annot_str(pool, ret_annot_fd, i32_ty, i8_ptr_ty, src_sha1) if ret_ty is None and fd.returns is not None: ret_ty = GenApi.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans) # if the return type is a pure t.State (no real type), use Void @@ -4272,6 +4504,12 @@ def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, # missing; an i32 default here would split "declare i32" from "define void". if ret_ty is None: ret_ty = mlirlite.Void(pool) + # C extern returning an aggregate: remember the symbol so the call site emits the C ABI + # (Win64 integer register <=8B / hidden sret pointer otherwise) instead of Viper's own + # aggregate-return convention. ExportT alone stays on the Viper convention. + if (has_extern != 0 or has_state != 0) and ret_ty.kind() == mlirlite.LLVMType.Struct: + GenApi.register_extern_agg(mangled_name) + # A value struct return type (e.g. Mat4) needs a full definition: declaring %Mat4 opaque makes # llc report "use of undefined type". Register the full definition. _ensure_full_value_types(mod, pool, ret_ty) @@ -4297,6 +4535,7 @@ def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, # convergence point: longjmp back to the REPL loop in a REPL session (diagnostics printed) VLogger.repl_abort() pi: sizet + decl_pi: int = 0 for pi in range(int(param_count)): ag: ast.Arg* = ast.Arg*(args_list.get(pi)) if ag is None: continue @@ -4313,6 +4552,9 @@ def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, # Bare struct param: C ABI by value / internal Ptr(struct) # (criterion and fallback in _AggregateParamTy; keep the three sites in sync) param_ty = _AggregateParamTy(mod, pool, pseg_ty, agg_is_c_abi) + # Win64 byref: mark the call site so it passes a copy, not the variable address. + if agg_is_c_abi != 0 and param_ty is not None and param_ty.kind() == mlirlite.LLVMType.Ptr: + GenApi.register_extern_byref_param(mangled_name, decl_pi) case mlirlite.LLVMType.Array(aelem_ty, _): param_ty = mlirlite.Ptr(pool, aelem_ty) case _: @@ -4325,6 +4567,8 @@ def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, match resolved_ty: case mlirlite.LLVMType.Struct(_, _, _): param_ty = _AggregateParamTy(mod, pool, resolved_ty, agg_is_c_abi) + if agg_is_c_abi != 0 and param_ty is not None and param_ty.kind() == mlirlite.LLVMType.Ptr: + GenApi.register_extern_byref_param(mangled_name, decl_pi) case mlirlite.LLVMType.Array(aelem_ty, _): param_ty = mlirlite.Ptr(pool, aelem_ty) case _: @@ -4334,6 +4578,7 @@ def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, # to i8* (no annotation is a legitimate default and keeps i8*). GenApi.fatal_error(ag.annotation, f"extern declaration '{fd.name}' parameter '{pname}' type annotation failed to resolve", "NameError") mlirlite.add_param(pool, func, param_ty, pname) + decl_pi += 1 # Text-cache repair: a corrupted vpi tree truncates args, leaving too few declare params; # complete them from the vpi text param annotations so declare matches define. param_count = _repair_declare_params_from_text(pool, func, src_sha1, None, fd.name, param_count, i32_ty, i8_ptr_ty) @@ -4395,7 +4640,7 @@ def _inject_method_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, m_ # parse); resolving m_fd.returns directly may yield garbage from a corrupted vpi tree node. ret_ty: mlirlite.LLVMType* = None ret_annot: str = _lookup_vpi_ret_annot(src_sha1, cls_name, m_fd.name) - if ret_annot is not None: ret_ty = _resolve_vpi_ret_annot_str(pool, ret_annot, i32_ty, i8_ptr_ty) + if ret_annot is not None: ret_ty = _resolve_vpi_ret_annot_str(pool, ret_annot, i32_ty, i8_ptr_ty, src_sha1) if ret_ty is None and m_fd.returns is not None: ret_ty = GenApi.resolve_annotation_type(pool, m_fd.returns, imported_modules, from_imports, trans) # infer __new__ without annotation as a class pointer (it returns a self pointer; i32 would truncate) diff --git a/App/lib/Stub/StubMerger/StubMergerBuild.vp b/App/lib/Stub/StubMerger/StubMergerBuild.vp index 05eea07..6cef003 100644 --- a/App/lib/Stub/StubMerger/StubMergerBuild.vp +++ b/App/lib/Stub/StubMerger/StubMergerBuild.vp @@ -5,8 +5,7 @@ import memhub import string import stdlib import mlirlite -import win32.win32file as win32file -import win32.win32base as win32base +import os import io import viperlib import sys @@ -629,29 +628,15 @@ define OBJ_CACHE_STAMP_TAG_CAP = 128 def _CompilerBuildTag(out: bytes, cap: sizet) -> int: - """Serialize the running compiler's build id (exe size + last write time) into out.""" + """Serialize the running compiler's build id (exe size + mtime) into out.""" if out is None or cap < 32: return 0 out[0] = '\0' - EXE_CAP: sizet = sizet(1024) - exe_path: bytes = stdlib.malloc(EXE_CAP) + exe_path: str = sys.executable() if exe_path is None: return 0 - string.memset(exe_path, 0, EXE_CAP) - n: uint32 = win32file.GetModuleFileNameA(None, exe_path, uint32(1024)) - ok: int = 0 - if int(n) > 0 and int(n) < 1024: - fd_size: sizet = sizet(win32file.WIN32_FIND_DATAA.__sizeof__()) - fd: win32file.WIN32_FIND_DATAA* = (win32file.WIN32_FIND_DATAA*)(stdlib.malloc(fd_size)) - if fd is not None: - string.memset(fd, 0, fd_size) - fh: win32base.HANDLE = win32file.FindFirstFileA(exe_path, fd) - if fh != win32base.INVALID_HANDLE_VALUE: - win32file.FindClose(fh) - viperlib.snprintf(out, cap, "%u.%u-%u.%u", fd.nFileSizeLow, fd.nFileSizeHigh, - fd.ftLastWriteTime.dwLowDateTime, fd.ftLastWriteTime.dwHighDateTime) - ok = 1 - stdlib.free(fd) - stdlib.free(exe_path) - return ok + st: os.stat_result* = os.stat(exe_path) + if st is None: return 0 + viperlib.snprintf(out, cap, "%lld.%lld", st.st_size, st.st_mtime) + return 1 def _BuildObjCacheStamp(out: bytes, cap: sizet, shas: bytes, incdirs: bytes, count: int) -> sizet: diff --git a/App/lib/Stub/StubMerger/StubMergerCollect.vp b/App/lib/Stub/StubMerger/StubMergerCollect.vp index 8fe1663..254d4e9 100644 --- a/App/lib/Stub/StubMerger/StubMergerCollect.vp +++ b/App/lib/Stub/StubMerger/StubMergerCollect.vp @@ -1,130 +1,128 @@ -from stdint import * -import memhub -import string -import stdlib -import mlirlite -import win32.win32file as win32file -import win32.win32base as win32base -import io -import viperlib -import sys -import ast -import lib.core.VLogger as VLogger -import lib.core.IncludesScanner as IncludesScanner -import lib.core.Gen.Translator as HandlesTranslator -import lib.Phase.BuildPipeline as BuildPipeline -import lib.Stub.StubGen.VpiReader as VpiReader -import lib.Projectrans.Utils as Utils -import lib.Projectrans.Config as Config - - -import lib.Stub.StubMerger.StubMergerStore as StubMergerStore -import lib.Stub.StubMerger.StubMergerTopo as StubMergerTopo -import lib.Stub.StubMerger.StubMergerIR as StubMergerIR -import lib.Stub.StubMerger.StubMergerBuild as StubMergerBuild -import lib.Stub.StubMerger as StubMerger - -# StubMergerCollect - slice-path construction and stub-file SHA1 collection. -# Functions live in this module; cross-module globals are accessed via StubMerger._xxx. - -# _sliced_path - build a slice path (stdlib.malloc; caller frees; creates subdir when level>0). -# Shards by Config.Sha1SliceLevel into SHA1-prefix subdirs; returns the full path or None on failure. -def _sliced_path(temp_dir: str, td_len: sizet, sha1: str, ext: str) -> str: - """Build a SHA1-sliced path (stdlib.malloc; caller frees; creates the subdir).""" - if temp_dir is None or sha1 is None or ext is None: return None - sha1_len: sizet = string.strlen(sha1) - ext_len: sizet = string.strlen(ext) - - # Subdir prefix: reuse Config.slice_subdir (the authoritative SHA1 slice impl; mbuddy-allocated, no free) - subdir_buf: str = Config.slice_subdir(sha1, Config.Sha1SliceLevel) - sub_len: sizet = 0 - if subdir_buf is not None: - sub_len = string.strlen(subdir_buf) - - # Ensure subdir {temp_dir}/{subdir} exists. - # Otherwise io.File(MODE.W) fails silently (file.closed==True) and the reader returns 0. - dir_path_len: sizet = td_len + sub_len + 2 - dir_path: bytes = stdlib.malloc(dir_path_len) - if dir_path is not None: - viperlib.snprintf(dir_path, dir_path_len, "%s/%s", temp_dir, subdir_buf) - BuildPipeline.ensure_dir(dir_path) - stdlib.free(dir_path) - - # Build the full path - path: str = None - path_len: sizet = 0 - if sub_len > 0: - path_len = td_len + sub_len + sha1_len + ext_len + 4 - path = stdlib.malloc(path_len) - if path is not None: viperlib.snprintf(path, path_len, "%s/%s/%s.%s", temp_dir, subdir_buf, sha1, ext) - else: - path_len = td_len + sha1_len + ext_len + 3 - path = stdlib.malloc(path_len) - if path is not None: viperlib.snprintf(path, path_len, "%s/%s.%s", temp_dir, sha1, ext) - - return path - -# _is_in_sha1_set - check whether a SHA1 is in the set. -def _is_in_sha1_set(sha1: str, sha1_set: str, set_count: int) -> int: - """Whether sha1 is present in the set.""" - if sha1 is None or sha1_set is None: return 0 - for i in range(int(set_count)): - if string.strcmp(sha1_set + i * 17, sha1) == 0: return 1 - return 0 -# Sha1MapPath - _sha1_map.txt 的落盘路径:放在**构建目录**(temp 的上一级), -# 便于直接按 SHA1 反查源文件;temp 内的 sliced 产物(.ll/.vpi/.meta)位置不变。 -# 返回 stdlib 缓冲区(调用方 free);temp_dir 无上一级(相对路径)时退回 temp 目录。 -def Sha1MapPath(pool: memhub.MemBuddy*, temp_dir: str) -> str: - """返回 {temp_dir 的上一级}/_sha1_map.txt""" - if temp_dir is None: return None - td: sizet = string.strlen(temp_dir) - end: sizet = td - while end > 0 and (temp_dir[end - 1] == '/' or temp_dir[end - 1] == '\\'): end -= 1 - cut: sizet = end - while cut > 0 and temp_dir[cut - 1] != '/' and temp_dir[cut - 1] != '\\': cut -= 1 - parent_len: sizet = 0 - if cut > 0: parent_len = cut - 1 - else: parent_len = end # 没有上一级(相对路径):退回 temp 目录本身 - buf: bytes = stdlib.malloc(parent_len + 32) - if buf is None: return None - if parent_len > 0: string.strncpy(buf, temp_dir, parent_len) - buf[parent_len] = '\0' - viperlib.snprintf(buf + parent_len, 32, "/_sha1_map.txt") - return buf -# WriteIncludesSha1Map - write includes scan results to _sha1_map.txt (human-readable debug output only). -# Internally the in-memory store filled by PopulateSha1MapStore is used; returns 0 on success, non-zero on failure. -def WriteIncludesSha1Map(mb: memhub.MemBuddy*, temp_dir: str, scan_result: IncludesScanner.ScanResult*, filter_set: str, filter_count: int) -> int: - """Write the includes scan result to _sha1_map.txt (human-readable debug output).""" - if temp_dir is None or scan_result is None: return 1 - - # Build the path {temp_dir 的上一级}/_sha1_map.txt - map_path: bytes = Sha1MapPath(mb, temp_dir) - if map_path is None: return 1 - - # Open the file for writing (CREATE_ALWAYS) - f: io.File* = io.File(map_path, io.MODE.W) - stdlib.free(map_path) - if f.closed: return 1 - - # Write each include entry: {sha1}:includes/{rel_path}\n - entry_size_w: sizet = IncludesScanner.FileEntry.__sizeof__() - line_buf: bytes = stdlib.malloc(512) - if line_buf is None: - f.close() - return 1 - - written_count: int = 0 - for i in range(int(scan_result.Count)): - ea_w: uint64 = uint64(scan_result.Entries) + i * entry_size_w - ent_w: IncludesScanner.FileEntry* = (IncludesScanner.FileEntry*)(ea_w) - if ent_w is None or ent_w.Sha1 is None or ent_w.RelPath is None: continue - # When a filter set is provided, write only its entries - if filter_set is not None and filter_count > 0: - if _is_in_sha1_set(ent_w.Sha1, filter_set, filter_count) == 0: continue - viperlib.snprintf(line_buf, 512, "%s:includes/%s\n", ent_w.Sha1, ent_w.RelPath) - line_len_w: sizet = string.strlen(line_buf) - f.write(line_buf, line_len_w) - written_count += 1 - - f.close() - return 0 +from stdint import * +import memhub +import string +import stdlib +import mlirlite +import io +import viperlib +import sys +import ast +import lib.core.VLogger as VLogger +import lib.core.IncludesScanner as IncludesScanner +import lib.core.Gen.Translator as HandlesTranslator +import lib.Phase.BuildPipeline as BuildPipeline +import lib.Stub.StubGen.VpiReader as VpiReader +import lib.Projectrans.Utils as Utils +import lib.Projectrans.Config as Config + + +import lib.Stub.StubMerger.StubMergerStore as StubMergerStore +import lib.Stub.StubMerger.StubMergerTopo as StubMergerTopo +import lib.Stub.StubMerger.StubMergerIR as StubMergerIR +import lib.Stub.StubMerger.StubMergerBuild as StubMergerBuild +import lib.Stub.StubMerger as StubMerger + +# StubMergerCollect - slice-path construction and stub-file SHA1 collection. +# Functions live in this module; cross-module globals are accessed via StubMerger._xxx. + +# _sliced_path - build a slice path (stdlib.malloc; caller frees; creates subdir when level>0). +# Shards by Config.Sha1SliceLevel into SHA1-prefix subdirs; returns the full path or None on failure. +def _sliced_path(temp_dir: str, td_len: sizet, sha1: str, ext: str) -> str: + """Build a SHA1-sliced path (stdlib.malloc; caller frees; creates the subdir).""" + if temp_dir is None or sha1 is None or ext is None: return None + sha1_len: sizet = string.strlen(sha1) + ext_len: sizet = string.strlen(ext) + + # Subdir prefix: reuse Config.slice_subdir (the authoritative SHA1 slice impl; mbuddy-allocated, no free) + subdir_buf: str = Config.slice_subdir(sha1, Config.Sha1SliceLevel) + sub_len: sizet = 0 + if subdir_buf is not None: + sub_len = string.strlen(subdir_buf) + + # Ensure subdir {temp_dir}/{subdir} exists. + # Otherwise io.File(MODE.W) fails silently (file.closed==True) and the reader returns 0. + dir_path_len: sizet = td_len + sub_len + 2 + dir_path: bytes = stdlib.malloc(dir_path_len) + if dir_path is not None: + viperlib.snprintf(dir_path, dir_path_len, "%s/%s", temp_dir, subdir_buf) + BuildPipeline.ensure_dir(dir_path) + stdlib.free(dir_path) + + # Build the full path + path: str = None + path_len: sizet = 0 + if sub_len > 0: + path_len = td_len + sub_len + sha1_len + ext_len + 4 + path = stdlib.malloc(path_len) + if path is not None: viperlib.snprintf(path, path_len, "%s/%s/%s.%s", temp_dir, subdir_buf, sha1, ext) + else: + path_len = td_len + sha1_len + ext_len + 3 + path = stdlib.malloc(path_len) + if path is not None: viperlib.snprintf(path, path_len, "%s/%s.%s", temp_dir, sha1, ext) + + return path + +# _is_in_sha1_set - check whether a SHA1 is in the set. +def _is_in_sha1_set(sha1: str, sha1_set: str, set_count: int) -> int: + """Whether sha1 is present in the set.""" + if sha1 is None or sha1_set is None: return 0 + for i in range(int(set_count)): + if string.strcmp(sha1_set + i * 17, sha1) == 0: return 1 + return 0 +# Sha1MapPath - _sha1_map.txt 的落盘路径:放在**构建目录**(temp 的上一级), +# 便于直接按 SHA1 反查源文件;temp 内的 sliced 产物(.ll/.vpi/.meta)位置不变。 +# 返回 stdlib 缓冲区(调用方 free);temp_dir 无上一级(相对路径)时退回 temp 目录。 +def Sha1MapPath(pool: memhub.MemBuddy*, temp_dir: str) -> str: + """返回 {temp_dir 的上一级}/_sha1_map.txt""" + if temp_dir is None: return None + td: sizet = string.strlen(temp_dir) + end: sizet = td + while end > 0 and (temp_dir[end - 1] == '/' or temp_dir[end - 1] == '\\'): end -= 1 + cut: sizet = end + while cut > 0 and temp_dir[cut - 1] != '/' and temp_dir[cut - 1] != '\\': cut -= 1 + parent_len: sizet = 0 + if cut > 0: parent_len = cut - 1 + else: parent_len = end # 没有上一级(相对路径):退回 temp 目录本身 + buf: bytes = stdlib.malloc(parent_len + 32) + if buf is None: return None + if parent_len > 0: string.strncpy(buf, temp_dir, parent_len) + buf[parent_len] = '\0' + viperlib.snprintf(buf + parent_len, 32, "/_sha1_map.txt") + return buf +# WriteIncludesSha1Map - write includes scan results to _sha1_map.txt (human-readable debug output only). +# Internally the in-memory store filled by PopulateSha1MapStore is used; returns 0 on success, non-zero on failure. +def WriteIncludesSha1Map(mb: memhub.MemBuddy*, temp_dir: str, scan_result: IncludesScanner.ScanResult*, filter_set: str, filter_count: int) -> int: + """Write the includes scan result to _sha1_map.txt (human-readable debug output).""" + if temp_dir is None or scan_result is None: return 1 + + # Build the path {temp_dir 的上一级}/_sha1_map.txt + map_path: bytes = Sha1MapPath(mb, temp_dir) + if map_path is None: return 1 + + # Open the file for writing (CREATE_ALWAYS) + f: io.File* = io.File(map_path, io.MODE.W) + stdlib.free(map_path) + if f.closed: return 1 + + # Write each include entry: {sha1}:includes/{rel_path}\n + entry_size_w: sizet = IncludesScanner.FileEntry.__sizeof__() + line_buf: bytes = stdlib.malloc(512) + if line_buf is None: + f.close() + return 1 + + written_count: int = 0 + for i in range(int(scan_result.Count)): + ea_w: uint64 = uint64(scan_result.Entries) + i * entry_size_w + ent_w: IncludesScanner.FileEntry* = (IncludesScanner.FileEntry*)(ea_w) + if ent_w is None or ent_w.Sha1 is None or ent_w.RelPath is None: continue + # When a filter set is provided, write only its entries + if filter_set is not None and filter_count > 0: + if _is_in_sha1_set(ent_w.Sha1, filter_set, filter_count) == 0: continue + viperlib.snprintf(line_buf, 512, "%s:includes/%s\n", ent_w.Sha1, ent_w.RelPath) + line_len_w: sizet = string.strlen(line_buf) + f.write(line_buf, line_len_w) + written_count += 1 + + f.close() + return 0 diff --git a/App/lib/Stub/StubMerger/StubMergerIR.vp b/App/lib/Stub/StubMerger/StubMergerIR.vp index 198aae6..fd248cc 100644 --- a/App/lib/Stub/StubMerger/StubMergerIR.vp +++ b/App/lib/Stub/StubMerger/StubMergerIR.vp @@ -4,8 +4,6 @@ import memhub import string import stdlib import mlirlite -import win32.win32file as win32file -import win32.win32base as win32base import io import viperlib import sys diff --git a/App/lib/Stub/StubMerger/StubMergerStore.vp b/App/lib/Stub/StubMerger/StubMergerStore.vp index b603e02..ef99538 100644 --- a/App/lib/Stub/StubMerger/StubMergerStore.vp +++ b/App/lib/Stub/StubMerger/StubMergerStore.vp @@ -3,8 +3,6 @@ import memhub import string import stdlib import mlirlite -import win32.win32file as win32file -import win32.win32base as win32base import io import viperlib import sys diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index 13781a8..2482edf 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -1356,7 +1356,7 @@ novirtual class AssignGenMixin: cl: ast.Call* = ast.Call*(rhs) return self._pure_str_call_owned(trans, cl) if k == ast.ASTKind.BinOp: - bo: ast.BinOp* = BinOp*(rhs) + bo: ast.BinOp* = ast.BinOp*(rhs) if bo is not None and bo.op == ast.OpKind.Add and trans._in_pure != 0: return self._pure_str_expr(trans, bo.left) | self._pure_str_expr(trans, bo.right) return 0 diff --git a/App/lib/core/Gen/Call/CallGenBase.vp b/App/lib/core/Gen/Call/CallGenBase.vp index 57ea9d4..8dcb121 100644 --- a/App/lib/core/Gen/Call/CallGenBase.vp +++ b/App/lib/core/Gen/Call/CallGenBase.vp @@ -12,13 +12,14 @@ import stdio import stdlib import viperlib import io -import win32.win32file as win32file +import os import lib.core.VLogger as VLogger import lib.Stub.StubMerger as StubMerger import lib.core.CompilerFactory as CompilerFactory import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect import lib.Stub.StubMerger.StubMergerStore as StubMergerStore import lib.Stub.StubGen.VpiReader as VpiReader +import lib.Projectrans.Config as Config # CallGenBase - split 1/9 of CallGenMixin (bottom parent): module symbol @@ -30,6 +31,9 @@ import lib.Stub.StubGen.VpiReader as VpiReader define GLOBAL_FUNC_MAX = 8192 define GLOBAL_DEFAULTS_MAX = 4096 define _CEXPORT_MAX = 256 +# C-extern by-value aggregate parameters passed by reference (Win64: sizes other than +# 1/2/4/8). One symbol slot carries a 64-bit mask of the parameter indices needing a copy. +define _EXTERN_BYREF_MAX = 1024 define ASM_MAX_OPERANDS = 16 define ASM_TEXT_BUF_SIZE = 2048 define ASM_CONST_BUF_SIZE = 512 @@ -56,6 +60,20 @@ novirtual class CallGenBase: CexportFuncArr: bytes = None CexportCount: int = 0 + # --- C-extern aggregate-return registry (symbol names). A C function returning a struct + # follows the target C ABI (Win64: integer register <=8B, hidden sret pointer otherwise), + # which differs from Viper's own aggregate convention. Call emission consults this table. + ExternAggArr: bytes = None + ExternAggCount: int = 0 + + # --- C-extern by-value aggregate parameter registry (symbol -> 64-bit param mask). + # Win64 passes aggregates whose size is not 1/2/4/8 by reference to a caller-owned copy; + # Viper's default argument lowering passes the address of the variable, so the call site + # must know which parameters need that copy. + ExternByrefArr: bytes = None + ExternByrefMasks: uint64* = None + ExternByrefCount: int = 0 + # --- static symbol registry (static / @t.StaticT -> defining module SHA1), # meaning no cross-module use (C internal linkage); PermPool fixed slots. StaticSha1Arr: bytes = None @@ -855,6 +873,224 @@ novirtual class CallGenBase: return self._is_cexport_func_from_vpi(sha1, func_name) + # ============================================================ + # C-extern aggregate return ABI (Win64) + # ============================================================ + def register_extern_agg(self, sym: char*) -> int: + """Record that `sym` is a C extern returning an aggregate in the C ABI (idempotent).""" + if self.Gen is None or sym is None: return 0 + if self.Gen.CallGenBase.ExternAggArr is None: + self.Gen.CallGenBase.ExternAggArr = CompilerFactory.factory.PermPool.alloc(_CEXPORT_MAX * 64) + if self.Gen.CallGenBase.ExternAggArr is None: return 0 + string.memset(self.Gen.CallGenBase.ExternAggArr, 0, _CEXPORT_MAX * 64) + for i in range(int(self.Gen.CallGenBase.ExternAggCount)): + idx: sizet = sizet(i) * 64 + if string.strcmp(self.Gen.CallGenBase.ExternAggArr + idx, sym) == 0: return 1 + if self.Gen.CallGenBase.ExternAggCount < _CEXPORT_MAX: + idx2: sizet = sizet(self.Gen.CallGenBase.ExternAggCount) * 64 + string.strncpy(self.Gen.CallGenBase.ExternAggArr + idx2, sym, 63) + self.Gen.CallGenBase.ExternAggArr[idx2 + 63] = '\0' + self.Gen.CallGenBase.ExternAggCount += 1 + return 1 + return 0 + + def is_extern_agg(self, sym: char*) -> int: + """1 when `sym` is a C extern returning an aggregate (C ABI at the call boundary).""" + if self.Gen is None or sym is None: return 0 + if self.Gen.CallGenBase.ExternAggArr is None: return 0 + for i in range(int(self.Gen.CallGenBase.ExternAggCount)): + idx: sizet = sizet(i) * 64 + if string.strcmp(self.Gen.CallGenBase.ExternAggArr + idx, sym) == 0: return 1 + return 0 + + def register_extern_byref_param(self, sym: char*, index: int) -> int: + """Mark param `index` of C extern `sym` as a by-value aggregate passed by reference.""" + if self.Gen is None or sym is None or index < 0 or index >= 64: return 0 + if self.Gen.CallGenBase.ExternByrefArr is None: + self.Gen.CallGenBase.ExternByrefArr = CompilerFactory.factory.PermPool.alloc(_EXTERN_BYREF_MAX * 64) + self.Gen.CallGenBase.ExternByrefMasks = CompilerFactory.factory.PermPool.alloc(_EXTERN_BYREF_MAX * 8) + if self.Gen.CallGenBase.ExternByrefArr is None or self.Gen.CallGenBase.ExternByrefMasks is None: return 0 + string.memset(self.Gen.CallGenBase.ExternByrefArr, 0, _EXTERN_BYREF_MAX * 64) + string.memset(self.Gen.CallGenBase.ExternByrefMasks, 0, _EXTERN_BYREF_MAX * 8) + for i in range(int(self.Gen.CallGenBase.ExternByrefCount)): + idx: sizet = sizet(i) * 64 + if string.strcmp(self.Gen.CallGenBase.ExternByrefArr + idx, sym) == 0: + self.Gen.CallGenBase.ExternByrefMasks[i] = self.Gen.CallGenBase.ExternByrefMasks[i] | (uint64(1) << uint64(index)) + return 1 + if self.Gen.CallGenBase.ExternByrefCount < _EXTERN_BYREF_MAX: + idx2: sizet = sizet(self.Gen.CallGenBase.ExternByrefCount) * 64 + string.strncpy(self.Gen.CallGenBase.ExternByrefArr + idx2, sym, 63) + self.Gen.CallGenBase.ExternByrefArr[idx2 + 63] = '\0' + self.Gen.CallGenBase.ExternByrefMasks[self.Gen.CallGenBase.ExternByrefCount] = uint64(1) << uint64(index) + self.Gen.CallGenBase.ExternByrefCount += 1 + return 1 + return 0 + + def is_extern_byref_param(self, sym: char*, index: int) -> int: + """1 when param `index` of C extern `sym` is a by-value aggregate passed by reference.""" + if self.Gen is None or sym is None or index < 0 or index >= 64: return 0 + if self.Gen.CallGenBase.ExternByrefArr is None: return 0 + for i in range(int(self.Gen.CallGenBase.ExternByrefCount)): + idx: sizet = sizet(i) * 64 + if string.strcmp(self.Gen.CallGenBase.ExternByrefArr + idx, sym) == 0: + if (self.Gen.CallGenBase.ExternByrefMasks[i] & (uint64(1) << uint64(index))) != 0: return 1 + return 0 + return 0 + + def _copy_byref_agg_arg(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, val: mlirlite.Value*, pty: mlirlite.LLVMType*) -> mlirlite.Value*: + """Pass a copy of a by-value aggregate argument that the C ABI hands over by reference.""" + if pool is None or builder is None or val is None or pty is None or val.Ty is None: return val + if pty.kind() != mlirlite.LLVMType.Ptr: return val + match val.Ty: + case mlirlite.LLVMType.Ptr(elem): + if elem is None or elem.kind() != mlirlite.LLVMType.Struct: return val + loaded: mlirlite.Value* = mlirlite.build_load(builder, elem, val) + if loaded is None: return val + tmp: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, elem) + if tmp is None: return val + mlirlite.build_store(builder, loaded, tmp) + if self._call_ty_equal(pool, pty, tmp.Ty) == 0: + bc: mlirlite.Value* = mlirlite.build_bitcast(builder, tmp, pty) + if bc is not None: return bc + return tmp + case _: + return val + + def _c_agg_is_packed(self, ty: mlirlite.LLVMType*) -> int: + """1 when the registered struct type is packed (no inter-field padding).""" + if ty is None or self.Gen is None: return 0 + se: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(ty) + if se is not None and se.IsPacked != 0: return 1 + return 0 + + def _c_agg_size(self, ty: mlirlite.LLVMType*) -> int: + """Byte size of a C aggregate (natural alignment, or exact layout when packed).""" + if ty is None: return 0 + match ty: + case mlirlite.LLVMType.Int(bits): + return (bits + 7) // 8 + case mlirlite.LLVMType.Float(bits): + return bits // 8 + case mlirlite.LLVMType.Ptr(_): + return 8 + case mlirlite.LLVMType.Array(elem, count): + return self._c_agg_size(elem) * count + case mlirlite.LLVMType.Struct(fields, fcount, _): + if fcount < 0: return 0 + packed: int = self._c_agg_is_packed(ty) + total: int = 0 + max_align: int = 1 + cur: mlirlite.ParamNode* = fields + i: int = 0 + while cur is not None and i < fcount: + fty: mlirlite.LLVMType* = cur.Ty + fsz: int = self._c_agg_size(fty) + fal: int = 1 if packed != 0 else self._c_agg_align(fty) + if fal < 1: fal = 1 + if fal > max_align: max_align = fal + total = (total + fal - 1) // fal * fal + total += fsz + cur = cur.Next + i += 1 + if packed == 0 and max_align > 1: total = (total + max_align - 1) // max_align * max_align + return total + case _: + return 0 + + def _c_agg_align(self, ty: mlirlite.LLVMType*) -> int: + """Natural alignment of a C scalar/aggregate type.""" + if ty is None: return 1 + match ty: + case mlirlite.LLVMType.Int(bits): + sz_a: int = (bits + 7) // 8 + if sz_a < 1: return 1 + if sz_a > 8: return 8 + return sz_a + case mlirlite.LLVMType.Float(bits): + if bits <= 0: return 1 + return bits // 8 + case mlirlite.LLVMType.Ptr(_): + return 8 + case mlirlite.LLVMType.Array(elem_a, _): + return self._c_agg_align(elem_a) + case mlirlite.LLVMType.Struct(fields_a, fcount_a, _): + if self._c_agg_is_packed(ty) != 0: return 1 + max_a: int = 1 + cur_a: mlirlite.ParamNode* = fields_a + i_a: int = 0 + while cur_a is not None and i_a < fcount_a: + fa: int = self._c_agg_align(cur_a.Ty) + if fa > max_a: max_a = fa + cur_a = cur_a.Next + i_a += 1 + return max_a + case _: + return 1 + + def is_win64_c_abi(self) -> int: + """1 when C ABI lowering follows Win64 (i.e. the target is not Linux/SysV).""" + if Config.TargetTriple is not None: + if string.strstr(Config.TargetTriple, "linux") is not None: return 0 + return 1 + + def c_agg_int_ty(self, pool: memhub.MemBuddy*, ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*: + """Win64: a 1/2/4/8-byte aggregate rides in one integer register; None = by reference.""" + if ty is None: return None + sz: int = self._c_agg_size(ty) + if sz == 1: return mlirlite.Int8(pool) + if sz == 2: return mlirlite.Int16(pool) + if sz == 4: return mlirlite.Int32(pool) + if sz == 8: return mlirlite.Int64(pool) + return None + + def extern_c_param_agg_ty(self, pool: memhub.MemBuddy*, ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*: + """LLVM type of a C extern's by-value aggregate parameter. + + Win64 passes exactly 1/2/4/8 bytes as an integer of that width (a struct's field types + are irrelevant) and every other size by reference, with the caller owning the temporary + copy. Emitting the struct value instead makes LLVM split the fields (floats to XMM, + larger aggregates to stack slots), which no C callee expects. Non-Windows keeps the + struct value and lets the backend apply the platform ABI. + """ + if ty is None: return ty + if self.is_win64_c_abi() == 0: return ty + it: mlirlite.LLVMType* = self.c_agg_int_ty(pool, ty) + if it is not None: return it + return mlirlite.Ptr(pool, ty) + + def emit_extern_agg_call(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, call_name: char*, head: mlirlite.Value*, arg_count: int, ret_ty: mlirlite.LLVMType*) -> mlirlite.Value*: + """Call a C extern whose aggregate return uses the Win64 C ABI; return the Viper struct value.""" + if pool is None or builder is None or call_name is None or ret_ty is None: return None + # Only the Windows x64 ABI is adapted here. On Linux/SysV keep the existing direct-call + # path (its eightbyte classification is a separate implementation). + if Config.TargetTriple is not None: + if string.strstr(Config.TargetTriple, "linux") is not None: return None + size_b: int = self._c_agg_size(ret_ty) + if size_b <= 0: return None + fp: mlirlite.Value* = mlirlite.build_addressof(builder, call_name) + if fp is None: return None + abi_int: mlirlite.LLVMType* = None + if size_b == 1: abi_int = mlirlite.Int8(pool) + elif size_b == 2: abi_int = mlirlite.Int16(pool) + elif size_b == 4: abi_int = mlirlite.Int32(pool) + elif size_b == 8: abi_int = mlirlite.Int64(pool) + if abi_int is not None: + # Exact 1/2/4/8 bytes: C returns the aggregate in RAX as an integer of that width. + raw: mlirlite.Value* = mlirlite.build_call_indirect(builder, fp, head, arg_count, abi_int) + if raw is None: return None + slot: mlirlite.Value* = mlirlite.build_alloca_at_entry(builder, abi_int) + if slot is None: return None + mlirlite.build_store(builder, raw, slot) + sp: mlirlite.Value* = mlirlite.build_bitcast(builder, slot, mlirlite.Ptr(pool, ret_ty)) + if sp is None: return None + return mlirlite.build_load(builder, ret_ty, sp) + # Any other size: C returns through a hidden sret pointer passed as the first argument. + tmp: mlirlite.Value* = mlirlite.build_alloca_at_entry(builder, ret_ty) + if tmp is None: return None + tmp.Next = head + mlirlite.build_call_indirect(builder, fp, tmp, arg_count + 1, mlirlite.Void(pool)) + return mlirlite.build_load(builder, ret_ty, tmp) + # RegisterStaticSymbol / IsStaticSymbol - static symbol registry: the four # syntaxes all normalize to t.StaticT; meaning = no cross-module use. def RegisterStaticSymbol(self, sha1: char*, name: char*) -> int: @@ -1223,6 +1459,7 @@ novirtual class CallGenBase: new_tail: mlirlite.Value* = None cur: mlirlite.Value* = head prm: mlirlite.Param* = mlirlite.function_get_param_head(func) + pi_c: int = 0 while cur is not None and prm is not None: nxt: mlirlite.Value* = cur.Next node_v: mlirlite.Value* = cur @@ -1230,12 +1467,18 @@ novirtual class CallGenBase: if pty is not None and cur.Ty is not None and self._call_ty_equal(pool, pty, cur.Ty) == 0: conv: mlirlite.Value* = self.Gen.CallGenLib._coerce_arg_value_to_ty(pool, builder, cur, pty, node, trans, 0) if conv is not None: node_v = conv + # Win64 byref aggregate parameter: C takes a private copy, so never pass the + # argument variable's own address (a callee write-back must not reach the caller). + if self.is_extern_byref_param(call_name, pi_c) != 0: + cp_v: mlirlite.Value* = self._copy_byref_agg_arg(pool, builder, node_v, pty) + if cp_v is not None: node_v = cp_v mlirlite.value_set_next(node_v, None) if new_head is None: new_head = node_v else: mlirlite.value_set_next(new_tail, node_v) new_tail = node_v cur = nxt prm = mlirlite.param_get_next(prm) + pi_c += 1 while cur is not None: nxt2: mlirlite.Value* = cur.Next mlirlite.value_set_next(cur, None) @@ -1737,7 +1980,7 @@ novirtual class CallGenBase: i32_p: mlirlite.LLVMType* = mlirlite.Int32(pb_d) i8_p: mlirlite.LLVMType* = mlirlite.Int8(pb_d) i8p_p: mlirlite.LLVMType* = mlirlite.Ptr(pb_d, i8_p) - rv_p: mlirlite.LLVMType* = VpiReader._resolve_vpi_ret_annot_str(pb_d, annot_d, i32_p, i8p_p) + rv_p: mlirlite.LLVMType* = VpiReader._resolve_vpi_ret_annot_str(pb_d, annot_d, i32_p, i8p_p, probe_sha) if rv_p is None: c_annot_res = "none" else: c_annot_res = "ok" # Annotate the tail class of the raw annotation: "missing" = the class was never @@ -2328,14 +2571,7 @@ novirtual class CallGenBase: subdir_buf[2] = '\0' viperlib.snprintf(path_buf, path_len, "%s/%s/%s.text.ll", self.Gen.CallGenBase.TempDir, subdir_buf, sha1) stdlib.free(subdir_buf) - # existence check via GetFileAttributesA (no file handle opened) - attr: ULONG = win32file.GetFileAttributesA(path_buf) - exists: int = 1 - # INVALID_FILE_ATTRIBUTES = missing/inaccessible -> 0. - if attr == 0xFFFFFFFF: exists = 0 - # The original CreateFileA rejected directories; exclude them explicitly to - # preserve the equivalent "readable regular file" semantics. - if (attr & win32file.FILE_ATTRIBUTE_DIRECTORY) != 0: exists = 0 + exists: int = 1 if os.isfile(path_buf) else 0 stdlib.free(path_buf) return exists diff --git a/App/lib/core/Gen/Call/CallGenCall.vp b/App/lib/core/Gen/Call/CallGenCall.vp index cafe529..17fe1d6 100644 --- a/App/lib/core/Gen/Call/CallGenCall.vp +++ b/App/lib/core/Gen/Call/CallGenCall.vp @@ -1870,7 +1870,7 @@ novirtual class CallGenCall(CallGenBase.CallGenBase): match entry.Ty: case mlirlite.LLVMType.Struct(fields, fcount, name): offset_fb: int = 0 - # packed (@c.Attribute(t.attr.packed)) structs have no field padding: + # packed (@Attribute(attr.packed)) structs have no field padding: # offset = sum of preceding field sizes (LLVM packed struct layout). is_packed_fb: int = entry.IsPacked cur_fb: mlirlite.ParamNode* = fields diff --git a/App/lib/core/Gen/Call/CallGenCtor.vp b/App/lib/core/Gen/Call/CallGenCtor.vp index e02a556..8780018 100644 --- a/App/lib/core/Gen/Call/CallGenCtor.vp +++ b/App/lib/core/Gen/Call/CallGenCtor.vp @@ -678,11 +678,24 @@ novirtual class CallGenCtor(CallGenBase.CallGenBase): self.Gen.TypeGenErr.fatal_error(new_node, f"`new`: storage allocation for the scalar type '{nm}' failed", "TypeError") return None # 零初始化:`new … T()` 走 __before_init__ 是零填充,标量/数组同语义(未写先读不再是垃圾) - zero_v: mlirlite.Value* = mlirlite.ConstZero(pool, prim_ty) - if zero_v is None: - self.Gen.TypeGenErr.fatal_error(new_node, f"`new`: cannot build a zero initializer for the scalar type '{nm}'", "TypeError") - return None - mlirlite.build_store(builder, zero_v, storage) + # 数组走 llvm.memset,不生成"整数组常量存储"IR(`store [N x i8] zeroinitializer`)。 + # N 较大时后者在 LLVM 22 llc/Win32 上会崩溃;语法/语义上它仍是数组,实际存储只是 + # malloc/memgr 给的字节块,不需要按数组聚合类型整体写一次。 + if elem_ty is not None: + zero_byte: mlirlite.Value* = mlirlite.ConstInt(pool, mlirlite.Int8(pool), 0, "0") + zero_len: mlirlite.Value* = mlirlite.const_int64(pool, size_bytes) + if zero_byte is None or zero_len is None: + self.Gen.TypeGenErr.fatal_error(new_node, f"`new`: cannot build a zero initializer for the array type '{nm}'", "TypeError") + return None + if self.Gen.CallGenLib._emit_llvm_memset_intrinsic(pool, builder, mod, storage, zero_byte, zero_len) is None: + self.Gen.TypeGenErr.fatal_error(new_node, f"`new`: cannot zero-initialize the array type '{nm}'", "TypeError") + return None + else: + zero_v: mlirlite.Value* = mlirlite.ConstZero(pool, prim_ty) + if zero_v is None: + self.Gen.TypeGenErr.fatal_error(new_node, f"`new`: cannot build a zero initializer for the scalar type '{nm}'", "TypeError") + return None + mlirlite.build_store(builder, zero_v, storage) # 返回值:标量 → 该指针本身;数组 → **首元素指针 Elem***(数组名退化,同址零成本 bitcast) ptr_ty: mlirlite.LLVMType* = None if elem_ty is not None: ptr_ty = mlirlite.Ptr(pool, elem_ty) diff --git a/App/lib/core/Gen/Call/CallGenLeafKit.vp b/App/lib/core/Gen/Call/CallGenLeafKit.vp index 05a2c46..894fa58 100644 --- a/App/lib/core/Gen/Call/CallGenLeafKit.vp +++ b/App/lib/core/Gen/Call/CallGenLeafKit.vp @@ -1426,6 +1426,122 @@ novirtual class CallGenLeafKit(CallGenCBuiltin.CallGenCBuiltin): return 1 + # ---- aggregate default encoding (struct constructor with scalar arguments) ---- + # Token format shared with VpiReader._VpiEncodeAggDefault: + # i: d: s:: b:<0|1> n:0 (one token per field, '|' split) + def _agg_enc_put(self, dst: bytes, cap: sizet, pos: sizet*, src: char*, n: sizet) -> int: + if dst is None or pos is None or src is None: return 0 + if n == 0: return 1 + if pos[0] + n + 1 > cap: return 0 + string.memcpy(dst + pos[0], src, n) + pos[0] += n + dst[pos[0]] = '\0' + return 1 + + def _agg_enc_int(self, dst: bytes, cap: sizet, pos: sizet*, v: int64) -> int: + tmp: str = stdlib.malloc(32) + if tmp is None: return 0 + viperlib.snprintf(tmp, 32, "%lld", v) + r: int = self._agg_enc_put(dst, cap, pos, tmp, string.strlen(tmp)) + stdlib.free(tmp) + return r + + def _agg_enc_float(self, dst: bytes, cap: sizet, pos: sizet*, v: double) -> int: + tmp: str = stdlib.malloc(64) + if tmp is None: return 0 + viperlib.snprintf(tmp, 64, "%.17g", v) + r: int = self._agg_enc_put(dst, cap, pos, "d:", 2) + if r != 0: r = self._agg_enc_put(dst, cap, pos, tmp, string.strlen(tmp)) + stdlib.free(tmp) + return r + + def _agg_enc_arg(self, dst: bytes, cap: sizet, pos: sizet*, node: ast.AST*) -> int: + """Encode one scalar constructor argument; 0 = unsupported.""" + if node is None: return 0 + k: int = node.kind() + if k == ast.ASTKind.Constant: + cn: ast.Constant* = ast.Constant*(node) + if cn.const_kind == ast.CONST_INT or cn.const_kind == ast.CONST_CHAR: + if self._agg_enc_put(dst, cap, pos, "i:", 2) == 0: return 0 + return self._agg_enc_int(dst, cap, pos, cn.int_val) + if cn.const_kind == ast.CONST_FLOAT: return self._agg_enc_float(dst, cap, pos, cn.float_val) + if cn.const_kind == ast.CONST_BOOL: + if cn.int_val != 0: return self._agg_enc_put(dst, cap, pos, "b:1", 3) + return self._agg_enc_put(dst, cap, pos, "b:0", 3) + if cn.const_kind == ast.CONST_NONE: return self._agg_enc_put(dst, cap, pos, "n:0", 3) + if cn.const_kind == ast.CONST_STR and cn.str_val is not None: + sl: sizet = string.strlen(cn.str_val) + if self._agg_enc_put(dst, cap, pos, "s:", 2) == 0: return 0 + if self._agg_enc_int(dst, cap, pos, int64(sl)) == 0: return 0 + if self._agg_enc_put(dst, cap, pos, ":", 1) == 0: return 0 + return self._agg_enc_put(dst, cap, pos, cn.str_val, sl) + return 0 + if k == ast.ASTKind.UnaryOp: + du: ast.UnaryOp* = ast.UnaryOp*(node) + if du.operand is None or du.operand.kind() != ast.ASTKind.Constant: return 0 + dc: ast.Constant* = ast.Constant*(du.operand) + if dc.const_kind == ast.CONST_INT or dc.const_kind == ast.CONST_CHAR: + v: int64 = dc.int_val + if du.op == ast.OpKind.USub: v = -v + elif du.op == ast.OpKind.Invert: v = ~v + if self._agg_enc_put(dst, cap, pos, "i:", 2) == 0: return 0 + return self._agg_enc_int(dst, cap, pos, v) + if dc.const_kind == ast.CONST_FLOAT: + fv: double = dc.float_val + if du.op == ast.OpKind.USub: fv = -fv + return self._agg_enc_float(dst, cap, pos, fv) + return 0 + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(node) + if nm.id is None: return 0 + cv: int64 = self.Gen.TypeGenCDef.lookup_cdefine_constant(nm.id) + if self.Gen.TypeGenCDef.is_cdefine_found() == 0: return 0 + if self._agg_enc_put(dst, cap, pos, "i:", 2) == 0: return 0 + return self._agg_enc_int(dst, cap, pos, cv) + return 0 + + def _agg_expr_text_at(self, node: ast.AST*, dst: bytes, cap: sizet, pos: sizet*) -> int: + if node is None: return 0 + k: int = node.kind() + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(node) + if nm.id is None: return 0 + return self._agg_enc_put(dst, cap, pos, nm.id, string.strlen(nm.id)) + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(node) + if at.value is None: return 0 + if self._agg_expr_text_at(at.value, dst, cap, pos) == 0: return 0 + if self._agg_enc_put(dst, cap, pos, ".", 1) == 0: return 0 + if at.attr is None: return 0 + return self._agg_enc_put(dst, cap, pos, at.attr, string.strlen(at.attr)) + return 0 + + def _agg_expr_text(self, node: ast.AST*, dst: bytes, cap: sizet) -> int: + """Render a Name/Attribute default (enum member / CDefine) into dst; 0 = unsupported.""" + if dst is None or cap == 0: return 0 + dst[0] = '\0' + pos: sizet = 0 + return self._agg_expr_text_at(node, dst, cap, &pos) + + def _encode_agg_default(self, cl: ast.Call*, dst: bytes, cap: sizet) -> int: + """Encode a struct-constructor default's scalar arguments; 0 = unsupported.""" + if cl is None or dst is None or cl.func is None: return 0 + if cl.keywords is not None and len(cl.keywords) > 0: return 0 + if cl.func.kind() != ast.ASTKind.Name: return 0 + nm: ast.Name* = ast.Name*(cl.func) + if nm.id is None: return 0 + if GenApi.find_struct(nm.id) is None: return 0 + if cl.args is None: return 0 + pos: sizet = 0 + dst[0] = '\0' + al: list[ast.AST*]* = cl.args + an: sizet = len(al) + for ai in range(int(an)): + if ai > 0: + if self._agg_enc_put(dst, cap, &pos, "|", 1) == 0: return 0 + if self._agg_enc_arg(dst, cap, &pos, al.get(ai)) == 0: return 0 + return 1 + def _register_global_defaults(self, name: char*, defaults: rawptr, default_count: int, param_count: int, def_sha1: char* = None) -> int: """Register a function's default arguments in the global table; returns 0 on success""" if name is None or defaults is None: return 1 @@ -1477,8 +1593,18 @@ novirtual class CallGenLeafKit(CallGenCBuiltin.CallGenCBuiltin): dnode: ast.AST* = dl.get(di) if dnode is not None and dnode.kind() == ast.ASTKind.Constant: dcn: ast.Constant* = ast.Constant*(dnode) - kinds[di] = dcn.const_kind - ints[di] = dcn.int_val + if dcn.const_kind == ast.CONST_FLOAT: + # Float defaults live in float_val; int_val is always 0. Store the + # IEEE-754 bit pattern so the snapshot rebuild can recover the value + # (int defaults still use int_val). + fv_reg: double = dcn.float_val + fb_reg: uint64 = 0 + *&fb_reg = *&fv_reg + kinds[di] = ast.CONST_FLOAT + ints[di] = int64(fb_reg) + else: + kinds[di] = dcn.const_kind + ints[di] = dcn.int_val if dcn.const_kind == ast.CONST_STR and dcn.str_val is not None: slen: sizet = string.strlen(dcn.str_val) scopy: char* = CompilerFactory.factory.PermPool.alloc(slen + 1) @@ -1494,6 +1620,16 @@ novirtual class CallGenLeafKit(CallGenCBuiltin.CallGenCBuiltin): if self.Gen.TypeGenCDef.is_cdefine_found() != 0: kinds[di] = ast.CONST_INT ints[di] = cdef_v + else: + # Not a CDefine: keep the name text and let the call site resolve + # it (e.g. an enum member imported unqualified). + txt_n: bytes = CompilerFactory.factory.PermPool.alloc(256) + if txt_n is not None and self._agg_expr_text(dnode, txt_n, 256) != 0: + kinds[di] = GenTypes.CONST_EXPR_TEXT + ints[di] = 0 + strs[di] = sizet(txt_n) + else: + kinds[di] = GenTypes.CONST_UNKNOWN_DEFAULT elif dnode is not None and dnode.kind() == ast.ASTKind.UnaryOp: # unary-literal default (e.g. ex3: int = -1): AST is UnaryOp over a # Constant; evaluate only when the operand is an int Constant. @@ -1510,6 +1646,44 @@ novirtual class CallGenLeafKit(CallGenCBuiltin.CallGenCBuiltin): elif du.op == ast.OpKind.Invert: kinds[di] = ast.CONST_INT ints[di] = ~doc.int_val + elif doc.const_kind == ast.CONST_FLOAT: + # Negative/positive float literal (e.g. -1.5): store bits. + fv_u: double = doc.float_val + if du.op == ast.OpKind.USub: fv_u = -fv_u + elif du.op == ast.OpKind.UAdd: pass + else: fv_u = 0.0 + fb_u: uint64 = 0 + *&fb_u = *&fv_u + kinds[di] = ast.CONST_FLOAT + ints[di] = int64(fb_u) + elif dnode is not None and dnode.kind() == ast.ASTKind.Attribute: + # Qualified constant (enum member `Flags.Name_`, module constant): + # store the text; the call site resolves it after enum registration. + txt_a: bytes = CompilerFactory.factory.PermPool.alloc(256) + if txt_a is not None and self._agg_expr_text(dnode, txt_a, 256) != 0: + kinds[di] = GenTypes.CONST_EXPR_TEXT + ints[di] = 0 + strs[di] = sizet(txt_a) + else: + kinds[di] = GenTypes.CONST_UNKNOWN_DEFAULT + elif dnode is not None and dnode.kind() == ast.ASTKind.Call: + # Struct-constructor default (e.g. Vec2(1.0, 2.0)): store the literal + # field tokens; cross-module callers rebuild it from the parameter type. + enc_buf: bytes = CompilerFactory.factory.PermPool.alloc(4096) + if enc_buf is not None and self._encode_agg_default(ast.Call*(dnode), enc_buf, 4096) != 0: + kinds[di] = GenTypes.CONST_AGG_DEFAULT + ints[di] = 0 + strs[di] = sizet(enc_buf) + else: + kinds[di] = GenTypes.CONST_UNKNOWN_DEFAULT + elif dnode is not None: + txt_x: bytes = CompilerFactory.factory.PermPool.alloc(256) + if txt_x is not None and self._agg_expr_text(dnode, txt_x, 256) != 0: + kinds[di] = GenTypes.CONST_EXPR_TEXT + ints[di] = 0 + strs[di] = sizet(txt_x) + else: + kinds[di] = GenTypes.CONST_UNKNOWN_DEFAULT entry.ConstKinds = kinds entry.ConstInts = ints entry.ConstStrs = strs @@ -1583,7 +1757,119 @@ novirtual class CallGenLeafKit(CallGenCBuiltin.CallGenCBuiltin): return None - def _build_const_from_snapshot(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, gd_entry: GenTypes.FuncDefaultsEntry*, di: int, trans: HT.Translator*, builder: mlirlite.IRBuilder*) -> mlirlite.Value*: + def _agg_parse_i64(self, vbuf: bytes, s: sizet, e: sizet) -> int64: + neg: int = 0 + i: sizet = s + if i < e and vbuf[i] == '-': neg = 1; i += 1 + v: int64 = 0 + while i < e: + ch: uint8 = vbuf[i] + if ch < 0x30 or ch > 0x39: break + v = v * 10 + int64(ch - 0x30) + i += 1 + if neg != 0: v = -v + return v + + def _agg_parse_f64(self, vbuf: bytes, s: sizet, e: sizet) -> double: + n: sizet = e - s + tb: bytes = stdlib.malloc(n + 1) + if tb is None: return 0.0 + string.memcpy(tb, vbuf + s, n) + tb[n] = '\0' + dv: double = string.atof(tb) + stdlib.free(tb) + return dv + + def _agg_token_value(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, tag: uint8, vbuf: bytes, vs: sizet, ve: sizet, fty: mlirlite.LLVMType*) -> mlirlite.Value*: + """Convert one encoded default token to a constant of the struct field's LLVM type.""" + if fty is None: return None + match fty: + case mlirlite.LLVMType.Int(bits): + if tag == 0x69: return mlirlite.const_int(pool, bits, self._agg_parse_i64(vbuf, vs, ve)) # 'i' + if tag == 0x62: # 'b' + bv: int64 = 1 if (ve > vs and vbuf[vs] == '1') else 0 + return mlirlite.const_int(pool, bits, bv) + if tag == 0x6E: return mlirlite.const_int(pool, bits, 0) # 'n' + return None + case mlirlite.LLVMType.Float(bits): + if tag == 0x64: return mlirlite.ConstFloat(pool, fty, self._agg_parse_f64(vbuf, vs, ve)) # 'd' + if tag == 0x69: return mlirlite.ConstFloat(pool, fty, double(self._agg_parse_i64(vbuf, vs, ve))) + return None + case mlirlite.LLVMType.Ptr(_): + if tag == 0x6E: return mlirlite.ConstNull(pool, fty, "null") + if tag == 0x73: # 's' + n: sizet = ve - vs + sv: bytes = CompilerFactory.factory.PermPool.alloc(n + 1) + if sv is None: return None + string.memcpy(sv, vbuf + vs, n) + sv[n] = '\0' + gs: mlirlite.Value* = self.Gen.ExprGenConst.create_global_string(builder, pool, mod, sv, trans) + if gs is None: return None + if self._call_ty_equal(pool, gs.Ty, fty) == 0: + bc: mlirlite.Value* = mlirlite.build_bitcast(builder, gs, fty) + if bc is not None: return bc + return gs + return None + case _: + return None + + def _build_agg_from_tokens(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, spec: char*, param_ty: mlirlite.LLVMType*) -> mlirlite.Value*: + """Rebuild an aggregate default from its field tokens and the parameter's struct type.""" + if spec is None or param_ty is None: return None + sty: mlirlite.LLVMType* = param_ty + match param_ty: + case mlirlite.LLVMType.Ptr(pe): sty = pe + case _: sty = param_ty + if sty is None or sty.kind() != mlirlite.LLVMType.Struct: return None + fields: mlirlite.ParamNode* = None + fcount: int = 0 + match sty: + case mlirlite.LLVMType.Struct(fs, fc, _): + fields = fs + fcount = fc + case _: + return None + if fcount <= 0: return None + slot: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, sty) + if slot is None: return None + sp: char* = spec + cur: mlirlite.ParamNode* = fields + i: int = 0 + while cur is not None and i < fcount: + fty: mlirlite.LLVMType* = cur.Ty + tag: uint8 = uint8(sp[0]) + if sp[1] != ':': return None + sp = sp + 2 + token_base: char* = sp + tl: sizet = 0 + if tag == 0x73: # 's': : + while sp[0] >= '0' and sp[0] <= '9': + tl = tl * 10 + sizet(int(sp[0]) - 48) + sp += 1 + if sp[0] != ':': return None + sp += 1 + token_base = sp + sp = sp + tl + else: + while sp[0] != 0 and sp[0] != '|': sp += 1 + tl = sizet(uint64(sp) - uint64(token_base)) + val: mlirlite.Value* = self._agg_token_value(pool, builder, mod, trans, tag, token_base, 0, tl, fty) + if val is None: return None + fp: mlirlite.Value* = mlirlite.build_gep_struct(builder, sty, fty, slot, i) + if fp is None: return None + mlirlite.build_store(builder, val, fp) + if sp[0] == '|': sp += 1 + cur = cur.Next + i += 1 + if cur is not None or sp[0] != 0: return None + if param_ty.kind() == mlirlite.LLVMType.Ptr: + if self._call_ty_equal(pool, param_ty, slot.Ty) == 0: + bc2: mlirlite.Value* = mlirlite.build_bitcast(builder, slot, param_ty) + if bc2 is not None: return bc2 + return slot + return mlirlite.build_load(builder, sty, slot) + + def _build_const_from_snapshot(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, gd_entry: GenTypes.FuncDefaultsEntry*, di: int, trans: HT.Translator*, builder: mlirlite.IRBuilder*, param_ty: mlirlite.LLVMType* = None) -> mlirlite.Value*: """Rebuild the di-th default-value constant IR from the snapshot""" if gd_entry is None or gd_entry.ConstKinds is None or gd_entry.ConstInts is None: return None if di < 0 or di >= gd_entry.DefaultCount: return None @@ -1603,7 +1889,44 @@ novirtual class CallGenLeafKit(CallGenCBuiltin.CallGenCBuiltin): if gd_entry.ConstStrs is not None and gd_entry.ConstStrs[di] != 0: sv: char* = rawptr(gd_entry.ConstStrs[di]) return self.Gen.ExprGenConst.create_global_string(builder, pool, mod, sv, trans) - # return None for unreconstructable values (CONST_FLOAT / complex expr); caller falls back to AST + elif kind == ast.CONST_FLOAT: + # The snapshot stores the IEEE-754 bit pattern in the int64 slot. + uv_f: uint64 = uint64(ival) + dv_f: double = 0.0 + *&dv_f = *&uv_f + return mlirlite.ConstFloat(pool, mlirlite.Double(pool), dv_f) + elif kind == GenTypes.CONST_AGG_DEFAULT: + if gd_entry.ConstStrs is None or gd_entry.ConstStrs[di] == 0: return None + spec: char* = rawptr(gd_entry.ConstStrs[di]) + return self._build_agg_from_tokens(pool, builder, mod, trans, spec, param_ty) + elif kind == GenTypes.CONST_EXPR_TEXT: + # Resolved now (enums/CDefines are registered): enum member `Flags.Name_` or a + # bare CDefine name. + if gd_entry.ConstStrs is None or gd_entry.ConstStrs[di] == 0: return None + sv: char* = rawptr(gd_entry.ConstStrs[di]) + dot: char* = string.strrchr(sv, '.') + if dot is not None: + slen: sizet = uint64(dot) - uint64(sv) + if slen == 0: return None + scope: bytes = pool.alloc(slen + 1) + if scope is None: return None + string.strncpy(scope, sv, slen) + scope[slen] = '\0' + me: GenTypes.EnumMember* = self.Gen.StmtGenDecl.lookup_enum_member(scope, dot + 1, None) + if me is None: return None + mbits: int = 32 + if me.Ty is not None: + match me.Ty: + case mlirlite.LLVMType.Int(bits_e): mbits = bits_e + case _: mbits = 32 + return mlirlite.const_int(pool, mbits, me.Value) + cv: int64 = self.Gen.TypeGenCDef.lookup_cdefine_constant(sv) + if self.Gen.TypeGenCDef.is_cdefine_found() == 0: return None + if cv > 2147483647 or cv < -2147483648: return mlirlite.const_int64(pool, cv) + return mlirlite.const_int32(pool, cv) + elif kind == GenTypes.CONST_UNKNOWN_DEFAULT: + return None + # return None for unreconstructable values (complex expr); caller falls back to AST return None diff --git a/App/lib/core/Gen/Call/CallGenLib.vp b/App/lib/core/Gen/Call/CallGenLib.vp index 33654c4..34c961f 100644 --- a/App/lib/core/Gen/Call/CallGenLib.vp +++ b/App/lib/core/Gen/Call/CallGenLib.vp @@ -346,7 +346,23 @@ novirtual class CallGenLib(CallGenBase.CallGenBase): return val case _: pass - case mlirlite.LLVMType.Ptr(_): + case mlirlite.LLVMType.Float(bits_fa): + match tgt_ty: + case mlirlite.LLVMType.Float(bits_fb): + if bits_fb == bits_fa: return val + if bits_fb > bits_fa: + nv_fe: mlirlite.Value* = mlirlite.build_fpext(builder, val, tgt_ty) + if nv_fe is not None: return nv_fe + return val + # Narrowing (double default literal -> float param): only constants, so an + # SSA value's precision is never silently dropped. + if val.IsConst != 0: + nv_ft: mlirlite.Value* = mlirlite.build_fptrunc(builder, val, tgt_ty) + if nv_ft is not None: return nv_ft + return val + case _: + pass + case mlirlite.LLVMType.Ptr(ptr_elem): match tgt_ty: case mlirlite.LLVMType.Ptr(_): # same-level pointers: bitcast when the arg type differs from the definition @@ -355,11 +371,21 @@ novirtual class CallGenLib(CallGenBase.CallGenBase): nv_p: mlirlite.Value* = mlirlite.build_bitcast(builder, val, tgt_ty) if nv_p is not None: return nv_p case mlirlite.LLVMType.Int(bits_pi): - # A pointer passed where the callee's parameter degraded to an integer - # (e.g. `rawptr` resolved as uint64 in a stale/mis-typed stub): convert at the - # call boundary instead of failing the implicit-cast rule, so passing a - # rawptr never breaks the build and the callee still receives the address. if bits_pi > 0: + # Win64 C ABI: a by-value aggregate parameter of exactly 1/2/4/8 bytes + # arrives here as the address of Viper's spilled copy. Pass the aggregate + # BITS, not the address (a C callee reads the struct from the register). + if ptr_elem is not None and ptr_elem.kind() == mlirlite.LLVMType.Struct: + sz_pi: int = self.Gen.CallGenBase._c_agg_size(ptr_elem) + if sz_pi > 0 and sz_pi * 8 == bits_pi and self.Gen.CallGenBase.is_win64_c_abi() != 0: + pi_ip: mlirlite.Value* = mlirlite.build_bitcast(builder, val, mlirlite.Ptr(pool, tgt_ty)) + if pi_ip is not None: + pi_ld: mlirlite.Value* = mlirlite.build_load(builder, tgt_ty, pi_ip) + if pi_ld is not None: return pi_ld + # A pointer passed where the callee's parameter degraded to an integer + # (e.g. `rawptr` resolved as uint64 in a stale/mis-typed stub): convert at the + # call boundary instead of failing the implicit-cast rule, so passing a + # rawptr never breaks the build and the callee still receives the address. nv_p2: mlirlite.Value* = mlirlite.build_ptrtoint(builder, val, tgt_ty) if nv_p2 is not None: return nv_p2 case _: @@ -374,6 +400,19 @@ novirtual class CallGenLib(CallGenBase.CallGenBase): if tmp_a is not None: mlirlite.build_store(builder, val, tmp_a) return tmp_a + case mlirlite.LLVMType.Int(bits_agg): + # Win64 C ABI: a 1/2/4/8-byte by-value aggregate is passed in one integer + # register. Spill the value and load its bits at the parameter width. + if bits_agg > 0 and self.Gen.CallGenBase.is_win64_c_abi() != 0 and self.Gen.CallGenBase._c_agg_size(val.Ty) * 8 == bits_agg: + vk_b: int = val.Ty.kind() + if vk_b == mlirlite.LLVMType.Struct or vk_b == mlirlite.LLVMType.Array: + tmp_b: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, val.Ty) + if tmp_b is not None: + mlirlite.build_store(builder, val, tmp_b) + ip_b: mlirlite.Value* = mlirlite.build_bitcast(builder, tmp_b, mlirlite.Ptr(pool, tgt_ty)) + if ip_b is not None: + ld_b: mlirlite.Value* = mlirlite.build_load(builder, tgt_ty, ip_b) + if ld_b is not None: return ld_b case _: pass return val @@ -582,6 +621,56 @@ novirtual class CallGenLib(CallGenBase.CallGenBase): # Return dst (matches C memcpy semantics: returns the dest pointer) return dst_coerced + + def _emit_llvm_memset_intrinsic(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, dst_val: mlirlite.Value*, byte_val: mlirlite.Value*, num_val: mlirlite.Value*) -> mlirlite.Value*: + """Fill memory via the @llvm.memset intrinsic (avoids whole-aggregate constant stores).""" + if pool is None or builder is None or mod is None: return None + if dst_val is None or byte_val is None or num_val is None: return None + + void_ty: mlirlite.LLVMType* = mlirlite.Void(pool) + i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool) + i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty) + i64_ty: mlirlite.LLVMType* = mlirlite.Int64(pool) + i1_ty: mlirlite.LLVMType* = mlirlite.Int1(pool) + + # Declare @llvm.memset (if not already declared): dst, val, len, isvolatile + memset_func: mlirlite.Function* = self.find_func_in_module(mod, "llvm.memset") + if memset_func is None: + memset_func = mlirlite.create_declare(pool, mod, "llvm.memset", void_ty) + if memset_func is not None: + mlirlite.add_param(pool, memset_func, i8_ptr_ty, "dst") + mlirlite.add_param(pool, memset_func, i8_ty, "val") + mlirlite.add_param(pool, memset_func, i64_ty, "len") + mlirlite.add_param(pool, memset_func, i1_ty, "isvolatile") + + # dst -> i8* + dst_coerced: mlirlite.Value* = dst_val + dst_ty: mlirlite.LLVMType* = mlirlite.value_get_ty(dst_val) + if self.Gen.ExprGenBase.is_ptr_type(dst_ty) != 0: + dst_coerced = mlirlite.build_bitcast(builder, dst_val, i8_ptr_ty) + else: + dst_bits: int = self.Gen.ExprGenBase.get_llvm_type_bits(dst_ty) + if dst_bits != 0: + if dst_bits < 64: dst_coerced = mlirlite.build_zext(builder, dst_val, i64_ty) + dst_coerced = mlirlite.build_inttoptr(builder, dst_coerced, i8_ptr_ty) + + # val -> i8; len -> i64 + byte_coerced: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, byte_val, i8_ty) + num_coerced: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, num_val, i64_ty) + if dst_coerced is None or byte_coerced is None or num_coerced is None: return None + + # Append the i1 false arg (isvolatile) + false_val: mlirlite.Value* = mlirlite.ConstInt(pool, i1_ty, 0, "false") + mlirlite.value_set_next(dst_coerced, byte_coerced) + mlirlite.value_set_next(byte_coerced, num_coerced) + if false_val is not None: + mlirlite.value_set_next(num_coerced, false_val) + mlirlite.value_set_next(false_val, None) + else: mlirlite.value_set_next(num_coerced, None) + + mlirlite.build_call(builder, "llvm.memset", dst_coerced, 4, void_ty, 0) + return dst_coerced + # _translate_call_with_kwargs: keyword-arg call; builds args in found_func.Params order (positional first, else match by param name) # ============================================================ @@ -682,4 +771,7 @@ novirtual class CallGenLib(CallGenBase.CallGenBase): # memcpy -> @llvm.memcpy intrinsic (avoid a missing cross-module @memcpy declare under monomorphization) if string.strcmp(func_name, "memcpy") == 0 and actual_count >= 3: return self._emit_llvm_memcpy_intrinsic(pool, builder, mod, mc_dst1, mc_src1, mc_num1) + if call_ret_ty is not None and call_ret_ty.kind() == mlirlite.LLVMType.Struct and self.Gen.CallGenBase.is_extern_agg(call_name) != 0: + agg_kw: mlirlite.Value* = self.Gen.CallGenBase.emit_extern_agg_call(pool, builder, mod, call_name, head, int(actual_count), call_ret_ty) + if agg_kw is not None: return agg_kw return mlirlite.build_call(builder, call_name, head, actual_count, call_ret_ty, 0) \ No newline at end of file diff --git a/App/lib/core/Gen/Call/CallGenMixin.vp b/App/lib/core/Gen/Call/CallGenMixin.vp index 46647f6..6d3f150 100644 --- a/App/lib/core/Gen/Call/CallGenMixin.vp +++ b/App/lib/core/Gen/Call/CallGenMixin.vp @@ -179,6 +179,55 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase): # 早期补参用的是 dfx_callee_sha,链式接收者(`self.Gen.X.m(...)`)解析不出发起模块时它会 # 退化成调用方 SHA1,表查不到;此时 call_name 已带定义模块 SHA1 前缀,是权威键,再补一次。 # 返回补齐后的实参个数。 + def _default_param_llvm_ty(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, call_name: char*, idx: int, trans: HT.Translator*) -> mlirlite.LLVMType*: + """LLVM type of parameter `idx` in the callee's declaration (None when unavailable). + + An aggregate default is rebuilt from this type's struct fields, so the declare (created + by the vpi injection / forward declaration) must already exist at default-fill time. + + The injected declare may have resolved a bare struct annotation against a same-named + struct in THIS module. Re-resolve it by (name, callee module SHA1) when the declared + struct belongs to another module; otherwise the default is built with the wrong layout. + """ + if mod is None or call_name is None or idx < 0: return None + func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, call_name) + if func is None: return None + prm: mlirlite.Param* = mlirlite.function_get_param_head(func) + j: int = 0 + while prm is not None: + if j == idx: break + prm = mlirlite.param_get_next(prm) + j += 1 + if prm is None: return None + pty: mlirlite.LLVMType* = mlirlite.param_get_ty(prm) + if pty is None or pool is None: return pty + pe: mlirlite.LLVMType* = None + match pty: + case mlirlite.LLVMType.Ptr(pe0): pe = pe0 + case _: pe = None + if pe is None or pe.kind() != mlirlite.LLVMType.Struct: return pty + sname: char* = None + match pe: + case mlirlite.LLVMType.Struct(_, _, sn): sname = sn + case _: sname = None + if sname is None: return pty + sha_pfx: int = GenApi.sha1_prefix_len(call_name) + if sha_pfx == 0: return pty + sha_buf: char* = pool.alloc(17) + if sha_buf is None: return pty + string.strncpy(sha_buf, call_name, 16) + sha_buf[16] = '\0' + # Already the callee module's struct? (name is "{sha1}.Class") + if string.strncmp(sname, sha_buf, 16) == 0 and sname[16] == '.': return pty + dot_s: char* = string.strrchr(sname, '.') + bare: char* = sname + if dot_s is not None: bare = dot_s + 1 + VpiReader._ensure_dep_vpi_injected(pool, mod, sha_buf, trans) + se: GenTypes.StructEntry* = GenApi.find_struct_by_module(bare, sha_buf) + if se is not None and se.Ty is not None: + return mlirlite.Ptr(pool, se.Ty) + return pty + def _fill_defaults_late(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, builder: mlirlite.IRBuilder*, trans: HT.Translator*, cl: ast.Call*, call_name: char*, head: mlirlite.Value*, can: sizet) -> sizet: """按 call_name 里的定义模块 SHA1 前缀补缺省参数;返回新的实参个数""" if head is None or can == 0 or call_name is None: return can @@ -207,7 +256,10 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase): for df_i in range(int(de.DefaultCount)): df_pidx: int = df_off + df_i if df_pidx < n: continue - df_val: mlirlite.Value* = self.Gen.CallGenLeafKit._build_const_from_snapshot(pool, mod, de, df_i, trans, builder) + df_ty: mlirlite.LLVMType* = None + if de.ConstKinds is not None and de.ConstKinds[df_i] == GenTypes.CONST_AGG_DEFAULT: + df_ty = self._default_param_llvm_ty(pool, mod, call_name, df_pidx, trans) + df_val: mlirlite.Value* = self.Gen.CallGenLeafKit._build_const_from_snapshot(pool, mod, de, df_i, trans, builder, df_ty) if df_val is None: self.Gen.TypeGenErr.fatal_error(cl, f"default argument {df_i + 1} of '{call_name}' cannot be rebuilt into a value; refusing to emit a call with fewer operands than the callee declares", "TypeError") df_val.Next = None @@ -1428,7 +1480,18 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase): if df_param_idx >= can: # Prefer rebuilding from the stdlib snapshot (gd_entry.Defaults' in-pool AST may already be reclaimed). df_arg_val: mlirlite.Value* = None - if gd_entry is not None: df_arg_val = self.Gen.CallGenLeafKit._build_const_from_snapshot(pool, mod, gd_entry, df_di, trans, builder) + if gd_entry is not None: + # Aggregate defaults need the callee's parameter type: the injected + # declare may have bound a same-named struct from this module, and + # building from that layout silently passes the wrong bytes. + df_pty: mlirlite.LLVMType* = None + if gd_entry.ConstKinds is not None and gd_entry.ConstKinds[df_di] == GenTypes.CONST_AGG_DEFAULT: + df_call_nm: char* = func_name + if found_func is not None: + df_fn_nm: char* = mlirlite.function_get_name(found_func) + if df_fn_nm is not None: df_call_nm = df_fn_nm + df_pty = self._default_param_llvm_ty(pool, mod, df_call_nm, df_param_idx, trans) + df_arg_val = self.Gen.CallGenLeafKit._build_const_from_snapshot(pool, mod, gd_entry, df_di, trans, builder, df_pty) if df_arg_val is None and df_defaults is not None: df_defaults_list: list[ast.AST*]* = list[ast.AST*]*(df_defaults) df_default_node: ast.AST* = df_defaults_list.get(df_di) @@ -1804,5 +1867,10 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase): # instead of emitting an llvm.call the module's llvm.func does not accept. head = self.Gen.CallGenBase._conform_call_args(pool, builder, mod, call_name, head, cl, trans) call_ret_ty = self.Gen.CallGenBase._call_symbol_ret_ty(pool, mod, call_name, call_ret_ty, cl) + # C extern returning an aggregate: the C ABI differs from Viper's aggregate convention + # (Win64: i8/i16/i32/i64 return for exactly 1/2/4/8 bytes, hidden sret pointer otherwise). + if call_ret_ty is not None and call_ret_ty.kind() == mlirlite.LLVMType.Struct and self.Gen.CallGenBase.is_extern_agg(call_name) != 0: + agg_res: mlirlite.Value* = self.Gen.CallGenBase.emit_extern_agg_call(pool, builder, mod, call_name, head, int(can), call_ret_ty) + if agg_res is not None: return agg_res # try-except: when the callee takes EH out-params, append the output args and check for exceptions after the call. return self.Gen.CallGenLeafKit._emit_eh_guarded_call(builder, pool, mod, trans, found_func, call_name, head, can, call_ret_ty, is_variadic_call) diff --git a/App/lib/core/Gen/Class/ClassGenBase.vp b/App/lib/core/Gen/Class/ClassGenBase.vp index 0250e98..0b7a5c0 100644 --- a/App/lib/core/Gen/Class/ClassGenBase.vp +++ b/App/lib/core/Gen/Class/ClassGenBase.vp @@ -4,6 +4,7 @@ import memhub import string import stdlib import viperlib +import mlirlite import lib.core.Gen.Translator as HT import lib.core.CompilerFactory as CompilerFactory import lib.core.Gen.GenTypes as GenTypes @@ -219,7 +220,7 @@ novirtual class ClassGenBase: return 1 if string.strcmp(at2.attr, attr_name) == 0 else 0 return 0 def _class_has_tattr(self, decorator_list: list[ast.AST*]*, attr_name: str) -> int: - """Return 1 if decorator_list contains t.attr. (@c.Attribute(t.attr.xxx) or @t.attr.xxx).""" + """Return 1 if decorator_list contains t.attr. (@Attribute(attr.xxx) or @t.attr.xxx).""" if decorator_list is None: return 0 dn: sizet = len(decorator_list) if dn == 0: return 0 @@ -242,6 +243,110 @@ novirtual class ClassGenBase: # Direct @t.attr.xxx form. if self._tattr_name(deco, attr_name) != 0: return 1 return 0 + def _decor_expr_text(self, pool: memhub.MemBuddy*, node: ast.AST*, depth: int = 0) -> str: + """Dotted decorator text for diagnostics (Name / Attribute / Call).""" + if node is None or depth > 8: return "?" + k: int = node.kind() + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(node) + if nm.id is not None: return nm.id + return "?" + if k == ast.ASTKind.Call: + cl: ast.Call* = ast.Call*(node) + return self._decor_expr_text(pool, cl.func, depth + 1) + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(node) + if at.attr is None: return "?" + base: str = self._decor_expr_text(pool, at.value, depth + 1) + if base is None or string.strcmp(base, "?") == 0: return at.attr + cap: sizet = string.strlen(base) + string.strlen(at.attr) + 2 + buf: str = pool.alloc(cap) + if buf is None: return at.attr + viperlib.snprintf(buf, cap, "%s.%s", base, at.attr) + return buf + return "?" + def _decor_is_builtin_name(self, name: str) -> int: + """1 when name is a compiler-recognized bare decorator marker (class/function/method).""" + if name is None: return 0 + if string.strcmp(name, "staticmethod") == 0: return 1 + if string.strcmp(name, "property") == 0: return 1 + if string.strcmp(name, "classmethod") == 0: return 1 + if string.strcmp(name, "NoVTable") == 0: return 1 + if string.strcmp(name, "VTableT") == 0: return 1 + if string.strcmp(name, "TLS") == 0: return 1 + if string.strcmp(name, "StaticT") == 0: return 1 + if string.strcmp(name, "ExportT") == 0: return 1 + if string.strcmp(name, "ExternT") == 0: return 1 + if string.strcmp(name, "State") == 0: return 1 + if string.strcmp(name, "PureT") == 0: return 1 + if string.strcmp(name, "DefineT") == 0: return 1 + if string.strcmp(name, "InlineT") == 0: return 1 + if string.strcmp(name, "Object") == 0: return 1 + if string.strcmp(name, "EnumBase") == 0: return 1 + if string.strcmp(name, "UnionBase") == 0: return 1 + if string.strcmp(name, "REnum") == 0: return 1 + if string.strcmp(name, "life") == 0: return 1 + if string.strcmp(name, "willFree") == 0: return 1 + if string.strcmp(name, "Attribute") == 0: return 1 + return 0 + def _decor_is_attr_marker(self, name: str) -> int: + """1 when name is a recognized attribute marker or property accessor.""" + if name is None: return 0 + if string.strcmp(name, "setter") == 0: return 1 + if string.strcmp(name, "getter") == 0: return 1 + if string.strcmp(name, "deleter") == 0: return 1 + if string.strcmp(name, "packed") == 0: return 1 + if string.strcmp(name, "aligned") == 0: return 1 + if string.strcmp(name, "section") == 0: return 1 + if string.strcmp(name, "always_inline") == 0: return 1 + if string.strcmp(name, "noinline") == 0: return 1 + if string.strcmp(name, "noreturn") == 0: return 1 + if string.strcmp(name, "pure") == 0: return 1 + if string.strcmp(name, "const") == 0: return 1 + if string.strcmp(name, "nounwind") == 0: return 1 + if string.strcmp(name, "noredzone") == 0: return 1 + if string.strcmp(name, "stackrealign") == 0: return 1 + if string.strcmp(name, "willreturn") == 0: return 1 + if string.strcmp(name, "mustprogress") == 0: return 1 + if string.strcmp(name, "weak") == 0: return 1 + if string.strcmp(name, "visibility") == 0: return 1 + if string.strcmp(name, "naked") == 0: return 1 + return 0 + def _decor_handler_known(self, trans: HT.Translator*, mod: mlirlite.LLVMModule*, name: str) -> int: + """1 when a bare decorator name resolves to a declared decorator handler function.""" + if name is None or trans is None: return 0 + if mod is not None and self.Gen.CallGenBase.find_func_in_module(mod, name) is not None: return 1 + if self.Gen.CallGenLeafKit.find_func_entry_global(name) is not None: return 1 + return 0 + def validate_decorators(self, trans: HT.Translator*, mod: mlirlite.LLVMModule*, pool: memhub.MemBuddy*, owner_name: str, owner_kind: str, decorator_list: list[ast.AST*]*, is_class: int) -> int: + """Fail fast on decorators the compiler does not recognize (never silently ignore them).""" + if decorator_list is None: return 0 + dn: sizet = len(decorator_list) + if dn == 0: return 0 + for di in range(int(dn)): + deco: ast.AST* = decorator_list.get(di) + if deco is None: continue + k: int = deco.kind() + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(deco) + if nm.id is not None and self._decor_is_builtin_name(nm.id) != 0: continue + if is_class == 0 and self._decor_handler_known(trans, mod, nm.id) != 0: continue + elif k == ast.ASTKind.Call: + cl: ast.Call* = ast.Call*(deco) + if cl.func is not None and cl.func.kind() == ast.ASTKind.Name: + cnm: ast.Name* = ast.Name*(cl.func) + if cnm.id is not None and self._decor_is_builtin_name(cnm.id) != 0: continue + if is_class == 0 and self._decor_handler_known(trans, mod, cnm.id) != 0: continue + elif cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute: + cat: ast.Attribute* = ast.Attribute*(cl.func) + if cat.attr is not None and self._decor_is_attr_marker(cat.attr) != 0: continue + elif k == ast.ASTKind.Attribute: + dat: ast.Attribute* = ast.Attribute*(deco) + if dat.attr is not None and self._decor_is_attr_marker(dat.attr) != 0: continue + dtxt: str = self._decor_expr_text(pool, deco, 0) + self.Gen.TypeGenErr.fatal_error(deco, f"unrecognized decorator '@{dtxt}' on {owner_kind} '{owner_name}': ViperC recognizes built-in decorators, `@Attribute(...)` / attribute markers, property accessors, and custom decorators with a declared handler function", "TypeError") + return 1 + return 0 def _tattr_aligned_int(self, node: ast.AST*) -> int: """Return N for t.attr.aligned(N); 0 when not aligned or the argument is missing.""" if node is None: return 0 diff --git a/App/lib/core/Gen/Class/ClassGenDef.vp b/App/lib/core/Gen/Class/ClassGenDef.vp index d6f7584..71b137a 100644 --- a/App/lib/core/Gen/Class/ClassGenDef.vp +++ b/App/lib/core/Gen/Class/ClassGenDef.vp @@ -29,6 +29,10 @@ novirtual class ClassGenDef(ClassGenBase.ClassGenBase): pool: memhub.MemBuddy* = trans.Pool class_name: str = cd.name + # Reject unknown decorators instead of silently dropping them (e.g. the retired + # `@c.Attribute(...)` spelling); only recognized built-ins/markers are accepted here. + self.Gen.ClassGenBase.validate_decorators(trans, trans.Module, pool, class_name, "class", cd.decorator_list, 1) + if trans.ModuleSha1 is None: # fail-fast: ModuleSha1 missing while translating a class (namespace isolation depends on it) VLogger.error("ModuleSha1 missing while translating a class", "CLASS") @@ -466,7 +470,7 @@ novirtual class ClassGenDef(ClassGenBase.ClassGenBase): # Register the named struct into the module (emit the %"name" = type { ... } line) mlirlite.module_add_named_type(trans.Module, pool, struct_ty) - # B5: @c.Attribute(t.attr.packed) emits <{...}> (LLVM packed struct, no padding) as + # B5: @Attribute(attr.packed) emits <{...}> (LLVM packed struct, no padding) as # needed by binary-format structs like idt/gdt/elf/fat32. is_packed_cd: int = self.Gen.ClassGenBase._class_has_tattr(cd.decorator_list, "packed") if is_packed_cd != 0: mlirlite.module_set_named_type_packed(trans.Module, pool, struct_ty, 1) @@ -499,7 +503,7 @@ novirtual class ClassGenDef(ClassGenBase.ClassGenBase): # B5: record the packed flag into StructEntry for __sizeof__/layout queries if is_packed_cd != 0: entry.IsPacked = 1 - # B5: record the aligned value (@c.Attribute(t.attr.aligned(N)), e.g. fpu_context 16) + # B5: record the aligned value (@Attribute(attr.aligned(N)), e.g. fpu_context 16) # for global/local alloca alignment. align_cd: int = self.Gen.ClassGenBase._class_aligned_value(cd.decorator_list) if align_cd > 0: entry.Align = align_cd diff --git a/App/lib/core/Gen/Class/ClassGenEnum.vp b/App/lib/core/Gen/Class/ClassGenEnum.vp index c241f97..90feac7 100644 --- a/App/lib/core/Gen/Class/ClassGenEnum.vp +++ b/App/lib/core/Gen/Class/ClassGenEnum.vp @@ -14,7 +14,8 @@ from . import ClassGenBase # Local constant copies (originally defined in HandlesEnum/HandlesClassDef; lost when the file was split). # Undefined names fold to 0 in constant context, silently skipping enum member collection and EnumBase registration. -define ENUM_MEMBER_MAX_LOCAL = 128 +# ENUM_MEMBER_MAX is NOT defined here: it lives in GenTypes and is shared with EnumEntry storage +# (StmtGenDeclMixin) and the .vpi reader (VpiReader), so the three cannot drift apart. define RENUM_CLASS_MAX = 64 define RENUM_VARIANT_PER_CLASS = 32 define RENUM_ALIAS_MAX = 512 @@ -220,13 +221,13 @@ novirtual class ClassGenEnum(ClassGenBase.ClassGenBase): cn: sizet = len(children) # Temporary arrays holding member info - member_names_buf: sizet* = pool.alloc(8 * ENUM_MEMBER_MAX_LOCAL) - member_types_buf: sizet* = pool.alloc(8 * ENUM_MEMBER_MAX_LOCAL) - member_values_buf: int64* = pool.alloc(8 * ENUM_MEMBER_MAX_LOCAL) + member_names_buf: sizet* = pool.alloc(8 * GenTypes.ENUM_MEMBER_MAX) + member_types_buf: sizet* = pool.alloc(8 * GenTypes.ENUM_MEMBER_MAX) + member_values_buf: int64* = pool.alloc(8 * GenTypes.ENUM_MEMBER_MAX) if member_names_buf is None or member_types_buf is None or member_values_buf is None: return 0 - string.memset(member_names_buf, 0, 8 * ENUM_MEMBER_MAX_LOCAL) - string.memset(member_types_buf, 0, 8 * ENUM_MEMBER_MAX_LOCAL) - string.memset(member_values_buf, 0, 8 * ENUM_MEMBER_MAX_LOCAL) + string.memset(member_names_buf, 0, 8 * GenTypes.ENUM_MEMBER_MAX) + string.memset(member_types_buf, 0, 8 * GenTypes.ENUM_MEMBER_MAX) + string.memset(member_values_buf, 0, 8 * GenTypes.ENUM_MEMBER_MAX) member_count: int = 0 next_val: int64 = 0 @@ -273,7 +274,7 @@ novirtual class ClassGenEnum(ClassGenBase.ClassGenBase): next_val = cur_val + 1 # Store - if member_count < ENUM_MEMBER_MAX_LOCAL: + if member_count < GenTypes.ENUM_MEMBER_MAX: member_names_buf[member_count] = sizet(tgt.id) member_types_buf[member_count] = sizet(member_ty) member_values_buf[member_count] = cur_val @@ -303,7 +304,7 @@ novirtual class ClassGenEnum(ClassGenBase.ClassGenBase): if t0 is None or t0.kind() != ast.ASTKind.Name: continue tgt_a: ast.Name* = ast.Name*(t0) if tgt_a.id is None: continue - if member_count < ENUM_MEMBER_MAX_LOCAL: + if member_count < GenTypes.ENUM_MEMBER_MAX: member_names_buf[member_count] = sizet(tgt_a.id) member_types_buf[member_count] = sizet(member_ty_a) member_values_buf[member_count] = cur_val_a diff --git a/App/lib/core/Gen/Class/ClassGenVTable.vp b/App/lib/core/Gen/Class/ClassGenVTable.vp index 8184b4f..cafd641 100644 --- a/App/lib/core/Gen/Class/ClassGenVTable.vp +++ b/App/lib/core/Gen/Class/ClassGenVTable.vp @@ -572,6 +572,10 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase): funcs_ptr: GenTypes.FuncEntry* = trans._funcs func_count: int = trans._func_count + # Method decorators are validated here as well: unknown markers must not be dropped + # silently just because a method body uses a different translation entry point. + self.Gen.ClassGenBase.validate_decorators(trans, mod, pool, fd.name, "method", fd.decorator_list, 0) + i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool) self_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, struct_ty) @@ -818,6 +822,27 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase): # module-level function path). No-op when DI is disabled. dbg_pushed: int = DebugInfoGen.PushFunction(pool, func_builder, func, fd.lineno, fd.col_offset + 1) + # Constructors may be invoked on a null object when __new__ failed (pool exhausted). + # __before_init__ is null-safe; __init__ must be too, or the first field store is an + # access violation (ASan: LspRange.__init__ in the LSP semantic-token path). + if fd.name is not None and string.strcmp(fd.name, "__init__") == 0 and is_static_m == 0: + mi_sp: mlirlite.Param* = mlirlite.function_get_param_head(func) + if mi_sp is not None: + mi_sp_ty: mlirlite.LLVMType* = mlirlite.param_get_ty(mi_sp) + mi_sp_nm: char* = mlirlite.param_get_name(mi_sp) + if mi_sp_ty is not None and mi_sp_nm is not None: + mi_self: mlirlite.Value* = mlirlite.SSAValue(pool, mi_sp_ty, mi_sp_nm) + mi_null: mlirlite.Value* = mlirlite.ConstNull(pool, mi_sp_ty, "null") + mi_null_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "init.selfnull") + mi_body_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "init.body") + if mi_self is not None and mi_null is not None and mi_null_bb is not None and mi_body_bb is not None: + mi_is_null: mlirlite.Value* = mlirlite.build_icmp(func_builder, mlirlite.ICMP_EQ, mi_self, mi_null) + if mi_is_null is not None: + mlirlite.build_cond_br(func_builder, mi_is_null, mi_null_bb, mi_body_bb) + mlirlite.position_at_end(func_builder, mi_null_bb) + mlirlite.build_ret_void(func_builder) + mlirlite.position_at_end(func_builder, mi_body_bb) + # @t.TLS one-time init guard (same implementation as FuncGenMixin.translate_function_def, also for methods). if is_tls_m != 0: # Exact capacities: the reference used to be 64 while the definition was 128, so a long @@ -965,6 +990,21 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase): # Register self as an SSA value. self_val: mlirlite.Value* = mlirlite.SSAValue(pool, self_ptr_ty, "%self") + # Null-safe: __new__ may return null when the pool is exhausted (e.g. a large LSP + # semantic-token response), and zero-filling through a null self is an access violation + # (ASan: JsonValue.__before_init__). Return immediately; the constructor then yields null + # and callers that already check `if v == None` treat it as OOM instead of crashing. + null_self: mlirlite.Value* = mlirlite.ConstNull(pool, self_ptr_ty, "null") + body_blk: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "body") + null_blk: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "nullret") + if null_self is not None and body_blk is not None and null_blk is not None: + is_null: mlirlite.Value* = mlirlite.build_icmp(func_builder, mlirlite.ICMP_EQ, self_val, null_self) + if is_null is not None: + mlirlite.build_cond_br(func_builder, is_null, null_blk, body_blk) + mlirlite.position_at_end(func_builder, null_blk) + mlirlite.build_ret_void(func_builder) + mlirlite.position_at_end(func_builder, body_blk) + # Zero-fill: store zeroinitializer into %self. Empty structs ({}) cannot use zeroinitializer # (invalid type for null constant), so field-less classes skip it; @t.VTableT classes always have a slot. struct_fcount_zi: int = mlirlite.get_struct_fcount(struct_ty) diff --git a/App/lib/core/Gen/DebugInfoGen.vp b/App/lib/core/Gen/DebugInfoGen.vp index d4cc270..1191f6d 100644 --- a/App/lib/core/Gen/DebugInfoGen.vp +++ b/App/lib/core/Gen/DebugInfoGen.vp @@ -1,374 +1,368 @@ -from stdint import * -import mlirlite -import memhub -import string -import stdlib -import viperlib -import win32.win32file as win32file -import lib.core.VLogger as VLogger - -# DebugInfoGen - emits function-level DWARF metadata (mlirlite text IR), mirroring TPC LLVMCG/DebugInfo.py node-for-node. -# Active only under --traceback (SetEnabled); hooks come from the Gen layer. When disabled every entry point returns before touching DI APIs, keeping IR byte-identical. - -# ---- module-scope state (reset per module by BeginModule) ---- -_g_enabled: int = 0 -_g_finalized: int = 0 -_g_mod: mlirlite.LLVMModule* = None -_g_file_path: str = None -_g_abs_file_path: str = None -_g_file_id: int = -1 -_g_null_id: int = -1 -_g_subtype_id: int = -1 -_g_cu_id: int = -1 -_g_cur_sp: int = -1 -_g_cur_entry_ref: str = None -_g_cur_builder: mlirlite.IRBuilder* = None -_g_depth: int = 0 - -# Push/Pop save stack: enclosing (sp id + 1, entry DILocation ref, builder) per level; +1 lets 0 mean "no enclosing function". -# The saved builder makes SetStatement reject a builder not pushed by the current DISubprogram (e.g. an async state machine in a bracketed function); such instrs must carry no !dbg or LLVM's verifier fails. -define DBG_STACK_MAX = 256 -_g_stk_sp: sizet* = None -_g_stk_ref: str = None -_g_stk_bld: str = None - -# DILocation cache: direct-mapped, up to 8-way linear probe, key (sp, line, col). -define DBG_LOC_SLOTS = 8192 -define DBG_LOC_PROBE = 8 -define DBG_LOC_KEY_STRIDE = 3 -_g_loc_key: sizet* = None -_g_loc_ref: str = None -_g_loc_used: bytes = None - - -# SetEnabled - mirror the --traceback switch (flag -> _g_enabled, forwarded to mlirlite's debug call-site recorder). -def SetEnabled(flag: bool) -> int: - """Enable/disable DI emission (mirrors BuildPipeline._traceback_mode).""" - global _g_enabled - if flag: _g_enabled = 1 - else: _g_enabled = 0 - # Forward the same switch to mlirlite's debug call-site recorder (under --traceback we emit a .vpcall side file for WriteStackMap+objdump). - mlirlite.DebugSetEnabled(flag) - return 0 - - -# _EnsureStorage - lazily allocate the long-lived stack / cache buffers. -def _EnsureStorage() -> int: - global _g_stk_sp, _g_stk_ref, _g_stk_bld, _g_loc_key, _g_loc_ref, _g_loc_used - if _g_stk_sp is not None: return 0 - sbuf1: bytes = stdlib.malloc(DBG_STACK_MAX * 8) - sbuf2: bytes = stdlib.malloc(DBG_STACK_MAX * 8) - sbuf3: bytes = stdlib.malloc(DBG_STACK_MAX * 8) - kbuf: bytes = stdlib.malloc(DBG_LOC_SLOTS * DBG_LOC_KEY_STRIDE * 8) - rbuf: bytes = stdlib.malloc(DBG_LOC_SLOTS * 8) - ubuf: bytes = stdlib.malloc(DBG_LOC_SLOTS) - if sbuf1 is None or sbuf2 is None or sbuf3 is None or kbuf is None or rbuf is None or ubuf is None: - VLogger.error("--traceback: failed to allocate DWARF metadata buffer, aborting compilation", "DWARF") - return 1 - string.memset(sbuf1, 0, DBG_STACK_MAX * 8) - string.memset(sbuf2, 0, DBG_STACK_MAX * 8) - string.memset(sbuf3, 0, DBG_STACK_MAX * 8) - string.memset(kbuf, 0, DBG_LOC_SLOTS * DBG_LOC_KEY_STRIDE * 8) - string.memset(rbuf, 0, DBG_LOC_SLOTS * 8) - string.memset(ubuf, 0, DBG_LOC_SLOTS) - _g_stk_sp = (sizet*)(sbuf1) - _g_stk_ref = sbuf2 - _g_stk_bld = sbuf3 - _g_loc_key = (sizet*)(kbuf) - _g_loc_ref = rbuf - _g_loc_used = ubuf - return 0 - - -def _RefOf(pool: memhub.MemBuddy*, node_id: int) -> str: - """Allocate a persistent "!N" reference string for a DI node id.""" - buf: str = pool.alloc(24) - if buf is None: return None - viperlib.snprintf(buf, 24, "!%d", node_id) - return buf - - -# BeginModule - start of one module translation. -def BeginModule(mod: mlirlite.LLVMModule*, file_path: str) -> int: - """Reset per-module DI state. Call once at the start of each module.""" - global _g_mod, _g_file_path, _g_abs_file_path, _g_finalized, _g_file_id, _g_null_id - global _g_subtype_id, _g_cu_id, _g_cur_sp, _g_cur_entry_ref, _g_cur_builder, _g_depth - if _g_enabled == 0: return 0 - if _EnsureStorage() != 0: return 1 - _g_mod = mod - _g_file_path = file_path - _g_abs_file_path = None - # Module-boundary fallback: clear any un-Pop'd debug function context (mid-translation failure) so the previous module's caller is not attributed here. - mlirlite.DebugResetFunc() - _g_finalized = 0 - _g_file_id = -1 - _g_null_id = -1 - _g_subtype_id = -1 - _g_cu_id = -1 - _g_cur_sp = -1 - _g_cur_entry_ref = None - _g_cur_builder = None - _g_depth = 0 - if _g_loc_used is not None: string.memset(_g_loc_used, 0, DBG_LOC_SLOTS) - return 0 - - -# _EnsureModuleNodes - lazily create DIFile / !{ null } / DISubroutineType / DICompileUnit at the first define (once per module). -def _EnsureModuleNodes(pool: memhub.MemBuddy*) -> int: - global _g_file_id, _g_null_id, _g_subtype_id, _g_cu_id, _g_abs_file_path - if _g_cu_id >= 0: return 0 - mod: mlirlite.LLVMModule* = _g_mod - if mod is None: return 1 - # DIFile: absolute path (forward slashes), filename keeps the whole path. - # Must be absolute: relative paths make LLVM's DWARF line records degrade to a bare "main.vp" that runtime cannot reload; absolutizing works whether LLVM keeps the full name or splits dir+basename. - src: str = _g_file_path - if src is None: src = "" - slen: sizet = string.strlen(src) - cap_n: sizet = slen + 4112 - nbuf: str = pool.alloc(cap_n) - if nbuf is None: - VLogger.error("--traceback: failed to allocate DIFile path buffer, aborting compilation", "DWARF") - return 1 - if slen > 0: string.strncpy(nbuf, src, slen) - nbuf[slen] = '\0' - if slen > 0 and nbuf[0] != '/' and not (slen >= 2 and nbuf[1] == ':'): - # Relative path -> prefix CWD via a win32file low-level call (as in Repl.vp), not os._mbuddy which the compiler process may not have initialized. - CwdBuf: bytes = pool.alloc(4112) - if CwdBuf is not None: - CwdBuf[0] = '\0' - nc: ULONG = win32file.GetCurrentDirectoryA(ULONG(4111), CwdBuf) - if nc > 0: - # Defensive: when the buffer is too small GetCurrentDirectoryA writes undefined content; force a NUL. - CwdBuf[4110] = '\0' - viperlib.snprintf(nbuf, cap_n, "%s/%s", CwdBuf, src) - # Normalize all backslashes (the CWD segment may contain '\' too). - flen: sizet = string.strlen(nbuf) - ni: sizet = 0 - for ni in range(int(flen)): - if nbuf[ni] == '\\': nbuf[ni] = '/' - # Save the absolute path for mlirlite's debug call-site recorder (.vpcall file paths must match DIFile: absolute + forward slash). - _g_abs_file_path = nbuf - # filename = whole path; directory = its prefix (both NUL-terminated, non-overlapping). - fname: str = nbuf - directory: str = "" - slash: str = string.strrchr(nbuf, '/') - if slash is not None: - dlen: sizet = string.strlen(nbuf) - string.strlen(slash + 1) - 1 - dirbuf: str = pool.alloc(dlen + 1) - if dirbuf is not None: - string.strncpy(dirbuf, nbuf, dlen) - dirbuf[dlen] = '\0' - directory = dirbuf - dsize: sizet = string.strlen(fname) + string.strlen(directory) + 96 - dbuf: str = pool.alloc(dsize) - if dbuf is None: - VLogger.error("--traceback: failed to allocate DIFile node buffer, aborting compilation", "DWARF") - return 1 - viperlib.snprintf(dbuf, dsize, "!DIFile(filename: \"%s\", directory: \"%s\")", fname, directory) - fid: int = mlirlite.module_add_di_node(mod, pool, dbuf) - if fid < 0: - VLogger.error("--traceback: failed to allocate DIFile metadata node, aborting compilation", "DWARF") - return 1 - _g_file_id = fid - # types: !{ null } placeholder list, then DISubroutineType referencing it - nid: int = mlirlite.module_add_di_node(mod, pool, "!{ null }") - if nid < 0: - VLogger.error("--traceback: failed to allocate DISubroutineType type table, aborting compilation", "DWARF") - return 1 - _g_null_id = nid - tbuf: str = pool.alloc(64) - if tbuf is None: - VLogger.error("--traceback: failed to allocate DISubroutineType buffer, aborting compilation", "DWARF") - return 1 - viperlib.snprintf(tbuf, 64, "!DISubroutineType(types: !%d)", _g_null_id) - tid: int = mlirlite.module_add_di_node(mod, pool, tbuf) - if tid < 0: - VLogger.error("--traceback: failed to allocate DISubroutineType metadata node, aborting compilation", "DWARF") - return 1 - _g_subtype_id = tid - cbuf: str = pool.alloc(256) - if cbuf is None: - VLogger.error("--traceback: failed to allocate DICompileUnit buffer, aborting compilation", "DWARF") - return 1 - viperlib.snprintf(cbuf, 256, "distinct !DICompileUnit(language: DW_LANG_C99, file: !%d, producer: \"TPC\", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)", _g_file_id) - cid: int = mlirlite.module_add_di_node(mod, pool, cbuf) - if cid < 0: - VLogger.error("--traceback: failed to allocate DICompileUnit metadata node, aborting compilation", "DWARF") - return 1 - _g_cu_id = cid - return 0 - - -# _LocRefAt - read a cached DILocation ref string pointer. -def _LocRefAt(slot: int) -> str: - slotp: str* = (str*)(uint64(_g_loc_ref) + sizet(slot) * 8) - r: str = slotp[0] - return r - - -# _EnsureLocation - DILocation ref for (sp, line, col), created + cached once. -def _EnsureLocation(pool: memhub.MemBuddy*, sp: int, line: int, col: int) -> str: - if line <= 0: return _g_cur_entry_ref - c: int = col - if c <= 0: c = 1 - if _g_loc_used is None or _g_mod is None: return None - h: uint64 = (uint64(sp) * 1000003 + uint64(line) * 31 + uint64(c)) & uint64(DBG_LOC_SLOTS - 1) - pi: int = 0 - while pi < DBG_LOC_PROBE: - slot: int = int((h + uint64(pi)) & uint64(DBG_LOC_SLOTS - 1)) - if _g_loc_used[slot] == 0: break - kbase: int = slot * DBG_LOC_KEY_STRIDE - if int(_g_loc_key[kbase]) == sp and int(_g_loc_key[kbase + 1]) == line and int(_g_loc_key[kbase + 2]) == c: return _LocRefAt(slot) - pi += 1 - lbuf: str = pool.alloc(96) - if lbuf is None: return None - viperlib.snprintf(lbuf, 96, "!DILocation(line: %d, column: %d, scope: !%d)", line, c, sp) - lid: int = mlirlite.module_add_di_node(_g_mod, pool, lbuf) - if lid < 0: - VLogger.error("--traceback: failed to allocate DILocation metadata node, aborting compilation", "DWARF") - return None - lref: str = _RefOf(pool, lid) - if lref is None: - VLogger.error("--traceback: failed to allocate DILocation reference buffer, aborting compilation", "DWARF") - return None - if pi < DBG_LOC_PROBE: - slot2: int = int((h + uint64(pi)) & uint64(DBG_LOC_SLOTS - 1)) - kbase2: int = slot2 * DBG_LOC_KEY_STRIDE - _g_loc_key[kbase2] = sizet(sp) - _g_loc_key[kbase2 + 1] = sizet(line) - _g_loc_key[kbase2 + 2] = sizet(c) - rslot: str* = (str*)(uint64(_g_loc_ref) + sizet(slot2) * 8) - rslot[0] = lref - _g_loc_used[slot2] = 1 - return lref - - -# PushFunction - create the DISubprogram for one define. -def PushFunction(pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, func: mlirlite.Function*, line: int, col: int) -> int: - """Attach a DISubprogram to one define and set the builder's entry DILocation.""" - global _g_cur_sp, _g_cur_entry_ref, _g_cur_builder, _g_depth - if _g_enabled == 0: return 0 - if pool is None or builder is None or func is None or _g_mod is None: return 0 - if _g_depth >= DBG_STACK_MAX: - VLogger.error("--traceback: DWARF function nesting depth exceeds the limit, aborting compilation", "DWARF") - return 0 - if _EnsureModuleNodes(pool) != 0: return 0 - name: str = mlirlite.function_get_name(func) - if name is None: - VLogger.error("--traceback: function name is empty, cannot emit DISubprogram, aborting compilation", "DWARF") - return 0 - dline: int = line - if dline <= 0: dline = 1 - dcol: int = col - if dcol <= 0: dcol = 1 - ssize: sizet = string.strlen(name) + 320 - spbuf: str = pool.alloc(ssize) - if spbuf is None: - VLogger.error("--traceback: failed to allocate DISubprogram buffer, aborting compilation", "DWARF") - return 0 - viperlib.snprintf(spbuf, ssize, "distinct !DISubprogram(name: \"%s\", scope: !%d, file: !%d, type: !%d, line: %d, scopeLine: %d, unit: !%d, spFlags: DISPFlagDefinition)", name, _g_file_id, _g_file_id, _g_subtype_id, dline, dline, _g_cu_id) - sp_id: int = mlirlite.module_add_di_node(_g_mod, pool, spbuf) - if sp_id < 0: - VLogger.error("--traceback: failed to allocate DISubprogram metadata node, aborting compilation", "DWARF") - return 0 - sp_ref: str = _RefOf(pool, sp_id) - if sp_ref is None: - VLogger.error("--traceback: failed to allocate DISubprogram reference buffer, aborting compilation", "DWARF") - return 0 - mlirlite.function_set_dbg(func, sp_ref) - # save the enclosing function's DI context (LIFO), then switch to this one - _g_stk_sp[_g_depth] = sizet(_g_cur_sp + 1) - rslot: str* = (str*)(uint64(_g_stk_ref) + sizet(_g_depth) * 8) - rslot[0] = _g_cur_entry_ref - bslot: str* = (str*)(uint64(_g_stk_bld) + sizet(_g_depth) * 8) - bslot[0] = _g_cur_builder - _g_depth += 1 - _g_cur_sp = sp_id - _g_cur_builder = builder - entry_ref: str = _EnsureLocation(pool, sp_id, dline, dcol) - if entry_ref is None: - VLogger.error("--traceback: failed to generate entry DILocation, aborting compilation", "DWARF") - return 0 - _g_cur_entry_ref = entry_ref - mlirlite.builder_set_debug_location(builder, entry_ref) - # Enter this define: tell mlirlite's call-site recorder the current symbol/file/def-line and write an F record (for the .vpcall file + WriteStackMap entry fallback line). - mlirlite.DebugNoteFunc(name, _g_abs_file_path, dline) - return 1 - - -# PopFunction - restore the enclosing function's DI context. -def PopFunction(builder: mlirlite.IRBuilder*) -> int: - global _g_cur_sp, _g_cur_entry_ref, _g_cur_builder, _g_depth - if _g_enabled == 0: return 0 - if _g_depth <= 0: return 0 - _g_depth -= 1 - _g_cur_sp = int(_g_stk_sp[_g_depth]) - 1 - rslot: str* = (str*)(uint64(_g_stk_ref) + sizet(_g_depth) * 8) - _g_cur_entry_ref = rslot[0] - bslot: str* = (str*)(uint64(_g_stk_bld) + sizet(_g_depth) * 8) - _g_cur_builder = (mlirlite.IRBuilder*)(bslot[0]) - if builder is not None: mlirlite.builder_clear_debug_location(builder) - # Leave this define: restore mlirlite's outer function context (for nested defines). - mlirlite.DebugClearFunc() - return 0 - - -# SetStatement - point the current builder at a statement DILocation. -def SetStatement(pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, line: int, col: int) -> int: - """Update the builder's DILocation to the current statement (line precision).""" - if _g_enabled == 0 or pool is None or builder is None: return 0 - # Record the current statement line: C records from build_call in this statement use it as the call-site line. - mlirlite.DebugNoteLine(line) - if _g_cur_sp < 0: return 0 - ref: str = _EnsureLocation(pool, _g_cur_sp, line, col) - if ref is None: return 0 - mlirlite.builder_set_debug_location(builder, ref) - return 1 - - -# _ModuleHasDefine - does the module emit any define (IsDeclared == 0)? -def _ModuleHasDefine(mod: mlirlite.LLVMModule*) -> int: - if mod is None: return 0 - cur: mlirlite.Function* = mod.FuncHead - steps: int = 0 - while cur is not None: - steps += 1 - if steps > 100000: break - if mlirlite.function_is_declared(cur) == 0: return 1 - cur = mlirlite.function_get_next(cur) - return 0 - - -# Finalize - register !llvm.dbg.cu / !llvm.module.flags before dump_ir. -def Finalize(pool: memhub.MemBuddy*) -> int: - """Module-level DI tail; idempotent per module, no-op when no SP was emitted.""" - global _g_finalized - if _g_enabled == 0: return 0 - if _g_finalized != 0: return 0 - mod: mlirlite.LLVMModule* = _g_mod - if mod is None or pool is None: return 0 - _g_finalized = 1 - # Self-check (mirrors TPC Phase2Translator.py:3444): a module that emits a - # define but carries no DI at all loses line info for its frames. - if mod.DIMetaCount == 0 and _ModuleHasDefine(mod) != 0: VLogger.warning("--traceback: this module emits a define but its IR carries no DWARF (!llvm.dbg.cu); its frames will show only addresses in the Ctrl+C stack", "DWARF") - if _g_cu_id < 0: return 0 - cu_ref: str = _RefOf(pool, _g_cu_id) - if cu_ref is None: - VLogger.error("--traceback: failed to allocate DICompileUnit reference buffer, aborting compilation", "DWARF") - return 1 - mlirlite.module_add_named_metadata(mod, pool, "llvm.dbg.cu", cu_ref) - fbuf: str = pool.alloc(64) - if fbuf is None: - VLogger.error("--traceback: failed to allocate module-level debug flags buffer, aborting compilation", "DWARF") - return 1 - viperlib.snprintf(fbuf, 64, "!{i32 2, !\"Debug Info Version\", i32 3}") - flags_id: int = mlirlite.module_add_di_node(mod, pool, fbuf) - if flags_id < 0: - VLogger.error("--traceback: failed to allocate llvm.module.flags metadata node, aborting compilation", "DWARF") - return 1 - fl_ref: str = _RefOf(pool, flags_id) - if fl_ref is None: - VLogger.error("--traceback: failed to allocate llvm.module.flags reference buffer, aborting compilation", "DWARF") - return 1 - mlirlite.module_add_named_metadata(mod, pool, "llvm.module.flags", fl_ref) - return 0 +from stdint import * +import mlirlite +import memhub +import string +import stdlib +import viperlib +import os +import lib.core.VLogger as VLogger + +# DebugInfoGen - emits function-level DWARF metadata (mlirlite text IR), mirroring TPC LLVMCG/DebugInfo.py node-for-node. +# Active only under --traceback (SetEnabled); hooks come from the Gen layer. When disabled every entry point returns before touching DI APIs, keeping IR byte-identical. + +# ---- module-scope state (reset per module by BeginModule) ---- +_g_enabled: int = 0 +_g_finalized: int = 0 +_g_mod: mlirlite.LLVMModule* = None +_g_file_path: str = None +_g_abs_file_path: str = None +_g_file_id: int = -1 +_g_null_id: int = -1 +_g_subtype_id: int = -1 +_g_cu_id: int = -1 +_g_cur_sp: int = -1 +_g_cur_entry_ref: str = None +_g_cur_builder: mlirlite.IRBuilder* = None +_g_depth: int = 0 + +# Push/Pop save stack: enclosing (sp id + 1, entry DILocation ref, builder) per level; +1 lets 0 mean "no enclosing function". +# The saved builder makes SetStatement reject a builder not pushed by the current DISubprogram (e.g. an async state machine in a bracketed function); such instrs must carry no !dbg or LLVM's verifier fails. +define DBG_STACK_MAX = 256 +_g_stk_sp: sizet* = None +_g_stk_ref: str = None +_g_stk_bld: str = None + +# DILocation cache: direct-mapped, up to 8-way linear probe, key (sp, line, col). +define DBG_LOC_SLOTS = 8192 +define DBG_LOC_PROBE = 8 +define DBG_LOC_KEY_STRIDE = 3 +_g_loc_key: sizet* = None +_g_loc_ref: str = None +_g_loc_used: bytes = None + + +# SetEnabled - mirror the --traceback switch (flag -> _g_enabled, forwarded to mlirlite's debug call-site recorder). +def SetEnabled(flag: bool) -> int: + """Enable/disable DI emission (mirrors BuildPipeline._traceback_mode).""" + global _g_enabled + if flag: _g_enabled = 1 + else: _g_enabled = 0 + # Forward the same switch to mlirlite's debug call-site recorder (under --traceback we emit a .vpcall side file for WriteStackMap+objdump). + mlirlite.DebugSetEnabled(flag) + return 0 + + +# _EnsureStorage - lazily allocate the long-lived stack / cache buffers. +def _EnsureStorage() -> int: + global _g_stk_sp, _g_stk_ref, _g_stk_bld, _g_loc_key, _g_loc_ref, _g_loc_used + if _g_stk_sp is not None: return 0 + sbuf1: bytes = stdlib.malloc(DBG_STACK_MAX * 8) + sbuf2: bytes = stdlib.malloc(DBG_STACK_MAX * 8) + sbuf3: bytes = stdlib.malloc(DBG_STACK_MAX * 8) + kbuf: bytes = stdlib.malloc(DBG_LOC_SLOTS * DBG_LOC_KEY_STRIDE * 8) + rbuf: bytes = stdlib.malloc(DBG_LOC_SLOTS * 8) + ubuf: bytes = stdlib.malloc(DBG_LOC_SLOTS) + if sbuf1 is None or sbuf2 is None or sbuf3 is None or kbuf is None or rbuf is None or ubuf is None: + VLogger.error("--traceback: failed to allocate DWARF metadata buffer, aborting compilation", "DWARF") + return 1 + string.memset(sbuf1, 0, DBG_STACK_MAX * 8) + string.memset(sbuf2, 0, DBG_STACK_MAX * 8) + string.memset(sbuf3, 0, DBG_STACK_MAX * 8) + string.memset(kbuf, 0, DBG_LOC_SLOTS * DBG_LOC_KEY_STRIDE * 8) + string.memset(rbuf, 0, DBG_LOC_SLOTS * 8) + string.memset(ubuf, 0, DBG_LOC_SLOTS) + _g_stk_sp = (sizet*)(sbuf1) + _g_stk_ref = sbuf2 + _g_stk_bld = sbuf3 + _g_loc_key = (sizet*)(kbuf) + _g_loc_ref = rbuf + _g_loc_used = ubuf + return 0 + + +def _RefOf(pool: memhub.MemBuddy*, node_id: int) -> str: + """Allocate a persistent "!N" reference string for a DI node id.""" + buf: str = pool.alloc(24) + if buf is None: return None + viperlib.snprintf(buf, 24, "!%d", node_id) + return buf + + +# BeginModule - start of one module translation. +def BeginModule(mod: mlirlite.LLVMModule*, file_path: str) -> int: + """Reset per-module DI state. Call once at the start of each module.""" + global _g_mod, _g_file_path, _g_abs_file_path, _g_finalized, _g_file_id, _g_null_id + global _g_subtype_id, _g_cu_id, _g_cur_sp, _g_cur_entry_ref, _g_cur_builder, _g_depth + if _g_enabled == 0: return 0 + if _EnsureStorage() != 0: return 1 + _g_mod = mod + _g_file_path = file_path + _g_abs_file_path = None + # Module-boundary fallback: clear any un-Pop'd debug function context (mid-translation failure) so the previous module's caller is not attributed here. + mlirlite.DebugResetFunc() + _g_finalized = 0 + _g_file_id = -1 + _g_null_id = -1 + _g_subtype_id = -1 + _g_cu_id = -1 + _g_cur_sp = -1 + _g_cur_entry_ref = None + _g_cur_builder = None + _g_depth = 0 + if _g_loc_used is not None: string.memset(_g_loc_used, 0, DBG_LOC_SLOTS) + return 0 + + +# _EnsureModuleNodes - lazily create DIFile / !{ null } / DISubroutineType / DICompileUnit at the first define (once per module). +def _EnsureModuleNodes(pool: memhub.MemBuddy*) -> int: + global _g_file_id, _g_null_id, _g_subtype_id, _g_cu_id, _g_abs_file_path + if _g_cu_id >= 0: return 0 + mod: mlirlite.LLVMModule* = _g_mod + if mod is None: return 1 + # DIFile: absolute path (forward slashes), filename keeps the whole path. + # Must be absolute: relative paths make LLVM's DWARF line records degrade to a bare "main.vp" that runtime cannot reload; absolutizing works whether LLVM keeps the full name or splits dir+basename. + src: str = _g_file_path + if src is None: src = "" + slen: sizet = string.strlen(src) + cap_n: sizet = slen + 4112 + nbuf: str = pool.alloc(cap_n) + if nbuf is None: + VLogger.error("--traceback: failed to allocate DIFile path buffer, aborting compilation", "DWARF") + return 1 + if slen > 0: string.strncpy(nbuf, src, slen) + nbuf[slen] = '\0' + if slen > 0 and nbuf[0] != '/' and not (slen >= 2 and nbuf[1] == ':'): + cwd: str = os.getcwd() + if cwd is not None: + viperlib.snprintf(nbuf, cap_n, "%s/%s", cwd, src) + # Normalize all backslashes (the CWD segment may contain '\' too). + flen: sizet = string.strlen(nbuf) + ni: sizet = 0 + for ni in range(int(flen)): + if nbuf[ni] == '\\': nbuf[ni] = '/' + # Save the absolute path for mlirlite's debug call-site recorder (.vpcall file paths must match DIFile: absolute + forward slash). + _g_abs_file_path = nbuf + # filename = whole path; directory = its prefix (both NUL-terminated, non-overlapping). + fname: str = nbuf + directory: str = "" + slash: str = string.strrchr(nbuf, '/') + if slash is not None: + dlen: sizet = string.strlen(nbuf) - string.strlen(slash + 1) - 1 + dirbuf: str = pool.alloc(dlen + 1) + if dirbuf is not None: + string.strncpy(dirbuf, nbuf, dlen) + dirbuf[dlen] = '\0' + directory = dirbuf + dsize: sizet = string.strlen(fname) + string.strlen(directory) + 96 + dbuf: str = pool.alloc(dsize) + if dbuf is None: + VLogger.error("--traceback: failed to allocate DIFile node buffer, aborting compilation", "DWARF") + return 1 + viperlib.snprintf(dbuf, dsize, "!DIFile(filename: \"%s\", directory: \"%s\")", fname, directory) + fid: int = mlirlite.module_add_di_node(mod, pool, dbuf) + if fid < 0: + VLogger.error("--traceback: failed to allocate DIFile metadata node, aborting compilation", "DWARF") + return 1 + _g_file_id = fid + # types: !{ null } placeholder list, then DISubroutineType referencing it + nid: int = mlirlite.module_add_di_node(mod, pool, "!{ null }") + if nid < 0: + VLogger.error("--traceback: failed to allocate DISubroutineType type table, aborting compilation", "DWARF") + return 1 + _g_null_id = nid + tbuf: str = pool.alloc(64) + if tbuf is None: + VLogger.error("--traceback: failed to allocate DISubroutineType buffer, aborting compilation", "DWARF") + return 1 + viperlib.snprintf(tbuf, 64, "!DISubroutineType(types: !%d)", _g_null_id) + tid: int = mlirlite.module_add_di_node(mod, pool, tbuf) + if tid < 0: + VLogger.error("--traceback: failed to allocate DISubroutineType metadata node, aborting compilation", "DWARF") + return 1 + _g_subtype_id = tid + cbuf: str = pool.alloc(256) + if cbuf is None: + VLogger.error("--traceback: failed to allocate DICompileUnit buffer, aborting compilation", "DWARF") + return 1 + viperlib.snprintf(cbuf, 256, "distinct !DICompileUnit(language: DW_LANG_C99, file: !%d, producer: \"TPC\", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)", _g_file_id) + cid: int = mlirlite.module_add_di_node(mod, pool, cbuf) + if cid < 0: + VLogger.error("--traceback: failed to allocate DICompileUnit metadata node, aborting compilation", "DWARF") + return 1 + _g_cu_id = cid + return 0 + + +# _LocRefAt - read a cached DILocation ref string pointer. +def _LocRefAt(slot: int) -> str: + slotp: str* = (str*)(uint64(_g_loc_ref) + sizet(slot) * 8) + r: str = slotp[0] + return r + + +# _EnsureLocation - DILocation ref for (sp, line, col), created + cached once. +def _EnsureLocation(pool: memhub.MemBuddy*, sp: int, line: int, col: int) -> str: + if line <= 0: return _g_cur_entry_ref + c: int = col + if c <= 0: c = 1 + if _g_loc_used is None or _g_mod is None: return None + h: uint64 = (uint64(sp) * 1000003 + uint64(line) * 31 + uint64(c)) & uint64(DBG_LOC_SLOTS - 1) + pi: int = 0 + while pi < DBG_LOC_PROBE: + slot: int = int((h + uint64(pi)) & uint64(DBG_LOC_SLOTS - 1)) + if _g_loc_used[slot] == 0: break + kbase: int = slot * DBG_LOC_KEY_STRIDE + if int(_g_loc_key[kbase]) == sp and int(_g_loc_key[kbase + 1]) == line and int(_g_loc_key[kbase + 2]) == c: return _LocRefAt(slot) + pi += 1 + lbuf: str = pool.alloc(96) + if lbuf is None: return None + viperlib.snprintf(lbuf, 96, "!DILocation(line: %d, column: %d, scope: !%d)", line, c, sp) + lid: int = mlirlite.module_add_di_node(_g_mod, pool, lbuf) + if lid < 0: + VLogger.error("--traceback: failed to allocate DILocation metadata node, aborting compilation", "DWARF") + return None + lref: str = _RefOf(pool, lid) + if lref is None: + VLogger.error("--traceback: failed to allocate DILocation reference buffer, aborting compilation", "DWARF") + return None + if pi < DBG_LOC_PROBE: + slot2: int = int((h + uint64(pi)) & uint64(DBG_LOC_SLOTS - 1)) + kbase2: int = slot2 * DBG_LOC_KEY_STRIDE + _g_loc_key[kbase2] = sizet(sp) + _g_loc_key[kbase2 + 1] = sizet(line) + _g_loc_key[kbase2 + 2] = sizet(c) + rslot: str* = (str*)(uint64(_g_loc_ref) + sizet(slot2) * 8) + rslot[0] = lref + _g_loc_used[slot2] = 1 + return lref + + +# PushFunction - create the DISubprogram for one define. +def PushFunction(pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, func: mlirlite.Function*, line: int, col: int) -> int: + """Attach a DISubprogram to one define and set the builder's entry DILocation.""" + global _g_cur_sp, _g_cur_entry_ref, _g_cur_builder, _g_depth + if _g_enabled == 0: return 0 + if pool is None or builder is None or func is None or _g_mod is None: return 0 + if _g_depth >= DBG_STACK_MAX: + VLogger.error("--traceback: DWARF function nesting depth exceeds the limit, aborting compilation", "DWARF") + return 0 + if _EnsureModuleNodes(pool) != 0: return 0 + name: str = mlirlite.function_get_name(func) + if name is None: + VLogger.error("--traceback: function name is empty, cannot emit DISubprogram, aborting compilation", "DWARF") + return 0 + dline: int = line + if dline <= 0: dline = 1 + dcol: int = col + if dcol <= 0: dcol = 1 + ssize: sizet = string.strlen(name) + 320 + spbuf: str = pool.alloc(ssize) + if spbuf is None: + VLogger.error("--traceback: failed to allocate DISubprogram buffer, aborting compilation", "DWARF") + return 0 + viperlib.snprintf(spbuf, ssize, "distinct !DISubprogram(name: \"%s\", scope: !%d, file: !%d, type: !%d, line: %d, scopeLine: %d, unit: !%d, spFlags: DISPFlagDefinition)", name, _g_file_id, _g_file_id, _g_subtype_id, dline, dline, _g_cu_id) + sp_id: int = mlirlite.module_add_di_node(_g_mod, pool, spbuf) + if sp_id < 0: + VLogger.error("--traceback: failed to allocate DISubprogram metadata node, aborting compilation", "DWARF") + return 0 + sp_ref: str = _RefOf(pool, sp_id) + if sp_ref is None: + VLogger.error("--traceback: failed to allocate DISubprogram reference buffer, aborting compilation", "DWARF") + return 0 + mlirlite.function_set_dbg(func, sp_ref) + # save the enclosing function's DI context (LIFO), then switch to this one + _g_stk_sp[_g_depth] = sizet(_g_cur_sp + 1) + rslot: str* = (str*)(uint64(_g_stk_ref) + sizet(_g_depth) * 8) + rslot[0] = _g_cur_entry_ref + bslot: str* = (str*)(uint64(_g_stk_bld) + sizet(_g_depth) * 8) + bslot[0] = _g_cur_builder + _g_depth += 1 + _g_cur_sp = sp_id + _g_cur_builder = builder + entry_ref: str = _EnsureLocation(pool, sp_id, dline, dcol) + if entry_ref is None: + VLogger.error("--traceback: failed to generate entry DILocation, aborting compilation", "DWARF") + return 0 + _g_cur_entry_ref = entry_ref + mlirlite.builder_set_debug_location(builder, entry_ref) + # Enter this define: tell mlirlite's call-site recorder the current symbol/file/def-line and write an F record (for the .vpcall file + WriteStackMap entry fallback line). + mlirlite.DebugNoteFunc(name, _g_abs_file_path, dline) + return 1 + + +# PopFunction - restore the enclosing function's DI context. +def PopFunction(builder: mlirlite.IRBuilder*) -> int: + global _g_cur_sp, _g_cur_entry_ref, _g_cur_builder, _g_depth + if _g_enabled == 0: return 0 + if _g_depth <= 0: return 0 + _g_depth -= 1 + _g_cur_sp = int(_g_stk_sp[_g_depth]) - 1 + rslot: str* = (str*)(uint64(_g_stk_ref) + sizet(_g_depth) * 8) + _g_cur_entry_ref = rslot[0] + bslot: str* = (str*)(uint64(_g_stk_bld) + sizet(_g_depth) * 8) + _g_cur_builder = (mlirlite.IRBuilder*)(bslot[0]) + if builder is not None: mlirlite.builder_clear_debug_location(builder) + # Leave this define: restore mlirlite's outer function context (for nested defines). + mlirlite.DebugClearFunc() + return 0 + + +# SetStatement - point the current builder at a statement DILocation. +def SetStatement(pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, line: int, col: int) -> int: + """Update the builder's DILocation to the current statement (line precision).""" + if _g_enabled == 0 or pool is None or builder is None: return 0 + # Record the current statement line: C records from build_call in this statement use it as the call-site line. + mlirlite.DebugNoteLine(line) + if _g_cur_sp < 0: return 0 + ref: str = _EnsureLocation(pool, _g_cur_sp, line, col) + if ref is None: return 0 + mlirlite.builder_set_debug_location(builder, ref) + return 1 + + +# _ModuleHasDefine - does the module emit any define (IsDeclared == 0)? +def _ModuleHasDefine(mod: mlirlite.LLVMModule*) -> int: + if mod is None: return 0 + cur: mlirlite.Function* = mod.FuncHead + steps: int = 0 + while cur is not None: + steps += 1 + if steps > 100000: break + if mlirlite.function_is_declared(cur) == 0: return 1 + cur = mlirlite.function_get_next(cur) + return 0 + + +# Finalize - register !llvm.dbg.cu / !llvm.module.flags before dump_ir. +def Finalize(pool: memhub.MemBuddy*) -> int: + """Module-level DI tail; idempotent per module, no-op when no SP was emitted.""" + global _g_finalized + if _g_enabled == 0: return 0 + if _g_finalized != 0: return 0 + mod: mlirlite.LLVMModule* = _g_mod + if mod is None or pool is None: return 0 + _g_finalized = 1 + # Self-check (mirrors TPC Phase2Translator.py:3444): a module that emits a + # define but carries no DI at all loses line info for its frames. + if mod.DIMetaCount == 0 and _ModuleHasDefine(mod) != 0: VLogger.warning("--traceback: this module emits a define but its IR carries no DWARF (!llvm.dbg.cu); its frames will show only addresses in the Ctrl+C stack", "DWARF") + if _g_cu_id < 0: return 0 + cu_ref: str = _RefOf(pool, _g_cu_id) + if cu_ref is None: + VLogger.error("--traceback: failed to allocate DICompileUnit reference buffer, aborting compilation", "DWARF") + return 1 + mlirlite.module_add_named_metadata(mod, pool, "llvm.dbg.cu", cu_ref) + fbuf: str = pool.alloc(64) + if fbuf is None: + VLogger.error("--traceback: failed to allocate module-level debug flags buffer, aborting compilation", "DWARF") + return 1 + viperlib.snprintf(fbuf, 64, "!{i32 2, !\"Debug Info Version\", i32 3}") + flags_id: int = mlirlite.module_add_di_node(mod, pool, fbuf) + if flags_id < 0: + VLogger.error("--traceback: failed to allocate llvm.module.flags metadata node, aborting compilation", "DWARF") + return 1 + fl_ref: str = _RefOf(pool, flags_id) + if fl_ref is None: + VLogger.error("--traceback: failed to allocate llvm.module.flags reference buffer, aborting compilation", "DWARF") + return 1 + mlirlite.module_add_named_metadata(mod, pool, "llvm.module.flags", fl_ref) + return 0 diff --git a/App/lib/core/Gen/Expr/ExprGenAttr.vp b/App/lib/core/Gen/Expr/ExprGenAttr.vp index 1a5120f..a8c3cf6 100644 --- a/App/lib/core/Gen/Expr/ExprGenAttr.vp +++ b/App/lib/core/Gen/Expr/ExprGenAttr.vp @@ -439,7 +439,13 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase): submod_buf: str = pool.alloc(submod_len) if submod_buf is not None: viperlib.snprintf(submod_buf, submod_len, "%s.%s", nm_ma.id, at.attr) - if self.Gen.ImportsGen.is_module_imported(trans._imported_modules, submod_buf) == 1: is_submod = 1 + # Imported package submodule (e.g. `import os` then `os.path.x`): the + # submodule must be recognized even when the current file only imported + # the package. `is_module_imported` alone misses it, so `path` was + # treated as a data global of `os` (`@.path`), emitted a dead + # addressof/load, and the linker reported the symbol as undefined + # (`os/__init__` only aliases the submodule, it never defines it). + if self.Gen.ImportsGen.is_module_imported_or_known(trans._imported_modules, submod_buf) == 1: is_submod = 1 # also check from_imports: the at.attr alias maps to the real module win32.win32file if is_submod == 0 and trans._from_imports is not None: fi_mod_raw: str = self.Gen.ImportsGen.lookup_from_import(trans._from_imports, at.attr, 0) @@ -609,6 +615,12 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase): if obj_ptr is None or obj_ptr.Ty is None: return None + # Aggregate rvalue base (e.g. `make().field` / `a.make().field`): the translated base is a + # struct/array SSA value, not an address, so GEP cannot be applied directly. Materialize it + # into a frame temporary and continue on the address path below. + obj_ptr = self._attr_base_as_ptr(builder, obj_ptr) + if obj_ptr is None or obj_ptr.Ty is None: return None + # if obj_ptr is Ptr(Ptr(Struct)) (the alloca of an X* variable), load to deref and get Ptr(Struct) obj_ptr = self._deref_if_ptr_ptr(builder, obj_ptr) @@ -794,6 +806,62 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase): # explicit return after fatal_error: the final match arm must be terminated, else falling off # the end is misjudged by the function tail as fail-fast return None + # _attr_base_as_ptr - make an attribute base addressable: aggregate rvalues (call results, + # literals, temporaries) are spilled to a frame alloca; pointers pass through unchanged. + def _attr_base_as_ptr(self, builder: mlirlite.IRBuilder*, obj_ptr: mlirlite.Value*) -> mlirlite.Value*: + """Spill a non-pointer aggregate base into a frame temp; return its address.""" + if obj_ptr is None or obj_ptr.Ty is None: return obj_ptr + if self.is_ptr_type(obj_ptr.Ty) != 0: return obj_ptr + bk: int = obj_ptr.Ty.kind() + if bk != mlirlite.LLVMType.Struct and bk != mlirlite.LLVMType.Array: return obj_ptr + tmp: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, obj_ptr.Ty) + if tmp is None: return None + mlirlite.build_store(builder, obj_ptr, tmp) + return tmp + + # _call_ret_class_name - class name of a call's return type (`f().field` chains). + # A normal function call is not a pointer cast: using the callee name (`make_o`) as the class + # makes lookup_field_by_class search for a class named after the function instead of the + # struct the call actually returns. + def _call_ret_class_name(self, pool: memhub.MemBuddy*, trans: HT.Translator*, call_node: ast.AST*) -> str: + """Class name of a call's return struct/class, or None.""" + if pool is None or trans is None or call_node is None: return None + if call_node.kind() != ast.ASTKind.Call: return None + cl_cr: ast.Call* = ast.Call*(call_node) + if cl_cr is None or cl_cr.func is None: return None + fname_cr: str = None + mod_sha1_cr: str = None + if cl_cr.func.kind() == ast.ASTKind.Name: + nm_cr: ast.Name* = ast.Name*(cl_cr.func) + fname_cr = nm_cr.id + mod_sha1_cr = trans.ModuleSha1 + elif cl_cr.func.kind() == ast.ASTKind.Attribute: + at_cr: ast.Attribute* = ast.Attribute*(cl_cr.func) + fname_cr = at_cr.attr + if at_cr.value is not None: + if at_cr.value.kind() == ast.ASTKind.Name: + mn_cr: ast.Name* = ast.Name*(at_cr.value) + if mn_cr.id is not None and self.Gen.ImportsGen.is_module_imported_or_known(trans._imported_modules, mn_cr.id) != 0: + mod_sha1_cr = self.Gen.CallGen._lookup_module_sha1(mn_cr.id) + elif at_cr.value.kind() == ast.ASTKind.Attribute: + mp_cr: str = self._attr_module_path_str(pool, at_cr.value) + if mp_cr is not None and self.Gen.ImportsGen.is_module_imported_or_known(trans._imported_modules, mp_cr) != 0: + mod_sha1_cr = self.Gen.CallGen._lookup_module_sha1(mp_cr) + if fname_cr is None or mod_sha1_cr is None: return None + f_cr: mlirlite.Function* = None + sym_len_cr: sizet = string.strlen(mod_sha1_cr) + string.strlen(fname_cr) + 2 + sym_buf_cr: str = pool.alloc(sym_len_cr) + if sym_buf_cr is not None: + viperlib.snprintf(sym_buf_cr, sym_len_cr, "%s.%s", mod_sha1_cr, fname_cr) + f_cr = self.Gen.CallGen.find_func_in_module(trans.Module, sym_buf_cr) + if f_cr is None: f_cr = self.Gen.CallGen.find_func_in_module(trans.Module, fname_cr) + if f_cr is None: return None + ret_cr: mlirlite.LLVMType* = mlirlite.function_get_ret_ty(f_cr) + if ret_cr is None: return None + # class-instance return (`-> T` is T* at the ABI): take the pointee struct name + if self.is_ptr_type(ret_cr) != 0 and ret_cr.Pointee is not None: ret_cr = ret_cr.Pointee + return self.Gen.StructGen.get_class_name_by_type(pool, ret_cr) + # get_attribute_ptr - get the field pointer for an attribute access (no value load, for assignment); # for a Name obj, use the alloca pointer directly (no struct load). def _resolve_attr_field_class(self, pool: memhub.MemBuddy*, trans: HT.Translator*, attr_node: ast.AST*, allow_module_global: int = 0) -> str: @@ -818,9 +886,12 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase): parent_class = self.Gen.VarGen.var_annot_class_name(var_entry) elif at.value.kind() == ast.ASTKind.Attribute: parent_class = self._resolve_attr_field_class(pool, trans, at.value, allow_module_global) elif at.value.kind() == ast.ASTKind.Call: - # pointer-cast base (the recursive parent of `(T*(x)).f.g`): extract the class name from the - # cast annotation (see the _base_class_from_cast_base header) - parent_class = self._base_class_from_cast_base(pool, trans, at.value) + # Pointer-cast base OR normal call. A cast/constructor names a type (`T*(x)`, `T(...)`), + # but a function call's class must come from its RETURN type: using the callee name + # (`make_o`) makes lookup_field_by_class search for a class named after the function + # (chained `a.make_o().inner.z`). + parent_class = self._call_ret_class_name(pool, trans, at.value) + if parent_class is None: parent_class = self._base_class_from_cast_base(pool, trans, at.value) else: return None if parent_class is None: return None # look up the at.attr field in the parent class and return its annotation class name @@ -1057,6 +1128,11 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase): if obj_ptr is None or obj_ptr.Ty is None: return None + # Aggregate rvalue base (same rule as the read path): spill it to a frame temporary so + # `f().field = v` resolves the field address instead of failing on a non-pointer base. + obj_ptr = self._attr_base_as_ptr(builder, obj_ptr) + if obj_ptr is None or obj_ptr.Ty is None: return None + # if obj_ptr is Ptr(Ptr(Struct)) (the alloca of an X* variable), load to deref and get Ptr(Struct) obj_ptr = self._deref_if_ptr_ptr(builder, obj_ptr) diff --git a/App/lib/core/Gen/Expr/ExprGenBase.vp b/App/lib/core/Gen/Expr/ExprGenBase.vp index 8ca2d0b..1c45d1b 100644 --- a/App/lib/core/Gen/Expr/ExprGenBase.vp +++ b/App/lib/core/Gen/Expr/ExprGenBase.vp @@ -524,7 +524,41 @@ novirtual class ExprGenBase: at: ast.Attribute* = ast.Attribute*(cl.func) callee = at.attr if callee is None: return None - ent: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(callee) + # Module-aware resolution: the ctor dispatch (CallGenMixin) already resolves the struct + # by module SHA1. A bare find_struct here returns the FIRST same-named struct (possibly + # from another module), so this "construction materialization" type would disagree with + # the value the ctor actually produced and `p: T = T()` would fail with a CastError + # whenever the same-named T is defined in another imported module. + ent: GenTypes.StructEntry* = None + if trans is not None: + if cl.func.kind() == ast.ASTKind.Name: + if trans.ModuleSha1 is not None: ent = self.Gen.StructGen.find_struct_by_module(callee, trans.ModuleSha1) + if ent is None and trans._from_imports is not None: + # from X import T (or `as Alias`): resolve through the from-import table so the + # defining module's T wins over a same-named struct from another module. + fi_sha: str = self.Gen.CallGen._lookup_mod_sha1(trans.Pool, trans._from_imports, callee, 1) + if fi_sha is not None: + fi_name: str = callee + fi_orig: str = self.Gen.ImportsGen.lookup_from_import_original(trans._from_imports, callee) + if fi_orig is not None: + fi_len: sizet = 0 + while fi_orig[fi_len] != ' ' and fi_orig[fi_len] != '\0': fi_len += 1 + fi_buf: str = trans.Pool.alloc(fi_len + 1) + if fi_buf is not None: + for fi_i in range(int(fi_len)): fi_buf[fi_i] = fi_orig[fi_i] + fi_buf[fi_len] = '\0' + fi_name = fi_buf + ent = self.Gen.StructGen.find_struct_by_module(fi_name, fi_sha) + else: + at_c: ast.Attribute* = ast.Attribute*(cl.func) + mod_path_c: str = None + if at_c.value is not None: + if at_c.value.kind() == ast.ASTKind.Name: mod_path_c = (ast.Name*(at_c.value)).id + elif at_c.value.kind() == ast.ASTKind.Attribute: mod_path_c = self.Gen.ExprGenAttr._attr_module_path_str(trans.Pool, at_c.value) + if mod_path_c is not None and self.Gen.ImportsGen.is_module_imported_or_known(trans._imported_modules, mod_path_c) != 0: + mod_sha1_c: str = self.Gen.CallGen._lookup_module_sha1(mod_path_c) + if mod_sha1_c is not None: ent = self.Gen.StructGen.find_struct_by_module(callee, mod_sha1_c) + if ent is None: ent = self.Gen.StructGen.find_struct(callee) if ent is not None and ent.Ty is not None: return ent.Ty # REnum variant ctor `EnumType.Variant(...)`: the callee is not a struct # name, but materializes the base enum, so return that aggregate type. diff --git a/App/lib/core/Gen/Func/FuncGenBase.vp b/App/lib/core/Gen/Func/FuncGenBase.vp index 8e6ef7a..fba352e 100644 --- a/App/lib/core/Gen/Func/FuncGenBase.vp +++ b/App/lib/core/Gen/Func/FuncGenBase.vp @@ -20,10 +20,10 @@ define FG_SCOPE_FUNCTION = 1 novirtual class FuncGenBase(CallGenBase.CallGenBase): """Shared helpers for function attributes, naming, mangling and creation.""" - # extract_func_attrs - extract c.Attribute attrs from decorator_list. - # Supports @c.Attribute(t.attr.xxx()/t.attr.xxx/t.attr.llvm.xxx); returns LLVM IR attr string or None. + # extract_func_attrs - extract Attribute attrs from decorator_list. + # Supports @Attribute(attr.xxx()/attr.xxx/attr.llvm.xxx); returns LLVM IR attr string or None. def extract_func_attrs(self, pool: memhub.MemBuddy*, decorator_list: list[ast.AST*]*, imported_modules: str) -> str: - """Extract c.Attribute attrs from decorators; return the LLVM IR attr string.""" + """Extract Attribute attrs from decorators; return the LLVM IR attr string.""" if decorator_list is None: return None dn: sizet = len(decorator_list) if dn == 0: return None diff --git a/App/lib/core/Gen/Func/FuncGenDef.vp b/App/lib/core/Gen/Func/FuncGenDef.vp index e3ac332..007d3ce 100644 --- a/App/lib/core/Gen/Func/FuncGenDef.vp +++ b/App/lib/core/Gen/Func/FuncGenDef.vp @@ -683,6 +683,7 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): if ags.args is not None: alist: list[ast.AST*]* = ags.args an: sizet = len(alist) + p_idx: int = 0 for ai in range(int(an)): arg: ast.Arg* = ast.Arg*(alist.get(ai)) if arg is None or arg.arg is None: continue @@ -698,7 +699,8 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): self.Gen.TypeGenErr.fatal_error(arg, f"forward declaration param '{arg.arg}' type annotation failed to resolve", "NameError") return 0 # Bare class param -> Ptr(struct) (object is a pointer); ArrayT param -> element pointer ty* (C array decay). - # Exception (keep in sync in 3 places): C ABI extern bare struct params pass by value %Struct with a full definition. + # Exception (keep in sync in 3 places): C ABI extern bare struct params use the + # C ABI type: Win64 iN for 1/2/4/8 bytes, otherwise Ptr(struct) (byref copy). match resolved: case mlirlite.LLVMType.Void(): # void is not a legal param type (void is only for void* and typedef of an opaque struct); @@ -708,7 +710,10 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): case mlirlite.LLVMType.Struct(_, _, _): if is_extern_decl != 0 and self.Gen.StructGen.find_struct_by_type(resolved) is not None: self.Gen.StructGen.ensure_struct_def_in_module(pool, mod, resolved) - param_ty = resolved + param_ty = self.Gen.CallGenBase.extern_c_param_agg_ty(pool, resolved) + # Win64 byref: the call site must pass a copy, not the variable address. + if param_ty is not None and param_ty.kind() == mlirlite.LLVMType.Ptr: + self.Gen.CallGenBase.register_extern_byref_param(mangled_name, p_idx) else: param_ty = mlirlite.Ptr(pool, resolved) case mlirlite.LLVMType.Array(aelem_ty, _): param_ty = mlirlite.Ptr(pool, aelem_ty) @@ -717,6 +722,7 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): pname: str = pool.alloc(32) if pname is not None: viperlib.snprintf(pname, 32, "%%%s", arg.arg) mlirlite.add_param(pool, func, param_ty, pname) + p_idx += 1 # *args variadic: set IsVarArg=1 to emit the ... suffix if ags.vararg is not None: func.IsVarArg = 1 @@ -737,6 +743,11 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): fd: ast.FunctionDef* = ast.FunctionDef*(node) if fd is None or fd.name is None: return 0 + # Reject unknown decorators instead of silently dropping them (e.g. the retired + # `@c.Attribute(...)` spelling). Runs before the generic/async early returns so every + # function form is checked. + self.Gen.ClassGenBase.validate_decorators(trans, trans.Module, trans.Pool, fd.name, "function", fd.decorator_list, 0) + # PEP 695 generic function templates are not emitted directly (same rule as forward_declare_functions): # clones are emitted at call sites by _specialize_generic_function; here we only ensure the template is registered. if fd.type_params is not None: @@ -878,6 +889,10 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): if (has_export != 0 or has_state != 0) and trans.ModuleSha1 is not None: self.Gen.CallGenBase.register_cexport_func(trans.ModuleSha1, fd.name) if is_extern_decl != 0: + # C extern returning an aggregate: remember the symbol so call sites emit the C ABI + # (integer register / hidden sret) instead of Viper's aggregate-return convention. + if ret_ty is not None and ret_ty.kind() == mlirlite.LLVMType.Struct: + self.Gen.CallGenBase.register_extern_agg(mangled_name) # extern name(...):runtime=True 在这里也生成 thunk(Phase A 的 extern 往往不走 # forward_declare_functions,两处都挂才覆盖全部路径;另一处靠 find_func_in_module 去重)。 if fd.ffi_lib is not None: @@ -917,6 +932,7 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): if ags_e.args is not None: alist_e: list[ast.AST*]* = ags_e.args an_e: sizet = len(alist_e) + p_idx_e: int = 0 for ai_e in range(int(an_e)): arg_e: ast.Arg* = ast.Arg*(alist_e.get(ai_e)) if arg_e is not None and arg_e.arg is not None: @@ -932,7 +948,8 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): resolved_e = i32_ty # unreachable (fatal_error already exited) if resolved_e is not None: # Bare class param -> Ptr(struct); ArrayT param -> element pointer ty* (C decay). This branch is - # always a C ABI extern declare: bare struct params pass by value %Struct (keep in sync with 2 others). + # always a C ABI extern declare: bare struct params use the C ABI type (Win64 iN for + # 1/2/4/8 bytes, otherwise Ptr(struct) as a byref copy; keep in sync with 2 others). match resolved_e: case mlirlite.LLVMType.Void(): # void is not a legal param type (see the same rule above) @@ -940,7 +957,10 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): case mlirlite.LLVMType.Struct(_, _, _): if self.Gen.StructGen.find_struct_by_type(resolved_e) is not None: self.Gen.StructGen.ensure_struct_def_in_module(pool, mod, resolved_e) - param_ty_e = resolved_e + param_ty_e = self.Gen.CallGenBase.extern_c_param_agg_ty(pool, resolved_e) + # Win64 byref: the call site must pass a copy, not the variable address. + if param_ty_e is not None and param_ty_e.kind() == mlirlite.LLVMType.Ptr: + self.Gen.CallGenBase.register_extern_byref_param(mangled_name, p_idx_e) else: param_ty_e = mlirlite.Ptr(pool, resolved_e) case mlirlite.LLVMType.Array(aelem_ty_e, _): param_ty_e = mlirlite.Ptr(pool, aelem_ty_e) @@ -949,8 +969,9 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): pname_e: str = pool.alloc(32) if pname_e is not None: viperlib.snprintf(pname_e, 32, "%%%s", arg_e.arg) mlirlite.add_param(pool, func, param_ty_e, pname_e) + p_idx_e += 1 - # Extract @c.Attribute decorator attributes and set them on the function + # Extract @Attribute decorator attributes and set them on the function func_attrs_e: str = self.Gen.FuncGenBase.extract_func_attrs(pool, fd.decorator_list, imported_modules) if func_attrs_e is not None: mlirlite.function_set_attrs(func, func_attrs_e) # B5: section string attribute goes through attribute group #N @@ -1010,7 +1031,7 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): VLogger.error(f"create_function {fd.name} failed", "FUNC") return 0 - # Extract @c.Attribute decorator attributes and set them on the function + # Extract @Attribute decorator attributes and set them on the function func_attrs: str = self.Gen.FuncGenBase.extract_func_attrs(pool, fd.decorator_list, imported_modules) if func_attrs is not None: mlirlite.function_set_attrs(func, func_attrs) # B5: section string attribute goes through attribute group #N (LLVM rejects inline section="...") @@ -1074,6 +1095,27 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): # (refined per statement); no-op when DI is disabled. dbg_pushed: int = DebugInfoGen.PushFunction(pool, func_builder, func, fd.lineno, fd.col_offset + 1) + # Constructors may be invoked on a null object when __new__ failed (pool exhausted). + # __before_init__ is null-safe; __init__ must be too, or its first field store is an + # access violation (ASan: LspRange.__init__ in the LSP semantic-token path). + if fd.name is not None and string.strcmp(fd.name, "__init__") == 0: + init_sp: mlirlite.Param* = mlirlite.function_get_param_head(func) + if init_sp is not None: + init_sp_ty: mlirlite.LLVMType* = mlirlite.param_get_ty(init_sp) + init_sp_nm: char* = mlirlite.param_get_name(init_sp) + if init_sp_ty is not None and init_sp_nm is not None and string.strcmp(init_sp_nm, "%self") == 0: + init_self: mlirlite.Value* = mlirlite.SSAValue(pool, init_sp_ty, init_sp_nm) + init_null: mlirlite.Value* = mlirlite.ConstNull(pool, init_sp_ty, "null") + init_null_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "init.selfnull") + init_body_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "init.body") + if init_self is not None and init_null is not None and init_null_bb is not None and init_body_bb is not None: + init_is_null: mlirlite.Value* = mlirlite.build_icmp(func_builder, mlirlite.ICMP_EQ, init_self, init_null) + if init_is_null is not None: + mlirlite.build_cond_br(func_builder, init_is_null, init_null_bb, init_body_bb) + mlirlite.position_at_end(func_builder, init_null_bb) + mlirlite.build_ret_void(func_builder) + mlirlite.position_at_end(func_builder, init_body_bb) + # @t.TLS one-time-init guard (aligned with TPC HandlesFunctions.py): the first call runs the body and sets # __tls_init_done_{name}=1; later calls return the default. Without it every call re-runs TlsAlloc and loses state. if has_tls != 0: diff --git a/App/lib/core/Gen/GenApi.vp b/App/lib/core/Gen/GenApi.vp index 323d31c..b302723 100644 --- a/App/lib/core/Gen/GenApi.vp +++ b/App/lib/core/Gen/GenApi.vp @@ -453,6 +453,46 @@ def get_enum_base_type_compat(enum_name: str) -> mlirlite.LLVMType*: return None +def get_enum_base_type_in_module(enum_name: str, sha1: str) -> mlirlite.LLVMType*: + """按 (名字, 定义模块 SHA1) 取枚举基类型:跨模块同名枚举按模块区分,避免取到先登记的同名项。""" + if _g_gen is not None: return _g_gen.StmtGenDecl.get_enum_base_type_in_module(enum_name, sha1) + return None + + +# ---------------- C-extern aggregate return registry ---------------- +# A C function returning a struct follows the C ABI (Win64: RAX integer <=8B, hidden sret +# pointer otherwise), unlike Viper's own aggregate return. Register the bare symbol when the +# extern declare is created; the call site consults it before emitting a direct call. +def register_extern_agg(sym: str) -> int: + """Delegate to CallGenBase.register_extern_agg (idempotent).""" + if _g_gen is not None: return _g_gen.CallGenBase.register_extern_agg(sym) + return 0 + + +def is_extern_agg(sym: str) -> int: + """Delegate to CallGenBase.is_extern_agg.""" + if _g_gen is not None: return _g_gen.CallGenBase.is_extern_agg(sym) + return 0 + + +def is_win64_c_abi() -> int: + """Delegate to CallGenBase.is_win64_c_abi.""" + if _g_gen is not None: return _g_gen.CallGenBase.is_win64_c_abi() + return 1 + + +def extern_c_param_agg_ty(pool: memhub.MemBuddy*, ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*: + """Delegate to CallGenBase.extern_c_param_agg_ty (C ABI by-value aggregate parameter type).""" + if _g_gen is not None: return _g_gen.CallGenBase.extern_c_param_agg_ty(pool, ty) + return ty + + +def register_extern_byref_param(sym: str, index: int) -> int: + """Delegate to CallGenBase.register_extern_byref_param.""" + if _g_gen is not None: return _g_gen.CallGenBase.register_extern_byref_param(sym, index) + return 0 + + # ---------------- add_visible_struct (delegates to StructGenMixin.add_visible_struct) ---------------- def add_visible_struct(pool: memhub.MemBuddy*, name: str): """Delegate to gen.add_visible_struct""" diff --git a/App/lib/core/Gen/GenTypes.vp b/App/lib/core/Gen/GenTypes.vp index 9f6cf1e..3f4f307 100644 --- a/App/lib/core/Gen/GenTypes.vp +++ b/App/lib/core/Gen/GenTypes.vp @@ -31,6 +31,12 @@ define FIELD_NAME_MAX = 64 define PARENT_MAX = 64 define MRO_MAX = 64 +# ============================================================ +# Enum capacities +# 单个枚举的成员数上限。唯一定义点:ClassGenEnum 的翻译期暂存数组、StmtGenDeclMixin 的 +# EnumEntry.Members 预分配、VpiReader 的 .vpi 预扫描都引用这一个值,禁止再写本地副本。 +define ENUM_MEMBER_MAX = 256 + # ============================================================ # for-iteration sequence kinds (formerly HandlesFor.SEQ_KIND_*) define SEQ_KIND_RANGE = 1 @@ -150,8 +156,8 @@ novirtual class StructEntry: FieldCount: int # Field count Fields: FieldEntry* # Field array (FIELD_MAX slots) IsUnion: int # 1=union / 0=normal struct - IsPacked: int # 1=@c.Attribute(t.attr.packed) packed struct (no padding) - Align: int # @c.Attribute(t.attr.aligned(N)) alignment in bytes (0=default) + IsPacked: int # 1=@Attribute(attr.packed) packed struct (no padding) + Align: int # @Attribute(attr.aligned(N)) alignment in bytes (0=default) IsOOP: int # 1=OOP struct (has methods) / 0=plain memory struct HasInit: int # 1=has __init__ / 0=no HasNew: int # 1=has __new__ / 0=no @@ -224,6 +230,19 @@ define FUNC_RET_SIGN_UNSIGNED = 1 define FUNC_RET_SIGN_SIGNED = 2 +# Snapshot kinds beyond ast.CONST_* (1..6): +# CONST_AGG_DEFAULT - aggregate literal (struct constructor with scalar arguments); the +# encoded field tokens live in FuncDefaultsEntry.ConstStrs[di]. +# CONST_UNKNOWN_DEFAULT - a default that cannot be rebuilt off-module; the call site fails +# fast instead of silently passing a null/uninitialized value. +define CONST_AGG_DEFAULT = 100 +define CONST_UNKNOWN_DEFAULT = 101 +# CONST_EXPR_TEXT - name/qualified-name default (enum member, CDefine); the text lives +# in ConstStrs[di] and is resolved at the call site (the .vpi pre-scan +# runs before enums are registered). +define CONST_EXPR_TEXT = 102 + + # ============================================================ # FuncDefaultsEntry - default-argument entry (formerly HandlesExprCall.FuncDefaultsEntry) novirtual class FuncDefaultsEntry: diff --git a/App/lib/core/Gen/Stmt/StmtGenDeclMixin.vp b/App/lib/core/Gen/Stmt/StmtGenDeclMixin.vp index 96bf781..38437f8 100644 --- a/App/lib/core/Gen/Stmt/StmtGenDeclMixin.vp +++ b/App/lib/core/Gen/Stmt/StmtGenDeclMixin.vp @@ -22,8 +22,8 @@ import lib.core.Gen.GenApi as GenApi # ============================================================ # Constants # 枚举表容量:键是 (定义模块 SHA1, 名字),同名枚举按模块各占一条(不再合并),故留足余量 +# ENUM_MEMBER_MAX(单枚举成员上限)唯一定义在 GenTypes,与 ClassGenEnum 暂存数组和 .vpi 预扫描共用。 define ENUM_MAX = 256 -define ENUM_MEMBER_MAX = 128 # ============================================================ # StmtGenDeclMixin - declaration/enum/nonlocal (mid layer) @@ -123,6 +123,19 @@ novirtual class StmtGenDeclMixin(StmtGenCifMixin.StmtGenCifMixin): if k == ast.ASTKind.AnnAssign: return self.Gen.AssignGen.translate_annassign_prescan(trans, node) if k == ast.ASTKind.If: ifn: ast.If* = ast.If*(node) + # cif/celif: only prescan the compile-time-selected branch, otherwise a pruned + # branch's platform-specific annotations (e.g. win32 types on a Linux target) + # would still be resolved and fail the build. + if self._is_cif_call(ifn.test) == 1: + cif_val_pre: int = self._prune_cif_test(trans.Pool, trans, ifn.test) + if cif_val_pre != 0: + if ifn.children is not None: + bn_pre_ci: sizet = len(ifn.children) + for bi_pre_ci in range(int(bn_pre_ci)): self.pre_scan_allocas(trans, ifn.children.get(bi_pre_ci)) + elif ifn.orelse is not None: + bn_pre_ce: sizet = len(ifn.orelse) + for bi_pre_ce in range(int(bn_pre_ce)): self.pre_scan_allocas(trans, ifn.orelse.get(bi_pre_ce)) + return 0 if ifn.children is not None: bn_pre_i: sizet = len(ifn.children) for bi_pre_i in range(int(bn_pre_i)): self.pre_scan_allocas(trans, ifn.children.get(bi_pre_i)) @@ -364,7 +377,7 @@ novirtual class StmtGenDeclMixin(StmtGenCifMixin.StmtGenCifMixin): def _get_enum_member(self, enum_entry: GenTypes.EnumEntry*, i: int) -> GenTypes.EnumMember*: """Get the i-th enum member entry""" - if enum_entry is None or i < 0 or i >= ENUM_MEMBER_MAX: return None + if enum_entry is None or i < 0 or i >= GenTypes.ENUM_MEMBER_MAX: return None member_size: sizet = GenTypes.EnumMember.__sizeof__() addr: uint64 = uint64(enum_entry.Members) + i * member_size return GenTypes.EnumMember*(addr) @@ -392,9 +405,9 @@ novirtual class StmtGenDeclMixin(StmtGenCifMixin.StmtGenCifMixin): # Allocate the member array (permanent pool: survives mark/reset like EnumTable) member_size: sizet = GenTypes.EnumMember.__sizeof__() - entry.Members = CompilerFactory.factory.PermPool.alloc(member_size * ENUM_MEMBER_MAX) + entry.Members = CompilerFactory.factory.PermPool.alloc(member_size * GenTypes.ENUM_MEMBER_MAX) if entry.Members is None: return None - string.memset(entry.Members, 0, member_size * ENUM_MEMBER_MAX) + string.memset(entry.Members, 0, member_size * GenTypes.ENUM_MEMBER_MAX) # Copy the class name (permanent-pool deep copy) name_len: sizet = string.strlen(name) @@ -435,7 +448,7 @@ novirtual class StmtGenDeclMixin(StmtGenCifMixin.StmtGenCifMixin): if ex_d is not None and ex_d.Name is not None: if string.strcmp(ex_d.Name, member_name) == 0: return ei_d - if enum_entry.MemberCount >= ENUM_MEMBER_MAX: + if enum_entry.MemberCount >= GenTypes.ENUM_MEMBER_MAX: VLogger.error(f"member table full for {enum_entry.Name}", "ENUM") return -1 diff --git a/App/lib/core/Gen/TranslatorGenMixin.vp b/App/lib/core/Gen/TranslatorGenMixin.vp index 8b73dba..b7e1c44 100644 --- a/App/lib/core/Gen/TranslatorGenMixin.vp +++ b/App/lib/core/Gen/TranslatorGenMixin.vp @@ -777,7 +777,7 @@ novirtual class TranslatorGenMixin: # Section name: write the field directly (_print_global adds the quotes for `, section "xxx"`). if gv_attr_section is not None: mlirlite.global_set_section(gv, gv_attr_section) - # B5: apply alignment to global instances of @c.Attribute(t.attr.aligned(N)) classes. + # B5: apply alignment to global instances of @Attribute(attr.aligned(N)) classes. # An explicit variable-level `T | t.attr.aligned(N)` wins over the class-level struct Align. al_ty: mlirlite.LLVMType* = var_ty match var_ty: diff --git a/App/lib/core/IncludesScanner.vp b/App/lib/core/IncludesScanner.vp index ced7271..572aa18 100644 --- a/App/lib/core/IncludesScanner.vp +++ b/App/lib/core/IncludesScanner.vp @@ -1,388 +1,349 @@ -from stdint import * -import memhub -import string -import stdlib -import win32.win32file as win32file -import win32.win32base as win32base -import io -import hashlib -import viperlib -import lib.Projectrans.Utils as Utils -import lib.core.VLogger as VLogger - - -# IncludesScanner - recursive scan of the includes directory (Win32 FindFirstFileA/ -# FindNextFileA); collects .vp paths and SHA1s for Phase1 stub gen / Phase2 merge. - -# global mbuddy pointer -_mbuddy: memhub.MemBuddy* - -# max files per scan -define MAX_FILES = 1024 - - -# FileEntry - file entry (path + SHA1) -novirtual class FileEntry: - """A scanned file entry.""" - Path: str # absolute file path - Sha1: str # first 16 SHA1 chars (16 bytes + null) - RelPath: str # path relative to the includes root - ModuleName: str # module name (e.g. "ast.parser") - IncDir: str # owning includes root dir (multi-root support) - - -# ScanResult - scan result -novirtual class ScanResult: - """A collection of scan results.""" - Entries: FileEntry* # FileEntry array - Count: int - Capacity: int - - -# create_scan_result - create the scan result container -def create_scan_result(pool: memhub.MemBuddy*) -> ScanResult*: - """Create the scan result container with MAX_FILES preallocated slots.""" - if pool is None: return None - size: sizet = MAX_FILES * FileEntry.__sizeof__() - # use stdlib.malloc to avoid exhausting the mbuddy pool (consistent with StubMerger) - entries: FileEntry* = stdlib.malloc(size) - if entries is None: return None - string.memset(entries, 0, size) - - result: ScanResult* = stdlib.malloc(ScanResult.__sizeof__()) - if result is None: return None - string.memset(result, 0, ScanResult.__sizeof__()) - result.Entries = entries - result.Count = 0 - result.Capacity = MAX_FILES - return result - - -# add_file_entry - append a file entry to the scan result -def add_file_entry(result: ScanResult*, pool: memhub.MemBuddy*, abs_path: str, rel_path: str, sha1: str, inc_dir: str) -> int: - """Append a file entry to the scan result; inc_dir is the owning includes root.""" - if result is None or pool is None: return 1 - if result.Count >= result.Capacity: return 1 - - # compute module name: '/' -> '.', strip .vp/.py (Utils handles __init__.vp) - mod_buf: str = Utils.PathToModuleName(rel_path, 0) - if mod_buf is None: return 1 - - # get the entry address - entry_size: sizet = FileEntry.__sizeof__() - entry_addr: uint64 = uint64(result.Entries) + result.Count * entry_size - entry: FileEntry* = (FileEntry*)(entry_addr) - if entry is None: return 1 - - # copy the path string - abs_len: sizet = string.strlen(abs_path) - abs_buf: str = stdlib.malloc(abs_len + 1) - if abs_buf is None: return 1 - string.strcpy(abs_buf, abs_path) - entry.Path = abs_buf - - # copy the SHA1 - sha1_buf: str = stdlib.malloc(17) - if sha1_buf is None: return 1 - string.strcpy(sha1_buf, sha1) - entry.Sha1 = sha1_buf - - # copy the relative path - rel_buf: str = stdlib.malloc(string.strlen(rel_path) + 1) - if rel_buf is None: return 1 - string.strcpy(rel_buf, rel_path) - entry.RelPath = rel_buf - - # copy the module name - entry.ModuleName = mod_buf - - # copy the owning includes root dir (used by Phase2 path joining) - if inc_dir is not None: - inc_len: sizet = string.strlen(inc_dir) - inc_buf: str = stdlib.malloc(inc_len + 1) - if inc_buf is None: return 1 - string.strcpy(inc_buf, inc_dir) - entry.IncDir = inc_buf - - result.Count += 1 - return 0 - - -# compute_file_sha1 - read file content and compute SHA1 (CRLF->LF, matching Projectrans.py) -def compute_file_sha1(pool: memhub.MemBuddy*, file_path: str) -> str: - """Read a file, normalize CRLF to LF, and return the first 16 SHA1 hex chars.""" - if pool is None or file_path is None: return None - - # read file content; BUF_SIZE must match SRC_BUF_SIZE (1MB) or trailing edits won't change SHA1 - BUF_SIZE: sizet = 1048576 - # Refuse larger files: hashing only the first 1 MiB gives two files that differ later the same - # SHA1, and the SHA1 keys the stub/cache files (colliding modules would reuse each other's IR). - sf_sz: io.File* = io.File(file_path, io.MODE.R) - if sf_sz.closed: return None - too_big: int = 0 - if sf_sz.size() >= BUF_SIZE: too_big = 1 - sf_sz.close() - if too_big != 0: - VLogger.error(f"source file exceeds the {BUF_SIZE}-byte limit and cannot be hashed completely: {file_path}", "IncludesScanner") - return None - buf: bytes = Utils.read_file_to_buf(file_path, BUF_SIZE) - if buf is None: return None - - # strip \r in place (CRLF -> LF), matching Projectrans.py text-mode reads - buf_len: sizet = string.strlen(buf) - write_pos: sizet = 0 - for ch in buf: - if ch != '\r': - buf[write_pos] = ch - write_pos += 1 - - # add a null terminator - buf[write_pos] = 0 - - # compute SHA1 - ctx: hashlib.sha1* = hashlib.sha1() - if ctx is None: - stdlib.free(buf) - return None - ctx.update(buf) - - digest: bytes = stdlib.malloc(hashlib.SHA1_DIGEST_LEN) - if digest is None: - stdlib.free(buf) - return None - ctx.final(digest) - - # convert to hex (first 8 bytes = 16 hex chars; stdlib allocation unchanged) - hex_buf: str = Utils.bytes_to_hex(pool, digest, 8, 0, 1) - stdlib.free(buf) - stdlib.free(digest) - return hex_buf - - -# scan_directory_recursive - recursively scan a directory (FindFirstFileA/FindNextFileA) -def scan_directory_recursive(pool: memhub.MemBuddy*, root_dir: str, rel_prefix: str, result: ScanResult*, inc_dir: str) -> int: - """Recursively collect .vp/.py files under a directory; inc_dir is the includes root.""" - if pool is None or root_dir is None or result is None: return 1 - # subdir recursion failure count (not ignored; fail-fast at the summary) - subdir_failed: int = 0 - - # build search pattern: root_dir/* - root_len: sizet = string.strlen(root_dir) - pattern: bytes = stdlib.malloc(root_len + 4) - if pattern is None: return 1 - viperlib.snprintf(pattern, root_len + 4, "%s/*", root_dir) - - # find_data buffer +16 slack: VPC's WIN32_FIND_DATAA.__sizeof__() undercounts Windows writes - fd_size_s: sizet = win32file.WIN32_FIND_DATAA.__sizeof__() - find_data: win32file.WIN32_FIND_DATAA* = stdlib.malloc(fd_size_s + 16) - if find_data is None: return 1 - string.memset(find_data, 0, fd_size_s + 16) - - handle: win32base.HANDLE = win32file.FindFirstFileA(pattern, find_data) - if handle == win32base.INVALID_HANDLE_VALUE: return 1 - - # iterate all files and subdirectories - while True: - # skip . and .. - fname: str = find_data.cFileName - if fname is not None: - fname0: char = fname[0] - if fname0 == '.': - fname1: char = fname[1] - if fname1 == '\0': - # "." - if win32file.FindNextFileA(handle, find_data) == 0: break - continue - elif fname1 == '.': - fname2: char = fname[2] - if fname2 == '\0': - # ".." - if win32file.FindNextFileA(handle, find_data) == 0: break - continue - - # check whether it is a directory - is_dir: int = find_data.dwFileAttributes & win32file.FILE_ATTRIBUTE_DIRECTORY - - # build the full path - fname_len: sizet = string.strlen(fname) - full_path: bytes = Utils.JoinPath(root_dir, fname) - if full_path is None: break - - # build the relative path - prefix_len: sizet = 0 - if rel_prefix is not None: prefix_len = string.strlen(rel_prefix) - rel_path: bytes = None - if rel_prefix is not None and prefix_len > 0: rel_path = Utils.JoinPath(rel_prefix, fname) - else: - rel_path = stdlib.malloc(fname_len + 1) - if rel_path is None: break - string.strcpy(rel_path, fname) - if rel_path is None: break - - if is_dir != 0: - # recurse into the subdirectory - if scan_directory_recursive(pool, full_path, rel_path, result, inc_dir) != 0: subdir_failed += 1 - else: - # check for a .vp source file (Viper uses .vp only; no longer .py) - is_py: int = 0 - if Utils.is_vp_at(fname, 0, fname_len) != 0: is_py = 1 - - if is_py != 0: - # skip files under __pycache__ - is_pycache: int = 0 - if rel_prefix is not None: - if string.strcmp(rel_prefix, "__pycache__") == 0: is_pycache = 1 - - if is_pycache == 0: - # compute SHA1 - sha1: str = compute_file_sha1(pool, full_path) - if sha1 is not None: - # fail-fast: a full table used to drop the file silently, so it never got a - # SHA1 entry / dependency edge and was therefore never compiled. - if add_file_entry(result, pool, full_path, rel_path, sha1, inc_dir) != 0: - VLogger.error(f"includes scan could not register '{full_path}' (the {MAX_FILES}-file capacity or an allocation failed); raise MAX_FILES", "scan") - VLogger.debug(f"scan: {rel_path} -> {sha1}", "scan") - else: VLogger.warning(f"SHA1 computation failed: {full_path}", "scan") - - # continue to the next file - if win32file.FindNextFileA(handle, find_data) == 0: break - - win32file.FindClose(handle) - if subdir_failed > 0: VLogger.error(f"subdirectory scan failed for {subdir_failed} entries", "scan") - - return 0 - - -# scan_includes - scan entry point: return all .vp file paths and SHA1s -def scan_includes(pool: memhub.MemBuddy*, includes_dirs: str) -> ScanResult*: - """Scan space-separated includes dirs and return all scanned .vp/.py files.""" - if pool is None or includes_dirs is None: return None - - result: ScanResult* = create_scan_result(pool) - if result is None: return None - - # parse space-separated includes dirs, recursively scanning and merging each - dirs_total: sizet = string.strlen(includes_dirs) - d_i: sizet = 0 - while d_i < dirs_total: - while d_i < dirs_total and includes_dirs[d_i] == ' ': d_i += 1 - if d_i >= dirs_total: break - d_start: sizet = d_i - while d_i < dirs_total and includes_dirs[d_i] != ' ': d_i += 1 - d_len: sizet = d_i - d_start - if d_len == 0: continue - dir_buf: bytes = stdlib.malloc(d_len + 1) - if dir_buf is None: VLogger.error("failed to allocate the includes directory; aborting scan", "scan") - string.strncpy(dir_buf, includes_dirs + d_start, d_len) - dir_buf[d_len] = '\0' - scan_directory_recursive(pool, dir_buf, None, result, dir_buf) - stdlib.free(dir_buf) - - return result - - -# _PathExistsA - path existence check (GetFileAttributesA) -def _PathExistsA(path: str) -> int: - """Return 1 if the path exists, else 0, using GetFileAttributesA.""" - if path is None: return 0 - attr: ULONG = win32file.GetFileAttributesA(path) - if attr == 0xFFFFFFFF: # INVALID_FILE_ATTRIBUTES - return 0 - return 1 - - -# _TryResolveModuleFile - try one candidate path and write outputs (1=hit, 0=miss); -# out_abs_path/out_rel_path/out_inc_dir are rawptr output slots. -def _TryResolveModuleFile(inc_dir: str, rel_seg: bytes, rel_len: sizet, suffix: str, out_abs_path: rawptr, out_rel_path: rawptr, out_inc_dir: rawptr) -> int: - if inc_dir is None or rel_seg is None or suffix is None: return 0 - inc_len: sizet = string.strlen(inc_dir) - suf_len: sizet = string.strlen(suffix) - # candidate absolute path: {inc}/{rel}{suffix} (hand-built to avoid %. *s) - cand_len: sizet = inc_len + 1 + rel_len + suf_len - cand: bytes = stdlib.malloc(cand_len + 1) - if cand is None: return 0 - string.strcpy(cand, inc_dir) - cand[inc_len] = '/' - string.strncpy(cand + inc_len + 1, rel_seg, rel_len) - string.strcpy(cand + inc_len + 1 + rel_len, suffix) - if _PathExistsA(cand) == 0: - stdlib.free(cand) - return 0 - # path relative to the includes root: {rel}{suffix} - rel_path: bytes = stdlib.malloc(rel_len + suf_len + 1) - if rel_path is None: - stdlib.free(cand) - return 0 - string.strncpy(rel_path, rel_seg, rel_len) - rel_path[rel_len] = '\0' - string.strcat(rel_path, suffix) - # Deep-copy the owning includes root dir; allocate everything before writing - # slots to avoid a half-written state when inc allocation fails. - inc_buf: bytes = None - if out_inc_dir is not None: - inc_buf = stdlib.malloc(inc_len + 1) - if inc_buf is None: - stdlib.free(cand) - stdlib.free(rel_path) - return 0 - string.strcpy(inc_buf, inc_dir) - # Write output slots (stdlib deep copies; caller frees). Slots must be cast to - # (sizet*): rawptr* folds to rawptr and [0] becomes a 1-byte access. - if out_abs_path is not None: - abs_slot: sizet* = (sizet*)(out_abs_path) - abs_slot[0] = sizet(cand) - else: stdlib.free(cand) - if out_rel_path is not None: - rel_slot: sizet* = (sizet*)(out_rel_path) - rel_slot[0] = sizet(rel_path) - else: stdlib.free(rel_path) - if out_inc_dir is not None and inc_buf is not None: - inc_slot: sizet* = (sizet*)(out_inc_dir) - inc_slot[0] = sizet(inc_buf) - return 1 - - -# ResolveModuleToPath - module name -> file path, resolved on demand (no full scan). -# Tries each includes dir, preferring file modules over packages. Returns 0 found, 1 not. -def ResolveModuleToPath(includes_dirs: str, mod_name: str, out_abs_path: rawptr, out_rel_path: rawptr, out_inc_dir: rawptr) -> int: - if includes_dirs is None or mod_name is None: return 1 - mn_len: sizet = string.strlen(mod_name) - if mn_len == 0: return 1 - # mod_name -> relative path segment ('.' -> '/'): ast.parser -> ast/parser - rel_seg: bytes = stdlib.malloc(mn_len + 1) - if rel_seg is None: return 1 - rel_len: sizet = 0 - for mi in range(int(mn_len)): - ch_m: char = mod_name[mi] - if ch_m == '.': rel_seg[rel_len] = '/' - else: rel_seg[rel_len] = ch_m - rel_len += 1 - rel_seg[rel_len] = '\0' - - found: int = 0 - d_total: sizet = string.strlen(includes_dirs) - d_i: sizet = 0 - while d_i < d_total: - while d_i < d_total and includes_dirs[d_i] == ' ': d_i += 1 - if d_i >= d_total: break - d_start: sizet = d_i - while d_i < d_total and includes_dirs[d_i] != ' ': d_i += 1 - d_len: sizet = d_i - d_start - if d_len == 0: continue - inc_dir_buf: bytes = stdlib.malloc(d_len + 1) - if inc_dir_buf is None: continue - string.strncpy(inc_dir_buf, includes_dirs + d_start, d_len) - inc_dir_buf[d_len] = '\0' - # candidate 1: {inc}/{rel}.vp (file module) - if _TryResolveModuleFile(inc_dir_buf, rel_seg, rel_len, ".vp", out_abs_path, out_rel_path, out_inc_dir) != 0: - found = 1 - stdlib.free(inc_dir_buf) - break - # candidate 2: {inc}/{rel}/__init__.vp (package module) - if _TryResolveModuleFile(inc_dir_buf, rel_seg, rel_len, "/__init__.vp", out_abs_path, out_rel_path, out_inc_dir) != 0: - found = 1 - stdlib.free(inc_dir_buf) - if found != 0: break - stdlib.free(rel_seg) - if found != 0: return 0 - return 1 +from stdint import * +import memhub +import string +import stdlib +import os +import io +import hashlib +import viperlib +import lib.Projectrans.Utils as Utils +import lib.core.VLogger as VLogger + + +# IncludesScanner - recursive scan of the includes directory via os.scandir; +# collects .vp paths and SHA1s for Phase1 stub gen / Phase2 merge. + +# global mbuddy pointer +_mbuddy: memhub.MemBuddy* + +# max files per scan +define MAX_FILES = 1024 + + +# FileEntry - file entry (path + SHA1) +novirtual class FileEntry: + """A scanned file entry.""" + Path: str # absolute file path + Sha1: str # first 16 SHA1 chars (16 bytes + null) + RelPath: str # path relative to the includes root + ModuleName: str # module name (e.g. "ast.parser") + IncDir: str # owning includes root dir (multi-root support) + + +# ScanResult - scan result +novirtual class ScanResult: + """A collection of scan results.""" + Entries: FileEntry* # FileEntry array + Count: int + Capacity: int + + +# create_scan_result - create the scan result container +def create_scan_result(pool: memhub.MemBuddy*) -> ScanResult*: + """Create the scan result container with MAX_FILES preallocated slots.""" + if pool is None: return None + size: sizet = MAX_FILES * FileEntry.__sizeof__() + # use stdlib.malloc to avoid exhausting the mbuddy pool (consistent with StubMerger) + entries: FileEntry* = stdlib.malloc(size) + if entries is None: return None + string.memset(entries, 0, size) + + result: ScanResult* = stdlib.malloc(ScanResult.__sizeof__()) + if result is None: return None + string.memset(result, 0, ScanResult.__sizeof__()) + result.Entries = entries + result.Count = 0 + result.Capacity = MAX_FILES + return result + + +# add_file_entry - append a file entry to the scan result +def add_file_entry(result: ScanResult*, pool: memhub.MemBuddy*, abs_path: str, rel_path: str, sha1: str, inc_dir: str) -> int: + """Append a file entry to the scan result; inc_dir is the owning includes root.""" + if result is None or pool is None: return 1 + if result.Count >= result.Capacity: return 1 + + # compute module name: '/' -> '.', strip .vp/.py (Utils handles __init__.vp) + mod_buf: str = Utils.PathToModuleName(rel_path, 0) + if mod_buf is None: return 1 + + # get the entry address + entry_size: sizet = FileEntry.__sizeof__() + entry_addr: uint64 = uint64(result.Entries) + result.Count * entry_size + entry: FileEntry* = (FileEntry*)(entry_addr) + if entry is None: return 1 + + # copy the path string + abs_len: sizet = string.strlen(abs_path) + abs_buf: str = stdlib.malloc(abs_len + 1) + if abs_buf is None: return 1 + string.strcpy(abs_buf, abs_path) + entry.Path = abs_buf + + # copy the SHA1 + sha1_buf: str = stdlib.malloc(17) + if sha1_buf is None: return 1 + string.strcpy(sha1_buf, sha1) + entry.Sha1 = sha1_buf + + # copy the relative path + rel_buf: str = stdlib.malloc(string.strlen(rel_path) + 1) + if rel_buf is None: return 1 + string.strcpy(rel_buf, rel_path) + entry.RelPath = rel_buf + + # copy the module name + entry.ModuleName = mod_buf + + # copy the owning includes root dir (used by Phase2 path joining) + if inc_dir is not None: + inc_len: sizet = string.strlen(inc_dir) + inc_buf: str = stdlib.malloc(inc_len + 1) + if inc_buf is None: return 1 + string.strcpy(inc_buf, inc_dir) + entry.IncDir = inc_buf + + result.Count += 1 + return 0 + + +# compute_file_sha1 - read file content and compute SHA1 (CRLF->LF, matching Projectrans.py) +def compute_file_sha1(pool: memhub.MemBuddy*, file_path: str) -> str: + """Read a file, normalize CRLF to LF, and return the first 16 SHA1 hex chars.""" + if pool is None or file_path is None: return None + + # read file content; BUF_SIZE must match SRC_BUF_SIZE (1MB) or trailing edits won't change SHA1 + BUF_SIZE: sizet = 1048576 + # Refuse larger files: hashing only the first 1 MiB gives two files that differ later the same + # SHA1, and the SHA1 keys the stub/cache files (colliding modules would reuse each other's IR). + sf_sz: io.File* = io.File(file_path, io.MODE.R) + if sf_sz.closed: return None + too_big: int = 0 + if sf_sz.size() >= BUF_SIZE: too_big = 1 + sf_sz.close() + if too_big != 0: + VLogger.error(f"source file exceeds the {BUF_SIZE}-byte limit and cannot be hashed completely: {file_path}", "IncludesScanner") + return None + buf: bytes = Utils.read_file_to_buf(file_path, BUF_SIZE) + if buf is None: return None + + # strip \r in place (CRLF -> LF), matching Projectrans.py text-mode reads + buf_len: sizet = string.strlen(buf) + write_pos: sizet = 0 + for ch in buf: + if ch != '\r': + buf[write_pos] = ch + write_pos += 1 + + # add a null terminator + buf[write_pos] = 0 + + # compute SHA1 + ctx: hashlib.sha1* = hashlib.sha1() + if ctx is None: + stdlib.free(buf) + return None + ctx.update(buf) + + digest: bytes = stdlib.malloc(hashlib.SHA1_DIGEST_LEN) + if digest is None: + stdlib.free(buf) + return None + ctx.final(digest) + + # convert to hex (first 8 bytes = 16 hex chars; stdlib allocation unchanged) + hex_buf: str = Utils.bytes_to_hex(pool, digest, 8, 0, 1) + stdlib.free(buf) + stdlib.free(digest) + return hex_buf + + +# scan_directory_recursive - recursively scan a directory via os.scandir +def scan_directory_recursive(pool: memhub.MemBuddy*, root_dir: str, rel_prefix: str, result: ScanResult*, inc_dir: str) -> int: + """Recursively collect .vp/.py files under a directory; inc_dir is the includes root.""" + if pool is None or root_dir is None or result is None: return 1 + subdir_failed: int = 0 + + reader: os.DirReader* = os.scandir(root_dir) + if reader is None: return 1 + entry: os.DirEntry = os.DirEntry() + + while True: + rr: int = os.readdir(reader, &entry) + if rr == 0: break + if rr < 0: + os.closedir(reader) + return 1 + fname: str = entry.name + fname_len: sizet = string.strlen(fname) + is_dir: int = 1 if entry.is_dir else 0 + full_path: bytes = Utils.JoinPath(root_dir, fname) + if full_path is None: + os.closedir(reader) + return 1 + + prefix_len: sizet = 0 + if rel_prefix is not None: prefix_len = string.strlen(rel_prefix) + rel_path: bytes = None + if rel_prefix is not None and prefix_len > 0: rel_path = Utils.JoinPath(rel_prefix, fname) + else: + rel_path = stdlib.malloc(fname_len + 1) + if rel_path is None: + os.closedir(reader) + return 1 + string.strcpy(rel_path, fname) + if rel_path is None: + os.closedir(reader) + return 1 + + if is_dir != 0: + if scan_directory_recursive(pool, full_path, rel_path, result, inc_dir) != 0: subdir_failed += 1 + else: + is_py: int = 0 + if Utils.is_vp_at(fname, 0, fname_len) != 0: is_py = 1 + if is_py != 0: + is_pycache: int = 0 + if rel_prefix is not None: + if string.strcmp(rel_prefix, "__pycache__") == 0: is_pycache = 1 + if is_pycache == 0: + sha1: str = compute_file_sha1(pool, full_path) + if sha1 is not None: + if add_file_entry(result, pool, full_path, rel_path, sha1, inc_dir) != 0: + VLogger.error(f"includes scan could not register '{full_path}' (the {MAX_FILES}-file capacity or an allocation failed); raise MAX_FILES", "scan") + VLogger.debug(f"scan: {rel_path} -> {sha1}", "scan") + else: VLogger.warning(f"SHA1 computation failed: {full_path}", "scan") + + os.closedir(reader) + if subdir_failed > 0: VLogger.error(f"subdirectory scan failed for {subdir_failed} entries", "scan") + return 0 + + +# scan_includes - scan entry point: return all .vp file paths and SHA1s +def scan_includes(pool: memhub.MemBuddy*, includes_dirs: str) -> ScanResult*: + """Scan space-separated includes dirs and return all scanned .vp/.py files.""" + if pool is None or includes_dirs is None: return None + + result: ScanResult* = create_scan_result(pool) + if result is None: return None + + # parse space-separated includes dirs, recursively scanning and merging each + dirs_total: sizet = string.strlen(includes_dirs) + d_i: sizet = 0 + while d_i < dirs_total: + while d_i < dirs_total and includes_dirs[d_i] == ' ': d_i += 1 + if d_i >= dirs_total: break + d_start: sizet = d_i + while d_i < dirs_total and includes_dirs[d_i] != ' ': d_i += 1 + d_len: sizet = d_i - d_start + if d_len == 0: continue + dir_buf: bytes = stdlib.malloc(d_len + 1) + if dir_buf is None: VLogger.error("failed to allocate the includes directory; aborting scan", "scan") + string.strncpy(dir_buf, includes_dirs + d_start, d_len) + dir_buf[d_len] = '\0' + scan_directory_recursive(pool, dir_buf, None, result, dir_buf) + stdlib.free(dir_buf) + + return result + + +# _PathExistsA - path existence check (os.exists) +def _PathExistsA(path: str) -> int: + """Return 1 if the path exists, else 0.""" + return 1 if os.exists(path) else 0 + + +# _TryResolveModuleFile - try one candidate path and write outputs (1=hit, 0=miss); +# out_abs_path/out_rel_path/out_inc_dir are rawptr output slots. +def _TryResolveModuleFile(inc_dir: str, rel_seg: bytes, rel_len: sizet, suffix: str, out_abs_path: rawptr, out_rel_path: rawptr, out_inc_dir: rawptr) -> int: + if inc_dir is None or rel_seg is None or suffix is None: return 0 + inc_len: sizet = string.strlen(inc_dir) + suf_len: sizet = string.strlen(suffix) + # candidate absolute path: {inc}/{rel}{suffix} (hand-built to avoid %. *s) + cand_len: sizet = inc_len + 1 + rel_len + suf_len + cand: bytes = stdlib.malloc(cand_len + 1) + if cand is None: return 0 + string.strcpy(cand, inc_dir) + cand[inc_len] = '/' + string.strncpy(cand + inc_len + 1, rel_seg, rel_len) + string.strcpy(cand + inc_len + 1 + rel_len, suffix) + if _PathExistsA(cand) == 0: + stdlib.free(cand) + return 0 + # path relative to the includes root: {rel}{suffix} + rel_path: bytes = stdlib.malloc(rel_len + suf_len + 1) + if rel_path is None: + stdlib.free(cand) + return 0 + string.strncpy(rel_path, rel_seg, rel_len) + rel_path[rel_len] = '\0' + string.strcat(rel_path, suffix) + # Deep-copy the owning includes root dir; allocate everything before writing + # slots to avoid a half-written state when inc allocation fails. + inc_buf: bytes = None + if out_inc_dir is not None: + inc_buf = stdlib.malloc(inc_len + 1) + if inc_buf is None: + stdlib.free(cand) + stdlib.free(rel_path) + return 0 + string.strcpy(inc_buf, inc_dir) + # Write output slots (stdlib deep copies; caller frees). Slots must be cast to + # (sizet*): rawptr* folds to rawptr and [0] becomes a 1-byte access. + if out_abs_path is not None: + abs_slot: sizet* = (sizet*)(out_abs_path) + abs_slot[0] = sizet(cand) + else: stdlib.free(cand) + if out_rel_path is not None: + rel_slot: sizet* = (sizet*)(out_rel_path) + rel_slot[0] = sizet(rel_path) + else: stdlib.free(rel_path) + if out_inc_dir is not None and inc_buf is not None: + inc_slot: sizet* = (sizet*)(out_inc_dir) + inc_slot[0] = sizet(inc_buf) + return 1 + + +# ResolveModuleToPath - module name -> file path, resolved on demand (no full scan). +# Tries each includes dir, preferring file modules over packages. Returns 0 found, 1 not. +def ResolveModuleToPath(includes_dirs: str, mod_name: str, out_abs_path: rawptr, out_rel_path: rawptr, out_inc_dir: rawptr) -> int: + if includes_dirs is None or mod_name is None: return 1 + mn_len: sizet = string.strlen(mod_name) + if mn_len == 0: return 1 + # mod_name -> relative path segment ('.' -> '/'): ast.parser -> ast/parser + rel_seg: bytes = stdlib.malloc(mn_len + 1) + if rel_seg is None: return 1 + rel_len: sizet = 0 + for mi in range(int(mn_len)): + ch_m: char = mod_name[mi] + if ch_m == '.': rel_seg[rel_len] = '/' + else: rel_seg[rel_len] = ch_m + rel_len += 1 + rel_seg[rel_len] = '\0' + + found: int = 0 + d_total: sizet = string.strlen(includes_dirs) + d_i: sizet = 0 + while d_i < d_total: + while d_i < d_total and includes_dirs[d_i] == ' ': d_i += 1 + if d_i >= d_total: break + d_start: sizet = d_i + while d_i < d_total and includes_dirs[d_i] != ' ': d_i += 1 + d_len: sizet = d_i - d_start + if d_len == 0: continue + inc_dir_buf: bytes = stdlib.malloc(d_len + 1) + if inc_dir_buf is None: continue + string.strncpy(inc_dir_buf, includes_dirs + d_start, d_len) + inc_dir_buf[d_len] = '\0' + # candidate 1: {inc}/{rel}.vp (file module) + if _TryResolveModuleFile(inc_dir_buf, rel_seg, rel_len, ".vp", out_abs_path, out_rel_path, out_inc_dir) != 0: + found = 1 + stdlib.free(inc_dir_buf) + break + # candidate 2: {inc}/{rel}/__init__.vp (package module) + if _TryResolveModuleFile(inc_dir_buf, rel_seg, rel_len, "/__init__.vp", out_abs_path, out_rel_path, out_inc_dir) != 0: + found = 1 + stdlib.free(inc_dir_buf) + if found != 0: break + stdlib.free(rel_seg) + if found != 0: return 0 + return 1 diff --git a/App/lib/core/Repl.vp b/App/lib/core/Repl.vp index 8040689..eff5c99 100644 --- a/App/lib/core/Repl.vp +++ b/App/lib/core/Repl.vp @@ -1,2109 +1,2097 @@ -from stdint import * -import stdio -import stdlib -import string -import viperlib -import mlirlite -import memhub -import ast -import io -import win32.win32file as win32file -import win32.win32base as win32base -import win32.win32process as win32process -import lib.core.VLogger as VLogger -import lib.core.Gen.GenApi as GenApi -import lib.core.Gen.GenTypes as GenTypes -import lib.core.Gen.Translator as HandlesTranslator -import lib.core.IncludesScanner as IncludesScanner -import lib.Phase.Phase1.Phase1Scan as Phase1Scan -import lib.Phase.BuildPipeline as BuildPipeline -import lib.Projectrans.Config as Config -import lib.Stub.StubMerger.StubMergerStore as StubMergerStore -import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect -import lib.Stub.StubGen.Converter as StubConverter -import lib.Stub.StubGen.VpiStub as VpiStub -import lib.Stub.StubGen.VpiReader as VpiReader - - -# Repl - interactive loop: wraps each fragment as an export def and JIT-compiles it to a DLL. -# Session variables/results are shared via ctx memory; errors recover via setjmp/longjmp. - -# Input line buffer capacity (incl. NUL terminator). -define _REPL_LINE_CAP = 4096 - -# Block input buffer capacity (session-level PermPool, incl. NUL): the raw user lines -# appended while in continuation mode, joined by '\n' with no trailing newline. -define _REPL_BLOCK_CAP = 8192 - -# Measured jmp_buf size on mingw-w64 x86_64: _JBLEN=16 times _JBTYPE (16B) = 256 bytes. -# Confirmed with a C probe (sizeof(jmp_buf) == 256). -define _REPL_JMPBUF_SIZE = 256 - -# setjmp/longjmp on mingw-w64 x86_64: _setjmp takes a frame pointer, so the setjmp -# macro expands to _setjmp(buf, NULL); passing None is fine for non-SEH use. -extern def _setjmp(buf: rawptr, frame: rawptr) -> int -extern def longjmp(buf: rawptr, val: int) - -# PermPool (injected by main): jmp_buf must outlive the session, so mb.reset_to must not reclaim it. -_perm_pool: memhub.MemBuddy* - - -# ============================================================ -# M2/M3 constants: wrapped fragment source capacity covers the single-line, block and lifted paths. -define _REPL_FRAG_SRC_CAP = 26624 -# Lifted module-level def source buffer capacity (incl. NUL). -define _REPL_LIFT_CAP = 12288 -# Max session functions lifted per fragment (<= GenTypes.REPL_FUNC_CAP; excess is an error). -define _REPL_LIFT_MAX = 16 -# Session function name length limit (ReplLiftNames slot width = limit + 1). -define _REPL_FN_NAME_MAX = 64 -# Session function name table capacity (_REPL_LIFT_MAX fixed-width slots). -define _REPL_FN_NAMES_CAP = 1024 -# Diagnostic display buffer (truncates non-NUL-terminated lines/names for %s). -define _REPL_DIAG_CAP = 128 -# Signature rendering buffer capacity ("(int, str) -> int" is well under 96). -define _REPL_SIG_TEXT_CAP = 192 -# Fragment symbol name / probe buffer capacity. -define _REPL_NAME_CAP = 64 -define _REPL_NEEDLE_CAP = 128 -# Temporary file path buffer capacity (directory/source paths). -define _REPL_PATH_CAP = 512 -# Full module IR text capacity (matches App/main.vp IR_BUF_SIZE). -define _REPL_IR_CAP = 262144 -# GetTempPathA output buffer capacity (MAX_PATH). -define _REPL_TMP_CAP = 260 -# External command (clang++ -shared) line capacity. M4 grew it 1024 -> 16384 because the -# link line also carries prior fragments' import libs, else their symbols stay undefined. -define _REPL_CMD_CAP = 16384 -# includes source read buffer (matches Phase1/BuildPipeline SRC_BUF_SIZE). -define _REPL_SRC_BUF_CAP = 1048576 -# .vpi stub buffer (matches BuildPipeline.PYI_BUF_SIZE). -define _REPL_VPI_BUF_CAP = 262144 -# ---- M4: session sha1 / fragment import lib / session baseline (DefineT + TypedefT) ---- -# Max prior fragment import lib slots (a fragment takes one if its IR mentions a session symbol). -define _REPL_FRAG_IMPLIB_MAX = 128 -# Fragment import lib record array capacity (slots x 8-byte pointers). -define _REPL_FRAG_IMPLIB_ARR_CAP = 1024 -# Session baseline (DefineT) entry limit; replayed when each fragment rebuilds its table. -define _REPL_BASE_CDEFINE_MAX = 512 -# Session baseline (TypedefT) entry limit. -define _REPL_BASE_CTYPEDEF_MAX = 512 -# includes class registration (done once at session init): max modules injected via vpi. -define _REPL_INC_CLS_MOD_MAX = 512 -# Convergence passes for includes class registration (field types may reference peers). -define _REPL_INC_CLS_PASSES = 3 -# Struct table watermark guard for includes class registration (STRUCT_MAX = 512). -define _REPL_INC_CLS_STRUCT_GUARD = 448 -# Per-module warning line limit for includes class registration; the rest are only counted. -define _REPL_INC_CLS_WARN_MAX = 3 -# Startup time budget (ms) for includes class registration: multi-pass vpi injection is the -# main session-start cost, so on overrun we stop and let later fragments import on demand. -define _REPL_INC_CLS_BUDGET_MS = 20000 -# Temporary LLVM module name for includes class registration (.vpi injection target). -define _REPL_INC_MOD_NAME = "repl_inc_init" -# ctx session buffer size = header 64 + 64 var slots x 16 + 32 func slots x 16 = 1600. -# Must stay in sync with the GenTypes REPL_* layout constants. -define _REPL_CTX_SIZE = 1600 -# Template project file name (searched in cwd then exe dir when --project is absent). -define _REPL_PROJECT_FILE = "repl.vpj" -# cwd candidate suffix: GetCurrentDirectoryA omits the trailing separator, so add '/'. -define _REPL_PROJECT_SEP_FILE = "/repl.vpj" -# Longest length of the two suffixes plus NUL headroom (candidate buffer check). -define _REPL_PROJECT_PROBE_CAP = 16 - - -# ============================================================ -# ReplFloatView - float view of the ctx header (F at offset 16 reinterpreted as double). -novirtual class ReplFloatView: - """Float view of the ctx header (result read as a double at +16).""" - Magic: int - Version: int - Kind: int - Reserved0: int - F: double - - -# ============================================================ -# ReplFuncSlotView - ctx function-pointer slot (16B: address + reserved), same on both sides. -novirtual class ReplFuncSlotView: - """ctx function-pointer slot (Addr = entry address in the defining fragment DLL).""" - Addr: VOIDPTR - Reserved: VOIDPTR - - -# ============================================================ -# ReplSigOut - parsed session function signature (host reads the fragment's model ABI). -novirtual class ReplSigOut: - """Parsed signature result (registration is rejected when Bad/EHTag is non-zero).""" - RetTag: int # GenTypes.REPL_TY_VOID / INT / FLOAT / STR - RetBits: int - ParamCount: int # excludes the leading ctx param - EHTag: int # 1=define has %__eh_msg_out__/%__eh_code_out__ (raise semantics) - Bad: int # 1=contains an unsupported type token - BadType: str # unsupported type string (points into IR text; for printing only) - - -# ============================================================ -# Template project lookup: tries cwd/repl.vpj then exe-dir/repl.vpj, writing diagnostics. -def ReplFindProject(Out: bytes, Cap: int, Diag: bytes, DiagCap: int) -> int: - """Locate repl.vpj in cwd then the exe directory: 1=found (writes Out), 0=miss (writes Diag).""" - if Out is None or Diag is None: return 0 - if Cap < 64 or DiagCap < 64: return 0 - # Separate buffers per candidate so both paths appear in the diagnostic text. - c1: bytes = stdlib.malloc(Cap) - c2: bytes = stdlib.malloc(Cap) - if c1 is None or c2 is None: - if c1 is not None: stdlib.free(c1) - if c2 is not None: stdlib.free(c2) - return 0 - c1[0] = 0 - c2[0] = 0 - # ① cwd/repl.vpj - n_cwd: ULONG = win32file.GetCurrentDirectoryA(ULONG(Cap), c1) - if n_cwd > 0: - cl: int = int(n_cwd) - # Defensive: GetCurrentDirectoryA writes are undefined on overflow, so force a NUL. - c1[Cap - 1] = 0 - if cl + _REPL_PROJECT_PROBE_CAP <= Cap: - viperlib.snprintf(c1 + cl, Cap - cl, "%s", _REPL_PROJECT_SEP_FILE) - if _is_file(c1) != 0: - string.strcpy(Out, c1) - stdlib.free(c1) - stdlib.free(c2) - return 1 - else: string.strcpy(c1, "(GetCurrentDirectoryA failed)") - # 2) exe-dir/repl.vpj (GetModuleFileNameA(NULL) returns our own full path). - n_exe: ULONG = win32file.GetModuleFileNameA(None, c2, ULONG(Cap)) - if n_exe > 0: - el: int = int(n_exe) - # Defensive: on truncation the return equals Cap and the string may lack a NUL. - c2[Cap - 1] = 0 - if el < Cap: - c2[el] = 0 - # Locate the last separator: c2[0..dlen) is the directory prefix (with separator); - # dlen=0 means no directory part, which the OS resolves against cwd. - dlen: int = 0 - si: int = el - while si > 0: - si -= 1 - ch: char = c2[si] - if ch == '/' or ch == '\\': - dlen = si + 1 - break - if dlen + _REPL_PROJECT_PROBE_CAP <= Cap: - viperlib.snprintf(c2 + dlen, Cap - dlen, "%s", _REPL_PROJECT_FILE) - if _is_file(c2) != 0: - string.strcpy(Out, c2) - stdlib.free(c1) - stdlib.free(c2) - return 1 - else: string.strcpy(c2, "(GetModuleFileNameA failed)") - # Not found in either place: emit diagnostics (needs --project + both searched paths). - viperlib.snprintf(Diag, DiagCap, " Searched(1): %s\n" " Searched(2): %s\n" " --project needed: without repl.vpj the REPL cannot get the project config; " "specify it with --project \n" " Template location: ViperC/repl.vpj in the repo; copy it next to viperc.exe" "or into the current working directory to be found automatically\n", c1, c2) - stdlib.free(c1) - stdlib.free(c2) - return 0 - - -# ============================================================ -# Session state: permanent buffers in PermPool; per-fragment strings in mb; function-pointer slots. -ReplFragFn: callable[[rawptr], int] = None -# Fragment sequence number: module-level and monotonic, so names/files are never reused. -ReplFragNo: int = 1 -# Session temp dir %TEMP%\ViperC_repl\ (keeps user projects clean; pid isolates sessions). -ReplTmpDir: bytes = None -# Fragment symbol name __repl_frag_. -ReplNameBuf: bytes = None -# Wrapped fragment source (for parsing/translation). -ReplSrcBuf: bytes = None -# On-disk path of the wrapped source (real file for error echo). -ReplSrcPath: bytes = None -# Block input buffer (session PermPool, _REPL_BLOCK_CAP): user lines joined by '\n', -# allocated once rather than per line. -ReplBlockBuf: bytes = None -# Current block buffer byte count (0 = empty; NUL is always at ReplBlockBuf[ReplBlockLen]). -ReplBlockLen: int = 0 -# Continuation state: 1 = caching a multi-line block ("... " prompt); 0 = normal (">>> "). -ReplInBlock: int = 0 -# Full module IR text (dump_ir OUTPUT_FULL output buffer). -ReplIrBuf: bytes = None -# IR probe for the export signature "llvm.func @(%__repl_ctx__: !llvm.ptr) -> i32". -ReplNeedleBuf: bytes = None -# ctx session buffer (shared by host and fragment; byte view, passed as the fragment's first arg). -ReplCtxBuf: bytes = None -# Session context object (read by Gen: tables/caps/param name/perm pool/scratch entry). -ReplCtxObj: GenTypes.ReplCtx* = None -# Session variable table (PermPool, ReplCtxObj.Table). -ReplVarTable: GenTypes.ReplVarEntry* = None -# Scratch entry (PermPool): classifies bare-expression results only; no session slot or table entry. -ReplScratch: GenTypes.ReplVarEntry* = None -# Recovery-point jmp_buf (PermPool, lives for the session): run _setjmp's into it and -# VLogger.repl_abort longjmps back from any pipeline depth; re-registered per fragment. -ReplJmpBuf: rawptr = None -# Session table watermark (ReplCtxObj.Count) at fragment start; rolled back on failure to -# drop half-registered entries (repl_ensure_entry Count+1 but repl_seal errored). -ReplSavedVarCount: int = 0 - -# ==== includes session-init results (once per session; for summary and recovery branches) ==== -# Registered includes module count (StubMerger SHA1 table entries). -ReplIncMods: int = 0 -# includes scan time (ms). -ReplIncMs: int = 0 -# Newly generated .vpi stubs. -ReplIncVpiNew: int = 0 -# Reused existing .vpi stubs. -ReplIncVpiReuse: int = 0 -# .vpi generation failures (read/parse/write). -ReplIncVpiFail: int = 0 - -# ==== Host export symbols (cross-module calls) ==== -# Host exe import lib path (.dll.a; PermPool, probed once); fragment link lines use it. -ReplHostImplib: bytes = None - -# ==== M3 session functions (cross-fragment functions) ==== -# Lifted module-level def source (session PermPool; length cleared at each fragment start). -ReplLiftBuf: bytes = None -# Lifted session function name table (PermPool, _REPL_FN_NAME_MAX x _REPL_LIFT_MAX slots). -ReplLiftNames: bytes = None -# Number of session functions lifted by this fragment. -ReplLiftNameCount: int = 0 -# Signature token scratch (PermPool, GenTypes.REPL_FUNC_PARAM_MAX bytes each; reused per function). -ReplSigTags: bytes = None -ReplSigBits: bytes = None -# Signature result struct (PermPool, reused; named to avoid the ReplSigOut class). -ReplSigResult: ReplSigOut* = None -# Session function table watermark (ReplCtxObj.FuncCount) at fragment start; restored on -# failure to drop half-registered entries (same idea as ReplSavedVarCount, see ⓪). -ReplSavedFuncCount: int = 0 -# Diagnostic line/name display buffer (PermPool, _REPL_DIAG_CAP; %s cannot print non-NUL lines). -ReplLineShow: bytes = None -# def-header name scratch (PermPool, _REPL_FN_NAME_MAX). -ReplMsgName: bytes = None -# Signature render buffers x2 (PermPool, _REPL_SIG_TEXT_CAP): new vs existing entry. -ReplSigText: bytes = None -ReplSigText2: bytes = None -# Fragment body (wrapper) write position (_repl_split_body writes, _repl_build_frag_src_multi reads). -ReplWrapPos: int = 0 - -# ==== M4: session module sha1 (the cross-fragment unique key for type names) ==== -# 16-hex-char string (copy of GenTypes.REPL_SESSION_SHA1), used as every fragment's ModuleSha1. -ReplSessionSha1: bytes = None - -# ==== M4: prior fragment import libs (resolution source for cross-fragment method symbols) ==== -# Fragment link lines add prior fragments' import libs so their class/method symbols resolve. -ReplFragImplibs: sizet* = None -ReplFragImplibCount: int = 0 - -# ==== M4: session baseline (DefineT/TypedefT survive across fragments) ==== -# Replayed each fragment after clear_*; only successful fragments append; failures truncate. -ReplBaseCDefineNames: sizet* = None -ReplBaseCDefineValues: sizet* = None -ReplBaseCDefineKinds: sizet* = None # 8 bytes per entry: 0=int / 1=double / 2=str -ReplBaseCDefineCount: int = 0 -ReplBaseCTypedefNames: sizet* = None -ReplBaseCTypedefTypeStrs: sizet* = None -ReplBaseCTypedefCount: int = 0 -# Baseline watermark at fragment start (truncated back to it on failure/rollback). -ReplSavedCDefineCount: int = 0 -ReplSavedCTypedefCount: int = 0 - -# ==== M4: includes class registration (done once at session init) ==== -# Counts of modules whose .vpi injected successfully (classes in the session StructTable) / failed / missing stub. -ReplIncClsMods: int = 0 -ReplIncClsFail: int = 0 -ReplIncClsMissing: int = 0 - - -# ============================================================ -# Private helpers. - -# Whether path is an existing regular file (GetFileAttributesA: attributes only, no buffer). -def _is_file(path: str) -> int: - """1=exists and is not a directory; 0=missing, a directory, or bad args.""" - if path is None: return 0 - attr: ULONG = win32file.GetFileAttributesA(path) - if attr == 0xFFFFFFFF: # INVALID_FILE_ATTRIBUTES - return 0 - if (attr & win32file.FILE_ATTRIBUTE_DIRECTORY) != 0: return 0 - return 1 - - -# ============================================================ -# Host export symbols: locate the host exe import lib so fragments resolve includes symbols. -def ReplFindHostImplib() -> int: - """Locate the host import lib (.dll.a): 1=found (writes ReplHostImplib), 0=not found.""" - global ReplHostImplib - if _perm_pool is None: return 0 - ReplHostImplib = None - exe_buf: bytes = _perm_pool.alloc(_REPL_PATH_CAP) - cand: bytes = _perm_pool.alloc(_REPL_PATH_CAP) - if exe_buf is None or cand is None: return 0 - exe_buf[0] = 0 - cand[0] = 0 - n_exe: ULONG = win32file.GetModuleFileNameA(None, exe_buf, ULONG(_REPL_PATH_CAP)) - if n_exe <= 0: return 0 - el: int = int(n_exe) - # Defensive: on truncation the write may lack a NUL (same as ReplFindProject). - if el >= _REPL_PATH_CAP: el = _REPL_PATH_CAP - 1 - exe_buf[el] = 0 - # Split dir/file: dlen = directory prefix length (incl. separator). - dlen: int = 0 - si: int = el - while si > 0: - si -= 1 - ch: char = exe_buf[si] - if ch == '/' or ch == '\\': - dlen = si + 1 - break - # Candidate 1: exe dir (exe_buf already has the directory prefix). - viperlib.snprintf(cand, _REPL_PATH_CAP, "%s%s.dll.a", exe_buf, exe_buf + dlen) - if _is_file(cand) != 0: - ReplHostImplib = cand - return 1 - # Candidate 2: Config.OutputDir (REPL uses repl.vpj, host uses project.vpj). - if Config.OutputDir is not None: - viperlib.snprintf(cand, _REPL_PATH_CAP, "%s/%s.dll.a", Config.OutputDir, exe_buf + dlen) - if _is_file(cand) != 0: - ReplHostImplib = cand - return 1 - return 0 - - -# Read one line byte-by-byte: works for console (ENABLE_LINE_INPUT) and pipes. -def _read_line(h: win32base.HANDLE, buf: bytes, cap: int) -> int: - """Read one line from h into buf (NUL-terminated); returns bytes read, -1 on EOF/error.""" - if buf is None or cap < 2: return -1 - total: int = 0 - bytes_read: ULONG = 0 - ok: BOOL = 0 - eof: int = 0 - while total < cap - 1: - bytes_read = 0 - ok = win32file.ReadFile(h, buf + total, 1, &bytes_read, None) - if ok == 0 or bytes_read == 0: - eof = 1 - break - # Do not use &(buf + total): its IR does an i32 load (4 bytes) so '\n'/'\r' - # never match; the byte index buf[total] is an i8 load. - ch: char = buf[total] - if ch == 10: # '\n': end of line - break - if ch == 13: # '\r': CRLF, drop and re-read in place - continue - total += 1 - buf[total] = 0 - if eof != 0 and total == 0: return -1 - return total - - -# Count top-level statements (Module.children). -def _count_top_stmts(tree: ast.AST*) -> int: - """Return the number of top-level statements in a Module (0 when children is empty).""" - if tree is None or tree.children is None: return 0 - stmts: list[ast.AST*]* = tree.children - return int(len(stmts)) - - -# ============================================================ -# Block continuation: a line ending in ':' opens a block, ended by a blank line or EOF. - -# Line test: after trimming trailing whitespace it ends with ':', or (M3) starts with '@'. -def _line_starts_block(line: str, n: int) -> int: - """1=line opens a block buffer (ends with ':' / starts with '@'); 0=no.""" - if line is None or n <= 0: return 0 - s: int = 0 - while s < n and (line[s] == ' ' or line[s] == '\t'): s += 1 - if s >= n: return 0 - if line[s] == '@': return 1 - i: int = int(n) - while i > 0: - ch: char = line[i - 1] - if ch != ' ' and ch != '\t': break - i -= 1 - if i <= 0: return 0 - if line[i - 1] == ':': return 1 - return 0 - - -# Append one line to the block buffer: join lines with '\n' and write a trailing NUL. -def _block_append(line: str, n: int) -> int: - """Append an n-byte line to the session block buffer; 0=ok, 1=out of capacity (buffer unchanged).""" - global ReplBlockLen - if ReplBlockBuf is None or line is None or n <= 0: return 1 - sep: int = 0 - if ReplBlockLen > 0: - sep = 1 # separator '\n' between lines - if ReplBlockLen + sep + n >= _REPL_BLOCK_CAP: # plus 1 byte for the trailing NUL - return 1 # capacity checked before writing, so no rollback - if ReplBlockLen > 0: - ReplBlockBuf[ReplBlockLen] = '\n' - ReplBlockLen += 1 - string.memcpy(ReplBlockBuf + ReplBlockLen, line, sizet(n)) - ReplBlockLen += n - ReplBlockBuf[ReplBlockLen] = 0 - return 0 - - -# ctx header view (int64 interpretation): host reads/writes magic/kind/result_a... -def _ctx_head() -> GenTypes.ReplResult*: - """ctx header view; None when the session is not initialized.""" - if ReplCtxBuf is None: return None - return GenTypes.ReplResult*(ReplCtxBuf) - - -# ctx header view (double interpretation of result_a): for echoing float results. -def _ctx_float_head() -> ReplFloatView*: - """Float view of the ctx header; None when the session is not initialized.""" - if ReplCtxBuf is None: return None - return ReplFloatView*(ReplCtxBuf) - - -# Result echo: read the ctx result area by result_kind and print it. -def _repl_echo_result(frag_name: str, res: GenTypes.ReplResult*) -> int: - """Echo this fragment's result by result_kind; 0=handled (including "no result").""" - if frag_name is None or res is None: return 1 - k: int = res.Kind - if k == GenTypes.REPL_RES_NONE: - # Declaration-only fragment (e.g. `x: int`): no result for this line. - return 0 - if k == GenTypes.REPL_RES_INT: - stdio.printf("%s => %lld\n", frag_name, res.A) - return 0 - if k == GenTypes.REPL_RES_FLOAT: - fv: ReplFloatView* = _ctx_float_head() - if fv is None: return 1 - stdio.printf("%s => %f\n", frag_name, fv.F) - return 0 - if k == GenTypes.REPL_RES_STR: - sp: rawptr = rawptr(res.A) - if sp is None: - stdio.printf("%s => \n", frag_name) - return 0 - stdio.printf("%s => %s\n", frag_name, sp) - return 0 - # REPL_RES_OBJ: result_a != 0 means a non-string pointer (print address); result_a == 0 - # means an inline struct value (no address; type name needs the cross-fragment table, M4). - if res.A == 0: - stdio.printf("%s => \n", frag_name) - return 0 - stdio.printf("%s => \n", frag_name, res.A) - return 0 - - -# ============================================================ -# M4: session sha1 / fragment import lib / session baseline / includes class registration. - -# Deep-copy a string into pool (caller supplies the pool; None on invalid input/alloc failure). -def _repl_str_dup(pool: memhub.MemBuddy*, s: str) -> str: - """Deep-copy s into pool; None on failure.""" - if pool is None or s is None: return None - n: int = int(string.strlen(s)) - b: str = pool.alloc(n + 1) - if b is None: return None - string.memcpy(b, s, sizet(n)) - b[n] = 0 - return b - - -# Build the session sha1 string (PermPool; used as ModuleSha1 during fragment translation). -def ReplInitSessionSha1() -> int: - """0=success; non-zero=permanent pool or allocation failure.""" - global ReplSessionSha1 - if _perm_pool is None: return 1 - ReplSessionSha1 = _repl_str_dup(_perm_pool, GenTypes.REPL_SESSION_SHA1) - if ReplSessionSha1 is None: return 1 - return 0 - - -# ---- Session baseline (DefineT/TypedefT): find / restore / append / truncate ---- - -def _repl_base_cdefine_find(name: str) -> int: - """Find a baseline DefineT entry index by name; -1 when not found.""" - if name is None or ReplBaseCDefineNames is None: return -1 - i: int - for i in range(int(ReplBaseCDefineCount)): - nm: str = rawptr(ReplBaseCDefineNames[i]) - if nm is not None and string.strcmp(nm, name) == 0: return i - return -1 - - -def ReplBaselineRestore(pool: memhub.MemBuddy*) -> int: - """Restore the session baseline (DefineT + TypedefT) into the just-cleared tables; 0=success.""" - if pool is None or ReplBaseCDefineNames is None: return 1 - if GenApi._g_gen is None: return 1 - i: int - for i in range(int(ReplBaseCDefineCount)): - nm: str = rawptr(ReplBaseCDefineNames[i]) - val: str = rawptr(ReplBaseCDefineValues[i]) - kind: int = int(ReplBaseCDefineKinds[i]) - if nm is None or val is None: continue - if kind == 0: GenApi.register_cdefine_constant(pool, nm, int64(string.atoll(val))) - elif kind == 1: GenApi.register_cdefine_double(pool, nm, string.atof(val)) - else: - # String DefineT: GenApi has no wrapper, so call gen directly (same shape as - # the clear_cdefine_constants wrapper). - GenApi._g_gen.TypeGenCDef.register_cdefine_string(pool, nm, val) - for i in range(int(ReplBaseCTypedefCount)): - tn: str = rawptr(ReplBaseCTypedefNames[i]) - tv: str = rawptr(ReplBaseCTypedefTypeStrs[i]) - if tn is None or tv is None: continue - GenApi.register_ctypedef(pool, tn, tv) - return 0 - - -def ReplBaselineCapture() -> int: - """Append baseline-missing DefineT/TypedefT entries from the current tables into the session baseline; 0=success.""" - global ReplBaseCDefineCount, ReplBaseCTypedefCount - if _perm_pool is None or GenApi._g_gen is None: return 1 - cdef_names: list[str]* = GenApi._g_gen.TypeGenCDef.CDefineNames - if cdef_names is not None: - cdef_vals: list[str]* = GenApi._g_gen.TypeGenCDef.CDefineValues - cdef_dbl: list[int]* = GenApi._g_gen.TypeGenCDef.CDefineDoubleFlags - cdef_str: list[int]* = GenApi._g_gen.TypeGenCDef.CDefineStrFlags - n: sizet = len(cdef_names) - i: sizet - for i in range(int(n)): - nm: str = cdef_names.get(i) - if nm is None: continue - if _repl_base_cdefine_find(nm) >= 0: continue - if ReplBaseCDefineCount >= _REPL_BASE_CDEFINE_MAX: break - val: str = None - if cdef_vals is not None: val = cdef_vals.get(i) - if val is None: continue - kind: int = 0 - if cdef_str is not None and int(cdef_str.get(i)) == 1: kind = 2 - # 浮点条目:1=f64 / 2=f32(见 TypeGenCDef 的 CDEFINE_FLOAT_KIND_*),非 0 即浮点 - elif cdef_dbl is not None and int(cdef_dbl.get(i)) != 0: kind = 1 - nc: str = _repl_str_dup(_perm_pool, nm) - vc: str = _repl_str_dup(_perm_pool, val) - if nc is None or vc is None: continue - j: int = ReplBaseCDefineCount - ReplBaseCDefineNames[j] = sizet(nc) - ReplBaseCDefineValues[j] = sizet(vc) - ReplBaseCDefineKinds[j] = sizet(kind) - ReplBaseCDefineCount = j + 1 - ct_names: list[str]* = GenApi._g_gen.TypeGenCType.CTypedefNames - if ct_names is not None: - ct_vals: list[str]* = GenApi._g_gen.TypeGenCType.CTypedefTypeStrs - cn2: sizet = len(ct_names) - i2: sizet - for i2 in range(int(cn2)): - tn: str = ct_names.get(i2) - if tn is None: continue - if ReplBaseCTypedefCount >= _REPL_BASE_CTYPEDEF_MAX: break - dup_hit: int = 0 - k: int - for k in range(int(ReplBaseCTypedefCount)): - ex: str = rawptr(ReplBaseCTypedefNames[k]) - if ex is not None and string.strcmp(ex, tn) == 0: - dup_hit = 1 - break - if dup_hit != 0: continue - tv: str = None - if ct_vals is not None: tv = ct_vals.get(i2) - if tv is None: continue - tnc: str = _repl_str_dup(_perm_pool, tn) - tvc: str = _repl_str_dup(_perm_pool, tv) - if tnc is None or tvc is None: continue - j2: int = ReplBaseCTypedefCount - ReplBaseCTypedefNames[j2] = sizet(tnc) - ReplBaseCTypedefTypeStrs[j2] = sizet(tvc) - ReplBaseCTypedefCount = j2 + 1 - return 0 - - -def ReplBaselineTruncate(cd_cnt: int, ct_cnt: int) -> None: - """Truncate the baseline watermarks back to the fragment-start values (failed deltas must not enter).""" - global ReplBaseCDefineCount, ReplBaseCTypedefCount - if cd_cnt >= 0 and cd_cnt <= ReplBaseCDefineCount: ReplBaseCDefineCount = cd_cnt - if ct_cnt >= 0 and ct_cnt <= ReplBaseCTypedefCount: ReplBaseCTypedefCount = ct_cnt - - -# ---- Fragment import lib: record / append to the link command line ---- - -def ReplFragImplibRecord(implib_path: str) -> int: - """Record a fragment import lib (PermPool deep copy); 1=recorded, 0=not (limit/allocation failure).""" - global ReplFragImplibCount - if implib_path is None or ReplFragImplibs is None or _perm_pool is None: return 0 - if ReplFragImplibCount >= _REPL_FRAG_IMPLIB_MAX: return 0 - dup: str = _repl_str_dup(_perm_pool, implib_path) - if dup is None: return 0 - ReplFragImplibs[ReplFragImplibCount] = sizet(dup) - ReplFragImplibCount += 1 - return 1 - - -# Append all recorded fragment import libs to the command buffer (space-separated); -# returns the new position (-1 = out of capacity, which the caller must treat as an error). -def _repl_cmd_append_implibs(cmd: bytes, cap: int, pos: int) -> int: - if cmd is None or ReplFragImplibs is None: return pos - i: int - for i in range(int(ReplFragImplibCount)): - p: str = rawptr(ReplFragImplibs[i]) - if p is None: continue - pos = _repl_cat_str(cmd, cap, int(pos), " ") - if pos < 0: return -1 - pos = _repl_cat_str(cmd, cap, int(pos), p) - if pos < 0: return -1 - return pos - - -# ---- includes class registration (once per session; per-module isolation, warnings only) ---- - -def ReplInitIncludesClasses(mb: memhub.MemBuddy*, sr: IncludesScanner.ScanResult*, vpi_dir: str) -> int: - """Inject each includes module's .vpi into a temp module so its classes enter the session StructTable; 0=done.""" - global ReplIncClsMods, ReplIncClsFail, ReplIncClsMissing - if mb is None or sr is None or vpi_dir is None or sr.Entries is None: return 1 - if ReplJmpBuf is None or _perm_pool is None: return 1 - n_mod: int = int(sr.Count) - if n_mod <= 0: return 1 - if n_mod > _REPL_INC_CLS_MOD_MAX: n_mod = _REPL_INC_CLS_MOD_MAX - # Per-module state (8 bytes/slot, PermPool): 0 = pending/retry, 1 = done (ok or unusable). - st: sizet* = _perm_pool.alloc(_REPL_INC_CLS_MOD_MAX * 8) - if st is None: return 1 - string.memset(st, 0, _REPL_INC_CLS_MOD_MAX * 8) - # Injection target: temporary LLVM module + translator (annotation context only); - # struct registration uses permanent allocation so entries/fields/types outlive fragments. - GenApi.set_stdlib_alloc(1) - GenApi.reset_visible_structs(mb, 0) - GenApi.set_temp_dir(vpi_dir) - GenApi.SetTempDir(vpi_dir) - GenApi.set_current_file_type(vpi_dir) - tmp_mod: mlirlite.LLVMModule* = mlirlite.new_module(_perm_pool, _REPL_INC_MOD_NAME) - tr_inc: HandlesTranslator.Translator* = _perm_pool.alloc(HandlesTranslator.Translator.__sizeof__()) - if tmp_mod is None or tr_inc is None: return 1 - tr_inc.__before_init__() - tr_inc.__init__() - # Give the temp translator real state (func/symbol tables, name buffers, Pool): VpiReader - # reads trans._funcs and trans.Pool/imported_modules while injecting, else it derefs None. - tr_inc._init_state(_perm_pool) - tr_inc.ModuleSha1 = ReplSessionSha1 - tr_inc._declare_only = 0 - # ReplCtx marks the translator as in a REPL session: annotations from .vpi may reference - # unregistered classes, so TypeGenResolve builds opaque placeholders instead of failing. - tr_inc.ReplCtx = ReplCtxObj - es: sizet = IncludesScanner.FileEntry.__sizeof__() - dl: sizet = string.strlen(vpi_dir) - warn_left: int = _REPL_INC_CLS_WARN_MAX - warn_dropped: int = 0 - budget_t0: uint64 = win32base.GetTickCount64() - budget_hit: int = 0 - p_i: int - for p_i in range(int(_REPL_INC_CLS_PASSES)): - pass_ok: int = 0 - i: int - for i in range(int(n_mod)): - if int(st[i]) != 0: continue - # Startup budget (one time check per module; GetTickCount64 is cheap): on overrun - # stop, leaving the remaining modules to the on-demand import path. - if int(win32base.GetTickCount64() - budget_t0) > _REPL_INC_CLS_BUDGET_MS: - budget_hit = 1 - break - if GenApi.get_struct_count() >= _REPL_INC_CLS_STRUCT_GUARD: - stdio.printf("includes class registration: struct table reached %d entries (near the limit); " "stopping registration; unregistered classes are unavailable in the session (importing their module " "can still register on demand)\n", GenApi.get_struct_count()) - stdio.fflush(None) - return 0 - ent: IncludesScanner.FileEntry* = IncludesScanner.FileEntry*(uint64(sr.Entries) + uint64(i) * uint64(es)) - if ent is None or ent.Sha1 is None: - st[i] = sizet(1) - ReplIncClsFail += 1 - continue - vm: sizet = mb.mark() - vpi_path: str = StubMergerCollect._sliced_path(vpi_dir, dl, ent.Sha1, "vpi") - if vpi_path is None: - st[i] = sizet(1) - ReplIncClsFail += 1 - mb.reset_to(vm) - continue - if _is_file(vpi_path) == 0: - # Missing stub (host never built / includes dir lacks this module): its classes - # are unavailable, but the session continues (import degrades the same way). - ReplIncClsMissing += 1 - if warn_left > 0: - stdio.printf("includes class registration: module %s has no .vpi stub -- its classes are " "unavailable in the session (other modules unaffected; build the host once with project.vpj " "to generate all stubs)\n", ent.Sha1) - warn_left -= 1 - else: warn_dropped += 1 - stdlib.free(vpi_path) - mb.reset_to(vm) - st[i] = sizet(1) - continue - # Set trans.ModuleSha1 to the current stub's source module sha1 (like batch), so bare - # class names and parent lookups resolve correctly during injection. - tr_inc.ModuleSha1 = ent.Sha1 - # Per-module recovery point: a fatal only discards this module's injection; the struct - # table dedups by name+sha1, and only mb.reset_to(vm) is done on the fatal path. - rc_m: int = _setjmp(ReplJmpBuf, None) - if rc_m != 0: - ReplIncClsFail += 1 - if warn_left > 0: - stdio.printf("includes class registration: injection of module %d's .vpi interrupted " "(its classes may be incomplete; session continues)\n", i) - warn_left -= 1 - else: warn_dropped += 1 - mb.reset_to(vm) - continue - ok_m: int = VpiReader.ParseVpiIntoModule(_perm_pool, tmp_mod, vpi_path, tr_inc) - if ok_m != 0: - st[i] = sizet(1) - ReplIncClsMods += 1 - pass_ok += 1 - else: - ReplIncClsFail += 1 - if warn_left > 0: - stdio.printf("includes class registration: injection of module %s's .vpi failed " "(that module's classes are incomplete/unavailable; session continues)\n", ent.Sha1) - warn_left -= 1 - else: warn_dropped += 1 - stdlib.free(vpi_path) - mb.reset_to(vm) - # No module succeeded this pass, so further passes add nothing; stop early. - if pass_ok == 0: break - # Summary (aggregated warnings): per-module warnings were capped, so print full counts. - skip_cnt: int = 0 - si: int - for si in range(int(n_mod)): - if int(st[si]) == 0: skip_cnt += 1 - stdio.printf("includes classes: registered vpi classes for %d modules (failed %d, missing stub %d, " "not attempted/to retry %d, warnings dropped %d)\n", ReplIncClsMods, ReplIncClsFail, ReplIncClsMissing, skip_cnt, warn_dropped) - if budget_hit != 0: - stdio.printf("includes classes: startup budget of %d ms exhausted, stopped startup registration (classes of the remaining %d " "modules are unavailable in the session; a fragment that `import`s such a module still registers on demand)\n", _REPL_INC_CLS_BUDGET_MS, skip_cnt) - stdio.fflush(None) - return 0 - - -# ============================================================ -# Session init: create %TEMP%\ViperC_repl\\ and allocate permanent buffers + ctx. -def ReplInitSession() -> int: - """Create the temp dir plus PermPool resident buffers and ctx session memory; 0=success.""" - global ReplTmpDir, ReplNameBuf, ReplSrcBuf, ReplSrcPath, ReplIrBuf, ReplNeedleBuf - global ReplCtxBuf, ReplCtxObj, ReplVarTable, ReplScratch - global ReplBlockBuf, ReplBlockLen, ReplInBlock - global ReplLiftBuf, ReplLiftNames, ReplLiftNameCount, ReplSigTags, ReplSigBits - global ReplSigResult, ReplLineShow, ReplMsgName - global ReplSigText, ReplSigText2, ReplWrapPos, ReplSavedFuncCount - global ReplSessionSha1, ReplFragImplibs, ReplFragImplibCount - global ReplBaseCDefineNames, ReplBaseCDefineValues, ReplBaseCDefineKinds - global ReplBaseCDefineCount, ReplBaseCTypedefNames, ReplBaseCTypedefTypeStrs - global ReplBaseCTypedefCount, ReplSavedCDefineCount, ReplSavedCTypedefCount - global ReplIncClsMods, ReplIncClsFail, ReplIncClsMissing - if _perm_pool is None: return 1 - # 1) %TEMP% prefix (GetTempPathA returns a trailing backslash; 0 means failure). - tmp_buf: bytes = _perm_pool.alloc(_REPL_TMP_CAP) - if tmp_buf is None: return 1 - tmp_len: ULONG = win32file.GetTempPathA(_REPL_TMP_CAP, tmp_buf) - if tmp_len == 0: return 1 - if int(tmp_len) >= _REPL_TMP_CAP: return 1 - # 2) Session dir ViperC_repl/ (pid isolates sessions; paths use '/'). - ReplTmpDir = _perm_pool.alloc(_REPL_PATH_CAP) - if ReplTmpDir is None: return 1 - viperlib.snprintf(ReplTmpDir, _REPL_PATH_CAP, "%sViperC_repl/%u", tmp_buf, win32process.GetCurrentProcessId()) - if BuildPipeline.ensure_dir(ReplTmpDir) != 0: return 1 - # 3) Permanent buffers (live for the session, not re-allocated per fragment). - ReplNameBuf = _perm_pool.alloc(_REPL_NAME_CAP) - ReplSrcBuf = _perm_pool.alloc(_REPL_FRAG_SRC_CAP) - ReplSrcPath = _perm_pool.alloc(_REPL_PATH_CAP) - ReplIrBuf = _perm_pool.alloc(_REPL_IR_CAP) - ReplNeedleBuf = _perm_pool.alloc(_REPL_NEEDLE_CAP) - ReplBlockBuf = _perm_pool.alloc(_REPL_BLOCK_CAP) - ReplBlockLen = 0 - ReplInBlock = 0 - if ReplNameBuf is None or ReplSrcBuf is None or ReplSrcPath is None: return 1 - if ReplIrBuf is None or ReplNeedleBuf is None: return 1 - if ReplBlockBuf is None: return 1 - ReplBlockBuf[0] = 0 - # 3') M3 permanent buffers: lifted source / name table / sig token scratch / render buffers. - ReplLiftBuf = _perm_pool.alloc(_REPL_LIFT_CAP) - ReplLiftNames = _perm_pool.alloc(_REPL_FN_NAMES_CAP) - ReplSigTags = _perm_pool.alloc(GenTypes.REPL_FUNC_PARAM_MAX) - ReplSigBits = _perm_pool.alloc(GenTypes.REPL_FUNC_PARAM_MAX) - ReplSigResult = _perm_pool.alloc(ReplSigOut.__sizeof__()) - ReplLineShow = _perm_pool.alloc(_REPL_DIAG_CAP) - ReplMsgName = _perm_pool.alloc(_REPL_FN_NAME_MAX) - ReplSigText = _perm_pool.alloc(_REPL_SIG_TEXT_CAP) - ReplSigText2 = _perm_pool.alloc(_REPL_SIG_TEXT_CAP) - if ReplLiftBuf is None or ReplLiftNames is None or ReplSigTags is None or ReplSigBits is None or ReplSigResult is None: return 1 - if ReplLineShow is None or ReplMsgName is None or ReplSigText is None or ReplSigText2 is None: return 1 - ReplLiftBuf[0] = 0 - ReplLiftNames[0] = 0 - ReplLiftNameCount = 0 - ReplWrapPos = 0 - ReplSavedFuncCount = 0 - # 4) ctx session memory (fragment's first arg): 64B header + 64x16B var slots + 32x16B func slots, zeroed. - ReplCtxBuf = _perm_pool.alloc(_REPL_CTX_SIZE) - if ReplCtxBuf is None: return 1 - string.memset(ReplCtxBuf, 0, _REPL_CTX_SIZE) - # 5) Session variable table + scratch entry (PermPool; names deep-copied by Gen into the same pool). - entry_size: sizet = GenTypes.ReplVarEntry.__sizeof__() - ReplVarTable = _perm_pool.alloc(entry_size * GenTypes.REPL_VAR_CAP) - if ReplVarTable is None: return 1 - string.memset(ReplVarTable, 0, entry_size * GenTypes.REPL_VAR_CAP) - ReplScratch = _perm_pool.alloc(entry_size) - if ReplScratch is None: return 1 - string.memset(ReplScratch, 0, entry_size) - # Scratch entry name (readable in error messages: ""). - repl_scratch_name: str = _perm_pool.alloc(8) - if repl_scratch_name is None: return 1 - viperlib.snprintf(repl_scratch_name, 8, "%s", "") - ReplScratch.Name = repl_scratch_name - ReplScratch.TyName = None - ReplScratch.Offset = 0 - ReplScratch.Kind = GenTypes.REPL_KIND_UNTYPED - ReplScratch.TyTag = 0 - ReplScratch.Bits = 0 - # 5') Session function table (M3; PermPool, host writes, Gen reads only) - entry array zeroed. - func_entry_size: sizet = GenTypes.ReplFuncEntry.__sizeof__() - ReplFuncTable: GenTypes.ReplFuncEntry* = _perm_pool.alloc(func_entry_size * GenTypes.REPL_FUNC_CAP) - if ReplFuncTable is None: return 1 - string.memset(ReplFuncTable, 0, func_entry_size * GenTypes.REPL_FUNC_CAP) - # 6) Session context object (the Gen-side switch: trans.ReplCtx). - ReplCtxObj = _perm_pool.alloc(GenTypes.ReplCtx.__sizeof__()) - if ReplCtxObj is None: return 1 - string.memset(ReplCtxObj, 0, GenTypes.ReplCtx.__sizeof__()) - ctx_param: str = _perm_pool.alloc(_REPL_NAME_CAP) - if ctx_param is None: return 1 - viperlib.snprintf(ctx_param, _REPL_NAME_CAP, "%s", GenTypes.REPL_CTX_PARAM) - ReplCtxObj.Marker = GenTypes.REPL_CTX_MARKER - ReplCtxObj.Table = ReplVarTable - ReplCtxObj.Count = 0 - ReplCtxObj.Cap = GenTypes.REPL_VAR_CAP - ReplCtxObj.CtxParam = ctx_param - ReplCtxObj.SlotSize = GenTypes.REPL_SLOT_SIZE - ReplCtxObj.Scratch = ReplScratch - ReplCtxObj.Perm = _perm_pool - # Session function table (M3): slices survive across fragments, so table/names/tokens are in PermPool. - ReplCtxObj.FuncTable = ReplFuncTable - ReplCtxObj.FuncCount = 0 - ReplCtxObj.FuncCap = GenTypes.REPL_FUNC_CAP - ReplSavedFuncCount = 0 - # 7) ctx fixed header: magic/version written once by the host (fragments read-only, no validation). - head: GenTypes.ReplResult* = _ctx_head() - if head is None: return 1 - head.Magic = GenTypes.REPL_CTX_MAGIC - head.Version = GenTypes.REPL_CTX_VERSION - head.Kind = GenTypes.REPL_RES_NONE - head.Reserved0 = 0 - head.A = 0 - head.B = 0 - head.VarCount = 0 - head.LastFrag = 0 - # 8) M4 permanent state: session sha1 + fragment import lib table + session baseline (DefineT/TypedefT). - if ReplInitSessionSha1() != 0: return 1 - ReplFragImplibs = _perm_pool.alloc(_REPL_FRAG_IMPLIB_ARR_CAP) - ReplBaseCDefineNames = _perm_pool.alloc(8 * _REPL_BASE_CDEFINE_MAX) - ReplBaseCDefineValues = _perm_pool.alloc(8 * _REPL_BASE_CDEFINE_MAX) - ReplBaseCDefineKinds = _perm_pool.alloc(8 * _REPL_BASE_CDEFINE_MAX) - ReplBaseCTypedefNames = _perm_pool.alloc(8 * _REPL_BASE_CTYPEDEF_MAX) - ReplBaseCTypedefTypeStrs = _perm_pool.alloc(8 * _REPL_BASE_CTYPEDEF_MAX) - if ReplFragImplibs is None or ReplBaseCDefineNames is None or ReplBaseCDefineValues is None or ReplBaseCDefineKinds is None: return 1 - if ReplBaseCTypedefNames is None or ReplBaseCTypedefTypeStrs is None: return 1 - string.memset(ReplFragImplibs, 0, _REPL_FRAG_IMPLIB_ARR_CAP) - string.memset(ReplBaseCDefineNames, 0, 8 * _REPL_BASE_CDEFINE_MAX) - string.memset(ReplBaseCDefineValues, 0, 8 * _REPL_BASE_CDEFINE_MAX) - string.memset(ReplBaseCDefineKinds, 0, 8 * _REPL_BASE_CDEFINE_MAX) - string.memset(ReplBaseCTypedefNames, 0, 8 * _REPL_BASE_CTYPEDEF_MAX) - string.memset(ReplBaseCTypedefTypeStrs, 0, 8 * _REPL_BASE_CTYPEDEF_MAX) - ReplFragImplibCount = 0 - ReplBaseCDefineCount = 0 - ReplBaseCTypedefCount = 0 - ReplSavedCDefineCount = 0 - ReplSavedCTypedefCount = 0 - ReplIncClsMods = 0 - ReplIncClsFail = 0 - ReplIncClsMissing = 0 - return 0 - - -# ============================================================ -# includes session init: scan dir -> register "module -> SHA1" -> pre-generate .vpi stubs. -def _repl_gen_includes_vpi(mb: memhub.MemBuddy*, sr: IncludesScanner.ScanResult*, vpi_dir: str) -> int: - """Pre-generate .vpi stubs for the includes scan; returns the count of new stubs (-1=bad args).""" - global ReplIncVpiNew, ReplIncVpiReuse, ReplIncVpiFail - if mb is None or sr is None or vpi_dir is None: return -1 - if sr.Entries is None: return -1 - es: sizet = IncludesScanner.FileEntry.__sizeof__() - dl: sizet = string.strlen(vpi_dir) - n: int - for n in range(int(sr.Count)): - ent: IncludesScanner.FileEntry* = IncludesScanner.FileEntry*(uint64(sr.Entries) + uint64(n) * es) - if ent is None or ent.Sha1 is None or ent.Path is None: - ReplIncVpiFail += 1 - continue - fm: sizet = mb.mark() - vpi_path: str = StubMergerCollect._sliced_path(vpi_dir, dl, ent.Sha1, "vpi") - if vpi_path is None: - ReplIncVpiFail += 1 - mb.reset_to(fm) - continue - # Existing stub -> reuse. - vf: io.File* = io.File(vpi_path, io.MODE.R) - if not vf.closed: - vf.close() - VpiStub.Ensure(vpi_path, ent.Sha1) - stdlib.free(vpi_path) - ReplIncVpiReuse += 1 - mb.reset_to(fm) - continue - # Read source file (1MB cap matching Phase1/BuildPipeline SRC_BUF_SIZE). - f: io.File* = io.File(ent.Path, io.MODE.R) - if f.closed: - stdlib.free(vpi_path) - ReplIncVpiFail += 1 - mb.reset_to(fm) - continue - if f.size() >= _REPL_SRC_BUF_CAP: - f.close() - stdlib.free(vpi_path) - ReplIncVpiFail += 1 - mb.reset_to(fm) - continue - src_buf: bytes = stdlib.malloc(_REPL_SRC_BUF_CAP) - if src_buf is None: - f.close() - stdlib.free(vpi_path) - ReplIncVpiFail += 1 - mb.reset_to(fm) - continue - bytes_read: LONG = f.read_all(src_buf, _REPL_SRC_BUF_CAP) - f.close() - if bytes_read <= 0: - stdlib.free(src_buf) - stdlib.free(vpi_path) - ReplIncVpiFail += 1 - mb.reset_to(fm) - continue - if bytes_read < _REPL_SRC_BUF_CAP: src_buf[bytes_read] = 0 - # Parse (all AST allocations stay within this file's mb window). - lx: ast.Lexer* = ast.new_lexer(mb) - tree: ast.AST* = None - if lx is not None: - ast._lexer_init(lx, src_buf, mb) - tokens: ast.Token* = ast.tokenize(lx) - tree = ast.parse_tokens(mb, tokens) - if tree is None: - stdlib.free(src_buf) - stdlib.free(vpi_path) - ReplIncVpiFail += 1 - mb.reset_to(fm) - continue - pyi_buf: bytes = stdlib.malloc(_REPL_VPI_BUF_CAP) - if pyi_buf is None: - stdlib.free(src_buf) - stdlib.free(vpi_path) - ReplIncVpiFail += 1 - mb.reset_to(fm) - continue - # Pass the absolute ent.Path as rel_path: vpi fatal_error reads source lines via it. - pos: sizet = StubConverter._GeneratePyiFromAst(mb, tree, ent.Path, pyi_buf, _REPL_VPI_BUF_CAP) - if pos > 0: - pf: io.File* = io.File(vpi_path, io.MODE.W) - if not pf.closed: - pf.write(pyi_buf, pos) - pf.close() - VpiStub.Emit(vpi_path, pyi_buf, pos, ent.Sha1) - ReplIncVpiNew += 1 - else: ReplIncVpiFail += 1 - else: ReplIncVpiFail += 1 - stdlib.free(pyi_buf) - stdlib.free(src_buf) - stdlib.free(vpi_path) - mb.reset_to(fm) - return ReplIncVpiNew - - -def ReplInitIncludes(mb: memhub.MemBuddy*) -> int: - """Session includes init: scan, register SHA1, pre-generate .vpi, register generic templates, then per-module classes; 0=success.""" - global ReplIncMods, ReplIncMs - if mb is None: return 1 - inc_dirs: str = Config.IncludesDirs - if inc_dirs is None or string.strlen(inc_dirs) == 0: - stdio.printf("REPL: project has no includes dir configured (project.vpj's includes); " "skipping includes init; importing includes modules will report an unregistered SHA1\n") - return 1 - # .vpi dir matches the fragment side: prefer the project temp dir, else the session dir. - vpi_dir: str = Config.TempDir - if vpi_dir is None: vpi_dir = ReplTmpDir - if vpi_dir is None: - stdio.printf("REPL: no usable .vpi dir (both Config.TempDir and the session dir are empty); " "skipping includes init\n") - return 1 - # 1) Scan includes (no reachability: REPL has no SourceDir/deps graph, so scan all). - t0: uint64 = win32base.GetTickCount64() - sr: IncludesScanner.ScanResult* = IncludesScanner.scan_includes(mb, inc_dirs) - if sr is None: - stdio.printf("REPL: includes scan failed (dir: %s); " "importing includes modules will report an unregistered SHA1\n", inc_dirs) - return 1 - # 2) Register "module -> SHA1" (same StubMerger global table batch consumers use). - n: int = StubMergerStore.PopulateSha1MapStore(sr) - ReplIncMs = int(win32base.GetTickCount64() - t0) - if n <= 0: - stdio.printf("REPL: includes scan registered no modules (dir: %s): " "check the includes path and the .vp files\n", inc_dirs) - return 1 - ReplIncMods = n - # 3) Pre-generate .vpi stubs (count failures, do not abort the session). - _repl_gen_includes_vpi(mb, sr, vpi_dir) - stdio.printf("includes: registered %d modules (scan took %d ms); " "stubs new %d / reused %d / failed %d\n", n, ReplIncMs, ReplIncVpiNew, ReplIncVpiReuse, ReplIncVpiFail) - # 3'') Register session generic templates for real: reuse includes sources to register - # generic class templates (list/dict/Vector...) so annotations like list[...] specialize. - cur_file_saved: str = GenApi.get_current_file() - n_gen_tpl: int = Phase1Scan._PreRegisterAllGenericTemplates(mb, sr) - if cur_file_saved is not None: GenApi.set_current_file_type(cur_file_saved) - if n_gen_tpl <= 0: - stdio.printf("includes generic templates: 0 -- list[...]/dict[...] in annotations can only be parsed as " "\"type-only\" placeholders (element layout and methods unavailable)\n") - # 3') M4.2: enable stub-layout recovery so vpi injection verifies and registers includes - # class layouts/vtable indices; also reset the generic-owner table for re-registration. - StubMergerStore.ResetGenericOwners() - VpiReader.ReplStubLayoutBegin(vpi_dir) - # 4) M4 class registration: inject each module's .vpi so its classes (with fields) enter the - # session StructTable, a prerequisite for cross-module construction/method calls. - ReplInitIncludesClasses(mb, sr, vpi_dir) - return 0 - - -# ============================================================ -# M2/M3: fragment execution path (stmt -> IR -> llc -> -shared DLL -> call -> ctx echo). - -# M3 top-level def lifting: lift a top-level def into the fragment module as an export def, -# so the host can GetProcAddress it and later fragments call it through a ctx slot. - -# Truncate a line (possibly non-NUL-terminated) into the session buffer for %s printing. -def _repl_show_line(p: bytes, n: int) -> str: - """Copy a line fragment into a session buffer as a NUL-terminated string (truncated to 118 bytes).""" - if ReplLineShow is None or p is None: return "(?)" - m: int = int(n) - if m > 118: m = 118 - if m < 0: m = 0 - string.memcpy(ReplLineShow, p, sizet(m)) - ReplLineShow[m] = 0 - return ReplLineShow - - -# Line length (up to '\n' or NUL). -def _repl_line_len(p: bytes) -> int: - n: int = 0 - while p[n] != '\n' and p[n] != 0: n += 1 - return n - - -# Count leading whitespace bytes. -def _repl_indent_len(p: bytes, n: int) -> int: - i: int = 0 - while i < n and (p[i] == ' ' or p[i] == '\t'): i += 1 - return i - - -# Whether [ind, n) starts with keyword kw (kw followed by whitespace). -def _repl_line_has_kw(p: bytes, n: int, ind: int, kw: str) -> int: - k: int = int(string.strlen(kw)) - if ind + k + 1 > n: return 0 - if string.strncmp(p + ind, kw, sizet(k)) != 0: return 0 - c: char = p[ind + k] - if c != ' ' and c != '\t': return 0 - return 1 - - -# Append a byte span to dst (keeping the trailing NUL); returns new length, -1 = out of capacity. -def _repl_cat(dst: bytes, cap: int, pos: int, src: bytes, n: int) -> int: - if dst is None or src is None or pos < 0 or n < 0: return -1 - if pos + n + 1 > cap: return -1 - string.memcpy(dst + pos, src, sizet(n)) - pos += n - dst[pos] = 0 - return pos - - -# Append a NUL-terminated string to dst; returns new length, -1 = out of capacity. -def _repl_cat_str(dst: bytes, cap: int, pos: int, s: str) -> int: - if s is None: return pos - return _repl_cat(dst, cap, int(pos), s, int(string.strlen(s))) - - -# Record a lifted function name (reserved-prefix check + same-fragment dedup + limit); 0=ok, 1=reject. -def _repl_lift_add_name(name: str) -> int: - global ReplLiftNameCount - if name is None or ReplLiftNames is None: return 1 - # Reserved prefix: the fragment entry name ("__repl_frag_") marks the fragment entry - # scope in Gen (session writes only work there), so users may not use it. - pfx: str = GenTypes.REPL_ENTRY_PREFIX - pl: int = int(string.strlen(pfx)) - if string.strncmp(name, pfx, sizet(pl)) == 0: - stdio.printf("M3: session function name cannot start with the reserved prefix '%s': %s\n", pfx, name) - return 1 - i: int - for i in range(int(ReplLiftNameCount)): - if string.strcmp(ReplLiftNames + i * _REPL_FN_NAME_MAX, name) == 0: - stdio.printf("M3: session function '%s' defined twice in the same fragment (define it only once)\n", name) - return 1 - if ReplLiftNameCount >= _REPL_LIFT_MAX: - stdio.printf("M3: a fragment can lift at most %d session functions (this fragment lifted %d)\n", _REPL_LIFT_MAX, ReplLiftNameCount) - return 1 - string.strcpy(ReplLiftNames + ReplLiftNameCount * _REPL_FN_NAME_MAX, name) - ReplLiftNameCount += 1 - return 0 - - -# Parse and rewrite one top-level def header into ReplLiftBuf as an export def with an -# injected ctx param; name written to name_out. Returns 0=ok/1=rejected/2=buffer full. -def _repl_lift_def_header(line: bytes, n: int, name_out: bytes, name_cap: int) -> int: - if n < 5 or name_out is None or ReplLiftBuf is None: return 1 - p: int = 3 # skip "def" - while p < n and (line[p] == ' ' or line[p] == '\t'): p += 1 - # Name (identifier characters). - ns: int = p - while p < n: - c: char = line[p] - is_id: int = 0 - if (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9') or c == '_': is_id = 1 - if is_id == 0: break - p += 1 - nlen: int = p - ns - if nlen <= 0: - stdio.printf("M3: def header is missing the function name: %s\n", _repl_show_line(line, int(n))) - return 1 - if nlen >= name_cap: - stdio.printf("M3: session function name too long (limit %d bytes): %s\n", name_cap - 1, _repl_show_line(line, int(n))) - return 1 - string.memcpy(name_out, line + ns, sizet(nlen)) - name_out[nlen] = 0 - # PEP 695 generic params [T]: skip balanced brackets (translation maps T to int). - if p < n and line[p] == '[': - d: int = 0 - while p < n: - if line[p] == '[': d += 1 - elif line[p] == ']': - d -= 1 - if d == 0: - p += 1 - break - p += 1 - if d != 0: - stdio.printf("M3: unclosed generic-parameter bracket in def header: %s\n", _repl_show_line(line, int(n))) - return 1 - if p >= n or line[p] != '(': - stdio.printf("M3: def header is missing '(' after the function name: %s\n", _repl_show_line(line, int(n))) - return 1 - lp: int = p - # Matching ')'. - d2: int = 0 - rp: int = -1 - q: int = lp - while q < n: - c2: char = line[q] - if c2 == '(': d2 += 1 - elif c2 == ')': - d2 -= 1 - if d2 == 0: - rp = q - break - q += 1 - if rp < 0: - stdio.printf("M3: def header is missing ')' (the signature must be on one line): %s\n", _repl_show_line(line, int(n))) - return 1 - # ':' must be on the same line (as must the return annotation). - has_colon: int = 0 - q2: int = rp + 1 - while q2 < n: - if line[q2] == ':': - has_colon = 1 - break - q2 += 1 - if has_colon == 0: - stdio.printf("M3: def header ':' is not on the same line (the signature must be on one line): %s\n", _repl_show_line(line, int(n))) - return 1 - # Whether the parameter list is empty. - empty: int = 1 - q3: int = lp + 1 - while q3 < rp: - if line[q3] != ' ' and line[q3] != '\t': - empty = 0 - break - q3 += 1 - # Reject a user-supplied __repl_ctx__ param (would duplicate the injected first param). - if empty == 0: - seg: int = lp + 1 - while seg + 12 <= rp: - if line[seg] == '_' and string.strncmp(line + seg, GenTypes.REPL_CTX_PARAM, sizet(12)) == 0: - stdio.printf("M3: session function '%s' cannot declare the %s parameter " "(it is auto-injected as the first parameter when lifting)\n", name_out, GenTypes.REPL_CTX_PARAM) - return 1 - seg += 1 - # Emit: "export " + original header [0,lp] + injection + original header [lp+1,n) + '\n'. - pos: int = int(string.strlen(ReplLiftBuf)) - pos = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), "export ") - pos = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), line, lp + 1) - if empty != 0: pos = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), "__repl_ctx__: bytes") - else: pos = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), "__repl_ctx__: bytes, ") - pos = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), line + lp + 1, n - lp - 1) - pos = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), "\n") - if pos < 0: - stdio.printf("M3: lift source buffer too small (%d bytes)\n", _REPL_LIFT_CAP) - return 2 - return 0 - - -# Split the fragment body: lifted blocks -> ReplLiftBuf (export def, unindented); other lines -# -> ReplSrcBuf (4 spaces each); records lifted names. 0=ok/1=rejected/2=buffer full. -def _repl_split_body(body: bytes) -> int: - global ReplWrapPos - if body is None or ReplLiftBuf is None or ReplSrcBuf is None: return 2 - total: int = int(string.strlen(body)) - i: int = 0 - pos: int = ReplWrapPos - in_lift: int = 0 # 1 = inside a lifted block - need_header: int = 0 # saw a decorator line, no def header yet - in_body: int = 0 # past the def header (indented lines are the body; check raise) - name_buf: bytes = ReplMsgName - while i < total: - n: int = int(_repl_line_len(body + i)) - ind: int = int(_repl_indent_len(body + i, int(n))) - blank: int = 0 - if ind >= n: blank = 1 - if in_lift != 0: - if blank != 0: - # Blank line: stays in the lift block (blank body lines are indentation-neutral). - lp0: int = int(string.strlen(ReplLiftBuf)) - lp0 = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, lp0, body + i, int(n)) - lp0 = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, lp0, "\n") - if lp0 < 0: return 2 - i += n + 1 - continue - if ind > 0: - if need_header != 0: - stdio.printf("M3: a def header must immediately follow the decorator: %s\n", _repl_show_line(body + i, int(n))) - return 1 - if in_body != 0 and _repl_line_has_kw(body + i, int(n), ind, "raise") != 0: - stdio.printf("M3: session function '%s' has raise in its body: %s\n" " M3 does not support cross-fragment exception propagation (raise appends the " "__eh_msg_out__/__eh_code_out__ out params to the function, " "but indirect calls through the function pointer do not pass them) -- the whole fragment is skipped\n", name_buf, _repl_show_line(body + i, int(n))) - return 1 - lp1: int = int(string.strlen(ReplLiftBuf)) - lp1 = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, lp1, body + i, int(n)) - lp1 = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, lp1, "\n") - if lp1 < 0: return 2 - i += n + 1 - continue - # Dedented non-blank line ends the lift block; re-evaluate this line (do not advance i). - in_lift = 0 - need_header = 0 - in_body = 0 - continue - if blank != 0 or ind > 0: - # Into the fragment body (4 spaces per line, same as the block path). - pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), " ") - pos = _repl_cat(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), body + i, int(n)) - pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), "\n") - if pos < 0: return 2 - i += n + 1 - continue - # Dedented non-blank line. - if body[i] == '@': - in_lift = 1 - need_header = 1 - in_body = 0 - lp2: int = int(string.strlen(ReplLiftBuf)) - lp2 = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, lp2, body + i, int(n)) - lp2 = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, lp2, "\n") - if lp2 < 0: return 2 - i += n + 1 - continue - if _repl_line_has_kw(body + i, int(n), 0, "def") != 0: - rc_h: int = int(_repl_lift_def_header(body + i, int(n), name_buf, _REPL_FN_NAME_MAX)) - if rc_h != 0: return rc_h - if _repl_lift_add_name(name_buf) != 0: return 1 - in_lift = 1 - need_header = 0 - in_body = 1 - i += n + 1 - continue - if _repl_line_has_kw(body + i, int(n), 0, "async") != 0 and _repl_line_has_kw(body + i, int(n), 6, "def") != 0: - stdio.printf("M3: async def session functions are not supported (coroutine ABI is incompatible with " "the M3 signature token; rewrite as a plain def): %s\n", _repl_show_line(body + i, int(n))) - return 1 - pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), " ") - pos = _repl_cat(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), body + i, int(n)) - pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), "\n") - if pos < 0: return 2 - i += n + 1 - if need_header != 0: - stdio.printf("M3: missing def header after decorator (end of fragment)\n") - return 1 - ReplWrapPos = pos - return 0 - - -# Assemble a multi-line block wrapper (lift + wrap): 0=ok/1=rejected/2=buffer full. -def _repl_build_frag_src_multi(name: str, body: bytes) -> int: - global ReplWrapPos - if name is None or body is None: return 2 - if ReplLiftBuf is None or ReplLiftNames is None or ReplSrcBuf is None: return 2 - # Lift buffer/name table watermarks are cleared per fragment by _build_frag_src; here we only - # assemble: emit the fragment function header, then body lines each get 4 spaces. - viperlib.snprintf(ReplSrcBuf, _REPL_FRAG_SRC_CAP, "export def %s(%s: bytes) -> int:\n", name, GenTypes.REPL_CTX_PARAM) - ReplWrapPos = int(string.strlen(ReplSrcBuf)) - rc: int = _repl_split_body(body) - if rc != 0: return rc - pos: int = ReplWrapPos - pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), " return 0\n") - if pos < 0: return 2 - # Append lifted module-level defs after the fragment entry function (module scope, order-free). - lift_len: int = int(string.strlen(ReplLiftBuf)) - if lift_len > 0: - pos = _repl_cat(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), ReplLiftBuf, lift_len) - if pos < 0: return 2 - ReplWrapPos = pos - return 0 - - -# ============================================================ -# M3 session function table: host owns it (writes table + ctx slots); Gen reads only. - -# Find a session function table entry by name (host side; same as Gen's repl_find_func_entry). -def _repl_func_entry_find(name: str) -> GenTypes.ReplFuncEntry*: - """1=entry found; None=not registered or table not built.""" - if ReplCtxObj is None or ReplCtxObj.FuncTable is None or name is None: return None - es: sizet = GenTypes.ReplFuncEntry.__sizeof__() - i: int - for i in range(int(ReplCtxObj.FuncCount)): - e: GenTypes.ReplFuncEntry* = GenTypes.ReplFuncEntry*(uint64(ReplCtxObj.FuncTable) + uint64(i) * uint64(es)) - if e.Name is not None and string.strcmp(e.Name, name) == 0: return e - return None - - -# Name of the idx-th lifted function (NUL-terminated); None if out of range. -def _repl_lift_name(idx: int) -> str: - if ReplLiftNames is None or idx < 0 or idx >= ReplLiftNameCount: return None - return ReplLiftNames + idx * _REPL_FN_NAME_MAX - - -# Deep-copy into PermPool (session table entry names etc. that outlive a fragment). -def _repl_perm_dup(s: str) -> str: - if s is None or _perm_pool is None: return None - n: sizet = string.strlen(s) - b: str = _perm_pool.alloc(n + 1) - if b is None: return None - string.strcpy(b, s) - return b - - -# Render signature result -> human-readable "(P1, P2) -> R" (into a PermPool buffer). -def _repl_sig_to_text(tag: int, bits: int, pc: int) -> str: - """Render ReplSigResult + ReplSigTags/Bits as "(P1, P2) -> R" (PermPool buffer).""" - if ReplSigText is None: return "(?)" - pos: int = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, 0, "(") - for i in range(int(pc)): - if i > 0: pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, int(pos), ", ") - pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, int(pos), _repl_tag_text(int(ReplSigTags[i]), int(ReplSigBits[i]))) - pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, int(pos), ") -> ") - pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, int(pos), _repl_tag_text(tag, bits)) - if pos < 0: return "(?)" - return ReplSigText - - -# Pre-registration check (before llc/load): per lifted function verify signature, EH params, -# type support, ctx first param, param count, redefinition, capacity - else abort the fragment. -def _repl_precheck_session_funcs(mod: mlirlite.LLVMModule*) -> int: - if ReplLiftNameCount <= 0: return 0 - if ReplCtxObj is None or ReplCtxObj.FuncTable is None or ReplSigResult is None: - stdio.printf("M3: session function table/signature buffer not initialized (session init failed)\n") - return 1 - new_cnt: int = 0 - idx: int - for idx in range(int(ReplLiftNameCount)): - nm: str = _repl_lift_name(idx) - if nm is None: return 1 - rc: int = _repl_model_func_sig(mod, nm, ReplSigResult) - if rc == 1: - stdio.printf("M3: function '%s' not found in the defining fragment's model (lift not effective?)\n", nm) - return 1 - if rc == 2: - stdio.printf("M3: session function '%s' signature has an unsupported type '%s'\n" " M3 supports void / i1 / i8 / i16 / i32 / i64 / float / double / " "i8* (str, bytes, rawptr); for struct values/arrays/multi-level pointers use pointer params " "or wait for M4\n", nm, ReplSigResult.BadType) - return 1 - if rc == 3: - stdio.printf("M3: session function '%s' signature has EH out params (%%__eh_msg_out__/" "%%__eh_code_out__) -- its body contains raise\n" " M3 does not support cross-fragment exception propagation: indirect calls through the function pointer do not pass these two args " "(arity mismatch); the whole fragment is skipped\n", nm) - return 1 - if rc == 4: - stdio.printf("M3: session function '%s' define's first param is not %s (lift template and calling convention " "disagree)\n", nm, GenTypes.REPL_CTX_PARAM) - return 1 - if rc == 5: - stdio.printf("M3: session function '%s' has more than %d params\n", nm, GenTypes.REPL_FUNC_PARAM_MAX) - return 1 - if rc != 0: - stdio.printf("M3: failed to parse session function '%s' signature (rc=%d)\n", nm, rc) - return 1 - old: GenTypes.ReplFuncEntry* = _repl_func_entry_find(nm) - if old is None: - new_cnt += 1 - continue - # Redefinition: only a same-signature overwrite is allowed (a different signature would - # make earlier fragments call new code with the old ABI -> silent UB). Same sig: update addr. - sig_same: int = 1 - if old.RetTag != ReplSigResult.RetTag or old.RetBits != ReplSigResult.RetBits or old.ParamCount != ReplSigResult.ParamCount: sig_same = 0 - if sig_same != 0: - for pi in range(int(old.ParamCount)): - if int(old.ParamTags[pi]) != int(ReplSigTags[pi]) or int(old.ParamBits[pi]) != int(ReplSigBits[pi]): - sig_same = 0 - break - if sig_same == 0: - stdio.printf("M3: redefinition of session function '%s' has a different signature (old %s, new %s)\n" " M3 allows a same-signature overwrite (address only); a different signature would make call " "sites already compiled in earlier fragments call new code with the old ABI -- the whole fragment is skipped; use another name\n", nm, _repl_entry_sig_text(old), _repl_sig_to_text(ReplSigResult.RetTag, ReplSigResult.RetBits, ReplSigResult.ParamCount)) - return 1 - if ReplCtxObj.FuncCount + new_cnt > ReplCtxObj.FuncCap: - stdio.printf("M3: session function limit of %d reached (ctx function pointer slots exhausted); %d added this time; " "restart the session or use fewer session functions\n", ReplCtxObj.FuncCap, new_cnt) - return 1 - return 0 - - -# Render an existing entry's signature (for redefinition-conflict diagnostics; into ReplSigText2). -def _repl_entry_sig_text(e: GenTypes.ReplFuncEntry*) -> str: - if e is None or ReplSigText2 is None: return "(?)" - pos: int = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, 0, "(") - for i in range(int(e.ParamCount)): - if i > 0: pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, int(pos), ", ") - pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, int(pos), _repl_tag_text(int(e.ParamTags[i]), int(e.ParamBits[i]))) - pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, int(pos), ") -> ") - pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, int(pos), _repl_tag_text(e.RetTag, e.RetBits)) - if pos < 0: return "(?)" - return ReplSigText2 - - -# Register after a successful DLL load: GetProcAddress(bare name) -> write ctx slot -> -# append/update the table. 0=ok; 1=failed (caller rolls back by ReplSavedFuncCount and aborts). -def _repl_register_session_funcs(h_lib: win32base.HANDLE, frag_no: int, mod: mlirlite.LLVMModule*) -> int: - if ReplLiftNameCount <= 0: return 0 - if ReplCtxObj is None or ReplCtxObj.FuncTable is None or ReplCtxBuf is None: - stdio.printf("M3: session function table/ctx not initialized\n") - return 1 - idx: int - for idx in range(int(ReplLiftNameCount)): - nm: str = _repl_lift_name(idx) - if nm is None: return 1 - # Exported symbol is the bare name: `export def` sets has_export without a SHA1 prefix, - # and -shared without a DEF file exports all symbols automatically. - addr: VOIDPTR = win32base.GetProcAddress(h_lib, nm) - if addr is None: - stdio.printf("M3: session function '%s' is absent from the fragment DLL's export table " "(GetProcAddress returned null; this fragment is skipped)\n", nm) - return 1 - ent: GenTypes.ReplFuncEntry* = _repl_func_entry_find(nm) - if ent is None: - # New entry (signature already validated; re-parse to recover the scratch tokens). - rc: int = _repl_model_func_sig(mod, nm, ReplSigResult) - if rc != 0: - stdio.printf("M3: failed to re-parse session function '%s' signature (rc=%d)\n", nm, rc) - return 1 - j: int = int(ReplCtxObj.FuncCount) - es: sizet = GenTypes.ReplFuncEntry.__sizeof__() - ent = GenTypes.ReplFuncEntry*(uint64(ReplCtxObj.FuncTable) + uint64(j) * uint64(es)) - nm_c: str = _repl_perm_dup(nm) - nm_s: str = _repl_perm_dup(nm) - tags_b: bytes = _perm_pool.alloc(GenTypes.REPL_FUNC_PARAM_MAX) - bits_b: bytes = _perm_pool.alloc(GenTypes.REPL_FUNC_PARAM_MAX) - if nm_c is None or nm_s is None or tags_b is None or bits_b is None: - stdio.printf("M3: failed to allocate the session function entry ('%s')\n", nm) - return 1 - string.memset(tags_b, 0, GenTypes.REPL_FUNC_PARAM_MAX) - string.memset(bits_b, 0, GenTypes.REPL_FUNC_PARAM_MAX) - string.memcpy(tags_b, ReplSigTags, sizet(ReplSigResult.ParamCount)) - string.memcpy(bits_b, ReplSigBits, sizet(ReplSigResult.ParamCount)) - ent.Name = nm_c - ent.SymName = nm_s - ent.FragNo = frag_no - ent.SlotOff = GenTypes.REPL_FUNC_BASE + j * GenTypes.REPL_FUNC_SLOT_SIZE - ent.RetTag = ReplSigResult.RetTag - ent.RetBits = ReplSigResult.RetBits - ent.ParamCount = ReplSigResult.ParamCount - ent.ParamTags = tags_b - ent.ParamBits = bits_b - ReplCtxObj.FuncCount = j + 1 - else: - # Redefinition (same signature): only swap address and fragment number; slot offset unchanged. - ent.FragNo = frag_no - # Write the ctx slot (low 8 bytes = address, high 8 bytes reserved 0). - sv: ReplFuncSlotView* = ReplFuncSlotView*(uint64(ReplCtxBuf) + uint64(ent.SlotOff)) - sv.Addr = addr - sv.Reserved = None - stdio.printf("session function %s: export symbol %s -> ctx slot +%d (defining fragment %d, signature %s)\n", nm, nm, ent.SlotOff, frag_no, _repl_entry_sig_text(ent)) - return 0 - - -# ============================================================ -# M3 session function signature parsing: read the definition-site ABI from the model. - - -# token -> display name (for logs/diagnostics only). -def _repl_tag_text(tag: int, bits: int) -> str: - if tag == GenTypes.REPL_TY_VOID: return "void" - if tag == GenTypes.REPL_TY_INT: - if bits == 1: return "bool" - if bits == 8: return "char" - if bits == 16: return "int16" - if bits == 32: return "int" - if bits == 64: return "int64" - return "int?" - if tag == GenTypes.REPL_TY_FLOAT: - if bits == 32: return "float" - if bits == 64: return "double" - return "float?" - if tag == GenTypes.REPL_TY_STR: return "str" - return "?" - - -# LLVMType* -> packed token (tag*1000 + bits); 0 = unsupported. Supported set matches the -# old string version: i1/i8/i16/i32/i64, float/double, Ptr(Int8) (str/bytes/rawptr). -def _repl_ty_ptr_token(ty: mlirlite.LLVMType*) -> int: - if ty is None: return 0 - match ty: - case mlirlite.LLVMType.Int(bits): - if bits == 1: return GenTypes.REPL_TY_INT * 1000 + 1 - if bits == 8: return GenTypes.REPL_TY_INT * 1000 + 8 - if bits == 16: return GenTypes.REPL_TY_INT * 1000 + 16 - if bits == 32: return GenTypes.REPL_TY_INT * 1000 + 32 - if bits == 64: return GenTypes.REPL_TY_INT * 1000 + 64 - return 0 - case mlirlite.LLVMType.Float(bits): - if bits == 32: return GenTypes.REPL_TY_FLOAT * 1000 + 32 - if bits == 64: return GenTypes.REPL_TY_FLOAT * 1000 + 64 - return 0 - case mlirlite.LLVMType.Ptr(pointee): - match pointee: - case mlirlite.LLVMType.Int(bits): - if bits == 8: return GenTypes.REPL_TY_STR * 1000 + 8 - return 0 - case _: - return 0 - case _: - return 0 - - -# Diagnostic: LLVMType* -> readable name (for unsupported-signature errors); none if unnamed. -def _repl_ty_name(ty: mlirlite.LLVMType*) -> str: - if ty is None: return "(none)" - nm: str = mlirlite.get_struct_name(ty) - if nm is not None: return nm - return "(unsupported)" - - -# Take a session function signature from the translation model (not by parsing IR define lines). -# Codes: 0=ok; 1=not found; 2=unsupported type; 3=EH params; 4=first param not ctx; 5=too many params. -def _repl_model_func_sig(mod: mlirlite.LLVMModule*, name: str, out: ReplSigOut*) -> int: - if name is None or out is None or mod is None: return 1 - out.RetTag = GenTypes.REPL_TY_VOID - out.RetBits = 0 - out.ParamCount = 0 - out.EHTag = 0 - out.Bad = 0 - out.BadType = None - fn: mlirlite.Function* = GenApi.find_func_in_module(mod, name) - if fn is None: return 1 - # 1) Return type. - rt_ty: mlirlite.LLVMType* = mlirlite.function_get_ret_ty(fn) - is_void: int = 0 - if rt_ty is None: is_void = 1 - else: - match rt_ty: - case mlirlite.LLVMType.Void(): - is_void = 1 - case _: - is_void = 0 - if is_void != 0: - out.RetTag = GenTypes.REPL_TY_VOID - out.RetBits = 0 - else: - rt: int = _repl_ty_ptr_token(rt_ty) - if rt == 0: - out.Bad = 1 - out.BadType = _repl_ty_name(rt_ty) - return 2 - out.RetTag = rt // 1000 - out.RetBits = rt % 1000 - # 2) Params: the first is ctx (Ptr(Int8) named __repl_ctx__), the rest are business params. - pi: int = 0 - p_cur: mlirlite.Param* = mlirlite.function_get_param_head(fn) - while p_cur is not None: - p_ty: mlirlite.LLVMType* = mlirlite.param_get_ty(p_cur) - p_nm: str = mlirlite.param_get_name(p_cur) - # Param names may or may not carry a leading '%' (the printer adds it on demand); strip it. - bare_nm: str = p_nm - if bare_nm is not None and bare_nm[0] == '%': bare_nm = bare_nm + 1 - if pi == 0: - if _repl_ty_ptr_token(p_ty) != GenTypes.REPL_TY_STR * 1000 + 8: return 4 - if bare_nm is None or string.strcmp(bare_nm, GenTypes.REPL_CTX_PARAM) != 0: return 4 - else: - # EH param (raise semantics). - if bare_nm is not None: - if string.strcmp(bare_nm, "__eh_msg_out__") == 0 or string.strcmp(bare_nm, "__eh_code_out__") == 0: - out.EHTag = 1 - return 3 - if out.ParamCount >= GenTypes.REPL_FUNC_PARAM_MAX: return 5 - tk: int = _repl_ty_ptr_token(p_ty) - if tk == 0: - out.Bad = 1 - out.BadType = _repl_ty_name(p_ty) - return 2 - ReplSigTags[out.ParamCount] = char(tk // 1000) - ReplSigBits[out.ParamCount] = char(tk % 1000) - out.ParamCount = out.ParamCount + 1 - pi += 1 - p_cur = mlirlite.param_get_next(p_cur) - return 0 - - - -# ============================================================ -# Assemble the wrapped fragment source into ReplSrcBuf (the fragment body is always a function body). -def _build_frag_src(name: str, body: str) -> int: - """Assemble the wrapped fragment source: single line uses the template verbatim; multi-line lifts and indents by 4.""" - global ReplLiftNameCount - if name is None or body is None: return 2 - # Clear the previous fragment's lifting state before assembling (single-line path lifts - # nothing; a stale name count would make the precheck wrongly reject this fragment). - ReplLiftNameCount = 0 - ReplLiftBuf[0] = 0 - ReplLiftNames[0] = 0 - if string.strchr(body, '\n') is None: - # Single line: existing template (ctx param bytes => i8*; export => no SHA1 prefix). - viperlib.snprintf(ReplSrcBuf, _REPL_FRAG_SRC_CAP, "export def %s(%s: bytes) -> int:\n %s\n return 0\n", name, GenTypes.REPL_CTX_PARAM, body) - return 0 - # Multi-line: lift top-level defs + assemble line by line (M3; rules in _repl_split_body). - return _repl_build_frag_src_multi(name, body) - - -# Execute one fragment source (single-line and block share this entry): parse the unwrapped -# source first (readable diagnostics), then hand it to ReplExecFragment to wrap/translate/run. -def _exec_repl_src(mb: memhub.MemBuddy*, src: str) -> int: - """Parse and execute one fragment source; 0=handled (diagnostics already printed).""" - if src is None: return 1 - tree: ast.AST* = ast.parse(src, mb) - if tree is None: - el: int = ast.get_parse_err_line() - ec: int = ast.get_parse_err_col() - em: str = ast.get_parse_err_msg() - if em is None: em = "(unknown)" - stdio.printf("ParseError: line %d column %d: %s\n", el, ec, em) - return 0 - if _count_top_stmts(tree) <= 0: - stdio.printf("Parsed: 0 top-level statements (skipping)\n") - return 0 - # M2/M3 execution path: whole block/line runs as one fragment function body; failures only - # print the reason (or longjmp back to the recovery point), never kill the process. - rc_frag: int = ReplExecFragment(mb, src) - if rc_frag != 0: - # Non-longjmp failures already printed their reason inside the fragment; here only roll - # back the session table watermarks so the tables and ctx stay consistent. - if ReplCtxObj is not None: - ReplCtxObj.Count = ReplSavedVarCount - ReplCtxObj.FuncCount = ReplSavedFuncCount - # M4: roll back the session baseline (DefineT/TypedefT) watermarks; a no-op on the normal - # path, it keeps the "failed fragment never changes the baseline" invariant. - ReplBaselineTruncate(ReplSavedCDefineCount, ReplSavedCTypedefCount) - return 0 - - -# Execute one fragment (any top-level statement: expr/assign/ann-assign/aug-assign/...). -# Returns 0=ok (echoed by result_kind); non-zero=failed. Fatal errors longjmp to run's point. -def ReplExecFragment(mb: memhub.MemBuddy*, src: str) -> int: - """Fragment -> DLL -> call -> ctx echo; 0=success.""" - global ReplFragFn, ReplFragNo, ReplSavedVarCount, ReplSavedFuncCount - global ReplSavedCDefineCount, ReplSavedCTypedefCount - if mb is None or src is None: return 1 - if ReplTmpDir is None or ReplNameBuf is None or ReplSrcBuf is None: - stdio.printf("REPL: session buffer not initialized\n") - return 1 - if ReplSrcPath is None or ReplIrBuf is None or ReplNeedleBuf is None: - stdio.printf("REPL: session buffer not initialized\n") - return 1 - if ReplCtxBuf is None or ReplCtxObj is None or ReplVarTable is None: - stdio.printf("REPL: ctx session memory not initialized\n") - return 1 - - # 0) Re-arm the recovery point and snapshot session table watermarks so a failed fragment's - # half-registered entries can be rolled back; set_repl_jmpbuf is idempotent. - VLogger.set_repl_jmpbuf(ReplJmpBuf) - ReplSavedVarCount = ReplCtxObj.Count - ReplSavedFuncCount = ReplCtxObj.FuncCount - # M4: session baseline watermarks (DefineT/TypedefT); a failed fragment must not add - # increments, same idea as the var/func watermarks (only success calls ReplBaselineCapture). - ReplSavedCDefineCount = ReplBaseCDefineCount - ReplSavedCTypedefCount = ReplBaseCTypedefCount - - # 1) Take the number and increment it immediately: names/files stay unique even after a failure. - n: int = ReplFragNo - ReplFragNo = n + 1 - viperlib.snprintf(ReplNameBuf, _REPL_NAME_CAP, "__repl_frag_%d", n) - - # 2) Strip leading whitespace (the template indents the body; stacking would error). For - # blocks only the first line is affected; step 3 indents the rest uniformly. - body: str = src - if body is not None: - while body[0] == ' ' or body[0] == '\t': body = body + 1 - - # 3) Wrap: ctx param (bytes => i8*); export => no SHA1 prefix. Single line uses the existing - # template; multi-line lifts top-level defs. Returns 0=ok/1=rejected/2=buffer full. - rc_build: int = _build_frag_src(ReplNameBuf, body) - if rc_build == 2: - stdio.printf("fragment %s: failed to assemble the wrapped source (buffer too small, %d bytes)\n", ReplNameBuf, _REPL_FRAG_SRC_CAP) - return 1 - if rc_build != 0: - # Lift rule rejection (async def / raise / duplicate / non-single-line signature / own - # ctx param...): the whole fragment is skipped (no partial side effects), session continues. - return 1 - - # 4) Write the wrapped source to disk (used only for error echo: fatal_error reads this file). - viperlib.snprintf(ReplSrcPath, _REPL_PATH_CAP, "%s/%s.vp", ReplTmpDir, ReplNameBuf) - sf: io.File* = io.File(ReplSrcPath, io.MODE.W) - if not sf.closed: - sf.write(ReplSrcBuf, string.strlen(ReplSrcBuf)) - sf.close() - - # 5) Reset per-fragment module-level translation state (mirrors Phase1Main/BuildPipeline); - # temp_dir is used for .vpi injection: prefer the project temp dir, else the session dir. - vpi_dir: str = Config.TempDir - if vpi_dir is None: vpi_dir = ReplTmpDir - GenApi.set_temp_dir(vpi_dir) - # CallGenBase.TempDir/TempDirLen (cross-module .vpi/.text.ll lookup, ExportT checks) is only - # set by SetTempDir; set it here once per fragment, same value as set_temp_dir. - GenApi.SetTempDir(vpi_dir) - GenApi.set_current_file_type(ReplSrcPath) - GenApi.set_current_file_imports(ReplSrcPath) - GenApi.clear_trace() - GenApi.clear_cdefine_constants() - GenApi.clear_ctypedefs() - # Visibility uses loose mode (strict=0, all registered structs visible): fragment types come - # from the session, so strict isolation would hide them and fail annotation resolution. - GenApi.reset_visible_structs(mb, 0) - # Struct registration uses permanent allocation (like Phase1b/Phase2 StdlibAlloc=1): table, - # entry names and field types go into PermPool, else later fragments read freed memory. - GenApi.set_stdlib_alloc(1) - # M4: replay the DefineT/TypedefT session baseline; the clear_* above emptied the tables, and - # this fragment's additions are captured on success by ReplBaselineCapture. - ReplBaselineRestore(mb) - - # 6) Parse the wrapped source. Fragments containing "class " use the permanent pool (generic - # templates store the ClassDef node for later specialization); others use mb as before. - parse_pool: memhub.MemBuddy* = mb - if _perm_pool is not None: - if string.strstr(ReplSrcBuf, "class ") is not None: parse_pool = _perm_pool - tree: ast.AST* = ast.parse(ReplSrcBuf, parse_pool) - if tree is None: - el: int = ast.get_parse_err_line() - ec: int = ast.get_parse_err_col() - em: str = ast.get_parse_err_msg() - if em is None: em = "(unknown)" - stdio.printf("fragment %s: failed to parse the wrapped source (line %d column %d: %s)\n", ReplNameBuf, el, ec, em) - return 1 - - # 7) Translate to full module IR. ModuleSha1 = session sha1 (types are "."); - # ReplCtx enables REPL mode and is cleared after translation. - tr: HandlesTranslator.Translator* = mb.alloc(HandlesTranslator.Translator.__sizeof__()) - if tr is None: - stdio.printf("fragment %s: Translator allocation failed\n", ReplNameBuf) - return 1 - tr.__before_init__() - tr.__init__() - tr.ModuleSha1 = ReplSessionSha1 - tr._declare_only = 0 - tr.ReplCtx = ReplCtxObj - rc_tr: int = tr.translate(tree) - tr.ReplCtx = None - if rc_tr != 0: - stdio.printf("fragment %s: translation failed\n", ReplNameBuf) - return 1 - tr.dump_ir(ReplIrBuf, _REPL_IR_CAP, mlirlite.OUTPUT_FULL) - - # 7') M4: if this fragment defines session-prefixed symbols ("."), produce and record - # its import lib so later fragments can resolve them at link time. - needs_implib: int = 0 - if ReplSessionSha1 is not None and ReplNeedleBuf is not None: - viperlib.snprintf(ReplNeedleBuf, _REPL_NEEDLE_CAP, "@\"%s.", ReplSessionSha1) - if string.strstr(ReplIrBuf, ReplNeedleBuf) is not None: needs_implib = 1 - - # 8) Export-symbol probe: the signature must be "(%__repl_ctx__: !llvm.ptr) -> i32"; otherwise - # the wrapper/calling convention is wrong (or raise added EH params), so abort. - viperlib.snprintf(ReplNeedleBuf, _REPL_NEEDLE_CAP, "llvm.func @%s(%%%s: !llvm.ptr) -> i32", ReplNameBuf, GenTypes.REPL_CTX_PARAM) - if string.strstr(ReplIrBuf, ReplNeedleBuf) is None: - stdio.printf("fragment %s: expected export function was not generated (expected `llvm.func @%s(%%__repl_ctx__: !llvm.ptr) -> i32`); " "fragments with raise append EH out params, which M2 does not support\n", ReplNameBuf, ReplNameBuf) - return 1 - - # 8') Session function signature check (M3), before llc/load: any failure skips the whole - # fragment; returns 0 immediately when no functions were lifted. - if _repl_precheck_session_funcs(tr.Module) != 0: return 1 - - # 9) Write .ll (reusing BuildPipeline: sliced path + ensure_dir). - ir_len: sizet = string.strlen(ReplIrBuf) - if BuildPipeline.write_ir_to_file(ReplIrBuf, ir_len, ReplTmpDir, ReplNameBuf) != 0: - stdio.printf("fragment %s: failed to write .ll (dir %s)\n", ReplNameBuf, ReplTmpDir) - return 1 - ir_path: str = BuildPipeline.build_sliced_path(ReplTmpDir, ReplNameBuf, "ll") - if ir_path is None: - stdio.printf("fragment %s: failed to build the .ll path\n", ReplNameBuf) - return 1 - - # 10) llc -> .obj (reusing existing machinery; VLogger.error longjmps on failure). - cc_cmd: str = Config.CompilerCmd - if cc_cmd is None: cc_cmd = "llc" - cc_flags: str = Config.CompilerFlags - if cc_flags is None: cc_flags = "-filetype=obj -relocation-model=pic" - if BuildPipeline.compile_ll_to_obj(ir_path, ReplTmpDir, ReplNameBuf, cc_cmd, cc_flags) != 0: - # This branch is only reached on compile_ll_to_obj's internal allocation failure (real llc - # failures longjmp), so print a note instead of silently returning to the prompt. - stdio.printf("fragment %s: llc failed to compile .obj (command/path buffer allocation failed; see diagnostics above)\n", ReplNameBuf) - return 1 - - # 11) clang++ -shared -> .dll; process launch reuses BuildPipeline._run_cmd_wait. - obj_path: str = BuildPipeline.build_sliced_path(ReplTmpDir, ReplNameBuf, "obj") - dll_path: str = BuildPipeline.build_sliced_path(ReplTmpDir, ReplNameBuf, "dll") - if obj_path is None or dll_path is None: - stdio.printf("fragment %s: failed to build the .obj/.dll path\n", ReplNameBuf) - return 1 - linker_cmd: str = Config.LinkerCmd - if linker_cmd is None: linker_cmd = "clang++" - cmd: bytes = mb.alloc(_REPL_CMD_CAP) - if cmd is None: - stdio.printf("fragment %s: command-line buffer allocation failed\n", ReplNameBuf) - return 1 - # Cross-module symbols come from the host exe, so add the host import lib (if any) to the link - # line; M4 also adds this fragment's --out-implib and prior fragments' import libs. - implib_path: str = mb.alloc(_REPL_PATH_CAP) - if implib_path is None: - stdio.printf("fragment %s: import lib path buffer allocation failed\n", ReplNameBuf) - return 1 - viperlib.snprintf(implib_path, _REPL_PATH_CAP, "%s.dll.a", dll_path) - pos_c: int = _repl_cat_str(cmd, _REPL_CMD_CAP, 0, linker_cmd) - pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, " -shared -o ") - pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, dll_path) - pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, " ") - pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, obj_path) - if ReplHostImplib is not None: - pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, " ") - pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, ReplHostImplib) - pos_c = _repl_cmd_append_implibs(cmd, _REPL_CMD_CAP, pos_c) - if pos_c >= 0: - pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, " -Wl,--out-implib=") - pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, implib_path) - if pos_c < 0: - stdio.printf("fragment %s: link command line exceeds %d bytes (%d registered fragment import libs " "in total) -- this fragment is skipped; restart the session to reduce the fragment count\n", ReplNameBuf, _REPL_CMD_CAP, ReplFragImplibCount) - return 1 - link_rc: int = BuildPipeline._run_cmd_wait(cmd) - if link_rc != 0: - stdio.printf("fragment %s: failed to generate .dll (exit=%d)\n command: %s\n", ReplNameBuf, link_rc, cmd) - if ReplHostImplib is None: - stdio.printf(" Hint: host import lib (.dll.a) not found -- " "cross-module symbols (of the form <16-hex>.) cannot be resolved; " "build the host with ViperC/project.vpj (including -Wl,--export-all-symbols), " "and keep .dll.a in the same dir as the exe\n") - else: - stdio.printf(" Hint: if the output contains undefined symbol of the form <16-hex>., " "that symbol is not defined/exported by the host (the corresponding includes module was not linked when building the host); " "if the function is `export def` in source (ExportT, bare-name definition) yet still carries a prefix, " "the include stub .vpi is probably missing (see the includes scan line at session start)\n") - return 1 - - # 11') M4: record this fragment's import lib (if it defined session-prefixed symbols) so later - # fragments resolve its class/method symbols; recording failure only warns. - if needs_implib != 0: - if _is_file(implib_path) != 0 and ReplFragImplibRecord(implib_path) != 0: - stdio.printf("fragment %s: registered fragment import lib (later fragments can resolve types/method " "symbols defined by this fragment): %s\n", ReplNameBuf, implib_path) - else: - stdio.printf("fragment %s: fragment import lib not registered (%d registered, limit %d; or the file " "is missing) -- later fragments may fail to resolve types/method symbols defined here\n", ReplNameBuf, ReplFragImplibCount, _REPL_FRAG_IMPLIB_MAX) - - # 12) Dynamic load + address lookup (LoadLibraryA/GetProcAddress). - h_lib: win32base.HANDLE = win32base.LoadLibraryA(dll_path) - if h_lib is None: - stdio.printf("fragment %s: LoadLibraryA failed: %s\n", ReplNameBuf, dll_path) - if ReplHostImplib is not None: - stdio.printf(" Hint: the fragment DLL import table points to the host exe (%s); " "if GetLastError is 127 (procedure not found) / loader 0xC0000139, " "the host and this import lib are usually from different builds, or the host lacks that export; " "it also fails when the process image name (%s) differs from the recorded import lib name\n", ReplHostImplib, dll_path) - return 1 - fp_addr: VOIDPTR = win32base.GetProcAddress(h_lib, ReplNameBuf) - if fp_addr is None: - stdio.printf("fragment %s: GetProcAddress returned null (symbol absent from the export table): %s\n", ReplNameBuf, dll_path) - return 1 - - # 12') Session function registration (M3), before calling the entry so a failure skips this - # fragment's statements; in-fragment calls to the same name still use direct calls. - if _repl_register_session_funcs(h_lib, int(n), tr.Module) != 0: - if ReplCtxObj is not None: ReplCtxObj.FuncCount = ReplSavedFuncCount - return 1 - - # 13) Reset the result area (kind=none) and refresh var_count/last_frag; call indirectly (the - # module stays resident, so no FreeLibrary). - head: GenTypes.ReplResult* = _ctx_head() - if head is None: return 1 - head.Kind = GenTypes.REPL_RES_NONE - head.VarCount = ReplCtxObj.Count - head.LastFrag = n - ReplFragFn = fp_addr - # The argument type is the constructed call signature's param type (bytes => i8*). - r_rc: int = ReplFragFn(ReplCtxBuf) - - # 14) Read back from the ctx result area and echo (the fragment return is only a status code). - if _repl_echo_result(ReplNameBuf, head) != 0: - stdio.printf("fragment %s: failed to read the result area\n", ReplNameBuf) - return 1 - if r_rc != 0: stdio.printf("fragment %s: fragment returned non-zero status %d\n", ReplNameBuf, r_rc) - # 14') M4: on success, merge this fragment's new DefineT/TypedefT entries into the session - # baseline; failure paths skip this, so the baseline never holds a failed fragment's leftovers. - ReplBaselineCapture() - return 0 - - -# ============================================================ -# run - the M2/M3 REPL main loop. -def run(mb: memhub.MemBuddy*) -> int: - """M2/M3 REPL: read a line/block, parse, compile to a DLL and call it (session vars via ctx slots), then echo; errors never kill the process.""" - global ReplJmpBuf, ReplSavedVarCount, ReplSavedFuncCount, ReplInBlock, ReplBlockLen - if mb is None or _perm_pool is None: - stdio.printf("REPL: need the main pool and PermPool (main injects _perm_pool)\n") - return 1 - - # Parse tables (keywords/operators) go in PermPool: they are ast module-level globals built - # once; on mb they would be freed by each line's reset_to(mark) and crash the next parse. - ast._init_tables(_perm_pool) - - jb: rawptr = _perm_pool.alloc(_REPL_JMPBUF_SIZE) - if jb is None: - stdio.printf("REPL: jmp_buf allocation failed\n") - return 1 - # Session-level registration: ReplJmpBuf is re-registered per fragment; the VLogger side is set - # once and cleared only on run exit (that is what covers the whole pipeline). - ReplJmpBuf = jb - ReplSavedVarCount = 0 - ReplSavedFuncCount = 0 - VLogger.set_repl_jmpbuf(jb) - # jit_flag is set True by main before calling run (see main.vp --repl); run resets it to False. - lg: VLogger.Logger* = VLogger.logger - - # Arm the recovery point before use: session init may longjmp via VLogger.error, and an - # un-armed jmp_buf would be UB; init is the only stage that must succeed, so exit on failure. - rc_init: int = _setjmp(jb, None) - if rc_init != 0: - stdio.printf("REPL: session init interrupted (see diagnostics above) -- no resident buffers/ctx, REPL cannot start\n") - stdio.fflush(None) - ReplJmpBuf = None - VLogger.clear_repl_jmpbuf() - if lg is not None: lg.set_jit_flag(False) - return 1 - - # Session init: temp dir + permanent buffers + ctx (all PermPool; see ReplInitSession). - if ReplInitSession() != 0: - stdio.printf("REPL: session init failed (temp dir/resident buffers/ctx)\n") - ReplJmpBuf = None - VLogger.clear_repl_jmpbuf() - if lg is not None: lg.set_jit_flag(False) - return 1 - - h_in: win32base.HANDLE = win32file.GetStdHandle(win32file.STD_INPUT_HANDLE) - line: bytes = stdlib.malloc(_REPL_LINE_CAP) - if line is None: - stdio.printf("REPL: line buffer allocation failed\n") - ReplJmpBuf = None - VLogger.clear_repl_jmpbuf() - if lg is not None: lg.set_jit_flag(False) - return 1 - - stdio.printf("ViperC REPL (M2/M3/M4) - session variables + session functions (cross-fragment) + ctx result echo;\n") - stdio.printf(" Supports expressions (1+2), definitions (x = 1 / x: int = 5), references (x + 1),\n") - stdio.printf(" augmented assignment (x += 2), address-of (p = &x), etc.; blank lines are skipped, Ctrl+Z or EOF exits\n") - stdio.printf(" Multi-line block: a trailing ':' enters continuation (prompt ...), a blank line ends it and runs the block once\n") - stdio.printf(" Cross-fragment functions: a top-level def in a block is lifted to the fragment module's export def (bare-name export),\n") - stdio.printf(" later fragments can call it directly (e.g. def f(x: int) -> int: newline + 4 spaces return x + 1);\n") - stdio.printf(" Restrictions: signature must be one line, async def is unsupported, raise in the body is unsupported, partial functions are unsupported\n") - stdio.printf(" Cross-fragment types (M4): a class in a block is translated into the session (type name %s.),\n", ReplSessionSha1) - stdio.printf(" later fragments can instantiate it, read/write fields and call methods; imported includes modules\n") - stdio.printf(" have their classes registered into the session too (when the .vpi stub is missing that module's classes are unavailable; the session continues)\n") - stdio.printf(" Fragment temp dir: %s\n", ReplTmpDir) - stdio.fflush(None) - - # === Session-level includes init (once; all failures only degrade includes classes) === - # The recovery point must land in this frame, so a fail-fast longjmp continues into the loop. - inc_mark: sizet = mb.mark() - rc_inc: int = _setjmp(jb, None) - if rc_inc != 0: - stdio.printf("REPL: includes init interrupted (%d modules registered, %d stubs): " "importing includes modules may fail; other fragments are unaffected\n", ReplIncMods, ReplIncVpiNew) - mb.reset_to(inc_mark) - else: - # Scan results/registry live in stdlib, so mb allocations can be reclaimed after this call, - # which also performs includes class registration (per-module _setjmp). - ReplInitIncludes(mb) - mb.reset_to(inc_mark) - # Re-arm the recovery point on run's own frame (not a helper: _setjmp's target belongs to the - # calling frame); this puts the finish step and read loop back under this frame's point. - rc_arm: int = _setjmp(jb, None) - if rc_arm != 0: - stdio.printf("REPL: includes class layout finish interrupted (see diagnostics above): layout registration may be incomplete; " "session continues\n") - else: - # Finish: converge classes whose parents became ready in later passes + per-class warnings. - # Done after re-arming so any longjmp lands in this frame (the dead-frame issue above). - VpiReader.ReplStubLayoutFinish() - # Disabling stub-layout recovery must be unconditional: a non-zero flag would make fragment - # translation take the REPL stub path (this function only sets a flag, no failure path). - VpiReader.ReplStubLayoutEnd() - - # === Host export symbols: locate the host exe import lib === - # If found, fragment link lines include it; otherwise warn and continue (calls may fail to link). - if ReplFindHostImplib() != 0: - stdio.printf("Host symbol import lib: %s (fragments can call includes functions/globals across modules)\n", ReplHostImplib) - else: - stdio.printf("Host symbol import lib: .dll.a not found; fragment calls to includes module-level " "functions will report undefined symbol at link time (the session does not terminate). Build the host with " "ViperC/project.vpj (which declares -Wl,--export-all-symbols), " "and keep the exe and .dll.a in the same dir\n") - stdio.fflush(None) - - # === Read loop: single-line fragments + block continuation === - # All init failures were absorbed, so the prompt is guaranteed to appear from here on. - stdio.printf("REPL: init complete, entering the read loop\n") - stdio.fflush(None) - # Block state is zeroed after every block end/failure/EOF/overflow, so a stale block cannot - # pollute the next line (the next append overwrites from index 0). - ReplInBlock = 0 - ReplBlockLen = 0 - while 1: - # Prompt: "... " while in a block, otherwise ">>> " (same print point and flush timing). - if ReplInBlock != 0: stdio.printf("... ") - else: stdio.printf(">>> ") - stdio.fflush(None) - n: int = _read_line(h_in, line, _REPL_LINE_CAP) - eof_hit: int = 0 - if n < 0: # EOF: treat as a terminating line, then exit - eof_hit = 1 - n = 0 - # Per line: snapshot watermarks, then _setjmp (arming this fragment's recovery point for the - # whole pipeline); any longjmp reclaims mark and continues. - mark: sizet = mb.mark() - rc: int = _setjmp(jb, None) - if rc != 0: - # longjmp return: diagnostics were already printed; clear block state, roll back session - # watermarks and reclaim this line's allocations, then print the next prompt. - ReplInBlock = 0 - ReplBlockLen = 0 - if ReplCtxObj is not None: - ReplCtxObj.Count = ReplSavedVarCount - ReplCtxObj.FuncCount = ReplSavedFuncCount - # M4: roll back the session baseline (DefineT/TypedefT) watermarks (same idea as above). - ReplBaselineTruncate(ReplSavedCDefineCount, ReplSavedCTypedefCount) - mb.reset_to(mark) - if eof_hit != 0: break - continue - if eof_hit != 0: - # EOF: while in a block with buffered content, execute the cached block first (like a - # blank line); otherwise take the existing exit path (print a newline then exit). - if ReplInBlock != 0 and ReplBlockLen > 0: _exec_repl_src(mb, ReplBlockBuf) - ReplInBlock = 0 - ReplBlockLen = 0 - mb.reset_to(mark) - stdio.printf("\n") - break - if ReplInBlock != 0: - if n == 0: - # Blank line ends continuation: run the whole block once; block errors only print. - if ReplBlockLen > 0: _exec_repl_src(mb, ReplBlockBuf) - ReplInBlock = 0 - ReplBlockLen = 0 - mb.reset_to(mark) - continue - # In continuation: every line is buffered; no state flips on ':' and no indent analysis. - if _block_append(line, int(n)) != 0: - stdio.printf("REPL: block input exceeds the block buffer cap of %d bytes: this block was discarded, " "returning to >>> (reduce the block size or run it in chunks)\n", _REPL_BLOCK_CAP) - ReplInBlock = 0 - ReplBlockLen = 0 - mb.reset_to(mark) - continue - # ==== Existing single-line path (unchanged when not in continuation) ==== - if n == 0: # blank line: skip - mb.reset_to(mark) - continue - if _line_starts_block(line, int(n)) != 0: - # Trailing ':' enters continuation: buffer this line without executing it. The first line - # cannot overflow (line cap < block cap), so append failure falls through below. - if _block_append(line, int(n)) == 0: - ReplInBlock = 1 - mb.reset_to(mark) - continue - _exec_repl_src(mb, line) - mb.reset_to(mark) - - stdlib.free(line) - # Clear the recovery point only at session end (never mid-session, so the pipeline stays protected). - ReplJmpBuf = None - VLogger.clear_repl_jmpbuf() - if lg is not None: lg.set_jit_flag(False) - return 0 +from stdint import * +import stdio +import stdlib +import string +import viperlib +import mlirlite +import memhub +import ast +import io +import os +import sys +import lib.core.VLogger as VLogger +import lib.core.Gen.GenApi as GenApi +import lib.core.Gen.GenTypes as GenTypes +import lib.core.Gen.Translator as HandlesTranslator +import lib.core.IncludesScanner as IncludesScanner +import lib.Phase.Phase1.Phase1Scan as Phase1Scan +import lib.Phase.BuildPipeline as BuildPipeline +import lib.Projectrans.Config as Config +import lib.Stub.StubMerger.StubMergerStore as StubMergerStore +import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect +import lib.Stub.StubGen.Converter as StubConverter +import lib.Stub.StubGen.VpiStub as VpiStub +import lib.Stub.StubGen.VpiReader as VpiReader + + +# Repl - interactive loop: wraps each fragment as an export def and JIT-compiles it to a DLL. +# Session variables/results are shared via ctx memory; errors recover via setjmp/longjmp. + +# Input line buffer capacity (incl. NUL terminator). +define _REPL_LINE_CAP = 4096 + +# Block input buffer capacity (session-level PermPool, incl. NUL): the raw user lines +# appended while in continuation mode, joined by '\n' with no trailing newline. +define _REPL_BLOCK_CAP = 8192 + +# Measured jmp_buf size on mingw-w64 x86_64: _JBLEN=16 times _JBTYPE (16B) = 256 bytes. +# Confirmed with a C probe (sizeof(jmp_buf) == 256). +define _REPL_JMPBUF_SIZE = 256 + +# setjmp/longjmp on mingw-w64 x86_64: _setjmp takes a frame pointer, so the setjmp +# macro expands to _setjmp(buf, NULL); passing None is fine for non-SEH use. +extern def _setjmp(buf: rawptr, frame: rawptr) -> int +extern def longjmp(buf: rawptr, val: int) + +# PermPool (injected by main): jmp_buf must outlive the session, so mb.reset_to must not reclaim it. +_perm_pool: memhub.MemBuddy* + + +# ============================================================ +# M2/M3 constants: wrapped fragment source capacity covers the single-line, block and lifted paths. +define _REPL_FRAG_SRC_CAP = 26624 +# Lifted module-level def source buffer capacity (incl. NUL). +define _REPL_LIFT_CAP = 12288 +# Max session functions lifted per fragment (<= GenTypes.REPL_FUNC_CAP; excess is an error). +define _REPL_LIFT_MAX = 16 +# Session function name length limit (ReplLiftNames slot width = limit + 1). +define _REPL_FN_NAME_MAX = 64 +# Session function name table capacity (_REPL_LIFT_MAX fixed-width slots). +define _REPL_FN_NAMES_CAP = 1024 +# Diagnostic display buffer (truncates non-NUL-terminated lines/names for %s). +define _REPL_DIAG_CAP = 128 +# Signature rendering buffer capacity ("(int, str) -> int" is well under 96). +define _REPL_SIG_TEXT_CAP = 192 +# Fragment symbol name / probe buffer capacity. +define _REPL_NAME_CAP = 64 +define _REPL_NEEDLE_CAP = 128 +# Temporary file path buffer capacity (directory/source paths). +define _REPL_PATH_CAP = 512 +# Full module IR text capacity (matches App/main.vp IR_BUF_SIZE). +define _REPL_IR_CAP = 262144 +# External command (clang++ -shared) line capacity. M4 grew it 1024 -> 16384 because the +# link line also carries prior fragments' import libs, else their symbols stay undefined. +define _REPL_CMD_CAP = 16384 +# includes source read buffer (matches Phase1/BuildPipeline SRC_BUF_SIZE). +define _REPL_SRC_BUF_CAP = 1048576 +# .vpi stub buffer (matches BuildPipeline.PYI_BUF_SIZE). +define _REPL_VPI_BUF_CAP = 262144 +# ---- M4: session sha1 / fragment import lib / session baseline (DefineT + TypedefT) ---- +# Max prior fragment import lib slots (a fragment takes one if its IR mentions a session symbol). +define _REPL_FRAG_IMPLIB_MAX = 128 +# Fragment import lib record array capacity (slots x 8-byte pointers). +define _REPL_FRAG_IMPLIB_ARR_CAP = 1024 +# Session baseline (DefineT) entry limit; replayed when each fragment rebuilds its table. +define _REPL_BASE_CDEFINE_MAX = 512 +# Session baseline (TypedefT) entry limit. +define _REPL_BASE_CTYPEDEF_MAX = 512 +# includes class registration (done once at session init): max modules injected via vpi. +define _REPL_INC_CLS_MOD_MAX = 512 +# Convergence passes for includes class registration (field types may reference peers). +define _REPL_INC_CLS_PASSES = 3 +# Struct table watermark guard for includes class registration (STRUCT_MAX = 512). +define _REPL_INC_CLS_STRUCT_GUARD = 448 +# Per-module warning line limit for includes class registration; the rest are only counted. +define _REPL_INC_CLS_WARN_MAX = 3 +# Startup time budget (ms) for includes class registration: multi-pass vpi injection is the +# main session-start cost, so on overrun we stop and let later fragments import on demand. +define _REPL_INC_CLS_BUDGET_MS = 20000 +# Temporary LLVM module name for includes class registration (.vpi injection target). +define _REPL_INC_MOD_NAME = "repl_inc_init" +# ctx session buffer size = header 64 + 64 var slots x 16 + 32 func slots x 16 = 1600. +# Must stay in sync with the GenTypes REPL_* layout constants. +define _REPL_CTX_SIZE = 1600 +# Template project file name (searched in cwd then exe dir when --project is absent). +define _REPL_PROJECT_FILE = "repl.vpj" +# cwd candidate suffix: os.getcwd() omits the trailing separator, so add '/'. +define _REPL_PROJECT_SEP_FILE = "/repl.vpj" +# Longest length of the two suffixes plus NUL headroom (candidate buffer check). +define _REPL_PROJECT_PROBE_CAP = 16 + + +# ============================================================ +# ReplFloatView - float view of the ctx header (F at offset 16 reinterpreted as double). +novirtual class ReplFloatView: + """Float view of the ctx header (result read as a double at +16).""" + Magic: int + Version: int + Kind: int + Reserved0: int + F: double + + +# ============================================================ +# ReplFuncSlotView - ctx function-pointer slot (16B: address + reserved), same on both sides. +novirtual class ReplFuncSlotView: + """ctx function-pointer slot (Addr = entry address in the defining fragment DLL).""" + Addr: VOIDPTR + Reserved: VOIDPTR + + +# ============================================================ +# ReplSigOut - parsed session function signature (host reads the fragment's model ABI). +novirtual class ReplSigOut: + """Parsed signature result (registration is rejected when Bad/EHTag is non-zero).""" + RetTag: int # GenTypes.REPL_TY_VOID / INT / FLOAT / STR + RetBits: int + ParamCount: int # excludes the leading ctx param + EHTag: int # 1=define has %__eh_msg_out__/%__eh_code_out__ (raise semantics) + Bad: int # 1=contains an unsupported type token + BadType: str # unsupported type string (points into IR text; for printing only) + + +# ============================================================ +# Template project lookup: tries cwd/repl.vpj then exe-dir/repl.vpj, writing diagnostics. +def ReplFindProject(Out: bytes, Cap: int, Diag: bytes, DiagCap: int) -> int: + """Locate repl.vpj in cwd then the exe directory: 1=found (writes Out), 0=miss (writes Diag).""" + if Out is None or Diag is None: return 0 + if Cap < 64 or DiagCap < 64: return 0 + # Separate buffers per candidate so both paths appear in the diagnostic text. + c1: bytes = stdlib.malloc(Cap) + c2: bytes = stdlib.malloc(Cap) + if c1 is None or c2 is None: + if c1 is not None: stdlib.free(c1) + if c2 is not None: stdlib.free(c2) + return 0 + c1[0] = 0 + c2[0] = 0 + # ① cwd/repl.vpj + cwd_path: str = os.getcwd() + if cwd_path is not None: + viperlib.snprintf(c1, Cap, "%s", cwd_path) + cl: int = int(string.strlen(c1)) + if cl + _REPL_PROJECT_PROBE_CAP <= Cap: + viperlib.snprintf(c1 + cl, Cap - cl, "%s", _REPL_PROJECT_SEP_FILE) + if _is_file(c1) != 0: + string.strcpy(Out, c1) + stdlib.free(c1) + stdlib.free(c2) + return 1 + else: string.strcpy(c1, "(os.getcwd failed)") + # 2) exe-dir/repl.vpj (the executable's own full path). + exe_path: str = sys.executable() + if exe_path is not None: + viperlib.snprintf(c2, Cap, "%s", exe_path) + el: int = int(string.strlen(c2)) + if el < Cap: + c2[el] = 0 + # Locate the last separator: c2[0..dlen) is the directory prefix (with separator); + # dlen=0 means no directory part, which the OS resolves against cwd. + dlen: int = 0 + si: int = el + while si > 0: + si -= 1 + ch: char = c2[si] + if ch == '/' or ch == '\\': + dlen = si + 1 + break + if dlen + _REPL_PROJECT_PROBE_CAP <= Cap: + viperlib.snprintf(c2 + dlen, Cap - dlen, "%s", _REPL_PROJECT_FILE) + if _is_file(c2) != 0: + string.strcpy(Out, c2) + stdlib.free(c1) + stdlib.free(c2) + return 1 + else: string.strcpy(c2, "(sys.executable failed)") + # Not found in either place: emit diagnostics (needs --project + both searched paths). + viperlib.snprintf(Diag, DiagCap, " Searched(1): %s\n" " Searched(2): %s\n" " --project needed: without repl.vpj the REPL cannot get the project config; " "specify it with --project \n" " Template location: ViperC/repl.vpj in the repo; copy it next to viperc.exe" "or into the current working directory to be found automatically\n", c1, c2) + stdlib.free(c1) + stdlib.free(c2) + return 0 + + +# ============================================================ +# Session state: permanent buffers in PermPool; per-fragment strings in mb; function-pointer slots. +ReplFragFn: callable[[rawptr], int] = None +# Fragment sequence number: module-level and monotonic, so names/files are never reused. +ReplFragNo: int = 1 +# Session temp dir %TEMP%\ViperC_repl\ (keeps user projects clean; pid isolates sessions). +ReplTmpDir: bytes = None +# Fragment symbol name __repl_frag_. +ReplNameBuf: bytes = None +# Wrapped fragment source (for parsing/translation). +ReplSrcBuf: bytes = None +# On-disk path of the wrapped source (real file for error echo). +ReplSrcPath: bytes = None +# Block input buffer (session PermPool, _REPL_BLOCK_CAP): user lines joined by '\n', +# allocated once rather than per line. +ReplBlockBuf: bytes = None +# Current block buffer byte count (0 = empty; NUL is always at ReplBlockBuf[ReplBlockLen]). +ReplBlockLen: int = 0 +# Continuation state: 1 = caching a multi-line block ("... " prompt); 0 = normal (">>> "). +ReplInBlock: int = 0 +# Full module IR text (dump_ir OUTPUT_FULL output buffer). +ReplIrBuf: bytes = None +# IR probe for the export signature "llvm.func @(%__repl_ctx__: !llvm.ptr) -> i32". +ReplNeedleBuf: bytes = None +# ctx session buffer (shared by host and fragment; byte view, passed as the fragment's first arg). +ReplCtxBuf: bytes = None +# Session context object (read by Gen: tables/caps/param name/perm pool/scratch entry). +ReplCtxObj: GenTypes.ReplCtx* = None +# Session variable table (PermPool, ReplCtxObj.Table). +ReplVarTable: GenTypes.ReplVarEntry* = None +# Scratch entry (PermPool): classifies bare-expression results only; no session slot or table entry. +ReplScratch: GenTypes.ReplVarEntry* = None +# Recovery-point jmp_buf (PermPool, lives for the session): run _setjmp's into it and +# VLogger.repl_abort longjmps back from any pipeline depth; re-registered per fragment. +ReplJmpBuf: rawptr = None +# Session table watermark (ReplCtxObj.Count) at fragment start; rolled back on failure to +# drop half-registered entries (repl_ensure_entry Count+1 but repl_seal errored). +ReplSavedVarCount: int = 0 + +# ==== includes session-init results (once per session; for summary and recovery branches) ==== +# Registered includes module count (StubMerger SHA1 table entries). +ReplIncMods: int = 0 +# includes scan time (ms). +ReplIncMs: int = 0 +# Newly generated .vpi stubs. +ReplIncVpiNew: int = 0 +# Reused existing .vpi stubs. +ReplIncVpiReuse: int = 0 +# .vpi generation failures (read/parse/write). +ReplIncVpiFail: int = 0 + +# ==== Host export symbols (cross-module calls) ==== +# Host exe import lib path (.dll.a; PermPool, probed once); fragment link lines use it. +ReplHostImplib: bytes = None + +# ==== M3 session functions (cross-fragment functions) ==== +# Lifted module-level def source (session PermPool; length cleared at each fragment start). +ReplLiftBuf: bytes = None +# Lifted session function name table (PermPool, _REPL_FN_NAME_MAX x _REPL_LIFT_MAX slots). +ReplLiftNames: bytes = None +# Number of session functions lifted by this fragment. +ReplLiftNameCount: int = 0 +# Signature token scratch (PermPool, GenTypes.REPL_FUNC_PARAM_MAX bytes each; reused per function). +ReplSigTags: bytes = None +ReplSigBits: bytes = None +# Signature result struct (PermPool, reused; named to avoid the ReplSigOut class). +ReplSigResult: ReplSigOut* = None +# Session function table watermark (ReplCtxObj.FuncCount) at fragment start; restored on +# failure to drop half-registered entries (same idea as ReplSavedVarCount, see ⓪). +ReplSavedFuncCount: int = 0 +# Diagnostic line/name display buffer (PermPool, _REPL_DIAG_CAP; %s cannot print non-NUL lines). +ReplLineShow: bytes = None +# def-header name scratch (PermPool, _REPL_FN_NAME_MAX). +ReplMsgName: bytes = None +# Signature render buffers x2 (PermPool, _REPL_SIG_TEXT_CAP): new vs existing entry. +ReplSigText: bytes = None +ReplSigText2: bytes = None +# Fragment body (wrapper) write position (_repl_split_body writes, _repl_build_frag_src_multi reads). +ReplWrapPos: int = 0 + +# ==== M4: session module sha1 (the cross-fragment unique key for type names) ==== +# 16-hex-char string (copy of GenTypes.REPL_SESSION_SHA1), used as every fragment's ModuleSha1. +ReplSessionSha1: bytes = None + +# ==== M4: prior fragment import libs (resolution source for cross-fragment method symbols) ==== +# Fragment link lines add prior fragments' import libs so their class/method symbols resolve. +ReplFragImplibs: sizet* = None +ReplFragImplibCount: int = 0 + +# ==== M4: session baseline (DefineT/TypedefT survive across fragments) ==== +# Replayed each fragment after clear_*; only successful fragments append; failures truncate. +ReplBaseCDefineNames: sizet* = None +ReplBaseCDefineValues: sizet* = None +ReplBaseCDefineKinds: sizet* = None # 8 bytes per entry: 0=int / 1=double / 2=str +ReplBaseCDefineCount: int = 0 +ReplBaseCTypedefNames: sizet* = None +ReplBaseCTypedefTypeStrs: sizet* = None +ReplBaseCTypedefCount: int = 0 +# Baseline watermark at fragment start (truncated back to it on failure/rollback). +ReplSavedCDefineCount: int = 0 +ReplSavedCTypedefCount: int = 0 + +# ==== M4: includes class registration (done once at session init) ==== +# Counts of modules whose .vpi injected successfully (classes in the session StructTable) / failed / missing stub. +ReplIncClsMods: int = 0 +ReplIncClsFail: int = 0 +ReplIncClsMissing: int = 0 + + +# ============================================================ +# Private helpers. + +# Whether path is an existing regular file. +def _is_file(path: str) -> int: + """1=exists and is not a directory; 0=missing, a directory, or bad args.""" + if path is None: return 0 + return 1 if os.isfile(path) else 0 + + +# ============================================================ +# Host export symbols: locate the host exe import lib so fragments resolve includes symbols. +def ReplFindHostImplib() -> int: + """Locate the host import lib (.dll.a): 1=found (writes ReplHostImplib), 0=not found.""" + global ReplHostImplib + if _perm_pool is None: return 0 + ReplHostImplib = None + exe_buf: bytes = _perm_pool.alloc(_REPL_PATH_CAP) + cand: bytes = _perm_pool.alloc(_REPL_PATH_CAP) + if exe_buf is None or cand is None: return 0 + exe_buf[0] = 0 + cand[0] = 0 + n_exe: int = 0 + exe_path: str = sys.executable() + if exe_path is not None: + viperlib.snprintf(exe_buf, _REPL_PATH_CAP, "%s", exe_path) + n_exe = int(string.strlen(exe_buf)) + if n_exe <= 0: return 0 + el: int = n_exe + # Defensive: on truncation the write may lack a NUL (same as ReplFindProject). + if el >= _REPL_PATH_CAP: el = _REPL_PATH_CAP - 1 + exe_buf[el] = 0 + # Split dir/file: dlen = directory prefix length (incl. separator). + dlen: int = 0 + si: int = el + while si > 0: + si -= 1 + ch: char = exe_buf[si] + if ch == '/' or ch == '\\': + dlen = si + 1 + break + # Candidate 1: exe dir (exe_buf already has the directory prefix). + viperlib.snprintf(cand, _REPL_PATH_CAP, "%s%s.dll.a", exe_buf, exe_buf + dlen) + if _is_file(cand) != 0: + ReplHostImplib = cand + return 1 + # Candidate 2: Config.OutputDir (REPL uses repl.vpj, host uses project.vpj). + if Config.OutputDir is not None: + viperlib.snprintf(cand, _REPL_PATH_CAP, "%s/%s.dll.a", Config.OutputDir, exe_buf + dlen) + if _is_file(cand) != 0: + ReplHostImplib = cand + return 1 + return 0 + + +# Read one line byte-by-byte: works for console and pipes. +def _read_line(buf: bytes, cap: int) -> int: + """Read one line from stdin into buf (NUL-terminated); returns bytes read, -1 on EOF/error.""" + if buf is None or cap < 2: return -1 + total: int = 0 + eof: int = 0 + while total < cap - 1: + got: sizet = stdio.read_fd(0, buf + total, 1) + if got == 0: + eof = 1 + break + # Do not use &(buf + total): its IR does an i32 load (4 bytes) so '\n'/'\r' + # never match; the byte index buf[total] is an i8 load. + ch: char = buf[total] + if ch == 10: # '\n': end of line + break + if ch == 13: # '\r': CRLF, drop and re-read in place + continue + total += 1 + buf[total] = 0 + if eof != 0 and total == 0: return -1 + return total + + +# Count top-level statements (Module.children). +def _count_top_stmts(tree: ast.AST*) -> int: + """Return the number of top-level statements in a Module (0 when children is empty).""" + if tree is None or tree.children is None: return 0 + stmts: list[ast.AST*]* = tree.children + return int(len(stmts)) + + +# ============================================================ +# Block continuation: a line ending in ':' opens a block, ended by a blank line or EOF. + +# Line test: after trimming trailing whitespace it ends with ':', or (M3) starts with '@'. +def _line_starts_block(line: str, n: int) -> int: + """1=line opens a block buffer (ends with ':' / starts with '@'); 0=no.""" + if line is None or n <= 0: return 0 + s: int = 0 + while s < n and (line[s] == ' ' or line[s] == '\t'): s += 1 + if s >= n: return 0 + if line[s] == '@': return 1 + i: int = int(n) + while i > 0: + ch: char = line[i - 1] + if ch != ' ' and ch != '\t': break + i -= 1 + if i <= 0: return 0 + if line[i - 1] == ':': return 1 + return 0 + + +# Append one line to the block buffer: join lines with '\n' and write a trailing NUL. +def _block_append(line: str, n: int) -> int: + """Append an n-byte line to the session block buffer; 0=ok, 1=out of capacity (buffer unchanged).""" + global ReplBlockLen + if ReplBlockBuf is None or line is None or n <= 0: return 1 + sep: int = 0 + if ReplBlockLen > 0: + sep = 1 # separator '\n' between lines + if ReplBlockLen + sep + n >= _REPL_BLOCK_CAP: # plus 1 byte for the trailing NUL + return 1 # capacity checked before writing, so no rollback + if ReplBlockLen > 0: + ReplBlockBuf[ReplBlockLen] = '\n' + ReplBlockLen += 1 + string.memcpy(ReplBlockBuf + ReplBlockLen, line, sizet(n)) + ReplBlockLen += n + ReplBlockBuf[ReplBlockLen] = 0 + return 0 + + +# ctx header view (int64 interpretation): host reads/writes magic/kind/result_a... +def _ctx_head() -> GenTypes.ReplResult*: + """ctx header view; None when the session is not initialized.""" + if ReplCtxBuf is None: return None + return GenTypes.ReplResult*(ReplCtxBuf) + + +# ctx header view (double interpretation of result_a): for echoing float results. +def _ctx_float_head() -> ReplFloatView*: + """Float view of the ctx header; None when the session is not initialized.""" + if ReplCtxBuf is None: return None + return ReplFloatView*(ReplCtxBuf) + + +# Result echo: read the ctx result area by result_kind and print it. +def _repl_echo_result(frag_name: str, res: GenTypes.ReplResult*) -> int: + """Echo this fragment's result by result_kind; 0=handled (including "no result").""" + if frag_name is None or res is None: return 1 + k: int = res.Kind + if k == GenTypes.REPL_RES_NONE: + # Declaration-only fragment (e.g. `x: int`): no result for this line. + return 0 + if k == GenTypes.REPL_RES_INT: + stdio.printf("%s => %lld\n", frag_name, res.A) + return 0 + if k == GenTypes.REPL_RES_FLOAT: + fv: ReplFloatView* = _ctx_float_head() + if fv is None: return 1 + stdio.printf("%s => %f\n", frag_name, fv.F) + return 0 + if k == GenTypes.REPL_RES_STR: + sp: rawptr = rawptr(res.A) + if sp is None: + stdio.printf("%s => \n", frag_name) + return 0 + stdio.printf("%s => %s\n", frag_name, sp) + return 0 + # REPL_RES_OBJ: result_a != 0 means a non-string pointer (print address); result_a == 0 + # means an inline struct value (no address; type name needs the cross-fragment table, M4). + if res.A == 0: + stdio.printf("%s => \n", frag_name) + return 0 + stdio.printf("%s => \n", frag_name, res.A) + return 0 + + +# ============================================================ +# M4: session sha1 / fragment import lib / session baseline / includes class registration. + +# Deep-copy a string into pool (caller supplies the pool; None on invalid input/alloc failure). +def _repl_str_dup(pool: memhub.MemBuddy*, s: str) -> str: + """Deep-copy s into pool; None on failure.""" + if pool is None or s is None: return None + n: int = int(string.strlen(s)) + b: str = pool.alloc(n + 1) + if b is None: return None + string.memcpy(b, s, sizet(n)) + b[n] = 0 + return b + + +# Build the session sha1 string (PermPool; used as ModuleSha1 during fragment translation). +def ReplInitSessionSha1() -> int: + """0=success; non-zero=permanent pool or allocation failure.""" + global ReplSessionSha1 + if _perm_pool is None: return 1 + ReplSessionSha1 = _repl_str_dup(_perm_pool, GenTypes.REPL_SESSION_SHA1) + if ReplSessionSha1 is None: return 1 + return 0 + + +# ---- Session baseline (DefineT/TypedefT): find / restore / append / truncate ---- + +def _repl_base_cdefine_find(name: str) -> int: + """Find a baseline DefineT entry index by name; -1 when not found.""" + if name is None or ReplBaseCDefineNames is None: return -1 + i: int + for i in range(int(ReplBaseCDefineCount)): + nm: str = rawptr(ReplBaseCDefineNames[i]) + if nm is not None and string.strcmp(nm, name) == 0: return i + return -1 + + +def ReplBaselineRestore(pool: memhub.MemBuddy*) -> int: + """Restore the session baseline (DefineT + TypedefT) into the just-cleared tables; 0=success.""" + if pool is None or ReplBaseCDefineNames is None: return 1 + if GenApi._g_gen is None: return 1 + i: int + for i in range(int(ReplBaseCDefineCount)): + nm: str = rawptr(ReplBaseCDefineNames[i]) + val: str = rawptr(ReplBaseCDefineValues[i]) + kind: int = int(ReplBaseCDefineKinds[i]) + if nm is None or val is None: continue + if kind == 0: GenApi.register_cdefine_constant(pool, nm, int64(string.atoll(val))) + elif kind == 1: GenApi.register_cdefine_double(pool, nm, string.atof(val)) + else: + # String DefineT: GenApi has no wrapper, so call gen directly (same shape as + # the clear_cdefine_constants wrapper). + GenApi._g_gen.TypeGenCDef.register_cdefine_string(pool, nm, val) + for i in range(int(ReplBaseCTypedefCount)): + tn: str = rawptr(ReplBaseCTypedefNames[i]) + tv: str = rawptr(ReplBaseCTypedefTypeStrs[i]) + if tn is None or tv is None: continue + GenApi.register_ctypedef(pool, tn, tv) + return 0 + + +def ReplBaselineCapture() -> int: + """Append baseline-missing DefineT/TypedefT entries from the current tables into the session baseline; 0=success.""" + global ReplBaseCDefineCount, ReplBaseCTypedefCount + if _perm_pool is None or GenApi._g_gen is None: return 1 + cdef_names: list[str]* = GenApi._g_gen.TypeGenCDef.CDefineNames + if cdef_names is not None: + cdef_vals: list[str]* = GenApi._g_gen.TypeGenCDef.CDefineValues + cdef_dbl: list[int]* = GenApi._g_gen.TypeGenCDef.CDefineDoubleFlags + cdef_str: list[int]* = GenApi._g_gen.TypeGenCDef.CDefineStrFlags + n: sizet = len(cdef_names) + i: sizet + for i in range(int(n)): + nm: str = cdef_names.get(i) + if nm is None: continue + if _repl_base_cdefine_find(nm) >= 0: continue + if ReplBaseCDefineCount >= _REPL_BASE_CDEFINE_MAX: break + val: str = None + if cdef_vals is not None: val = cdef_vals.get(i) + if val is None: continue + kind: int = 0 + if cdef_str is not None and int(cdef_str.get(i)) == 1: kind = 2 + # 浮点条目:1=f64 / 2=f32(见 TypeGenCDef 的 CDEFINE_FLOAT_KIND_*),非 0 即浮点 + elif cdef_dbl is not None and int(cdef_dbl.get(i)) != 0: kind = 1 + nc: str = _repl_str_dup(_perm_pool, nm) + vc: str = _repl_str_dup(_perm_pool, val) + if nc is None or vc is None: continue + j: int = ReplBaseCDefineCount + ReplBaseCDefineNames[j] = sizet(nc) + ReplBaseCDefineValues[j] = sizet(vc) + ReplBaseCDefineKinds[j] = sizet(kind) + ReplBaseCDefineCount = j + 1 + ct_names: list[str]* = GenApi._g_gen.TypeGenCType.CTypedefNames + if ct_names is not None: + ct_vals: list[str]* = GenApi._g_gen.TypeGenCType.CTypedefTypeStrs + cn2: sizet = len(ct_names) + i2: sizet + for i2 in range(int(cn2)): + tn: str = ct_names.get(i2) + if tn is None: continue + if ReplBaseCTypedefCount >= _REPL_BASE_CTYPEDEF_MAX: break + dup_hit: int = 0 + k: int + for k in range(int(ReplBaseCTypedefCount)): + ex: str = rawptr(ReplBaseCTypedefNames[k]) + if ex is not None and string.strcmp(ex, tn) == 0: + dup_hit = 1 + break + if dup_hit != 0: continue + tv: str = None + if ct_vals is not None: tv = ct_vals.get(i2) + if tv is None: continue + tnc: str = _repl_str_dup(_perm_pool, tn) + tvc: str = _repl_str_dup(_perm_pool, tv) + if tnc is None or tvc is None: continue + j2: int = ReplBaseCTypedefCount + ReplBaseCTypedefNames[j2] = sizet(tnc) + ReplBaseCTypedefTypeStrs[j2] = sizet(tvc) + ReplBaseCTypedefCount = j2 + 1 + return 0 + + +def ReplBaselineTruncate(cd_cnt: int, ct_cnt: int) -> None: + """Truncate the baseline watermarks back to the fragment-start values (failed deltas must not enter).""" + global ReplBaseCDefineCount, ReplBaseCTypedefCount + if cd_cnt >= 0 and cd_cnt <= ReplBaseCDefineCount: ReplBaseCDefineCount = cd_cnt + if ct_cnt >= 0 and ct_cnt <= ReplBaseCTypedefCount: ReplBaseCTypedefCount = ct_cnt + + +# ---- Fragment import lib: record / append to the link command line ---- + +def ReplFragImplibRecord(implib_path: str) -> int: + """Record a fragment import lib (PermPool deep copy); 1=recorded, 0=not (limit/allocation failure).""" + global ReplFragImplibCount + if implib_path is None or ReplFragImplibs is None or _perm_pool is None: return 0 + if ReplFragImplibCount >= _REPL_FRAG_IMPLIB_MAX: return 0 + dup: str = _repl_str_dup(_perm_pool, implib_path) + if dup is None: return 0 + ReplFragImplibs[ReplFragImplibCount] = sizet(dup) + ReplFragImplibCount += 1 + return 1 + + +# Append all recorded fragment import libs to the command buffer (space-separated); +# returns the new position (-1 = out of capacity, which the caller must treat as an error). +def _repl_cmd_append_implibs(cmd: bytes, cap: int, pos: int) -> int: + if cmd is None or ReplFragImplibs is None: return pos + i: int + for i in range(int(ReplFragImplibCount)): + p: str = rawptr(ReplFragImplibs[i]) + if p is None: continue + pos = _repl_cat_str(cmd, cap, int(pos), " ") + if pos < 0: return -1 + pos = _repl_cat_str(cmd, cap, int(pos), p) + if pos < 0: return -1 + return pos + + +# ---- includes class registration (once per session; per-module isolation, warnings only) ---- + +def ReplInitIncludesClasses(mb: memhub.MemBuddy*, sr: IncludesScanner.ScanResult*, vpi_dir: str) -> int: + """Inject each includes module's .vpi into a temp module so its classes enter the session StructTable; 0=done.""" + global ReplIncClsMods, ReplIncClsFail, ReplIncClsMissing + if mb is None or sr is None or vpi_dir is None or sr.Entries is None: return 1 + if ReplJmpBuf is None or _perm_pool is None: return 1 + n_mod: int = int(sr.Count) + if n_mod <= 0: return 1 + if n_mod > _REPL_INC_CLS_MOD_MAX: n_mod = _REPL_INC_CLS_MOD_MAX + # Per-module state (8 bytes/slot, PermPool): 0 = pending/retry, 1 = done (ok or unusable). + st: sizet* = _perm_pool.alloc(_REPL_INC_CLS_MOD_MAX * 8) + if st is None: return 1 + string.memset(st, 0, _REPL_INC_CLS_MOD_MAX * 8) + # Injection target: temporary LLVM module + translator (annotation context only); + # struct registration uses permanent allocation so entries/fields/types outlive fragments. + GenApi.set_stdlib_alloc(1) + GenApi.reset_visible_structs(mb, 0) + GenApi.set_temp_dir(vpi_dir) + GenApi.SetTempDir(vpi_dir) + GenApi.set_current_file_type(vpi_dir) + tmp_mod: mlirlite.LLVMModule* = mlirlite.new_module(_perm_pool, _REPL_INC_MOD_NAME) + tr_inc: HandlesTranslator.Translator* = _perm_pool.alloc(HandlesTranslator.Translator.__sizeof__()) + if tmp_mod is None or tr_inc is None: return 1 + tr_inc.__before_init__() + tr_inc.__init__() + # Give the temp translator real state (func/symbol tables, name buffers, Pool): VpiReader + # reads trans._funcs and trans.Pool/imported_modules while injecting, else it derefs None. + tr_inc._init_state(_perm_pool) + tr_inc.ModuleSha1 = ReplSessionSha1 + tr_inc._declare_only = 0 + # ReplCtx marks the translator as in a REPL session: annotations from .vpi may reference + # unregistered classes, so TypeGenResolve builds opaque placeholders instead of failing. + tr_inc.ReplCtx = ReplCtxObj + es: sizet = IncludesScanner.FileEntry.__sizeof__() + dl: sizet = string.strlen(vpi_dir) + warn_left: int = _REPL_INC_CLS_WARN_MAX + warn_dropped: int = 0 + budget_t0: uint64 = sys.monotonic_ms() + budget_hit: int = 0 + p_i: int + for p_i in range(int(_REPL_INC_CLS_PASSES)): + pass_ok: int = 0 + i: int + for i in range(int(n_mod)): + if int(st[i]) != 0: continue + # Startup budget (one time check per module; sys.monotonic_ms is cheap): on overrun + # stop, leaving the remaining modules to the on-demand import path. + if int(sys.monotonic_ms() - budget_t0) > _REPL_INC_CLS_BUDGET_MS: + budget_hit = 1 + break + if GenApi.get_struct_count() >= _REPL_INC_CLS_STRUCT_GUARD: + stdio.printf("includes class registration: struct table reached %d entries (near the limit); " "stopping registration; unregistered classes are unavailable in the session (importing their module " "can still register on demand)\n", GenApi.get_struct_count()) + stdio.fflush(None) + return 0 + ent: IncludesScanner.FileEntry* = IncludesScanner.FileEntry*(uint64(sr.Entries) + uint64(i) * uint64(es)) + if ent is None or ent.Sha1 is None: + st[i] = sizet(1) + ReplIncClsFail += 1 + continue + vm: sizet = mb.mark() + vpi_path: str = StubMergerCollect._sliced_path(vpi_dir, dl, ent.Sha1, "vpi") + if vpi_path is None: + st[i] = sizet(1) + ReplIncClsFail += 1 + mb.reset_to(vm) + continue + if _is_file(vpi_path) == 0: + # Missing stub (host never built / includes dir lacks this module): its classes + # are unavailable, but the session continues (import degrades the same way). + ReplIncClsMissing += 1 + if warn_left > 0: + stdio.printf("includes class registration: module %s has no .vpi stub -- its classes are " "unavailable in the session (other modules unaffected; build the host once with project.vpj " "to generate all stubs)\n", ent.Sha1) + warn_left -= 1 + else: warn_dropped += 1 + stdlib.free(vpi_path) + mb.reset_to(vm) + st[i] = sizet(1) + continue + # Set trans.ModuleSha1 to the current stub's source module sha1 (like batch), so bare + # class names and parent lookups resolve correctly during injection. + tr_inc.ModuleSha1 = ent.Sha1 + # Per-module recovery point: a fatal only discards this module's injection; the struct + # table dedups by name+sha1, and only mb.reset_to(vm) is done on the fatal path. + rc_m: int = _setjmp(ReplJmpBuf, None) + if rc_m != 0: + ReplIncClsFail += 1 + if warn_left > 0: + stdio.printf("includes class registration: injection of module %d's .vpi interrupted " "(its classes may be incomplete; session continues)\n", i) + warn_left -= 1 + else: warn_dropped += 1 + mb.reset_to(vm) + continue + ok_m: int = VpiReader.ParseVpiIntoModule(_perm_pool, tmp_mod, vpi_path, tr_inc) + if ok_m != 0: + st[i] = sizet(1) + ReplIncClsMods += 1 + pass_ok += 1 + else: + ReplIncClsFail += 1 + if warn_left > 0: + stdio.printf("includes class registration: injection of module %s's .vpi failed " "(that module's classes are incomplete/unavailable; session continues)\n", ent.Sha1) + warn_left -= 1 + else: warn_dropped += 1 + stdlib.free(vpi_path) + mb.reset_to(vm) + # No module succeeded this pass, so further passes add nothing; stop early. + if pass_ok == 0: break + # Summary (aggregated warnings): per-module warnings were capped, so print full counts. + skip_cnt: int = 0 + si: int + for si in range(int(n_mod)): + if int(st[si]) == 0: skip_cnt += 1 + stdio.printf("includes classes: registered vpi classes for %d modules (failed %d, missing stub %d, " "not attempted/to retry %d, warnings dropped %d)\n", ReplIncClsMods, ReplIncClsFail, ReplIncClsMissing, skip_cnt, warn_dropped) + if budget_hit != 0: + stdio.printf("includes classes: startup budget of %d ms exhausted, stopped startup registration (classes of the remaining %d " "modules are unavailable in the session; a fragment that `import`s such a module still registers on demand)\n", _REPL_INC_CLS_BUDGET_MS, skip_cnt) + stdio.fflush(None) + return 0 + + +# ============================================================ +# Session init: create %TEMP%\ViperC_repl\\ and allocate permanent buffers + ctx. +def ReplInitSession() -> int: + """Create the temp dir plus PermPool resident buffers and ctx session memory; 0=success.""" + global ReplTmpDir, ReplNameBuf, ReplSrcBuf, ReplSrcPath, ReplIrBuf, ReplNeedleBuf + global ReplCtxBuf, ReplCtxObj, ReplVarTable, ReplScratch + global ReplBlockBuf, ReplBlockLen, ReplInBlock + global ReplLiftBuf, ReplLiftNames, ReplLiftNameCount, ReplSigTags, ReplSigBits + global ReplSigResult, ReplLineShow, ReplMsgName + global ReplSigText, ReplSigText2, ReplWrapPos, ReplSavedFuncCount + global ReplSessionSha1, ReplFragImplibs, ReplFragImplibCount + global ReplBaseCDefineNames, ReplBaseCDefineValues, ReplBaseCDefineKinds + global ReplBaseCDefineCount, ReplBaseCTypedefNames, ReplBaseCTypedefTypeStrs + global ReplBaseCTypedefCount, ReplSavedCDefineCount, ReplSavedCTypedefCount + global ReplIncClsMods, ReplIncClsFail, ReplIncClsMissing + if _perm_pool is None: return 1 + # 1) %TEMP% prefix (no trailing separator; None means unavailable). + tmp_dir: str = os.getenv("TEMP") + if tmp_dir is None: return 1 + # 2) Session dir /ViperC_repl/ (pid isolates sessions; paths use '/'). + ReplTmpDir = _perm_pool.alloc(_REPL_PATH_CAP) + if ReplTmpDir is None: return 1 + viperlib.snprintf(ReplTmpDir, _REPL_PATH_CAP, "%s/ViperC_repl/%d", tmp_dir, sys.get_pid()) + if BuildPipeline.ensure_dir(ReplTmpDir) != 0: return 1 + # 3) Permanent buffers (live for the session, not re-allocated per fragment). + ReplNameBuf = _perm_pool.alloc(_REPL_NAME_CAP) + ReplSrcBuf = _perm_pool.alloc(_REPL_FRAG_SRC_CAP) + ReplSrcPath = _perm_pool.alloc(_REPL_PATH_CAP) + ReplIrBuf = _perm_pool.alloc(_REPL_IR_CAP) + ReplNeedleBuf = _perm_pool.alloc(_REPL_NEEDLE_CAP) + ReplBlockBuf = _perm_pool.alloc(_REPL_BLOCK_CAP) + ReplBlockLen = 0 + ReplInBlock = 0 + if ReplNameBuf is None or ReplSrcBuf is None or ReplSrcPath is None: return 1 + if ReplIrBuf is None or ReplNeedleBuf is None: return 1 + if ReplBlockBuf is None: return 1 + ReplBlockBuf[0] = 0 + # 3') M3 permanent buffers: lifted source / name table / sig token scratch / render buffers. + ReplLiftBuf = _perm_pool.alloc(_REPL_LIFT_CAP) + ReplLiftNames = _perm_pool.alloc(_REPL_FN_NAMES_CAP) + ReplSigTags = _perm_pool.alloc(GenTypes.REPL_FUNC_PARAM_MAX) + ReplSigBits = _perm_pool.alloc(GenTypes.REPL_FUNC_PARAM_MAX) + ReplSigResult = _perm_pool.alloc(ReplSigOut.__sizeof__()) + ReplLineShow = _perm_pool.alloc(_REPL_DIAG_CAP) + ReplMsgName = _perm_pool.alloc(_REPL_FN_NAME_MAX) + ReplSigText = _perm_pool.alloc(_REPL_SIG_TEXT_CAP) + ReplSigText2 = _perm_pool.alloc(_REPL_SIG_TEXT_CAP) + if ReplLiftBuf is None or ReplLiftNames is None or ReplSigTags is None or ReplSigBits is None or ReplSigResult is None: return 1 + if ReplLineShow is None or ReplMsgName is None or ReplSigText is None or ReplSigText2 is None: return 1 + ReplLiftBuf[0] = 0 + ReplLiftNames[0] = 0 + ReplLiftNameCount = 0 + ReplWrapPos = 0 + ReplSavedFuncCount = 0 + # 4) ctx session memory (fragment's first arg): 64B header + 64x16B var slots + 32x16B func slots, zeroed. + ReplCtxBuf = _perm_pool.alloc(_REPL_CTX_SIZE) + if ReplCtxBuf is None: return 1 + string.memset(ReplCtxBuf, 0, _REPL_CTX_SIZE) + # 5) Session variable table + scratch entry (PermPool; names deep-copied by Gen into the same pool). + entry_size: sizet = GenTypes.ReplVarEntry.__sizeof__() + ReplVarTable = _perm_pool.alloc(entry_size * GenTypes.REPL_VAR_CAP) + if ReplVarTable is None: return 1 + string.memset(ReplVarTable, 0, entry_size * GenTypes.REPL_VAR_CAP) + ReplScratch = _perm_pool.alloc(entry_size) + if ReplScratch is None: return 1 + string.memset(ReplScratch, 0, entry_size) + # Scratch entry name (readable in error messages: ""). + repl_scratch_name: str = _perm_pool.alloc(8) + if repl_scratch_name is None: return 1 + viperlib.snprintf(repl_scratch_name, 8, "%s", "") + ReplScratch.Name = repl_scratch_name + ReplScratch.TyName = None + ReplScratch.Offset = 0 + ReplScratch.Kind = GenTypes.REPL_KIND_UNTYPED + ReplScratch.TyTag = 0 + ReplScratch.Bits = 0 + # 5') Session function table (M3; PermPool, host writes, Gen reads only) - entry array zeroed. + func_entry_size: sizet = GenTypes.ReplFuncEntry.__sizeof__() + ReplFuncTable: GenTypes.ReplFuncEntry* = _perm_pool.alloc(func_entry_size * GenTypes.REPL_FUNC_CAP) + if ReplFuncTable is None: return 1 + string.memset(ReplFuncTable, 0, func_entry_size * GenTypes.REPL_FUNC_CAP) + # 6) Session context object (the Gen-side switch: trans.ReplCtx). + ReplCtxObj = _perm_pool.alloc(GenTypes.ReplCtx.__sizeof__()) + if ReplCtxObj is None: return 1 + string.memset(ReplCtxObj, 0, GenTypes.ReplCtx.__sizeof__()) + ctx_param: str = _perm_pool.alloc(_REPL_NAME_CAP) + if ctx_param is None: return 1 + viperlib.snprintf(ctx_param, _REPL_NAME_CAP, "%s", GenTypes.REPL_CTX_PARAM) + ReplCtxObj.Marker = GenTypes.REPL_CTX_MARKER + ReplCtxObj.Table = ReplVarTable + ReplCtxObj.Count = 0 + ReplCtxObj.Cap = GenTypes.REPL_VAR_CAP + ReplCtxObj.CtxParam = ctx_param + ReplCtxObj.SlotSize = GenTypes.REPL_SLOT_SIZE + ReplCtxObj.Scratch = ReplScratch + ReplCtxObj.Perm = _perm_pool + # Session function table (M3): slices survive across fragments, so table/names/tokens are in PermPool. + ReplCtxObj.FuncTable = ReplFuncTable + ReplCtxObj.FuncCount = 0 + ReplCtxObj.FuncCap = GenTypes.REPL_FUNC_CAP + ReplSavedFuncCount = 0 + # 7) ctx fixed header: magic/version written once by the host (fragments read-only, no validation). + head: GenTypes.ReplResult* = _ctx_head() + if head is None: return 1 + head.Magic = GenTypes.REPL_CTX_MAGIC + head.Version = GenTypes.REPL_CTX_VERSION + head.Kind = GenTypes.REPL_RES_NONE + head.Reserved0 = 0 + head.A = 0 + head.B = 0 + head.VarCount = 0 + head.LastFrag = 0 + # 8) M4 permanent state: session sha1 + fragment import lib table + session baseline (DefineT/TypedefT). + if ReplInitSessionSha1() != 0: return 1 + ReplFragImplibs = _perm_pool.alloc(_REPL_FRAG_IMPLIB_ARR_CAP) + ReplBaseCDefineNames = _perm_pool.alloc(8 * _REPL_BASE_CDEFINE_MAX) + ReplBaseCDefineValues = _perm_pool.alloc(8 * _REPL_BASE_CDEFINE_MAX) + ReplBaseCDefineKinds = _perm_pool.alloc(8 * _REPL_BASE_CDEFINE_MAX) + ReplBaseCTypedefNames = _perm_pool.alloc(8 * _REPL_BASE_CTYPEDEF_MAX) + ReplBaseCTypedefTypeStrs = _perm_pool.alloc(8 * _REPL_BASE_CTYPEDEF_MAX) + if ReplFragImplibs is None or ReplBaseCDefineNames is None or ReplBaseCDefineValues is None or ReplBaseCDefineKinds is None: return 1 + if ReplBaseCTypedefNames is None or ReplBaseCTypedefTypeStrs is None: return 1 + string.memset(ReplFragImplibs, 0, _REPL_FRAG_IMPLIB_ARR_CAP) + string.memset(ReplBaseCDefineNames, 0, 8 * _REPL_BASE_CDEFINE_MAX) + string.memset(ReplBaseCDefineValues, 0, 8 * _REPL_BASE_CDEFINE_MAX) + string.memset(ReplBaseCDefineKinds, 0, 8 * _REPL_BASE_CDEFINE_MAX) + string.memset(ReplBaseCTypedefNames, 0, 8 * _REPL_BASE_CTYPEDEF_MAX) + string.memset(ReplBaseCTypedefTypeStrs, 0, 8 * _REPL_BASE_CTYPEDEF_MAX) + ReplFragImplibCount = 0 + ReplBaseCDefineCount = 0 + ReplBaseCTypedefCount = 0 + ReplSavedCDefineCount = 0 + ReplSavedCTypedefCount = 0 + ReplIncClsMods = 0 + ReplIncClsFail = 0 + ReplIncClsMissing = 0 + return 0 + + +# ============================================================ +# includes session init: scan dir -> register "module -> SHA1" -> pre-generate .vpi stubs. +def _repl_gen_includes_vpi(mb: memhub.MemBuddy*, sr: IncludesScanner.ScanResult*, vpi_dir: str) -> int: + """Pre-generate .vpi stubs for the includes scan; returns the count of new stubs (-1=bad args).""" + global ReplIncVpiNew, ReplIncVpiReuse, ReplIncVpiFail + if mb is None or sr is None or vpi_dir is None: return -1 + if sr.Entries is None: return -1 + es: sizet = IncludesScanner.FileEntry.__sizeof__() + dl: sizet = string.strlen(vpi_dir) + n: int + for n in range(int(sr.Count)): + ent: IncludesScanner.FileEntry* = IncludesScanner.FileEntry*(uint64(sr.Entries) + uint64(n) * es) + if ent is None or ent.Sha1 is None or ent.Path is None: + ReplIncVpiFail += 1 + continue + fm: sizet = mb.mark() + vpi_path: str = StubMergerCollect._sliced_path(vpi_dir, dl, ent.Sha1, "vpi") + if vpi_path is None: + ReplIncVpiFail += 1 + mb.reset_to(fm) + continue + # Existing stub -> reuse. + vf: io.File* = io.File(vpi_path, io.MODE.R) + if not vf.closed: + vf.close() + VpiStub.Ensure(vpi_path, ent.Sha1) + stdlib.free(vpi_path) + ReplIncVpiReuse += 1 + mb.reset_to(fm) + continue + # Read source file (1MB cap matching Phase1/BuildPipeline SRC_BUF_SIZE). + f: io.File* = io.File(ent.Path, io.MODE.R) + if f.closed: + stdlib.free(vpi_path) + ReplIncVpiFail += 1 + mb.reset_to(fm) + continue + if f.size() >= _REPL_SRC_BUF_CAP: + f.close() + stdlib.free(vpi_path) + ReplIncVpiFail += 1 + mb.reset_to(fm) + continue + src_buf: bytes = stdlib.malloc(_REPL_SRC_BUF_CAP) + if src_buf is None: + f.close() + stdlib.free(vpi_path) + ReplIncVpiFail += 1 + mb.reset_to(fm) + continue + bytes_read: LONG = f.read_all(src_buf, _REPL_SRC_BUF_CAP) + f.close() + if bytes_read <= 0: + stdlib.free(src_buf) + stdlib.free(vpi_path) + ReplIncVpiFail += 1 + mb.reset_to(fm) + continue + if bytes_read < _REPL_SRC_BUF_CAP: src_buf[bytes_read] = 0 + # Parse (all AST allocations stay within this file's mb window). + lx: ast.Lexer* = ast.new_lexer(mb) + tree: ast.AST* = None + if lx is not None: + ast._lexer_init(lx, src_buf, mb) + tokens: ast.Token* = ast.tokenize(lx) + tree = ast.parse_tokens(mb, tokens) + if tree is None: + stdlib.free(src_buf) + stdlib.free(vpi_path) + ReplIncVpiFail += 1 + mb.reset_to(fm) + continue + pyi_buf: bytes = stdlib.malloc(_REPL_VPI_BUF_CAP) + if pyi_buf is None: + stdlib.free(src_buf) + stdlib.free(vpi_path) + ReplIncVpiFail += 1 + mb.reset_to(fm) + continue + # Pass the absolute ent.Path as rel_path: vpi fatal_error reads source lines via it. + pos: sizet = StubConverter._GeneratePyiFromAst(mb, tree, ent.Path, pyi_buf, _REPL_VPI_BUF_CAP) + if pos > 0: + pf: io.File* = io.File(vpi_path, io.MODE.W) + if not pf.closed: + pf.write(pyi_buf, pos) + pf.close() + VpiStub.Emit(vpi_path, pyi_buf, pos, ent.Sha1) + ReplIncVpiNew += 1 + else: ReplIncVpiFail += 1 + else: ReplIncVpiFail += 1 + stdlib.free(pyi_buf) + stdlib.free(src_buf) + stdlib.free(vpi_path) + mb.reset_to(fm) + return ReplIncVpiNew + + +def ReplInitIncludes(mb: memhub.MemBuddy*) -> int: + """Session includes init: scan, register SHA1, pre-generate .vpi, register generic templates, then per-module classes; 0=success.""" + global ReplIncMods, ReplIncMs + if mb is None: return 1 + inc_dirs: str = Config.IncludesDirs + if inc_dirs is None or string.strlen(inc_dirs) == 0: + stdio.printf("REPL: project has no includes dir configured (project.vpj's includes); " "skipping includes init; importing includes modules will report an unregistered SHA1\n") + return 1 + # .vpi dir matches the fragment side: prefer the project temp dir, else the session dir. + vpi_dir: str = Config.TempDir + if vpi_dir is None: vpi_dir = ReplTmpDir + if vpi_dir is None: + stdio.printf("REPL: no usable .vpi dir (both Config.TempDir and the session dir are empty); " "skipping includes init\n") + return 1 + # 1) Scan includes (no reachability: REPL has no SourceDir/deps graph, so scan all). + t0: uint64 = sys.monotonic_ms() + sr: IncludesScanner.ScanResult* = IncludesScanner.scan_includes(mb, inc_dirs) + if sr is None: + stdio.printf("REPL: includes scan failed (dir: %s); " "importing includes modules will report an unregistered SHA1\n", inc_dirs) + return 1 + # 2) Register "module -> SHA1" (same StubMerger global table batch consumers use). + n: int = StubMergerStore.PopulateSha1MapStore(sr) + ReplIncMs = int(sys.monotonic_ms() - t0) + if n <= 0: + stdio.printf("REPL: includes scan registered no modules (dir: %s): " "check the includes path and the .vp files\n", inc_dirs) + return 1 + ReplIncMods = n + # 3) Pre-generate .vpi stubs (count failures, do not abort the session). + _repl_gen_includes_vpi(mb, sr, vpi_dir) + stdio.printf("includes: registered %d modules (scan took %d ms); " "stubs new %d / reused %d / failed %d\n", n, ReplIncMs, ReplIncVpiNew, ReplIncVpiReuse, ReplIncVpiFail) + # 3'') Register session generic templates for real: reuse includes sources to register + # generic class templates (list/dict/Vector...) so annotations like list[...] specialize. + cur_file_saved: str = GenApi.get_current_file() + n_gen_tpl: int = Phase1Scan._PreRegisterAllGenericTemplates(mb, sr) + if cur_file_saved is not None: GenApi.set_current_file_type(cur_file_saved) + if n_gen_tpl <= 0: + stdio.printf("includes generic templates: 0 -- list[...]/dict[...] in annotations can only be parsed as " "\"type-only\" placeholders (element layout and methods unavailable)\n") + # 3') M4.2: enable stub-layout recovery so vpi injection verifies and registers includes + # class layouts/vtable indices; also reset the generic-owner table for re-registration. + StubMergerStore.ResetGenericOwners() + VpiReader.ReplStubLayoutBegin(vpi_dir) + # 4) M4 class registration: inject each module's .vpi so its classes (with fields) enter the + # session StructTable, a prerequisite for cross-module construction/method calls. + ReplInitIncludesClasses(mb, sr, vpi_dir) + return 0 + + +# ============================================================ +# M2/M3: fragment execution path (stmt -> IR -> llc -> -shared DLL -> call -> ctx echo). + +# M3 top-level def lifting: lift a top-level def into the fragment module as an export def, +# so the host can resolve it and later fragments call it through a ctx slot. + +# Truncate a line (possibly non-NUL-terminated) into the session buffer for %s printing. +def _repl_show_line(p: bytes, n: int) -> str: + """Copy a line fragment into a session buffer as a NUL-terminated string (truncated to 118 bytes).""" + if ReplLineShow is None or p is None: return "(?)" + m: int = int(n) + if m > 118: m = 118 + if m < 0: m = 0 + string.memcpy(ReplLineShow, p, sizet(m)) + ReplLineShow[m] = 0 + return ReplLineShow + + +# Line length (up to '\n' or NUL). +def _repl_line_len(p: bytes) -> int: + n: int = 0 + while p[n] != '\n' and p[n] != 0: n += 1 + return n + + +# Count leading whitespace bytes. +def _repl_indent_len(p: bytes, n: int) -> int: + i: int = 0 + while i < n and (p[i] == ' ' or p[i] == '\t'): i += 1 + return i + + +# Whether [ind, n) starts with keyword kw (kw followed by whitespace). +def _repl_line_has_kw(p: bytes, n: int, ind: int, kw: str) -> int: + k: int = int(string.strlen(kw)) + if ind + k + 1 > n: return 0 + if string.strncmp(p + ind, kw, sizet(k)) != 0: return 0 + c: char = p[ind + k] + if c != ' ' and c != '\t': return 0 + return 1 + + +# Append a byte span to dst (keeping the trailing NUL); returns new length, -1 = out of capacity. +def _repl_cat(dst: bytes, cap: int, pos: int, src: bytes, n: int) -> int: + if dst is None or src is None or pos < 0 or n < 0: return -1 + if pos + n + 1 > cap: return -1 + string.memcpy(dst + pos, src, sizet(n)) + pos += n + dst[pos] = 0 + return pos + + +# Append a NUL-terminated string to dst; returns new length, -1 = out of capacity. +def _repl_cat_str(dst: bytes, cap: int, pos: int, s: str) -> int: + if s is None: return pos + return _repl_cat(dst, cap, int(pos), s, int(string.strlen(s))) + + +# Record a lifted function name (reserved-prefix check + same-fragment dedup + limit); 0=ok, 1=reject. +def _repl_lift_add_name(name: str) -> int: + global ReplLiftNameCount + if name is None or ReplLiftNames is None: return 1 + # Reserved prefix: the fragment entry name ("__repl_frag_") marks the fragment entry + # scope in Gen (session writes only work there), so users may not use it. + pfx: str = GenTypes.REPL_ENTRY_PREFIX + pl: int = int(string.strlen(pfx)) + if string.strncmp(name, pfx, sizet(pl)) == 0: + stdio.printf("M3: session function name cannot start with the reserved prefix '%s': %s\n", pfx, name) + return 1 + i: int + for i in range(int(ReplLiftNameCount)): + if string.strcmp(ReplLiftNames + i * _REPL_FN_NAME_MAX, name) == 0: + stdio.printf("M3: session function '%s' defined twice in the same fragment (define it only once)\n", name) + return 1 + if ReplLiftNameCount >= _REPL_LIFT_MAX: + stdio.printf("M3: a fragment can lift at most %d session functions (this fragment lifted %d)\n", _REPL_LIFT_MAX, ReplLiftNameCount) + return 1 + string.strcpy(ReplLiftNames + ReplLiftNameCount * _REPL_FN_NAME_MAX, name) + ReplLiftNameCount += 1 + return 0 + + +# Parse and rewrite one top-level def header into ReplLiftBuf as an export def with an +# injected ctx param; name written to name_out. Returns 0=ok/1=rejected/2=buffer full. +def _repl_lift_def_header(line: bytes, n: int, name_out: bytes, name_cap: int) -> int: + if n < 5 or name_out is None or ReplLiftBuf is None: return 1 + p: int = 3 # skip "def" + while p < n and (line[p] == ' ' or line[p] == '\t'): p += 1 + # Name (identifier characters). + ns: int = p + while p < n: + c: char = line[p] + is_id: int = 0 + if (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9') or c == '_': is_id = 1 + if is_id == 0: break + p += 1 + nlen: int = p - ns + if nlen <= 0: + stdio.printf("M3: def header is missing the function name: %s\n", _repl_show_line(line, int(n))) + return 1 + if nlen >= name_cap: + stdio.printf("M3: session function name too long (limit %d bytes): %s\n", name_cap - 1, _repl_show_line(line, int(n))) + return 1 + string.memcpy(name_out, line + ns, sizet(nlen)) + name_out[nlen] = 0 + # PEP 695 generic params [T]: skip balanced brackets (translation maps T to int). + if p < n and line[p] == '[': + d: int = 0 + while p < n: + if line[p] == '[': d += 1 + elif line[p] == ']': + d -= 1 + if d == 0: + p += 1 + break + p += 1 + if d != 0: + stdio.printf("M3: unclosed generic-parameter bracket in def header: %s\n", _repl_show_line(line, int(n))) + return 1 + if p >= n or line[p] != '(': + stdio.printf("M3: def header is missing '(' after the function name: %s\n", _repl_show_line(line, int(n))) + return 1 + lp: int = p + # Matching ')'. + d2: int = 0 + rp: int = -1 + q: int = lp + while q < n: + c2: char = line[q] + if c2 == '(': d2 += 1 + elif c2 == ')': + d2 -= 1 + if d2 == 0: + rp = q + break + q += 1 + if rp < 0: + stdio.printf("M3: def header is missing ')' (the signature must be on one line): %s\n", _repl_show_line(line, int(n))) + return 1 + # ':' must be on the same line (as must the return annotation). + has_colon: int = 0 + q2: int = rp + 1 + while q2 < n: + if line[q2] == ':': + has_colon = 1 + break + q2 += 1 + if has_colon == 0: + stdio.printf("M3: def header ':' is not on the same line (the signature must be on one line): %s\n", _repl_show_line(line, int(n))) + return 1 + # Whether the parameter list is empty. + empty: int = 1 + q3: int = lp + 1 + while q3 < rp: + if line[q3] != ' ' and line[q3] != '\t': + empty = 0 + break + q3 += 1 + # Reject a user-supplied __repl_ctx__ param (would duplicate the injected first param). + if empty == 0: + seg: int = lp + 1 + while seg + 12 <= rp: + if line[seg] == '_' and string.strncmp(line + seg, GenTypes.REPL_CTX_PARAM, sizet(12)) == 0: + stdio.printf("M3: session function '%s' cannot declare the %s parameter " "(it is auto-injected as the first parameter when lifting)\n", name_out, GenTypes.REPL_CTX_PARAM) + return 1 + seg += 1 + # Emit: "export " + original header [0,lp] + injection + original header [lp+1,n) + '\n'. + pos: int = int(string.strlen(ReplLiftBuf)) + pos = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), "export ") + pos = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), line, lp + 1) + if empty != 0: pos = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), "__repl_ctx__: bytes") + else: pos = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), "__repl_ctx__: bytes, ") + pos = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), line + lp + 1, n - lp - 1) + pos = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, int(pos), "\n") + if pos < 0: + stdio.printf("M3: lift source buffer too small (%d bytes)\n", _REPL_LIFT_CAP) + return 2 + return 0 + + +# Split the fragment body: lifted blocks -> ReplLiftBuf (export def, unindented); other lines +# -> ReplSrcBuf (4 spaces each); records lifted names. 0=ok/1=rejected/2=buffer full. +def _repl_split_body(body: bytes) -> int: + global ReplWrapPos + if body is None or ReplLiftBuf is None or ReplSrcBuf is None: return 2 + total: int = int(string.strlen(body)) + i: int = 0 + pos: int = ReplWrapPos + in_lift: int = 0 # 1 = inside a lifted block + need_header: int = 0 # saw a decorator line, no def header yet + in_body: int = 0 # past the def header (indented lines are the body; check raise) + name_buf: bytes = ReplMsgName + while i < total: + n: int = int(_repl_line_len(body + i)) + ind: int = int(_repl_indent_len(body + i, int(n))) + blank: int = 0 + if ind >= n: blank = 1 + if in_lift != 0: + if blank != 0: + # Blank line: stays in the lift block (blank body lines are indentation-neutral). + lp0: int = int(string.strlen(ReplLiftBuf)) + lp0 = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, lp0, body + i, int(n)) + lp0 = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, lp0, "\n") + if lp0 < 0: return 2 + i += n + 1 + continue + if ind > 0: + if need_header != 0: + stdio.printf("M3: a def header must immediately follow the decorator: %s\n", _repl_show_line(body + i, int(n))) + return 1 + if in_body != 0 and _repl_line_has_kw(body + i, int(n), ind, "raise") != 0: + stdio.printf("M3: session function '%s' has raise in its body: %s\n" " M3 does not support cross-fragment exception propagation (raise appends the " "__eh_msg_out__/__eh_code_out__ out params to the function, " "but indirect calls through the function pointer do not pass them) -- the whole fragment is skipped\n", name_buf, _repl_show_line(body + i, int(n))) + return 1 + lp1: int = int(string.strlen(ReplLiftBuf)) + lp1 = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, lp1, body + i, int(n)) + lp1 = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, lp1, "\n") + if lp1 < 0: return 2 + i += n + 1 + continue + # Dedented non-blank line ends the lift block; re-evaluate this line (do not advance i). + in_lift = 0 + need_header = 0 + in_body = 0 + continue + if blank != 0 or ind > 0: + # Into the fragment body (4 spaces per line, same as the block path). + pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), " ") + pos = _repl_cat(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), body + i, int(n)) + pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), "\n") + if pos < 0: return 2 + i += n + 1 + continue + # Dedented non-blank line. + if body[i] == '@': + in_lift = 1 + need_header = 1 + in_body = 0 + lp2: int = int(string.strlen(ReplLiftBuf)) + lp2 = _repl_cat(ReplLiftBuf, _REPL_LIFT_CAP, lp2, body + i, int(n)) + lp2 = _repl_cat_str(ReplLiftBuf, _REPL_LIFT_CAP, lp2, "\n") + if lp2 < 0: return 2 + i += n + 1 + continue + if _repl_line_has_kw(body + i, int(n), 0, "def") != 0: + rc_h: int = int(_repl_lift_def_header(body + i, int(n), name_buf, _REPL_FN_NAME_MAX)) + if rc_h != 0: return rc_h + if _repl_lift_add_name(name_buf) != 0: return 1 + in_lift = 1 + need_header = 0 + in_body = 1 + i += n + 1 + continue + if _repl_line_has_kw(body + i, int(n), 0, "async") != 0 and _repl_line_has_kw(body + i, int(n), 6, "def") != 0: + stdio.printf("M3: async def session functions are not supported (coroutine ABI is incompatible with " "the M3 signature token; rewrite as a plain def): %s\n", _repl_show_line(body + i, int(n))) + return 1 + pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), " ") + pos = _repl_cat(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), body + i, int(n)) + pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), "\n") + if pos < 0: return 2 + i += n + 1 + if need_header != 0: + stdio.printf("M3: missing def header after decorator (end of fragment)\n") + return 1 + ReplWrapPos = pos + return 0 + + +# Assemble a multi-line block wrapper (lift + wrap): 0=ok/1=rejected/2=buffer full. +def _repl_build_frag_src_multi(name: str, body: bytes) -> int: + global ReplWrapPos + if name is None or body is None: return 2 + if ReplLiftBuf is None or ReplLiftNames is None or ReplSrcBuf is None: return 2 + # Lift buffer/name table watermarks are cleared per fragment by _build_frag_src; here we only + # assemble: emit the fragment function header, then body lines each get 4 spaces. + viperlib.snprintf(ReplSrcBuf, _REPL_FRAG_SRC_CAP, "export def %s(%s: bytes) -> int:\n", name, GenTypes.REPL_CTX_PARAM) + ReplWrapPos = int(string.strlen(ReplSrcBuf)) + rc: int = _repl_split_body(body) + if rc != 0: return rc + pos: int = ReplWrapPos + pos = _repl_cat_str(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), " return 0\n") + if pos < 0: return 2 + # Append lifted module-level defs after the fragment entry function (module scope, order-free). + lift_len: int = int(string.strlen(ReplLiftBuf)) + if lift_len > 0: + pos = _repl_cat(ReplSrcBuf, _REPL_FRAG_SRC_CAP, int(pos), ReplLiftBuf, lift_len) + if pos < 0: return 2 + ReplWrapPos = pos + return 0 + + +# ============================================================ +# M3 session function table: host owns it (writes table + ctx slots); Gen reads only. + +# Find a session function table entry by name (host side; same as Gen's repl_find_func_entry). +def _repl_func_entry_find(name: str) -> GenTypes.ReplFuncEntry*: + """1=entry found; None=not registered or table not built.""" + if ReplCtxObj is None or ReplCtxObj.FuncTable is None or name is None: return None + es: sizet = GenTypes.ReplFuncEntry.__sizeof__() + i: int + for i in range(int(ReplCtxObj.FuncCount)): + e: GenTypes.ReplFuncEntry* = GenTypes.ReplFuncEntry*(uint64(ReplCtxObj.FuncTable) + uint64(i) * uint64(es)) + if e.Name is not None and string.strcmp(e.Name, name) == 0: return e + return None + + +# Name of the idx-th lifted function (NUL-terminated); None if out of range. +def _repl_lift_name(idx: int) -> str: + if ReplLiftNames is None or idx < 0 or idx >= ReplLiftNameCount: return None + return ReplLiftNames + idx * _REPL_FN_NAME_MAX + + +# Deep-copy into PermPool (session table entry names etc. that outlive a fragment). +def _repl_perm_dup(s: str) -> str: + if s is None or _perm_pool is None: return None + n: sizet = string.strlen(s) + b: str = _perm_pool.alloc(n + 1) + if b is None: return None + string.strcpy(b, s) + return b + + +# Render signature result -> human-readable "(P1, P2) -> R" (into a PermPool buffer). +def _repl_sig_to_text(tag: int, bits: int, pc: int) -> str: + """Render ReplSigResult + ReplSigTags/Bits as "(P1, P2) -> R" (PermPool buffer).""" + if ReplSigText is None: return "(?)" + pos: int = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, 0, "(") + for i in range(int(pc)): + if i > 0: pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, int(pos), ", ") + pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, int(pos), _repl_tag_text(int(ReplSigTags[i]), int(ReplSigBits[i]))) + pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, int(pos), ") -> ") + pos = _repl_cat_str(ReplSigText, _REPL_SIG_TEXT_CAP, int(pos), _repl_tag_text(tag, bits)) + if pos < 0: return "(?)" + return ReplSigText + + +# Pre-registration check (before llc/load): per lifted function verify signature, EH params, +# type support, ctx first param, param count, redefinition, capacity - else abort the fragment. +def _repl_precheck_session_funcs(mod: mlirlite.LLVMModule*) -> int: + if ReplLiftNameCount <= 0: return 0 + if ReplCtxObj is None or ReplCtxObj.FuncTable is None or ReplSigResult is None: + stdio.printf("M3: session function table/signature buffer not initialized (session init failed)\n") + return 1 + new_cnt: int = 0 + idx: int + for idx in range(int(ReplLiftNameCount)): + nm: str = _repl_lift_name(idx) + if nm is None: return 1 + rc: int = _repl_model_func_sig(mod, nm, ReplSigResult) + if rc == 1: + stdio.printf("M3: function '%s' not found in the defining fragment's model (lift not effective?)\n", nm) + return 1 + if rc == 2: + stdio.printf("M3: session function '%s' signature has an unsupported type '%s'\n" " M3 supports void / i1 / i8 / i16 / i32 / i64 / float / double / " "i8* (str, bytes, rawptr); for struct values/arrays/multi-level pointers use pointer params " "or wait for M4\n", nm, ReplSigResult.BadType) + return 1 + if rc == 3: + stdio.printf("M3: session function '%s' signature has EH out params (%%__eh_msg_out__/" "%%__eh_code_out__) -- its body contains raise\n" " M3 does not support cross-fragment exception propagation: indirect calls through the function pointer do not pass these two args " "(arity mismatch); the whole fragment is skipped\n", nm) + return 1 + if rc == 4: + stdio.printf("M3: session function '%s' define's first param is not %s (lift template and calling convention " "disagree)\n", nm, GenTypes.REPL_CTX_PARAM) + return 1 + if rc == 5: + stdio.printf("M3: session function '%s' has more than %d params\n", nm, GenTypes.REPL_FUNC_PARAM_MAX) + return 1 + if rc != 0: + stdio.printf("M3: failed to parse session function '%s' signature (rc=%d)\n", nm, rc) + return 1 + old: GenTypes.ReplFuncEntry* = _repl_func_entry_find(nm) + if old is None: + new_cnt += 1 + continue + # Redefinition: only a same-signature overwrite is allowed (a different signature would + # make earlier fragments call new code with the old ABI -> silent UB). Same sig: update addr. + sig_same: int = 1 + if old.RetTag != ReplSigResult.RetTag or old.RetBits != ReplSigResult.RetBits or old.ParamCount != ReplSigResult.ParamCount: sig_same = 0 + if sig_same != 0: + for pi in range(int(old.ParamCount)): + if int(old.ParamTags[pi]) != int(ReplSigTags[pi]) or int(old.ParamBits[pi]) != int(ReplSigBits[pi]): + sig_same = 0 + break + if sig_same == 0: + stdio.printf("M3: redefinition of session function '%s' has a different signature (old %s, new %s)\n" " M3 allows a same-signature overwrite (address only); a different signature would make call " "sites already compiled in earlier fragments call new code with the old ABI -- the whole fragment is skipped; use another name\n", nm, _repl_entry_sig_text(old), _repl_sig_to_text(ReplSigResult.RetTag, ReplSigResult.RetBits, ReplSigResult.ParamCount)) + return 1 + if ReplCtxObj.FuncCount + new_cnt > ReplCtxObj.FuncCap: + stdio.printf("M3: session function limit of %d reached (ctx function pointer slots exhausted); %d added this time; " "restart the session or use fewer session functions\n", ReplCtxObj.FuncCap, new_cnt) + return 1 + return 0 + + +# Render an existing entry's signature (for redefinition-conflict diagnostics; into ReplSigText2). +def _repl_entry_sig_text(e: GenTypes.ReplFuncEntry*) -> str: + if e is None or ReplSigText2 is None: return "(?)" + pos: int = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, 0, "(") + for i in range(int(e.ParamCount)): + if i > 0: pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, int(pos), ", ") + pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, int(pos), _repl_tag_text(int(e.ParamTags[i]), int(e.ParamBits[i]))) + pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, int(pos), ") -> ") + pos = _repl_cat_str(ReplSigText2, _REPL_SIG_TEXT_CAP, int(pos), _repl_tag_text(e.RetTag, e.RetBits)) + if pos < 0: return "(?)" + return ReplSigText2 + + +# Register after a successful DLL load: sys.symbol(bare name) -> write ctx slot -> +# append/update the table. 0=ok; 1=failed (caller rolls back by ReplSavedFuncCount and aborts). +def _repl_register_session_funcs(h_lib: rawptr, frag_no: int, mod: mlirlite.LLVMModule*) -> int: + if ReplLiftNameCount <= 0: return 0 + if ReplCtxObj is None or ReplCtxObj.FuncTable is None or ReplCtxBuf is None: + stdio.printf("M3: session function table/ctx not initialized\n") + return 1 + idx: int + for idx in range(int(ReplLiftNameCount)): + nm: str = _repl_lift_name(idx) + if nm is None: return 1 + # Exported symbol is the bare name: `export def` sets has_export without a SHA1 prefix, + # and -shared without a DEF file exports all symbols automatically. + addr: rawptr = sys.symbol(h_lib, nm) + if addr is None: + stdio.printf("M3: session function '%s' is absent from the fragment DLL's export table " "(sys.symbol returned null; this fragment is skipped)\n", nm) + return 1 + ent: GenTypes.ReplFuncEntry* = _repl_func_entry_find(nm) + if ent is None: + # New entry (signature already validated; re-parse to recover the scratch tokens). + rc: int = _repl_model_func_sig(mod, nm, ReplSigResult) + if rc != 0: + stdio.printf("M3: failed to re-parse session function '%s' signature (rc=%d)\n", nm, rc) + return 1 + j: int = int(ReplCtxObj.FuncCount) + es: sizet = GenTypes.ReplFuncEntry.__sizeof__() + ent = GenTypes.ReplFuncEntry*(uint64(ReplCtxObj.FuncTable) + uint64(j) * uint64(es)) + nm_c: str = _repl_perm_dup(nm) + nm_s: str = _repl_perm_dup(nm) + tags_b: bytes = _perm_pool.alloc(GenTypes.REPL_FUNC_PARAM_MAX) + bits_b: bytes = _perm_pool.alloc(GenTypes.REPL_FUNC_PARAM_MAX) + if nm_c is None or nm_s is None or tags_b is None or bits_b is None: + stdio.printf("M3: failed to allocate the session function entry ('%s')\n", nm) + return 1 + string.memset(tags_b, 0, GenTypes.REPL_FUNC_PARAM_MAX) + string.memset(bits_b, 0, GenTypes.REPL_FUNC_PARAM_MAX) + string.memcpy(tags_b, ReplSigTags, sizet(ReplSigResult.ParamCount)) + string.memcpy(bits_b, ReplSigBits, sizet(ReplSigResult.ParamCount)) + ent.Name = nm_c + ent.SymName = nm_s + ent.FragNo = frag_no + ent.SlotOff = GenTypes.REPL_FUNC_BASE + j * GenTypes.REPL_FUNC_SLOT_SIZE + ent.RetTag = ReplSigResult.RetTag + ent.RetBits = ReplSigResult.RetBits + ent.ParamCount = ReplSigResult.ParamCount + ent.ParamTags = tags_b + ent.ParamBits = bits_b + ReplCtxObj.FuncCount = j + 1 + else: + # Redefinition (same signature): only swap address and fragment number; slot offset unchanged. + ent.FragNo = frag_no + # Write the ctx slot (low 8 bytes = address, high 8 bytes reserved 0). + sv: ReplFuncSlotView* = ReplFuncSlotView*(uint64(ReplCtxBuf) + uint64(ent.SlotOff)) + sv.Addr = addr + sv.Reserved = None + stdio.printf("session function %s: export symbol %s -> ctx slot +%d (defining fragment %d, signature %s)\n", nm, nm, ent.SlotOff, frag_no, _repl_entry_sig_text(ent)) + return 0 + + +# ============================================================ +# M3 session function signature parsing: read the definition-site ABI from the model. + + +# token -> display name (for logs/diagnostics only). +def _repl_tag_text(tag: int, bits: int) -> str: + if tag == GenTypes.REPL_TY_VOID: return "void" + if tag == GenTypes.REPL_TY_INT: + if bits == 1: return "bool" + if bits == 8: return "char" + if bits == 16: return "int16" + if bits == 32: return "int" + if bits == 64: return "int64" + return "int?" + if tag == GenTypes.REPL_TY_FLOAT: + if bits == 32: return "float" + if bits == 64: return "double" + return "float?" + if tag == GenTypes.REPL_TY_STR: return "str" + return "?" + + +# LLVMType* -> packed token (tag*1000 + bits); 0 = unsupported. Supported set matches the +# old string version: i1/i8/i16/i32/i64, float/double, Ptr(Int8) (str/bytes/rawptr). +def _repl_ty_ptr_token(ty: mlirlite.LLVMType*) -> int: + if ty is None: return 0 + match ty: + case mlirlite.LLVMType.Int(bits): + if bits == 1: return GenTypes.REPL_TY_INT * 1000 + 1 + if bits == 8: return GenTypes.REPL_TY_INT * 1000 + 8 + if bits == 16: return GenTypes.REPL_TY_INT * 1000 + 16 + if bits == 32: return GenTypes.REPL_TY_INT * 1000 + 32 + if bits == 64: return GenTypes.REPL_TY_INT * 1000 + 64 + return 0 + case mlirlite.LLVMType.Float(bits): + if bits == 32: return GenTypes.REPL_TY_FLOAT * 1000 + 32 + if bits == 64: return GenTypes.REPL_TY_FLOAT * 1000 + 64 + return 0 + case mlirlite.LLVMType.Ptr(pointee): + match pointee: + case mlirlite.LLVMType.Int(bits): + if bits == 8: return GenTypes.REPL_TY_STR * 1000 + 8 + return 0 + case _: + return 0 + case _: + return 0 + + +# Diagnostic: LLVMType* -> readable name (for unsupported-signature errors); none if unnamed. +def _repl_ty_name(ty: mlirlite.LLVMType*) -> str: + if ty is None: return "(none)" + nm: str = mlirlite.get_struct_name(ty) + if nm is not None: return nm + return "(unsupported)" + + +# Take a session function signature from the translation model (not by parsing IR define lines). +# Codes: 0=ok; 1=not found; 2=unsupported type; 3=EH params; 4=first param not ctx; 5=too many params. +def _repl_model_func_sig(mod: mlirlite.LLVMModule*, name: str, out: ReplSigOut*) -> int: + if name is None or out is None or mod is None: return 1 + out.RetTag = GenTypes.REPL_TY_VOID + out.RetBits = 0 + out.ParamCount = 0 + out.EHTag = 0 + out.Bad = 0 + out.BadType = None + fn: mlirlite.Function* = GenApi.find_func_in_module(mod, name) + if fn is None: return 1 + # 1) Return type. + rt_ty: mlirlite.LLVMType* = mlirlite.function_get_ret_ty(fn) + is_void: int = 0 + if rt_ty is None: is_void = 1 + else: + match rt_ty: + case mlirlite.LLVMType.Void(): + is_void = 1 + case _: + is_void = 0 + if is_void != 0: + out.RetTag = GenTypes.REPL_TY_VOID + out.RetBits = 0 + else: + rt: int = _repl_ty_ptr_token(rt_ty) + if rt == 0: + out.Bad = 1 + out.BadType = _repl_ty_name(rt_ty) + return 2 + out.RetTag = rt // 1000 + out.RetBits = rt % 1000 + # 2) Params: the first is ctx (Ptr(Int8) named __repl_ctx__), the rest are business params. + pi: int = 0 + p_cur: mlirlite.Param* = mlirlite.function_get_param_head(fn) + while p_cur is not None: + p_ty: mlirlite.LLVMType* = mlirlite.param_get_ty(p_cur) + p_nm: str = mlirlite.param_get_name(p_cur) + # Param names may or may not carry a leading '%' (the printer adds it on demand); strip it. + bare_nm: str = p_nm + if bare_nm is not None and bare_nm[0] == '%': bare_nm = bare_nm + 1 + if pi == 0: + if _repl_ty_ptr_token(p_ty) != GenTypes.REPL_TY_STR * 1000 + 8: return 4 + if bare_nm is None or string.strcmp(bare_nm, GenTypes.REPL_CTX_PARAM) != 0: return 4 + else: + # EH param (raise semantics). + if bare_nm is not None: + if string.strcmp(bare_nm, "__eh_msg_out__") == 0 or string.strcmp(bare_nm, "__eh_code_out__") == 0: + out.EHTag = 1 + return 3 + if out.ParamCount >= GenTypes.REPL_FUNC_PARAM_MAX: return 5 + tk: int = _repl_ty_ptr_token(p_ty) + if tk == 0: + out.Bad = 1 + out.BadType = _repl_ty_name(p_ty) + return 2 + ReplSigTags[out.ParamCount] = char(tk // 1000) + ReplSigBits[out.ParamCount] = char(tk % 1000) + out.ParamCount = out.ParamCount + 1 + pi += 1 + p_cur = mlirlite.param_get_next(p_cur) + return 0 + + + +# ============================================================ +# Assemble the wrapped fragment source into ReplSrcBuf (the fragment body is always a function body). +def _build_frag_src(name: str, body: str) -> int: + """Assemble the wrapped fragment source: single line uses the template verbatim; multi-line lifts and indents by 4.""" + global ReplLiftNameCount + if name is None or body is None: return 2 + # Clear the previous fragment's lifting state before assembling (single-line path lifts + # nothing; a stale name count would make the precheck wrongly reject this fragment). + ReplLiftNameCount = 0 + ReplLiftBuf[0] = 0 + ReplLiftNames[0] = 0 + if string.strchr(body, '\n') is None: + # Single line: existing template (ctx param bytes => i8*; export => no SHA1 prefix). + viperlib.snprintf(ReplSrcBuf, _REPL_FRAG_SRC_CAP, "export def %s(%s: bytes) -> int:\n %s\n return 0\n", name, GenTypes.REPL_CTX_PARAM, body) + return 0 + # Multi-line: lift top-level defs + assemble line by line (M3; rules in _repl_split_body). + return _repl_build_frag_src_multi(name, body) + + +# Execute one fragment source (single-line and block share this entry): parse the unwrapped +# source first (readable diagnostics), then hand it to ReplExecFragment to wrap/translate/run. +def _exec_repl_src(mb: memhub.MemBuddy*, src: str) -> int: + """Parse and execute one fragment source; 0=handled (diagnostics already printed).""" + if src is None: return 1 + tree: ast.AST* = ast.parse(src, mb) + if tree is None: + el: int = ast.get_parse_err_line() + ec: int = ast.get_parse_err_col() + em: str = ast.get_parse_err_msg() + if em is None: em = "(unknown)" + stdio.printf("ParseError: line %d column %d: %s\n", el, ec, em) + return 0 + if _count_top_stmts(tree) <= 0: + stdio.printf("Parsed: 0 top-level statements (skipping)\n") + return 0 + # M2/M3 execution path: whole block/line runs as one fragment function body; failures only + # print the reason (or longjmp back to the recovery point), never kill the process. + rc_frag: int = ReplExecFragment(mb, src) + if rc_frag != 0: + # Non-longjmp failures already printed their reason inside the fragment; here only roll + # back the session table watermarks so the tables and ctx stay consistent. + if ReplCtxObj is not None: + ReplCtxObj.Count = ReplSavedVarCount + ReplCtxObj.FuncCount = ReplSavedFuncCount + # M4: roll back the session baseline (DefineT/TypedefT) watermarks; a no-op on the normal + # path, it keeps the "failed fragment never changes the baseline" invariant. + ReplBaselineTruncate(ReplSavedCDefineCount, ReplSavedCTypedefCount) + return 0 + + +# Execute one fragment (any top-level statement: expr/assign/ann-assign/aug-assign/...). +# Returns 0=ok (echoed by result_kind); non-zero=failed. Fatal errors longjmp to run's point. +def ReplExecFragment(mb: memhub.MemBuddy*, src: str) -> int: + """Fragment -> DLL -> call -> ctx echo; 0=success.""" + global ReplFragFn, ReplFragNo, ReplSavedVarCount, ReplSavedFuncCount + global ReplSavedCDefineCount, ReplSavedCTypedefCount + if mb is None or src is None: return 1 + if ReplTmpDir is None or ReplNameBuf is None or ReplSrcBuf is None: + stdio.printf("REPL: session buffer not initialized\n") + return 1 + if ReplSrcPath is None or ReplIrBuf is None or ReplNeedleBuf is None: + stdio.printf("REPL: session buffer not initialized\n") + return 1 + if ReplCtxBuf is None or ReplCtxObj is None or ReplVarTable is None: + stdio.printf("REPL: ctx session memory not initialized\n") + return 1 + + # 0) Re-arm the recovery point and snapshot session table watermarks so a failed fragment's + # half-registered entries can be rolled back; set_repl_jmpbuf is idempotent. + VLogger.set_repl_jmpbuf(ReplJmpBuf) + ReplSavedVarCount = ReplCtxObj.Count + ReplSavedFuncCount = ReplCtxObj.FuncCount + # M4: session baseline watermarks (DefineT/TypedefT); a failed fragment must not add + # increments, same idea as the var/func watermarks (only success calls ReplBaselineCapture). + ReplSavedCDefineCount = ReplBaseCDefineCount + ReplSavedCTypedefCount = ReplBaseCTypedefCount + + # 1) Take the number and increment it immediately: names/files stay unique even after a failure. + n: int = ReplFragNo + ReplFragNo = n + 1 + viperlib.snprintf(ReplNameBuf, _REPL_NAME_CAP, "__repl_frag_%d", n) + + # 2) Strip leading whitespace (the template indents the body; stacking would error). For + # blocks only the first line is affected; step 3 indents the rest uniformly. + body: str = src + if body is not None: + while body[0] == ' ' or body[0] == '\t': body = body + 1 + + # 3) Wrap: ctx param (bytes => i8*); export => no SHA1 prefix. Single line uses the existing + # template; multi-line lifts top-level defs. Returns 0=ok/1=rejected/2=buffer full. + rc_build: int = _build_frag_src(ReplNameBuf, body) + if rc_build == 2: + stdio.printf("fragment %s: failed to assemble the wrapped source (buffer too small, %d bytes)\n", ReplNameBuf, _REPL_FRAG_SRC_CAP) + return 1 + if rc_build != 0: + # Lift rule rejection (async def / raise / duplicate / non-single-line signature / own + # ctx param...): the whole fragment is skipped (no partial side effects), session continues. + return 1 + + # 4) Write the wrapped source to disk (used only for error echo: fatal_error reads this file). + viperlib.snprintf(ReplSrcPath, _REPL_PATH_CAP, "%s/%s.vp", ReplTmpDir, ReplNameBuf) + sf: io.File* = io.File(ReplSrcPath, io.MODE.W) + if not sf.closed: + sf.write(ReplSrcBuf, string.strlen(ReplSrcBuf)) + sf.close() + + # 5) Reset per-fragment module-level translation state (mirrors Phase1Main/BuildPipeline); + # temp_dir is used for .vpi injection: prefer the project temp dir, else the session dir. + vpi_dir: str = Config.TempDir + if vpi_dir is None: vpi_dir = ReplTmpDir + GenApi.set_temp_dir(vpi_dir) + # CallGenBase.TempDir/TempDirLen (cross-module .vpi/.text.ll lookup, ExportT checks) is only + # set by SetTempDir; set it here once per fragment, same value as set_temp_dir. + GenApi.SetTempDir(vpi_dir) + GenApi.set_current_file_type(ReplSrcPath) + GenApi.set_current_file_imports(ReplSrcPath) + GenApi.clear_trace() + GenApi.clear_cdefine_constants() + GenApi.clear_ctypedefs() + # Visibility uses loose mode (strict=0, all registered structs visible): fragment types come + # from the session, so strict isolation would hide them and fail annotation resolution. + GenApi.reset_visible_structs(mb, 0) + # Struct registration uses permanent allocation (like Phase1b/Phase2 StdlibAlloc=1): table, + # entry names and field types go into PermPool, else later fragments read freed memory. + GenApi.set_stdlib_alloc(1) + # M4: replay the DefineT/TypedefT session baseline; the clear_* above emptied the tables, and + # this fragment's additions are captured on success by ReplBaselineCapture. + ReplBaselineRestore(mb) + + # 6) Parse the wrapped source. Fragments containing "class " use the permanent pool (generic + # templates store the ClassDef node for later specialization); others use mb as before. + parse_pool: memhub.MemBuddy* = mb + if _perm_pool is not None: + if string.strstr(ReplSrcBuf, "class ") is not None: parse_pool = _perm_pool + tree: ast.AST* = ast.parse(ReplSrcBuf, parse_pool) + if tree is None: + el: int = ast.get_parse_err_line() + ec: int = ast.get_parse_err_col() + em: str = ast.get_parse_err_msg() + if em is None: em = "(unknown)" + stdio.printf("fragment %s: failed to parse the wrapped source (line %d column %d: %s)\n", ReplNameBuf, el, ec, em) + return 1 + + # 7) Translate to full module IR. ModuleSha1 = session sha1 (types are "."); + # ReplCtx enables REPL mode and is cleared after translation. + tr: HandlesTranslator.Translator* = mb.alloc(HandlesTranslator.Translator.__sizeof__()) + if tr is None: + stdio.printf("fragment %s: Translator allocation failed\n", ReplNameBuf) + return 1 + tr.__before_init__() + tr.__init__() + tr.ModuleSha1 = ReplSessionSha1 + tr._declare_only = 0 + tr.ReplCtx = ReplCtxObj + rc_tr: int = tr.translate(tree) + tr.ReplCtx = None + if rc_tr != 0: + stdio.printf("fragment %s: translation failed\n", ReplNameBuf) + return 1 + tr.dump_ir(ReplIrBuf, _REPL_IR_CAP, mlirlite.OUTPUT_FULL) + + # 7') M4: if this fragment defines session-prefixed symbols ("."), produce and record + # its import lib so later fragments can resolve them at link time. + needs_implib: int = 0 + if ReplSessionSha1 is not None and ReplNeedleBuf is not None: + viperlib.snprintf(ReplNeedleBuf, _REPL_NEEDLE_CAP, "@\"%s.", ReplSessionSha1) + if string.strstr(ReplIrBuf, ReplNeedleBuf) is not None: needs_implib = 1 + + # 8) Export-symbol probe: the signature must be "(%__repl_ctx__: !llvm.ptr) -> i32"; otherwise + # the wrapper/calling convention is wrong (or raise added EH params), so abort. + viperlib.snprintf(ReplNeedleBuf, _REPL_NEEDLE_CAP, "llvm.func @%s(%%%s: !llvm.ptr) -> i32", ReplNameBuf, GenTypes.REPL_CTX_PARAM) + if string.strstr(ReplIrBuf, ReplNeedleBuf) is None: + stdio.printf("fragment %s: expected export function was not generated (expected `llvm.func @%s(%%__repl_ctx__: !llvm.ptr) -> i32`); " "fragments with raise append EH out params, which M2 does not support\n", ReplNameBuf, ReplNameBuf) + return 1 + + # 8') Session function signature check (M3), before llc/load: any failure skips the whole + # fragment; returns 0 immediately when no functions were lifted. + if _repl_precheck_session_funcs(tr.Module) != 0: return 1 + + # 9) Write .ll (reusing BuildPipeline: sliced path + ensure_dir). + ir_len: sizet = string.strlen(ReplIrBuf) + if BuildPipeline.write_ir_to_file(ReplIrBuf, ir_len, ReplTmpDir, ReplNameBuf) != 0: + stdio.printf("fragment %s: failed to write .ll (dir %s)\n", ReplNameBuf, ReplTmpDir) + return 1 + ir_path: str = BuildPipeline.build_sliced_path(ReplTmpDir, ReplNameBuf, "ll") + if ir_path is None: + stdio.printf("fragment %s: failed to build the .ll path\n", ReplNameBuf) + return 1 + + # 10) llc -> .obj (reusing existing machinery; VLogger.error longjmps on failure). + cc_cmd: str = Config.CompilerCmd + if cc_cmd is None: cc_cmd = "llc" + cc_flags: str = Config.CompilerFlags + if cc_flags is None: cc_flags = "-filetype=obj -relocation-model=pic" + if BuildPipeline.compile_ll_to_obj(ir_path, ReplTmpDir, ReplNameBuf, cc_cmd, cc_flags) != 0: + # This branch is only reached on compile_ll_to_obj's internal allocation failure (real llc + # failures longjmp), so print a note instead of silently returning to the prompt. + stdio.printf("fragment %s: llc failed to compile .obj (command/path buffer allocation failed; see diagnostics above)\n", ReplNameBuf) + return 1 + + # 11) clang++ -shared -> .dll; process launch reuses BuildPipeline._run_cmd_wait. + obj_path: str = BuildPipeline.build_sliced_path(ReplTmpDir, ReplNameBuf, "obj") + dll_path: str = BuildPipeline.build_sliced_path(ReplTmpDir, ReplNameBuf, "dll") + if obj_path is None or dll_path is None: + stdio.printf("fragment %s: failed to build the .obj/.dll path\n", ReplNameBuf) + return 1 + linker_cmd: str = Config.LinkerCmd + if linker_cmd is None: linker_cmd = "clang++" + cmd: bytes = mb.alloc(_REPL_CMD_CAP) + if cmd is None: + stdio.printf("fragment %s: command-line buffer allocation failed\n", ReplNameBuf) + return 1 + # Cross-module symbols come from the host exe, so add the host import lib (if any) to the link + # line; M4 also adds this fragment's --out-implib and prior fragments' import libs. + implib_path: str = mb.alloc(_REPL_PATH_CAP) + if implib_path is None: + stdio.printf("fragment %s: import lib path buffer allocation failed\n", ReplNameBuf) + return 1 + viperlib.snprintf(implib_path, _REPL_PATH_CAP, "%s.dll.a", dll_path) + pos_c: int = _repl_cat_str(cmd, _REPL_CMD_CAP, 0, linker_cmd) + pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, " -shared -o ") + pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, dll_path) + pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, " ") + pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, obj_path) + if ReplHostImplib is not None: + pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, " ") + pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, ReplHostImplib) + pos_c = _repl_cmd_append_implibs(cmd, _REPL_CMD_CAP, pos_c) + if pos_c >= 0: + pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, " -Wl,--out-implib=") + pos_c = _repl_cat_str(cmd, _REPL_CMD_CAP, pos_c, implib_path) + if pos_c < 0: + stdio.printf("fragment %s: link command line exceeds %d bytes (%d registered fragment import libs " "in total) -- this fragment is skipped; restart the session to reduce the fragment count\n", ReplNameBuf, _REPL_CMD_CAP, ReplFragImplibCount) + return 1 + link_rc: int = BuildPipeline._run_cmd_wait(cmd) + if link_rc != 0: + stdio.printf("fragment %s: failed to generate .dll (exit=%d)\n command: %s\n", ReplNameBuf, link_rc, cmd) + if ReplHostImplib is None: + stdio.printf(" Hint: host import lib (.dll.a) not found -- " "cross-module symbols (of the form <16-hex>.) cannot be resolved; " "build the host with ViperC/project.vpj (including -Wl,--export-all-symbols), " "and keep .dll.a in the same dir as the exe\n") + else: + stdio.printf(" Hint: if the output contains undefined symbol of the form <16-hex>., " "that symbol is not defined/exported by the host (the corresponding includes module was not linked when building the host); " "if the function is `export def` in source (ExportT, bare-name definition) yet still carries a prefix, " "the include stub .vpi is probably missing (see the includes scan line at session start)\n") + return 1 + + # 11') M4: record this fragment's import lib (if it defined session-prefixed symbols) so later + # fragments resolve its class/method symbols; recording failure only warns. + if needs_implib != 0: + if _is_file(implib_path) != 0 and ReplFragImplibRecord(implib_path) != 0: + stdio.printf("fragment %s: registered fragment import lib (later fragments can resolve types/method " "symbols defined by this fragment): %s\n", ReplNameBuf, implib_path) + else: + stdio.printf("fragment %s: fragment import lib not registered (%d registered, limit %d; or the file " "is missing) -- later fragments may fail to resolve types/method symbols defined here\n", ReplNameBuf, ReplFragImplibCount, _REPL_FRAG_IMPLIB_MAX) + + # 12) Dynamic load + address lookup (sys.load_library/sys.symbol). + h_lib: rawptr = sys.load_library(dll_path) + if h_lib is None: + stdio.printf("fragment %s: sys.load_library failed: %s\n", ReplNameBuf, dll_path) + if ReplHostImplib is not None: + stdio.printf(" Hint: the fragment DLL import table points to the host exe (%s); " "if GetLastError is 127 (procedure not found) / loader 0xC0000139, " "the host and this import lib are usually from different builds, or the host lacks that export; " "it also fails when the process image name (%s) differs from the recorded import lib name\n", ReplHostImplib, dll_path) + return 1 + fp_addr: rawptr = sys.symbol(h_lib, ReplNameBuf) + if fp_addr is None: + stdio.printf("fragment %s: sys.symbol returned null (symbol absent from the export table): %s\n", ReplNameBuf, dll_path) + return 1 + + # 12') Session function registration (M3), before calling the entry so a failure skips this + # fragment's statements; in-fragment calls to the same name still use direct calls. + if _repl_register_session_funcs(h_lib, int(n), tr.Module) != 0: + if ReplCtxObj is not None: ReplCtxObj.FuncCount = ReplSavedFuncCount + return 1 + + # 13) Reset the result area (kind=none) and refresh var_count/last_frag; call indirectly (the + # module stays resident, so no close_library). + head: GenTypes.ReplResult* = _ctx_head() + if head is None: return 1 + head.Kind = GenTypes.REPL_RES_NONE + head.VarCount = ReplCtxObj.Count + head.LastFrag = n + ReplFragFn = fp_addr + # The argument type is the constructed call signature's param type (bytes => i8*). + r_rc: int = ReplFragFn(ReplCtxBuf) + + # 14) Read back from the ctx result area and echo (the fragment return is only a status code). + if _repl_echo_result(ReplNameBuf, head) != 0: + stdio.printf("fragment %s: failed to read the result area\n", ReplNameBuf) + return 1 + if r_rc != 0: stdio.printf("fragment %s: fragment returned non-zero status %d\n", ReplNameBuf, r_rc) + # 14') M4: on success, merge this fragment's new DefineT/TypedefT entries into the session + # baseline; failure paths skip this, so the baseline never holds a failed fragment's leftovers. + ReplBaselineCapture() + return 0 + + +# ============================================================ +# run - the M2/M3 REPL main loop. +def run(mb: memhub.MemBuddy*) -> int: + """M2/M3 REPL: read a line/block, parse, compile to a DLL and call it (session vars via ctx slots), then echo; errors never kill the process.""" + global ReplJmpBuf, ReplSavedVarCount, ReplSavedFuncCount, ReplInBlock, ReplBlockLen + if mb is None or _perm_pool is None: + stdio.printf("REPL: need the main pool and PermPool (main injects _perm_pool)\n") + return 1 + + # Parse tables (keywords/operators) go in PermPool: they are ast module-level globals built + # once; on mb they would be freed by each line's reset_to(mark) and crash the next parse. + ast._init_tables(_perm_pool) + + jb: rawptr = _perm_pool.alloc(_REPL_JMPBUF_SIZE) + if jb is None: + stdio.printf("REPL: jmp_buf allocation failed\n") + return 1 + # Session-level registration: ReplJmpBuf is re-registered per fragment; the VLogger side is set + # once and cleared only on run exit (that is what covers the whole pipeline). + ReplJmpBuf = jb + ReplSavedVarCount = 0 + ReplSavedFuncCount = 0 + VLogger.set_repl_jmpbuf(jb) + # jit_flag is set True by main before calling run (see main.vp --repl); run resets it to False. + lg: VLogger.Logger* = VLogger.logger + + # Arm the recovery point before use: session init may longjmp via VLogger.error, and an + # un-armed jmp_buf would be UB; init is the only stage that must succeed, so exit on failure. + rc_init: int = _setjmp(jb, None) + if rc_init != 0: + stdio.printf("REPL: session init interrupted (see diagnostics above) -- no resident buffers/ctx, REPL cannot start\n") + stdio.fflush(None) + ReplJmpBuf = None + VLogger.clear_repl_jmpbuf() + if lg is not None: lg.set_jit_flag(False) + return 1 + + # Session init: temp dir + permanent buffers + ctx (all PermPool; see ReplInitSession). + if ReplInitSession() != 0: + stdio.printf("REPL: session init failed (temp dir/resident buffers/ctx)\n") + ReplJmpBuf = None + VLogger.clear_repl_jmpbuf() + if lg is not None: lg.set_jit_flag(False) + return 1 + + line: bytes = stdlib.malloc(_REPL_LINE_CAP) + if line is None: + stdio.printf("REPL: line buffer allocation failed\n") + ReplJmpBuf = None + VLogger.clear_repl_jmpbuf() + if lg is not None: lg.set_jit_flag(False) + return 1 + + stdio.printf("ViperC REPL (M2/M3/M4) - session variables + session functions (cross-fragment) + ctx result echo;\n") + stdio.printf(" Supports expressions (1+2), definitions (x = 1 / x: int = 5), references (x + 1),\n") + stdio.printf(" augmented assignment (x += 2), address-of (p = &x), etc.; blank lines are skipped, Ctrl+Z or EOF exits\n") + stdio.printf(" Multi-line block: a trailing ':' enters continuation (prompt ...), a blank line ends it and runs the block once\n") + stdio.printf(" Cross-fragment functions: a top-level def in a block is lifted to the fragment module's export def (bare-name export),\n") + stdio.printf(" later fragments can call it directly (e.g. def f(x: int) -> int: newline + 4 spaces return x + 1);\n") + stdio.printf(" Restrictions: signature must be one line, async def is unsupported, raise in the body is unsupported, partial functions are unsupported\n") + stdio.printf(" Cross-fragment types (M4): a class in a block is translated into the session (type name %s.),\n", ReplSessionSha1) + stdio.printf(" later fragments can instantiate it, read/write fields and call methods; imported includes modules\n") + stdio.printf(" have their classes registered into the session too (when the .vpi stub is missing that module's classes are unavailable; the session continues)\n") + stdio.printf(" Fragment temp dir: %s\n", ReplTmpDir) + stdio.fflush(None) + + # === Session-level includes init (once; all failures only degrade includes classes) === + # The recovery point must land in this frame, so a fail-fast longjmp continues into the loop. + inc_mark: sizet = mb.mark() + rc_inc: int = _setjmp(jb, None) + if rc_inc != 0: + stdio.printf("REPL: includes init interrupted (%d modules registered, %d stubs): " "importing includes modules may fail; other fragments are unaffected\n", ReplIncMods, ReplIncVpiNew) + mb.reset_to(inc_mark) + else: + # Scan results/registry live in stdlib, so mb allocations can be reclaimed after this call, + # which also performs includes class registration (per-module _setjmp). + ReplInitIncludes(mb) + mb.reset_to(inc_mark) + # Re-arm the recovery point on run's own frame (not a helper: _setjmp's target belongs to the + # calling frame); this puts the finish step and read loop back under this frame's point. + rc_arm: int = _setjmp(jb, None) + if rc_arm != 0: + stdio.printf("REPL: includes class layout finish interrupted (see diagnostics above): layout registration may be incomplete; " "session continues\n") + else: + # Finish: converge classes whose parents became ready in later passes + per-class warnings. + # Done after re-arming so any longjmp lands in this frame (the dead-frame issue above). + VpiReader.ReplStubLayoutFinish() + # Disabling stub-layout recovery must be unconditional: a non-zero flag would make fragment + # translation take the REPL stub path (this function only sets a flag, no failure path). + VpiReader.ReplStubLayoutEnd() + + # === Host export symbols: locate the host exe import lib === + # If found, fragment link lines include it; otherwise warn and continue (calls may fail to link). + if ReplFindHostImplib() != 0: + stdio.printf("Host symbol import lib: %s (fragments can call includes functions/globals across modules)\n", ReplHostImplib) + else: + stdio.printf("Host symbol import lib: .dll.a not found; fragment calls to includes module-level " "functions will report undefined symbol at link time (the session does not terminate). Build the host with " "ViperC/project.vpj (which declares -Wl,--export-all-symbols), " "and keep the exe and .dll.a in the same dir\n") + stdio.fflush(None) + + # === Read loop: single-line fragments + block continuation === + # All init failures were absorbed, so the prompt is guaranteed to appear from here on. + stdio.printf("REPL: init complete, entering the read loop\n") + stdio.fflush(None) + # Block state is zeroed after every block end/failure/EOF/overflow, so a stale block cannot + # pollute the next line (the next append overwrites from index 0). + ReplInBlock = 0 + ReplBlockLen = 0 + while 1: + # Prompt: "... " while in a block, otherwise ">>> " (same print point and flush timing). + if ReplInBlock != 0: stdio.printf("... ") + else: stdio.printf(">>> ") + stdio.fflush(None) + n: int = _read_line(line, _REPL_LINE_CAP) + eof_hit: int = 0 + if n < 0: # EOF: treat as a terminating line, then exit + eof_hit = 1 + n = 0 + # Per line: snapshot watermarks, then _setjmp (arming this fragment's recovery point for the + # whole pipeline); any longjmp reclaims mark and continues. + mark: sizet = mb.mark() + rc: int = _setjmp(jb, None) + if rc != 0: + # longjmp return: diagnostics were already printed; clear block state, roll back session + # watermarks and reclaim this line's allocations, then print the next prompt. + ReplInBlock = 0 + ReplBlockLen = 0 + if ReplCtxObj is not None: + ReplCtxObj.Count = ReplSavedVarCount + ReplCtxObj.FuncCount = ReplSavedFuncCount + # M4: roll back the session baseline (DefineT/TypedefT) watermarks (same idea as above). + ReplBaselineTruncate(ReplSavedCDefineCount, ReplSavedCTypedefCount) + mb.reset_to(mark) + if eof_hit != 0: break + continue + if eof_hit != 0: + # EOF: while in a block with buffered content, execute the cached block first (like a + # blank line); otherwise take the existing exit path (print a newline then exit). + if ReplInBlock != 0 and ReplBlockLen > 0: _exec_repl_src(mb, ReplBlockBuf) + ReplInBlock = 0 + ReplBlockLen = 0 + mb.reset_to(mark) + stdio.printf("\n") + break + if ReplInBlock != 0: + if n == 0: + # Blank line ends continuation: run the whole block once; block errors only print. + if ReplBlockLen > 0: _exec_repl_src(mb, ReplBlockBuf) + ReplInBlock = 0 + ReplBlockLen = 0 + mb.reset_to(mark) + continue + # In continuation: every line is buffered; no state flips on ':' and no indent analysis. + if _block_append(line, int(n)) != 0: + stdio.printf("REPL: block input exceeds the block buffer cap of %d bytes: this block was discarded, " "returning to >>> (reduce the block size or run it in chunks)\n", _REPL_BLOCK_CAP) + ReplInBlock = 0 + ReplBlockLen = 0 + mb.reset_to(mark) + continue + # ==== Existing single-line path (unchanged when not in continuation) ==== + if n == 0: # blank line: skip + mb.reset_to(mark) + continue + if _line_starts_block(line, int(n)) != 0: + # Trailing ':' enters continuation: buffer this line without executing it. The first line + # cannot overflow (line cap < block cap), so append failure falls through below. + if _block_append(line, int(n)) == 0: + ReplInBlock = 1 + mb.reset_to(mark) + continue + _exec_repl_src(mb, line) + mb.reset_to(mark) + + stdlib.free(line) + # Clear the recovery point only at session end (never mid-session, so the pipeline stays protected). + ReplJmpBuf = None + VLogger.clear_repl_jmpbuf() + if lg is not None: lg.set_jit_flag(False) + return 0 diff --git a/App/lib/core/SelfBuild.vp b/App/lib/core/SelfBuild.vp index 8a1c9a8..e527160 100644 --- a/App/lib/core/SelfBuild.vp +++ b/App/lib/core/SelfBuild.vp @@ -1,307 +1,272 @@ -from stdint import * -import stdio -import stdlib -import string -import viperlib -import os -import os.path -import win32.win32base -import win32.win32file -import win32.win32process -import lib.Phase.BuildPipeline as BuildPipeline - - -define SELF_EXE_CAP = 4096 -define SELF_BUILD_DIR = "__vpcbuild_2__" -define COPY_DELAY_SECONDS = 5 - - -def _GetSelfExe() -> str: - buf: bytes = stdlib.malloc(sizet(SELF_EXE_CAP)) - if buf is None: - return None - n: uint32 = win32.win32file.GetModuleFileNameA(None, buf, uint32(SELF_EXE_CAP)) - if int(n) <= 0: - stdlib.free(buf) - return None - ni: int = int(n) - if ni >= SELF_EXE_CAP: - ni = SELF_EXE_CAP - 1 - buf[ni] = 0 - return str(buf) - - -def _TryRoot(dir: str) -> str: - if dir is None or len(dir) == 0: - return None - root: str = os.path.normpath(dir) - if root is None: - return None - project: str = os.path.join(root, "project.vpj") - copyself_project: str = os.path.join(os.path.join(root, "copyself"), "project.vpj") - if project is not None and copyself_project is not None: - if os.isfile(project) and os.isfile(copyself_project): - return root - return None - - -def _FindViperCRoot(self_exe: str) -> str: - base: str = os.path.dirname(self_exe) - if base is None: - base = "" - parent1: str = os.path.dirname(base) - parent2: str = os.path.dirname(parent1) - - # Executable-derived candidates first: cwd is normally the workspace root, - # not the compiler tree. - root: str = _TryRoot(base) - if root is not None: - return root - root = _TryRoot(parent1) - if root is not None: - return root - root = _TryRoot(parent2) - if root is not None: - return root - root = _TryRoot(os.path.join(base, "ViperC")) - if root is not None: - return root - root = _TryRoot(os.path.join(base, "../ViperC")) - if root is not None: - return root - root = _TryRoot(os.path.join(base, "../../ViperC")) - if root is not None: - return root - - # Fallback only when the executable is detached from the compiler tree. - cwd: str = os.getcwd() - root = _TryRoot(cwd) - if root is not None: - return root - if cwd is not None: - root = _TryRoot(os.path.join(cwd, "ViperC")) - if root is not None: - return root - root = _TryRoot(os.path.join(os.path.dirname(cwd), "ViperC")) - if root is not None: - return root - return None - - -def CollectForwardedArgs(argc: int, argv: str*) -> str: - """--build-self: argv minus --build-self / --project / --build-dir (and their values). - - No whitelist: any current or future flag is forwarded verbatim, so a self-build can - enable the same modes as a normal build (e.g. `viperc --build-self --spend-file p.txt - --vpf 3`). The three excluded entries are the ones the self-build invocation owns. - """ - if argv is None or argc <= 1: return "" - total: sizet = 1 - i: int = 1 - while i < argc: - a: str = argv[i] - if a is not None: - if string.strcmp(a, "--build-self") == 0: - i += 1 - continue - if string.strcmp(a, "--project") == 0 or string.strcmp(a, "--build-dir") == 0: - i += 2 - continue - total += len(a) + 1 - i += 1 - buf: str = stdlib.malloc(total + 1) - if buf is None: return "" - buf[0] = '\0' - i = 1 - while i < argc: - a2: str = argv[i] - if a2 is not None: - if string.strcmp(a2, "--build-self") == 0: - i += 1 - continue - if string.strcmp(a2, "--project") == 0 or string.strcmp(a2, "--build-dir") == 0: - i += 2 - continue - if buf[0] != '\0': string.strcat(buf, " ") - string.strcat(buf, a2) - i += 1 - return buf - - -def _FormatSelfBuildCommand(self_exe: str, project: str, build_dir: str, extra: str) -> str: - # `extra` is the caller's forwarded argument string (empty when nothing was forwarded): - # the rebuilt compiler then carries exactly the modes the user asked for. - extra_len: sizet = 0 - if extra is not None: extra_len = len(extra) - need: sizet = len(self_exe) + len(project) + len(build_dir) + extra_len + 64 - buf: bytes = stdlib.malloc(need) - if buf is None: - return None - viperlib.snprintf(buf, need, "\"%s\" --project \"%s\" --clean --build-dir \"%s\"", - self_exe, project, build_dir) - if extra is not None and extra_len > 0: - pos: sizet = string.strlen(str(buf)) - viperlib.snprintf(buf + pos, need - pos, " %s", extra) - return str(buf) - - -def _FindCopySelfExe(root: str) -> str: - base: str = os.path.join(root, "copyself") - standard: str = os.path.join(os.path.join(os.path.join(base, "__vpcbuild__"), "output"), "copyself.exe") - if os.isfile(standard): - return standard - legacy: str = os.path.join(os.path.join(base, "output"), "copyself.exe") - if os.isfile(legacy): - return legacy - return None - - -def _FormatCopySelfCommand(copyself_exe: str) -> str: - need: sizet = len(copyself_exe) + 128 - buf: bytes = stdlib.malloc(need) - if buf is None: - return None - viperlib.snprintf(buf, need, "\"%s\" %d ../../__vpcbuild_2__ ../../__vpcbuild__", - copyself_exe, COPY_DELAY_SECONDS) - return str(buf) - - -def _SpawnDetached(cmd: str, cwd: str) -> int: - if cmd is None or cwd is None: - stdio.printf("FAIL: --build-self cannot launch copyself without a command and working directory\n") - return -1 - clen: sizet = len(cmd) - cmd_line: bytes = stdlib.malloc(clen + 1) - if cmd_line is None: - stdio.printf("FAIL: --build-self failed to allocate the copyself command line\n") - return -1 - string.strcpy(cmd_line, cmd) - - si: win32.win32process.STARTUPINFOA = win32.win32process.STARTUPINFOA() - pi: win32.win32process.PROCESS_INFORMATION = win32.win32process.PROCESS_INFORMATION() - string.memset(&si, 0, win32.win32process.STARTUPINFOA.__sizeof__()) - string.memset(&pi, 0, win32.win32process.PROCESS_INFORMATION.__sizeof__()) - si.cb = win32.win32process.STARTUPINFOA.__sizeof__() - - ok: int = win32.win32process.CreateProcessA(None, cmd_line, None, None, 1, 0, - None, cwd, &si, &pi) - if ok == 0: - err: uint32 = win32.win32base.GetLastError() - stdio.printf("FAIL: --build-self could not launch copyself (Win32 error %d)\n", int(err)) - stdio.printf(" command: %s\n", cmd) - stdlib.free(cmd_line) - return -1 - - stdio.printf("build-self: copyself launched (pid=%d)\n", int(pi.dwProcessId)) - win32.win32base.CloseHandle(pi.hProcess) - win32.win32base.CloseHandle(pi.hThread) - stdlib.free(cmd_line) - return 0 - - -# _SelfExeDir - directory containing the running compiler ("D:/x/viperc.exe" -> "D:/x"). -def _SelfExeDir(self_exe: str) -> str: - if self_exe is None: return None - n: sizet = string.strlen(self_exe) - cut: sizet = n - while cut > 0: - ch: char = self_exe[cut - 1] - if ch == '\\' or ch == '/': - cut -= 1 - break - cut -= 1 - if cut >= n: return None - out: str = stdlib.malloc(cut + 1) - if out is None: return None - if cut > 0: string.strncpy(out, self_exe, cut) - out[cut] = '\0' - return out - - -# _CrashHint - name the common NTSTATUS exit codes. The child inherits the parent console, so -# its own output is already visible; these codes are raised as fail-fast and carry no SEH -# exception, which is why a traceback handler inside the child cannot print a stack. -def _CrashHint(rc: int) -> None: - u: uint32 = uint32(rc) - if u == uint32(0xC0000005): - stdio.printf(" child crashed: access violation (0xC0000005)\n") - elif u == uint32(0xC0000374): - stdio.printf(" child crashed: heap corruption (0xC0000374) -- fail-fast, no SEH traceback\n") - elif u == uint32(0xC00000FD): - stdio.printf(" child crashed: stack overflow (0xC00000FD)\n") - elif u == uint32(0xC0000409): - stdio.printf(" child crashed: fail-fast / security check (0xC0000409)\n") - - -def RunSelfBuild(extra: str) -> int: - self_exe: str = _GetSelfExe() - if self_exe is None: - stdio.printf("FAIL: --build-self could not resolve the compiler executable path\n") - return 1 - root: str = _FindViperCRoot(self_exe) - if root is None: - stdio.printf("FAIL: --build-self could not locate the ViperC root\n") - stdio.printf(" expected both: project.vpj and copyself/project.vpj\n") - return 1 - - project: str = os.path.join(root, "project.vpj") - build_dir: str = os.path.join(root, SELF_BUILD_DIR) - copyself_exe: str = _FindCopySelfExe(root) - if copyself_exe is None: - stdio.printf("FAIL: copyself.exe not found under %s\n", os.path.join(root, "copyself")) - stdio.printf(" expected: output/copyself.exe or __vpcbuild__/output/copyself.exe\n") - stdio.printf(" build it first: viperc --project %s --clean\n", - os.path.join(os.path.join(root, "copyself"), "project.vpj")) - return 1 - - cmd: str = _FormatSelfBuildCommand(self_exe, project, build_dir, extra) - if cmd is None: - stdio.printf("FAIL: --build-self failed to build the compiler command line\n") - return 1 - stdio.printf("build-self: %s\n", cmd) - rc: int = BuildPipeline._run_cmd_wait(cmd) - if rc != 0: - stdio.printf("FAIL: --build-self compilation failed (exit=%d)\n", rc) - _CrashHint(rc) - return 1 - - built_exe: str = os.path.join(os.path.join(build_dir, "output"), "viperc.exe") - if not os.isfile(built_exe): - stdio.printf("FAIL: --build-self did not produce %s\n", built_exe) - return 1 - - # The requested arguments are relative to ViperC/copyself/output. Use that - # directory as the launch cwd even when copyself.exe lives under - # copyself/__vpcbuild__/output. - copyself_cwd: str = os.path.join(os.path.join(root, "copyself"), "output") - if not os.isdir(copyself_cwd): - if os.mkdir(copyself_cwd) != 0: - if not os.isdir(copyself_cwd): - err_mkdir: ULONG = win32.win32base.GetLastError() - stdio.printf("FAIL: --build-self could not create %s (Win32 error %d)\n", - copyself_cwd, int(err_mkdir)) - return 1 - # copyself replaces ViperC\__vpcbuild__ wholesale, so everything the compiler needs next to - # viperc.exe must be in the new output directory *before* the copy runs. ircore.dll is the - # important one: without it every PhaseB module silently falls back to spawning - # mlir-opt/mlir-translate/llc (3 processes per module), which is both slower and a - # different code path than the one being profiled. - self_dir: str = _SelfExeDir(self_exe) - if self_dir is not None: - src_dll: str = os.path.join(self_dir, "ircore.dll") - dst_dll: str = os.path.join(os.path.join(build_dir, "output"), "ircore.dll") - if os.isfile(src_dll) and not os.isfile(dst_dll): - cop: BOOL = win32.win32file.CopyFileA(src_dll, dst_dll, 0) - if cop == 0: - stdio.printf("build-self: warning: could not copy ircore.dll next to the new viperc.exe\n") - else: - stdio.printf("build-self: copied ircore.dll into the new output directory\n") - - copy_cmd: str = _FormatCopySelfCommand(copyself_exe) - if copy_cmd is None: - stdio.printf("FAIL: --build-self failed to build the copyself command line\n") - return 1 - if _SpawnDetached(copy_cmd, copyself_cwd) != 0: - return 1 - stdio.printf("build-self: __vpcbuild__ will be replaced after %d seconds\n", COPY_DELAY_SECONDS) - return 0 +from stdint import * +import stdio +import stdlib +import string +import viperlib +import os +import os.path +import sys +import shutil +import lib.Phase.BuildPipeline as BuildPipeline + + +define SELF_EXE_CAP = 4096 +define SELF_BUILD_DIR = "__vpcbuild_2__" +define COPY_DELAY_SECONDS = 5 + + +def _GetSelfExe() -> str: + return sys.executable() + + +def _TryRoot(dir: str) -> str: + if dir is None or len(dir) == 0: + return None + root: str = os.path.normpath(dir) + if root is None: + return None + project: str = os.path.join(root, "project.vpj") + copyself_project: str = os.path.join(os.path.join(root, "copyself"), "project.vpj") + if project is not None and copyself_project is not None: + if os.isfile(project) and os.isfile(copyself_project): + return root + return None + + +def _FindViperCRoot(self_exe: str) -> str: + base: str = os.path.dirname(self_exe) + if base is None: + base = "" + parent1: str = os.path.dirname(base) + parent2: str = os.path.dirname(parent1) + + # Executable-derived candidates first: cwd is normally the workspace root, + # not the compiler tree. + root: str = _TryRoot(base) + if root is not None: + return root + root = _TryRoot(parent1) + if root is not None: + return root + root = _TryRoot(parent2) + if root is not None: + return root + root = _TryRoot(os.path.join(base, "ViperC")) + if root is not None: + return root + root = _TryRoot(os.path.join(base, "../ViperC")) + if root is not None: + return root + root = _TryRoot(os.path.join(base, "../../ViperC")) + if root is not None: + return root + + # Fallback only when the executable is detached from the compiler tree. + cwd: str = os.getcwd() + root = _TryRoot(cwd) + if root is not None: + return root + if cwd is not None: + root = _TryRoot(os.path.join(cwd, "ViperC")) + if root is not None: + return root + root = _TryRoot(os.path.join(os.path.dirname(cwd), "ViperC")) + if root is not None: + return root + return None + + +def CollectForwardedArgs(argc: int, argv: str*) -> str: + """--build-self: argv minus --build-self / --project / --build-dir (and their values). + + No whitelist: any current or future flag is forwarded verbatim, so a self-build can + enable the same modes as a normal build (e.g. `viperc --build-self --spend-file p.txt + --vpf 3`). The three excluded entries are the ones the self-build invocation owns. + """ + if argv is None or argc <= 1: return "" + total: sizet = 1 + i: int = 1 + while i < argc: + a: str = argv[i] + if a is not None: + if string.strcmp(a, "--build-self") == 0: + i += 1 + continue + if string.strcmp(a, "--project") == 0 or string.strcmp(a, "--build-dir") == 0: + i += 2 + continue + total += len(a) + 1 + i += 1 + buf: str = stdlib.malloc(total + 1) + if buf is None: return "" + buf[0] = '\0' + i = 1 + while i < argc: + a2: str = argv[i] + if a2 is not None: + if string.strcmp(a2, "--build-self") == 0: + i += 1 + continue + if string.strcmp(a2, "--project") == 0 or string.strcmp(a2, "--build-dir") == 0: + i += 2 + continue + if buf[0] != '\0': string.strcat(buf, " ") + string.strcat(buf, a2) + i += 1 + return buf + + +def _FormatSelfBuildCommand(self_exe: str, project: str, build_dir: str, extra: str) -> str: + # `extra` is the caller's forwarded argument string (empty when nothing was forwarded): + # the rebuilt compiler then carries exactly the modes the user asked for. + extra_len: sizet = 0 + if extra is not None: extra_len = len(extra) + need: sizet = len(self_exe) + len(project) + len(build_dir) + extra_len + 64 + buf: bytes = stdlib.malloc(need) + if buf is None: + return None + viperlib.snprintf(buf, need, "\"%s\" --project \"%s\" --clean --build-dir \"%s\"", + self_exe, project, build_dir) + if extra is not None and extra_len > 0: + pos: sizet = string.strlen(str(buf)) + viperlib.snprintf(buf + pos, need - pos, " %s", extra) + return str(buf) + + +def _FindCopySelfExe(root: str) -> str: + base: str = os.path.join(root, "copyself") + standard: str = os.path.join(os.path.join(os.path.join(base, "__vpcbuild__"), "output"), "copyself.exe") + if os.isfile(standard): + return standard + legacy: str = os.path.join(os.path.join(base, "output"), "copyself.exe") + if os.isfile(legacy): + return legacy + return None + + +def _FormatCopySelfCommand(copyself_exe: str) -> str: + need: sizet = len(copyself_exe) + 128 + buf: bytes = stdlib.malloc(need) + if buf is None: + return None + viperlib.snprintf(buf, need, "\"%s\" %d ../../__vpcbuild_2__ ../../__vpcbuild__", + copyself_exe, COPY_DELAY_SECONDS) + return str(buf) + + +def _SpawnDetached(cmd: str, cwd: str) -> int: + if cmd is None or cwd is None: + stdio.printf("FAIL: --build-self cannot launch copyself without a command and working directory\n") + return -1 + pid: int = sys.spawn_detached(cmd, cwd) + if pid < 0: + stdio.printf("FAIL: --build-self could not launch copyself\n") + stdio.printf(" command: %s\n", cmd) + return -1 + stdio.printf("build-self: copyself launched (pid=%d)\n", pid) + return 0 + + +# _SelfExeDir - directory containing the running compiler ("D:/x/viperc.exe" -> "D:/x"). +def _SelfExeDir(self_exe: str) -> str: + if self_exe is None: return None + n: sizet = string.strlen(self_exe) + cut: sizet = n + while cut > 0: + ch: char = self_exe[cut - 1] + if ch == '\\' or ch == '/': + cut -= 1 + break + cut -= 1 + if cut >= n: return None + out: str = stdlib.malloc(cut + 1) + if out is None: return None + if cut > 0: string.strncpy(out, self_exe, cut) + out[cut] = '\0' + return out + + +# _CrashHint - name the common NTSTATUS exit codes. The child inherits the parent console, so +# its own output is already visible; these codes are raised as fail-fast and carry no SEH +# exception, which is why a traceback handler inside the child cannot print a stack. +def _CrashHint(rc: int) -> None: + u: uint32 = uint32(rc) + if u == uint32(0xC0000005): + stdio.printf(" child crashed: access violation (0xC0000005)\n") + elif u == uint32(0xC0000374): + stdio.printf(" child crashed: heap corruption (0xC0000374) -- fail-fast, no SEH traceback\n") + elif u == uint32(0xC00000FD): + stdio.printf(" child crashed: stack overflow (0xC00000FD)\n") + elif u == uint32(0xC0000409): + stdio.printf(" child crashed: fail-fast / security check (0xC0000409)\n") + + +def RunSelfBuild(extra: str) -> int: + self_exe: str = _GetSelfExe() + if self_exe is None: + stdio.printf("FAIL: --build-self could not resolve the compiler executable path\n") + return 1 + root: str = _FindViperCRoot(self_exe) + if root is None: + stdio.printf("FAIL: --build-self could not locate the ViperC root\n") + stdio.printf(" expected both: project.vpj and copyself/project.vpj\n") + return 1 + + project: str = os.path.join(root, "project.vpj") + build_dir: str = os.path.join(root, SELF_BUILD_DIR) + copyself_exe: str = _FindCopySelfExe(root) + if copyself_exe is None: + stdio.printf("FAIL: copyself.exe not found under %s\n", os.path.join(root, "copyself")) + stdio.printf(" expected: output/copyself.exe or __vpcbuild__/output/copyself.exe\n") + stdio.printf(" build it first: viperc --project %s --clean\n", + os.path.join(os.path.join(root, "copyself"), "project.vpj")) + return 1 + + cmd: str = _FormatSelfBuildCommand(self_exe, project, build_dir, extra) + if cmd is None: + stdio.printf("FAIL: --build-self failed to build the compiler command line\n") + return 1 + stdio.printf("build-self: %s\n", cmd) + rc: int = BuildPipeline._run_cmd_wait(cmd) + if rc != 0: + stdio.printf("FAIL: --build-self compilation failed (exit=%d)\n", rc) + _CrashHint(rc) + return 1 + + built_exe: str = os.path.join(os.path.join(build_dir, "output"), "viperc.exe") + if not os.isfile(built_exe): + stdio.printf("FAIL: --build-self did not produce %s\n", built_exe) + return 1 + + # The requested arguments are relative to ViperC/copyself/output. Use that + # directory as the launch cwd even when copyself.exe lives under + # copyself/__vpcbuild__/output. + copyself_cwd: str = os.path.join(os.path.join(root, "copyself"), "output") + if not os.isdir(copyself_cwd): + if os.mkdir(copyself_cwd) != 0: + if not os.isdir(copyself_cwd): + stdio.printf("FAIL: --build-self could not create %s\n", copyself_cwd) + return 1 + # copyself replaces ViperC\__vpcbuild__ wholesale, so everything the compiler needs next to + # viperc.exe must be in the new output directory *before* the copy runs. ircore.dll is the + # important one: without it every PhaseB module silently falls back to spawning + # mlir-opt/mlir-translate/llc (3 processes per module), which is both slower and a + # different code path than the one being profiled. + self_dir: str = _SelfExeDir(self_exe) + if self_dir is not None: + src_dll: str = os.path.join(self_dir, "ircore.dll") + dst_dll: str = os.path.join(os.path.join(build_dir, "output"), "ircore.dll") + if os.isfile(src_dll) and not os.isfile(dst_dll): + if shutil.copyfile(src_dll, dst_dll) != 0: + stdio.printf("build-self: warning: could not copy ircore.dll next to the new viperc.exe\n") + else: + stdio.printf("build-self: copied ircore.dll into the new output directory\n") + + copy_cmd: str = _FormatCopySelfCommand(copyself_exe) + if copy_cmd is None: + stdio.printf("FAIL: --build-self failed to build the copyself command line\n") + return 1 + if _SpawnDetached(copy_cmd, copyself_cwd) != 0: + return 1 + stdio.printf("build-self: __vpcbuild__ will be replaced after %d seconds\n", COPY_DELAY_SECONDS) + return 0 diff --git a/App/lib/core/VLogger.vp b/App/lib/core/VLogger.vp index 22d7f6e..04592fd 100644 --- a/App/lib/core/VLogger.vp +++ b/App/lib/core/VLogger.vp @@ -1,197 +1,194 @@ -from stdint import * -import stdio -import memhub -import sys -import win32.win32console as win32cmd -import win32.win32file as win32file -import win32.win32base as win32base - - -# VLogger - native logger with colored Windows console output. -# Format: ├ LEVEL: message or ├ LEVEL[category]: message (aligned with TPC lib/core/VLogger.py). - -# Error recovery (REPL/JIT): after error, longjmp back to the REPL setjmp point. -# mingw-w64 x86_64: longjmp is void longjmp(jmp_buf, int); setjmp expands to _setjmp(buf, NULL). -extern def longjmp(buf: rawptr, val: int) - - -# REPL recovery-point pointer: registered at session start, cleared at session end. -# Covers the whole fragment pipeline; batch mode never sets it, so it stays None and exits via sys.exit(1). -_repl_jmpbuf: rawptr = None - - -# repl_abort - the single "terminate after diagnosis" exit in VPC. -# Caller already printed the diagnostic; returns to REPL via longjmp if a recovery point is set, else sys.exit(1). -def repl_abort() -> int: - """Unified exit after diagnosis: longjmp to the REPL if a recovery point is set, else exit.""" - if _repl_jmpbuf is not None: - stdio.fflush(None) - longjmp(_repl_jmpbuf, 1) - sys.exit(1) - return 0 - - -def set_repl_jmpbuf(buf: rawptr) -> int: - """Register the REPL jmp_buf; idempotent at each fragment entry.""" - global _repl_jmpbuf - _repl_jmpbuf = buf - return 0 - - -def clear_repl_jmpbuf() -> int: - """Clear the registered jmp_buf at REPL session end.""" - global _repl_jmpbuf - _repl_jmpbuf = None - return 0 - -# Log levels -enum LogLevel: - DEBUG, INFO, WARNING, ERROR, SUCCESS - - -# Win32 console foreground color attributes -# 值用 WORD(...) 显式定宽为 uint16(与 _log 的 sym_color/level_color 形参一致)。 -# 注意:仅写 `define X: WORD = 0x0002` 不够 —— define 的宽度由**右侧表达式的类型** -# 决定(win32file.vp 的 `define STD_OUTPUT_HANDLE: DWORD = DWORD(-10)` 同款), -# 否则常量仍是 i32 → 传给 WORD 形参即 32→16 隐式窄化。 -define FOREGROUND_BLUE: WORD = WORD(0x0001) -define FOREGROUND_GREEN: WORD = WORD(0x0002) -define FOREGROUND_RED: WORD = WORD(0x0004) -define FOREGROUND_INTENSITY: WORD = WORD(0x0008) -define FOREGROUND_WHITE: WORD = WORD(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE) -define FOREGROUND_YELLOW: WORD = WORD(FOREGROUND_RED | FOREGROUND_GREEN) - -novirtual class Logger: - _level: int - _console_handle: win32base.HANDLE - _use_color: int - jit_flag: bool - __mbuddy__: memhub.MemBuddy* - - def __init__(self, level: int = 1): - self._level = level - self._console_handle = win32file.GetStdHandle(win32file.STD_OUTPUT_HANDLE) - self._use_color = 1 - self.jit_flag = False - self.__mbuddy__ = _mbuddy - - def _set_color(self, attr: WORD) -> int: - uc: int = self._use_color - if uc: - ch: win32base.HANDLE = self._console_handle - ret: int = win32cmd.SetConsoleTextAttribute(ch, attr) - return ret - return 0 - - def _reset_color(self) -> int: - if self._use_color: return win32cmd.SetConsoleTextAttribute(self._console_handle, FOREGROUND_WHITE) - return 0 - - # _log - core log output with segmented coloring (prefix symbol / level tag / message). - # Format: ├ LEVEL: msg or ├ LEVEL[category]: msg. - def _log(self, level: int, prefix_sym: str, level_str: str, msg: str, sym_color: WORD, level_color: WORD, category: str = "") -> int: - # Default level is INFO; DEBUG messages stay silent unless the level is lowered. - if level < self._level: return 0 - # Color the prefix symbol - self._set_color(sym_color) - stdio.printf("%s ", prefix_sym) - # Color the level tag - self._set_color(level_color) - stdio.printf("%s", level_str) - self._reset_color() - # Optional category - if category is not None and category[0] != 0: stdio.printf(" [%s]", category) - stdio.printf(": %s\n", msg) - return 0 - - def debug(self, msg: str, category: str = "") -> int: - return self._log(LogLevel.DEBUG, "├", "DEBUG", msg, FOREGROUND_INTENSITY, FOREGROUND_INTENSITY, category) - - def info(self, msg: str, category: str = "") -> int: - ret: int = self._log(LogLevel.INFO, "├", "INFO", msg, FOREGROUND_GREEN, WORD(FOREGROUND_GREEN | FOREGROUND_INTENSITY), category) - return ret - - def warning(self, msg: str, category: str = "") -> int: - return self._log(LogLevel.WARNING, "├", "WARN", msg, FOREGROUND_YELLOW, WORD(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY), category) - - def error(self, msg: str, category: str = "") -> int: - ret: int = self._log(LogLevel.ERROR, "├", "ERROR", msg, FOREGROUND_RED, WORD(FOREGROUND_RED | FOREGROUND_INTENSITY), category) - # JIT/REPL: diagnostic already printed; jump back to the REPL loop via the unified exit. - if self.jit_flag: - repl_abort() - return ret - # Batch mode: terminate immediately on error. - sys.exit(1) - return ret - - def success(self, msg: str, category: str = "") -> int: - return self._log(LogLevel.SUCCESS, "├", "SUCCESS", msg, FOREGROUND_GREEN, WORD(FOREGROUND_GREEN | FOREGROUND_INTENSITY), category) - - def set_jit_flag(self, v: bool) -> int: - """Set the JIT/REPL flag: True makes error longjmp back to the REPL loop.""" - self.jit_flag = v - return 0 - - # Advanced log methods. - - def banner(self, msg: str) -> int: - """Print a blue-highlighted section banner.""" - self._set_color(FOREGROUND_BLUE | FOREGROUND_INTENSITY) - stdio.printf("\n=== %s ===\n\n", msg) - self._reset_color() - return 0 - -# Global mbuddy pointer (injected by lib.InitLib). -_mbuddy: memhub.MemBuddy* - -# Global logger pointer -logger: Logger* -log: Logger* - -def init_logger(): - """Initialize the global logger instance.""" - global logger, log - raw: rawptr = _mbuddy.alloc(Logger.__sizeof__()) - # if raw is None: return None (memory management still unclear) - logger = raw - logger.__before_init__() - logger.__init__(LogLevel.INFO) - log = logger - # if fmtbuffer is None: return None - - -# --nowarn-register switch: suppresses Phase A dependency WARN output (output only). -_nowarn_register: int = 0 - -# Module-level convenience functions (aligned with TPC lib.core.VLogger; no get_logger() needed). - -def info(msg: str, category: str = "") -> int: - """Log at INFO level.""" - if log is not None: return log.info(msg, category) - return 0 - - -def warning(msg: str, category: str = "") -> int: - """Log at WARN level.""" - if log is not None: return log.warning(msg, category) - return 0 - - -def error(msg: str, category: str = "") -> int: - """Log at ERROR level: exit in batch mode, longjmp to REPL under JIT/REPL.""" - if log is not None: return log.error(msg, category) - # When the logger is unavailable, still use the unified exit (REPL can recover if registered) - repl_abort() - return 0 - - -def success(msg: str, category: str = "") -> int: - """Log at SUCCESS level.""" - if log is not None: return log.success(msg, category) - return 0 - - -def debug(msg: str, category: str = "") -> int: - """Log at DEBUG level.""" - if log is not None: return log.debug(msg, category) - return 0 +from stdint import * +import stdio +import memhub +import sys +import win32.win32console as win32cmd +import win32.win32file as win32file +import win32.win32base as win32base + + +# VLogger - native logger with colored Windows console output. +# Format: ├ LEVEL: message or ├ LEVEL[category]: message (aligned with TPC lib/core/VLogger.py). + +# Error recovery (REPL/JIT): after error, longjmp back to the REPL setjmp point. +# mingw-w64 x86_64: longjmp is void longjmp(jmp_buf, int); setjmp expands to _setjmp(buf, NULL). +extern def longjmp(buf: rawptr, val: int) + + +# REPL recovery-point pointer: registered at session start, cleared at session end. +# Covers the whole fragment pipeline; batch mode never sets it, so it stays None and exits via sys.exit(1). +_repl_jmpbuf: rawptr = None + + +# repl_abort - the single "terminate after diagnosis" exit in VPC. +# Caller already printed the diagnostic; returns to REPL via longjmp if a recovery point is set, else sys.exit(1). +def repl_abort() -> int: + """Unified exit after diagnosis: longjmp to the REPL if a recovery point is set, else exit.""" + if _repl_jmpbuf is not None: + stdio.fflush(None) + longjmp(_repl_jmpbuf, 1) + sys.exit(1) + return 0 + + +def set_repl_jmpbuf(buf: rawptr) -> int: + """Register the REPL jmp_buf; idempotent at each fragment entry.""" + global _repl_jmpbuf + _repl_jmpbuf = buf + return 0 + + +def clear_repl_jmpbuf() -> int: + """Clear the registered jmp_buf at REPL session end.""" + global _repl_jmpbuf + _repl_jmpbuf = None + return 0 + +# Log levels +enum LogLevel: + DEBUG, INFO, WARNING, ERROR, SUCCESS + + +# Console color attributes (restored: color output is enabled on the Windows console). +# 值用 uint16(...) 显式定宽为 uint16(与 _log 的 sym_color/level_color 形参一致)。 +# 注意:仅写 `define X: WORD = 0x0002` 不够 —— define 的宽度由**右侧表达式的类型** +# 决定(win32file.vp 的 `define STD_OUTPUT_HANDLE: DWORD = Duint16(-10)` 同款), +# 否则常量仍是 i32 → 传给 WORD 形参即 32→16 隐式窄化。 +define FOREGROUND_BLUE: uint16 = uint16(0x0001) +define FOREGROUND_GREEN: uint16 = uint16(0x0002) +define FOREGROUND_RED: uint16 = uint16(0x0004) +define FOREGROUND_INTENSITY: uint16 = uint16(0x0008) +define FOREGROUND_WHITE: uint16 = uint16(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE) +define FOREGROUND_YELLOW: uint16 = uint16(FOREGROUND_RED | FOREGROUND_GREEN) + +novirtual class Logger: + _level: int + _console_handle: win32base.HANDLE + _use_color: int + jit_flag: bool + __mbuddy__: memhub.MemBuddy* + + def __init__(self, level: int = 1): + self._level = level + self._console_handle = win32file.GetStdHandle(win32file.STD_OUTPUT_HANDLE) + self._use_color = 1 + self.jit_flag = False + self.__mbuddy__ = _mbuddy + + def _set_color(self, attr: uint16) -> int: + if self._use_color: + return win32cmd.SetConsoleTextAttribute(self._console_handle, attr) + return 0 + + def _reset_color(self) -> int: + if self._use_color: return win32cmd.SetConsoleTextAttribute(self._console_handle, FOREGROUND_WHITE) + return 0 + + # _log - core log output with segmented coloring (prefix symbol / level tag / message). + # Format: ├ LEVEL: msg or ├ LEVEL[category]: msg. + def _log(self, level: int, prefix_sym: str, level_str: str, msg: str, sym_color: uint16, level_color: uint16, category: str = "") -> int: + # Default level is INFO; DEBUG messages stay silent unless the level is lowered. + if level < self._level: return 0 + # Color the prefix symbol + self._set_color(sym_color) + stdio.printf("%s ", prefix_sym) + # Color the level tag + self._set_color(level_color) + stdio.printf("%s", level_str) + self._reset_color() + # Optional category + if category is not None and category[0] != 0: stdio.printf(" [%s]", category) + stdio.printf(": %s\n", msg) + return 0 + + def debug(self, msg: str, category: str = "") -> int: + return self._log(LogLevel.DEBUG, "├", "DEBUG", msg, FOREGROUND_INTENSITY, FOREGROUND_INTENSITY, category) + + def info(self, msg: str, category: str = "") -> int: + ret: int = self._log(LogLevel.INFO, "├", "INFO", msg, FOREGROUND_GREEN, uint16(FOREGROUND_GREEN | FOREGROUND_INTENSITY), category) + return ret + + def warning(self, msg: str, category: str = "") -> int: + return self._log(LogLevel.WARNING, "├", "WARN", msg, FOREGROUND_YELLOW, uint16(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY), category) + + def error(self, msg: str, category: str = "") -> int: + ret: int = self._log(LogLevel.ERROR, "├", "ERROR", msg, FOREGROUND_RED, uint16(FOREGROUND_RED | FOREGROUND_INTENSITY), category) + # JIT/REPL: diagnostic already printed; jump back to the REPL loop via the unified exit. + if self.jit_flag: + repl_abort() + return ret + # Batch mode: terminate immediately on error. + sys.exit(1) + return ret + + def success(self, msg: str, category: str = "") -> int: + return self._log(LogLevel.SUCCESS, "├", "SUCCESS", msg, FOREGROUND_GREEN, uint16(FOREGROUND_GREEN | FOREGROUND_INTENSITY), category) + + def set_jit_flag(self, v: bool) -> int: + """Set the JIT/REPL flag: True makes error longjmp back to the REPL loop.""" + self.jit_flag = v + return 0 + + # Advanced log methods. + + def banner(self, msg: str) -> int: + """Print a blue-highlighted section banner.""" + self._set_color(FOREGROUND_BLUE | FOREGROUND_INTENSITY) + stdio.printf("\n=== %s ===\n\n", msg) + self._reset_color() + return 0 + +# Global mbuddy pointer (injected by lib.InitLib). +_mbuddy: memhub.MemBuddy* + +# Global logger pointer +logger: Logger* +log: Logger* + +def init_logger(): + """Initialize the global logger instance.""" + global logger, log + raw: rawptr = _mbuddy.alloc(Logger.__sizeof__()) + # if raw is None: return None (memory management still unclear) + logger = raw + logger.__before_init__() + logger.__init__(LogLevel.INFO) + log = logger + # if fmtbuffer is None: return None + + +# --nowarn-register switch: suppresses Phase A dependency WARN output (output only). +_nowarn_register: int = 0 + +# Module-level convenience functions (aligned with TPC lib.core.VLogger; no get_logger() needed). + +def info(msg: str, category: str = "") -> int: + """Log at INFO level.""" + if log is not None: return log.info(msg, category) + return 0 + + +def warning(msg: str, category: str = "") -> int: + """Log at WARN level.""" + if log is not None: return log.warning(msg, category) + return 0 + + +def error(msg: str, category: str = "") -> int: + """Log at ERROR level: exit in batch mode, longjmp to REPL under JIT/REPL.""" + if log is not None: return log.error(msg, category) + # When the logger is unavailable, still use the unified exit (REPL can recover if registered) + repl_abort() + return 0 + + +def success(msg: str, category: str = "") -> int: + """Log at SUCCESS level.""" + if log is not None: return log.success(msg, category) + return 0 + + +def debug(msg: str, category: str = "") -> int: + """Log at DEBUG level.""" + if log is not None: return log.debug(msg, category) + return 0 diff --git a/App/lib/core/builtin/ViperBuild.vp b/App/lib/core/builtin/ViperBuild.vp index e8931f5..ca57dde 100644 --- a/App/lib/core/builtin/ViperBuild.vp +++ b/App/lib/core/builtin/ViperBuild.vp @@ -1,1562 +1,1537 @@ -"""Compiler-side implementation of the `viperbuild` built-in macro library (--src Pass 0).""" -from stdint import * -from win32.win32base import * -import memhub -import string -import stdlib -import ast -import win32.win32file as win32file -import lib.Projectrans.Utils as Utils -import lib.Projectrans.Config as Config -import lib.core.VLogger as VLogger -import lib.core.IncludesScanner as IncludesScanner -import stdio - -# kernel32!GetEnvironmentVariableA (DWORD(LPCSTR, LPSTR, DWORD)) - declared here, same as includes/traceback.vp; -# win32base.vp omits it but kernel32 is linked anyway; only for selfmachine host detection. -extern def GetEnvironmentVariableA(lpName: LPCSTR, lpBuffer: CHARPTR, nSize: DWORD) -> DWORD - -# Path/source buffer caps (same convention as main.vp's SRC_BUF_SIZE) -define VB_PATH_CAP = 1024 -define VB_SRC_CAP = 1048576 -# Scratch pool size (BFS working set + constant tables; not part of main-pool reset) -define VB_SCRATCH = 8388608 -# Visited cap (cycle defense; fatal if the closure greatly exceeds it) -define VB_MAX_VISIT = 4096 -# Element separator for list constants: the char code of newline (source paths/flags never contain newlines). -# Must store the code then char(): in Viper 'c' literals are single-char strings (i8*), not assignable to a char slot. -define VB_SEP_CODE = 10 - -# Module state -_scratch: memhub.MemBuddy* = None -_entry_dir: str = None # entry file dir (default source; normalized to "." for bare filenames) -_search_dirs: str = None # closure search roots (space-separated): entry dir first, then "." -_work: list[str]* = None # BFS pending file paths -_visited: list[str]* = None # processed file paths -_pending: list[str]* = None # import module names collected in one file (pending BFS) -_proj_vars: list[str]* = None # variable names bound to a vb.Project(...) instance -_alias: str = None # local alias of viperbuild (import viperbuild as vb -> "vb") -# ffilib 的本地别名(import ffilib / import ffilib as F);ffilib.CDLL(...) 是纯构建配置 -_ffi_alias: str = None -# ffilib.CDLL 句柄表(两个平行列表):变量名 / runtime(1=运行时加载, 0=链接期) -# 供 `extern name(...)` 校验句柄来源与选择行为。 -_ffi_handles_names: list[str]* = None -_ffi_handles_rt: list[int]* = None -# 句柄对应的库路径(runtime=True 时 thunk 直接用绝对路径 LoadLibraryA) -_ffi_handles_path: list[str]* = None -_ctor_name: str = None # from viperbuild import Project as P -> "P" - -# Per-file constant tables (valid only while scanning that file; pool-allocated) -_f_str_names: list[str]* = None -_f_str_vals: list[str]* = None -_f_list_names: list[str]* = None -_f_list_vals: list[str]* = None - -# Constant-eval output slot -_g_out_str: str = None - -# Extracted config (all strings stdlib.malloc) -_c_name: str = None -_c_version: str = None -_c_source: str = None -_c_buildroot: str = None -_c_includes: str = None -_c_compiler: str = None -_c_linker: str = None -_c_static_libs: str = None -_c_dynamic_libs: str = None -_c_output: str = None -_c_triple: str = None -_c_datalayout: str = None -_c_selfmachine: int = 0 -_c_has_project: int = 0 -_c_has_build: int = 0 -# proj.advanced.* 高级开关(等价 CLI 的 --asan / --traceback) -_c_adv_asan: int = 0 -_c_adv_traceback: int = 0 -# proj.clean.* 细粒度清理(等价 --clean 的子集;build_dir = 整个 __vpcbuild__) -_c_clean_all: int = 0 -_c_clean_output: int = 0 -_c_clean_temp: int = 0 -_c_clean_build_dir: int = 0 -_c_clean_includes: int = 0 -# build(run=, args=):编译后直接运行(args 非空时隐含 run) -_c_run: int = 0 -_c_run_args: str = None - -# Whether `import viperbuild` / `from viperbuild import ...` appeared in the closure (diagnostics only) -_saw_viperbuild: int = 0 - -# Whether applied successfully (for main.vp to decide viperbuild's linker.output) -_applied: int = 0 - - -# Basic helpers -def _DupStd(s: str) -> str: - """Deep-copy a string with stdlib.malloc so it survives reset_to.""" - if s is None: return None - n: sizet = string.strlen(s) - buf: str = stdlib.malloc(n + 1) - if buf is None: return None - string.strcpy(buf, s) - return buf - - -def _DupScratch(s: str) -> str: - """Deep-copy a string into the scratch pool, unaffected by main-pool reset_to.""" - if s is None or _scratch is None: return None - n: sizet = string.strlen(s) - buf: str = _scratch.alloc(n + 1) - if buf is None: return None - string.strcpy(buf, s) - return buf - - -def _Join2(a: str, b: str) -> str: - """Join a and b with a slash; returns b when a is empty.""" - if a is None: return b - if b is None: return a - al: sizet = string.strlen(a) - if al == 0: return b - bl: sizet = string.strlen(b) - buf: str = stdlib.malloc(al + 1 + bl + 1) - if buf is None: return None - string.strcpy(buf, a) - buf[al] = '/' - string.strcpy(buf + al + 1, b) - return buf - - -def _DirOf(path: str) -> str: - """Return the directory part of path (no trailing separator); empty string if none.""" - if path is None: return None - n: sizet = string.strlen(path) - i: sizet = n - while i > 0: - i -= 1 - ch: char = path[i] - if ch == '/' or ch == '\\': - buf: str = stdlib.malloc(i + 1) - if buf is None: return None - string.strncpy(buf, path, i) - buf[i] = '\0' - return buf - return _DupStd("") - - -def _BaseNameOf(path: str) -> str: - """Return the file name of path, including its extension.""" - if path is None: return None - n: sizet = string.strlen(path) - start: sizet = 0 - for i in range(int(n)): - ch: char = path[i] - if ch == '/' or ch == '\\': start = i + 1 - return _DupStd(path + start) - - -def _IsDir(path: str) -> int: - """Whether path is an existing directory; 1=yes.""" - if path is None: return 0 - attr: ULONG = win32file.GetFileAttributesA(path) - if attr == 0xFFFFFFFF: return 0 - if (attr & win32file.FILE_ATTRIBUTE_DIRECTORY) != 0: return 1 - return 0 - - -def _HasPrefix(s: str, prefix: str) -> int: - """Whether s starts with prefix; 1=yes (None/empty/"." counts as true).""" - if s is None: return 0 - if prefix is None: return 1 - if string.strcmp(prefix, ".") == 0: return 1 - if string.strncmp(s, "./", 2) == 0: s = s + 2 - if string.strncmp(prefix, "./", 2) == 0: prefix = prefix + 2 - pl: sizet = string.strlen(prefix) - if pl == 0: return 1 - sl: sizet = string.strlen(s) - if sl < pl: return 0 - if string.strncmp(s, prefix, pl) == 0: return 1 - return 0 - - -# Constant tables -def _FindInList(names: list[str]*, name: str) -> int: - """Linear-search names for name; return its index, or -1 if not found.""" - if names is None or name is None: return -1 - n: sizet = names.__len__() - for i in range(int(n)): - s: str = names.get(i) - if s is not None: - if string.strcmp(s, name) == 0: return int(i) - return -1 - - -def _LookupStrConst(name: str) -> int: - """Resolve a name to a string constant in _g_out_str; 0=failure.""" - global _g_out_str - idx: int = _FindInList(_f_str_names, name) - if idx < 0: return 0 - v: str = _f_str_vals.get(sizet(idx)) - if v is None: return 0 - _g_out_str = v - return 1 - - -def _LookupListConst(name: str, out: list[str]*) -> int: - """Resolve a name to a list constant, split into out; 0=failure.""" - idx: int = _FindInList(_f_list_names, name) - if idx < 0: return 0 - v: str = _f_list_vals.get(sizet(idx)) - if v is None: return 0 - return _SplitInto(out, v) - - -def _SplitInto(out: list[str]*, joined: str) -> int: - """Split a VB_SEP_CODE-joined string into out; empty string means empty list.""" - if out is None or joined is None: return 0 - n: sizet = string.strlen(joined) - if n == 0: return 1 - start: sizet = 0 - for i in range(int(n + 1)): - if i == n or joined[i] == char(VB_SEP_CODE): - seg: str = stdlib.malloc(i - start + 1) - if seg is None: return 0 - string.strncpy(seg, joined + start, i - start) - seg[i - start] = '\0' - out.append(_DupScratch(seg)) - stdlib.free(seg) - start = i + 1 - return 1 - - -def _JoinList(lst: list[str]*, sep: char, use_scratch: int) -> str: - """Join a list[str] with sep; use_scratch selects the scratch pool allocator.""" - if lst is None: return None - n: sizet = lst.__len__() - if n == 0: return None - total: sizet = 0 - i: sizet = 0 - while i < n: - s: str = lst.get(i) - if s is not None: total += string.strlen(s) + 1 - i += 1 - if total == 0: return None - buf: str = None - if use_scratch != 0: buf = _scratch.alloc(total + 1) - else: buf = stdlib.malloc(total + 1) - if buf is None: return None - pos: sizet = 0 - for i in range(int(n)): - s = lst.get(i) - if s is not None: - sl: sizet = string.strlen(s) - if pos > 0: - buf[pos] = sep - pos += 1 - string.strcpy(buf + pos, s) - pos += sl - buf[pos] = '\0' - return buf - - -def _JoinSep(lst: list[str]*) -> str: - """Join a list[str] with VB_SEP_CODE (pool-allocated); None for an empty list.""" - return _JoinList(lst, char(VB_SEP_CODE), 1) - - -def _JoinSpace(lst: list[str]*) -> str: - """Join a list[str] with spaces (stdlib-allocated); None for an empty list.""" - return _JoinList(lst, ' ', 0) - - -def _FirstSpaceTok(joined: str) -> str: - """Return the first space-separated token (stdlib-allocated); None if empty.""" - if joined is None: return None - n: sizet = string.strlen(joined) - if n == 0: return None - sp: sizet = n - for i in range(int(n)): - if joined[i] == ' ': - sp = i - break - seg: str = stdlib.malloc(sp + 1) - if seg is None: return None - string.strncpy(seg, joined, sp) - seg[sp] = '\0' - return seg - - -# Constant expression evaluation -def _EvalStr(node: ast.AST*) -> int: - """Evaluate a string constant expression into _g_out_str; 0=failure.""" - global _g_out_str - if node is None: return 0 - k: int = node.kind() - if k == ast.ASTKind.Constant: - cn: ast.Constant* = ast.Constant*(node) - if cn is None or cn.const_kind != ast.CONST_STR: return 0 - _g_out_str = cn.str_val - return 1 - if k == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(node) - if nm is None or nm.id is None: return 0 - return _LookupStrConst(nm.id) - if k == ast.ASTKind.BinOp: - bop: ast.BinOp* = ast.BinOp*(node) - if bop is None or bop.op != ast.OpKind.Add: return 0 - if _EvalStr(bop.left) == 0: return 0 - lv: str = _g_out_str - if _EvalStr(bop.right) == 0: return 0 - rv: str = _g_out_str - _g_out_str = _ConcatStd(lv, rv) - return 1 - return 0 - - -def _ConcatStd(a: str, b: str) -> str: - """Concatenate two strings with stdlib allocation.""" - if a is None: return b - if b is None: return a - al: sizet = string.strlen(a) - bl: sizet = string.strlen(b) - buf: str = stdlib.malloc(al + bl + 1) - if buf is None: return None - string.strcpy(buf, a) - string.strcpy(buf + al, b) - return buf - - -def _EvalList(node: ast.AST*, out: list[str]*) -> int: - """Evaluate a list[str] constant expression into out; 0=failure.""" - if node is None or out is None: return 0 - k: int = node.kind() - if k == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(node) - if nm is None or nm.id is None: return 0 - return _LookupListConst(nm.id, out) - if k == ast.ASTKind.List: - lst: ast.List* = ast.List*(node) - if lst is None or lst.elts is None: return 0 - n: sizet = lst.elts.__len__() - for i in range(int(n)): - e: ast.AST* = lst.elts.get(i) - if e is None: return 0 - if _IsListUnpack(e) != 0: - inner: ast.AST* = _ListUnpackOperand(e) - if inner is None: return 0 - if _EvalList(inner, out) == 0: return 0 - else: - if _EvalStr(e) == 0: return 0 - out.append(_DupScratch(_g_out_str)) - return 1 - if k == ast.ASTKind.BinOp: - bop: ast.BinOp* = ast.BinOp*(node) - if bop is None or bop.op != ast.OpKind.Add: return 0 - if _EvalList(bop.left, out) == 0: return 0 - if _EvalList(bop.right, out) == 0: return 0 - return 1 - return 0 - - -# Macro-list unpacking: `[*COMMON_FLAGS]` / `A + [*B]`. -# The parser has no Starred node in list displays: a leading `*` in an element position is -# parsed as a deref UnaryOp (expression-position `*x` is pointer deref). Inside a compile-time -# macro list the only meaningful interpretation is unpacking, so both shapes are accepted here. -# Keep in sync with _ListUnpackOperand. -def _IsListUnpack(node: ast.AST*) -> int: - """Whether a list element is an unpacking form (`*X`); 1=yes / 0=no.""" - if node is None: return 0 - k: int = node.kind() - if k == ast.ASTKind.Starred: return 1 - if k == ast.ASTKind.UnaryOp: - uo: ast.UnaryOp* = ast.UnaryOp*(node) - if uo is not None and uo.op == ast.OpKind.Deref: return 1 - return 0 - - -def _ListUnpackOperand(node: ast.AST*) -> ast.AST*: - """Operand of an unpacking element (`*X` -> X); None when node is not an unpacking form.""" - if _IsListUnpack(node) == 0: return None - if node.kind() == ast.ASTKind.Starred: - st: ast.Starred* = ast.Starred*(node) - if st is None: return None - return st.value - uo: ast.UnaryOp* = ast.UnaryOp*(node) - if uo is None: return None - return uo.operand - - -def _EvalBool(node: ast.AST*) -> int: - """Evaluate a bool constant; -1 means it is not a bool constant.""" - if node is None: return -1 - if node.kind() != ast.ASTKind.Constant: return -1 - cn: ast.Constant* = ast.Constant*(node) - if cn is None or cn.const_kind != ast.CONST_BOOL: return -1 - if cn.int_val != 0: return 1 - return 0 - - -# Macro-reference detection (reading is forbidden) -def _IsProjRef(node: ast.AST*) -> int: - """Whether a node directly references a Project macro object; 1=yes.""" - if node is None: return 0 - k: int = node.kind() - if k == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(node) - if nm is None or nm.id is None: return 0 - if _FindInList(_proj_vars, nm.id) >= 0: return 1 - return 0 - if k == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(node) - if at is None or at.value is None: return 0 - return _IsProjRef(at.value) - return 0 - - -# Single-file scan -def _ResetFileTables(pool: memhub.MemBuddy*) -> int: - """Rebuild the per-file constant tables (pool-allocated).""" - global _f_str_names, _f_str_vals, _f_list_names, _f_list_vals - _f_str_names = list[str](pool, 8) - _f_str_vals = list[str](pool, 8) - _f_list_names = list[str](pool, 8) - _f_list_vals = list[str](pool, 8) - if _f_str_names is None or _f_str_vals is None: return 1 - if _f_list_names is None or _f_list_vals is None: return 1 - return 0 - - -def _AddPending(mod_name: str) -> None: - """Append a module name to the BFS queue (deep-copied into the scratch pool).""" - if mod_name is None or _pending is None: return - if string.strlen(mod_name) == 0: return - _pending.append(_DupScratch(mod_name)) - - -def _HandleImport(stmt: ast.Import*) -> int: - """Handle `import X [as Y]`: detect viperbuild/ffilib, queue other modules for BFS.""" - global _alias, _ffi_alias, _saw_viperbuild - if stmt is None or stmt.names is None: return 0 - n: sizet = stmt.names.__len__() - for i in range(int(n)): - al: ast.Alias* = ast.Alias*(stmt.names.get(i)) - if al is not None and al.name is not None: - if string.strcmp(al.name, "viperbuild") == 0: - _saw_viperbuild = 1 - if al.asname is not None: _alias = _DupScratch(al.asname) - else: _alias = _DupScratch("viperbuild") - elif string.strcmp(al.name, "ffilib") == 0: - # 宏库:不参与 BFS,只记别名(CDLL 语句由 _HandleCDLL 抽取)。 - # _ffilib 的依赖只在真正出现 runtime=True 的 CDLL 时才加(见 _RegisterFfiHandle), - # 纯链接期(runtime=False)的工程不需要编译这个运行时库。 - if al.asname is not None: _ffi_alias = _DupScratch(al.asname) - else: _ffi_alias = _DupScratch("ffilib") - else: _AddPending(al.name) - return 0 - - -def _HandleImportFrom(stmt: ast.ImportFrom*) -> int: - """Handle `from X import Y`: detect viperbuild's Project, queue other modules.""" - global _ctor_name, _saw_viperbuild - if stmt is None: return 0 - if stmt.level != 0: - # Relative imports are not part of the local closure (no package context: skip) - return 0 - if stmt.module is None: return 0 - if string.strcmp(stmt.module, "viperbuild") != 0: - _AddPending(stmt.module) - return 0 - _saw_viperbuild = 1 - if stmt.names is None: return 0 - n: sizet = stmt.names.__len__() - for i in range(int(n)): - al: ast.Alias* = ast.Alias*(stmt.names.get(i)) - if al is not None and al.name is not None: - if string.strcmp(al.name, "Project") == 0: - if al.asname is not None: _ctor_name = _DupScratch(al.asname) - else: _ctor_name = _DupScratch("Project") - return 0 - - -def _IsProjectCtor(vnode: ast.AST*) -> int: - """Whether an expression is a vb.Project(...) or Project(...) call; 1=yes.""" - if vnode is None: return 0 - if vnode.kind() != ast.ASTKind.Call: return 0 - cl: ast.Call* = ast.Call*(vnode) - if cl is None or cl.func is None: return 0 - fk: int = cl.func.kind() - if fk == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(cl.func) - if at is None or at.attr is None or at.value is None: return 0 - if at.value.kind() != ast.ASTKind.Name: return 0 - bn: ast.Name* = ast.Name*(at.value) - if bn is None or bn.id is None or _alias is None: return 0 - if string.strcmp(bn.id, _alias) != 0: return 0 - if string.strcmp(at.attr, "Project") == 0: return 1 - return 0 - if fk == ast.ASTKind.Name: - nm: ast.Name* = ast.Name*(cl.func) - if nm is None or nm.id is None or _ctor_name is None: return 0 - if string.strcmp(nm.id, _ctor_name) == 0: return 1 - return 0 - - -def _ApplyCtorArgs(cl: ast.Call*) -> int: - """Extract name/version from Project(...) args into _c_name / _c_version.""" - global _c_name, _c_version - if cl is None: return 0 - if cl.args is not None: - an: sizet = cl.args.__len__() - if an > 2: - VLogger.error("viperbuild: Project(...) accepts at most 2 positional arguments (name, version)", "viperbuild") - return 1 - if an >= 1: - if _EvalStr(cl.args.get(0)) == 0: - VLogger.error("viperbuild: Project(name) must be a string constant", "viperbuild") - return 1 - _c_name = _DupStd(_g_out_str) - if an >= 2: - if _EvalStr(cl.args.get(1)) == 0: - VLogger.error("viperbuild: Project(version) must be a string constant", "viperbuild") - return 1 - _c_version = _DupStd(_g_out_str) - if cl.keywords is None: return 0 - n: sizet = cl.keywords.__len__() - for i in range(int(n)): - kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i)) - if kw is not None and kw.arg is not None and kw.value is not None: - if string.strcmp(kw.arg, "name") == 0: - if _EvalStr(kw.value) == 0: - VLogger.error("viperbuild: Project(name=...) must be a string constant", "viperbuild") - return 1 - _c_name = _DupStd(_g_out_str) - elif string.strcmp(kw.arg, "version") == 0: - if _EvalStr(kw.value) == 0: - VLogger.error("viperbuild: Project(version=...) must be a string constant", "viperbuild") - return 1 - _c_version = _DupStd(_g_out_str) - else: - VLogger.error(f"viperbuild: unsupported keyword {kw.arg} for Project(...)", "viperbuild") - return 1 - return 0 - - -def _SetField(fname: str, vnode: ast.AST*) -> int: - """Apply a proj. assignment or config(=...) keyword; 0=success.""" - global _c_source, _c_buildroot, _c_includes, _c_compiler, _c_linker - global _c_static_libs, _c_dynamic_libs, _c_output, _c_name, _c_version - if fname is None or vnode is None: return 1 - # ---- String fields ---- - if string.strcmp(fname, "source") == 0: - if _EvalStr(vnode) == 0: - VLogger.error("viperbuild: source must be a string constant expression", "viperbuild") - return 1 - _c_source = _DupStd(_g_out_str) - return 0 - if string.strcmp(fname, "buildroot") == 0: - if _EvalStr(vnode) == 0: - VLogger.error("viperbuild: buildroot must be a string constant expression", "viperbuild") - return 1 - _c_buildroot = _DupStd(_g_out_str) - return 0 - if string.strcmp(fname, "output") == 0: - if _EvalStr(vnode) == 0: - VLogger.error("viperbuild: output must be a string constant expression", "viperbuild") - return 1 - _c_output = _DupStd(_g_out_str) - return 0 - if string.strcmp(fname, "name") == 0: - if _EvalStr(vnode) == 0: - VLogger.error("viperbuild: name must be a string constant expression", "viperbuild") - return 1 - _c_name = _DupStd(_g_out_str) - return 0 - if string.strcmp(fname, "version") == 0: - if _EvalStr(vnode) == 0: - VLogger.error("viperbuild: version must be a string constant expression", "viperbuild") - return 1 - _c_version = _DupStd(_g_out_str) - return 0 - # ---- List fields ---- - if string.strcmp(fname, "includes") == 0: return _SetListField(0, vnode) - if string.strcmp(fname, "compiler") == 0: return _SetListField(1, vnode) - if string.strcmp(fname, "linker") == 0: return _SetListField(2, vnode) - if string.strcmp(fname, "static_libs") == 0: return _SetListField(3, vnode) - if string.strcmp(fname, "dynamic_libs") == 0: return _SetListField(4, vnode) - VLogger.error(f"viperbuild: unknown field proj.{fname}", "viperbuild") - return 1 - - -def _SetListField(slot: int, vnode: ast.AST*) -> int: - """Assign a list field (0=includes, 1=compiler, 2=linker, 3=static_libs, 4=dynamic_libs).""" - global _c_includes, _c_compiler, _c_linker, _c_static_libs, _c_dynamic_libs - if _scratch is None: return 1 - tmp: list[str]* = list[str](_scratch, 8) - if tmp is None: return 1 - if _EvalList(vnode, tmp) == 0: - VLogger.error("viperbuild: list fields must be list[str] constant expressions", "viperbuild") - return 1 - joined: str = _JoinSpace(tmp) - if slot == 0: _c_includes = joined - elif slot == 1: _c_compiler = joined - elif slot == 2: _c_linker = joined - elif slot == 3: _c_static_libs = joined - else: _c_dynamic_libs = joined - return 0 - - -def _HandleAssign(stmt: ast.Assign*) -> int: - """Handle an assignment: constant, Project instance, or proj field.""" - if stmt is None or stmt.targets is None or stmt.value is None: return 0 - if stmt.targets.__len__() != 1: return 0 - tgt: ast.AST* = stmt.targets.get(0) - if tgt is None: return 0 - tk: int = tgt.kind() - if tk == ast.ASTKind.Name: return _HandleNameBind(ast.Name*(tgt), stmt.value) - if tk == ast.ASTKind.Attribute: return _HandleAttrAssign(ast.Attribute*(tgt), stmt.value) - return 0 - - -def _HandleAnnAssign(stmt: ast.AnnAssign*) -> int: - """Handle an annotated assignment (including define-desugared AnnAssign).""" - if stmt is None or stmt.target is None or stmt.value is None: return 0 - if stmt.target.kind() != ast.ASTKind.Name: return 0 - return _HandleNameBind(ast.Name*(stmt.target), stmt.value) - - -def _HandleNameBind(tgt: ast.Name*, vnode: ast.AST*) -> int: - """Handle NAME = value: Project binding, string constant, or list constant.""" - global _c_has_project - if tgt is None or vnode is None or tgt.id is None: return 0 - # Reading a Project macro object on the RHS is fatal (reading forbidden) - if _IsProjRef(vnode) != 0: - VLogger.error(f"viperbuild: reading the Project macro object is forbidden ({tgt.id} = proj ...)", "viperbuild") - return 1 - # Project instance binding - if _IsProjectCtor(vnode) != 0: - if _c_has_project != 0: - VLogger.error("viperbuild: only one Project object is allowed per dependency closure", "viperbuild") - return 1 - _c_has_project = 1 - _proj_vars.append(_DupScratch(tgt.id)) - cl: ast.Call* = ast.Call*(vnode) - return _ApplyCtorArgs(cl) - # String constant - if _EvalStr(vnode) != 0: - _f_str_names.append(tgt.id) - _f_str_vals.append(_DupScratch(_g_out_str)) - return 0 - # List constant - if _scratch is not None: - tmp: list[str]* = list[str](_scratch, 8) - if tmp is not None: - if _EvalList(vnode, tmp) != 0: - _f_list_names.append(tgt.id) - _f_list_vals.append(_JoinSep(tmp)) - return 0 - tmp.clear() - return 0 - - -def _SetAdvancedField(fname: str, vnode: ast.AST*) -> int: - """Apply proj.advanced. = ; 0=success.""" - global _c_adv_asan, _c_adv_traceback - if fname is None or vnode is None: return 1 - b: int = _EvalBool(vnode) - if b < 0: - VLogger.error(f"viperbuild: proj.advanced.{fname} must be a boolean constant (True/False)", "viperbuild") - return 1 - if string.strcmp(fname, "asan") == 0: - _c_adv_asan = b - return 0 - if string.strcmp(fname, "traceback") == 0: - _c_adv_traceback = b - return 0 - VLogger.error(f"viperbuild: unknown field proj.advanced.{fname} (supported: asan / traceback)", "viperbuild") - return 1 - - -def _SetCleanField(fname: str, vnode: ast.AST*) -> int: - """Apply proj.clean. = ; 0=success.""" - global _c_clean_all, _c_clean_output, _c_clean_temp, _c_clean_build_dir, _c_clean_includes - if fname is None or vnode is None: return 1 - b: int = _EvalBool(vnode) - if b < 0: - VLogger.error(f"viperbuild: proj.clean.{fname} must be a boolean constant (True/False)", "viperbuild") - return 1 - if string.strcmp(fname, "all") == 0: - _c_clean_all = b - return 0 - if string.strcmp(fname, "output") == 0: - _c_clean_output = b - return 0 - if string.strcmp(fname, "temp") == 0: - _c_clean_temp = b - return 0 - if string.strcmp(fname, "build_dir") == 0: - _c_clean_build_dir = b - return 0 - if string.strcmp(fname, "includes") == 0: - _c_clean_includes = b - return 0 - VLogger.error(f"viperbuild: unknown field proj.clean.{fname} (supported: all / output / temp / build_dir / includes)", "viperbuild") - return 1 - - -def _HandleAttrAssign(tgt: ast.Attribute*, vnode: ast.AST*) -> int: - """Handle proj. = , proj.advanced. / proj.clean. = .""" - if tgt is None or tgt.attr is None or tgt.value is None: return 0 - # Nested form: proj.advanced.{asan,traceback} / proj.clean.{output,temp,build_dir,includes} - if tgt.value.kind() == ast.ASTKind.Attribute: - mid: ast.Attribute* = ast.Attribute*(tgt.value) - if mid is None or mid.attr is None or mid.value is None: return 0 - if mid.value.kind() != ast.ASTKind.Name: return 0 - mn: ast.Name* = ast.Name*(mid.value) - if mn is None or mn.id is None: return 0 - if _FindInList(_proj_vars, mn.id) < 0: return 0 - if string.strcmp(mid.attr, "advanced") != 0 and string.strcmp(mid.attr, "clean") != 0: - VLogger.error(f"viperbuild: unknown nested field proj.{mid.attr}.{tgt.attr}", "viperbuild") - return 1 - if _IsProjRef(vnode) != 0: - VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild") - return 1 - if string.strcmp(mid.attr, "clean") == 0: return _SetCleanField(tgt.attr, vnode) - return _SetAdvancedField(tgt.attr, vnode) - if tgt.value.kind() != ast.ASTKind.Name: return 0 - bn: ast.Name* = ast.Name*(tgt.value) - if bn is None or bn.id is None: return 0 - if _FindInList(_proj_vars, bn.id) < 0: return 0 - if _IsProjRef(vnode) != 0: - VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild") - return 1 - return _SetField(tgt.attr, vnode) - - -def _HandleConfigCall(cl: ast.Call*) -> int: - """Handle a proj.config(kw=...) call.""" - if cl is None or cl.keywords is None: return 0 - n: sizet = cl.keywords.__len__() - for i in range(int(n)): - kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i)) - if kw is not None and kw.arg is not None and kw.value is not None: - if _SetField(kw.arg, kw.value) != 0: return 1 - return 0 - - -def _JoinRunArgs(lst: list[str]*) -> str: - """Join run args for CreateProcess: elements containing blanks are wrapped in double quotes. - - Embedded double quotes are rejected (fail-fast) — Windows argv quoting rules would make the - actual argv ambiguous, and macro config should not silently alter the user's arguments. - """ - if lst is None: return None - n: sizet = len(lst) - if n == 0: return None - total: sizet = 0 - for i in range(int(n)): - s: str = lst.get(sizet(i)) - if s is None: continue - total += string.strlen(s) + 3 - buf: str = stdlib.malloc(total + 1) - if buf is None: return None - pos: sizet = 0 - for i in range(int(n)): - s = lst.get(sizet(i)) - if s is None: continue - sl: sizet = string.strlen(s) - need_quote: int = 0 - j: sizet = 0 - while j < sl: - if s[j] == ' ' or s[j] == '\t': - need_quote = 1 - if s[j] == '"': - stdlib.free(buf) - return None - j += 1 - if pos > 0: - buf[pos] = ' ' - pos += 1 - if need_quote != 0: - buf[pos] = '"' - pos += 1 - string.strcpy(buf + pos, s) - pos += sl - if need_quote != 0: - buf[pos] = '"' - pos += 1 - buf[pos] = '\0' - return buf - - -def _HandleBuildCall(cl: ast.Call*) -> int: - """Handle a proj.build(triple=, datalayout=, selfmachine=, run=, args=) call.""" - global _c_triple, _c_datalayout, _c_selfmachine, _c_has_build, _c_run, _c_run_args - if cl is None: return 1 - if _c_has_build != 0: - VLogger.error("viperbuild: proj.build() may be called only once", "viperbuild") - return 1 - _c_has_build = 1 - if cl.keywords is None: return 0 - n: sizet = cl.keywords.__len__() - for i in range(int(n)): - kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i)) - if kw is not None and kw.arg is not None and kw.value is not None: - if string.strcmp(kw.arg, "triple") == 0: - if _EvalStr(kw.value) == 0: - VLogger.error("viperbuild: build(triple=...) must be a string constant", "viperbuild") - return 1 - _c_triple = _DupStd(_g_out_str) - elif string.strcmp(kw.arg, "datalayout") == 0: - if _EvalStr(kw.value) == 0: - VLogger.error("viperbuild: build(datalayout=...) must be a string constant", "viperbuild") - return 1 - _c_datalayout = _DupStd(_g_out_str) - elif string.strcmp(kw.arg, "selfmachine") == 0: - b: int = _EvalBool(kw.value) - if b < 0: - VLogger.error("viperbuild: build(selfmachine=...) must be a boolean constant", "viperbuild") - return 1 - _c_selfmachine = b - elif string.strcmp(kw.arg, "run") == 0: - rb: int = _EvalBool(kw.value) - if rb < 0: - VLogger.error("viperbuild: build(run=...) must be a boolean constant", "viperbuild") - return 1 - _c_run = rb - elif string.strcmp(kw.arg, "args") == 0: - if _scratch is None: return 1 - tmp_args: list[str]* = list[str](_scratch, 8) - if tmp_args is None: return 1 - if _EvalList(kw.value, tmp_args) == 0: - VLogger.error("viperbuild: build(args=...) must be a list[str] constant expression", "viperbuild") - return 1 - joined_args: str = _JoinRunArgs(tmp_args) - # None + non-empty list = element rejected (embedded quote); empty list is legal. - if joined_args is None and len(tmp_args) > 0: - VLogger.error("viperbuild: build(args=...) has an element that cannot be passed through (embedded double quote); quote it in code or avoid it", "viperbuild") - return 1 - _c_run_args = joined_args - # Non-empty args imply run: `build(args=["--a"])` == run the exe with --a. - if _c_run_args is not None and string.strlen(_c_run_args) > 0: _c_run = 1 - else: - VLogger.error(f"viperbuild: unsupported keyword {kw.arg} for build(...)", "viperbuild") - return 1 - return 0 - - -def _CallArgsHaveProjRef(cl: ast.Call*) -> int: - """Whether any call argument references a Project macro object; 1=yes.""" - if cl is None: return 0 - if cl.args is not None: - n: sizet = cl.args.__len__() - for i in range(int(n)): - if _IsProjRef(cl.args.get(i)) != 0: return 1 - if cl.keywords is not None: - n = cl.keywords.__len__() - for i in range(int(n)): - kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i)) - if kw is not None and kw.value is not None: - if _IsProjRef(kw.value) != 0: return 1 - return 0 - - -def _HandleExprStmt(stmt: ast.Expr*) -> int: - """Handle an expression statement: proj.config/build calls, else check reads.""" - if stmt is None or stmt.value is None: return 0 - v: ast.AST* = stmt.value - if v.kind() != ast.ASTKind.Call: return 0 - cl: ast.Call* = ast.Call*(v) - if cl is None or cl.func is None: return 0 - # Non-Project method call: if an argument reads the macro object -> fatal (reading forbidden) - if cl.func.kind() != ast.ASTKind.Attribute: - if _CallArgsHaveProjRef(cl) != 0: - VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild") - return 1 - return 0 - at: ast.Attribute* = ast.Attribute*(cl.func) - if at is None or at.attr is None or at.value is None: return 0 - if at.value.kind() != ast.ASTKind.Name: - if _CallArgsHaveProjRef(cl) != 0: - VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild") - return 1 - return 0 - bn: ast.Name* = ast.Name*(at.value) - if bn is None or bn.id is None: return 0 - if _FindInList(_proj_vars, bn.id) < 0: - if _CallArgsHaveProjRef(cl) != 0: - VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild") - return 1 - return 0 - if string.strcmp(at.attr, "config") == 0: return _HandleConfigCall(cl) - if string.strcmp(at.attr, "build") == 0: return _HandleBuildCall(cl) - VLogger.error(f"viperbuild: unsupported method {at.attr}() on Project object", "viperbuild") - return 1 - - -# ---------------- ffilib.CDLL(纯构建配置) ---------------- - -# _BoolConst - 布尔字面量 -> 1(True) / 0(False) / -1(不是布尔常量) -def _BoolConst(node: ast.AST*) -> int: - """布尔常量求值;非布尔常量返回 -1""" - if node is None or node.kind() != ast.ASTKind.Constant: return -1 - c: ast.Constant* = ast.Constant*(node) - if c is None or c.const_kind != ast.CONST_BOOL: return -1 - if c.int_val != 0: return 1 - return 0 - - -# _AppendLibPath - 把路径追加到 static_libs(slot=0) / dynamic_libs(slot=1) 的空格拼接串 -def _AppendLibPath(slot: int, path: str) -> int: - """追加库路径;0=成功""" - global _c_static_libs, _c_dynamic_libs - if path is None: return 1 - cur: str = _c_static_libs - if slot == 1: cur = _c_dynamic_libs - pl: sizet = string.strlen(path) - if pl == 0: return 0 - ol: sizet = 0 - if cur is not None: ol = string.strlen(cur) - buf: str = stdlib.malloc(ol + pl + 2) - if buf is None: return 1 - pos: sizet = 0 - if ol > 0: - string.strcpy(buf, cur) - buf[ol] = ' ' - pos = ol + 1 - string.strcpy(buf + pos, path) - if slot == 1: _c_dynamic_libs = buf - else: _c_static_libs = buf - return 0 - - -# _IsCDLLCall - node 是否是 ffilib.CDLL(...)(别名/全名都认) -def _IsCDLLCall(node: ast.AST*) -> int: - """是 ffilib.CDLL 调用时返回 1""" - if node is None or node.kind() != ast.ASTKind.Call: return 0 - cl: ast.Call* = ast.Call*(node) - if cl is None or cl.func is None: return 0 - if cl.func.kind() != ast.ASTKind.Attribute: return 0 - at: ast.Attribute* = ast.Attribute*(cl.func) - if at is None or at.attr is None or string.strcmp(at.attr, "CDLL") != 0: return 0 - if at.value is None or at.value.kind() != ast.ASTKind.Name: return 0 - nm: ast.Name* = ast.Name*(at.value) - if nm.id is None: return 0 - if _ffi_alias is not None and string.strcmp(nm.id, _ffi_alias) == 0: return 1 - if string.strcmp(nm.id, "ffilib") == 0: return 1 - return 0 - - -# _RegisterFfiHandle - 登记 CDLL 句柄(变量名 -> runtime + 库路径) -def _RegisterFfiHandle(handle: str, runtime: int, path: str) -> None: - """登记 ffilib.CDLL 绑定的变量名、runtime 标志与库路径;句柄名为 None 时忽略""" - global _ffi_handles_names, _ffi_handles_rt, _ffi_handles_path - if handle is None or _ffi_handles_names is None or _ffi_handles_rt is None or _ffi_handles_path is None: return - nm: str = _DupScratch(handle) - if nm is None: return - pth: str = _DupScratch(path) - _ffi_handles_names.append(nm) - _ffi_handles_rt.append(runtime) - _ffi_handles_path.append(pth) - # runtime=True:thunk 运行期要调 _ffilib.Load/Sym,把该运行时库加入依赖闭包 - # (按需可达性扫描只收录被 import 的模块,否则 _resolve_module_sha1("_ffilib") 为 None)。 - if runtime != 0: _AddPending("_ffilib") - - -# _AssignTargetName - 取 `x = ...` 的赋值目标名;非简单 Name 时返回 None -def _AssignTargetName(asn: ast.Assign*) -> str: - """赋值目标名;多目标/非 Name 返回 None""" - if asn is None or asn.targets is None: return None - if len(asn.targets) != 1: return None - t: ast.AST* = asn.targets.get(0) - if t is None or t.kind() != ast.ASTKind.Name: return None - tn: ast.Name* = ast.Name*(t) - if tn is None: return None - return tn.id - - -# _HandleCDLL - 按实参形态写入静态/动态库表;list 回退形式本期 fail-fast -def _HandleCDLL(node: ast.AST*, handle: str) -> int: - """抽取 ffilib.CDLL(...):0=成功""" - if node is None or node.kind() != ast.ASTKind.Call: return 0 - call: ast.Call* = ast.Call*(node) - args: list[ast.AST*]* = call.args - if args is None or len(args) < 1: - VLogger.error("ffilib: CDLL(...) requires at least one argument", "ffilib") - return 1 - a0: ast.AST* = args.get(0) - if a0 is None: return 1 - - # 1) tag 形式:Project.StaticLib(path) / Project.DynamicLib(path) - if a0.kind() == ast.ASTKind.Call: - tag: ast.Call* = ast.Call*(a0) - is_static: int = 0 - is_dynamic: int = 0 - if tag is not None and tag.func is not None and tag.func.kind() == ast.ASTKind.Attribute: - at: ast.Attribute* = ast.Attribute*(tag.func) - if at is not None and at.attr is not None: - if string.strcmp(at.attr, "StaticLib") == 0: is_static = 1 - elif string.strcmp(at.attr, "DynamicLib") == 0: is_dynamic = 1 - if is_static == 0 and is_dynamic == 0: - VLogger.error("ffilib: CDLL(tag) expects Project.StaticLib(path) or Project.DynamicLib(path)", "ffilib") - return 1 - if tag.args is None or len(tag.args) < 1: - VLogger.error("ffilib: StaticLib/DynamicLib requires a path argument", "ffilib") - return 1 - p0: ast.AST* = tag.args.get(0) - if p0 is None or p0.kind() != ast.ASTKind.Constant: - VLogger.error("ffilib: library path must be a string constant", "ffilib") - return 1 - c0: ast.Constant* = ast.Constant*(p0) - if c0 is None or c0.const_kind != ast.CONST_STR: - VLogger.error("ffilib: library path must be a string constant", "ffilib") - return 1 - if is_static != 0: - rc_t: int = _AppendLibPath(0, c0.str_val) - if rc_t == 0: _RegisterFfiHandle(handle, 0, c0.str_val) - return rc_t - rc_td: int = _AppendLibPath(1, c0.str_val) - if rc_td == 0: _RegisterFfiHandle(handle, 1, c0.str_val) - return rc_td - - # 2) 回退列表(预留):需要 _ffilib 运行时库,本期编译期拦截 - if a0.kind() == ast.ASTKind.List: - VLogger.error("ffilib: CDLL(list[str], runtime) fallback-list form is not implemented yet (requires the _ffilib runtime library)", "ffilib") - return 1 - - # 3) 字符串 + runtime(位置参数或 runtime= 关键字) - if a0.kind() != ast.ASTKind.Constant: - VLogger.error("ffilib: CDLL(path, runtime) path must be a string constant", "ffilib") - return 1 - c1: ast.Constant* = ast.Constant*(a0) - if c1 is None or c1.const_kind != ast.CONST_STR: - VLogger.error("ffilib: CDLL path must be a string constant", "ffilib") - return 1 - rt: int = -1 - if len(args) >= 2: rt = _BoolConst(args.get(1)) - if rt < 0 and call.keywords is not None: - kn: sizet = len(call.keywords) - for ki in range(int(kn)): - kw: ast.Keyword* = ast.Keyword*(call.keywords.get(ki)) - if kw is None or kw.arg is None: continue - if string.strcmp(kw.arg, "runtime") == 0: - rt = _BoolConst(kw.value) - break - if rt < 0: - VLogger.error("ffilib: CDLL(path) needs a constant runtime=True/False, or use Project.StaticLib/DynamicLib", "ffilib") - return 1 - if rt == 1: - # runtime=True:不进链接输入(运行期由 thunk 用绝对路径 LoadLibraryA), - # 只登记句柄与路径;否则会同时生成导入表依赖,与 thunk 重复。 - _RegisterFfiHandle(handle, 1, c1.str_val) - return 0 - rc_s: int = _AppendLibPath(0, c1.str_val) - if rc_s == 0: _RegisterFfiHandle(handle, 0, c1.str_val) - return rc_s - - -# IsCdlLDeclCall - 供 Gen 层判断"这条模块级赋值是 ffilib.CDLL 声明"(纯编译期配置,不能生成全局) -def IsCdlLDeclCall(node: ast.AST*) -> int: - """node 是 ffilib.CDLL(...) 调用时返回 1""" - return _IsCDLLCall(node) - - -# HasRuntimeFfiHandle - 是否存在 runtime=True 的 CDLL 句柄(决定是否要引入 _ffilib 依赖) -def HasRuntimeFfiHandle() -> int: - """至少一个句柄 runtime=1 时返回 1""" - global _ffi_handles_rt - if _ffi_handles_rt is None: return 0 - n: sizet = _ffi_handles_rt.__len__() - for i in range(int(n)): - if _ffi_handles_rt.get(i) != 0: return 1 - return 0 - - -# FfiHandlePath - 查询 extern name(...) 的库路径(runtime=True 的 thunk 用它 LoadLibraryA) -def FfiHandlePath(name: str) -> str: - """库路径;未登记返回 None""" - global _ffi_handles_names, _ffi_handles_path - if name is None or _ffi_handles_names is None or _ffi_handles_path is None: return None - n: sizet = _ffi_handles_names.__len__() - for i in range(int(n)): - nm: str = _ffi_handles_names.get(i) - if nm is not None and string.strcmp(nm, name) == 0: return _ffi_handles_path.get(i) - return None - - -# FfiHandleRuntime - 查询 extern name(...) 的库句柄标志 -def FfiHandleRuntime(name: str) -> int: - """1=运行时加载 / 0=链接期 / -1=未登记(不是 ffilib.CDLL 绑定的变量)""" - global _ffi_handles_names, _ffi_handles_rt - if name is None or _ffi_handles_names is None or _ffi_handles_rt is None: return -1 - n: sizet = _ffi_handles_names.__len__() - for i in range(int(n)): - nm: str = _ffi_handles_names.get(i) - if nm is not None and string.strcmp(nm, name) == 0: return _ffi_handles_rt.get(i) - return -1 - - -def _ScanFile(pool: memhub.MemBuddy*, path: str) -> int: - """Parse and scan one .vp file; 0=ok, 1=failed, 2=skippable (unreadable/not .vp).""" - if path is None: return 2 - sl: sizet = string.strlen(path) - if sl == 0: return 2 - if Utils.is_vp_at(path, 0, sl) == 0: - # Not .vp: excluded from the closure (.vpi/.vpnci invisible to the compiler) - return 2 - - src_buf: bytes = Utils.read_file_to_buf(path, VB_SRC_CAP) - if src_buf is None: - VLogger.warning(f"viperbuild: cannot read dependency file {path}", "viperbuild") - return 2 - - if _ResetFileTables(pool) != 0: - stdlib.free(src_buf) - VLogger.error("viperbuild: constant table allocation failed", "viperbuild") - return 1 - - mark: sizet = pool.mark() - lx: ast.Lexer* = ast.new_lexer(pool) - if lx is None: - pool.reset_to(mark) - stdlib.free(src_buf) - VLogger.error("viperbuild: lexer allocation failed", "viperbuild") - return 1 - ast._lexer_init(lx, src_buf, pool) - tokens: ast.Token* = ast.tokenize(lx) - if tokens is None: - pool.reset_to(mark) - stdlib.free(src_buf) - VLogger.error(f"viperbuild: tokenization failed {path}", "viperbuild") - return 1 - tree: ast.AST* = ast.parse_tokens(pool, tokens) - if tree is None: - pool.reset_to(mark) - stdlib.free(src_buf) - VLogger.error(f"viperbuild: parsing failed {path}", "viperbuild") - return 1 - - body: list[ast.AST*]* = tree.children - rc: int = 0 - if body is not None: - cn: sizet = len(body) - for ci in range(int(cn)): - child: ast.AST* = body.get(ci) - if child is not None: - kd: int = child.kind() - if kd == ast.ASTKind.Import: _HandleImport(ast.Import*(child)) - elif kd == ast.ASTKind.ImportFrom: _HandleImportFrom(ast.ImportFrom*(child)) - elif kd == ast.ASTKind.Assign: - asn_f: ast.Assign* = ast.Assign*(child) - if asn_f is not None and _IsCDLLCall(asn_f.value) != 0: rc = _HandleCDLL(asn_f.value, _AssignTargetName(asn_f)) - else: rc = _HandleAssign(asn_f) - elif kd == ast.ASTKind.AnnAssign: rc = _HandleAnnAssign(ast.AnnAssign*(child)) - elif kd == ast.ASTKind.Expr: - ex_f: ast.Expr* = ast.Expr*(child) - if ex_f is not None and _IsCDLLCall(ex_f.value) != 0: rc = _HandleCDLL(ex_f.value, None) - else: rc = _HandleExprStmt(ex_f) - if rc != 0: break - - pool.reset_to(mark) - stdlib.free(src_buf) - return rc - - -# Dependency closure BFS -def _ResolveModuleToFile(module_name: str) -> str: - """Resolve a module name to a local project file path; None if not found.""" - if module_name is None or _search_dirs is None: return None - if string.strlen(module_name) == 0: return None - out_abs: rawptr = stdlib.malloc(8) - out_rel: rawptr = stdlib.malloc(8) - out_inc: rawptr = stdlib.malloc(8) - if out_abs is None or out_rel is None or out_inc is None: - if out_abs is not None: stdlib.free(out_abs) - if out_rel is not None: stdlib.free(out_rel) - if out_inc is not None: stdlib.free(out_inc) - return None - abs_slot: sizet* = (sizet*)(out_abs) - rel_slot: sizet* = (sizet*)(out_rel) - inc_slot: sizet* = (sizet*)(out_inc) - abs_slot[0] = 0 - rel_slot[0] = 0 - inc_slot[0] = 0 - rcode: int = IncludesScanner.ResolveModuleToPath(_search_dirs, module_name, out_abs, out_rel, out_inc) - result: str = None - if rcode == 0 and abs_slot[0] != 0: - abs_path: str = rawptr(abs_slot[0]) - if abs_path is not None: result = _DupScratch(abs_path) - if abs_slot[0] != 0: stdlib.free(rawptr(abs_slot[0])) - if rel_slot[0] != 0: stdlib.free(rawptr(rel_slot[0])) - if inc_slot[0] != 0: stdlib.free(rawptr(inc_slot[0])) - stdlib.free(out_abs) - stdlib.free(out_rel) - stdlib.free(out_inc) - return result - - -def _Enqueue(path: str) -> None: - """Add a file path to the BFS queue (deduped against worklist and visited).""" - if path is None or _work is None: return - if _FindInList(_visited, path) >= 0: return - if _FindInList(_work, path) >= 0: return - _work.append(_DupScratch(path)) - - -def _RunBfs(pool: memhub.MemBuddy*, entry_path: str) -> int: - """Expand the dependency closure from the entry file; 0=success.""" - global _pending - _work.append(_DupScratch(entry_path)) - while _work.__len__() > 0: - cur: str = _work.pop() - if cur is None: continue - if _FindInList(_visited, cur) >= 0: continue - if _visited.__len__() >= VB_MAX_VISIT: - VLogger.error(f"viperbuild: dependency closure exceeds {VB_MAX_VISIT} files, likely a cycle", "viperbuild") - return 1 - _visited.append(_DupScratch(cur)) - - _pending = list[str](_scratch, 8) - if _pending is None: - VLogger.error("viperbuild: BFS queue allocation failed", "viperbuild") - return 1 - rc: int = _ScanFile(pool, cur) - if rc == 1: return 1 - # Resolve module names collected from this file - pn: sizet = _pending.__len__() - for pi in range(int(pn)): - mod: str = _pending.get(pi) - if mod is not None: - dep: str = _ResolveModuleToFile(mod) - if dep is not None: _Enqueue(dep) - return 0 - - -# Defaults / validation / apply -def _DetectHost() -> int: - """Detect the host triple/datalayout via PROCESSOR_ARCHITECTURE; 0=success.""" - global _c_triple, _c_datalayout - buf: CHARPTR = stdlib.malloc(64) - if buf is None: - VLogger.error("viperbuild: selfmachine probe buffer allocation failed", "viperbuild") - return 1 - buf[0] = char(0) - n: DWORD = GetEnvironmentVariableA("PROCESSOR_ARCHITECTURE", buf, DWORD(64)) - if n == 0 or n >= DWORD(64): - stdlib.free(buf) - VLogger.error("viperbuild: selfmachine probe failed (PROCESSOR_ARCHITECTURE unavailable)", "viperbuild") - return 1 - buf[int(n)] = char(0) - # "AMD64\0" (A=65 M=77 D=68 6=54 4=52) - is_amd64: int = 0 - if buf[0] == char(65) and buf[1] == char(77) and buf[2] == char(68) and buf[3] == char(54) and buf[4] == char(52) and buf[5] == char(0): is_amd64 = 1 - if is_amd64 != 0: - _c_triple = "x86_64-pc-windows-gnu" - _c_datalayout = "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" - stdlib.free(buf) - return 0 - stdlib.free(buf) - VLogger.error("viperbuild: selfmachine does not support the host architecture yet (currently only AMD64)", "viperbuild") - return 1 - - -def _ProbeIncludes() -> str: - """Probe up to 3 levels above the exe dir for an includes directory; else None.""" - exe_buf: bytes = stdlib.malloc(VB_PATH_CAP) - if exe_buf is None: return None - exe_buf[0] = '\0' - n_exe: ULONG = win32file.GetModuleFileNameA(None, exe_buf, ULONG(VB_PATH_CAP)) - if n_exe <= 0: - stdlib.free(exe_buf) - return None - el: int = int(n_exe) - if el >= VB_PATH_CAP: el = VB_PATH_CAP - 1 - exe_buf[el] = '\0' - exe_dir: str = _DirOf(exe_buf) - stdlib.free(exe_buf) - if exe_dir is None: return None - for k in range(int(3 + 1)): - base: str = _DupStd(exe_dir) - for j in range(int(k)): - base = _Join2(base, "..") - cand: str = _Join2(base, "includes") - if _IsDir(cand) != 0: return cand - return None - - -def _DeriveOutputFromEntry(entry_path: str) -> str: - """Derive the default output name from the entry file name.""" - base: str = _BaseNameOf(entry_path) - if base is None: return "app.exe" - n: sizet = string.strlen(base) - cut: sizet = n - for i in range(int(n)): - if base[i] == '.': - cut = i - break - head: str = stdlib.malloc(cut + 5) - if head is None: return "app.exe" - string.strncpy(head, base, cut) - head[cut] = '.' - head[cut + 1] = 'e' - head[cut + 2] = 'x' - head[cut + 3] = 'e' - head[cut + 4] = '\0' - return head - - -def _FinalizeCfg(entry_path: str) -> int: - """Fill defaults, validate and write back into Config; 0=success.""" - global _c_source, _c_buildroot, _c_includes, _c_compiler, _c_linker - global _c_static_libs, _c_dynamic_libs, _c_output, _c_triple, _c_datalayout - global _c_selfmachine - - if _c_has_project == 0: - VLogger.error("viperbuild: no vb.Project(...) definition found in the dependency closure", "viperbuild") - return 1 - if _c_has_build == 0: - VLogger.error("viperbuild: proj.build(...) was not called, cannot determine the build target", "viperbuild") - return 1 - - # ---- Defaults ---- - if _c_source is None: _c_source = _DupStd(_entry_dir) - if _c_buildroot is None: _c_buildroot = _Join2(_c_source, "__vpcbuild__") - if _c_includes is None: - probed: str = _ProbeIncludes() - if probed is None: - VLogger.error("viperbuild: could not probe an includes directory (no includes up to 0..3 levels above the exe directory)", "viperbuild") - return 1 - _c_includes = probed - if _c_compiler is None: _c_compiler = _DupStd("llc -filetype=obj -relocation-model=pic") - if _c_linker is None: - # Built-in default = the verified link args from this repo's ViperC/project.vpj. - # Do NOT add -lgcc: msys64/mingw64 + lld has no libgcc and fails with "unable to find library -lgcc". - _c_linker = _DupStd("clang++ -fuse-ld=lld -lmsvcrt -lucrt -lpthread -lmingwex -lkernel32 -Wl,--stack,0x1000000 -Wl,--no-insert-timestamp -Wl,--export-all-symbols -g") - if _c_output is None: _c_output = _DeriveOutputFromEntry(entry_path) - - # ---- Entry file must be under source ---- - if _HasPrefix(entry_path, _c_source) == 0: - VLogger.error(f"viperbuild: entry file is not under the source directory\n entry: {entry_path}\n source: {_c_source}", "viperbuild") - return 1 - - # ---- Target platform ---- - explicit_triple: int = 0 - if _c_triple is not None or _c_datalayout is not None: explicit_triple = 1 - if _c_selfmachine != 0 and explicit_triple != 0: - VLogger.error("viperbuild: build(selfmachine=True) is mutually exclusive with an explicit triple/datalayout", "viperbuild") - return 1 - if explicit_triple != 0: - if _c_triple is None or _c_datalayout is None: - VLogger.error("viperbuild: build() triple and datalayout must be given as a pair", "viperbuild") - return 1 - else: - # Neither given, or selfmachine=True: detect the host at runtime - if _DetectHost() != 0: return 1 - - # ---- Write back to Config ---- - Config.ProjectName = _c_name - Config.ProjectVersion = _c_version - Config.SourceDir = _c_source - Config.BuildDir = _c_buildroot - Config.IncludesDirs = _c_includes - Config.IncludesDir = _FirstSpaceTok(_c_includes) - if Config.IncludesDir is None: Config.IncludesDir = _c_includes - Config.TargetTriple = _c_triple - Config.TargetDataLayout = _c_datalayout - Config.LinkerStaticLibs = _c_static_libs - Config.LinkerDynamicLibs = _c_dynamic_libs - Config.LinkerOutput = _c_output - Config.Sha1SliceLevel = 1 - Config.StrictMode = 1 - _SplitToolCmd(_c_compiler, 0) - _SplitToolCmd(_c_linker, 1) - - Config.resolve_paths(_c_source) - return 0 - - -def _SplitToolCmd(joined: str, which: int) -> None: - """Split a space-joined tool command into Config Cmd/Flags (0=compiler, 1=linker).""" - if joined is None: return - n: sizet = string.strlen(joined) - sp: sizet = n - for i in range(int(n)): - if joined[i] == ' ': - sp = i - break - cmd: str = stdlib.malloc(sp + 1) - if cmd is not None: - string.strncpy(cmd, joined, sp) - cmd[sp] = '\0' - flags: str = None - if sp < n: flags = _DupStd(joined + sp + 1) - if which == 0: - Config.CompilerCmd = cmd - Config.CompilerFlags = flags - else: - Config.LinkerCmd = cmd - Config.LinkerFlags = flags - - -# Entry point -def _ResetAll() -> None: - """Reset module state before each DiscoverAndApply call.""" - global _alias, _ctor_name, _g_out_str, _applied, _entry_dir, _search_dirs - global _c_name, _c_version, _c_source, _c_buildroot, _c_includes - global _c_compiler, _c_linker, _c_static_libs, _c_dynamic_libs, _c_output, _ffi_alias - global _ffi_handles_names, _ffi_handles_rt, _ffi_handles_path - global _c_triple, _c_datalayout, _c_selfmachine, _c_has_project, _c_has_build - global _c_adv_asan, _c_adv_traceback - global _c_clean_all, _c_clean_output, _c_clean_temp, _c_clean_build_dir, _c_clean_includes - global _c_run, _c_run_args - global _saw_viperbuild - _alias = None - _ctor_name = None - _g_out_str = None - _applied = 0 - _entry_dir = None - _search_dirs = None - _saw_viperbuild = 0 - _c_name = None - _c_version = None - _c_source = None - _c_buildroot = None - _c_includes = None - _c_compiler = None - _c_linker = None - _c_static_libs = None - _c_dynamic_libs = None - _ffi_alias = None - _ffi_handles_names = None - _ffi_handles_rt = None - _ffi_handles_path = None - _c_output = None - _c_triple = None - _c_datalayout = None - _c_selfmachine = 0 - _c_has_project = 0 - _c_has_build = 0 - _c_adv_asan = 0 - _c_adv_traceback = 0 - _c_clean_all = 0 - _c_clean_output = 0 - _c_clean_temp = 0 - _c_clean_build_dir = 0 - _c_clean_includes = 0 - _c_run = 0 - _c_run_args = None - - -def DiscoverAndApply(mb: memhub.MemBuddy*, entry_path: str) -> int: - """Pass 0 for --src: discover and apply viperbuild config; 0=success, else failed.""" - global _scratch, _work, _visited, _pending, _proj_vars, _entry_dir, _applied - global _search_dirs - global _ffi_handles_path, _ffi_handles_names, _ffi_handles_rt - - if mb is None or entry_path is None: - VLogger.error("viperbuild: argument is null", "viperbuild") - return 1 - - _ResetAll() - - # Entry dir (default source + closure search root) - _entry_dir = _DirOf(entry_path) - if _entry_dir is None: - VLogger.error(f"viperbuild: cannot resolve the entry file directory {entry_path}", "viperbuild") - return 1 - # Bare filename (no separator) -> _DirOf returns ""; must normalize to ".", otherwise - # ResolveModuleToPath("") tries no candidate and same-dir deps (often the viperbuild config) never resolve. - if string.strlen(_entry_dir) == 0: _entry_dir = _DupStd(".") - # Closure search roots: entry dir first, then "." (the config module may live elsewhere) - _search_dirs = _ConcatStd(_DupStd(_entry_dir), _DupStd(" .")) - - # Scratch pool (BFS working set; not affected by main-pool reset) - arena: bytes = stdlib.malloc(VB_SCRATCH) - if arena is None: - VLogger.error("viperbuild: scratch memory allocation failed", "viperbuild") - return 1 - # Heap-allocate the manager: `_scratch` is a module-level global, so a frame-local - # `MemBuddy` temp would hand out a dangling address once this frame returns (LifetimeError). - # `hlife` is explicit so NLL frame-demotion cannot apply. - _scratch = new heap hlife memhub.MemBuddy(arena, VB_SCRATCH, 0, 0) - if _scratch is None: - VLogger.error("viperbuild: scratch pool initialization failed", "viperbuild") - return 1 - - _work = list[str](_scratch, 8) - _visited = list[str](_scratch, 8) - _pending = list[str](_scratch, 8) - _proj_vars = list[str](_scratch, 8) - _ffi_handles_names = list[str](_scratch, 8) - _ffi_handles_rt = list[int](_scratch, 8) - _ffi_handles_path = list[str](_scratch, 8) - if _work is None or _visited is None or _pending is None or _proj_vars is None: - VLogger.error("viperbuild: working set allocation failed", "viperbuild") - return 1 - if _ffi_handles_names is None or _ffi_handles_rt is None or _ffi_handles_path is None: - VLogger.error("viperbuild: ffilib handle table allocation failed", "viperbuild") - return 1 - - # Keyword/operator tables must be built before the main-pool reset window (else freed -> dangling crash) - ast._init_tables(mb) - - if _RunBfs(mb, entry_path) != 0: return 1 - - if _proj_vars.__len__() == 0: - if _saw_viperbuild == 0: VLogger.error(f"viperbuild: `import viperbuild` not found in the entry file dependency closure (entry: {entry_path}; scanned {_visited.__len__()} files)", "viperbuild") - else: VLogger.error(f"viperbuild: found `import viperbuild` but the dependency closure has no `proj = vb.Project(...)` binding (entry: {entry_path})", "viperbuild") - return 1 - - if _FinalizeCfg(entry_path) != 0: return 1 - - _applied = 1 - return 0 - - -def IsApplied() -> int: - """Whether viperbuild config was applied in this process; 1=yes.""" - return _applied - - -def AdvancedAsan() -> int: - """Whether viperbuild requested ASan mode (proj.advanced.asan = True); 1=yes / 0=no.""" - return _c_adv_asan - - -def AdvancedTraceback() -> int: - """Whether viperbuild requested traceback mode (proj.advanced.traceback = True); 1=yes / 0=no.""" - return _c_adv_traceback - - -def CleanAll() -> int: - """Whether viperbuild requested a full clean (proj.clean.all: buildroot + includes); 1=yes / 0=no.""" - return _c_clean_all - - -def CleanOutput() -> int: - """Whether viperbuild requested cleaning the output dir (proj.clean.output); 1=yes / 0=no.""" - return _c_clean_output - - -def CleanTemp() -> int: - """Whether viperbuild requested cleaning the temp dir (proj.clean.temp); 1=yes / 0=no.""" - return _c_clean_temp - - -def CleanBuildDir() -> int: - """Whether viperbuild requested cleaning the whole build dir (proj.clean.build_dir); 1=yes / 0=no.""" - return _c_clean_build_dir - - -def CleanIncludes() -> int: - """Whether viperbuild requested cleaning the includes cache (proj.clean.includes); 1=yes / 0=no.""" - return _c_clean_includes - - -def RunAfterBuild() -> int: - """Whether viperbuild requested running the exe after a successful build; 1=yes / 0=no.""" - return _c_run - - -def RunArgs() -> str: - """Space-joined run arguments for build(args=[...]) (already quoted where needed); None = none.""" - return _c_run_args +"""Compiler-side implementation of the `viperbuild` built-in macro library (--src Pass 0).""" +from stdint import * +import memhub +import string +import stdlib +import ast +import os +import sys +import lib.Projectrans.Utils as Utils +import lib.Projectrans.Config as Config +import lib.core.VLogger as VLogger +import lib.core.IncludesScanner as IncludesScanner +import stdio + +# Path/source buffer caps (same convention as main.vp's SRC_BUF_SIZE) +define VB_PATH_CAP = 1024 +define VB_SRC_CAP = 1048576 +# Scratch pool size (BFS working set + constant tables; not part of main-pool reset) +define VB_SCRATCH = 8388608 +# Visited cap (cycle defense; fatal if the closure greatly exceeds it) +define VB_MAX_VISIT = 4096 +# Element separator for list constants: the char code of newline (source paths/flags never contain newlines). +# Must store the code then char(): in Viper 'c' literals are single-char strings (i8*), not assignable to a char slot. +define VB_SEP_CODE = 10 + +# Module state +_scratch: memhub.MemBuddy* = None +_entry_dir: str = None # entry file dir (default source; normalized to "." for bare filenames) +_search_dirs: str = None # closure search roots (space-separated): entry dir first, then "." +_work: list[str]* = None # BFS pending file paths +_visited: list[str]* = None # processed file paths +_pending: list[str]* = None # import module names collected in one file (pending BFS) +_proj_vars: list[str]* = None # variable names bound to a vb.Project(...) instance +_alias: str = None # local alias of viperbuild (import viperbuild as vb -> "vb") +# ffilib 的本地别名(import ffilib / import ffilib as F);ffilib.CDLL(...) 是纯构建配置 +_ffi_alias: str = None +# ffilib.CDLL 句柄表(两个平行列表):变量名 / runtime(1=运行时加载, 0=链接期) +# 供 `extern name(...)` 校验句柄来源与选择行为。 +_ffi_handles_names: list[str]* = None +_ffi_handles_rt: list[int]* = None +# 句柄对应的库路径(runtime=True 时 thunk 直接用绝对路径 LoadLibraryA) +_ffi_handles_path: list[str]* = None +_ctor_name: str = None # from viperbuild import Project as P -> "P" + +# Per-file constant tables (valid only while scanning that file; pool-allocated) +_f_str_names: list[str]* = None +_f_str_vals: list[str]* = None +_f_list_names: list[str]* = None +_f_list_vals: list[str]* = None + +# Constant-eval output slot +_g_out_str: str = None + +# Extracted config (all strings stdlib.malloc) +_c_name: str = None +_c_version: str = None +_c_source: str = None +_c_buildroot: str = None +_c_includes: str = None +_c_compiler: str = None +_c_linker: str = None +_c_static_libs: str = None +_c_dynamic_libs: str = None +_c_output: str = None +_c_triple: str = None +_c_datalayout: str = None +_c_selfmachine: int = 0 +_c_has_project: int = 0 +_c_has_build: int = 0 +# proj.advanced.* 高级开关(等价 CLI 的 --asan / --traceback) +_c_adv_asan: int = 0 +_c_adv_traceback: int = 0 +# proj.clean.* 细粒度清理(等价 --clean 的子集;build_dir = 整个 __vpcbuild__) +_c_clean_all: int = 0 +_c_clean_output: int = 0 +_c_clean_temp: int = 0 +_c_clean_build_dir: int = 0 +_c_clean_includes: int = 0 +# build(run=, args=):编译后直接运行(args 非空时隐含 run) +_c_run: int = 0 +_c_run_args: str = None + +# Whether `import viperbuild` / `from viperbuild import ...` appeared in the closure (diagnostics only) +_saw_viperbuild: int = 0 + +# Whether applied successfully (for main.vp to decide viperbuild's linker.output) +_applied: int = 0 + + +# Basic helpers +def _DupStd(s: str) -> str: + """Deep-copy a string with stdlib.malloc so it survives reset_to.""" + if s is None: return None + n: sizet = string.strlen(s) + buf: str = stdlib.malloc(n + 1) + if buf is None: return None + string.strcpy(buf, s) + return buf + + +def _DupScratch(s: str) -> str: + """Deep-copy a string into the scratch pool, unaffected by main-pool reset_to.""" + if s is None or _scratch is None: return None + n: sizet = string.strlen(s) + buf: str = _scratch.alloc(n + 1) + if buf is None: return None + string.strcpy(buf, s) + return buf + + +def _Join2(a: str, b: str) -> str: + """Join a and b with a slash; returns b when a is empty.""" + if a is None: return b + if b is None: return a + al: sizet = string.strlen(a) + if al == 0: return b + bl: sizet = string.strlen(b) + buf: str = stdlib.malloc(al + 1 + bl + 1) + if buf is None: return None + string.strcpy(buf, a) + buf[al] = '/' + string.strcpy(buf + al + 1, b) + return buf + + +def _DirOf(path: str) -> str: + """Return the directory part of path (no trailing separator); empty string if none.""" + if path is None: return None + n: sizet = string.strlen(path) + i: sizet = n + while i > 0: + i -= 1 + ch: char = path[i] + if ch == '/' or ch == '\\': + buf: str = stdlib.malloc(i + 1) + if buf is None: return None + string.strncpy(buf, path, i) + buf[i] = '\0' + return buf + return _DupStd("") + + +def _BaseNameOf(path: str) -> str: + """Return the file name of path, including its extension.""" + if path is None: return None + n: sizet = string.strlen(path) + start: sizet = 0 + for i in range(int(n)): + ch: char = path[i] + if ch == '/' or ch == '\\': start = i + 1 + return _DupStd(path + start) + + +def _IsDir(path: str) -> int: + """Whether path is an existing directory; 1=yes.""" + if path is None: return 0 + return 1 if os.isdir(path) else 0 + + +def _HasPrefix(s: str, prefix: str) -> int: + """Whether s starts with prefix; 1=yes (None/empty/"." counts as true).""" + if s is None: return 0 + if prefix is None: return 1 + if string.strcmp(prefix, ".") == 0: return 1 + if string.strncmp(s, "./", 2) == 0: s = s + 2 + if string.strncmp(prefix, "./", 2) == 0: prefix = prefix + 2 + pl: sizet = string.strlen(prefix) + if pl == 0: return 1 + sl: sizet = string.strlen(s) + if sl < pl: return 0 + if string.strncmp(s, prefix, pl) == 0: return 1 + return 0 + + +# Constant tables +def _FindInList(names: list[str]*, name: str) -> int: + """Linear-search names for name; return its index, or -1 if not found.""" + if names is None or name is None: return -1 + n: sizet = names.__len__() + for i in range(int(n)): + s: str = names.get(i) + if s is not None: + if string.strcmp(s, name) == 0: return int(i) + return -1 + + +def _LookupStrConst(name: str) -> int: + """Resolve a name to a string constant in _g_out_str; 0=failure.""" + global _g_out_str + idx: int = _FindInList(_f_str_names, name) + if idx < 0: return 0 + v: str = _f_str_vals.get(sizet(idx)) + if v is None: return 0 + _g_out_str = v + return 1 + + +def _LookupListConst(name: str, out: list[str]*) -> int: + """Resolve a name to a list constant, split into out; 0=failure.""" + idx: int = _FindInList(_f_list_names, name) + if idx < 0: return 0 + v: str = _f_list_vals.get(sizet(idx)) + if v is None: return 0 + return _SplitInto(out, v) + + +def _SplitInto(out: list[str]*, joined: str) -> int: + """Split a VB_SEP_CODE-joined string into out; empty string means empty list.""" + if out is None or joined is None: return 0 + n: sizet = string.strlen(joined) + if n == 0: return 1 + start: sizet = 0 + for i in range(int(n + 1)): + if i == n or joined[i] == char(VB_SEP_CODE): + seg: str = stdlib.malloc(i - start + 1) + if seg is None: return 0 + string.strncpy(seg, joined + start, i - start) + seg[i - start] = '\0' + out.append(_DupScratch(seg)) + stdlib.free(seg) + start = i + 1 + return 1 + + +def _JoinList(lst: list[str]*, sep: char, use_scratch: int) -> str: + """Join a list[str] with sep; use_scratch selects the scratch pool allocator.""" + if lst is None: return None + n: sizet = lst.__len__() + if n == 0: return None + total: sizet = 0 + i: sizet = 0 + while i < n: + s: str = lst.get(i) + if s is not None: total += string.strlen(s) + 1 + i += 1 + if total == 0: return None + buf: str = None + if use_scratch != 0: buf = _scratch.alloc(total + 1) + else: buf = stdlib.malloc(total + 1) + if buf is None: return None + pos: sizet = 0 + for i in range(int(n)): + s = lst.get(i) + if s is not None: + sl: sizet = string.strlen(s) + if pos > 0: + buf[pos] = sep + pos += 1 + string.strcpy(buf + pos, s) + pos += sl + buf[pos] = '\0' + return buf + + +def _JoinSep(lst: list[str]*) -> str: + """Join a list[str] with VB_SEP_CODE (pool-allocated); None for an empty list.""" + return _JoinList(lst, char(VB_SEP_CODE), 1) + + +def _JoinSpace(lst: list[str]*) -> str: + """Join a list[str] with spaces (stdlib-allocated); None for an empty list.""" + return _JoinList(lst, ' ', 0) + + +def _FirstSpaceTok(joined: str) -> str: + """Return the first space-separated token (stdlib-allocated); None if empty.""" + if joined is None: return None + n: sizet = string.strlen(joined) + if n == 0: return None + sp: sizet = n + for i in range(int(n)): + if joined[i] == ' ': + sp = i + break + seg: str = stdlib.malloc(sp + 1) + if seg is None: return None + string.strncpy(seg, joined, sp) + seg[sp] = '\0' + return seg + + +# Constant expression evaluation +def _EvalStr(node: ast.AST*) -> int: + """Evaluate a string constant expression into _g_out_str; 0=failure.""" + global _g_out_str + if node is None: return 0 + k: int = node.kind() + if k == ast.ASTKind.Constant: + cn: ast.Constant* = ast.Constant*(node) + if cn is None or cn.const_kind != ast.CONST_STR: return 0 + _g_out_str = cn.str_val + return 1 + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(node) + if nm is None or nm.id is None: return 0 + return _LookupStrConst(nm.id) + if k == ast.ASTKind.BinOp: + bop: ast.BinOp* = ast.BinOp*(node) + if bop is None or bop.op != ast.OpKind.Add: return 0 + if _EvalStr(bop.left) == 0: return 0 + lv: str = _g_out_str + if _EvalStr(bop.right) == 0: return 0 + rv: str = _g_out_str + _g_out_str = _ConcatStd(lv, rv) + return 1 + return 0 + + +def _ConcatStd(a: str, b: str) -> str: + """Concatenate two strings with stdlib allocation.""" + if a is None: return b + if b is None: return a + al: sizet = string.strlen(a) + bl: sizet = string.strlen(b) + buf: str = stdlib.malloc(al + bl + 1) + if buf is None: return None + string.strcpy(buf, a) + string.strcpy(buf + al, b) + return buf + + +def _EvalList(node: ast.AST*, out: list[str]*) -> int: + """Evaluate a list[str] constant expression into out; 0=failure.""" + if node is None or out is None: return 0 + k: int = node.kind() + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(node) + if nm is None or nm.id is None: return 0 + return _LookupListConst(nm.id, out) + if k == ast.ASTKind.List: + lst: ast.List* = ast.List*(node) + if lst is None or lst.elts is None: return 0 + n: sizet = lst.elts.__len__() + for i in range(int(n)): + e: ast.AST* = lst.elts.get(i) + if e is None: return 0 + if _IsListUnpack(e) != 0: + inner: ast.AST* = _ListUnpackOperand(e) + if inner is None: return 0 + if _EvalList(inner, out) == 0: return 0 + else: + if _EvalStr(e) == 0: return 0 + out.append(_DupScratch(_g_out_str)) + return 1 + if k == ast.ASTKind.BinOp: + bop: ast.BinOp* = ast.BinOp*(node) + if bop is None or bop.op != ast.OpKind.Add: return 0 + if _EvalList(bop.left, out) == 0: return 0 + if _EvalList(bop.right, out) == 0: return 0 + return 1 + return 0 + + +# Macro-list unpacking: `[*COMMON_FLAGS]` / `A + [*B]`. +# The parser has no Starred node in list displays: a leading `*` in an element position is +# parsed as a deref UnaryOp (expression-position `*x` is pointer deref). Inside a compile-time +# macro list the only meaningful interpretation is unpacking, so both shapes are accepted here. +# Keep in sync with _ListUnpackOperand. +def _IsListUnpack(node: ast.AST*) -> int: + """Whether a list element is an unpacking form (`*X`); 1=yes / 0=no.""" + if node is None: return 0 + k: int = node.kind() + if k == ast.ASTKind.Starred: return 1 + if k == ast.ASTKind.UnaryOp: + uo: ast.UnaryOp* = ast.UnaryOp*(node) + if uo is not None and uo.op == ast.OpKind.Deref: return 1 + return 0 + + +def _ListUnpackOperand(node: ast.AST*) -> ast.AST*: + """Operand of an unpacking element (`*X` -> X); None when node is not an unpacking form.""" + if _IsListUnpack(node) == 0: return None + if node.kind() == ast.ASTKind.Starred: + st: ast.Starred* = ast.Starred*(node) + if st is None: return None + return st.value + uo: ast.UnaryOp* = ast.UnaryOp*(node) + if uo is None: return None + return uo.operand + + +def _EvalBool(node: ast.AST*) -> int: + """Evaluate a bool constant; -1 means it is not a bool constant.""" + if node is None: return -1 + if node.kind() != ast.ASTKind.Constant: return -1 + cn: ast.Constant* = ast.Constant*(node) + if cn is None or cn.const_kind != ast.CONST_BOOL: return -1 + if cn.int_val != 0: return 1 + return 0 + + +# Macro-reference detection (reading is forbidden) +def _IsProjRef(node: ast.AST*) -> int: + """Whether a node directly references a Project macro object; 1=yes.""" + if node is None: return 0 + k: int = node.kind() + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(node) + if nm is None or nm.id is None: return 0 + if _FindInList(_proj_vars, nm.id) >= 0: return 1 + return 0 + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(node) + if at is None or at.value is None: return 0 + return _IsProjRef(at.value) + return 0 + + +# Single-file scan +def _ResetFileTables(pool: memhub.MemBuddy*) -> int: + """Rebuild the per-file constant tables (pool-allocated).""" + global _f_str_names, _f_str_vals, _f_list_names, _f_list_vals + _f_str_names = list[str](pool, 8) + _f_str_vals = list[str](pool, 8) + _f_list_names = list[str](pool, 8) + _f_list_vals = list[str](pool, 8) + if _f_str_names is None or _f_str_vals is None: return 1 + if _f_list_names is None or _f_list_vals is None: return 1 + return 0 + + +def _AddPending(mod_name: str) -> None: + """Append a module name to the BFS queue (deep-copied into the scratch pool).""" + if mod_name is None or _pending is None: return + if string.strlen(mod_name) == 0: return + _pending.append(_DupScratch(mod_name)) + + +def _HandleImport(stmt: ast.Import*) -> int: + """Handle `import X [as Y]`: detect viperbuild/ffilib, queue other modules for BFS.""" + global _alias, _ffi_alias, _saw_viperbuild + if stmt is None or stmt.names is None: return 0 + n: sizet = stmt.names.__len__() + for i in range(int(n)): + al: ast.Alias* = ast.Alias*(stmt.names.get(i)) + if al is not None and al.name is not None: + if string.strcmp(al.name, "viperbuild") == 0: + _saw_viperbuild = 1 + if al.asname is not None: _alias = _DupScratch(al.asname) + else: _alias = _DupScratch("viperbuild") + elif string.strcmp(al.name, "ffilib") == 0: + # 宏库:不参与 BFS,只记别名(CDLL 语句由 _HandleCDLL 抽取)。 + # _ffilib 的依赖只在真正出现 runtime=True 的 CDLL 时才加(见 _RegisterFfiHandle), + # 纯链接期(runtime=False)的工程不需要编译这个运行时库。 + if al.asname is not None: _ffi_alias = _DupScratch(al.asname) + else: _ffi_alias = _DupScratch("ffilib") + else: _AddPending(al.name) + return 0 + + +def _HandleImportFrom(stmt: ast.ImportFrom*) -> int: + """Handle `from X import Y`: detect viperbuild's Project, queue other modules.""" + global _ctor_name, _saw_viperbuild + if stmt is None: return 0 + if stmt.level != 0: + # Relative imports are not part of the local closure (no package context: skip) + return 0 + if stmt.module is None: return 0 + if string.strcmp(stmt.module, "viperbuild") != 0: + _AddPending(stmt.module) + return 0 + _saw_viperbuild = 1 + if stmt.names is None: return 0 + n: sizet = stmt.names.__len__() + for i in range(int(n)): + al: ast.Alias* = ast.Alias*(stmt.names.get(i)) + if al is not None and al.name is not None: + if string.strcmp(al.name, "Project") == 0: + if al.asname is not None: _ctor_name = _DupScratch(al.asname) + else: _ctor_name = _DupScratch("Project") + return 0 + + +def _IsProjectCtor(vnode: ast.AST*) -> int: + """Whether an expression is a vb.Project(...) or Project(...) call; 1=yes.""" + if vnode is None: return 0 + if vnode.kind() != ast.ASTKind.Call: return 0 + cl: ast.Call* = ast.Call*(vnode) + if cl is None or cl.func is None: return 0 + fk: int = cl.func.kind() + if fk == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(cl.func) + if at is None or at.attr is None or at.value is None: return 0 + if at.value.kind() != ast.ASTKind.Name: return 0 + bn: ast.Name* = ast.Name*(at.value) + if bn is None or bn.id is None or _alias is None: return 0 + if string.strcmp(bn.id, _alias) != 0: return 0 + if string.strcmp(at.attr, "Project") == 0: return 1 + return 0 + if fk == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(cl.func) + if nm is None or nm.id is None or _ctor_name is None: return 0 + if string.strcmp(nm.id, _ctor_name) == 0: return 1 + return 0 + + +def _ApplyCtorArgs(cl: ast.Call*) -> int: + """Extract name/version from Project(...) args into _c_name / _c_version.""" + global _c_name, _c_version + if cl is None: return 0 + if cl.args is not None: + an: sizet = cl.args.__len__() + if an > 2: + VLogger.error("viperbuild: Project(...) accepts at most 2 positional arguments (name, version)", "viperbuild") + return 1 + if an >= 1: + if _EvalStr(cl.args.get(0)) == 0: + VLogger.error("viperbuild: Project(name) must be a string constant", "viperbuild") + return 1 + _c_name = _DupStd(_g_out_str) + if an >= 2: + if _EvalStr(cl.args.get(1)) == 0: + VLogger.error("viperbuild: Project(version) must be a string constant", "viperbuild") + return 1 + _c_version = _DupStd(_g_out_str) + if cl.keywords is None: return 0 + n: sizet = cl.keywords.__len__() + for i in range(int(n)): + kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i)) + if kw is not None and kw.arg is not None and kw.value is not None: + if string.strcmp(kw.arg, "name") == 0: + if _EvalStr(kw.value) == 0: + VLogger.error("viperbuild: Project(name=...) must be a string constant", "viperbuild") + return 1 + _c_name = _DupStd(_g_out_str) + elif string.strcmp(kw.arg, "version") == 0: + if _EvalStr(kw.value) == 0: + VLogger.error("viperbuild: Project(version=...) must be a string constant", "viperbuild") + return 1 + _c_version = _DupStd(_g_out_str) + else: + VLogger.error(f"viperbuild: unsupported keyword {kw.arg} for Project(...)", "viperbuild") + return 1 + return 0 + + +def _SetField(fname: str, vnode: ast.AST*) -> int: + """Apply a proj. assignment or config(=...) keyword; 0=success.""" + global _c_source, _c_buildroot, _c_includes, _c_compiler, _c_linker + global _c_static_libs, _c_dynamic_libs, _c_output, _c_name, _c_version + if fname is None or vnode is None: return 1 + # ---- String fields ---- + if string.strcmp(fname, "source") == 0: + if _EvalStr(vnode) == 0: + VLogger.error("viperbuild: source must be a string constant expression", "viperbuild") + return 1 + _c_source = _DupStd(_g_out_str) + return 0 + if string.strcmp(fname, "buildroot") == 0: + if _EvalStr(vnode) == 0: + VLogger.error("viperbuild: buildroot must be a string constant expression", "viperbuild") + return 1 + _c_buildroot = _DupStd(_g_out_str) + return 0 + if string.strcmp(fname, "output") == 0: + if _EvalStr(vnode) == 0: + VLogger.error("viperbuild: output must be a string constant expression", "viperbuild") + return 1 + _c_output = _DupStd(_g_out_str) + return 0 + if string.strcmp(fname, "name") == 0: + if _EvalStr(vnode) == 0: + VLogger.error("viperbuild: name must be a string constant expression", "viperbuild") + return 1 + _c_name = _DupStd(_g_out_str) + return 0 + if string.strcmp(fname, "version") == 0: + if _EvalStr(vnode) == 0: + VLogger.error("viperbuild: version must be a string constant expression", "viperbuild") + return 1 + _c_version = _DupStd(_g_out_str) + return 0 + # ---- List fields ---- + if string.strcmp(fname, "includes") == 0: return _SetListField(0, vnode) + if string.strcmp(fname, "compiler") == 0: return _SetListField(1, vnode) + if string.strcmp(fname, "linker") == 0: return _SetListField(2, vnode) + if string.strcmp(fname, "static_libs") == 0: return _SetListField(3, vnode) + if string.strcmp(fname, "dynamic_libs") == 0: return _SetListField(4, vnode) + VLogger.error(f"viperbuild: unknown field proj.{fname}", "viperbuild") + return 1 + + +def _SetListField(slot: int, vnode: ast.AST*) -> int: + """Assign a list field (0=includes, 1=compiler, 2=linker, 3=static_libs, 4=dynamic_libs).""" + global _c_includes, _c_compiler, _c_linker, _c_static_libs, _c_dynamic_libs + if _scratch is None: return 1 + tmp: list[str]* = list[str](_scratch, 8) + if tmp is None: return 1 + if _EvalList(vnode, tmp) == 0: + VLogger.error("viperbuild: list fields must be list[str] constant expressions", "viperbuild") + return 1 + joined: str = _JoinSpace(tmp) + if slot == 0: _c_includes = joined + elif slot == 1: _c_compiler = joined + elif slot == 2: _c_linker = joined + elif slot == 3: _c_static_libs = joined + else: _c_dynamic_libs = joined + return 0 + + +def _HandleAssign(stmt: ast.Assign*) -> int: + """Handle an assignment: constant, Project instance, or proj field.""" + if stmt is None or stmt.targets is None or stmt.value is None: return 0 + if stmt.targets.__len__() != 1: return 0 + tgt: ast.AST* = stmt.targets.get(0) + if tgt is None: return 0 + tk: int = tgt.kind() + if tk == ast.ASTKind.Name: return _HandleNameBind(ast.Name*(tgt), stmt.value) + if tk == ast.ASTKind.Attribute: return _HandleAttrAssign(ast.Attribute*(tgt), stmt.value) + return 0 + + +def _HandleAnnAssign(stmt: ast.AnnAssign*) -> int: + """Handle an annotated assignment (including define-desugared AnnAssign).""" + if stmt is None or stmt.target is None or stmt.value is None: return 0 + if stmt.target.kind() != ast.ASTKind.Name: return 0 + return _HandleNameBind(ast.Name*(stmt.target), stmt.value) + + +def _HandleNameBind(tgt: ast.Name*, vnode: ast.AST*) -> int: + """Handle NAME = value: Project binding, string constant, or list constant.""" + global _c_has_project + if tgt is None or vnode is None or tgt.id is None: return 0 + # Reading a Project macro object on the RHS is fatal (reading forbidden) + if _IsProjRef(vnode) != 0: + VLogger.error(f"viperbuild: reading the Project macro object is forbidden ({tgt.id} = proj ...)", "viperbuild") + return 1 + # Project instance binding + if _IsProjectCtor(vnode) != 0: + if _c_has_project != 0: + VLogger.error("viperbuild: only one Project object is allowed per dependency closure", "viperbuild") + return 1 + _c_has_project = 1 + _proj_vars.append(_DupScratch(tgt.id)) + cl: ast.Call* = ast.Call*(vnode) + return _ApplyCtorArgs(cl) + # String constant + if _EvalStr(vnode) != 0: + _f_str_names.append(tgt.id) + _f_str_vals.append(_DupScratch(_g_out_str)) + return 0 + # List constant + if _scratch is not None: + tmp: list[str]* = list[str](_scratch, 8) + if tmp is not None: + if _EvalList(vnode, tmp) != 0: + _f_list_names.append(tgt.id) + _f_list_vals.append(_JoinSep(tmp)) + return 0 + tmp.clear() + return 0 + + +def _SetAdvancedField(fname: str, vnode: ast.AST*) -> int: + """Apply proj.advanced. = ; 0=success.""" + global _c_adv_asan, _c_adv_traceback + if fname is None or vnode is None: return 1 + b: int = _EvalBool(vnode) + if b < 0: + VLogger.error(f"viperbuild: proj.advanced.{fname} must be a boolean constant (True/False)", "viperbuild") + return 1 + if string.strcmp(fname, "asan") == 0: + _c_adv_asan = b + return 0 + if string.strcmp(fname, "traceback") == 0: + _c_adv_traceback = b + return 0 + VLogger.error(f"viperbuild: unknown field proj.advanced.{fname} (supported: asan / traceback)", "viperbuild") + return 1 + + +def _SetCleanField(fname: str, vnode: ast.AST*) -> int: + """Apply proj.clean. = ; 0=success.""" + global _c_clean_all, _c_clean_output, _c_clean_temp, _c_clean_build_dir, _c_clean_includes + if fname is None or vnode is None: return 1 + b: int = _EvalBool(vnode) + if b < 0: + VLogger.error(f"viperbuild: proj.clean.{fname} must be a boolean constant (True/False)", "viperbuild") + return 1 + if string.strcmp(fname, "all") == 0: + _c_clean_all = b + return 0 + if string.strcmp(fname, "output") == 0: + _c_clean_output = b + return 0 + if string.strcmp(fname, "temp") == 0: + _c_clean_temp = b + return 0 + if string.strcmp(fname, "build_dir") == 0: + _c_clean_build_dir = b + return 0 + if string.strcmp(fname, "includes") == 0: + _c_clean_includes = b + return 0 + VLogger.error(f"viperbuild: unknown field proj.clean.{fname} (supported: all / output / temp / build_dir / includes)", "viperbuild") + return 1 + + +def _HandleAttrAssign(tgt: ast.Attribute*, vnode: ast.AST*) -> int: + """Handle proj. = , proj.advanced. / proj.clean. = .""" + if tgt is None or tgt.attr is None or tgt.value is None: return 0 + # Nested form: proj.advanced.{asan,traceback} / proj.clean.{output,temp,build_dir,includes} + if tgt.value.kind() == ast.ASTKind.Attribute: + mid: ast.Attribute* = ast.Attribute*(tgt.value) + if mid is None or mid.attr is None or mid.value is None: return 0 + if mid.value.kind() != ast.ASTKind.Name: return 0 + mn: ast.Name* = ast.Name*(mid.value) + if mn is None or mn.id is None: return 0 + if _FindInList(_proj_vars, mn.id) < 0: return 0 + if string.strcmp(mid.attr, "advanced") != 0 and string.strcmp(mid.attr, "clean") != 0: + VLogger.error(f"viperbuild: unknown nested field proj.{mid.attr}.{tgt.attr}", "viperbuild") + return 1 + if _IsProjRef(vnode) != 0: + VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild") + return 1 + if string.strcmp(mid.attr, "clean") == 0: return _SetCleanField(tgt.attr, vnode) + return _SetAdvancedField(tgt.attr, vnode) + if tgt.value.kind() != ast.ASTKind.Name: return 0 + bn: ast.Name* = ast.Name*(tgt.value) + if bn is None or bn.id is None: return 0 + if _FindInList(_proj_vars, bn.id) < 0: return 0 + if _IsProjRef(vnode) != 0: + VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild") + return 1 + return _SetField(tgt.attr, vnode) + + +def _HandleConfigCall(cl: ast.Call*) -> int: + """Handle a proj.config(kw=...) call.""" + if cl is None or cl.keywords is None: return 0 + n: sizet = cl.keywords.__len__() + for i in range(int(n)): + kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i)) + if kw is not None and kw.arg is not None and kw.value is not None: + if _SetField(kw.arg, kw.value) != 0: return 1 + return 0 + + +def _JoinRunArgs(lst: list[str]*) -> str: + """Join run args for CreateProcess: elements containing blanks are wrapped in double quotes. + + Embedded double quotes are rejected (fail-fast) — Windows argv quoting rules would make the + actual argv ambiguous, and macro config should not silently alter the user's arguments. + """ + if lst is None: return None + n: sizet = len(lst) + if n == 0: return None + total: sizet = 0 + for i in range(int(n)): + s: str = lst.get(sizet(i)) + if s is None: continue + total += string.strlen(s) + 3 + buf: str = stdlib.malloc(total + 1) + if buf is None: return None + pos: sizet = 0 + for i in range(int(n)): + s = lst.get(sizet(i)) + if s is None: continue + sl: sizet = string.strlen(s) + need_quote: int = 0 + j: sizet = 0 + while j < sl: + if s[j] == ' ' or s[j] == '\t': + need_quote = 1 + if s[j] == '"': + stdlib.free(buf) + return None + j += 1 + if pos > 0: + buf[pos] = ' ' + pos += 1 + if need_quote != 0: + buf[pos] = '"' + pos += 1 + string.strcpy(buf + pos, s) + pos += sl + if need_quote != 0: + buf[pos] = '"' + pos += 1 + buf[pos] = '\0' + return buf + + +def _HandleBuildCall(cl: ast.Call*) -> int: + """Handle a proj.build(triple=, datalayout=, selfmachine=, run=, args=) call.""" + global _c_triple, _c_datalayout, _c_selfmachine, _c_has_build, _c_run, _c_run_args + if cl is None: return 1 + if _c_has_build != 0: + VLogger.error("viperbuild: proj.build() may be called only once", "viperbuild") + return 1 + _c_has_build = 1 + if cl.keywords is None: return 0 + n: sizet = cl.keywords.__len__() + for i in range(int(n)): + kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i)) + if kw is not None and kw.arg is not None and kw.value is not None: + if string.strcmp(kw.arg, "triple") == 0: + if _EvalStr(kw.value) == 0: + VLogger.error("viperbuild: build(triple=...) must be a string constant", "viperbuild") + return 1 + _c_triple = _DupStd(_g_out_str) + elif string.strcmp(kw.arg, "datalayout") == 0: + if _EvalStr(kw.value) == 0: + VLogger.error("viperbuild: build(datalayout=...) must be a string constant", "viperbuild") + return 1 + _c_datalayout = _DupStd(_g_out_str) + elif string.strcmp(kw.arg, "selfmachine") == 0: + b: int = _EvalBool(kw.value) + if b < 0: + VLogger.error("viperbuild: build(selfmachine=...) must be a boolean constant", "viperbuild") + return 1 + _c_selfmachine = b + elif string.strcmp(kw.arg, "run") == 0: + rb: int = _EvalBool(kw.value) + if rb < 0: + VLogger.error("viperbuild: build(run=...) must be a boolean constant", "viperbuild") + return 1 + _c_run = rb + elif string.strcmp(kw.arg, "args") == 0: + if _scratch is None: return 1 + tmp_args: list[str]* = list[str](_scratch, 8) + if tmp_args is None: return 1 + if _EvalList(kw.value, tmp_args) == 0: + VLogger.error("viperbuild: build(args=...) must be a list[str] constant expression", "viperbuild") + return 1 + joined_args: str = _JoinRunArgs(tmp_args) + # None + non-empty list = element rejected (embedded quote); empty list is legal. + if joined_args is None and len(tmp_args) > 0: + VLogger.error("viperbuild: build(args=...) has an element that cannot be passed through (embedded double quote); quote it in code or avoid it", "viperbuild") + return 1 + _c_run_args = joined_args + # Non-empty args imply run: `build(args=["--a"])` == run the exe with --a. + if _c_run_args is not None and string.strlen(_c_run_args) > 0: _c_run = 1 + else: + VLogger.error(f"viperbuild: unsupported keyword {kw.arg} for build(...)", "viperbuild") + return 1 + return 0 + + +def _CallArgsHaveProjRef(cl: ast.Call*) -> int: + """Whether any call argument references a Project macro object; 1=yes.""" + if cl is None: return 0 + if cl.args is not None: + n: sizet = cl.args.__len__() + for i in range(int(n)): + if _IsProjRef(cl.args.get(i)) != 0: return 1 + if cl.keywords is not None: + n = cl.keywords.__len__() + for i in range(int(n)): + kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i)) + if kw is not None and kw.value is not None: + if _IsProjRef(kw.value) != 0: return 1 + return 0 + + +def _HandleExprStmt(stmt: ast.Expr*) -> int: + """Handle an expression statement: proj.config/build calls, else check reads.""" + if stmt is None or stmt.value is None: return 0 + v: ast.AST* = stmt.value + if v.kind() != ast.ASTKind.Call: return 0 + cl: ast.Call* = ast.Call*(v) + if cl is None or cl.func is None: return 0 + # Non-Project method call: if an argument reads the macro object -> fatal (reading forbidden) + if cl.func.kind() != ast.ASTKind.Attribute: + if _CallArgsHaveProjRef(cl) != 0: + VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild") + return 1 + return 0 + at: ast.Attribute* = ast.Attribute*(cl.func) + if at is None or at.attr is None or at.value is None: return 0 + if at.value.kind() != ast.ASTKind.Name: + if _CallArgsHaveProjRef(cl) != 0: + VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild") + return 1 + return 0 + bn: ast.Name* = ast.Name*(at.value) + if bn is None or bn.id is None: return 0 + if _FindInList(_proj_vars, bn.id) < 0: + if _CallArgsHaveProjRef(cl) != 0: + VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild") + return 1 + return 0 + if string.strcmp(at.attr, "config") == 0: return _HandleConfigCall(cl) + if string.strcmp(at.attr, "build") == 0: return _HandleBuildCall(cl) + VLogger.error(f"viperbuild: unsupported method {at.attr}() on Project object", "viperbuild") + return 1 + + +# ---------------- ffilib.CDLL(纯构建配置) ---------------- + +# _BoolConst - 布尔字面量 -> 1(True) / 0(False) / -1(不是布尔常量) +def _BoolConst(node: ast.AST*) -> int: + """布尔常量求值;非布尔常量返回 -1""" + if node is None or node.kind() != ast.ASTKind.Constant: return -1 + c: ast.Constant* = ast.Constant*(node) + if c is None or c.const_kind != ast.CONST_BOOL: return -1 + if c.int_val != 0: return 1 + return 0 + + +# _AppendLibPath - 把路径追加到 static_libs(slot=0) / dynamic_libs(slot=1) 的空格拼接串 +def _AppendLibPath(slot: int, path: str) -> int: + """追加库路径;0=成功""" + global _c_static_libs, _c_dynamic_libs + if path is None: return 1 + cur: str = _c_static_libs + if slot == 1: cur = _c_dynamic_libs + pl: sizet = string.strlen(path) + if pl == 0: return 0 + ol: sizet = 0 + if cur is not None: ol = string.strlen(cur) + buf: str = stdlib.malloc(ol + pl + 2) + if buf is None: return 1 + pos: sizet = 0 + if ol > 0: + string.strcpy(buf, cur) + buf[ol] = ' ' + pos = ol + 1 + string.strcpy(buf + pos, path) + if slot == 1: _c_dynamic_libs = buf + else: _c_static_libs = buf + return 0 + + +# _IsCDLLCall - node 是否是 ffilib.CDLL(...)(别名/全名都认) +def _IsCDLLCall(node: ast.AST*) -> int: + """是 ffilib.CDLL 调用时返回 1""" + if node is None or node.kind() != ast.ASTKind.Call: return 0 + cl: ast.Call* = ast.Call*(node) + if cl is None or cl.func is None: return 0 + if cl.func.kind() != ast.ASTKind.Attribute: return 0 + at: ast.Attribute* = ast.Attribute*(cl.func) + if at is None or at.attr is None or string.strcmp(at.attr, "CDLL") != 0: return 0 + if at.value is None or at.value.kind() != ast.ASTKind.Name: return 0 + nm: ast.Name* = ast.Name*(at.value) + if nm.id is None: return 0 + if _ffi_alias is not None and string.strcmp(nm.id, _ffi_alias) == 0: return 1 + if string.strcmp(nm.id, "ffilib") == 0: return 1 + return 0 + + +# _RegisterFfiHandle - 登记 CDLL 句柄(变量名 -> runtime + 库路径) +def _RegisterFfiHandle(handle: str, runtime: int, path: str) -> None: + """登记 ffilib.CDLL 绑定的变量名、runtime 标志与库路径;句柄名为 None 时忽略""" + global _ffi_handles_names, _ffi_handles_rt, _ffi_handles_path + if handle is None or _ffi_handles_names is None or _ffi_handles_rt is None or _ffi_handles_path is None: return + nm: str = _DupScratch(handle) + if nm is None: return + pth: str = _DupScratch(path) + _ffi_handles_names.append(nm) + _ffi_handles_rt.append(runtime) + _ffi_handles_path.append(pth) + # runtime=True:thunk 运行期要调 _ffilib.Load/Sym,把该运行时库加入依赖闭包 + # (按需可达性扫描只收录被 import 的模块,否则 _resolve_module_sha1("_ffilib") 为 None)。 + if runtime != 0: _AddPending("_ffilib") + + +# _AssignTargetName - 取 `x = ...` 的赋值目标名;非简单 Name 时返回 None +def _AssignTargetName(asn: ast.Assign*) -> str: + """赋值目标名;多目标/非 Name 返回 None""" + if asn is None or asn.targets is None: return None + if len(asn.targets) != 1: return None + t: ast.AST* = asn.targets.get(0) + if t is None or t.kind() != ast.ASTKind.Name: return None + tn: ast.Name* = ast.Name*(t) + if tn is None: return None + return tn.id + + +# _HandleCDLL - 按实参形态写入静态/动态库表;list 回退形式本期 fail-fast +def _HandleCDLL(node: ast.AST*, handle: str) -> int: + """抽取 ffilib.CDLL(...):0=成功""" + if node is None or node.kind() != ast.ASTKind.Call: return 0 + call: ast.Call* = ast.Call*(node) + args: list[ast.AST*]* = call.args + if args is None or len(args) < 1: + VLogger.error("ffilib: CDLL(...) requires at least one argument", "ffilib") + return 1 + a0: ast.AST* = args.get(0) + if a0 is None: return 1 + + # 1) tag 形式:Project.StaticLib(path) / Project.DynamicLib(path) + if a0.kind() == ast.ASTKind.Call: + tag: ast.Call* = ast.Call*(a0) + is_static: int = 0 + is_dynamic: int = 0 + if tag is not None and tag.func is not None and tag.func.kind() == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(tag.func) + if at is not None and at.attr is not None: + if string.strcmp(at.attr, "StaticLib") == 0: is_static = 1 + elif string.strcmp(at.attr, "DynamicLib") == 0: is_dynamic = 1 + if is_static == 0 and is_dynamic == 0: + VLogger.error("ffilib: CDLL(tag) expects Project.StaticLib(path) or Project.DynamicLib(path)", "ffilib") + return 1 + if tag.args is None or len(tag.args) < 1: + VLogger.error("ffilib: StaticLib/DynamicLib requires a path argument", "ffilib") + return 1 + p0: ast.AST* = tag.args.get(0) + if p0 is None or p0.kind() != ast.ASTKind.Constant: + VLogger.error("ffilib: library path must be a string constant", "ffilib") + return 1 + c0: ast.Constant* = ast.Constant*(p0) + if c0 is None or c0.const_kind != ast.CONST_STR: + VLogger.error("ffilib: library path must be a string constant", "ffilib") + return 1 + if is_static != 0: + rc_t: int = _AppendLibPath(0, c0.str_val) + if rc_t == 0: _RegisterFfiHandle(handle, 0, c0.str_val) + return rc_t + rc_td: int = _AppendLibPath(1, c0.str_val) + if rc_td == 0: _RegisterFfiHandle(handle, 1, c0.str_val) + return rc_td + + # 2) 回退列表(预留):需要 _ffilib 运行时库,本期编译期拦截 + if a0.kind() == ast.ASTKind.List: + VLogger.error("ffilib: CDLL(list[str], runtime) fallback-list form is not implemented yet (requires the _ffilib runtime library)", "ffilib") + return 1 + + # 3) 字符串 + runtime(位置参数或 runtime= 关键字) + if a0.kind() != ast.ASTKind.Constant: + VLogger.error("ffilib: CDLL(path, runtime) path must be a string constant", "ffilib") + return 1 + c1: ast.Constant* = ast.Constant*(a0) + if c1 is None or c1.const_kind != ast.CONST_STR: + VLogger.error("ffilib: CDLL path must be a string constant", "ffilib") + return 1 + rt: int = -1 + if len(args) >= 2: rt = _BoolConst(args.get(1)) + if rt < 0 and call.keywords is not None: + kn: sizet = len(call.keywords) + for ki in range(int(kn)): + kw: ast.Keyword* = ast.Keyword*(call.keywords.get(ki)) + if kw is None or kw.arg is None: continue + if string.strcmp(kw.arg, "runtime") == 0: + rt = _BoolConst(kw.value) + break + if rt < 0: + VLogger.error("ffilib: CDLL(path) needs a constant runtime=True/False, or use Project.StaticLib/DynamicLib", "ffilib") + return 1 + if rt == 1: + # runtime=True:不进链接输入(运行期由 thunk 用绝对路径 LoadLibraryA), + # 只登记句柄与路径;否则会同时生成导入表依赖,与 thunk 重复。 + _RegisterFfiHandle(handle, 1, c1.str_val) + return 0 + rc_s: int = _AppendLibPath(0, c1.str_val) + if rc_s == 0: _RegisterFfiHandle(handle, 0, c1.str_val) + return rc_s + + +# IsCdlLDeclCall - 供 Gen 层判断"这条模块级赋值是 ffilib.CDLL 声明"(纯编译期配置,不能生成全局) +def IsCdlLDeclCall(node: ast.AST*) -> int: + """node 是 ffilib.CDLL(...) 调用时返回 1""" + return _IsCDLLCall(node) + + +# HasRuntimeFfiHandle - 是否存在 runtime=True 的 CDLL 句柄(决定是否要引入 _ffilib 依赖) +def HasRuntimeFfiHandle() -> int: + """至少一个句柄 runtime=1 时返回 1""" + global _ffi_handles_rt + if _ffi_handles_rt is None: return 0 + n: sizet = _ffi_handles_rt.__len__() + for i in range(int(n)): + if _ffi_handles_rt.get(i) != 0: return 1 + return 0 + + +# FfiHandlePath - 查询 extern name(...) 的库路径(runtime=True 的 thunk 用它 LoadLibraryA) +def FfiHandlePath(name: str) -> str: + """库路径;未登记返回 None""" + global _ffi_handles_names, _ffi_handles_path + if name is None or _ffi_handles_names is None or _ffi_handles_path is None: return None + n: sizet = _ffi_handles_names.__len__() + for i in range(int(n)): + nm: str = _ffi_handles_names.get(i) + if nm is not None and string.strcmp(nm, name) == 0: return _ffi_handles_path.get(i) + return None + + +# FfiHandleRuntime - 查询 extern name(...) 的库句柄标志 +def FfiHandleRuntime(name: str) -> int: + """1=运行时加载 / 0=链接期 / -1=未登记(不是 ffilib.CDLL 绑定的变量)""" + global _ffi_handles_names, _ffi_handles_rt + if name is None or _ffi_handles_names is None or _ffi_handles_rt is None: return -1 + n: sizet = _ffi_handles_names.__len__() + for i in range(int(n)): + nm: str = _ffi_handles_names.get(i) + if nm is not None and string.strcmp(nm, name) == 0: return _ffi_handles_rt.get(i) + return -1 + + +def _ScanFile(pool: memhub.MemBuddy*, path: str) -> int: + """Parse and scan one .vp file; 0=ok, 1=failed, 2=skippable (unreadable/not .vp).""" + if path is None: return 2 + sl: sizet = string.strlen(path) + if sl == 0: return 2 + if Utils.is_vp_at(path, 0, sl) == 0: + # Not .vp: excluded from the closure (.vpi/.vpnci invisible to the compiler) + return 2 + + src_buf: bytes = Utils.read_file_to_buf(path, VB_SRC_CAP) + if src_buf is None: + VLogger.warning(f"viperbuild: cannot read dependency file {path}", "viperbuild") + return 2 + + if _ResetFileTables(pool) != 0: + stdlib.free(src_buf) + VLogger.error("viperbuild: constant table allocation failed", "viperbuild") + return 1 + + mark: sizet = pool.mark() + lx: ast.Lexer* = ast.new_lexer(pool) + if lx is None: + pool.reset_to(mark) + stdlib.free(src_buf) + VLogger.error("viperbuild: lexer allocation failed", "viperbuild") + return 1 + ast._lexer_init(lx, src_buf, pool) + tokens: ast.Token* = ast.tokenize(lx) + if tokens is None: + pool.reset_to(mark) + stdlib.free(src_buf) + VLogger.error(f"viperbuild: tokenization failed {path}", "viperbuild") + return 1 + tree: ast.AST* = ast.parse_tokens(pool, tokens) + if tree is None: + pool.reset_to(mark) + stdlib.free(src_buf) + VLogger.error(f"viperbuild: parsing failed {path}", "viperbuild") + return 1 + + body: list[ast.AST*]* = tree.children + rc: int = 0 + if body is not None: + cn: sizet = len(body) + for ci in range(int(cn)): + child: ast.AST* = body.get(ci) + if child is not None: + kd: int = child.kind() + if kd == ast.ASTKind.Import: _HandleImport(ast.Import*(child)) + elif kd == ast.ASTKind.ImportFrom: _HandleImportFrom(ast.ImportFrom*(child)) + elif kd == ast.ASTKind.Assign: + asn_f: ast.Assign* = ast.Assign*(child) + if asn_f is not None and _IsCDLLCall(asn_f.value) != 0: rc = _HandleCDLL(asn_f.value, _AssignTargetName(asn_f)) + else: rc = _HandleAssign(asn_f) + elif kd == ast.ASTKind.AnnAssign: rc = _HandleAnnAssign(ast.AnnAssign*(child)) + elif kd == ast.ASTKind.Expr: + ex_f: ast.Expr* = ast.Expr*(child) + if ex_f is not None and _IsCDLLCall(ex_f.value) != 0: rc = _HandleCDLL(ex_f.value, None) + else: rc = _HandleExprStmt(ex_f) + if rc != 0: break + + pool.reset_to(mark) + stdlib.free(src_buf) + return rc + + +# Dependency closure BFS +def _ResolveModuleToFile(module_name: str) -> str: + """Resolve a module name to a local project file path; None if not found.""" + if module_name is None or _search_dirs is None: return None + if string.strlen(module_name) == 0: return None + out_abs: rawptr = stdlib.malloc(8) + out_rel: rawptr = stdlib.malloc(8) + out_inc: rawptr = stdlib.malloc(8) + if out_abs is None or out_rel is None or out_inc is None: + if out_abs is not None: stdlib.free(out_abs) + if out_rel is not None: stdlib.free(out_rel) + if out_inc is not None: stdlib.free(out_inc) + return None + abs_slot: sizet* = (sizet*)(out_abs) + rel_slot: sizet* = (sizet*)(out_rel) + inc_slot: sizet* = (sizet*)(out_inc) + abs_slot[0] = 0 + rel_slot[0] = 0 + inc_slot[0] = 0 + rcode: int = IncludesScanner.ResolveModuleToPath(_search_dirs, module_name, out_abs, out_rel, out_inc) + result: str = None + if rcode == 0 and abs_slot[0] != 0: + abs_path: str = rawptr(abs_slot[0]) + if abs_path is not None: result = _DupScratch(abs_path) + if abs_slot[0] != 0: stdlib.free(rawptr(abs_slot[0])) + if rel_slot[0] != 0: stdlib.free(rawptr(rel_slot[0])) + if inc_slot[0] != 0: stdlib.free(rawptr(inc_slot[0])) + stdlib.free(out_abs) + stdlib.free(out_rel) + stdlib.free(out_inc) + return result + + +def _Enqueue(path: str) -> None: + """Add a file path to the BFS queue (deduped against worklist and visited).""" + if path is None or _work is None: return + if _FindInList(_visited, path) >= 0: return + if _FindInList(_work, path) >= 0: return + _work.append(_DupScratch(path)) + + +def _RunBfs(pool: memhub.MemBuddy*, entry_path: str) -> int: + """Expand the dependency closure from the entry file; 0=success.""" + global _pending + _work.append(_DupScratch(entry_path)) + while _work.__len__() > 0: + cur: str = _work.pop() + if cur is None: continue + if _FindInList(_visited, cur) >= 0: continue + if _visited.__len__() >= VB_MAX_VISIT: + VLogger.error(f"viperbuild: dependency closure exceeds {VB_MAX_VISIT} files, likely a cycle", "viperbuild") + return 1 + _visited.append(_DupScratch(cur)) + + _pending = list[str](_scratch, 8) + if _pending is None: + VLogger.error("viperbuild: BFS queue allocation failed", "viperbuild") + return 1 + rc: int = _ScanFile(pool, cur) + if rc == 1: return 1 + # Resolve module names collected from this file + pn: sizet = _pending.__len__() + for pi in range(int(pn)): + mod: str = _pending.get(pi) + if mod is not None: + dep: str = _ResolveModuleToFile(mod) + if dep is not None: _Enqueue(dep) + return 0 + + +# Defaults / validation / apply +def _DetectHost() -> int: + """Detect the host triple/datalayout via PROCESSOR_ARCHITECTURE; 0=success.""" + global _c_triple, _c_datalayout + arch: str = os.getenv("PROCESSOR_ARCHITECTURE") + if arch is None: + VLogger.error("viperbuild: selfmachine probe failed (PROCESSOR_ARCHITECTURE unavailable)", "viperbuild") + return 1 + if string.strcmp(arch, "AMD64") == 0: + _c_triple = "x86_64-pc-windows-gnu" + _c_datalayout = "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" + return 0 + VLogger.error("viperbuild: selfmachine does not support the host architecture yet (currently only AMD64)", "viperbuild") + return 1 + + +def _ProbeIncludes() -> str: + """Probe up to 3 levels above the exe dir for an includes directory; else None.""" + exe_buf: str = sys.executable() + if exe_buf is None: return None + exe_dir: str = _DirOf(exe_buf) + if exe_dir is None: return None + for k in range(int(3 + 1)): + base: str = _DupStd(exe_dir) + for j in range(int(k)): + parent: str = _DirOf(base) + if parent is None: break + base = _DupStd(parent) + cand: str = os.path.join(base, "includes") + if _IsDir(cand): return cand + return None + + + +def _DeriveOutputFromEntry(entry_path: str) -> str: + """Derive the default output name from the entry file name.""" + base: str = _BaseNameOf(entry_path) + if base is None: return "app.exe" + n: sizet = string.strlen(base) + cut: sizet = n + for i in range(int(n)): + if base[i] == '.': + cut = i + break + head: str = stdlib.malloc(cut + 5) + if head is None: return "app.exe" + string.strncpy(head, base, cut) + head[cut] = '.' + head[cut + 1] = 'e' + head[cut + 2] = 'x' + head[cut + 3] = 'e' + head[cut + 4] = '\0' + return head + + +def _FinalizeCfg(entry_path: str) -> int: + """Fill defaults, validate and write back into Config; 0=success.""" + global _c_source, _c_buildroot, _c_includes, _c_compiler, _c_linker + global _c_static_libs, _c_dynamic_libs, _c_output, _c_triple, _c_datalayout + global _c_selfmachine + + if _c_has_project == 0: + VLogger.error("viperbuild: no vb.Project(...) definition found in the dependency closure", "viperbuild") + return 1 + if _c_has_build == 0: + VLogger.error("viperbuild: proj.build(...) was not called, cannot determine the build target", "viperbuild") + return 1 + + # ---- Defaults ---- + if _c_source is None: _c_source = _DupStd(_entry_dir) + if _c_buildroot is None: _c_buildroot = _Join2(_c_source, "__vpcbuild__") + if _c_includes is None: + probed: str = _ProbeIncludes() + if probed is None: + VLogger.error("viperbuild: could not probe an includes directory (no includes up to 0..3 levels above the exe directory)", "viperbuild") + return 1 + _c_includes = probed + if _c_compiler is None: _c_compiler = _DupStd("llc -filetype=obj -relocation-model=pic") + if _c_linker is None: + # Built-in default = the verified link args from this repo's ViperC/project.vpj. + # Do NOT add -lgcc: msys64/mingw64 + lld has no libgcc and fails with "unable to find library -lgcc". + _c_linker = _DupStd("clang++ -fuse-ld=lld -lmsvcrt -lucrt -lpthread -lmingwex -lkernel32 -Wl,--stack,0x1000000 -Wl,--no-insert-timestamp -Wl,--export-all-symbols -g") + if _c_output is None: _c_output = _DeriveOutputFromEntry(entry_path) + + # ---- Entry file must be under source ---- + if _HasPrefix(entry_path, _c_source) == 0: + VLogger.error(f"viperbuild: entry file is not under the source directory\n entry: {entry_path}\n source: {_c_source}", "viperbuild") + return 1 + + # ---- Target platform ---- + explicit_triple: int = 0 + if _c_triple is not None or _c_datalayout is not None: explicit_triple = 1 + if _c_selfmachine != 0 and explicit_triple != 0: + VLogger.error("viperbuild: build(selfmachine=True) is mutually exclusive with an explicit triple/datalayout", "viperbuild") + return 1 + if explicit_triple != 0: + if _c_triple is None or _c_datalayout is None: + VLogger.error("viperbuild: build() triple and datalayout must be given as a pair", "viperbuild") + return 1 + else: + # Neither given, or selfmachine=True: detect the host at runtime + if _DetectHost() != 0: return 1 + + # ---- Write back to Config ---- + Config.ProjectName = _c_name + Config.ProjectVersion = _c_version + Config.SourceDir = _c_source + Config.BuildDir = _c_buildroot + Config.IncludesDirs = _c_includes + Config.IncludesDir = _FirstSpaceTok(_c_includes) + if Config.IncludesDir is None: Config.IncludesDir = _c_includes + Config.TargetTriple = _c_triple + Config.TargetDataLayout = _c_datalayout + Config.LinkerStaticLibs = _c_static_libs + Config.LinkerDynamicLibs = _c_dynamic_libs + Config.LinkerOutput = _c_output + Config.Sha1SliceLevel = 1 + Config.StrictMode = 1 + _SplitToolCmd(_c_compiler, 0) + _SplitToolCmd(_c_linker, 1) + + Config.resolve_paths(_c_source) + return 0 + + +def _SplitToolCmd(joined: str, which: int) -> None: + """Split a space-joined tool command into Config Cmd/Flags (0=compiler, 1=linker).""" + if joined is None: return + n: sizet = string.strlen(joined) + sp: sizet = n + for i in range(int(n)): + if joined[i] == ' ': + sp = i + break + cmd: str = stdlib.malloc(sp + 1) + if cmd is not None: + string.strncpy(cmd, joined, sp) + cmd[sp] = '\0' + flags: str = None + if sp < n: flags = _DupStd(joined + sp + 1) + if which == 0: + Config.CompilerCmd = cmd + Config.CompilerFlags = flags + else: + Config.LinkerCmd = cmd + Config.LinkerFlags = flags + + +# Entry point +def _ResetAll() -> None: + """Reset module state before each DiscoverAndApply call.""" + global _alias, _ctor_name, _g_out_str, _applied, _entry_dir, _search_dirs + global _c_name, _c_version, _c_source, _c_buildroot, _c_includes + global _c_compiler, _c_linker, _c_static_libs, _c_dynamic_libs, _c_output, _ffi_alias + global _ffi_handles_names, _ffi_handles_rt, _ffi_handles_path + global _c_triple, _c_datalayout, _c_selfmachine, _c_has_project, _c_has_build + global _c_adv_asan, _c_adv_traceback + global _c_clean_all, _c_clean_output, _c_clean_temp, _c_clean_build_dir, _c_clean_includes + global _c_run, _c_run_args + global _saw_viperbuild + _alias = None + _ctor_name = None + _g_out_str = None + _applied = 0 + _entry_dir = None + _search_dirs = None + _saw_viperbuild = 0 + _c_name = None + _c_version = None + _c_source = None + _c_buildroot = None + _c_includes = None + _c_compiler = None + _c_linker = None + _c_static_libs = None + _c_dynamic_libs = None + _ffi_alias = None + _ffi_handles_names = None + _ffi_handles_rt = None + _ffi_handles_path = None + _c_output = None + _c_triple = None + _c_datalayout = None + _c_selfmachine = 0 + _c_has_project = 0 + _c_has_build = 0 + _c_adv_asan = 0 + _c_adv_traceback = 0 + _c_clean_all = 0 + _c_clean_output = 0 + _c_clean_temp = 0 + _c_clean_build_dir = 0 + _c_clean_includes = 0 + _c_run = 0 + _c_run_args = None + + +def DiscoverAndApply(mb: memhub.MemBuddy*, entry_path: str) -> int: + """Pass 0 for --src: discover and apply viperbuild config; 0=success, else failed.""" + global _scratch, _work, _visited, _pending, _proj_vars, _entry_dir, _applied + global _search_dirs + global _ffi_handles_path, _ffi_handles_names, _ffi_handles_rt + + if mb is None or entry_path is None: + VLogger.error("viperbuild: argument is null", "viperbuild") + return 1 + + _ResetAll() + + # Entry dir (default source + closure search root) + _entry_dir = _DirOf(entry_path) + if _entry_dir is None: + VLogger.error(f"viperbuild: cannot resolve the entry file directory {entry_path}", "viperbuild") + return 1 + # Bare filename (no separator) -> _DirOf returns ""; must normalize to ".", otherwise + # ResolveModuleToPath("") tries no candidate and same-dir deps (often the viperbuild config) never resolve. + if string.strlen(_entry_dir) == 0: _entry_dir = _DupStd(".") + # Closure search roots: entry dir first, then "." (the config module may live elsewhere) + _search_dirs = _ConcatStd(_DupStd(_entry_dir), _DupStd(" .")) + + # Scratch pool (BFS working set; not affected by main-pool reset) + arena: bytes = stdlib.malloc(VB_SCRATCH) + if arena is None: + VLogger.error("viperbuild: scratch memory allocation failed", "viperbuild") + return 1 + # Heap-allocate the manager: `_scratch` is a module-level global, so a frame-local + # `MemBuddy` temp would hand out a dangling address once this frame returns (LifetimeError). + # `hlife` is explicit so NLL frame-demotion cannot apply. + _scratch = new heap hlife memhub.MemBuddy(arena, VB_SCRATCH, 0, 0) + if _scratch is None: + VLogger.error("viperbuild: scratch pool initialization failed", "viperbuild") + return 1 + + _work = list[str](_scratch, 8) + _visited = list[str](_scratch, 8) + _pending = list[str](_scratch, 8) + _proj_vars = list[str](_scratch, 8) + _ffi_handles_names = list[str](_scratch, 8) + _ffi_handles_rt = list[int](_scratch, 8) + _ffi_handles_path = list[str](_scratch, 8) + if _work is None or _visited is None or _pending is None or _proj_vars is None: + VLogger.error("viperbuild: working set allocation failed", "viperbuild") + return 1 + if _ffi_handles_names is None or _ffi_handles_rt is None or _ffi_handles_path is None: + VLogger.error("viperbuild: ffilib handle table allocation failed", "viperbuild") + return 1 + + # Keyword/operator tables must be built before the main-pool reset window (else freed -> dangling crash) + ast._init_tables(mb) + + if _RunBfs(mb, entry_path) != 0: return 1 + + if _proj_vars.__len__() == 0: + if _saw_viperbuild == 0: VLogger.error(f"viperbuild: `import viperbuild` not found in the entry file dependency closure (entry: {entry_path}; scanned {_visited.__len__()} files)", "viperbuild") + else: VLogger.error(f"viperbuild: found `import viperbuild` but the dependency closure has no `proj = vb.Project(...)` binding (entry: {entry_path})", "viperbuild") + return 1 + + if _FinalizeCfg(entry_path) != 0: return 1 + + _applied = 1 + return 0 + + +def IsApplied() -> int: + """Whether viperbuild config was applied in this process; 1=yes.""" + return _applied + + +def AdvancedAsan() -> int: + """Whether viperbuild requested ASan mode (proj.advanced.asan = True); 1=yes / 0=no.""" + return _c_adv_asan + + +def AdvancedTraceback() -> int: + """Whether viperbuild requested traceback mode (proj.advanced.traceback = True); 1=yes / 0=no.""" + return _c_adv_traceback + + +def CleanAll() -> int: + """Whether viperbuild requested a full clean (proj.clean.all: buildroot + includes); 1=yes / 0=no.""" + return _c_clean_all + + +def CleanOutput() -> int: + """Whether viperbuild requested cleaning the output dir (proj.clean.output); 1=yes / 0=no.""" + return _c_clean_output + + +def CleanTemp() -> int: + """Whether viperbuild requested cleaning the temp dir (proj.clean.temp); 1=yes / 0=no.""" + return _c_clean_temp + + +def CleanBuildDir() -> int: + """Whether viperbuild requested cleaning the whole build dir (proj.clean.build_dir); 1=yes / 0=no.""" + return _c_clean_build_dir + + +def CleanIncludes() -> int: + """Whether viperbuild requested cleaning the includes cache (proj.clean.includes); 1=yes / 0=no.""" + return _c_clean_includes + + +def RunAfterBuild() -> int: + """Whether viperbuild requested running the exe after a successful build; 1=yes / 0=no.""" + return _c_run + + +def RunArgs() -> str: + """Space-joined run arguments for build(args=[...]) (already quoted where needed); None = none.""" + return _c_run_args diff --git a/App/lib/lsp/LspSemantic.vp b/App/lib/lsp/LspSemantic.vp index 592ee1d..ebafbf4 100644 --- a/App/lib/lsp/LspSemantic.vp +++ b/App/lib/lsp/LspSemantic.vp @@ -578,7 +578,9 @@ def _AppendInt(pool: memhub.MemManager*, arr: json.JsonValue*, value: int, return tail node: json.JsonValue* = json.int_val(pool, int64(value)) if node is None: - return tail + # Pool exhausted: signal failure so _Encode can stop at a token boundary instead of + # emitting a partial token (the constructor is now null-safe and returns None). + return None if tail is None: arr.child = node else: @@ -587,6 +589,35 @@ def _AppendInt(pool: memhub.MemManager*, arr: json.JsonValue*, value: int, return node +def _AppendRollback(arr: json.JsonValue*, tail_before: json.JsonValue*, count_before: sizet) -> json.JsonValue*: + """Undo a partially appended token after an allocation failure.""" + if tail_before is None: + arr.child = None + else: + tail_before.next = None + arr.child_count = count_before + return None + + +def _AppendToken(pool: memhub.MemManager*, arr: json.JsonValue*, + dl: int, dc: int, length: int, typ: int, mods: int, + tail: json.JsonValue*) -> json.JsonValue*: + """Append one 5-int semantic token atomically; None when the pool is exhausted.""" + tail_before: json.JsonValue* = tail + count_before: sizet = arr.child_count + tail = _AppendInt(pool, arr, dl, tail) + if tail is None: return _AppendRollback(arr, tail_before, count_before) + tail = _AppendInt(pool, arr, dc, tail) + if tail is None: return _AppendRollback(arr, tail_before, count_before) + tail = _AppendInt(pool, arr, length, tail) + if tail is None: return _AppendRollback(arr, tail_before, count_before) + tail = _AppendInt(pool, arr, typ, tail) + if tail is None: return _AppendRollback(arr, tail_before, count_before) + tail = _AppendInt(pool, arr, mods, tail) + if tail is None: return _AppendRollback(arr, tail_before, count_before) + return tail + + def _Encode(pool: memhub.MemManager*, out: json.JsonValue*, ast_tokens: list[LspSemToken*]*, lex_tokens: list[LspSemToken*]*, has_range: int, start_line: int, end_line: int): @@ -628,11 +659,8 @@ def _Encode(pool: memhub.MemManager*, out: json.JsonValue*, length: int = t.ec - t.sc if length <= 0: continue - tail = _AppendInt(pool, out, dl, tail) - tail = _AppendInt(pool, out, dc, tail) - tail = _AppendInt(pool, out, length, tail) - tail = _AppendInt(pool, out, t.typ, tail) - tail = _AppendInt(pool, out, t.mods, tail) + tail = _AppendToken(pool, out, dl, dc, length, t.typ, t.mods, tail) + if tail is None: break prev_line = t.sl prev_char = t.sc last = t diff --git a/App/lib/lsp/LspServer.vp b/App/lib/lsp/LspServer.vp index b8969ec..328cb6d 100644 --- a/App/lib/lsp/LspServer.vp +++ b/App/lib/lsp/LspServer.vp @@ -3,9 +3,6 @@ import string import json import memhub import os -import os._win32 -import os._posix -import os.path import lib.lsp.LspProtocol as LspProtocol import lib.lsp.LspTransport as LspTransport import lib.lsp.LspRpc as LspRpc @@ -448,10 +445,7 @@ def _WritePingResult(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, def Run(pool: memhub.MemBuddy*, perm_pool: memhub.MemBuddy*) -> int: global _cancel_int, _cancel_str - os._mbuddy = perm_pool - os._win32._mbuddy = perm_pool - os._posix._mbuddy = perm_pool - os.path._mbuddy = perm_pool + os.set_mbuddy(perm_pool) tr: LspTransport.LspTransport* = LspTransport.LspTransport(perm_pool, pool) if tr is None or not tr.IsValid(): diff --git a/App/lib/lsp/LspTransport.vp b/App/lib/lsp/LspTransport.vp index d20856c..f7e443c 100644 --- a/App/lib/lsp/LspTransport.vp +++ b/App/lib/lsp/LspTransport.vp @@ -1,165 +1,143 @@ -from stdint import * -import stdlib -import string -import viperlib -import memhub -import win32.win32base -import win32.win32file - - -define LSP_READ_BUFFER_SIZE = 65536 -define LSP_HEADER_LINE_MAX = 512 -define LSP_MAX_MESSAGE_BYTES = 16777216 - - -class LspTransport: - pool: memhub.MemManager* - in_handle: win32.win32base.HANDLE - out_handle: win32.win32base.HANDLE - buf: bytes - buf_cap: sizet - buf_len: int - buf_pos: int - eof: int - error: int - - def __new__(self, owner: memhub.MemManager*, request: memhub.MemManager*) -> rawptr: - ptr: LspTransport* = (LspTransport*)(owner.alloc(LspTransport.__sizeof__())) - if ptr: - string.memset(ptr, 0, LspTransport.__sizeof__()) - return rawptr(ptr) - - def __init__(self, owner: memhub.MemManager*, request: memhub.MemManager*): - self.pool = request - self.in_handle = win32.win32file.GetStdHandle(win32.win32file.STD_INPUT_HANDLE) - self.out_handle = win32.win32file.GetStdHandle(win32.win32file.STD_OUTPUT_HANDLE) - self.buf_cap = sizet(LSP_READ_BUFFER_SIZE) - self.buf = stdlib.malloc(self.buf_cap) - self.buf_len = 0 - self.buf_pos = 0 - self.eof = 0 - self.error = 0 - - def IsValid(self) -> bool: - if self.in_handle == win32.win32base.INVALID_HANDLE_VALUE: - return False - if self.out_handle == win32.win32base.INVALID_HANDLE_VALUE: - return False - if self.buf is None: - return False - return True - - def _ReadByte(self) -> int: - if self.buf_pos >= self.buf_len: - if self.eof != 0: - return -1 - n: ULONG = 0 - ok: int = win32.win32file.ReadFile(self.in_handle, self.buf, - ULONG(self.buf_cap), &n, None) - if ok == 0 or int(n) == 0: - self.eof = 1 - return -1 - self.buf_len = int(n) - self.buf_pos = 0 - ch: char = self.buf[self.buf_pos] - self.buf_pos += 1 - return int(ch) - - def ReadLine(self) -> str: - line: bytes = self.pool.alloc(sizet(LSP_HEADER_LINE_MAX)) - if line is None: - self.error = 1 - return None - i: int = 0 - while True: - b: int = self._ReadByte() - if b < 0: - self.error = 1 - return None - if b == 10: - break - if b == 13: - continue - if i >= LSP_HEADER_LINE_MAX - 1: - self.error = 1 - return None - line[i] = char(b) - i += 1 - line[i] = 0 - return str(line) - - def ReadExact(self, n: sizet) -> str: - out: bytes = self.pool.alloc(n + 1) - if out is None: - self.error = 1 - return None - copied: sizet = 0 - while copied < n: - if self.buf_pos < self.buf_len: - avail: sizet = sizet(self.buf_len - self.buf_pos) - take: sizet = n - copied - if take > avail: - take = avail - string.memcpy(out + copied, self.buf + self.buf_pos, take) - self.buf_pos += int(take) - copied += take - else: - got: ULONG = 0 - need: ULONG = ULONG(n - copied) - ok: int = win32.win32file.ReadFile(self.in_handle, out + copied, - need, &got, None) - if ok == 0 or int(got) == 0: - self.error = 1 - return None - copied += sizet(int(got)) - out[n] = 0 - return str(out) - - def ReadMessage(self) -> str: - content_len: int = -1 - while True: - line: str = self.ReadLine() - if line is None: - return None - if len(line) == 0: - break - if string.strncmp(line, "Content-Length:", 15) == 0: - content_len = string.atoi(line + 15) - if content_len <= 0 or content_len > LSP_MAX_MESSAGE_BYTES: - self.error = 1 - return None - return self.ReadExact(sizet(content_len)) - - def _WriteAll(self, data: str, n: sizet) -> int: - sent: sizet = 0 - while sent < n: - written: ULONG = 0 - ok: int = win32.win32file.WriteFile(self.out_handle, data + sent, - ULONG(n - sent), &written, None) - if ok == 0 or int(written) == 0: - return -1 - sent += sizet(int(written)) - return 0 - - def WriteMessage(self, body: str) -> int: - if body is None: - return -1 - blen: sizet = string.strlen(body) - header: bytes = self.pool.alloc(sizet(64)) - if header is None: - return -1 - viperlib.snprintf(header, sizet(64), "Content-Length: %d\r\n\r\n", int(blen)) - if self._WriteAll(str(header), string.strlen(header)) != 0: - return -1 - return self._WriteAll(body, blen) - - -def StderrWrite(msg: str) -> int: - if msg is None: - return 0 - h: win32.win32base.HANDLE = win32.win32file.GetStdHandle(win32.win32file.STD_ERROR_HANDLE) - if h == win32.win32base.INVALID_HANDLE_VALUE: - return -1 - written: ULONG = 0 - win32.win32file.WriteFile(h, msg, ULONG(string.strlen(msg)), &written, None) - return 0 +from stdint import * +import stdlib +import string +import viperlib +import memhub +import stdio + + +define LSP_READ_BUFFER_SIZE = 65536 +define LSP_HEADER_LINE_MAX = 512 +define LSP_MAX_MESSAGE_BYTES = 16777216 + + +class LspTransport: + pool: memhub.MemManager* + buf: bytes + buf_cap: sizet + buf_len: int + buf_pos: int + eof: int + error: int + + def __new__(self, owner: memhub.MemManager*, request: memhub.MemManager*) -> rawptr: + ptr: LspTransport* = (LspTransport*)(owner.alloc(LspTransport.__sizeof__())) + if ptr: + string.memset(ptr, 0, LspTransport.__sizeof__()) + return rawptr(ptr) + + def __init__(self, owner: memhub.MemManager*, request: memhub.MemManager*): + self.pool = request + self.buf_cap = sizet(LSP_READ_BUFFER_SIZE) + self.buf = stdlib.malloc(self.buf_cap) + self.buf_len = 0 + self.buf_pos = 0 + self.eof = 0 + self.error = 0 + + def IsValid(self) -> bool: + return self.buf is not None + + def _ReadByte(self) -> int: + if self.buf_pos >= self.buf_len: + if self.eof != 0: + return -1 + n: sizet = stdio.read_fd(0, self.buf, self.buf_cap) + if n == 0: + self.eof = 1 + return -1 + self.buf_len = int(n) + self.buf_pos = 0 + ch: char = self.buf[self.buf_pos] + self.buf_pos += 1 + return int(ch) + + def ReadLine(self) -> str: + line: bytes = self.pool.alloc(sizet(LSP_HEADER_LINE_MAX)) + if line is None: + self.error = 1 + return None + i: int = 0 + while True: + b: int = self._ReadByte() + if b < 0: + self.error = 1 + return None + if b == 10: + break + if b == 13: + continue + if i >= LSP_HEADER_LINE_MAX - 1: + self.error = 1 + return None + line[i] = char(b) + i += 1 + line[i] = 0 + return str(line) + + def ReadExact(self, n: sizet) -> str: + out: bytes = self.pool.alloc(n + 1) + if out is None: + self.error = 1 + return None + copied: sizet = 0 + while copied < n: + if self.buf_pos < self.buf_len: + avail: sizet = sizet(self.buf_len - self.buf_pos) + take: sizet = n - copied + if take > avail: + take = avail + string.memcpy(out + copied, self.buf + self.buf_pos, take) + self.buf_pos += int(take) + copied += take + else: + got: sizet = stdio.read_fd(0, out + copied, n - copied) + if got == 0: + self.error = 1 + return None + copied += got + out[n] = 0 + return str(out) + + def ReadMessage(self) -> str: + content_len: int = -1 + while True: + line: str = self.ReadLine() + if line is None: + return None + if len(line) == 0: + break + if string.strncmp(line, "Content-Length:", 15) == 0: + content_len = string.atoi(line + 15) + if content_len <= 0 or content_len > LSP_MAX_MESSAGE_BYTES: + self.error = 1 + return None + return self.ReadExact(sizet(content_len)) + + def _WriteAll(self, data: str, n: sizet) -> int: + sent: sizet = 0 + while sent < n: + written: sizet = stdio.write_fd(1, data + sent, n - sent) + if written == 0: + return -1 + sent += written + return 0 + + def WriteMessage(self, body: str) -> int: + if body is None: + return -1 + blen: sizet = string.strlen(body) + header: bytes = self.pool.alloc(sizet(64)) + if header is None: + return -1 + viperlib.snprintf(header, sizet(64), "Content-Length: %d\r\n\r\n", int(blen)) + if self._WriteAll(str(header), string.strlen(header)) != 0: + return -1 + return self._WriteAll(body, blen) + + +def StderrWrite(msg: str) -> int: + if msg is None: + return 0 + written: sizet = stdio.write_fd(2, msg, string.strlen(msg)) + return 0 if written == string.strlen(msg) else -1 diff --git a/App/lib/lsp/LspWorkspace.vp b/App/lib/lsp/LspWorkspace.vp index 6902ebf..b21cb98 100644 --- a/App/lib/lsp/LspWorkspace.vp +++ b/App/lib/lsp/LspWorkspace.vp @@ -1,8 +1,6 @@ from stdint import * import string import os -import os._win32 -import os._posix import os.path import memhub import lib.lsp.LspPools as LspPools diff --git a/App/main.vp b/App/main.vp index e214a7c..c5d0c76 100644 --- a/App/main.vp +++ b/App/main.vp @@ -1,639 +1,638 @@ -import lib.core.Gen.GenApi as GenApi -import lib.core.Gen.GenFlags as GenFlags -import lib.core.VLogger as VLogger -import lib.core.Repl as Repl -import lib.core.SelfBuild as SelfBuild -import lib.lsp.LspServer as LspServer -import lib.lsp.LspTransport as LspTransport -import lib.core.Gen.Translator as HandlesTranslator -import lib.Phase.BuildPipeline as BuildPipeline -import lib.core.IncludesScanner as IncludesScanner -import lib.Stub.StubMerger as StubMerger -import lib.Phase.Phase1 as Phase1 -import lib.Phase.Phase2 as Phase2 -import lib.core.CompilerFactory as CompilerFactory -import lib.core.Gen.Generator as GenGenerator -import lib.Projectrans.Config as Config -import lib.Projectrans.Utils as Utils -import lib.core.builtin.ViperBuild as ViperBuild -import lib -import stdio -import string -import stdlib -import viperlib -import memhub -import argparse -import platmacro -cif platmacro.IS_WINDOWS: - import win32.win32console as win32cmd - import win32.win32base as win32base -import sys -import ast -import os -import os._win32 -import os._posix -import os.path -import shutil -import mlirlite -import subprocess -import lib.Phase.Phase1.Phase1Main as Phase1Main - -# Main Pool 1 GiB(不要简单调大:2 GiB 会让 MemBuddy 内部索引/位宽溢出 —— -# ASan: access-violation at memhub.vp:858 _fl_push,就在 __init__ 阶段。 -# 池耗尽的正解是修泄漏:mlirlite 全局数组 dense 渲染的"每元素 ~5KB 不复用",见 includes e9e1933。) -define POOL_SIZE = 1073741824 -# Permanent memory pool size: 2^29 + 264 = 512MB usable, then change to 1 GiB -define PERM_POOL_SIZE = 1073741824 -# LSP mode uses smaller pools: the language server does not run code generation. -define LSP_POOL_SIZE = 67108864 -define LSP_PERM_POOL_SIZE = 33554432 -# Code page: UTF-8 (Windows 65001) -define CODE_PAGE = 65001 -# Source code buffer size (1MB) -define SRC_BUF_SIZE = 1048576 -# Version (dev/beta/rc) -# Fixed-size so the bytes stay inline in .PRVERSN even when this file is compiled by the -# previous bootstrap compiler; the new `[]char` value-array rule infers the same 14 bytes. -V_VPC_VERSION: const __attribute__(section(".PRVERSN")) [32]char = "0.1.1-dev.964" -define V_VIPER_VERSION = "1.0.6-dev.194 2026-11-03 Non-LTS" -define V_RUNTIME_VERSION = "0.2.4+297-Non-LTS" -define V_TPC_COMMIT = "077d705609" -define V_VPC_COMMIT = "2c65cea97f" -define V_RUNTIME_COMMIT = "a0a71e7a85" -define V_TARGET = "x86_64-w64-windows-gnu" -define V_LLVM = "22.1.8" -# --repl template-project lookup buffers (repl.vpj path / miss diagnostic text) -define REPL_PROJ_PATH_CAP = 1024 -define REPL_PROJ_DIAG_CAP = 2048 - - -# main: Command-line entry -def main(argc: int, argv: []str) -> int: - detect_flag: int = 0 - deep_detect_flag: int = 0 - lsp_flag: int = 0 - cif platmacro.IS_WINDOWS: - win32cmd.SetConsoleOutputCP(CODE_PAGE) - win32cmd.SetConsoleCP(CODE_PAGE) - - for i in range(argc): - if not argv[i]: break - if string.strstr(argv[i], "--detection") is not None: detect_flag = 1 - if string.strstr(argv[i], "--detection-deep") is not None: deep_detect_flag = 1 - if string.strstr(argv[i], "--lsp") is not None: lsp_flag = 1 - - - # [MEMDBG] free-list walk diagnostics are off by default (each free would walk the whole free - # list of its order); --detection / --detection-deep turn them back on with the other checks. - if detect_flag != 0 or deep_detect_flag != 0: memhub._memdbg_enable = 1 - - # Initialize the mbuddy memory pool - pool_size: sizet = sizet(POOL_SIZE) - perm_size: sizet = sizet(PERM_POOL_SIZE) - if lsp_flag != 0: - pool_size = sizet(LSP_POOL_SIZE) - perm_size = sizet(LSP_PERM_POOL_SIZE) - arena: bytes = stdlib.malloc(pool_size) - if arena is None: - stdio.printf("FAIL: malloc for arena failed\n") - return 1 - # Create a global permanent memory pool (perm_pool, 512MB) - perm_arena: bytes = stdlib.malloc(perm_size) - if perm_arena is None: - stdio.printf("FAIL: malloc for perm_pool arena failed\n") - return 1 - - mb: memhub.MemBuddy* = memhub.MemBuddy(arena, pool_size, detect_flag, deep_detect_flag) - if mb is None: - stdio.printf("FAIL: MemBuddy init failed\n") - return 1 - - perm_pool: memhub.MemBuddy* = memhub.MemBuddy(perm_arena, perm_size, detect_flag, deep_detect_flag) - - if perm_pool is None: - stdio.printf("FAIL: perm_pool init failed\n") - return 1 - - CompilerFactory.factory = CompilerFactory.CompilerFactory(mb, perm_pool) - - # set the global mbuddy pointer (needed by sys and argparse) - sys._mbuddy = mb - argparse._mbuddy = mb - ast._mbuddy = mb - lib._mbuddy = mb - Config._mbuddy = mb - Utils._mbuddy = mb - HandlesTranslator._mbuddy = mb - BuildPipeline._mbuddy = mb - IncludesScanner._mbuddy = mb - StubMerger._mbuddy = mb - Phase1._mbuddy = mb - Phase2._mbuddy = mb - subprocess._mbuddy = mb - # Runtime-dep deploy primitives (BuildPipeline.deploy_runtime_deps): os.mkdir + shutil.copyfile. - os._mbuddy = mb - os._win32._mbuddy = mb - os._posix._mbuddy = mb - os.path._mbuddy = mb - shutil._mbuddy = mb - # hashlib._mbuddy = (mb) - VLogger._mbuddy = mb - lib._mbuddy = mb - - # P2: create the project-level Generator singleton (reused across Phase1/Phase2/BuildPipeline; - gen: GenGenerator.Generator* = mb.alloc(GenGenerator.Generator.__sizeof__()) - if gen is None: - stdio.printf("FAIL: Generator alloc failed\n") - return 1 - gen.__before_init__() - gen.__init__(mb) - - GenGenerator._g_gen = gen - # P2-P5: inject module-level Handles singletons (Phase1 calls their state funcs); module globals carry sha1 prefixes, assign each - GenApi._g_gen = gen - # P6: inject statement/assign/var/import/translator-driver module singletons - - # initialize VLogger and print the startup log - VLogger.init_logger() - log: VLogger.Logger* = VLogger.logger - if log is None: log.error("ViperC Failed to parse parameters") - - # initialize command-line args (Windows: GetCommandLineA, POSIX: /proc/self/cmdline) - sys._init_argv() - - # create the argument parser - parser: argparse.ArgumentParser = argparse.ArgumentParser("ViperC", "ViperC command-line parameter parser", pool=mb) - - # register arguments (matching Projectrans.py main()) - parser.add_argument("--project", None, argparse.STRING, 0, None, False, argparse.STORE, "Project VPJ path, by default it looks in the current directory") - parser.add_argument("--src", None, argparse.STRING, 0, None, False, argparse.STORE, "Source directory") - parser.add_argument("--temp", None, argparse.STRING, 0, None, False, argparse.STORE, "Declare a temporary directory for the interface") - parser.add_argument("--output", None, argparse.STRING, 0, None, False, argparse.STORE, "Output directory") - parser.add_argument("--phase", None, argparse.STRING, 0, None, False, argparse.STORE, "Phase: 1 = Generate statement, 2 = Translate and compile, all = Everything") - parser.add_argument("--cc", None, argparse.STRING, 0, None, False, argparse.STORE, "LLVM compiler command") - parser.add_argument("--clean", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Clean output and temp directories") - parser.add_argument("--run", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Run the generated executable after compilation") - parser.add_argument("--rebuild-includes", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Delete the includes.binary precompiled cache and rebuild all the includes") - parser.add_argument("--clear-cache", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Clear .transpyc_cache global cache") - parser.add_argument("--asan", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "ASan mode: .ll first opt -passes=asan; ASan runtime libs must be declared in project.vpj (linker.static_libs)") - parser.add_argument("--traceback", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Traceback mode: force-compile includes/traceback.vp + Install() ctor, llc -frame-pointer=all, link -ldbghelp, export .stackmap") - parser.add_argument("--spend-file", None, argparse.STRING, 0, None, False, argparse.STORE, "vprofile report path for the built program (relative to the output exe directory, ../x.txt and absolute paths allowed); enables instrumentation") - parser.add_argument("--vpf", None, argparse.INT, -1, None, False, argparse.STORE, "vprofile granularity: 0=off 1=call counts 2=+per-function time 3=+caller/callee edges (default 3 with --spend-file)") - parser.add_argument("--phase2command", None, argparse.STRING, 0, None, False, argparse.STORE, "Do not run llc/link: write Phase2 llc(opt+llc) and clang link commands to the given .bat path") - parser.add_argument("--phaseb-legacy", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "PhaseB: use the legacy mlir-opt/mlir-translate/llc child-process flow instead of the in-process ircore.dll backend") - parser.add_argument("--nowarn-register", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Suppress Phase A dependency WARN") - parser.add_argument("--detection", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Enable MemBuddy allocation detection (0xCDCD markers + record-table probe for out-of-bounds writes; applies to both mb and perm_pool)") - # Registered only so argparse accepts the flag: the value is read via raw - # command-line strstr before MemBuddy construction, same as --detection. - parser.add_argument("--detection-deep", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Deep detection: probe every allocation and enable the periodic full-table scan (implies --detection; extremely slow, diagnostics only)") - parser.add_argument("--show-args", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Show all parsed arguments") - parser.add_argument("--repl", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Start the interactive REPL (M2 session variables + M3 cross-fragment functions: fragment -> DLL -> call)") - parser.add_argument("--lsp", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Start the LSP server over stdio (JSON-RPC 2.0)") - parser.add_argument("--build-self", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Compile ViperC into __vpcbuild_2__ and launch copyself to replace __vpcbuild__") - parser.add_argument("--build-dir", None, argparse.STRING, 0, None, False, argparse.STORE, "Override project.vpj build_dir (default ./__vpcbuild__; relative paths resolve against the project dir)") - parser.add_argument("--no-llvmpeephole", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Disable all local mlirlite peephole passes (T2 store/load forward & ret-zero, T3 no-op bitcast) in FunctionPrint serialization") - parser.add_argument("--version", "-v", argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Show version and exit") - parser.add_argument("--help", "-h", argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Show help and exit") - - # parse command-line arguments - # 必须用指针:parse_args 返回 ParsedArgs*,失败时返回 NULL。声明成值类型会让编译器 - # 生成"从返回指针处拷贝 struct"的代码(`mov rax,[rcx]`),NULL 时直接 AV; - # 同时 `args is None` 会退化成对栈地址判空(恒不为真),错误处理永远进不去。 - args: argparse.ParsedArgs* = parser.parse_args(sys._argc, sys._argv) - if args is None: - log.error("Failed to parse arguments", "argparse") - return 1 - - if args.get_bool("help"): - parser.print_help() - return 0 - - if args.get_bool("version"): - stdio.printf("""Viper %s -Copyright (C) 2026 Viper Project -License MIT -GVSDS(TM) Viper SE Runtime Environment and Library (VRTE) %s -Viper Frontend Compiler ViperC %s -Author: terminexus@gvsds.com - -Tpv commit: %s -Tpc commit: %s -VRTE commit: %s -Target: %s - -""", V_VIPER_VERSION, V_RUNTIME_VERSION, V_VPC_VERSION, V_VPC_COMMIT, V_TPC_COMMIT, V_RUNTIME_COMMIT, V_TARGET, V_LLVM) - return 0 - - # --lsp is a standalone stdio server. It must not share stdout with any build - # or diagnostic mode; protocol frames are the only stdout output. - if args.get_bool("lsp"): - if args.get_str("project") is not None or args.get_str("src") is not None or args.get_str("phase") is not None or args.get_bool("run") or args.get_bool("clean") or args.get_bool("repl") or args.get_bool("build-self") or args.get_bool("asan") or args.get_bool("traceback") or args.get_bool("rebuild-includes") or args.get_bool("clear-cache") or args.get_bool("nowarn-register") or args.get_bool("no-llvmpeephole") or args.get_bool("show-args") or args.get_str("phase2command") is not None or args.get_str("build-dir") is not None or args.get_str("output") is not None or args.get_str("temp") is not None or args.get_str("cc") is not None or detect_flag != 0 or deep_detect_flag != 0: - LspTransport.StderrWrite("FAIL: --lsp is a standalone mode; do not combine it with build or diagnostic options\n") - argparse.release(args) - return 2 - argparse.release(args) - return LspServer.Run(mb, perm_pool) - - # --build-self is a standalone mode: it compiles this project into __vpcbuild_2__, - # then launches copyself to replace __vpcbuild__ after a delay. - if args.get_bool("build-self"): - # Forward every other argument verbatim (no whitelist): the rebuilt compiler then - # carries the same modes as a normal build. Only --project/--build-dir are excluded, - # because the self-build invocation owns them. - fwd: str = SelfBuild.CollectForwardedArgs(sys._argc, sys._argv) - argparse.release(args) - return SelfBuild.RunSelfBuild(fwd) - - # --repl guard: mutually exclusive with --phase2command / --asan, checked first - # before their global-state/cleanup side effects; both would break fragment DLLs. - repl_flag: bool = args.get_bool("repl") - if repl_flag: - if args.get_str("phase2command") is not None: - stdio.printf("FAIL: --repl and --phase2command are mutually exclusive\n") - stdio.printf(" Reason: under --phase2command, compile_ll_to_obj only writes the bat and does not run llc,\n") - stdio.printf(" so the fragment .obj is never produced and the subsequent clang++ -shared necessarily fails\n") - stdio.printf(" Fix: drop --phase2command (REPL must produce and load the fragment DLL immediately)\n") - argparse.release(args) - return 2 - if args.get_bool("asan"): - stdio.printf("FAIL: --repl and --asan are mutually exclusive\n") - stdio.printf(" Reason: --asan first instruments the fragment .ll with ASan, but the fragment artifact is a runtime\n") - stdio.printf(" DLL loaded by LoadLibraryA, and the host process does not link the ASan runtime; instrumentation semantics are undefined\n") - stdio.printf(" Fix: drop --asan (to debug memory issues, build an ASan ViperC in project mode)\n") - argparse.release(args) - return 2 - # --traceback is also rejected under --repl: the fragment is a runtime-loaded - # DLL (DI metadata / startup ctor semantics undefined) and it has no .stackmap. - if args.get_bool("traceback"): - stdio.printf("FAIL: --repl and --traceback are mutually exclusive\n") - stdio.printf(" Reason: --traceback relies on startup-ctor injection + a per-module DWARF side file,\n") - stdio.printf(" but the fragment is a runtime LoadLibraryA-loaded DLL; the two semantics are undefined\n") - stdio.printf(" Fix: drop --traceback (for crash stacks, build a --traceback ViperC in project mode)\n") - argparse.release(args) - return 2 - # --spend-file: the fragment DLL is loaded at runtime and does not link the vprofile - # runtime (nor its atexit report); profiling a project build is the supported path. - if args.get_str("spend-file") is not None: - stdio.printf("FAIL: --repl and --spend-file are mutually exclusive\n") - stdio.printf(" Reason: the fragment is a runtime LoadLibraryA-loaded DLL and does not link includes/vprofile.vp\n") - stdio.printf(" Fix: drop --spend-file (profile a project build instead)\n") - argparse.release(args) - return 2 - - # === --repl: M1 interactive loop (expression fragment -> DLL -> echo). Runs - # after project-config load and before Phase1/Phase2, then returns. - - # print parse result - # log.banner("ViperC argument parse result") - - # string arguments - project: str = args.get_str("project") - src: str = args.get_str("src") - temp: str = args.get_str("temp") - output: str = args.get_str("output") - phase: str = args.get_str("phase") - cc: str = args.get_str("cc") - build_dir: str = args.get_str("build-dir") - - # === --project and --src are mutually exclusive: --project uses project.vpj, - # --src uses viperbuild built-in config; giving both fails fast. - if project is not None and src is not None: - stdio.printf("FAIL: --project and --src are mutually exclusive\n") - stdio.printf(" Reason: --project uses project.vpj project mode, --src uses the entry file's built-in viperbuild config\n") - stdio.printf(" Fix: choose one; in --src mode the build config goes in 'import viperbuild' within the entry file's dependency closure\n") - argparse.release(args) - return 2 - - _gb_clean: bool = args.get_bool("clean") - show_args: bool = args.get_bool("show-args") - if show_args: - if project is not None: stdio.printf(" --project: %s\n", project) - else: stdio.printf(" --project: (not specified)\n") - if src is not None: stdio.printf(" --src: %s\n", src) - else: stdio.printf(" --src: (not specified)\n") - if temp is not None: stdio.printf(" --temp: %s\n", temp) - else: stdio.printf(" --temp: (not specified)\n") - if output is not None: stdio.printf(" --output: %s\n", output) - else: stdio.printf(" --output: (not specified)\n") - if phase is not None: stdio.printf(" --phase: %s\n", phase) - else: stdio.printf(" --phase: (not specified, default all)\n") - if cc is not None: stdio.printf(" --cc: %s\n", cc) - else: stdio.printf(" --cc: (not specified)\n") - if _gb_clean: stdio.printf(" --clean: True\n") - else: stdio.printf(" --clean: False\n") - if args.get_bool("run"): stdio.printf(" --run: True\n") - else: stdio.printf(" --run: False\n") - if args.get_bool("rebuild-includes"): stdio.printf(" --rebuild-includes: True\n") - else: stdio.printf(" --rebuild-includes: False\n") - if args.get_bool("clear-cache"): stdio.printf(" --clear-cache: True\n") - else: stdio.printf(" --clear-cache: False\n") - stdio.printf("Argument parsing complete.\n\n") - - # Mode switches: isolate includes caches by suffix so mode-specific objects do - # not mix (ASan/traceback objects are incompatible); suffix combines both flags. - gb_asan: bool = args.get_bool("asan") - gb_traceback: bool = args.get_bool("traceback") - if gb_asan: BuildPipeline._asan_mode = True - else: BuildPipeline._asan_mode = False - if gb_traceback: BuildPipeline._traceback_mode = True - else: BuildPipeline._traceback_mode = False - # PhaseB backend: ircore.dll (single in-process MLIR -> obj call) or the legacy - # mlir-opt/mlir-translate/llc child-process flow (rollback switch). - if args.get_bool("phaseb-legacy"): - BuildPipeline._phaseb_backend = BuildPipeline.PHASEB_BACKEND_LEGACY - log.info("PhaseB: legacy backend (mlir-opt + mlir-translate + llc child processes)", "phaseb") - else: - BuildPipeline._phaseb_backend = BuildPipeline.PHASEB_BACKEND_IRCORE - # --spend-file / --vpf: vprofile instrumentation level. -1 means "--vpf was not given", - # which defaults to 3 (counts + time + edges) when a report path is present; an explicit - # --vpf 0 stays off. - vpf_lvl: int = args.get_int("vpf") - vpf_spend: str = args.get_str("spend-file") - if vpf_spend is None: - vpf_lvl = 0 - elif vpf_lvl < 0: - vpf_lvl = 3 - if vpf_spend is not None and vpf_lvl > 3: - log.error("--vpf must be 0..3 (0=off 1=call counts 2=+per-function time 3=+caller/callee edges)", "vpf") - argparse.release(args) - return 2 - GenFlags._vpf_level = vpf_lvl - if gb_asan and gb_traceback: Config._inc_bin_suffix = ".binary_traceback_asan" - elif gb_traceback: Config._inc_bin_suffix = ".binary_traceback" - elif gb_asan: Config._inc_bin_suffix = ".binary_asan" - else: Config._inc_bin_suffix = ".binary" - if gb_asan: log.info("ASan mode enabled (opt -passes=asan instrumentation; runtime libs must be declared in project.vpj)", "asan") - if gb_traceback: log.info("Traceback mode enabled (llc keep frame pointer + link dbghelp + Install ctor + .stackmap)", "traceback") - # Enabling either mode clears that suffix's includes cache to force a rebuild - if gb_asan or gb_traceback: - mf_inc_bin_mode: str = Config.get_includes_binary_dir() - if mf_inc_bin_mode is not None: - n_mode: int = Utils.CleanDir(mf_inc_bin_mode) - log.info(f"Mode cache cleanup: {mf_inc_bin_mode} deleted {n_mode} files (will rebuild with {Config._inc_bin_suffix})", "mode") - - # === --phase2command: only write llc/link commands to a bat, do not execute them === - p2c: str = args.get_str("phase2command") - if p2c is not None: - BuildPipeline._phase2_cmd_path = p2c - log.info(f"--phase2command: not running llc/link, commands written to {p2c}", "phase2cmd") - - # --nowarn-register: suppress Phase A dependency WARNs (cross-module text.ll not yet generated) - if args.get_bool("nowarn-register"): - VLogger._nowarn_register = 1 - log.info("--nowarn-register enabled: suppress register-class WARN output", "vlogger") - - # --no-llvmpeephole: disable all local peepholes (T2/T3); FunctionPrint serializes directly - if args.get_bool("no-llvmpeephole"): - mlirlite.G_PeepEnable = 0 - log.info("--no-llvmpeephole enabled: disable FunctionPrint local peepholes (RenumberSSA kept)", "peep") - - # === --repl convenience: without --project, search cwd then the exe dir for - # repl.vpj and feed it to Config.Load_project_config; miss fails fast. - proj_loaded: int = 0 - proj_path: str = project - if repl_flag and proj_path is None: - repl_proj_buf: bytes = stdlib.malloc(REPL_PROJ_PATH_CAP) - repl_diag_buf: bytes = stdlib.malloc(REPL_PROJ_DIAG_CAP) - if repl_proj_buf is None or repl_diag_buf is None: - stdio.printf("FAIL: --repl failed to allocate the path buffer for locating repl.vpj\n") - argparse.release(args) - return 1 - if Repl.ReplFindProject(repl_proj_buf, REPL_PROJ_PATH_CAP, repl_diag_buf, REPL_PROJ_DIAG_CAP) != 0: - proj_path = repl_proj_buf - log.info(f"--repl: --project not given, using the located template project {proj_path}", "repl") - else: - stdio.printf("FAIL: --repl could not find the project file repl.vpj\n") - stdio.printf("%s", repl_diag_buf) - stdio.printf(" You can also pass --project to specify the project file explicitly (e.g. ViperC/project.vpj in this repo)\n") - argparse.release(args) - return 1 - # load project config (explicit --project / repl.vpj found by --repl) - if proj_path is not None: - log.banner("Project config") - cfg_ret: int = Config.Load_project_config(proj_path) - if cfg_ret == 0: proj_loaded = 1 - else: log.warning(f"Failed to load project.vpj: {proj_path}", "config") - else: - # try the default project.vpj (current and parent dir); --src mode must not - # implicitly load project.vpj (it would pollute the config source). - if src is None: - proj_path = "project.vpj" - if Config.Load_project_config(proj_path) == 0: proj_loaded = 1 - else: - proj_path = "../project.vpj" - if Config.Load_project_config(proj_path) == 0: proj_loaded = 1 - if proj_loaded == 1: - # extract project.vpj's dir so relative paths resolve against it - proj_len: sizet = string.strlen(proj_path) - slash_pos: sizet = proj_len - for i in range(int(proj_len)): - idx: sizet = proj_len - 1 - i - ch: char = proj_path[idx] - if ch == '/' or ch == '\\': - slash_pos = idx - break - proj_dir: str = "" - if slash_pos > 0 and slash_pos < proj_len: - proj_dir = stdlib.malloc(slash_pos + 1) - if proj_dir is not None: - string.memcpy(proj_dir, proj_path, slash_pos) - proj_dir[slash_pos] = '\0' - # [--build-dir] overrides build_dir before resolve_paths so paths stay project_dir-based - if build_dir is not None: Config.BuildDir = build_dir - Config.resolve_paths(proj_dir) - Config.print_config() - - # === --src: viperbuild built-in config discovery/apply (Pass 0), before the - # switches read Config; a miss or bad config fails fast. - if src is not None: - log.banner("viperbuild") - if ViperBuild.DiscoverAndApply(mb, src) != 0: - argparse.release(args) - return 1 - Config.print_config() - # proj.advanced.asan / .traceback 等价 CLI 的 --asan / --traceback(两者取并集)。 - # 必须在 Phase1/Phase2 之前应用:includes 缓存后缀要与模式一起切换。 - adv_asan: bool = ViperBuild.AdvancedAsan() != 0 - adv_traceback: bool = ViperBuild.AdvancedTraceback() != 0 - if adv_asan or adv_traceback: - if args.get_bool("repl"): - log.error("--repl is mutually exclusive with proj.advanced.asan / .traceback:", "viperbuild") - log.error(" a fragment is a runtime LoadLibraryA DLL; ASan instrumentation and traceback ctor/DWARF semantics are undefined", "viperbuild") - argparse.release(args) - return 2 - prev_asan: bool = BuildPipeline._asan_mode != 0 - prev_traceback: bool = BuildPipeline._traceback_mode != 0 - if adv_asan: BuildPipeline._asan_mode = True - if adv_traceback: BuildPipeline._traceback_mode = True - now_asan: bool = BuildPipeline._asan_mode != 0 - now_traceback: bool = BuildPipeline._traceback_mode != 0 - if now_asan and now_traceback: Config._inc_bin_suffix = ".binary_traceback_asan" - elif now_traceback: Config._inc_bin_suffix = ".binary_traceback" - elif now_asan: Config._inc_bin_suffix = ".binary_asan" - else: Config._inc_bin_suffix = ".binary" - if adv_asan: log.info("ASan mode enabled by viperbuild (proj.advanced.asan)", "asan") - if adv_traceback: log.info("Traceback mode enabled by viperbuild (proj.advanced.traceback)", "traceback") - if now_asan != prev_asan or now_traceback != prev_traceback: - mf_inc_adv: str = Config.get_includes_binary_dir() - if mf_inc_adv is not None: - n_adv: int = Utils.CleanDir(mf_inc_adv) - log.info(f"Mode cache cleanup: {mf_inc_adv} deleted {n_adv} files (will rebuild with {Config._inc_bin_suffix})", "mode") - - # --spend-file / --vpf: instrumented include objects must never mix with normal ones, so - # the include cache gets its own suffix and is cleared once when vpf is enabled. - if GenFlags._vpf_level > 0: - base_sfx: str = Config._inc_bin_suffix - sfx_len: sizet = len(base_sfx) + 24 - sfx_buf: str = stdlib.malloc(sfx_len) - if sfx_buf is not None: - viperlib.snprintf(sfx_buf, sfx_len, "%s.vpf%d", base_sfx, GenFlags._vpf_level) - if string.strcmp(sfx_buf, base_sfx) != 0: - Config._inc_bin_suffix = sfx_buf - mf_inc_vpf: str = Config.get_includes_binary_dir() - if mf_inc_vpf is not None: - n_vpf: int = Utils.CleanDir(mf_inc_vpf) - log.info(f"vprofile: cleared {n_vpf} include objects (cache suffix {Config._inc_bin_suffix})", "vpf") - # Resolve the report path now that the output directory is known: absolute stays, - # relative (including ../x.txt) joins the output exe directory. - spend_path: str = args.get_str("spend-file") - if Config.OutputDir is None: - log.error("--spend-file requires an output directory (project.vpj or viperbuild config)", "vpf") - argparse.release(args) - return 2 - GenFlags._vpf_path = BuildPipeline.ResolveSpendPath(Config.OutputDir, spend_path) - log.info(f"vprofile: level {GenFlags._vpf_level}, report -> {GenFlags._vpf_path}", "vpf") - - # === --repl: M1 interactive loop (fragment -> IR -> llc -> clang++ -shared -> - # DLL -> echo), after config load and before Phase1/Phase2. - if args.get_bool("repl"): - Repl._perm_pool = perm_pool # jmp_buf/parse-table/session buffers must live across calls; must use PermPool - if log is not None: - log.set_jit_flag(True) # errors longjmp back to REPL (reset to False when Repl.run exits) - rc_repl: int = Repl.run(mb) - argparse.release(args) - return rc_repl - - # --asan requires the project to declare the ASan runtime in project.vpj linker.static_libs (no hardcoded paths); missing fails fast - if BuildPipeline._asan_mode != 0 and Config.asan_runtime_declared() == 0: - log.error("Enabling --asan requires declaring the ASan runtime in project.vpj's linker.static_libs:", "asan") - log.error(" /clang64/lib/clang//lib/windows/libclang_rt.asan_dynamic-x86_64.dll.a", "asan") - log.error(" /clang64/lib/clang//lib/windows/libclang_rt.asan_dynamic_runtime_thunk-x86_64.a", "asan") - log.error("The clang64 root is auto-detected from PATH (see README section 8.1)", "asan") - return 1 - - # === --clean: remove temp, output and includes.binary directories === - if _gb_clean: - log.info("Cleaning temp directory...", "clean") - if Config.TempDir is not None: - n: int = Utils.CleanDir(Config.TempDir) - log.info(f"{Config.TempDir}: deleted {n} files", "clean") - if Config.OutputDir is not None: - n2: int = Utils.CleanDir(Config.OutputDir) - log.info(f"{Config.OutputDir}: deleted {n2} files", "clean") - # Clean the includes.binary dir (stale .obj files would skip recompilation) - mf_inc_bin_clean: str = Config.get_includes_binary_dir() - if mf_inc_bin_clean is not None: - n3: int = Utils.CleanDir(mf_inc_bin_clean) - log.info(f"{mf_inc_bin_clean}: deleted {n3} files", "clean") - - # === --rebuild-includes: delete the includes.binary precompile cache === - # Forces all includes to recompile next time (temp/output untouched) - if args.get_bool("rebuild-includes"): - log.info("Rebuilding includes cache...", "rebuild-includes") - mf_inc_bin_rb: str = Config.get_includes_binary_dir() - if mf_inc_bin_rb is not None: - n_rb: int = Utils.CleanDir(mf_inc_bin_rb) - log.info(f"{mf_inc_bin_rb}: deleted {n_rb} files (includes will be recompiled)", "rebuild-includes") - - # === --clear-cache: clear the temp dir cache (.vpi/.stub.ll/.text.ll) === - # Forces Phase1 to regenerate stubs next time (output/includes.binary untouched) - if args.get_bool("clear-cache"): - log.info("Clearing temp cache...", "clear-cache") - if Config.TempDir is not None: - n_cc: int = Utils.CleanDir(Config.TempDir) - log.info(f"{Config.TempDir}: deleted {n_cc} files (.vpi/.stub.ll cache)", "clear-cache") - - # === viperbuild: proj.clean.* 细粒度清理 === - # build_dir 指整个构建根(缺省 {source}/__vpcbuild__,含 temp + output),优先级高于 - # 单列的 temp/output。--clean 已覆盖全部目录时跳过,避免重复删除与重复日志。 - if src is not None and _gb_clean == False: - # clean.all 是总开关:等价于 build_dir + includes(temp/output 已被 build_dir 覆盖), - # 个体开关写 False 不能关掉它。 - c_all: bool = ViperBuild.CleanAll() != 0 - c_build_dir: bool = c_all or ViperBuild.CleanBuildDir() != 0 - c_temp: bool = c_all or ViperBuild.CleanTemp() != 0 - c_output: bool = c_all or ViperBuild.CleanOutput() != 0 - c_includes: bool = c_all or ViperBuild.CleanIncludes() != 0 - if c_build_dir: - if Config.BuildDir is not None: - n_cb: int = Utils.CleanDir(Config.BuildDir) - log.info(f"{Config.BuildDir}: deleted {n_cb} files (proj.clean.build_dir)", "viperbuild-clean") - else: - if c_temp and Config.TempDir is not None: - n_ct: int = Utils.CleanDir(Config.TempDir) - log.info(f"{Config.TempDir}: deleted {n_ct} files (proj.clean.temp)", "viperbuild-clean") - if c_output and Config.OutputDir is not None: - n_co: int = Utils.CleanDir(Config.OutputDir) - log.info(f"{Config.OutputDir}: deleted {n_co} files (proj.clean.output)", "viperbuild-clean") - if c_includes: - mf_inc_clean_vb: str = Config.get_includes_binary_dir() - if mf_inc_clean_vb is not None: - n_ci: int = Utils.CleanDir(mf_inc_clean_vb) - log.info(f"{mf_inc_clean_vb}: deleted {n_ci} files (proj.clean.includes)", "viperbuild-clean") - - # === --phase: 1=stub split only; 2=stub merge+compile only; all=both (default) === - phase_mode: str = phase - if phase_mode is None: phase_mode = "all" - do_phase1: int = 0 - do_phase2: int = 0 - # Note: use string.strcmp instead of == to avoid the compiler == semantics bug - if string.strcmp(phase_mode, "1") == 0 or string.strcmp(phase_mode, "all") == 0: do_phase1 = 1 - if string.strcmp(phase_mode, "2") == 0 or string.strcmp(phase_mode, "all") == 0: do_phase2 = 1 - log.info(f"Mode: {phase_mode} (phase1={do_phase1} phase2={do_phase2})", "phase") - - # === Phase1: scan includes (on-demand translation) === - # For each .vp/.py file under includes, translate and split the stub if absent - if do_phase1 != 0 and Config.IncludesDirs is not None: - ph1_temp: str = Config.TempDir - if ph1_temp is None: ph1_temp = "." - if Phase1Main.RunPhase1(mb, Config.IncludesDirsAll, ph1_temp, log) != 0: - VLogger.error("Phase1 failed, aborting compilation", "Phase1") - return 1 - # Phase1-only (no Phase2): exit right away - if do_phase2 == 0: - log.info("Phase1 complete, exiting", "phase") - return 0 - - # AST parse: when --src or --project is given, read the file and parse it into an AST - src_path: str = args.get_str("src") - - # No project config and no --src: fail fast (uninitialized Config fields are garbage pointers -> crash) - if proj_loaded == 0 and src_path is None: - log.error("project.vpj not found (current/parent dir) and --src not given; cannot build", "config") - argparse.release(args) - return 1 - - # Project mode (multi-file): --project scans source_dir for all .vp; --src + viperbuild - # reuses it (Pass 0 writes config back, SourceDir as root); the two are mutually exclusive. - if Config.SourceDir is not None and (project is not None or ViperBuild.IsApplied() != 0): - mf_temp: str = Config.TempDir if Config.TempDir is not None else "." - mf_output: str = Config.OutputDir if Config.OutputDir is not None else "." - mf_cc: str = Config.CompilerCmd if Config.CompilerCmd is not None else "llc" - mf_cc_flags: str = Config.CompilerFlags if Config.CompilerFlags is not None else "-filetype=obj -relocation-model=pic" - mf_linker: str = Config.LinkerCmd if Config.LinkerCmd is not None else "clang++" - mf_linker_flags: str = Config.LinkerFlags if Config.LinkerFlags is not None else "-lmsvcrt -lucrt -lpthread -lmingwex -lkernel32 -lgcc" - mf_linker_out: str = Config.LinkerOutput if Config.LinkerOutput is not None else "app.exe" - mf_static_libs: str = Config.LinkerStaticLibs - mf_dynamic_libs: str = Config.LinkerDynamicLibs - mf_includes_bin: str = Config.get_includes_binary_dir() - - mf_inc_dirs: str = Config.IncludesDirsAll if Config.IncludesDirsAll is not None else Config.IncludesDirs - mf_ret: int = Phase2.RunMultiFileProject(mb, Config.SourceDir, mf_temp, mf_output, mf_cc, mf_cc_flags, mf_linker, mf_linker_flags, mf_linker_out, mf_includes_bin, mf_inc_dirs, do_phase1, do_phase2, log, args, mf_static_libs, mf_dynamic_libs) - argparse.release(args) - log.success("ViperC complete") - return mf_ret - - # Reaching here means no buildable project root (SourceDir missing): fail fast, no silent "done" - log.error("No buildable project root: project.vpj is missing source_dir, or viperbuild provided no source", "config") - argparse.release(args) - return 1 +import lib.core.Gen.GenApi as GenApi +import lib.core.Gen.GenFlags as GenFlags +import lib.core.VLogger as VLogger +import lib.core.Repl as Repl +import lib.core.SelfBuild as SelfBuild +import lib.lsp.LspServer as LspServer +import lib.lsp.LspTransport as LspTransport +import lib.core.Gen.Translator as HandlesTranslator +import lib.Phase.BuildPipeline as BuildPipeline +import lib.core.IncludesScanner as IncludesScanner +import lib.Stub.StubMerger as StubMerger +import lib.Phase.Phase1 as Phase1 +import lib.Phase.Phase2 as Phase2 +import lib.core.CompilerFactory as CompilerFactory +import lib.core.Gen.Generator as GenGenerator +import lib.Projectrans.Config as Config +import lib.Projectrans.Utils as Utils +import lib.core.builtin.ViperBuild as ViperBuild +import lib +import stdio +import string +import stdlib +import viperlib +import memhub +import argparse +import platmacro +import sys +import ast +import os +import os.path +import shutil +import mlirlite +import subprocess +import lib.Phase.Phase1.Phase1Main as Phase1Main + +# Main Pool 1 GiB(不要简单调大:2 GiB 会让 MemBuddy 内部索引/位宽溢出 —— +# ASan: access-violation at memhub.vp:858 _fl_push,就在 __init__ 阶段。 +# 池耗尽的正解是修泄漏:mlirlite 全局数组 dense 渲染的"每元素 ~5KB 不复用",见 includes e9e1933。) +define POOL_SIZE = 1073741824 +# Permanent memory pool size: 2^29 + 264 = 512MB usable, then change to 1 GiB +define PERM_POOL_SIZE = 1073741824 +# LSP mode uses smaller pools: the language server does not run code generation. +# 256MB: semantic-token analysis of large include graphs (e.g. imgui) allocates symbols, +# ranges and lists in the per-request pool; 64MB exhausted it and the next list allocation +# returned null (ASan: _AppendOutlineSym null list). +define LSP_POOL_SIZE = 268435456 +define LSP_PERM_POOL_SIZE = 33554432 +# Code page: UTF-8 (Windows 65001) +# Source code buffer size (1MB) +define SRC_BUF_SIZE = 1048576 +# Version (dev/beta/rc) +# Fixed-size so the bytes stay inline in .PRVERSN even when this file is compiled by the +# previous bootstrap compiler; the new `[]char` value-array rule infers the same 14 bytes. +V_VPC_VERSION: const __attribute__(section(".PRVERSN")) [32]char = "0.1.1-dev.964" +define V_VIPER_VERSION = "1.0.6-dev.194 2026-11-03 Non-LTS" +define V_RUNTIME_VERSION = "0.2.4+297-Non-LTS" +define V_TPC_COMMIT = "077d705609" +define V_VPC_COMMIT = "2c65cea97f" +define V_RUNTIME_COMMIT = "a0a71e7a85" +define V_TARGET = "x86_64-w64-windows-gnu" +define V_LLVM = "22.1.8" +# --repl template-project lookup buffers (repl.vpj path / miss diagnostic text) +define REPL_PROJ_PATH_CAP = 1024 +define REPL_PROJ_DIAG_CAP = 2048 + + +def _build_target() -> str: + """Target triple this compiler binary was built for (Linux cross-builds differ).""" + if string.strcmp(sys.platform(), "linux") == 0: return "x86_64-pc-linux-gnu" + return V_TARGET + + +# main: Command-line entry +def main(argc: int, argv: []str) -> int: + detect_flag: int = 0 + deep_detect_flag: int = 0 + lsp_flag: int = 0 + cif platmacro.IS_WINDOWS: + sys.set_console_utf8() + + for i in range(argc): + if not argv[i]: break + if string.strstr(argv[i], "--detection") is not None: detect_flag = 1 + if string.strstr(argv[i], "--detection-deep") is not None: deep_detect_flag = 1 + if string.strstr(argv[i], "--lsp") is not None: lsp_flag = 1 + + + # [MEMDBG] free-list walk diagnostics are off by default (each free would walk the whole free + # list of its order); --detection / --detection-deep turn them back on with the other checks. + if detect_flag != 0 or deep_detect_flag != 0: memhub._memdbg_enable = 1 + + # Initialize the mbuddy memory pool + pool_size: sizet = sizet(POOL_SIZE) + perm_size: sizet = sizet(PERM_POOL_SIZE) + if lsp_flag != 0: + pool_size = sizet(LSP_POOL_SIZE) + perm_size = sizet(LSP_PERM_POOL_SIZE) + arena: bytes = stdlib.malloc(pool_size) + if arena is None: + stdio.printf("FAIL: malloc for arena failed\n") + return 1 + # Create a global permanent memory pool (perm_pool, 512MB) + perm_arena: bytes = stdlib.malloc(perm_size) + if perm_arena is None: + stdio.printf("FAIL: malloc for perm_pool arena failed\n") + return 1 + + mb: memhub.MemBuddy* = memhub.MemBuddy(arena, pool_size, detect_flag, deep_detect_flag) + if mb is None: + stdio.printf("FAIL: MemBuddy init failed\n") + return 1 + + perm_pool: memhub.MemBuddy* = memhub.MemBuddy(perm_arena, perm_size, detect_flag, deep_detect_flag) + + if perm_pool is None: + stdio.printf("FAIL: perm_pool init failed\n") + return 1 + + CompilerFactory.factory = CompilerFactory.CompilerFactory(mb, perm_pool) + + # set the global mbuddy pointer (needed by sys and argparse) + sys._mbuddy = mb + argparse._mbuddy = mb + ast._mbuddy = mb + lib._mbuddy = mb + Config._mbuddy = mb + Utils._mbuddy = mb + HandlesTranslator._mbuddy = mb + BuildPipeline._mbuddy = mb + IncludesScanner._mbuddy = mb + StubMerger._mbuddy = mb + Phase1._mbuddy = mb + Phase2._mbuddy = mb + subprocess._mbuddy = mb + # Runtime-dep deploy primitives (BuildPipeline.deploy_runtime_deps): os.mkdir + shutil.copyfile. + os.set_mbuddy(mb) + shutil._mbuddy = mb + # hashlib._mbuddy = (mb) + VLogger._mbuddy = mb + lib._mbuddy = mb + + # P2: create the project-level Generator singleton (reused across Phase1/Phase2/BuildPipeline; + gen: GenGenerator.Generator* = mb.alloc(GenGenerator.Generator.__sizeof__()) + if gen is None: + stdio.printf("FAIL: Generator alloc failed\n") + return 1 + gen.__before_init__() + gen.__init__(mb) + + GenGenerator._g_gen = gen + # P2-P5: inject module-level Handles singletons (Phase1 calls their state funcs); module globals carry sha1 prefixes, assign each + GenApi._g_gen = gen + # P6: inject statement/assign/var/import/translator-driver module singletons + + # initialize VLogger and print the startup log + VLogger.init_logger() + log: VLogger.Logger* = VLogger.logger + if log is None: log.error("ViperC Failed to parse parameters") + + # initialize command-line args (Windows: GetCommandLineA, POSIX: /proc/self/cmdline) + sys._init_argv() + + # create the argument parser + parser: argparse.ArgumentParser = argparse.ArgumentParser("ViperC", "ViperC command-line parameter parser", pool=mb) + + # register arguments (matching Projectrans.py main()) + parser.add_argument("--project", None, argparse.STRING, 0, None, False, argparse.STORE, "Project VPJ path, by default it looks in the current directory") + parser.add_argument("--src", None, argparse.STRING, 0, None, False, argparse.STORE, "Source directory") + parser.add_argument("--temp", None, argparse.STRING, 0, None, False, argparse.STORE, "Declare a temporary directory for the interface") + parser.add_argument("--output", None, argparse.STRING, 0, None, False, argparse.STORE, "Output directory") + parser.add_argument("--phase", None, argparse.STRING, 0, None, False, argparse.STORE, "Phase: 1 = Generate statement, 2 = Translate and compile, all = Everything") + parser.add_argument("--cc", None, argparse.STRING, 0, None, False, argparse.STORE, "LLVM compiler command") + parser.add_argument("--clean", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Clean output and temp directories") + parser.add_argument("--run", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Run the generated executable after compilation") + parser.add_argument("--rebuild-includes", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Delete the includes.binary precompiled cache and rebuild all the includes") + parser.add_argument("--clear-cache", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Clear .transpyc_cache global cache") + parser.add_argument("--asan", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "ASan mode: .ll first opt -passes=asan; ASan runtime libs must be declared in project.vpj (linker.static_libs)") + parser.add_argument("--traceback", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Traceback mode: force-compile includes/traceback.vp + Install() ctor, llc -frame-pointer=all, link -ldbghelp, export .stackmap") + parser.add_argument("--spend-file", None, argparse.STRING, 0, None, False, argparse.STORE, "vprofile report path for the built program (relative to the output exe directory, ../x.txt and absolute paths allowed); enables instrumentation") + parser.add_argument("--vpf", None, argparse.INT, -1, None, False, argparse.STORE, "vprofile granularity: 0=off 1=call counts 2=+per-function time 3=+caller/callee edges (default 3 with --spend-file)") + parser.add_argument("--phase2command", None, argparse.STRING, 0, None, False, argparse.STORE, "Do not run llc/link: write Phase2 llc(opt+llc) and clang link commands to the given .bat path") + parser.add_argument("--phaseb-legacy", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "PhaseB: use the legacy mlir-opt/mlir-translate/llc child-process flow instead of the in-process ircore.dll backend") + parser.add_argument("--nowarn-register", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Suppress Phase A dependency WARN") + parser.add_argument("--detection", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Enable MemBuddy allocation detection (0xCDCD markers + record-table probe for out-of-bounds writes; applies to both mb and perm_pool)") + # Registered only so argparse accepts the flag: the value is read via raw + # command-line strstr before MemBuddy construction, same as --detection. + parser.add_argument("--detection-deep", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Deep detection: probe every allocation and enable the periodic full-table scan (implies --detection; extremely slow, diagnostics only)") + parser.add_argument("--show-args", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Show all parsed arguments") + parser.add_argument("--repl", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Start the interactive REPL (M2 session variables + M3 cross-fragment functions: fragment -> DLL -> call)") + parser.add_argument("--lsp", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Start the LSP server over stdio (JSON-RPC 2.0)") + parser.add_argument("--build-self", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Compile ViperC into __vpcbuild_2__ and launch copyself to replace __vpcbuild__") + parser.add_argument("--build-dir", None, argparse.STRING, 0, None, False, argparse.STORE, "Override project.vpj build_dir (default ./__vpcbuild__; relative paths resolve against the project dir)") + parser.add_argument("--no-llvmpeephole", None, argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Disable all local mlirlite peephole passes (T2 store/load forward & ret-zero, T3 no-op bitcast) in FunctionPrint serialization") + parser.add_argument("--version", "-v", argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Show version and exit") + parser.add_argument("--help", "-h", argparse.BOOL, 0, None, False, argparse.STORE_TRUE, "Show help and exit") + + # parse command-line arguments + # 必须用指针:parse_args 返回 ParsedArgs*,失败时返回 NULL。声明成值类型会让编译器 + # 生成"从返回指针处拷贝 struct"的代码(`mov rax,[rcx]`),NULL 时直接 AV; + # 同时 `args is None` 会退化成对栈地址判空(恒不为真),错误处理永远进不去。 + args: argparse.ParsedArgs* = parser.parse_args(sys._argc, sys._argv) + if args is None: + log.error("Failed to parse arguments", "argparse") + return 1 + + if args.get_bool("help"): + parser.print_help() + return 0 + + if args.get_bool("version"): + stdio.printf("""Viper %s +Copyright (C) 2026 Viper Project +License MIT +GVSDS(TM) Viper SE Runtime Environment and Library (VRTE) %s +Viper Frontend Compiler ViperC %s +Author: terminexus@gvsds.com + +Tpv commit: %s +Tpc commit: %s +VRTE commit: %s +Target: %s + +""", V_VIPER_VERSION, V_RUNTIME_VERSION, V_VPC_VERSION, V_VPC_COMMIT, V_TPC_COMMIT, V_RUNTIME_COMMIT, _build_target(), V_LLVM) + return 0 + + # --lsp is a standalone stdio server. It must not share stdout with any build + # or diagnostic mode; protocol frames are the only stdout output. + if args.get_bool("lsp"): + if args.get_str("project") is not None or args.get_str("src") is not None or args.get_str("phase") is not None or args.get_bool("run") or args.get_bool("clean") or args.get_bool("repl") or args.get_bool("build-self") or args.get_bool("asan") or args.get_bool("traceback") or args.get_bool("rebuild-includes") or args.get_bool("clear-cache") or args.get_bool("nowarn-register") or args.get_bool("no-llvmpeephole") or args.get_bool("show-args") or args.get_str("phase2command") is not None or args.get_str("build-dir") is not None or args.get_str("output") is not None or args.get_str("temp") is not None or args.get_str("cc") is not None or detect_flag != 0 or deep_detect_flag != 0: + LspTransport.StderrWrite("FAIL: --lsp is a standalone mode; do not combine it with build or diagnostic options\n") + argparse.release(args) + return 2 + argparse.release(args) + return LspServer.Run(mb, perm_pool) + + # --build-self is a standalone mode: it compiles this project into __vpcbuild_2__, + # then launches copyself to replace __vpcbuild__ after a delay. + if args.get_bool("build-self"): + # Forward every other argument verbatim (no whitelist): the rebuilt compiler then + # carries the same modes as a normal build. Only --project/--build-dir are excluded, + # because the self-build invocation owns them. + fwd: str = SelfBuild.CollectForwardedArgs(sys._argc, sys._argv) + argparse.release(args) + return SelfBuild.RunSelfBuild(fwd) + + # --repl guard: mutually exclusive with --phase2command / --asan, checked first + # before their global-state/cleanup side effects; both would break fragment DLLs. + repl_flag: bool = args.get_bool("repl") + if repl_flag: + if args.get_str("phase2command") is not None: + stdio.printf("FAIL: --repl and --phase2command are mutually exclusive\n") + stdio.printf(" Reason: under --phase2command, compile_ll_to_obj only writes the bat and does not run llc,\n") + stdio.printf(" so the fragment .obj is never produced and the subsequent clang++ -shared necessarily fails\n") + stdio.printf(" Fix: drop --phase2command (REPL must produce and load the fragment DLL immediately)\n") + argparse.release(args) + return 2 + if args.get_bool("asan"): + stdio.printf("FAIL: --repl and --asan are mutually exclusive\n") + stdio.printf(" Reason: --asan first instruments the fragment .ll with ASan, but the fragment artifact is a runtime\n") + stdio.printf(" DLL loaded by LoadLibraryA, and the host process does not link the ASan runtime; instrumentation semantics are undefined\n") + stdio.printf(" Fix: drop --asan (to debug memory issues, build an ASan ViperC in project mode)\n") + argparse.release(args) + return 2 + # --traceback is also rejected under --repl: the fragment is a runtime-loaded + # DLL (DI metadata / startup ctor semantics undefined) and it has no .stackmap. + if args.get_bool("traceback"): + stdio.printf("FAIL: --repl and --traceback are mutually exclusive\n") + stdio.printf(" Reason: --traceback relies on startup-ctor injection + a per-module DWARF side file,\n") + stdio.printf(" but the fragment is a runtime LoadLibraryA-loaded DLL; the two semantics are undefined\n") + stdio.printf(" Fix: drop --traceback (for crash stacks, build a --traceback ViperC in project mode)\n") + argparse.release(args) + return 2 + # --spend-file: the fragment DLL is loaded at runtime and does not link the vprofile + # runtime (nor its atexit report); profiling a project build is the supported path. + if args.get_str("spend-file") is not None: + stdio.printf("FAIL: --repl and --spend-file are mutually exclusive\n") + stdio.printf(" Reason: the fragment is a runtime LoadLibraryA-loaded DLL and does not link includes/vprofile.vp\n") + stdio.printf(" Fix: drop --spend-file (profile a project build instead)\n") + argparse.release(args) + return 2 + + # === --repl: M1 interactive loop (expression fragment -> DLL -> echo). Runs + # after project-config load and before Phase1/Phase2, then returns. + + # print parse result + # log.banner("ViperC argument parse result") + + # string arguments + project: str = args.get_str("project") + src: str = args.get_str("src") + temp: str = args.get_str("temp") + output: str = args.get_str("output") + phase: str = args.get_str("phase") + cc: str = args.get_str("cc") + build_dir: str = args.get_str("build-dir") + + # === --project and --src are mutually exclusive: --project uses project.vpj, + # --src uses viperbuild built-in config; giving both fails fast. + if project is not None and src is not None: + stdio.printf("FAIL: --project and --src are mutually exclusive\n") + stdio.printf(" Reason: --project uses project.vpj project mode, --src uses the entry file's built-in viperbuild config\n") + stdio.printf(" Fix: choose one; in --src mode the build config goes in 'import viperbuild' within the entry file's dependency closure\n") + argparse.release(args) + return 2 + + _gb_clean: bool = args.get_bool("clean") + show_args: bool = args.get_bool("show-args") + if show_args: + if project is not None: stdio.printf(" --project: %s\n", project) + else: stdio.printf(" --project: (not specified)\n") + if src is not None: stdio.printf(" --src: %s\n", src) + else: stdio.printf(" --src: (not specified)\n") + if temp is not None: stdio.printf(" --temp: %s\n", temp) + else: stdio.printf(" --temp: (not specified)\n") + if output is not None: stdio.printf(" --output: %s\n", output) + else: stdio.printf(" --output: (not specified)\n") + if phase is not None: stdio.printf(" --phase: %s\n", phase) + else: stdio.printf(" --phase: (not specified, default all)\n") + if cc is not None: stdio.printf(" --cc: %s\n", cc) + else: stdio.printf(" --cc: (not specified)\n") + if _gb_clean: stdio.printf(" --clean: True\n") + else: stdio.printf(" --clean: False\n") + if args.get_bool("run"): stdio.printf(" --run: True\n") + else: stdio.printf(" --run: False\n") + if args.get_bool("rebuild-includes"): stdio.printf(" --rebuild-includes: True\n") + else: stdio.printf(" --rebuild-includes: False\n") + if args.get_bool("clear-cache"): stdio.printf(" --clear-cache: True\n") + else: stdio.printf(" --clear-cache: False\n") + stdio.printf("Argument parsing complete.\n\n") + + # Mode switches: isolate includes caches by suffix so mode-specific objects do + # not mix (ASan/traceback objects are incompatible); suffix combines both flags. + gb_asan: bool = args.get_bool("asan") + gb_traceback: bool = args.get_bool("traceback") + if gb_asan: BuildPipeline._asan_mode = True + else: BuildPipeline._asan_mode = False + if gb_traceback: BuildPipeline._traceback_mode = True + else: BuildPipeline._traceback_mode = False + # PhaseB backend: ircore.dll (single in-process MLIR -> obj call) or the legacy + # mlir-opt/mlir-translate/llc child-process flow (rollback switch). + if args.get_bool("phaseb-legacy"): + BuildPipeline._phaseb_backend = BuildPipeline.PHASEB_BACKEND_LEGACY + log.info("PhaseB: legacy backend (mlir-opt + mlir-translate + llc child processes)", "phaseb") + else: + BuildPipeline._phaseb_backend = BuildPipeline.PHASEB_BACKEND_IRCORE + # --spend-file / --vpf: vprofile instrumentation level. -1 means "--vpf was not given", + # which defaults to 3 (counts + time + edges) when a report path is present; an explicit + # --vpf 0 stays off. + vpf_lvl: int = args.get_int("vpf") + vpf_spend: str = args.get_str("spend-file") + if vpf_spend is None: + vpf_lvl = 0 + elif vpf_lvl < 0: + vpf_lvl = 3 + if vpf_spend is not None and vpf_lvl > 3: + log.error("--vpf must be 0..3 (0=off 1=call counts 2=+per-function time 3=+caller/callee edges)", "vpf") + argparse.release(args) + return 2 + GenFlags._vpf_level = vpf_lvl + if gb_asan and gb_traceback: Config._inc_bin_suffix = ".binary_traceback_asan" + elif gb_traceback: Config._inc_bin_suffix = ".binary_traceback" + elif gb_asan: Config._inc_bin_suffix = ".binary_asan" + else: Config._inc_bin_suffix = ".binary" + if gb_asan: log.info("ASan mode enabled (opt -passes=asan instrumentation; runtime libs must be declared in project.vpj)", "asan") + if gb_traceback: log.info("Traceback mode enabled (llc keep frame pointer + link dbghelp + Install ctor + .stackmap)", "traceback") + # Enabling either mode clears that suffix's includes cache to force a rebuild + if gb_asan or gb_traceback: + mf_inc_bin_mode: str = Config.get_includes_binary_dir() + if mf_inc_bin_mode is not None: + n_mode: int = Utils.CleanDir(mf_inc_bin_mode) + log.info(f"Mode cache cleanup: {mf_inc_bin_mode} deleted {n_mode} files (will rebuild with {Config._inc_bin_suffix})", "mode") + + # === --phase2command: only write llc/link commands to a bat, do not execute them === + p2c: str = args.get_str("phase2command") + if p2c is not None: + BuildPipeline._phase2_cmd_path = p2c + log.info(f"--phase2command: not running llc/link, commands written to {p2c}", "phase2cmd") + + # --nowarn-register: suppress Phase A dependency WARNs (cross-module text.ll not yet generated) + if args.get_bool("nowarn-register"): + VLogger._nowarn_register = 1 + log.info("--nowarn-register enabled: suppress register-class WARN output", "vlogger") + + # --no-llvmpeephole: disable all local peepholes (T2/T3); FunctionPrint serializes directly + if args.get_bool("no-llvmpeephole"): + mlirlite.G_PeepEnable = 0 + log.info("--no-llvmpeephole enabled: disable FunctionPrint local peepholes (RenumberSSA kept)", "peep") + + # === --repl convenience: without --project, search cwd then the exe dir for + # repl.vpj and feed it to Config.Load_project_config; miss fails fast. + proj_loaded: int = 0 + proj_path: str = project + if repl_flag and proj_path is None: + repl_proj_buf: bytes = stdlib.malloc(REPL_PROJ_PATH_CAP) + repl_diag_buf: bytes = stdlib.malloc(REPL_PROJ_DIAG_CAP) + if repl_proj_buf is None or repl_diag_buf is None: + stdio.printf("FAIL: --repl failed to allocate the path buffer for locating repl.vpj\n") + argparse.release(args) + return 1 + if Repl.ReplFindProject(repl_proj_buf, REPL_PROJ_PATH_CAP, repl_diag_buf, REPL_PROJ_DIAG_CAP) != 0: + proj_path = repl_proj_buf + log.info(f"--repl: --project not given, using the located template project {proj_path}", "repl") + else: + stdio.printf("FAIL: --repl could not find the project file repl.vpj\n") + stdio.printf("%s", repl_diag_buf) + stdio.printf(" You can also pass --project to specify the project file explicitly (e.g. ViperC/project.vpj in this repo)\n") + argparse.release(args) + return 1 + # load project config (explicit --project / repl.vpj found by --repl) + if proj_path is not None: + log.banner("Project config") + cfg_ret: int = Config.Load_project_config(proj_path) + if cfg_ret == 0: proj_loaded = 1 + else: log.warning(f"Failed to load project.vpj: {proj_path}", "config") + else: + # try the default project.vpj (current and parent dir); --src mode must not + # implicitly load project.vpj (it would pollute the config source). + if src is None: + proj_path = "project.vpj" + if Config.Load_project_config(proj_path) == 0: proj_loaded = 1 + else: + proj_path = "../project.vpj" + if Config.Load_project_config(proj_path) == 0: proj_loaded = 1 + if proj_loaded == 1: + # extract project.vpj's dir so relative paths resolve against it + proj_len: sizet = string.strlen(proj_path) + slash_pos: sizet = proj_len + for i in range(int(proj_len)): + idx: sizet = proj_len - 1 - i + ch: char = proj_path[idx] + if ch == '/' or ch == '\\': + slash_pos = idx + break + proj_dir: str = "" + if slash_pos > 0 and slash_pos < proj_len: + proj_dir = stdlib.malloc(slash_pos + 1) + if proj_dir is not None: + string.memcpy(proj_dir, proj_path, slash_pos) + proj_dir[slash_pos] = '\0' + # [--build-dir] overrides build_dir before resolve_paths so paths stay project_dir-based + if build_dir is not None: Config.BuildDir = build_dir + Config.resolve_paths(proj_dir) + Config.print_config() + + # === --src: viperbuild built-in config discovery/apply (Pass 0), before the + # switches read Config; a miss or bad config fails fast. + if src is not None: + log.banner("viperbuild") + if ViperBuild.DiscoverAndApply(mb, src) != 0: + argparse.release(args) + return 1 + Config.print_config() + # proj.advanced.asan / .traceback 等价 CLI 的 --asan / --traceback(两者取并集)。 + # 必须在 Phase1/Phase2 之前应用:includes 缓存后缀要与模式一起切换。 + adv_asan: bool = ViperBuild.AdvancedAsan() != 0 + adv_traceback: bool = ViperBuild.AdvancedTraceback() != 0 + if adv_asan or adv_traceback: + if args.get_bool("repl"): + log.error("--repl is mutually exclusive with proj.advanced.asan / .traceback:", "viperbuild") + log.error(" a fragment is a runtime LoadLibraryA DLL; ASan instrumentation and traceback ctor/DWARF semantics are undefined", "viperbuild") + argparse.release(args) + return 2 + prev_asan: bool = BuildPipeline._asan_mode != 0 + prev_traceback: bool = BuildPipeline._traceback_mode != 0 + if adv_asan: BuildPipeline._asan_mode = True + if adv_traceback: BuildPipeline._traceback_mode = True + now_asan: bool = BuildPipeline._asan_mode != 0 + now_traceback: bool = BuildPipeline._traceback_mode != 0 + if now_asan and now_traceback: Config._inc_bin_suffix = ".binary_traceback_asan" + elif now_traceback: Config._inc_bin_suffix = ".binary_traceback" + elif now_asan: Config._inc_bin_suffix = ".binary_asan" + else: Config._inc_bin_suffix = ".binary" + if adv_asan: log.info("ASan mode enabled by viperbuild (proj.advanced.asan)", "asan") + if adv_traceback: log.info("Traceback mode enabled by viperbuild (proj.advanced.traceback)", "traceback") + if now_asan != prev_asan or now_traceback != prev_traceback: + mf_inc_adv: str = Config.get_includes_binary_dir() + if mf_inc_adv is not None: + n_adv: int = Utils.CleanDir(mf_inc_adv) + log.info(f"Mode cache cleanup: {mf_inc_adv} deleted {n_adv} files (will rebuild with {Config._inc_bin_suffix})", "mode") + + # --spend-file / --vpf: instrumented include objects must never mix with normal ones, so + # the include cache gets its own suffix and is cleared once when vpf is enabled. + if GenFlags._vpf_level > 0: + base_sfx: str = Config._inc_bin_suffix + sfx_len: sizet = len(base_sfx) + 24 + sfx_buf: str = stdlib.malloc(sfx_len) + if sfx_buf is not None: + viperlib.snprintf(sfx_buf, sfx_len, "%s.vpf%d", base_sfx, GenFlags._vpf_level) + if string.strcmp(sfx_buf, base_sfx) != 0: + Config._inc_bin_suffix = sfx_buf + mf_inc_vpf: str = Config.get_includes_binary_dir() + if mf_inc_vpf is not None: + n_vpf: int = Utils.CleanDir(mf_inc_vpf) + log.info(f"vprofile: cleared {n_vpf} include objects (cache suffix {Config._inc_bin_suffix})", "vpf") + # Resolve the report path now that the output directory is known: absolute stays, + # relative (including ../x.txt) joins the output exe directory. + spend_path: str = args.get_str("spend-file") + if Config.OutputDir is None: + log.error("--spend-file requires an output directory (project.vpj or viperbuild config)", "vpf") + argparse.release(args) + return 2 + GenFlags._vpf_path = BuildPipeline.ResolveSpendPath(Config.OutputDir, spend_path) + log.info(f"vprofile: level {GenFlags._vpf_level}, report -> {GenFlags._vpf_path}", "vpf") + + # === --repl: M1 interactive loop (fragment -> IR -> llc -> clang++ -shared -> + # DLL -> echo), after config load and before Phase1/Phase2. + if args.get_bool("repl"): + Repl._perm_pool = perm_pool # jmp_buf/parse-table/session buffers must live across calls; must use PermPool + if log is not None: + log.set_jit_flag(True) # errors longjmp back to REPL (reset to False when Repl.run exits) + rc_repl: int = Repl.run(mb) + argparse.release(args) + return rc_repl + + # --asan requires the project to declare the ASan runtime in project.vpj linker.static_libs (no hardcoded paths); missing fails fast + if BuildPipeline._asan_mode != 0 and Config.asan_runtime_declared() == 0: + log.error("Enabling --asan requires declaring the ASan runtime in project.vpj's linker.static_libs:", "asan") + log.error(" /clang64/lib/clang//lib/windows/libclang_rt.asan_dynamic-x86_64.dll.a", "asan") + log.error(" /clang64/lib/clang//lib/windows/libclang_rt.asan_dynamic_runtime_thunk-x86_64.a", "asan") + log.error("The clang64 root is auto-detected from PATH (see README section 8.1)", "asan") + return 1 + + # === --clean: remove temp, output and includes.binary directories === + if _gb_clean: + log.info("Cleaning temp directory...", "clean") + if Config.TempDir is not None: + n: int = Utils.CleanDir(Config.TempDir) + log.info(f"{Config.TempDir}: deleted {n} files", "clean") + if Config.OutputDir is not None: + n2: int = Utils.CleanDir(Config.OutputDir) + log.info(f"{Config.OutputDir}: deleted {n2} files", "clean") + # Clean the includes.binary dir (stale .obj files would skip recompilation) + mf_inc_bin_clean: str = Config.get_includes_binary_dir() + if mf_inc_bin_clean is not None: + n3: int = Utils.CleanDir(mf_inc_bin_clean) + log.info(f"{mf_inc_bin_clean}: deleted {n3} files", "clean") + + # === --rebuild-includes: delete the includes.binary precompile cache === + # Forces all includes to recompile next time (temp/output untouched) + if args.get_bool("rebuild-includes"): + log.info("Rebuilding includes cache...", "rebuild-includes") + mf_inc_bin_rb: str = Config.get_includes_binary_dir() + if mf_inc_bin_rb is not None: + n_rb: int = Utils.CleanDir(mf_inc_bin_rb) + log.info(f"{mf_inc_bin_rb}: deleted {n_rb} files (includes will be recompiled)", "rebuild-includes") + + # === --clear-cache: clear the temp dir cache (.vpi/.stub.ll/.text.ll) === + # Forces Phase1 to regenerate stubs next time (output/includes.binary untouched) + if args.get_bool("clear-cache"): + log.info("Clearing temp cache...", "clear-cache") + if Config.TempDir is not None: + n_cc: int = Utils.CleanDir(Config.TempDir) + log.info(f"{Config.TempDir}: deleted {n_cc} files (.vpi/.stub.ll cache)", "clear-cache") + + # === viperbuild: proj.clean.* 细粒度清理 === + # build_dir 指整个构建根(缺省 {source}/__vpcbuild__,含 temp + output),优先级高于 + # 单列的 temp/output。--clean 已覆盖全部目录时跳过,避免重复删除与重复日志。 + if src is not None and _gb_clean == False: + # clean.all 是总开关:等价于 build_dir + includes(temp/output 已被 build_dir 覆盖), + # 个体开关写 False 不能关掉它。 + c_all: bool = ViperBuild.CleanAll() != 0 + c_build_dir: bool = c_all or ViperBuild.CleanBuildDir() != 0 + c_temp: bool = c_all or ViperBuild.CleanTemp() != 0 + c_output: bool = c_all or ViperBuild.CleanOutput() != 0 + c_includes: bool = c_all or ViperBuild.CleanIncludes() != 0 + if c_build_dir: + if Config.BuildDir is not None: + n_cb: int = Utils.CleanDir(Config.BuildDir) + log.info(f"{Config.BuildDir}: deleted {n_cb} files (proj.clean.build_dir)", "viperbuild-clean") + else: + if c_temp and Config.TempDir is not None: + n_ct: int = Utils.CleanDir(Config.TempDir) + log.info(f"{Config.TempDir}: deleted {n_ct} files (proj.clean.temp)", "viperbuild-clean") + if c_output and Config.OutputDir is not None: + n_co: int = Utils.CleanDir(Config.OutputDir) + log.info(f"{Config.OutputDir}: deleted {n_co} files (proj.clean.output)", "viperbuild-clean") + if c_includes: + mf_inc_clean_vb: str = Config.get_includes_binary_dir() + if mf_inc_clean_vb is not None: + n_ci: int = Utils.CleanDir(mf_inc_clean_vb) + log.info(f"{mf_inc_clean_vb}: deleted {n_ci} files (proj.clean.includes)", "viperbuild-clean") + + # === --phase: 1=stub split only; 2=stub merge+compile only; all=both (default) === + phase_mode: str = phase + if phase_mode is None: phase_mode = "all" + do_phase1: int = 0 + do_phase2: int = 0 + # Note: use string.strcmp instead of == to avoid the compiler == semantics bug + if string.strcmp(phase_mode, "1") == 0 or string.strcmp(phase_mode, "all") == 0: do_phase1 = 1 + if string.strcmp(phase_mode, "2") == 0 or string.strcmp(phase_mode, "all") == 0: do_phase2 = 1 + log.info(f"Mode: {phase_mode} (phase1={do_phase1} phase2={do_phase2})", "phase") + + # === Phase1: scan includes (on-demand translation) === + # For each .vp/.py file under includes, translate and split the stub if absent + if do_phase1 != 0 and Config.IncludesDirs is not None: + ph1_temp: str = Config.TempDir + if ph1_temp is None: ph1_temp = "." + if Phase1Main.RunPhase1(mb, Config.IncludesDirsAll, ph1_temp, log) != 0: + VLogger.error("Phase1 failed, aborting compilation", "Phase1") + return 1 + # Phase1-only (no Phase2): exit right away + if do_phase2 == 0: + log.info("Phase1 complete, exiting", "phase") + return 0 + + # AST parse: when --src or --project is given, read the file and parse it into an AST + src_path: str = args.get_str("src") + + # No project config and no --src: fail fast (uninitialized Config fields are garbage pointers -> crash) + if proj_loaded == 0 and src_path is None: + log.error("project.vpj not found (current/parent dir) and --src not given; cannot build", "config") + argparse.release(args) + return 1 + + # Project mode (multi-file): --project scans source_dir for all .vp; --src + viperbuild + # reuses it (Pass 0 writes config back, SourceDir as root); the two are mutually exclusive. + if Config.SourceDir is not None and (project is not None or ViperBuild.IsApplied() != 0): + mf_temp: str = Config.TempDir if Config.TempDir is not None else "." + mf_output: str = Config.OutputDir if Config.OutputDir is not None else "." + mf_cc: str = Config.CompilerCmd if Config.CompilerCmd is not None else "llc" + mf_cc_flags: str = Config.CompilerFlags if Config.CompilerFlags is not None else "-filetype=obj -relocation-model=pic" + mf_linker: str = Config.LinkerCmd if Config.LinkerCmd is not None else "clang++" + mf_linker_flags: str = Config.LinkerFlags if Config.LinkerFlags is not None else "-lmsvcrt -lucrt -lpthread -lmingwex -lkernel32 -lgcc" + mf_linker_out: str = Config.LinkerOutput if Config.LinkerOutput is not None else "app.exe" + mf_static_libs: str = Config.LinkerStaticLibs + mf_dynamic_libs: str = Config.LinkerDynamicLibs + mf_includes_bin: str = Config.get_includes_binary_dir() + + mf_inc_dirs: str = Config.IncludesDirsAll if Config.IncludesDirsAll is not None else Config.IncludesDirs + mf_ret: int = Phase2.RunMultiFileProject(mb, Config.SourceDir, mf_temp, mf_output, mf_cc, mf_cc_flags, mf_linker, mf_linker_flags, mf_linker_out, mf_includes_bin, mf_inc_dirs, do_phase1, do_phase2, log, args, mf_static_libs, mf_dynamic_libs) + argparse.release(args) + log.success("ViperC complete") + return mf_ret + + # Reaching here means no buildable project root (SourceDir missing): fail fast, no silent "done" + log.error("No buildable project root: project.vpj is missing source_dir, or viperbuild provided no source", "config") + argparse.release(args) + return 1 diff --git a/Tools/linux_link.sh b/Tools/linux_link.sh new file mode 100644 index 0000000..95092aa --- /dev/null +++ b/Tools/linux_link.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# linux_link.sh - Windows -> Linux link wrapper for project.linux.vpj. +# +# ViperC emits ELF objects for x86_64-pc-linux-gnu, but Windows has no native +# Linux linker available. Invoke this through WSL: +# +# wsl.exe -d Ubuntu -- bash Tools/linux_link.sh +# +# project.linux.vpj sets linker.cmd to exactly that command. WSL starts in the +# calling Windows directory, so the relative script path resolves; every +# Windows path argument (D:/... or D:\...) is converted with wslpath. +set -e + +args=() +for a in "$@"; do + case "$a" in + [A-Za-z]:[\\/]*) + args+=("$(wslpath -u "$a")") + ;; + *) + args+=("$a") + ;; + esac +done + +exec clang++ "${args[@]}" diff --git a/project.linux.vpj b/project.linux.vpj new file mode 100644 index 0000000..62a4ede --- /dev/null +++ b/project.linux.vpj @@ -0,0 +1,38 @@ +{ + "$schema": "https://raw.githubusercontent.com/TermiNexus/TransPyC/main/schemas/project-schema.json", + "name": "ViperC-Linux", + "version": "1.0.0", + "source_dir": "./App", + "build_dir": "./__vpcbuild_linux__", + "compiler": { + "cmd": "llc", + "flags": [ + "-filetype=obj", + "-relocation-model=pic", + "-O0" + ] + }, + "linker": { + "cmd": "wsl.exe -d Ubuntu -- bash Tools/linux_link.sh", + "flags": [ + "-fuse-ld=lld", + "-lpthread", + "-lm", + "-ldl", + "-g" + ], + "output": "viperc" + }, + "includes": [ + "../includes" + ], + "target": { + "triple": "x86_64-pc-linux-gnu", + "datalayout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128" + }, + "options": { + "slice_level": 3, + "sha1_slice_level": 1, + "strict_mode": true + } +}