Skip to content

Commit

Permalink
v: add callstack support on v.debug (#20680)
Browse files Browse the repository at this point in the history
  • Loading branch information
felipensp committed Feb 1, 2024
1 parent 673a2f4 commit 98e0293
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 4 deletions.
14 changes: 14 additions & 0 deletions vlib/v/ast/ast.v
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
37 changes: 36 additions & 1 deletion vlib/v/checker/fn.v
Expand Up @@ -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 {
Expand All @@ -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
}
}
Expand All @@ -1489,13 +1491,15 @@ 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)) {
if typ := c.table.resolve_generic_to_concrete(func.return_type, func.generic_names,
node.concrete_types)
{
node.return_type = typ
c.register_trace_call(node, func)
return typ
}
}
Expand All @@ -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)
Expand Down Expand Up @@ -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
}

Expand Down
40 changes: 40 additions & 0 deletions 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]
}
4 changes: 3 additions & 1 deletion vlib/v/gen/c/cgen.v
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions vlib/v/gen/c/dumpexpr.v
Expand Up @@ -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 {
Expand Down
74 changes: 72 additions & 2 deletions vlib/v/gen/c/fn.v
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(')')
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions vlib/v/gen/c/testdata/callstack.out
@@ -0,0 +1 @@
121enter
66 changes: 66 additions & 0 deletions 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
}
4 changes: 4 additions & 0 deletions vlib/v/pref/pref.c.v
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 98e0293

Please sign in to comment.