--spend-file <path> instruments every compiled function with vpf_enter(id, name) / vpf_leave(id); main additionally calls vpf_install(path, level). includes/vprofile.vp keeps the tables (no allocation on the hot path), times with QPC and writes the report from atexit, so normal return and exit() both produce it. --vpf 0 off | 1 call counts | 2 +per-function time (default) | 3 +caller/callee edges *A report path is resolved against the output exe directory, so ../prof.txt and absolute paths behave as documented regardless of the process cwd. *Instrumented include objects use their own cache suffix (.binary.vpf<N>, stacked with the asan/traceback suffixes) and that cache is cleared once when profiling is enabled. *StubMergerBuild force-adds the vprofile runtime to the reachable set; FuncGenBase emits a declare on demand because the instrumented module usually does not import vprofile. *--repl is rejected: the fragment DLL is loaded at runtime and does not link the runtime. --build-self no longer keeps a whitelist: SelfBuild.CollectForwardedArgs drops only --build-self / --project / --build-dir and forwards every other argument verbatim, so a self-build can enable the same modes as a normal build.
767 lines
37 KiB
Plaintext
767 lines
37 KiB
Plaintext
import lib.core.Gen.GenApi as GenApi
|
|
import lib.core.Gen.GenFlags as GenFlags
|
|
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.StubMergerCollect as StubMergerCollect
|
|
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 as StubMerger
|
|
|
|
# _IsAppModule - whether a module name is an App source file (store entry with empty incdir)
|
|
# ============================================================
|
|
def _IsAppModule(mod_name: str) -> int:
|
|
"""Return whether mod_name is an App source file (store hit with empty incdir)."""
|
|
if mod_name is None: return 0
|
|
if StubMerger._sha1_store_arr is None or StubMerger._sha1_store_mod is None: return 0
|
|
for i in range(int(StubMerger._sha1_store_count)):
|
|
midx: sizet = sizet(i) * 64
|
|
if string.strcmp(StubMerger._sha1_store_mod + midx, mod_name) == 0:
|
|
iidx: sizet = sizet(i) * StubMerger.MAX_INC_DIR_LEN
|
|
if StubMerger._sha1_store_incdir[iidx] == '\0': return 1
|
|
return 0
|
|
return 0
|
|
# _AppendDepsToWorklist - append space-separated module names to the worklist (dedup against worklist/processed).
|
|
# Returns the new length (>=0), or -1 on overflow/arg error (caller fail-fast).
|
|
def _AppendDepsToWorklist(worklist: bytes, wl_len: sizet, deps_str: str, processed: str) -> int:
|
|
if worklist is None or deps_str is None: return -1
|
|
cur_len: sizet = wl_len
|
|
dl: sizet = string.strlen(deps_str)
|
|
dpos: sizet = 0
|
|
while dpos < dl:
|
|
dstart: sizet = dpos
|
|
while dpos < dl and deps_str[dpos] != ' ': dpos += 1
|
|
dlen: sizet = dpos - dstart
|
|
if dpos < dl: dpos += 1
|
|
if dlen == 0: continue
|
|
dmod: bytes = stdlib.malloc(dlen + 1)
|
|
if dmod is None: continue
|
|
string.strncpy(dmod, deps_str + dstart, dlen)
|
|
dmod[dlen] = '\0'
|
|
in_wl: int = 0
|
|
if cur_len > 0:
|
|
if GenApi.is_module_imported(worklist, dmod) != 0: in_wl = 1
|
|
if in_wl == 0 and processed is not None:
|
|
if GenApi.is_module_imported(processed, dmod) != 0: in_wl = 1
|
|
if in_wl == 0:
|
|
if cur_len + dlen + 2 < StubMerger.REACH_WORKLIST_BUF:
|
|
if cur_len > 0 and worklist[cur_len - 1] != ' ':
|
|
worklist[cur_len] = ' '
|
|
cur_len = cur_len + 1
|
|
string.strcpy(worklist + cur_len, dmod)
|
|
cur_len = cur_len + dlen
|
|
worklist[cur_len] = ' '
|
|
cur_len = cur_len + 1
|
|
worklist[cur_len] = '\0'
|
|
else:
|
|
stdlib.free(dmod)
|
|
VLogger.error("dependency worklist overflow", "StubMerger")
|
|
return -1
|
|
stdlib.free(dmod)
|
|
return int(cur_len)
|
|
|
|
# _BuildReachableSha1Set - build the reachable SHA1 set (on-demand dep-graph scan; unreferenced includes are never read).
|
|
# BFS from app_deps: resolve each module to an includes file and read+hash+parse only reachable ones; returns count (>0 ok, 0 none, <0 fail).
|
|
def _BuildReachableSha1Set(mb: memhub.MemBuddy*, source_dir: str, temp_dir: str, reachable_set: str, app_deps: str = None, includes_dirs: str = None) -> int:
|
|
"""Build the reachable SHA1 set by on-demand dependency-graph scanning."""
|
|
if source_dir is None or temp_dir is None or reachable_set is None: return -1
|
|
if includes_dirs is None:
|
|
VLogger.error("includes_dirs is missing; cannot resolve the dependency graph on demand", "Reachable")
|
|
return -1
|
|
|
|
SRC_BUF_SIZE_R: sizet = 1048576
|
|
|
|
# Worklist (space-separated module names)
|
|
worklist: bytes = stdlib.malloc(StubMerger.REACH_WORKLIST_BUF)
|
|
if worklist is None: return -1
|
|
worklist[0] = '\0'
|
|
wl_len: sizet = 0
|
|
|
|
# 1. App scan (skipped when app_deps is non-empty; use the caller-provided initial queue directly)
|
|
if app_deps is None or app_deps[0] == '\0':
|
|
VLogger.info("App scan: resolving import dependencies of source_dir", "Reachable")
|
|
scan_res_r: IncludesScanner.ScanResult* = IncludesScanner.scan_includes(mb, source_dir)
|
|
if scan_res_r is not None:
|
|
es_r: sizet = IncludesScanner.FileEntry.__sizeof__()
|
|
for e_i_r in range(int(scan_res_r.Count)):
|
|
e_r: IncludesScanner.FileEntry* = IncludesScanner.FileEntry*(uint64(scan_res_r.Entries) + uint64(e_i_r) * es_r)
|
|
if e_r is None or e_r.Path is None: continue
|
|
# File-level mark: reclaim the AST right after parsing (keeping all App ASTs resident would OOM)
|
|
app_mark: sizet = mb.mark()
|
|
sbuf: bytes = Utils.read_file_to_buf(e_r.Path, SRC_BUF_SIZE_R)
|
|
if sbuf is None:
|
|
mb.reset_to(app_mark)
|
|
continue
|
|
lx: ast.Lexer* = ast.new_lexer(mb)
|
|
tree: ast.AST* = None
|
|
if lx is not None:
|
|
ast._lexer_init(lx, sbuf, mb)
|
|
tokens: ast.Token* = ast.tokenize(lx)
|
|
tree = ast.parse_tokens(mb, tokens)
|
|
if tree is not None:
|
|
tr: HandlesTranslator.Translator* = HandlesTranslator.Translator()
|
|
if tr is not None:
|
|
tr._declare_only = 2
|
|
# Source package name from RelPath (relative to source_dir, supports subdirs),
|
|
# so relative imports like `from .xxx` resolve correctly
|
|
tr.CurrentPackage = GenApi.compute_package_from_relpath(mb, e_r.RelPath)
|
|
GenApi.set_current_file_type(e_r.Path)
|
|
GenApi.set_current_file_imports(e_r.Path)
|
|
GenApi.set_current_module_sha1(e_r.Sha1)
|
|
GenApi.clear_cdefine_constants()
|
|
GenApi.clear_ctypedefs()
|
|
GenApi.reset_visible_structs(mb, 0)
|
|
ret_a: int = tr.translate(tree)
|
|
if ret_a == 0:
|
|
# Container use detection: inject _list/_dict deps when an App file uses list/dict (bare annotation/ctor)
|
|
tr._inject_auto_container_visibility(mb, tree)
|
|
# assert lowers to testcheck.check(...): inject the testcheck dep when used
|
|
tr._inject_auto_assert_import(mb, tree)
|
|
# pure lowers str/bytes to pure_str: inject the _pure dep when used
|
|
tr._inject_auto_pure_import(mb, tree)
|
|
# Register the App file in the store (empty incdir) and fill deps; read by
|
|
# LookupIncludesDeps(app_sha1) when BFS expands its includes deps
|
|
if e_r.Sha1 is not None:
|
|
StubMergerStore.AppendToSha1MapStore(e_r.Sha1, e_r.RelPath)
|
|
StubMergerStore.PopulateIncludesDeps(e_r.Sha1, tr._imported_modules)
|
|
# Collect deps into the initial queue (dedup per module)
|
|
if tr._imported_modules is not None:
|
|
im_len_a: sizet = string.strlen(tr._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._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_buf_a: bytes = stdlib.malloc(im_mod_len + 1)
|
|
if im_buf_a is None: continue
|
|
string.strncpy(im_buf_a, tr._imported_modules + im_start_a, im_mod_len)
|
|
im_buf_a[im_mod_len] = '\0'
|
|
in_wl_a: int = 0
|
|
if wl_len > 0 and GenApi.is_module_imported(worklist, im_buf_a) != 0: in_wl_a = 1
|
|
if in_wl_a == 0:
|
|
if wl_len + im_mod_len + 2 < StubMerger.REACH_WORKLIST_BUF:
|
|
if wl_len > 0:
|
|
worklist[wl_len] = ' '
|
|
wl_len += 1
|
|
string.strcpy(worklist + wl_len, im_buf_a)
|
|
wl_len += im_mod_len
|
|
worklist[wl_len] = ' '
|
|
wl_len += 1
|
|
worklist[wl_len] = '\0'
|
|
stdlib.free(im_buf_a)
|
|
# Release Translator resources
|
|
if tr._global_names is not None: stdlib.free(tr._global_names)
|
|
if tr._nonlocal_names is not None: stdlib.free(tr._nonlocal_names)
|
|
stdlib.free(sbuf)
|
|
mb.reset_to(app_mark)
|
|
else:
|
|
# app_deps already provided (Phase2): use it directly as the initial queue
|
|
ad_len: sizet = string.strlen(app_deps)
|
|
if ad_len > 0 and ad_len < StubMerger.REACH_WORKLIST_BUF:
|
|
string.strcpy(worklist, app_deps)
|
|
wl_len = ad_len
|
|
else:
|
|
VLogger.error("app_deps is too long; cannot build the reachable set", "StubMerger")
|
|
stdlib.free(worklist)
|
|
return -1
|
|
|
|
# Trim the trailing extra space
|
|
if wl_len > 0 and worklist[wl_len - 1] == ' ':
|
|
worklist[wl_len - 1] = '\0'
|
|
wl_len -= 1
|
|
|
|
# 2. Pre-add base modules (builtins): builtins is always compiled in; _list/_dict are injected only for
|
|
# explicit list/dict annotations/ctors (list literals to t.ArrayT are array init and do not trigger _list).
|
|
wl_len = _AppendDepsToWorklist(worklist, wl_len, "builtins", None)
|
|
if wl_len < 0:
|
|
stdlib.free(worklist)
|
|
return -1
|
|
|
|
# 2b. --traceback: force the traceback runtime module into the reachable set (TPC: used_includes.add('traceback')).
|
|
# Its Install() symbol and the @llvm.global_ctors ctor come only from compiling this module, so a missing file is fail-fast.
|
|
if BuildPipeline._traceback_mode != 0:
|
|
tb_abs: rawptr = stdlib.malloc(8)
|
|
tb_rel: rawptr = stdlib.malloc(8)
|
|
tb_inc: rawptr = stdlib.malloc(8)
|
|
tb_found: int = -1
|
|
if tb_abs is not None and tb_rel is not None and tb_inc is not None:
|
|
tb_abs_slot: sizet* = (sizet*)(tb_abs)
|
|
tb_rel_slot: sizet* = (sizet*)(tb_rel)
|
|
tb_inc_slot: sizet* = (sizet*)(tb_inc)
|
|
tb_abs_slot[0] = 0
|
|
tb_rel_slot[0] = 0
|
|
tb_inc_slot[0] = 0
|
|
tb_found = IncludesScanner.ResolveModuleToPath(includes_dirs, "traceback", tb_abs, tb_rel, tb_inc)
|
|
if tb_abs is not None: stdlib.free(tb_abs)
|
|
if tb_rel is not None: stdlib.free(tb_rel)
|
|
if tb_inc is not None: stdlib.free(tb_inc)
|
|
if tb_found != 0:
|
|
stdlib.free(worklist)
|
|
VLogger.error(f"--traceback: traceback.vp not found in the includes dirs; cannot compile and link Install() (make sure the includes of project.vpj contains the repository includes directory; scanned: {includes_dirs})", "Traceback")
|
|
return -1
|
|
tb_wl: int = _AppendDepsToWorklist(worklist, wl_len, "traceback", None)
|
|
if tb_wl < 0:
|
|
stdlib.free(worklist)
|
|
return -1
|
|
wl_len = sizet(tb_wl)
|
|
|
|
# 2c. --spend-file / --vpf: force the vprofile runtime into the reachable set; the
|
|
# instrumented functions call vpf_enter/vpf_leave/vpf_install, which only exist when
|
|
# includes/vprofile.vp is compiled and linked.
|
|
if GenFlags._vpf_level > 0:
|
|
vpf_abs: rawptr = stdlib.malloc(8)
|
|
vpf_rel: rawptr = stdlib.malloc(8)
|
|
vpf_inc: rawptr = stdlib.malloc(8)
|
|
vpf_found: int = -1
|
|
if vpf_abs is not None and vpf_rel is not None and vpf_inc is not None:
|
|
vpf_abs_slot: sizet* = (sizet*)(vpf_abs)
|
|
vpf_rel_slot: sizet* = (sizet*)(vpf_rel)
|
|
vpf_inc_slot: sizet* = (sizet*)(vpf_inc)
|
|
vpf_abs_slot[0] = 0
|
|
vpf_rel_slot[0] = 0
|
|
vpf_inc_slot[0] = 0
|
|
vpf_found = IncludesScanner.ResolveModuleToPath(includes_dirs, "vprofile", vpf_abs, vpf_rel, vpf_inc)
|
|
if vpf_abs is not None: stdlib.free(vpf_abs)
|
|
if vpf_rel is not None: stdlib.free(vpf_rel)
|
|
if vpf_inc is not None: stdlib.free(vpf_inc)
|
|
if vpf_found != 0:
|
|
stdlib.free(worklist)
|
|
VLogger.error(f"--spend-file: vprofile.vp not found in the includes dirs (scanned: {includes_dirs}); add the repository includes directory to project.vpj's includes", "vprofile")
|
|
return -1
|
|
vpf_wl: int = _AppendDepsToWorklist(worklist, wl_len, "vprofile", None)
|
|
if vpf_wl < 0:
|
|
stdlib.free(worklist)
|
|
return -1
|
|
wl_len = sizet(vpf_wl)
|
|
|
|
# 3. Worklist BFS: module name -> filesystem resolution -> read+hash+parse imports
|
|
processed: bytes = stdlib.malloc(StubMerger.REACH_WORKLIST_BUF)
|
|
if processed is None:
|
|
stdlib.free(worklist)
|
|
return -1
|
|
processed[0] = '\0'
|
|
proc_len: sizet = 0
|
|
reachable_count: int = 0
|
|
wl_pos: rawptr = stdlib.malloc(8)
|
|
if wl_pos is None:
|
|
stdlib.free(worklist)
|
|
stdlib.free(processed)
|
|
return -1
|
|
wl_pos_slot: sizet* = (sizet*)(wl_pos)
|
|
wl_pos_slot[0] = 0
|
|
|
|
while wl_pos_slot[0] < wl_len:
|
|
pos: sizet = wl_pos_slot[0]
|
|
# Extract one module name (up to a space or the end)
|
|
name_start: sizet = pos
|
|
while pos < wl_len and worklist[pos] != ' ': pos += 1
|
|
name_len: sizet = pos - name_start
|
|
if pos < wl_len: pos += 1
|
|
wl_pos_slot[0] = pos
|
|
if name_len == 0: continue
|
|
|
|
# Copy the module name
|
|
mod_buf: bytes = stdlib.malloc(name_len + 1)
|
|
if mod_buf is None: continue
|
|
string.strncpy(mod_buf, worklist + name_start, name_len)
|
|
mod_buf[name_len] = '\0'
|
|
|
|
# Dedup against already processed
|
|
if GenApi.is_module_imported(processed, mod_buf) != 0:
|
|
stdlib.free(mod_buf)
|
|
continue
|
|
# Add to processed
|
|
if proc_len + name_len + 1 < StubMerger.REACH_WORKLIST_BUF:
|
|
string.strcpy(processed + proc_len, mod_buf)
|
|
proc_len = proc_len + name_len
|
|
processed[proc_len] = ' '
|
|
proc_len = proc_len + 1
|
|
processed[proc_len] = '\0'
|
|
else:
|
|
VLogger.error("dependency processed overflow", "StubMerger")
|
|
stdlib.free(mod_buf)
|
|
continue
|
|
|
|
# App intermediate node: only expand its includes deps, do not add to reachable_set
|
|
# (Phase B already compiled its .obj; adding it would recompile and duplicate symbols)
|
|
if _IsAppModule(mod_buf) != 0:
|
|
app_sha1: str = StubMergerStore.LookupSha1ByModName(mod_buf)
|
|
if app_sha1 is not None:
|
|
app_deps_str: str = StubMergerStore.LookupIncludesDeps(app_sha1)
|
|
if app_deps_str is not None:
|
|
ret_ap: int = _AppendDepsToWorklist(worklist, wl_len, app_deps_str, processed)
|
|
if ret_ap < 0:
|
|
stdlib.free(mod_buf)
|
|
stdlib.free(worklist)
|
|
stdlib.free(processed)
|
|
stdlib.free(wl_pos)
|
|
return -1
|
|
wl_len = sizet(ret_ap)
|
|
stdlib.free(mod_buf)
|
|
continue
|
|
|
|
# includes: resolve on demand via the filesystem (unreferenced includes are never touched)
|
|
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)
|
|
stdlib.free(mod_buf)
|
|
continue
|
|
# Defensive init of the output slots (the success path always writes all). The slot union must use (sizet*):
|
|
# rawptr* collapses to rawptr and [0] becomes a 1-byte access, truncating the pointer (0x2e crash root cause).
|
|
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(includes_dirs, mod_buf, out_abs, out_rel, out_inc)
|
|
if rcode != 0:
|
|
# Builtin module (t/c/builtins internal libs) or not found: silently skip
|
|
stdlib.free(out_abs)
|
|
stdlib.free(out_rel)
|
|
stdlib.free(out_inc)
|
|
stdlib.free(mod_buf)
|
|
continue
|
|
abs_path: str = rawptr(abs_slot[0])
|
|
rel_path: str = rawptr(rel_slot[0])
|
|
inc_dir: str = rawptr(inc_slot[0])
|
|
|
|
# Read the file and compute SHA1 (CRLF->LF, matching IncludesScanner)
|
|
sha1_r2: str = IncludesScanner.compute_file_sha1(mb, abs_path)
|
|
if sha1_r2 is None:
|
|
stdlib.free(abs_path)
|
|
stdlib.free(rel_path)
|
|
stdlib.free(inc_dir)
|
|
stdlib.free(out_abs)
|
|
stdlib.free(out_rel)
|
|
stdlib.free(out_inc)
|
|
stdlib.free(mod_buf)
|
|
continue
|
|
|
|
# Already in the reachable set (parsed by an earlier BFS): only expand its deps
|
|
if StubMergerCollect._is_in_sha1_set(sha1_r2, reachable_set, reachable_count) != 0:
|
|
deps_ex: str = StubMergerStore.LookupIncludesDeps(sha1_r2)
|
|
if deps_ex is not None:
|
|
ret_ex: int = _AppendDepsToWorklist(worklist, wl_len, deps_ex, processed)
|
|
if ret_ex < 0:
|
|
stdlib.free(abs_path)
|
|
stdlib.free(rel_path)
|
|
stdlib.free(inc_dir)
|
|
stdlib.free(out_abs)
|
|
stdlib.free(out_rel)
|
|
stdlib.free(out_inc)
|
|
stdlib.free(mod_buf)
|
|
stdlib.free(worklist)
|
|
stdlib.free(processed)
|
|
stdlib.free(wl_pos)
|
|
return -1
|
|
wl_len = sizet(ret_ex)
|
|
stdlib.free(abs_path)
|
|
stdlib.free(rel_path)
|
|
stdlib.free(inc_dir)
|
|
stdlib.free(out_abs)
|
|
stdlib.free(out_rel)
|
|
stdlib.free(out_inc)
|
|
stdlib.free(mod_buf)
|
|
continue
|
|
|
|
# Add to the reachable set and register in Sha1Store (reachable includes only)
|
|
reachable_count = StubMergerIR._AddSha1ToSet(reachable_set, reachable_count, sha1_r2)
|
|
StubMergerStore.AddIncludeToSha1MapStore(sha1_r2, rel_path, inc_dir, mod_buf)
|
|
|
|
# File-level mark: reclaim the AST right after parsing imports (keeping all reachable includes
|
|
# ASTs resident would OOM the main pool)
|
|
scan_mark_r: sizet = mb.mark()
|
|
sbuf_r: bytes = Utils.read_file_to_buf(abs_path, SRC_BUF_SIZE_R)
|
|
if sbuf_r is None:
|
|
stdlib.free(abs_path)
|
|
stdlib.free(rel_path)
|
|
stdlib.free(inc_dir)
|
|
stdlib.free(out_abs)
|
|
stdlib.free(out_rel)
|
|
stdlib.free(out_inc)
|
|
stdlib.free(mod_buf)
|
|
mb.reset_to(scan_mark_r)
|
|
continue
|
|
lx_r: ast.Lexer* = ast.new_lexer(mb)
|
|
tree_r: ast.AST* = None
|
|
if lx_r is not None:
|
|
ast._lexer_init(lx_r, sbuf_r, mb)
|
|
tokens_r: ast.Token* = ast.tokenize(lx_r)
|
|
tree_r = ast.parse_tokens(mb, tokens_r)
|
|
if tree_r is None:
|
|
# parse_tokens keeps the error detail (line/col/message) only in the global channel, so print the unified
|
|
# parse traceback before the one-line log; without it a reachable-includes syntax error is undiagnosable.
|
|
GenApi.dump_parse_traceback(abs_path)
|
|
VLogger.error(f"reachable includes parse failed: {abs_path}", "Reachable")
|
|
stdlib.free(abs_path)
|
|
stdlib.free(rel_path)
|
|
stdlib.free(inc_dir)
|
|
stdlib.free(out_abs)
|
|
stdlib.free(out_rel)
|
|
stdlib.free(out_inc)
|
|
stdlib.free(sbuf_r)
|
|
stdlib.free(mod_buf)
|
|
stdlib.free(worklist)
|
|
stdlib.free(processed)
|
|
stdlib.free(wl_pos)
|
|
mb.reset_to(scan_mark_r)
|
|
return -1
|
|
tr_r2: HandlesTranslator.Translator* = HandlesTranslator.Translator()
|
|
if tr_r2 is None:
|
|
stdlib.free(abs_path)
|
|
stdlib.free(rel_path)
|
|
stdlib.free(inc_dir)
|
|
stdlib.free(out_abs)
|
|
stdlib.free(out_rel)
|
|
stdlib.free(out_inc)
|
|
stdlib.free(sbuf_r)
|
|
stdlib.free(mod_buf)
|
|
mb.reset_to(scan_mark_r)
|
|
continue
|
|
tr_r2.ModuleSha1 = sha1_r2
|
|
tr_r2._declare_only = 2
|
|
# Package name: RelPath relative to the includes root (supports relative imports like `from .xxx`)
|
|
tr_r2.CurrentPackage = GenApi.compute_package_from_relpath(mb, rel_path)
|
|
GenApi.set_current_file_type(abs_path)
|
|
GenApi.set_current_file_imports(abs_path)
|
|
GenApi.set_current_module_sha1(sha1_r2)
|
|
GenApi.clear_cdefine_constants()
|
|
GenApi.clear_ctypedefs()
|
|
GenApi.reset_visible_structs(mb, 0)
|
|
ret_r2: int = tr_r2.translate(tree_r)
|
|
if ret_r2 != 0:
|
|
VLogger.error(f"import dependency scan failed (reachable includes): {abs_path}", "Reachable")
|
|
stdlib.free(abs_path)
|
|
stdlib.free(rel_path)
|
|
stdlib.free(inc_dir)
|
|
stdlib.free(out_abs)
|
|
stdlib.free(out_rel)
|
|
stdlib.free(out_inc)
|
|
stdlib.free(sbuf_r)
|
|
if tr_r2._global_names is not None: stdlib.free(tr_r2._global_names)
|
|
if tr_r2._nonlocal_names is not None: stdlib.free(tr_r2._nonlocal_names)
|
|
stdlib.free(mod_buf)
|
|
stdlib.free(worklist)
|
|
stdlib.free(processed)
|
|
stdlib.free(wl_pos)
|
|
mb.reset_to(scan_mark_r)
|
|
return -1
|
|
# Inject containers on demand (append _list/_dict when includes use list/dict generics);
|
|
# inlined to break the _list/_dict cycle
|
|
tr_r2._inject_auto_container_visibility(mb, tree_r)
|
|
# assert lowers to testcheck.check(...): inject the testcheck dep when used
|
|
tr_r2._inject_auto_assert_import(mb, tree_r)
|
|
# pure lowers str/bytes to pure_str: inject the _pure dep when used
|
|
tr_r2._inject_auto_pure_import(mb, tree_r)
|
|
|
|
# Fill in-memory deps and write deps.txt (for dep-graph expansion and on-demand Phase B loading)
|
|
deps_r2: str = tr_r2._imported_modules
|
|
StubMergerStore.PopulateIncludesDeps(sha1_r2, deps_r2)
|
|
if deps_r2 is not None:
|
|
td_len_w: sizet = string.strlen(temp_dir)
|
|
deps_path_w: str = StubMergerCollect._sliced_path(temp_dir, td_len_w, sha1_r2, "deps.txt")
|
|
if deps_path_w is not None:
|
|
df_w: io.File* = io.File(deps_path_w, io.MODE.W)
|
|
if not df_w.closed:
|
|
dl_w: sizet = string.strlen(deps_r2)
|
|
if dl_w > 0: df_w.write(deps_r2, dl_w)
|
|
df_w.close()
|
|
stdlib.free(deps_path_w)
|
|
# Append deps to the worklist (dedup to prevent overflow)
|
|
ret_w: int = _AppendDepsToWorklist(worklist, wl_len, deps_r2, processed)
|
|
if ret_w < 0:
|
|
stdlib.free(abs_path)
|
|
stdlib.free(rel_path)
|
|
stdlib.free(inc_dir)
|
|
stdlib.free(out_abs)
|
|
stdlib.free(out_rel)
|
|
stdlib.free(out_inc)
|
|
stdlib.free(sbuf_r)
|
|
stdlib.free(mod_buf)
|
|
stdlib.free(worklist)
|
|
stdlib.free(processed)
|
|
stdlib.free(wl_pos)
|
|
mb.reset_to(scan_mark_r)
|
|
return -1
|
|
wl_len = sizet(ret_w)
|
|
else: VLogger.debug(f"no dependencies: {rel_path}", "Reachable")
|
|
|
|
# Free the Translator's C malloc resources and reclaim this file's AST
|
|
if tr_r2._global_names is not None: stdlib.free(tr_r2._global_names)
|
|
if tr_r2._nonlocal_names is not None: stdlib.free(tr_r2._nonlocal_names)
|
|
stdlib.free(sbuf_r)
|
|
mb.reset_to(scan_mark_r)
|
|
|
|
stdlib.free(abs_path)
|
|
stdlib.free(rel_path)
|
|
stdlib.free(inc_dir)
|
|
stdlib.free(out_abs)
|
|
stdlib.free(out_rel)
|
|
stdlib.free(out_inc)
|
|
stdlib.free(mod_buf)
|
|
|
|
stdlib.free(worklist)
|
|
stdlib.free(processed)
|
|
stdlib.free(wl_pos)
|
|
return reachable_count
|
|
|
|
# LoadFullIR - read {temp_dir}/{sha1}.full.ll into out_buf (④-c: merge phase uses the online FULL artifact).
|
|
# Contract: returns bytes written (sizet; 0=failure); out_buf is '\0'-terminated. full.ll is a self-contained single-module IR.
|
|
def LoadFullIR(temp_dir: str, sha1: str, out_buf: bytes, out_size: sizet) -> sizet:
|
|
"""Read {sha1}.full.ll into out_buf; returns the length (0 on failure)."""
|
|
if temp_dir is None or sha1 is None or out_buf is None or out_size == 0: return 0
|
|
td_len: sizet = string.strlen(temp_dir)
|
|
p: str = StubMergerCollect._sliced_path(temp_dir, td_len, sha1, "full.ll")
|
|
if p is None: return 0
|
|
f: io.File* = io.File(p, io.MODE.R)
|
|
stdlib.free(p)
|
|
out_buf[0] = '\0'
|
|
if f.closed: return 0
|
|
if f.size() >= out_size:
|
|
f.close()
|
|
VLogger.error("full.ll exceeds the IR buffer limit", "StubMerger")
|
|
return 0
|
|
rd: int64 = f.read_all(out_buf, out_size)
|
|
f.close()
|
|
if rd <= 0: return 0
|
|
out_buf[rd] = '\0'
|
|
return sizet(rd)
|
|
|
|
# LoadDepVpiIntoModule - load dependency modules' .vpi files and inject their declares into the current module.
|
|
# Call after Import handling and before translate_children; returns the number of successfully loaded .vpi files.
|
|
def LoadDepVpiIntoModule(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, imported_modules: str, from_imports: str, temp_dir: str, trans: HandlesTranslator.Translator*) -> int:
|
|
"""Load dependency modules' .vpi files and inject their declares into the current module."""
|
|
if pool is None or mod is None or temp_dir is None or trans is None: return 0
|
|
if imported_modules is None: return 0
|
|
|
|
# Reset the typedef recursion guard before injecting each module's stub: the TypedefT table is
|
|
# per-file, so stale visited state registers stdint typedefs only once and degrades param types (FuncTest root cause).
|
|
VpiReader.ResetVpiTypedefVisited()
|
|
# Reset the on-demand injection reentry table too: the target mod differs per module, so carrying it
|
|
# over would cache-hit and miss the declares this module needs.
|
|
VpiReader.ResetVpiInjectVisited()
|
|
|
|
td_len: sizet = string.strlen(temp_dir)
|
|
total_len: sizet = string.strlen(imported_modules)
|
|
success_count: int = 0
|
|
|
|
# Iterate each module name in imported_modules (space-separated)
|
|
ci: sizet = 0
|
|
while ci < total_len:
|
|
# Skip leading spaces
|
|
while ci < total_len and imported_modules[ci] == ' ': ci += 1
|
|
if ci >= total_len: break
|
|
# Extract the module name (up to the next space or end of string)
|
|
word_start: sizet = ci
|
|
while ci < total_len and imported_modules[ci] != ' ': ci += 1
|
|
word_len: sizet = ci - word_start
|
|
if word_len == 0: continue
|
|
|
|
# Copy the module name into a temp buffer and null-terminate it
|
|
mod_nm_buf: str = stdlib.malloc(word_len + 1)
|
|
if mod_nm_buf is None: continue
|
|
string.strncpy(mod_nm_buf, imported_modules + word_start, word_len)
|
|
mod_nm_buf[word_len] = '\0'
|
|
|
|
# Look up the module's SHA1 via LookupSha1ByModName
|
|
sha1_val: str = StubMergerStore.LookupSha1ByModName(mod_nm_buf)
|
|
|
|
# Import alias (import X as Y, e.g. HT->lib.core.Handles.HandlesTranslator): when exact/package
|
|
# lookups fail, resolve the alias via from_imports; otherwise its vpi/stub is not injected and cross-module symbols fail.
|
|
if sha1_val is None and from_imports is not None: sha1_val = StubMergerStore.LookupSha1ByAlias(from_imports, mod_nm_buf)
|
|
|
|
# Exact lookup failed: package fallback (e.g. "ast" -> "ast.__init__") is already handled inside LookupSha1ByModName
|
|
|
|
if sha1_val is not None:
|
|
# Build the .vpi file path
|
|
vpi_path: str = StubMergerCollect._sliced_path(temp_dir, td_len, sha1_val, "vpi")
|
|
if vpi_path is not None:
|
|
# Check that the .vpi file exists (is readable)
|
|
vf: io.File* = io.File(vpi_path, io.MODE.R)
|
|
if not vf.closed:
|
|
vf.close()
|
|
# Parse the .vpi via VpiReader and inject declares (the error context frame is kept by the caller HandlesTranslator)
|
|
ret: int = VpiReader.ParseVpiIntoModule(pool, mod, vpi_path, trans)
|
|
if ret == 1: success_count += 1
|
|
stdlib.free(vpi_path)
|
|
# sha1_val points into internal storage, no free needed
|
|
|
|
stdlib.free(mod_nm_buf)
|
|
|
|
return success_count
|
|
|
|
|
|
# ============================================================
|
|
# Include object-cache stamp
|
|
# ============================================================
|
|
# A cached include .obj is keyed by the module's own source sha1 only, while the object body
|
|
# embeds the sha1-prefixed symbols of every module it calls. When a dependency (or the
|
|
# compiler itself) changes, reusing such an object links stale code, or fails with an
|
|
# undefined symbol for a module sha1 that no longer exists. The cache therefore carries a
|
|
# stamp holding the compiler build id plus the sorted include sha1 set of this build; a
|
|
# mismatch discards the cached objects so they are recompiled against the current inputs.
|
|
define OBJ_CACHE_STAMP_NAME = "/.vpc_objcache"
|
|
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."""
|
|
if out is None or cap < 32: return 0
|
|
out[0] = '\0'
|
|
EXE_CAP: sizet = sizet(1024)
|
|
exe_path: bytes = stdlib.malloc(EXE_CAP)
|
|
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
|
|
|
|
|
|
def _BuildObjCacheStamp(out: bytes, cap: sizet, shas: bytes, incdirs: bytes, count: int) -> sizet:
|
|
"""Serialize '<buildtag>\\n<sha1>\\n...' (sha1s sorted, App entries skipped) into out."""
|
|
if out is None or cap == 0 or shas is None or count <= 0: return 0
|
|
out[0] = '\0'
|
|
pos: sizet = 0
|
|
tag: bytes = stdlib.malloc(sizet(OBJ_CACHE_STAMP_TAG_CAP))
|
|
if tag is not None:
|
|
string.memset(tag, 0, sizet(OBJ_CACHE_STAMP_TAG_CAP))
|
|
if _CompilerBuildTag(tag, sizet(OBJ_CACHE_STAMP_TAG_CAP)) != 0:
|
|
tl: sizet = string.strlen(tag)
|
|
if pos + tl + 2 > cap:
|
|
stdlib.free(tag)
|
|
return 0
|
|
string.strcpy(out + pos, tag)
|
|
pos += tl
|
|
stdlib.free(tag)
|
|
if pos + 2 > cap: return 0
|
|
out[pos] = '\n'
|
|
pos += 1
|
|
# Sorted insertion keeps the stamp independent of translation order.
|
|
rec_size: sizet = sizet(count) * 17
|
|
rec: bytes = stdlib.malloc(rec_size)
|
|
if rec is None: return 0
|
|
string.memset(rec, 0, rec_size)
|
|
n_rec: int = 0
|
|
for i in range(count):
|
|
# App sources carry an empty include dir and are never cached as include objects;
|
|
# leaving them out keeps an App-only edit from invalidating the include cache.
|
|
if incdirs is not None:
|
|
if incdirs[sizet(i) * sizet(StubMerger.MAX_INC_DIR_LEN)] == '\0': continue
|
|
src: str = shas + sizet(i) * 17
|
|
if src[0] == '\0': continue
|
|
at: int = n_rec
|
|
while at > 0 and string.strcmp(rec + sizet(at - 1) * 17, src) > 0:
|
|
string.strcpy(rec + sizet(at) * 17, rec + sizet(at - 1) * 17)
|
|
at -= 1
|
|
string.strcpy(rec + sizet(at) * 17, src)
|
|
n_rec += 1
|
|
for i in range(n_rec):
|
|
s: str = rec + sizet(i) * 17
|
|
sl: sizet = string.strlen(s)
|
|
if pos + sl + 2 > cap: break
|
|
string.strcpy(out + pos, s)
|
|
pos += sl
|
|
out[pos] = '\n'
|
|
pos += 1
|
|
stdlib.free(rec)
|
|
out[pos] = '\0'
|
|
return pos
|
|
|
|
|
|
def _ObjCacheStampPath(cache_dir: str) -> str:
|
|
"""Full path of the object-cache stamp inside cache_dir, or None."""
|
|
if cache_dir is None: return None
|
|
dl: sizet = string.strlen(cache_dir)
|
|
sl: sizet = string.strlen(OBJ_CACHE_STAMP_NAME)
|
|
buf: bytes = stdlib.malloc(dl + sl + 1)
|
|
if buf is None: return None
|
|
string.memset(buf, 0, dl + sl + 1)
|
|
viperlib.snprintf(buf, dl + sl + 1, "%s%s", cache_dir, OBJ_CACHE_STAMP_NAME)
|
|
return str(buf)
|
|
|
|
|
|
def EnsureObjCacheStamp(cache_dir: str, shas: bytes, incdirs: bytes, count: int) -> int:
|
|
"""Validate the include .obj cache against this build; wipe it when the stamp differs.
|
|
|
|
Returns 1 when the cache is usable (already matching, or just re-stamped) and 0 when the
|
|
stamp could not be written. A mismatch is not an error: the objects are deleted and the
|
|
build recompiles them from the current sources with the current compiler.
|
|
"""
|
|
if cache_dir is None or shas is None or count <= 0: return 1
|
|
stamp_path: str = _ObjCacheStampPath(cache_dir)
|
|
if stamp_path is None: return 0
|
|
cap: sizet = sizet(count) * 17 + sizet(OBJ_CACHE_STAMP_TAG_CAP) + 8
|
|
want: bytes = stdlib.malloc(cap)
|
|
got: bytes = stdlib.malloc(cap)
|
|
if want is None or got is None:
|
|
if want is not None: stdlib.free(want)
|
|
if got is not None: stdlib.free(got)
|
|
stdlib.free(stamp_path)
|
|
return 0
|
|
string.memset(want, 0, cap)
|
|
string.memset(got, 0, cap)
|
|
want_len: sizet = _BuildObjCacheStamp(want, cap - 1, shas, incdirs, count)
|
|
got_len: sizet = 0
|
|
f: io.File* = io.File(stamp_path, io.MODE.R)
|
|
if not f.closed:
|
|
rn: int = f.read_all(got, cap - 1)
|
|
if rn > 0: got_len = sizet(rn)
|
|
f.close()
|
|
got[got_len] = '\0'
|
|
if want_len > 0 and got_len == want_len and string.strcmp(got, want) == 0:
|
|
stdlib.free(want)
|
|
stdlib.free(got)
|
|
stdlib.free(stamp_path)
|
|
return 1
|
|
# Stamp missing or different: drop every cached object (they embed dependency sha1s).
|
|
n_wipe: int = Utils.CleanDir(cache_dir)
|
|
if n_wipe >= 0:
|
|
VLogger.info(f"include object cache invalidated ({n_wipe} files deleted): include set or compiler build changed", "PhaseB+")
|
|
BuildPipeline.ensure_dir(cache_dir)
|
|
ok: int = 0
|
|
wf: io.File* = io.File(stamp_path, io.MODE.W)
|
|
if not wf.closed:
|
|
if wf.write(want, want_len) >= 0: ok = 1
|
|
wf.close()
|
|
stdlib.free(want)
|
|
stdlib.free(got)
|
|
stdlib.free(stamp_path)
|
|
return ok
|