feat(vpc): vprofile instrumentation (--spend-file/--vpf) + verbatim --build-self forwarding
--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.
This commit is contained in:
1 parent
c9d33bc202
commit
91e188c539
9 files changed
+299
-25
No files matched your search
@@ -183,6 +183,20 @@ def _file_exists(path: str) -> int:
|
||||
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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import lib.core.Gen.GenApi as GenApi
|
||||
import lib.core.Gen.GenFlags as GenFlags
|
||||
from stdint import *
|
||||
import memhub
|
||||
import string
|
||||
@@ -223,6 +224,35 @@ def _BuildReachableSha1Set(mb: memhub.MemBuddy*, source_dir: str, temp_dir: str,
|
||||
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:
|
||||
|
||||
@@ -9,6 +9,7 @@ import lib.core.VLogger as VLogger
|
||||
import lib.core.Gen.Translator as HT
|
||||
import lib.core.Gen.GenApi as GenApi
|
||||
import lib.core.Gen.GenTypes as GenTypes
|
||||
import lib.core.Gen.GenFlags as GenFlags
|
||||
import lib.core.Gen.Call.CallGenBase as CallGenBase
|
||||
|
||||
# Symbol-table scope kind of a function body (same value as FuncGenMixin.SCOPE_FUNCTION;
|
||||
@@ -511,6 +512,8 @@ novirtual class FuncGenBase(CallGenBase.CallGenBase):
|
||||
if last_stmt is not None and last_stmt.kind() == ast.ASTKind.Return: last_is_return = 1
|
||||
if last_is_return != 0: return 0
|
||||
if mlirlite.builder_cur_block_is_terminated(builder) != 0: return 0
|
||||
# --spend-file / --vpf: pair the entry hook on the implicit-return path.
|
||||
self._vpf_emit_leave(trans, builder)
|
||||
|
||||
last_is_ctrl: int = 0
|
||||
if bn3 > 0:
|
||||
@@ -563,6 +566,113 @@ novirtual class FuncGenBase(CallGenBase.CallGenBase):
|
||||
self.Gen.TypeGenErr.fatal_error(fd, f"{ctx_label} flow falls off the end without an explicit return", "TypeError")
|
||||
return 0
|
||||
|
||||
# ============================================================================
|
||||
# --spend-file / --vpf instrumentation (vprofile runtime)
|
||||
#
|
||||
# Every instrumented function gets vpf_enter(id, name) at entry and vpf_leave(id) at
|
||||
# every exit; main additionally calls vpf_install(path, level) once. The runtime
|
||||
# (includes/vprofile.vp) keeps the tables and writes the report from atexit.
|
||||
# ============================================================================
|
||||
|
||||
# _vpf_skip_file - the profiler runtime must never instrument itself (infinite recursion).
|
||||
def _vpf_skip_file(self) -> int:
|
||||
cur: str = self.Gen.TypeGenResolve.get_current_file()
|
||||
if cur is None: return 0
|
||||
n: sizet = len(cur)
|
||||
if n >= 11:
|
||||
if string.strcmp(cur + (n - 11), "vprofile.vp") == 0: return 1
|
||||
if n >= 12:
|
||||
if string.strcmp(cur + (n - 12), "traceback.vp") == 0: return 1
|
||||
return 0
|
||||
|
||||
# _vpf_hash - FNV-1a over the display name, forced odd: 0 marks an empty table slot.
|
||||
def _vpf_hash(self, s: str) -> int:
|
||||
h: uint32 = uint32(2166136261)
|
||||
n: sizet = len(s)
|
||||
i: sizet = 0
|
||||
while i < n:
|
||||
h = (h ^ uint32(uint8(s[i]))) * uint32(16777619)
|
||||
i += 1
|
||||
return int(h | uint32(1))
|
||||
|
||||
# _vpf_display_name - mangled symbol without the "{16 hex}." module prefix.
|
||||
def _vpf_display_name(self, mangled: str) -> str:
|
||||
if mangled is None: return None
|
||||
if GenApi.sha1_prefix_len(mangled) != 0: return mangled + sizet(17)
|
||||
return mangled
|
||||
|
||||
# _vpf_ensure_decl - declare a vprofile entry point in this module when it is absent.
|
||||
# The instrumented module usually does not `import vprofile` (the runtime is force-added by
|
||||
# the build), so no stub declaration reaches it; without this, LLVM rejects the call as an
|
||||
# undeclared symbol. `t2` is None for the one-argument vpf_leave.
|
||||
def _vpf_ensure_decl(self, trans: HT.Translator*, name: str, t1: mlirlite.LLVMType*, t2: mlirlite.LLVMType*) -> int:
|
||||
if self.Gen.CallGenBase.find_func_in_module(trans.Module, name, None) is not None: return 1
|
||||
pool: memhub.MemBuddy* = trans.Pool
|
||||
f: mlirlite.Function* = mlirlite.create_declare(pool, trans.Module, name, mlirlite.Void(pool))
|
||||
if f is None: return 0
|
||||
mlirlite.add_param(pool, f, t1, None)
|
||||
if t2 is not None: mlirlite.add_param(pool, f, t2, None)
|
||||
return 1
|
||||
|
||||
# _vpf_emit_call1 / _vpf_emit_call2 - emit a call to a vprofile runtime entry point.
|
||||
def _vpf_emit_call1(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, callee: str, a: mlirlite.Value*) -> int:
|
||||
if a is None: return 0
|
||||
if self._vpf_ensure_decl(trans, callee, mlirlite.Int32(trans.Pool), None) == 0: return 0
|
||||
mlirlite.value_set_next(a, None)
|
||||
mlirlite.build_call(builder, callee, a, 1, mlirlite.Void(trans.Pool), 0)
|
||||
return 1
|
||||
|
||||
def _vpf_emit_call2(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, callee: str, t1: mlirlite.LLVMType*, t2: mlirlite.LLVMType*, a: mlirlite.Value*, b: mlirlite.Value*) -> int:
|
||||
if a is None or b is None: return 0
|
||||
if self._vpf_ensure_decl(trans, callee, t1, t2) == 0: return 0
|
||||
mlirlite.value_set_next(a, None)
|
||||
mlirlite.value_set_next(b, None)
|
||||
mlirlite.value_set_next(a, b)
|
||||
mlirlite.build_call(builder, callee, a, 2, mlirlite.Void(trans.Pool), 0)
|
||||
return 1
|
||||
|
||||
# _vpf_const_i32 - i32 constant whose printed text is the decimal value (ConstInt's
|
||||
# `name` argument is the constant text, not a symbol name).
|
||||
def _vpf_const_i32(self, pool: memhub.MemBuddy*, val: int) -> mlirlite.Value*:
|
||||
buf: str = pool.alloc(32)
|
||||
if buf is None: return None
|
||||
viperlib.snprintf(buf, 32, "%d", val)
|
||||
return mlirlite.ConstInt(pool, mlirlite.Int32(pool), int64(val), buf)
|
||||
|
||||
# _vpf_emit_install - vpf_install("<absolute report path>", level) for main's entry.
|
||||
def _vpf_emit_install(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*) -> int:
|
||||
if GenFlags._vpf_level <= 0 or GenFlags._vpf_path is None: return 0
|
||||
pool: memhub.MemBuddy* = trans.Pool
|
||||
path_v: mlirlite.Value* = self.Gen.ExprGenConst.create_global_string(builder, pool, trans.Module, GenFlags._vpf_path, trans)
|
||||
lvl_v: mlirlite.Value* = self._vpf_const_i32(pool, GenFlags._vpf_level)
|
||||
return self._vpf_emit_call2(trans, builder, "vpf_install", mlirlite.Ptr(pool, mlirlite.Int8(pool)), mlirlite.Int32(pool), path_v, lvl_v)
|
||||
|
||||
# _vpf_emit_enter - vpf_enter(id, "name"); records the function so exits emit vpf_leave.
|
||||
def _vpf_emit_enter(self, trans: HT.Translator*, fd: ast.FunctionDef*, func: mlirlite.Function*, builder: mlirlite.IRBuilder*) -> int:
|
||||
if GenFlags._vpf_level <= 0: return 0
|
||||
if self._vpf_skip_file() != 0: return 0
|
||||
mangled: str = mlirlite.function_get_name(func)
|
||||
if mangled is None: return 0
|
||||
pool: memhub.MemBuddy* = trans.Pool
|
||||
disp: str = self._vpf_display_name(mangled)
|
||||
vpf_id: int = self._vpf_hash(disp)
|
||||
id_v: mlirlite.Value* = self._vpf_const_i32(pool, vpf_id)
|
||||
name_v: mlirlite.Value* = self.Gen.ExprGenConst.create_global_string(builder, pool, trans.Module, disp, trans)
|
||||
if self._vpf_emit_call2(trans, builder, "vpf_enter", mlirlite.Int32(pool), mlirlite.Ptr(pool, mlirlite.Int8(pool)), id_v, name_v) == 0: return 0
|
||||
trans._vpf_func = func
|
||||
trans._vpf_id = vpf_id
|
||||
return 1
|
||||
|
||||
# _vpf_emit_leave - vpf_leave(id) before an exit of the instrumented function only
|
||||
# (nested/lambda bodies translate with a different _cur_func).
|
||||
def _vpf_emit_leave(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*) -> int:
|
||||
if GenFlags._vpf_level <= 0: return 0
|
||||
if trans._vpf_func is None: return 0
|
||||
if uint64(trans._vpf_func) != uint64(trans._cur_func): return 0
|
||||
pool: memhub.MemBuddy* = trans.Pool
|
||||
id_v: mlirlite.Value* = self._vpf_const_i32(pool, trans._vpf_id)
|
||||
return self._vpf_emit_call1(trans, builder, "vpf_leave", id_v)
|
||||
|
||||
# translate_func_body - the shared body pipeline: scope entry, parameter storage,
|
||||
# local pre-scan, capture preparation, statement translation, implicit return, restore.
|
||||
#
|
||||
@@ -604,6 +714,16 @@ novirtual class FuncGenBase(CallGenBase.CallGenBase):
|
||||
# Stack slots for self/cls and every declared parameter.
|
||||
self.emit_param_allocas(trans, fd, builder, ctx_label, skip_self, self_ty, self_name, self_class_name, pure_mode)
|
||||
|
||||
# --spend-file / --vpf: main installs the profiler once, then this body is
|
||||
# instrumented (enter here, leave before every exit).
|
||||
old_vpf_func: mlirlite.Function* = trans._vpf_func
|
||||
old_vpf_id: int = trans._vpf_id
|
||||
trans._vpf_func = None
|
||||
trans._vpf_id = 0
|
||||
if fd.name is not None and string.strcmp(fd.name, "main") == 0:
|
||||
self._vpf_emit_install(trans, builder)
|
||||
self._vpf_emit_enter(trans, fd, func, builder)
|
||||
|
||||
# try/except: this function's own EH output params (added to the signature by the caller).
|
||||
trans._eh_msg_out_param = None
|
||||
trans._eh_code_out_param = None
|
||||
@@ -672,6 +792,9 @@ novirtual class FuncGenBase(CallGenBase.CallGenBase):
|
||||
# Implicit return (one implementation for every caller).
|
||||
self.emit_func_epilogue(trans, fd, builder, ret_ty, ctx_label, unannotated_void)
|
||||
|
||||
trans._vpf_func = old_vpf_func
|
||||
trans._vpf_id = old_vpf_id
|
||||
|
||||
# Leave the function scope and hand back the enclosing state.
|
||||
self.Gen.VarGen.exit_scope(trans.SymTab)
|
||||
self.Gen.VarGen._SlifeRestore(old_slife_base)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# GenFlags - process-wide codegen options shared by the CLI and the generators.
|
||||
#
|
||||
# Lives in its own tiny module so both main.vp and the Gen mixins can read it without an
|
||||
# import cycle (it imports nothing).
|
||||
#
|
||||
# vpf_level / vpf_path implement --spend-file + --vpf:
|
||||
# 0 = off, 1 = call counts, 2 = + per-function time, 3 = + caller/callee edges
|
||||
# vpf_path is the absolute report path; the compiled program writes it at exit.
|
||||
_vpf_level: int = 0
|
||||
_vpf_path: str = None
|
||||
@@ -552,16 +552,19 @@ novirtual class StmtGenCifMixin:
|
||||
if is_void_ret != 0:
|
||||
# M3 owned slife(§4.2):每个 return 都是函数出口,先释放本函数登记的 slife
|
||||
self.Gen.VarGen._SlifeEmitFrees(pool, builder, mod, trans)
|
||||
self.Gen.FuncGenBase._vpf_emit_leave(trans, builder)
|
||||
mlirlite.build_ret_void(builder)
|
||||
return 0
|
||||
if is_ptr_ret != 0:
|
||||
null_val: mlirlite.Value* = mlirlite.ConstNull(pool, ret_ty, "null")
|
||||
self.Gen.VarGen._SlifeEmitFrees(pool, builder, mod, trans)
|
||||
self.Gen.FuncGenBase._vpf_emit_leave(trans, builder)
|
||||
mlirlite.build_ret(builder, null_val)
|
||||
return 0
|
||||
if is_float_ret != 0:
|
||||
zero_f: mlirlite.Value* = mlirlite.ConstFloat(pool, ret_ty, 0.0)
|
||||
self.Gen.VarGen._SlifeEmitFrees(pool, builder, mod, trans)
|
||||
self.Gen.FuncGenBase._vpf_emit_leave(trans, builder)
|
||||
mlirlite.build_ret(builder, zero_f)
|
||||
return 0
|
||||
if ret_ty is None:
|
||||
@@ -607,6 +610,7 @@ novirtual class StmtGenCifMixin:
|
||||
if cn_rt.const_kind == ast.CONST_NONE: proc_ret = 1
|
||||
if proc_ret != 0:
|
||||
self.Gen.VarGen._SlifeEmitFrees(pool, builder, mod, trans)
|
||||
self.Gen.FuncGenBase._vpf_emit_leave(trans, builder)
|
||||
mlirlite.build_ret_void(builder)
|
||||
return 0
|
||||
if ret_ty_rt is None:
|
||||
@@ -676,6 +680,7 @@ novirtual class StmtGenCifMixin:
|
||||
# M3 owned slife(§4.2):返回值已算完,此处是常规出口——先释放 slife 再 ret
|
||||
# (必须在 val 求值之后:`return p.x` 之类若先释放 p 再读就成了 use-after-free)。
|
||||
self.Gen.VarGen._SlifeEmitFrees(pool, builder, mod, trans)
|
||||
self.Gen.FuncGenBase._vpf_emit_leave(trans, builder)
|
||||
mlirlite.build_ret(builder, val)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -184,6 +184,11 @@ novirtual class Translator:
|
||||
# translate_raise stores 1 there on raise StopIteration; None outside __next__.
|
||||
_stop_iter_flag_ptr: mlirlite.Value*
|
||||
|
||||
# --spend-file / --vpf: the function currently instrumented with vpf_enter/vpf_leave
|
||||
# (None when the body is not instrumented). _vpf_id is its compile-time hash id.
|
||||
_vpf_func: mlirlite.Function*
|
||||
_vpf_id: int
|
||||
|
||||
# try-except state: dispatch block plus exception code/message allocas of the innermost try.
|
||||
# Set by translate_try; written by translate_raise and call-site EH checks. None outside try.
|
||||
_eh_dispatch_bb: mlirlite.BasicBlock*
|
||||
@@ -266,6 +271,8 @@ novirtual class Translator:
|
||||
self._va_list_ptr = None
|
||||
self._vararg_name = None
|
||||
self._stop_iter_flag_ptr = None
|
||||
self._vpf_func = None
|
||||
self._vpf_id = 0
|
||||
self._eh_dispatch_bb = None
|
||||
self._eh_code_ptr = None
|
||||
self._eh_msg_ptr = None
|
||||
|
||||
+51
-15
@@ -88,24 +88,60 @@ def _FindViperCRoot(self_exe: str) -> str:
|
||||
return None
|
||||
|
||||
|
||||
def _FormatSelfBuildCommand(self_exe: str, project: str, build_dir: str, traceback: bool, asan: bool) -> str:
|
||||
# Mode flags are appended to the child invocation so the rebuilt compiler carries the
|
||||
# requested instrumentation; each literal includes its own leading space.
|
||||
extra: sizet = 0
|
||||
if traceback: extra += len(" --traceback")
|
||||
if asan: extra += len(" --asan")
|
||||
need: sizet = len(self_exe) + len(project) + len(build_dir) + extra + 64
|
||||
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)
|
||||
pos: sizet = string.strlen(str(buf))
|
||||
if traceback:
|
||||
viperlib.snprintf(buf + pos, need - pos, " --traceback")
|
||||
pos = string.strlen(str(buf))
|
||||
if asan:
|
||||
viperlib.snprintf(buf + pos, need - pos, " --asan")
|
||||
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)
|
||||
|
||||
|
||||
@@ -163,7 +199,7 @@ def _SpawnDetached(cmd: str, cwd: str) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def RunSelfBuild(traceback: bool, asan: bool) -> int:
|
||||
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")
|
||||
@@ -184,7 +220,7 @@ def RunSelfBuild(traceback: bool, asan: bool) -> int:
|
||||
os.path.join(os.path.join(root, "copyself"), "project.vpj"))
|
||||
return 1
|
||||
|
||||
cmd: str = _FormatSelfBuildCommand(self_exe, project, build_dir, traceback, asan)
|
||||
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
|
||||
|
||||
+54
-9
@@ -1,4 +1,5 @@
|
||||
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
|
||||
@@ -19,6 +20,7 @@ import lib
|
||||
import stdio
|
||||
import string
|
||||
import stdlib
|
||||
import viperlib
|
||||
import memhub
|
||||
import argparse
|
||||
import platmacro
|
||||
@@ -176,6 +178,8 @@ def main(argc: int, argv: []str) -> int:
|
||||
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 <exe>.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 2 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")
|
||||
@@ -234,16 +238,12 @@ Target: %s
|
||||
# --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"):
|
||||
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("rebuild-includes") or args.get_bool("clear-cache") or args.get_bool("nowarn-register") or args.get_bool("no-llvmpeephole") 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:
|
||||
stdio.printf("FAIL: --build-self is a standalone mode; do not combine it with build options\n")
|
||||
argparse.release(args)
|
||||
return 2
|
||||
# --traceback / --asan select the mode of the compiler being rebuilt, so they are
|
||||
# forwarded to the child build instead of being rejected here.
|
||||
self_traceback: bool = args.get_bool("traceback")
|
||||
self_asan: bool = args.get_bool("asan")
|
||||
# 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(self_traceback, self_asan)
|
||||
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.
|
||||
@@ -272,6 +272,14 @@ Target: %s
|
||||
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.
|
||||
@@ -337,6 +345,19 @@ Target: %s
|
||||
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 2 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 = 2
|
||||
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"
|
||||
@@ -458,6 +479,30 @@ Target: %s
|
||||
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"):
|
||||
|
||||
@@ -138,8 +138,12 @@ python Test\SequentialRegressionTest.py --exe exe\viperc.exe
|
||||
| `--rebuild-includes` | delete only the includes cache (`includes.binary`), forcing a full includes rebuild |
|
||||
| `--clear-cache` | clear only temp (`.vpi` / `.stub.ll` / `.text.ll`), forcing stub regeneration |
|
||||
| `--run` | execute the produced binary on success |
|
||||
| `--repl` | interactive REPL (M1: expression fragment → DLL → call and echo); without `--project` it looks for the `repl.vpj` template project in cwd, then next to `viperc.exe` (errors out with a `--project` hint if neither exists); mutually exclusive with `--asan` / `--phase2command` |
|
||||
| `--repl` | interactive REPL (M1: expression fragment → DLL → call and echo); without `--project` it looks for the `repl.vpj` template project in cwd, then next to `viperc.exe` (errors out with a `--project` hint if neither exists); mutually exclusive with `--asan` / `--phase2command` / `--spend-file` |
|
||||
| `--asan` | ASan mode: `opt -passes=asan` first; runtimes must be declared in `project.vpj` `linker.static_libs`, else fail-fast |
|
||||
| `--spend-file <path>` | vprofile report path for the built program (relative to the output exe dir, `../x.txt` and absolute paths allowed); enables function instrumentation and force-links `includes/vprofile.vp`. Mutually exclusive with `--repl` |
|
||||
| `--vpf <0-3>` | vprofile granularity: 0=off, 1=call counts, 2=+per-function time (default with `--spend-file`), 3=+caller/callee edges |
|
||||
| `--build-self` | rebuild the compiler into `ViperC\__vpcbuild_2__` and let `copyself` replace `ViperC\__vpcbuild__`; every other argument is forwarded verbatim except `--project` / `--build-dir` (the self-build owns those) |
|
||||
| `--phaseb-legacy` | use the legacy `mlir-opt` + `mlir-translate` + `llc` child-process PhaseB instead of the in-process `ircore.dll` backend |
|
||||
| `--phase2command <bat>` | do not run `llc`/link; write the commands into the given `.bat` |
|
||||
| `--nowarn-register` | suppress Phase A dependency WARNs (cross-module `text.ll` not yet emitted, converges next pass) |
|
||||
| `--no-llvmpeephole` | disable FunctionPrint local peepholes (T2 store/load forwarding, T3 no-op bitcast) |
|
||||
|
||||
Reference in new issue
Block a user