diff --git a/vlib/v/builder/builder.v b/vlib/v/builder/builder.v index f3b2c77b0a..79598dd0e9 100644 --- a/vlib/v/builder/builder.v +++ b/vlib/v/builder/builder.v @@ -8,9 +8,10 @@ import v.ast import v.vmod import v.checker import v.parser -import v.depgraph import v.markused +import v.depgraph import v.callgraph +import v.dotgraph pub struct Builder { pub: @@ -52,6 +53,9 @@ pub fn new_builder(pref &pref.Preferences) Builder { } } util.timing_set_should_print(pref.show_timings || pref.is_verbose) + if pref.show_callgraph || pref.show_depgraph { + dotgraph.start_digraph() + } return Builder{ pref: pref table: table @@ -180,6 +184,9 @@ pub fn (mut b Builder) resolve_deps() { eprintln(deps_resolved.display()) eprintln('------------------------------------------') } + if b.pref.show_depgraph { + depgraph.show(deps_resolved, b.pref.path) + } cycles := deps_resolved.display_cycles() if cycles.len > 1 { verror('error: import cycle detected between the following modules: \n' + cycles) diff --git a/vlib/v/callgraph/callgraph.v b/vlib/v/callgraph/callgraph.v index e5c2b4b691..da57bc7fc2 100644 --- a/vlib/v/callgraph/callgraph.v +++ b/vlib/v/callgraph/callgraph.v @@ -3,7 +3,7 @@ module callgraph import v.ast import v.ast.walker import v.pref -import strings +import v.dotgraph // callgraph.show walks the AST, starting at main() and prints a DOT output describing the calls // that function make transitively @@ -11,18 +11,14 @@ pub fn show(mut table ast.Table, pref &pref.Preferences, ast_files []&ast.File) mut mapper := &Mapper{ pref: pref table: table + dg: dotgraph.new('CallGraph', 'CallGraph for $pref.path', 'green') } - mapper.sb.writeln('digraph G {') - mapper.sb.writeln('\tedge [fontname="Helvetica",fontsize="10",labelfontname="Helvetica",labelfontsize="10",style="solid",color="black"];') - mapper.sb.writeln('\tnode [fontname="Helvetica",fontsize="10",style="filled",fontcolor="black",fillcolor="white",color="black",shape="box"];') - mapper.sb.writeln('\trankdir="LR";') // Node14 [shape="box",label="PrivateBase",URL="$classPrivateBase.html"]; // Node15 -> Node9 [dir=back,color="midnightblue",fontsize=10,style="solid"]; for afile in ast_files { walker.walk(mapper, afile) } - mapper.sb.writeln('}') - println(mapper.sb.str()) + mapper.dg.finish() } [heap] @@ -37,7 +33,7 @@ mut: caller_name string dot_caller_name string is_caller_used bool - sb strings.Builder = strings.new_builder(1024) + dg dotgraph.DotGraph } fn (mut m Mapper) dot_normalise_node_name(name string) string { @@ -104,11 +100,10 @@ fn (mut m Mapper) visit(node &ast.Node) ? { m.caller_name = m.fn_name(node.name, node.receiver.typ, node.is_method) m.dot_caller_name = m.dot_fn_name(node.name, node.receiver.typ, node.is_method) if m.is_caller_used { - if m.caller_name == 'main.main' { - m.sb.writeln('\t$m.dot_caller_name [label="fn main()",color="blue",height=0.2,width=0.4,fillcolor="#00FF00",tooltip="The main program entry point.",shape=oval];') - } else { - m.sb.writeln('\t$m.dot_caller_name [shape="box",label="$m.caller_name"];') - } + m.dg.new_node(m.caller_name, + node_name: m.dot_caller_name + should_highlight: m.caller_name == 'main.main' + ) } } else {} @@ -121,11 +116,9 @@ fn (mut m Mapper) visit(node &ast.Node) ? { dot_called_name := m.dot_fn_name(node.name, node.receiver_type, node.is_method) // Node15 -> Node9 [dir=back,color="midnightblue",fontsize=10,style="solid"]; - if m.caller_name == 'main.main' { - m.sb.writeln('\t$m.dot_caller_name -> $dot_called_name [color="blue"];') - } else { - m.sb.writeln('\t$m.dot_caller_name -> $dot_called_name') - } + m.dg.new_edge(m.dot_caller_name, dot_called_name, + should_highlight: m.caller_name == 'main.main' + ) } } else {} @@ -134,13 +127,3 @@ fn (mut m Mapper) visit(node &ast.Node) ? { else {} } } - -/* -mut fpath := '' - if m.file != 0 { - fpath = m.file.path - } - node_pos := node.position() - fpos := '$fpath:$node_pos.line_nr:$node_pos.col:' - println('$fpos $node.type_name() | $node') -*/ diff --git a/vlib/v/depgraph/depgraph.v b/vlib/v/depgraph/depgraph.v index a4c8875244..cc6a8eb9f7 100644 --- a/vlib/v/depgraph/depgraph.v +++ b/vlib/v/depgraph/depgraph.v @@ -5,6 +5,8 @@ // this implementation is specifically suited to ordering dependencies module depgraph +import v.dotgraph + struct DepGraphNode { pub mut: name string @@ -197,3 +199,20 @@ fn (mut nn NodeNames) is_part_of_cycle(name string, already_seen []string) (bool nn.is_cycle[name] = false return false, new_already_seen } + +pub fn show(graph &DepGraph, path string) { + mut dg := dotgraph.new('ModGraph', 'ModGraph for $path', 'blue') + mbuiltin := 'builtin' + for node in graph.nodes { + is_main := node.name == 'main' + dg.new_node(node.name, should_highlight: is_main) + mut deps := node.deps.clone() + if node.name != mbuiltin && mbuiltin !in deps { + deps << mbuiltin + } + for dep in deps { + dg.new_edge(node.name, dep, should_highlight: is_main) + } + } + dg.finish() +} diff --git a/vlib/v/dotgraph/dotgraph.c.v b/vlib/v/dotgraph/dotgraph.c.v new file mode 100644 index 0000000000..a98f7d8949 --- /dev/null +++ b/vlib/v/dotgraph/dotgraph.c.v @@ -0,0 +1,8 @@ +module dotgraph + +pub fn start_digraph() { + println('digraph G {') + C.atexit(fn () { + println('}') + }) +} diff --git a/vlib/v/dotgraph/dotgraph.v b/vlib/v/dotgraph/dotgraph.v new file mode 100644 index 0000000000..087aef82c1 --- /dev/null +++ b/vlib/v/dotgraph/dotgraph.v @@ -0,0 +1,81 @@ +module dotgraph + +import strings + +[heap] +struct DotGraph { +mut: + sb strings.Builder +} + +pub fn new(name string, label string, color string) &DotGraph { + mut res := &DotGraph{ + sb: strings.new_builder(1024) + } + res.writeln(' subgraph cluster_$name {') + res.writeln('\tedge [fontname="Helvetica",fontsize="10",labelfontname="Helvetica",labelfontsize="10",style="solid",color="black"];') + res.writeln('\tnode [fontname="Helvetica",fontsize="10",style="filled",fontcolor="black",fillcolor="white",color="black",shape="box"];') + res.writeln('\trankdir="LR";') + res.writeln('\tcolor="$color";') + res.writeln('\tlabel="$label";') + // Node14 [shape="box",label="PrivateBase",URL="$classPrivateBase.html"]; + // Node15 -> Node9 [dir=back,color="midnightblue",fontsize=10,style="solid"]; + return res +} + +pub fn (mut d DotGraph) writeln(line string) { + d.sb.writeln(line) +} + +pub fn (mut d DotGraph) finish() { + d.sb.writeln(' }') + println(d.sb.str()) +} + +// + +pub struct NewNodeConfig { + node_name string + should_highlight bool + tooltip string + ctx voidptr = voidptr(0) + name2node_fn FnLabel2NodeName = node_name +} + +pub fn (mut d DotGraph) new_node(nlabel string, cfg NewNodeConfig) { + mut nname := cfg.name2node_fn(nlabel, cfg.ctx) + if cfg.node_name != '' { + nname = cfg.node_name + } + if cfg.should_highlight { + d.writeln('\t$nname [label="$nlabel",color="blue",height=0.2,width=0.4,fillcolor="#00FF00",tooltip="$cfg.tooltip",shape=oval];') + } else { + d.writeln('\t$nname [shape="box",label="$nlabel"];') + } +} + +// + +pub struct NewEdgeConfig { + should_highlight bool + ctx voidptr = voidptr(0) + name2node_fn FnLabel2NodeName = node_name +} + +pub fn (mut d DotGraph) new_edge(source string, target string, cfg NewEdgeConfig) { + nsource := cfg.name2node_fn(source, cfg.ctx) + ntarget := cfg.name2node_fn(target, cfg.ctx) + if cfg.should_highlight { + d.writeln('\t$nsource -> $ntarget [color="blue"];') + } else { + d.writeln('\t$nsource -> $ntarget;') + } +} + +// + +pub type FnLabel2NodeName = fn (string, voidptr) string + +pub fn node_name(name string, context voidptr) string { + return name.replace('.', '_') +} diff --git a/vlib/v/pref/pref.v b/vlib/v/pref/pref.v index a8c17d48a2..f823972dec 100644 --- a/vlib/v/pref/pref.v +++ b/vlib/v/pref/pref.v @@ -98,37 +98,36 @@ pub mut: // verbosity VerboseLevel is_verbose bool // nofmt bool // disable vfmt - is_test bool // `v test string_test.v` - is_script bool // single file mode (`v program.v`), main function can be skipped - is_vsh bool // v script (`file.vsh`) file, the `os` module should be made global - is_livemain bool // main program that contains live/hot code - is_liveshared bool // a shared library, that will be used in a -live main program - is_shared bool // an ordinary shared library, -shared, no matter if it is live or not - is_prof bool // benchmark every function - profile_file string // the profile results will be stored inside profile_file - profile_no_inline bool // when true, [inline] functions would not be profiled - translated bool // `v translate doom.v` are we running V code translated from C? allow globals, ++ expressions, etc - is_prod bool // use "-O2" - obfuscate bool // `v -obf program.v`, renames functions to "f_XXX" - is_repl bool - is_run bool - sanitize bool // use Clang's new "-fsanitize" option - is_debug bool // false by default, turned on by -g or -cg, it tells v to pass -g to the C backend compiler. - sourcemap bool // JS Backend: -sourcemap will create a source map - default false - sourcemap_inline bool = true // JS Backend: -sourcemap-inline will embed the source map in the generated JaaScript file - currently default true only implemented - sourcemap_src_included bool // JS Backend: -sourcemap-src-included includes V source code in source map - default false - is_vlines bool // turned on by -g, false by default (it slows down .tmp.c generation slightly). - show_cc bool // -showcc, print cc command - show_c_output bool // -show-c-output, print all cc output even if the code was compiled correctly - show_callgraph bool // -show-callgraph, print the program callgraph, in a Graphviz DOT format to stdout + is_test bool // `v test string_test.v` + is_script bool // single file mode (`v program.v`), main function can be skipped + is_vsh bool // v script (`file.vsh`) file, the `os` module should be made global + is_livemain bool // main program that contains live/hot code + is_liveshared bool // a shared library, that will be used in a -live main program + is_shared bool // an ordinary shared library, -shared, no matter if it is live or not + is_prof bool // benchmark every function + profile_file string // the profile results will be stored inside profile_file + profile_no_inline bool // when true, [inline] functions would not be profiled + translated bool // `v translate doom.v` are we running V code translated from C? allow globals, ++ expressions, etc + is_prod bool // use "-O2" + obfuscate bool // `v -obf program.v`, renames functions to "f_XXX" + is_repl bool + is_run bool + is_debug bool // turned on by -g or -cg, it tells v to pass -g to the C backend compiler. + is_vlines bool // turned on by -g (it slows down .tmp.c generation slightly). // NB: passing -cg instead of -g will set is_vlines to false and is_debug to true, thus making v generate cleaner C files, // which are sometimes easier to debug / inspect manually than the .tmp.c files by plain -g (when/if v line number generation breaks). - // use cached modules to speed up compilation. - dump_c_flags string // `-dump-c-flags file.txt` - let V store all C flags, passed to the backend C compiler - // in `file.txt`, one C flag/value per line. - use_cache bool // = true - retry_compilation bool = true // retry the compilation with another C compiler, if tcc fails. - is_stats bool // `v -stats file_test.v` will produce more detailed statistics for the tests that were run + sanitize bool // use Clang's new "-fsanitize" option + sourcemap bool // JS Backend: -sourcemap will create a source map - default false + sourcemap_inline bool = true // JS Backend: -sourcemap-inline will embed the source map in the generated JaaScript file - currently default true only implemented + sourcemap_src_included bool // JS Backend: -sourcemap-src-included includes V source code in source map - default false + show_cc bool // -showcc, print cc command + show_c_output bool // -show-c-output, print all cc output even if the code was compiled correctly + show_callgraph bool // -show-callgraph, print the program callgraph, in a Graphviz DOT format to stdout + show_depgraph bool // -show-depgraph, print the program module dependency graph, in a Graphviz DOT format to stdout + dump_c_flags string // `-dump-c-flags file.txt` - let V store all C flags, passed to the backend C compiler in `file.txt`, one C flag/value per line. + use_cache bool // when set, use cached modules to speed up subsequent compilations, at the cost of slower initial ones (while the modules are cached) + retry_compilation bool = true // retry the compilation with another C compiler, if tcc fails. + is_stats bool // `v -stats file_test.v` will produce more detailed statistics for the tests that were run // TODO Convert this into a []string cflags string // Additional options which will be passed to the C compiler. // For example, passing -cflags -Os will cause the C compiler to optimize the generated binaries for size. @@ -141,7 +140,7 @@ pub mut: building_v bool autofree bool // `v -manualfree` => false, `v -autofree` => true; false by default for now. // Disabling `free()` insertion results in better performance in some applications (e.g. compilers) - compress bool + compress bool // when set, use `upx` to compress the generated executable // skip_builtin bool // Skips re-compilation of the builtin module // to increase compilation time. // This is on by default, since a vast majority of users do not @@ -444,6 +443,9 @@ pub fn parse_args(known_external_commands []string, args []string) (&Preferences '-show-callgraph' { res.show_callgraph = true } + '-show-depgraph' { + res.show_depgraph = true + } '-dump-c-flags' { res.dump_c_flags = cmdline.option(current_args, arg, '-') i++