From 98e0293ec0839fc4934b0afa87bdc8068c01765d Mon Sep 17 00:00:00 2001 From: Felipe Pena Date: Thu, 1 Feb 2024 02:10:23 -0300 Subject: [PATCH] v: add callstack support on v.debug (#20680) --- vlib/v/ast/ast.v | 14 ++++++ vlib/v/checker/fn.v | 37 ++++++++++++++- vlib/v/debug/callstack.v | 40 ++++++++++++++++ vlib/v/gen/c/cgen.v | 4 +- vlib/v/gen/c/dumpexpr.v | 7 +++ vlib/v/gen/c/fn.v | 74 ++++++++++++++++++++++++++++- vlib/v/gen/c/testdata/callstack.out | 1 + vlib/v/gen/c/testdata/callstack.vv | 66 +++++++++++++++++++++++++ vlib/v/pref/pref.c.v | 4 ++ 9 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 vlib/v/debug/callstack.v create mode 100644 vlib/v/gen/c/testdata/callstack.out create mode 100644 vlib/v/gen/c/testdata/callstack.vv diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index c4a21b5eb..fc4ba99b9 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -573,6 +573,8 @@ pub mut: params []Param stmts []Stmt defer_stmts []DeferStmt + trace_fns map[string]FnTrace + has_trace_fns bool return_type Type return_type_pos token.Pos // `string` in `fn (u User) name() string` position has_return bool @@ -609,6 +611,18 @@ pub fn (f &FnDecl) new_method_with_receiver_type(new_type_ Type) FnDecl { } } +@[minify] +pub struct FnTrace { +pub mut: + name string +pub: + file string + line i64 + return_type Type + func &Fn = unsafe { nil } + is_fn_var bool +} + @[minify] pub struct Fn { pub: diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index 17d7ef039..de0a55af2 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -1459,6 +1459,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. } } } + // resolve return generics struct to concrete type if func.generic_names.len > 0 && func.return_type.has_flag(.generic) && c.table.cur_fn != unsafe { nil } && c.table.cur_fn.generic_names.len == 0 { @@ -1473,6 +1474,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. concrete_types) { node.return_type = typ + c.register_trace_call(node, func) return typ } } @@ -1489,6 +1491,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. node.return_type = typ } } + c.register_trace_call(node, func) return node.return_type } else { if node.concrete_types.len > 0 && !node.concrete_types.any(it.has_flag(.generic)) { @@ -1496,6 +1499,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. node.concrete_types) { node.return_type = typ + c.register_trace_call(node, func) return typ } } @@ -1505,13 +1509,44 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. if typ.has_flag(.generic) { node.return_type = typ } + c.register_trace_call(node, func) return typ } } } + c.register_trace_call(node, func) return func.return_type } +// register_trace_call registers the wrapper funcs for calling funcs for callstack feature +fn (mut c Checker) register_trace_call(node ast.CallExpr, func ast.Fn) { + is_traceable := c.pref.is_callstack && c.table.cur_fn != unsafe { nil } && c.pref.is_callstack + && c.file.imports.any(it.mod == 'v.debug') && node.name != 'v.debug.callstack' + if is_traceable { + generic_name := node.concrete_types.map(c.table.type_to_str(it)).join('_') + hash_fn := '_v__trace__${c.table.cur_fn.name}_${node.name}_${generic_name}_${node.pos.line_nr}' + fn_name := if generic_name != '' { + '${node.name}_T_${generic_name}' + } else { + node.name + } + calling_fn := if func.is_method { + '${c.table.type_to_str(c.unwrap_generic(node.left_type))}_${fn_name}' + } else { + fn_name + } + c.table.cur_fn.trace_fns[hash_fn] = ast.FnTrace{ + name: calling_fn + file: c.file.path + line: node.pos.line_nr + 1 + return_type: node.return_type + func: &func + is_fn_var: node.is_fn_var + } + c.table.cur_fn.has_trace_fns = true + } +} + fn (mut c Checker) resolve_comptime_args(func ast.Fn, node_ ast.CallExpr, concrete_types []ast.Type) map[int]ast.Type { mut comptime_args := map[int]ast.Type{} has_dynamic_vars := (c.table.cur_fn != unsafe { nil } && c.table.cur_fn.generic_names.len > 0) @@ -2421,7 +2456,7 @@ fn (mut c Checker) method_call(mut node ast.CallExpr) ast.Type { } } } - + c.register_trace_call(node, method) return node.return_type } diff --git a/vlib/v/debug/callstack.v b/vlib/v/debug/callstack.v new file mode 100644 index 000000000..b08d0060d --- /dev/null +++ b/vlib/v/debug/callstack.v @@ -0,0 +1,40 @@ +// Copyright (c) 2019-2024 Felipe Pena. All rights reserved. +// Use of this source code is governed by an MIT license that can be found in the LICENSE file. +@[has_globals] +module debug + +// function call location trace +@[markused] +pub struct FnTrace { +pub: + name string + file string + line i64 +} + +@[markused] +__global g_callstack = []FnTrace{} + +// dump_callstack dumps callstack to the user +@[markused] +pub fn dump_callstack() { + bar := '-'.repeat(50).str + C.printf(c'Backtrace:\n') + C.printf(c'%s\n', bar) + callstack_len := g_callstack.len + for i := 0; i < callstack_len; i++ { + item := g_callstack[i] + C.printf(c'%s:%-4d | %s> %s\n', &char(item.file.str), item.line, ' '.repeat(i).str, + item.name) + } + C.printf(c'%s\n', bar) +} + +// callstack retrieves the supplied stack frame based on supplied depth +@[markused] +pub fn callstack(depth int) ?FnTrace { + if depth >= g_callstack.len { + return none + } + return g_callstack[depth] +} diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 863df5936..075db8012 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -138,6 +138,7 @@ mut: inside_or_block bool inside_call bool inside_curry_call bool // inside foo()()!, foo()()?, foo()() + inside_dump_fn bool expected_fixed_arr bool inside_for_c_stmt bool // inside_comptime_for_field bool @@ -180,7 +181,8 @@ mut: sumtype_casting_fns []SumtypeCastingFn anon_fn_definitions []string // anon generated functions definition list sumtype_definitions map[int]bool // `_TypeA_to_sumtype_TypeB()` fns that have been generated - json_types []ast.Type // to avoid json gen duplicates + trace_fn_definitions []string + json_types []ast.Type // to avoid json gen duplicates pcs []ProfileCounterMeta // -prof profile counter fn_names => fn counter name hotcode_fn_names []string hotcode_fpaths []string diff --git a/vlib/v/gen/c/dumpexpr.v b/vlib/v/gen/c/dumpexpr.v index e34c75bd6..6139b6a35 100644 --- a/vlib/v/gen/c/dumpexpr.v +++ b/vlib/v/gen/c/dumpexpr.v @@ -15,6 +15,13 @@ fn (mut g Gen) dump_expr(node ast.DumpExpr) { mut name := node.cname mut expr_type := node.expr_type + if node.expr is ast.CallExpr { + g.inside_dump_fn = true + defer { + g.inside_dump_fn = false + } + } + if g.cur_fn != unsafe { nil } && g.cur_fn.generic_names.len > 0 { // generic func with recursion rewrite node.expr_type if node.expr is ast.Ident { diff --git a/vlib/v/gen/c/fn.v b/vlib/v/gen/c/fn.v index 3f622c8ca..376fb2d1d 100644 --- a/vlib/v/gen/c/fn.v +++ b/vlib/v/gen/c/fn.v @@ -273,6 +273,50 @@ fn (mut g Gen) gen_fn_decl(node &ast.FnDecl, skip bool) { g.last_fn_c_name = last_fn_c_name_save } g.last_fn_c_name = impl_fn_name + + if node.trace_fns.len > 0 { + for trace_fn, call_fn in node.trace_fns { + if trace_fn in g.trace_fn_definitions { + continue + } + trace_fn_ret_type := g.typ(call_fn.return_type) + + g.write('VV_LOCAL_SYMBOL ${trace_fn_ret_type} ${c_name(trace_fn)}(') + g.definitions.write_string('VV_LOCAL_SYMBOL ${trace_fn_ret_type} ${c_name(trace_fn)}(') + + if call_fn.is_fn_var { + sig := g.fn_var_signature(call_fn.func.return_type, call_fn.func.params.map(it.typ), + call_fn.name) + g.write(sig) + g.definitions.write_string(sig) + } else { + g.fn_decl_params(call_fn.func.params, unsafe { nil }, call_fn.func.is_variadic) + } + + g.writeln(') {') + g.definitions.write_string(');\n') + + orig_fn_args := call_fn.func.params.map(it.name).join(', ') + + if g.cur_fn.is_method || g.cur_fn.is_static_type_method { + g.writeln('\tarray_push((array*)&g_callstack, _MOV((v__debug__FnTrace[]){ ((v__debug__FnTrace){.name = _SLIT("${g.table.type_to_str(g.cur_fn.receiver.typ)}.${g.cur_fn.name.all_after_last('__static__')}"),.file = _SLIT("${call_fn.file}"),.line = ${call_fn.line},}) }));') + } else { + g.writeln('\tarray_push((array*)&g_callstack, _MOV((v__debug__FnTrace[]){ ((v__debug__FnTrace){.name = _SLIT("${g.cur_fn.name}"),.file = _SLIT("${call_fn.file}"),.line = ${call_fn.line},}) }));') + } + if call_fn.return_type == 0 || call_fn.return_type == ast.void_type { + g.writeln('\t${c_name(call_fn.name)}(${orig_fn_args});') + g.writeln('\tarray_pop((array*)&g_callstack);') + } else { + g.writeln('\t${g.typ(call_fn.return_type)} ret = ${c_name(call_fn.name)}(${orig_fn_args});') + g.writeln('\tarray_pop((array*)&g_callstack);') + g.writeln('\treturn ret;') + } + g.writeln('}') + g.writeln('') + g.trace_fn_definitions << trace_fn + } + } + // if is_live_wrap { if is_livemain { @@ -1491,7 +1535,12 @@ fn (mut g Gen) method_call(node ast.CallExpr) { } g.write('${name}${noscan}(') } else { - g.write('${name}(') + if g.cur_fn != unsafe { nil } && g.cur_fn.has_trace_fns { + g.gen_trace_call(node, name) + g.write('(') + } else { + g.write('${name}(') + } } } is_array_method_first_last_repeat := final_left_sym.kind == .array @@ -1935,7 +1984,14 @@ fn (mut g Gen) fn_call(node ast.CallExpr) { } } if !is_fn_var { - g.write(g.get_ternary_name(name)) + if g.cur_fn != unsafe { nil } && g.cur_fn.has_trace_fns { + g.gen_trace_call(node, name) + if node.is_fn_var { + return + } + } else { + g.write(g.get_ternary_name(name)) + } } if is_interface_call { g.write(')') @@ -1979,6 +2035,20 @@ fn (mut g Gen) fn_call(node ast.CallExpr) { g.is_json_fn = false } +// gen_trace_call generates call to the wrapper trace fn if the call is traceable +fn (mut g Gen) gen_trace_call(node ast.CallExpr, name string) { + generic_name := node.concrete_types.map(g.table.type_to_str(it)).join('_') + trace_fn_name := '_v__trace__${g.cur_fn.name}_${node.name}_${generic_name}_${node.pos.line_nr}' + if _ := g.cur_fn.trace_fns[trace_fn_name] { + g.write(c_name(trace_fn_name)) + if node.is_fn_var { + g.write('(${node.name})') + } + } else { + g.write(g.get_ternary_name(name)) + } +} + fn (mut g Gen) autofree_call_pregen(node ast.CallExpr) { // g.writeln('// autofree_call_pregen()') // Create a temporary var before fn call for each argument in order to free it (only if it's a complex expression, diff --git a/vlib/v/gen/c/testdata/callstack.out b/vlib/v/gen/c/testdata/callstack.out new file mode 100644 index 000000000..942e79733 --- /dev/null +++ b/vlib/v/gen/c/testdata/callstack.out @@ -0,0 +1 @@ +121enter \ No newline at end of file diff --git a/vlib/v/gen/c/testdata/callstack.vv b/vlib/v/gen/c/testdata/callstack.vv new file mode 100644 index 000000000..256047e08 --- /dev/null +++ b/vlib/v/gen/c/testdata/callstack.vv @@ -0,0 +1,66 @@ +module main + +import v.debug + +// vtest vflags: -d callstack + +struct Test {} + +fn (t Test) test_method() { + fn_test4() +} + +fn Test.test_static_method() { + fn_test3() +} + +fn fn_test2() ? { + a := 1 + // debug.dump_callstack() + print(a) + assert debug.callstack(1)?.line == 43 + assert debug.callstack(0)?.line == 51 +} + +fn fn_test3() ? { + // debug.dump_callstack() + assert debug.callstack(3)?.line == 8 + assert debug.callstack(1)?.line == 41 + assert debug.callstack(0)?.line == 51 + return +} + +fn fn_test4() ? { + // debug.dump_callstack() + assert debug.callstack(1)?.line == 10 +} + +fn fn_test_anon(cb fn ()) { + cb() +} + +fn fn_test() ? { + fn_test2() + print('enter') + fn_test3() + assert debug.callstack(0)?.line == 51 +} + +fn main() { + print(12) + fn_test() + // debug.dump_callstack() + // dump(debug.callstack(0)) + mut ret := debug.callstack(0) + Test{}.test_method() + Test.test_static_method() + fn_test_anon(fn () { + // debug.dump_callstack() + ret := debug.callstack(3) or { return } + res := ret.name.starts_with('anon_fn') + assert res + }) + + ret = debug.callstack(0) + assert ret == none +} diff --git a/vlib/v/pref/pref.c.v b/vlib/v/pref/pref.c.v index 11babff7d..41203d68c 100644 --- a/vlib/v/pref/pref.c.v +++ b/vlib/v/pref/pref.c.v @@ -133,6 +133,7 @@ pub mut: is_help bool // -h, -help or --help was passed is_quiet bool // do not show the repetitive explanatory messages like the one for `v -prod run file.v` . is_cstrict bool // turn on more C warnings; slightly slower + is_callstack bool // turn on callstack registers on each call when v.debug is imported eval_argument string // `println(2+2)` on `v -e "println(2+2)"`. Note that this source code, will be evaluated in vsh mode, so 'v -e 'println(ls(".")!)' is valid. test_runner string // can be 'simple' (fastest, but much less detailed), 'tap', 'normal' profile_file string // the profile results will be stored inside profile_file @@ -1042,6 +1043,9 @@ pub fn parse_args_and_show_errors(known_external_commands []string, args []strin res.compile_defines << 'musl' res.compile_defines_all << 'musl' } + if 'callstack' in res.compile_defines_all { + res.is_callstack = true + } // keep only the unique res.build_options: mut m := map[string]string{} for x in res.build_options { -- 2.39.5