Skip to content

Commit

Permalink
v: $dbg statement - native V debugger REPL (#20533)
Browse files Browse the repository at this point in the history
  • Loading branch information
felipensp committed Jan 19, 2024
1 parent 45e13ea commit 9f6448e
Show file tree
Hide file tree
Showing 33 changed files with 786 additions and 14 deletions.
8 changes: 8 additions & 0 deletions cmd/tools/vast/vast.v
Expand Up @@ -440,6 +440,7 @@ fn (t Tree) stmt(node ast.Stmt) &Node {
ast.AsmStmt { return t.asm_stmt(node) }
ast.NodeError { return t.node_error(node) }
ast.EmptyStmt { return t.empty_stmt(node) }
ast.DebuggerStmt { return t.debugger_stmt(node) }
}
return t.null_node()
}
Expand Down Expand Up @@ -1932,6 +1933,13 @@ fn (t Tree) empty_stmt(node ast.EmptyStmt) &Node {
return obj
}

fn (t Tree) debugger_stmt(node ast.DebuggerStmt) &Node {
mut obj := new_object()
obj.add_terse('ast_type', t.string_node('DebuggerStmt'))
obj.add('pos', t.pos(node.pos))
return obj
}

fn (t Tree) nil_expr(node ast.Nil) &Node {
mut obj := new_object()
obj.add_terse('ast_type', t.string_node('Nil'))
Expand Down
3 changes: 2 additions & 1 deletion vlib/builtin/backtraces_nix.c.v
@@ -1,6 +1,7 @@
module builtin

fn print_backtrace_skipping_top_frames(xskipframes int) bool {
// print_backtrace_skipping_top_frames prints the backtrace skipping N top frames
pub fn print_backtrace_skipping_top_frames(xskipframes int) bool {
$if no_backtrace ? {
return false
} $else {
Expand Down
3 changes: 2 additions & 1 deletion vlib/builtin/backtraces_windows.c.v
Expand Up @@ -61,7 +61,8 @@ const symopt_include_32bit_modules = 0x00002000
const symopt_allow_zero_address = 0x01000000
const symopt_debug = u32(0x80000000)

fn print_backtrace_skipping_top_frames(skipframes int) bool {
// print_backtrace_skipping_top_frames prints the backtrace skipping N top frames
pub fn print_backtrace_skipping_top_frames(skipframes int) bool {
$if msvc {
return print_backtrace_skipping_top_frames_msvc(skipframes)
}
Expand Down
6 changes: 6 additions & 0 deletions vlib/v/ast/ast.v
Expand Up @@ -79,6 +79,7 @@ pub type Stmt = AsmStmt
| BranchStmt
| ComptimeFor
| ConstDecl
| DebuggerStmt
| DeferStmt
| EmptyStmt
| EnumDecl
Expand Down Expand Up @@ -1721,6 +1722,11 @@ pub const riscv_with_number_register_list = {
'a#': 8
}

pub struct DebuggerStmt {
pub:
pos token.Pos
}

// `assert a == 0, 'a is zero'`
@[minify]
pub struct AssertStmt {
Expand Down
14 changes: 14 additions & 0 deletions vlib/v/ast/scope.v
Expand Up @@ -182,6 +182,20 @@ pub fn (s &Scope) innermost(pos int) &Scope {
return s
}

// get_all_vars extracts all current scope vars
pub fn (s &Scope) get_all_vars() []ScopeObject {
mut scope_vars := []ScopeObject{}
for sc := unsafe { s }; true; sc = sc.parent {
if sc.objects.len > 0 {
scope_vars << sc.objects.values().filter(|it| it is Var)
}
if sc.dont_lookup_parent() {
break
}
}
return scope_vars
}

@[inline]
pub fn (s &Scope) contains(pos int) bool {
return pos >= s.start_pos && pos <= s.end_pos
Expand Down
1 change: 1 addition & 0 deletions vlib/v/checker/checker.v
Expand Up @@ -2000,6 +2000,7 @@ fn (mut c Checker) stmt(mut node ast.Stmt) {
}
}
ast.NodeError {}
ast.DebuggerStmt {}
ast.AsmStmt {
c.asm_stmt(mut node)
}
Expand Down
284 changes: 284 additions & 0 deletions vlib/v/debug/debug.v
@@ -0,0 +1,284 @@
// 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

import os
import math
import readline
import strings

__global g_debugger = Debugger{}

// Debugger holds the V debug information for REPL
@[heap]
struct Debugger {
mut:
is_tty bool = os.is_atty(0) > 0 // is tty?
exited bool // user exiting flag
last_cmd string // save the last cmd
last_args string // save the last args
watch_vars []string // save the watched vars
cmdline readline.Readline = readline.Readline{
completion_list: [
'anon?',
'bt',
'continue',
'generic?',
'heap',
'help',
'list',
'mem',
'memory',
'method?',
'mod',
'print',
'quit',
'scope',
'unwatch',
'watch',
]
}
}

// DebugContextVar holds the scope variable information
pub struct DebugContextVar {
name string // var name
typ string // its type name
value string // its str value
}

// DebugContextInfo has the context info for the debugger repl
pub struct DebugContextInfo {
is_anon bool // cur fn is anon?
is_generic bool // cur fn is a generic?
is_method bool // cur fn is a bool?
receiver_typ_name string // cur receiver type name (method only)
line int // cur line number
file string // cur file name
mod string // cur module name
fn_name string // cur function name
scope map[string]DebugContextVar // scope var data
}

fn flush_println(s string) {
println(s)
flush_stdout()
}

// show_variable prints the variable info if found into the cur context
fn (d DebugContextInfo) show_variable(var_name string, is_watch bool) {
if info := d.scope[var_name] {
flush_println('${var_name} = ${info.value} (${info.typ})')
} else if !is_watch {
eprintln('[error] var `${var_name}` not found')
}
}

fn (d DebugContextInfo) show_watched_vars(watch_vars []string) {
for var in watch_vars {
d.show_variable(var, true)
}
}

// show_scope prints the cur context scope variables
fn (d DebugContextInfo) show_scope() {
for k, v in d.scope {
flush_println('${k} = ${v.value} (${v.typ})')
}
}

// DebugContextInfo.ctx displays info about the current fn context
fn (d DebugContextInfo) ctx() string {
mut s := strings.new_builder(512)
if d.is_method {
s.write_string('[${d.mod}] (${d.receiver_typ_name}) ${d.fn_name}')
} else {
s.write_string('[${d.mod}] ${d.fn_name}')
}
return s.str()
}

// print_help prints the debugger REPL commands help
fn (mut d Debugger) print_help() {
println('vdbg commands:')
println(' anon?\t\t\tcheck if the current context is anon')
println(' bt\t\t\tprints a backtrace')
println(' c, continue\t\tcontinue debugging')
println(' generic?\t\tcheck if the current context is generic')
println(' heap\t\t\tshow heap memory usage')
println(' h, help, ?\t\tshow this help')
println(' l, list [lines]\tshow some lines from current break (default: 3)')
println(' mem, memory\t\tshow memory usage')
println(' method?\t\tcheck if the current context is a method')
println(' m, mod\t\tshow current module name')
println(' p, print <var>\tprints an variable')
println(' q, quit\t\texits debugging session in the code')
println(' scope\t\t\tshow the vars in the current scope')
println(' u, unwatch <var>\tunwatches a variable')
println(' w, watch <var>\twatches a variable')
flush_println('')
}

// read_line provides the user prompt based on tty flag
fn (mut d Debugger) read_line(prompt string) (string, bool) {
if d.is_tty {
mut is_ctrl := false
line := d.cmdline.read_line(prompt) or {
is_ctrl = true
''
}
return line.trim_right('\r\n '), is_ctrl
} else {
print(prompt)
flush_stdout()
return os.get_raw_line().trim_right('\r\n '), false
}
}

fn (mut d Debugger) parse_input(input string, is_ctrl bool) (string, string) {
splitted := input.split(' ')
if !is_ctrl && splitted[0] == '' {
return d.last_cmd, d.last_args
} else {
cmd := if is_ctrl { d.last_cmd } else { splitted[0] }
args := if splitted.len > 1 { splitted[1] } else { '' }
d.last_cmd = cmd
d.last_args = args
return cmd, args
}
}

// print_context_lines prints N lines before and after the current location
fn (mut d Debugger) print_context_lines(path string, line int, lines int) ! {
file_content := os.read_file(path)!
chunks := file_content.split('\n')
offset := math.max(line - lines, 1)
for n, s in chunks[offset - 1..math.min(chunks.len, line + lines)] {
ind := if n + offset == line { '>' } else { ' ' }
flush_println('${n + offset:04}${ind} ${s}')
}
}

// print_memory_use prints the GC memory use
fn (d &Debugger) print_memory_use() {
flush_println(gc_memory_use().str())
}

// print_heap_usage prints the GC heap usage
fn (d &Debugger) print_heap_usage() {
h := gc_heap_usage()
flush_println('heap size: ${h.heap_size}')
flush_println('free bytes: ${h.free_bytes}')
flush_println('total bytes: ${h.total_bytes}')
}

// watch_var adds a variable to watch_list
fn (mut d Debugger) watch_var(var string) bool {
if var !in d.watch_vars {
d.watch_vars << var
return true
}
return false
}

// unwatch_var removes a variable from watch list
fn (mut d Debugger) unwatch_var(var string) {
item := d.watch_vars.index(var)
if item >= 0 {
d.watch_vars.delete(item)
}
}

// interact displays the V debugger REPL for user interaction
@[markused]
pub fn (mut d Debugger) interact(info DebugContextInfo) ! {
if d.exited {
return
}

flush_println('Break on ${info.ctx()} in ${info.file}:${info.line}')
if d.watch_vars.len > 0 {
info.show_watched_vars(d.watch_vars)
}
for {
input, is_ctrl := d.read_line('${info.file}:${info.line} vdbg> ')
cmd, args := d.parse_input(input, is_ctrl)
match cmd {
'anon?' {
flush_println(info.is_anon.str())
}
'bt' {
print_backtrace_skipping_top_frames(2)
flush_stdout()
}
'c', 'continue' {
break
}
'generic?' {
flush_println(info.is_generic.str())
}
'heap' {
d.print_heap_usage()
}
'?', 'h', 'help' {
d.print_help()
}
'l', 'list' {
lines := if args != '' { args.int() } else { 3 }
if lines < 0 {
eprintln('[error] cannot use negative line amount')
} else {
d.print_context_lines(info.file, info.line, lines)!
}
}
'method?' {
flush_println(info.is_method.str())
}
'mem', 'memory' {
d.print_memory_use()
}
'm', 'mod' {
flush_println(info.mod)
}
'p', 'print' {
if args == '' {
eprintln('[error] var name is expected as parameter')
} else {
info.show_variable(args, false)
}
}
'scope' {
info.show_scope()
}
'q', 'quit' {
d.exited = true
break
}
'u', 'unwatch' {
if args == '' {
eprintln('[error] var name is expected as parameter')
} else {
d.unwatch_var(args)
}
}
'w', 'watch' {
if args == '' {
eprintln('[error] var name is expected as parameter')
} else {
if d.watch_var(args) {
info.show_variable(args, false)
}
}
}
'' {
if is_ctrl {
flush_println('')
}
}
else {
eprintln('unknown command `${cmd}`')
}
}
}
}

0 comments on commit 9f6448e

Please sign in to comment.