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:
TermiNexus committed 2026-10-30 22:24:01 +08:00
1 parent c9d33bc202
commit 91e188c539
9 files changed
+299 -25

No files matched your search

+14
View File
@@ -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:
+123
View File
@@ -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)
+10
View File
@@ -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
+5
View File
@@ -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
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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"):
+5 -1
View File
@@ -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) |