From 91e188c53960b2bf6d3fdbd9950d578ec195cb38 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Fri, 30 Oct 2026 22:24:01 +0800 Subject: [PATCH] feat(vpc): vprofile instrumentation (--spend-file/--vpf) + verbatim --build-self forwarding --spend-file 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, 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. --- App/lib/Phase/BuildPipeline.vp | 14 +++ App/lib/Stub/StubMerger/StubMergerBuild.vp | 30 +++++ App/lib/core/Gen/Func/FuncGenBase.vp | 123 +++++++++++++++++++++ App/lib/core/Gen/GenFlags.vp | 10 ++ App/lib/core/Gen/Stmt/StmtGenCifMixin.vp | 5 + App/lib/core/Gen/Translator.vp | 7 ++ App/lib/core/SelfBuild.vp | 66 ++++++++--- App/main.vp | 63 +++++++++-- README.md | 6 +- 9 files changed, 299 insertions(+), 25 deletions(-) create mode 100644 App/lib/core/Gen/GenFlags.vp diff --git a/App/lib/Phase/BuildPipeline.vp b/App/lib/Phase/BuildPipeline.vp index c6b70c4..29dc6de 100644 --- a/App/lib/Phase/BuildPipeline.vp +++ b/App/lib/Phase/BuildPipeline.vp @@ -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 diff --git a/App/lib/Stub/StubMerger/StubMergerBuild.vp b/App/lib/Stub/StubMerger/StubMergerBuild.vp index 3cf2ebe..05eea07 100644 --- a/App/lib/Stub/StubMerger/StubMergerBuild.vp +++ b/App/lib/Stub/StubMerger/StubMergerBuild.vp @@ -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: diff --git a/App/lib/core/Gen/Func/FuncGenBase.vp b/App/lib/core/Gen/Func/FuncGenBase.vp index 6e73e8f..8b1e618 100644 --- a/App/lib/core/Gen/Func/FuncGenBase.vp +++ b/App/lib/core/Gen/Func/FuncGenBase.vp @@ -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("", 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) diff --git a/App/lib/core/Gen/GenFlags.vp b/App/lib/core/Gen/GenFlags.vp new file mode 100644 index 0000000..c3c0c8b --- /dev/null +++ b/App/lib/core/Gen/GenFlags.vp @@ -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 diff --git a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp index bb501a4..21f482e 100644 --- a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp +++ b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp @@ -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 diff --git a/App/lib/core/Gen/Translator.vp b/App/lib/core/Gen/Translator.vp index c998451..7cc8d7b 100644 --- a/App/lib/core/Gen/Translator.vp +++ b/App/lib/core/Gen/Translator.vp @@ -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 diff --git a/App/lib/core/SelfBuild.vp b/App/lib/core/SelfBuild.vp index bed762b..006e69b 100644 --- a/App/lib/core/SelfBuild.vp +++ b/App/lib/core/SelfBuild.vp @@ -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 diff --git a/App/main.vp b/App/main.vp index 8a4b865..a01f9fb 100644 --- a/App/main.vp +++ b/App/main.vp @@ -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 .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"): diff --git a/README.md b/README.md index c189a6f..bad2838 100644 --- a/README.md +++ b/README.md @@ -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 ` | 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 ` | 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) |