From 9f6448e30e3af6fddc4998652d27ad4e060fe2f3 Mon Sep 17 00:00:00 2001 From: Felipe Pena Date: Fri, 19 Jan 2024 02:10:17 -0300 Subject: [PATCH] v: $dbg statement - native V debugger REPL (#20533) --- cmd/tools/vast/vast.v | 8 + vlib/builtin/backtraces_nix.c.v | 3 +- vlib/builtin/backtraces_windows.c.v | 3 +- vlib/v/ast/ast.v | 6 + vlib/v/ast/scope.v | 14 + vlib/v/checker/checker.v | 1 + vlib/v/debug/debug.v | 284 +++++++++++++++++++ vlib/v/debug/interactive_test.v | 59 ++++ vlib/v/debug/tests/aggregate.expect | 9 + vlib/v/debug/tests/aggregate.vv | 30 ++ vlib/v/debug/tests/comptime_smartcast.expect | 10 + vlib/v/debug/tests/comptime_smartcast.vv | 12 + vlib/v/debug/tests/comptime_variant.expect | 8 + vlib/v/debug/tests/comptime_variant.vv | 15 + vlib/v/debug/tests/interface_var.expect | 8 + vlib/v/debug/tests/interface_var.vv | 22 ++ vlib/v/debug/tests/iteration.expect | 17 ++ vlib/v/debug/tests/iteration.vv | 6 + vlib/v/debug/tests/mut_arg.expect | 8 + vlib/v/debug/tests/mut_arg.vv | 18 ++ vlib/v/debug/tests/option_unwrap.expect | 9 + vlib/v/debug/tests/option_unwrap.vv | 10 + vlib/v/debug/tests/smartcast.expect | 8 + vlib/v/debug/tests/smartcast.vv | 9 + vlib/v/debug/tests/sumtype.expect | 10 + vlib/v/debug/tests/sumtype.vv | 31 ++ vlib/v/fmt/fmt.v | 7 + vlib/v/gen/c/cgen.v | 143 +++++++++- vlib/v/gen/golang/golang.v | 1 + vlib/v/gen/js/js.v | 2 + vlib/v/markused/walker.v | 1 + vlib/v/parser/parser.v | 27 +- vlib/v/transformer/transformer.v | 1 + 33 files changed, 786 insertions(+), 14 deletions(-) create mode 100644 vlib/v/debug/debug.v create mode 100644 vlib/v/debug/interactive_test.v create mode 100644 vlib/v/debug/tests/aggregate.expect create mode 100644 vlib/v/debug/tests/aggregate.vv create mode 100644 vlib/v/debug/tests/comptime_smartcast.expect create mode 100644 vlib/v/debug/tests/comptime_smartcast.vv create mode 100644 vlib/v/debug/tests/comptime_variant.expect create mode 100644 vlib/v/debug/tests/comptime_variant.vv create mode 100644 vlib/v/debug/tests/interface_var.expect create mode 100644 vlib/v/debug/tests/interface_var.vv create mode 100644 vlib/v/debug/tests/iteration.expect create mode 100644 vlib/v/debug/tests/iteration.vv create mode 100644 vlib/v/debug/tests/mut_arg.expect create mode 100644 vlib/v/debug/tests/mut_arg.vv create mode 100644 vlib/v/debug/tests/option_unwrap.expect create mode 100644 vlib/v/debug/tests/option_unwrap.vv create mode 100644 vlib/v/debug/tests/smartcast.expect create mode 100644 vlib/v/debug/tests/smartcast.vv create mode 100644 vlib/v/debug/tests/sumtype.expect create mode 100644 vlib/v/debug/tests/sumtype.vv diff --git a/cmd/tools/vast/vast.v b/cmd/tools/vast/vast.v index c3c85f6ad4..54edcc6ec8 100644 --- a/cmd/tools/vast/vast.v +++ b/cmd/tools/vast/vast.v @@ -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() } @@ -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')) diff --git a/vlib/builtin/backtraces_nix.c.v b/vlib/builtin/backtraces_nix.c.v index 322562a3e6..01cbe6a289 100644 --- a/vlib/builtin/backtraces_nix.c.v +++ b/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 { diff --git a/vlib/builtin/backtraces_windows.c.v b/vlib/builtin/backtraces_windows.c.v index f674d1baa0..23a8f2f819 100644 --- a/vlib/builtin/backtraces_windows.c.v +++ b/vlib/builtin/backtraces_windows.c.v @@ -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) } diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index a9b80b84fe..d8df01ec1a 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -79,6 +79,7 @@ pub type Stmt = AsmStmt | BranchStmt | ComptimeFor | ConstDecl + | DebuggerStmt | DeferStmt | EmptyStmt | EnumDecl @@ -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 { diff --git a/vlib/v/ast/scope.v b/vlib/v/ast/scope.v index 60ac0bb92e..635a84530b 100644 --- a/vlib/v/ast/scope.v +++ b/vlib/v/ast/scope.v @@ -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 diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 0800af2e18..2ef28628ed 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -2000,6 +2000,7 @@ fn (mut c Checker) stmt(mut node ast.Stmt) { } } ast.NodeError {} + ast.DebuggerStmt {} ast.AsmStmt { c.asm_stmt(mut node) } diff --git a/vlib/v/debug/debug.v b/vlib/v/debug/debug.v new file mode 100644 index 0000000000..160789e20c --- /dev/null +++ b/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 \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 \tunwatches a variable') + println(' w, watch \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}`') + } + } + } +} diff --git a/vlib/v/debug/interactive_test.v b/vlib/v/debug/interactive_test.v new file mode 100644 index 0000000000..19b4f23d02 --- /dev/null +++ b/vlib/v/debug/interactive_test.v @@ -0,0 +1,59 @@ +import os +import term +import time + +const vexe = @VEXE +const expect_tests_path = os.join_path(@VEXEROOT, 'vlib', 'v', 'debug', 'tests') +const test_module_path = os.join_path(os.vtmp_dir(), 'test_vdbg_input') +const bar = term.yellow('-'.repeat(100)) + +const expect_exe = os.quoted_path(os.find_abs_path_of_executable('expect') or { + eprintln('skipping test, since expect is missing') + exit(0) +}) + +fn testsuite_begin() { + os.chdir(@VEXEROOT) or {} + os.rmdir_all(test_module_path) or {} + os.mkdir_all(test_module_path) or {} + dump(test_module_path) +} + +fn gprintln(msg string) { + println(term.gray(msg)) +} + +fn test_debugger() { + os.chdir(test_module_path)! + all_expect_files := os.walk_ext(expect_tests_path, '.expect') + assert all_expect_files.len > 0, 'no .expect files found in ${expect_tests_path}' + for eidx, efile in all_expect_files { + vfile := efile.replace('.expect', '.vv') + output_file := os.join_path(test_module_path, os.file_name(efile).replace('.expect', + '.exe')) + + println(bar) + gprintln('>>>> running test [${eidx + 1}/${all_expect_files.len}], ${term.magenta(efile)} ...') + + compile_sw := time.new_stopwatch() + comp_res := os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(output_file)} ${os.quoted_path(vfile)}') + gprintln('>>>>>>>>>>> compilation took ${compile_sw.elapsed().milliseconds()} ms, comp_res: ${comp_res}') + + expect_cmd := '${expect_exe} -d -c "set stty_init {rows 24 cols 80}" -c "set timeout 15" ${os.quoted_path(efile)} ${os.quoted_path(output_file)} ${os.quoted_path(vfile)}' + println(term.cyan(expect_cmd)) + flush_stdout() + sw := time.new_stopwatch() + res := os.system(expect_cmd) + gprintln('>>>>>>>>>>> expect took: ${sw.elapsed().milliseconds()} ms, res: ${res}') + + if res != 0 { + assert false, term.red('failed expect cmd: ${expect_cmd}') + } + assert true + } + os.chdir(@VEXEROOT) or {} + os.rmdir_all(test_module_path) or {} + + println(bar) + gprintln(term.bold('A total of ${all_expect_files.len} tests for the debugger are OK')) +} diff --git a/vlib/v/debug/tests/aggregate.expect b/vlib/v/debug/tests/aggregate.expect new file mode 100644 index 0000000000..247a825c47 --- /dev/null +++ b/vlib/v/debug/tests/aggregate.expect @@ -0,0 +1,9 @@ +#!/usr/bin/env expect +spawn [lindex $argv 0] +set test_file [lindex $argv 1] + +expect -ex "\[${test_file}:25\] a.a: 123\r\n" +expect -ex "Break on \[main\] main in ${test_file}:26\r\n" +expect -ex "${test_file}:26 vdbg> " { send "p a\n" } timeout { exit 1 } +expect -ex "a = Test{\r\n a: 123\r\n} ((main.main.Test | main.main.Test2))" { send "q\n" } timeout { exit 1 } +expect eof diff --git a/vlib/v/debug/tests/aggregate.vv b/vlib/v/debug/tests/aggregate.vv new file mode 100644 index 0000000000..e4b06c1b9d --- /dev/null +++ b/vlib/v/debug/tests/aggregate.vv @@ -0,0 +1,30 @@ +struct Test { + a int +} + +struct Test2 { + a int +} + +struct Test3 { + a int +} + +interface ITest { + a int +} + +type TestSum = Test | Test2 | Test3 + +fn main() { + a := TestSum(Test{ + a: 123 + }) + match a { + Test, Test2 { + dump(a.a) + $dbg; + } + else {} + } +} diff --git a/vlib/v/debug/tests/comptime_smartcast.expect b/vlib/v/debug/tests/comptime_smartcast.expect new file mode 100644 index 0000000000..32110bd637 --- /dev/null +++ b/vlib/v/debug/tests/comptime_smartcast.expect @@ -0,0 +1,10 @@ +#!/usr/bin/env expect +spawn [lindex $argv 0] +set test_file [lindex $argv 1] + +expect -ex "Break on \[main\] comptime_smartcast in ${test_file}:3\r\n" +expect "${test_file}:3 vdbg> " { send "p v\n" } timeout { exit 1 } +expect "v = 1 (int)" { send "c\n" } timeout { exit 1 } +expect "${test_file}:5 vdbg> " { send "p v\n" } timeout { exit 1 } +expect "v = true (bool)" { send "q\n" } timeout { exit 1 } +expect eof diff --git a/vlib/v/debug/tests/comptime_smartcast.vv b/vlib/v/debug/tests/comptime_smartcast.vv new file mode 100644 index 0000000000..8d0a123821 --- /dev/null +++ b/vlib/v/debug/tests/comptime_smartcast.vv @@ -0,0 +1,12 @@ +fn comptime_smartcast[T](v T) { + $if v is int { + $dbg; + } $else { + $dbg; + } +} + +fn main() { + comptime_smartcast(1) + comptime_smartcast(true) +} diff --git a/vlib/v/debug/tests/comptime_variant.expect b/vlib/v/debug/tests/comptime_variant.expect new file mode 100644 index 0000000000..c686d1886c --- /dev/null +++ b/vlib/v/debug/tests/comptime_variant.expect @@ -0,0 +1,8 @@ +#!/usr/bin/env expect +spawn [lindex $argv 0] +set test_file [lindex $argv 1] + +expect -ex "Break on \[main\] comptime_variant_int in ${test_file}:7" +expect -ex "${test_file}:7 vdbg> " { send "p a\n" } timeout { exit 1 } +expect -ex "a = 0 (int)" { send "q\n" } timeout { exit 1 } +expect eof diff --git a/vlib/v/debug/tests/comptime_variant.vv b/vlib/v/debug/tests/comptime_variant.vv new file mode 100644 index 0000000000..a45c04a933 --- /dev/null +++ b/vlib/v/debug/tests/comptime_variant.vv @@ -0,0 +1,15 @@ +type MySum = int | string + +fn comptime_variant_int() { + a := MySum(int(0)) + $for v in MySum.variants { + if a is v { + $dbg; + dump(a) + } + } +} + +fn main() { + comptime_variant_int() +} diff --git a/vlib/v/debug/tests/interface_var.expect b/vlib/v/debug/tests/interface_var.expect new file mode 100644 index 0000000000..92be1eaaad --- /dev/null +++ b/vlib/v/debug/tests/interface_var.expect @@ -0,0 +1,8 @@ +#!/usr/bin/env expect +spawn [lindex $argv 0] +set test_file [lindex $argv 1] + +expect -ex "Break on \[main\] interface_var in ${test_file}:14\r\n" +expect -ex "${test_file}:14 vdbg> " { send "p a\n" } timeout { exit 1 } +expect -ex "a = Test{\r\n a: MySum(true)" { send "q\n" } timeout { exit 1 } +expect eof diff --git a/vlib/v/debug/tests/interface_var.vv b/vlib/v/debug/tests/interface_var.vv new file mode 100644 index 0000000000..6ca7ded01e --- /dev/null +++ b/vlib/v/debug/tests/interface_var.vv @@ -0,0 +1,22 @@ +interface ITest { + a MySum +} + +struct Test { + a MySum +} + +type MySum = bool | int + +fn interface_var(a ITest) { + match a { + Test { + $dbg; + } + else {} + } +} + +fn main() { + interface_var(Test{ a: true }) +} diff --git a/vlib/v/debug/tests/iteration.expect b/vlib/v/debug/tests/iteration.expect new file mode 100644 index 0000000000..562ba093a7 --- /dev/null +++ b/vlib/v/debug/tests/iteration.expect @@ -0,0 +1,17 @@ +#!/usr/bin/env expect +spawn [lindex $argv 0] +set test_file [lindex $argv 1] + +expect "0" +expect "1" +expect "2" +expect "4" +expect "${test_file}:3 vdbg> " { send "p x\n" } timeout { exit 1 } +expect "x = 5 (int literal)" { send "c\n"} timeout { exit 1 } +expect "${test_file}:3 vdbg> " { send "p x\n" } timeout { exit 1 } +expect "x = 6 (int literal)" { send "c\n"} timeout { exit 1 } +expect "${test_file}:3 vdbg> " { send "q\n" } timeout { exit 1 } +expect "7" +expect "8" +expect "9" +expect eof diff --git a/vlib/v/debug/tests/iteration.vv b/vlib/v/debug/tests/iteration.vv new file mode 100644 index 0000000000..57ed4096b0 --- /dev/null +++ b/vlib/v/debug/tests/iteration.vv @@ -0,0 +1,6 @@ +for x in 0 .. 10 { + if x > 4 { + $dbg; + } + println(x) +} diff --git a/vlib/v/debug/tests/mut_arg.expect b/vlib/v/debug/tests/mut_arg.expect new file mode 100644 index 0000000000..597e02ed3e --- /dev/null +++ b/vlib/v/debug/tests/mut_arg.expect @@ -0,0 +1,8 @@ +#!/usr/bin/env expect +spawn [lindex $argv 0] +set test_file [lindex $argv 1] + +expect -ex "Break on \[main\] mut_arg in ${test_file}:10\r\n" +expect "${test_file}:10 vdbg> " { send "p b\n" } timeout { exit 1 } +expect "b = foo (&main.Test)" { send "q\n" } timeout { exit 1 } +expect eof diff --git a/vlib/v/debug/tests/mut_arg.vv b/vlib/v/debug/tests/mut_arg.vv new file mode 100644 index 0000000000..4c4d27c16d --- /dev/null +++ b/vlib/v/debug/tests/mut_arg.vv @@ -0,0 +1,18 @@ +struct Test { + a string +} + +fn (t &Test) str() string { + return t.a +} + +fn test_mut(mut b Test) { + $dbg; +} + +fn main() { + mut a := Test{ + a: 'foo' + } + test_mut(mut a) +} diff --git a/vlib/v/debug/tests/option_unwrap.expect b/vlib/v/debug/tests/option_unwrap.expect new file mode 100644 index 0000000000..56ba47f756 --- /dev/null +++ b/vlib/v/debug/tests/option_unwrap.expect @@ -0,0 +1,9 @@ +#!/usr/bin/env expect +spawn [lindex $argv 0] +set test_file [lindex $argv 1] + +expect -ex "Break on \[main\] option_unwrap in ${test_file}:4\r\n" +expect -ex "${test_file}:4 vdbg> " { send "p a\n" } timeout { exit 1 } +expect -ex "a = Option(123) (?int)" { send "p b\n" } timeout { exit 1 } +expect -ex "b = 123 (int)" { send "q\n" } timeout { exit 1 } +expect eof diff --git a/vlib/v/debug/tests/option_unwrap.vv b/vlib/v/debug/tests/option_unwrap.vv new file mode 100644 index 0000000000..a18e500e68 --- /dev/null +++ b/vlib/v/debug/tests/option_unwrap.vv @@ -0,0 +1,10 @@ +fn option_unwrap() ? { + a := ?int(123) + b := a? + $dbg; + dump(b) +} + +fn main() { + option_unwrap()? +} diff --git a/vlib/v/debug/tests/smartcast.expect b/vlib/v/debug/tests/smartcast.expect new file mode 100644 index 0000000000..4d0fb5cf51 --- /dev/null +++ b/vlib/v/debug/tests/smartcast.expect @@ -0,0 +1,8 @@ +#!/usr/bin/env expect +spawn [lindex $argv 0] +set test_file [lindex $argv 1] + +expect -ex "Break on \[main\] smartcast in ${test_file}:3" +expect -ex "${test_file}:3 vdbg> " { send "p a\n" } timeout { exit 1 } +expect -ex "a = 1 (int)" { send "q\n" } timeout { exit 1 } +expect eof diff --git a/vlib/v/debug/tests/smartcast.vv b/vlib/v/debug/tests/smartcast.vv new file mode 100644 index 0000000000..2d689cbb3e --- /dev/null +++ b/vlib/v/debug/tests/smartcast.vv @@ -0,0 +1,9 @@ +fn smartcast(a ?int) { + if a != none { + $dbg; + } +} + +fn main() { + smartcast(1) +} diff --git a/vlib/v/debug/tests/sumtype.expect b/vlib/v/debug/tests/sumtype.expect new file mode 100644 index 0000000000..f24b4ab7d7 --- /dev/null +++ b/vlib/v/debug/tests/sumtype.expect @@ -0,0 +1,10 @@ +#!/usr/bin/env expect +spawn [lindex $argv 0] +set test_file [lindex $argv 1] + +expect -ex "Break on \[main\] sumtype in ${test_file}:17" +expect -ex "${test_file}:17 vdbg> " { send "p a\n" } timeout { exit 1 } +expect -ex "a = Test{\r\n a: MySum(false)\r\n} (main.Test)" { send "c\n" } timeout { exit 1 } +expect -ex "${test_file}:25 vdbg> " { send "p b\n" } timeout { exit 1 } +expect -ex "b = Test{\r\n a: MySum(1)\r\n} (main.Test)" { send "q\n" } timeout { exit 1 } +expect eof diff --git a/vlib/v/debug/tests/sumtype.vv b/vlib/v/debug/tests/sumtype.vv new file mode 100644 index 0000000000..66ffe8bcfd --- /dev/null +++ b/vlib/v/debug/tests/sumtype.vv @@ -0,0 +1,31 @@ +struct Test { + a MySum +} + +struct Test2 { + a ?MySum +} + +type MySum = Test | bool | int + +fn sumtype() { + a := MySum(Test{ + a: false + }) + if a is Test { + dump(a) + $dbg; + } + + b := ?MySum(Test{ + a: 1 + }) + if b is Test { + dump(b) + $dbg; + } +} + +fn main() { + sumtype() +} diff --git a/vlib/v/fmt/fmt.v b/vlib/v/fmt/fmt.v index 2f450b329c..591588f938 100644 --- a/vlib/v/fmt/fmt.v +++ b/vlib/v/fmt/fmt.v @@ -513,6 +513,9 @@ pub fn (mut f Fmt) stmt(node ast.Stmt) { ast.ConstDecl { f.const_decl(node) } + ast.DebuggerStmt { + f.debugger_stmt(node) + } ast.DeferStmt { f.defer_stmt(node) } @@ -874,6 +877,10 @@ pub fn (mut f Fmt) block(node ast.Block) { f.writeln('}') } +pub fn (mut f Fmt) debugger_stmt(node ast.DebuggerStmt) { + f.writeln('\$dbg;') +} + pub fn (mut f Fmt) branch_stmt(node ast.BranchStmt) { f.writeln(node.str()) } diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 2e6a8888aa..174023e59e 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -240,10 +240,10 @@ mut: ///////// // out_parallel []strings.Builder // out_idx int - out_fn_start_pos []int // for generating multiple .c files, stores locations of all fn positions in `out` string builder - static_modifier string // for parallel_cc - - has_reflection bool + out_fn_start_pos []int // for generating multiple .c files, stores locations of all fn positions in `out` string builder + static_modifier string // for parallel_cc + has_reflection bool // v.reflection has been imported + has_debugger bool // $dbg has been used in the code reflection_strings &map[string]int defer_return_tmp_var string vweb_filter_fn_name string // vweb__filter or x__vweb__filter, used by $vweb.html() for escaping strings in the templates, depending on which `vweb` import is used @@ -319,6 +319,7 @@ pub fn gen(files []&ast.File, table &ast.Table, pref_ &pref.Preferences) (string || pref_.os in [.wasm32, .wasm32_emscripten]) static_modifier: if pref_.parallel_cc { 'static' } else { '' } has_reflection: 'v.reflection' in table.modules + has_debugger: 'v.debug' in table.modules reflection_strings: &reflection_strings } @@ -687,6 +688,7 @@ fn cgen_process_one_file_cb(mut p pool.PoolProcessor, idx int, wid int) &Gen { is_cc_msvc: global_g.is_cc_msvc use_segfault_handler: global_g.use_segfault_handler has_reflection: 'v.reflection' in global_g.table.modules + has_debugger: 'v.debug' in global_g.table.modules reflection_strings: global_g.reflection_strings } g.comptime = &comptime.ComptimeInfo{ @@ -2071,6 +2073,9 @@ fn (mut g Gen) stmt(node ast.Stmt) { ast.ComptimeFor { g.comptime_for(node) } + ast.DebuggerStmt { + g.debugger_stmt(node) + } ast.DeferStmt { mut defer_stmt := node defer_stmt.ifdef = g.defer_ifdef @@ -3910,6 +3915,133 @@ fn (mut g Gen) selector_expr(node ast.SelectorExpr) { } } +// debugger_stmt writes the call to V debugger REPL +fn (mut g Gen) debugger_stmt(node ast.DebuggerStmt) { + paline, pafile, pamod, pafn := g.panic_debug_info(node.pos) + is_anon := g.cur_fn != unsafe { nil } && g.cur_fn.is_anon + is_generic := g.cur_fn != unsafe { nil } && g.cur_fn.generic_names.len > 0 + is_method := g.cur_fn != unsafe { nil } && g.cur_fn.is_method + receiver_type := if g.cur_fn != unsafe { nil } && g.cur_fn.is_method { + g.table.type_to_str(g.cur_fn.receiver.typ) + } else { + '' + } + scope_vars := g.file.scope.innermost(node.pos.pos).get_all_vars() + + // prepares the map containing the scope variable infos + mut vars := []string{} + mut keys := strings.new_builder(100) + mut values := strings.new_builder(100) + mut count := 1 + for _, obj in scope_vars { + if obj.name !in vars { + if obj is ast.Var && obj.pos.pos < node.pos.pos { + keys.write_string('_SLIT("${obj.name}")') + var_typ := if obj.smartcasts.len > 0 { obj.smartcasts.last() } else { obj.typ } + values.write_string('{.typ=_SLIT("${g.table.type_to_str(g.unwrap_generic(var_typ))}"),.value=') + obj_sym := g.table.sym(obj.typ) + cast_sym := g.table.sym(var_typ) + + mut param_var := strings.new_builder(50) + if obj.smartcasts.len > 0 { + is_option_unwrap := obj.typ.has_flag(.option) + && var_typ == obj.typ.clear_flag(.option) + is_option := obj.typ.has_flag(.option) + mut opt_cast := false + mut func := if cast_sym.info is ast.Aggregate { + '' + } else { + g.get_str_fn(var_typ) + } + + param_var.write_string('(') + if obj_sym.kind == .sum_type && !obj.is_auto_heap { + if is_option { + if !is_option_unwrap { + param_var.write_string('*(') + } + styp := g.base_type(obj.typ) + param_var.write_string('*(${styp}*)') + opt_cast = true + } else { + param_var.write_string('*') + } + } else if g.table.is_interface_var(obj) || obj.ct_type_var == .smartcast { + param_var.write_string('*') + } else if is_option { + opt_cast = true + param_var.write_string('*(${g.base_type(obj.typ)}*)') + } + + dot := if obj.orig_type.is_ptr() || obj.is_auto_heap { '->' } else { '.' } + if obj.ct_type_var == .smartcast { + cur_variant_sym := g.table.sym(g.unwrap_generic(g.comptime.type_map['${g.comptime.comptime_for_variant_var}.typ'])) + param_var.write_string('${obj.name}${dot}_${cur_variant_sym.cname}') + } else if cast_sym.info is ast.Aggregate { + sym := g.table.sym(cast_sym.info.types[g.aggregate_type_idx]) + func = g.get_str_fn(cast_sym.info.types[g.aggregate_type_idx]) + param_var.write_string('${obj.name}${dot}_${sym.cname}') + } else if obj_sym.kind == .interface_ && cast_sym.kind == .interface_ { + ptr := '*'.repeat(obj.typ.nr_muls()) + param_var.write_string('I_${obj_sym.cname}_as_I_${cast_sym.cname}(${ptr}${obj.name})') + } else if obj_sym.kind in [.sum_type, .interface_] { + param_var.write_string('${obj.name}') + if opt_cast { + param_var.write_string('.data)') + } + param_var.write_string('${dot}_${cast_sym.cname}') + } else if obj.typ.has_flag(.option) && !var_typ.has_flag(.option) { + param_var.write_string('${obj.name}.data') + } else { + param_var.write_string('${obj.name}') + } + param_var.write_string(')') + + values.write_string('${func}(${param_var.str()})}') + } else { + func := g.get_str_fn(var_typ) + if obj.typ.has_flag(.option) && !var_typ.has_flag(.option) { + // option unwrap + base_typ := g.base_type(obj.typ) + values.write_string('${func}(*(${base_typ}*)${obj.name}.data)}') + } else { + _, str_method_expects_ptr, _ := cast_sym.str_method_info() + deref := if str_method_expects_ptr && !obj.typ.is_ptr() { + '&' + } else if obj.typ.is_ptr() && !obj.is_auto_deref { + '&' + } else if obj.typ.is_ptr() && obj.is_auto_deref { + '' + } else if obj.is_auto_heap + || (!var_typ.has_flag(.option) && var_typ.is_ptr()) { + '*' + } else { + '' + } + values.write_string('${func}(${deref}${obj.name})}') + } + } + vars << obj.name + if count != scope_vars.len { + keys.write_string(',') + values.write_string(',') + } + } + } + count += 1 + } + g.writeln('{') + g.writeln('\tMap_string_string _scope = new_map_init(&map_hash_string, &map_eq_string, &map_clone_string, &map_free_string, ${vars.len}, sizeof(string), sizeof(v__debug__DebugContextVar),') + g.write('\t\t_MOV((string[${vars.len}]){') + g.write(keys.str()) + g.writeln('}),') + g.write('\t\t_MOV((v__debug__DebugContextVar[${vars.len}]){') + g.write(values.str()) + g.writeln('}));') + g.writeln('\tv__debug__Debugger_interact(&g_debugger, (v__debug__DebugContextInfo){.is_anon=${is_anon},.is_generic=${is_generic},.is_method=${is_method},.receiver_typ_name=_SLIT("${receiver_type}"),.line=${paline},.file=_SLIT("${pafile}"),.mod=_SLIT("${pamod}"),.fn_name=_SLIT("${pafn}"),.scope=_scope});') + g.write('}') +} + fn (mut g Gen) enum_decl(node ast.EnumDecl) { enum_name := util.no_dots(node.name) is_flag := node.is_flag @@ -5948,12 +6080,11 @@ fn (mut g Gen) write_init_function() { g.gen_reflection_data() } } - mut cleaning_up_array := []string{cap: g.table.modules.len} for mod_name in g.table.modules { if g.has_reflection && mod_name == 'v.reflection' { - // ignore v.reflection already initialized above + // ignore v.reflection and v.debug already initialized above continue } mut const_section_header_shown := false diff --git a/vlib/v/gen/golang/golang.v b/vlib/v/gen/golang/golang.v index dedf067717..a163a14e02 100644 --- a/vlib/v/gen/golang/golang.v +++ b/vlib/v/gen/golang/golang.v @@ -425,6 +425,7 @@ pub fn (mut f Gen) stmt(node ast.Stmt) { ast.ConstDecl { f.const_decl(node) } + ast.DebuggerStmt {} ast.DeferStmt { f.defer_stmt(node) } diff --git a/vlib/v/gen/js/js.v b/vlib/v/gen/js/js.v index e31a015e0c..250b4fdd54 100644 --- a/vlib/v/gen/js/js.v +++ b/vlib/v/gen/js/js.v @@ -668,6 +668,7 @@ fn (mut g JsGen) stmt_no_semi(node_ ast.Stmt) { g.write_v_source_line_info(node.pos) g.gen_const_decl(node) } + ast.DebuggerStmt {} ast.DeferStmt { g.defer_stmts << node } @@ -775,6 +776,7 @@ fn (mut g JsGen) stmt(node_ ast.Stmt) { g.write_v_source_line_info(node.pos) g.gen_const_decl(node) } + ast.DebuggerStmt {} ast.DeferStmt { g.defer_stmts << node } diff --git a/vlib/v/markused/walker.v b/vlib/v/markused/walker.v index d402a551e3..09a5d20116 100644 --- a/vlib/v/markused/walker.v +++ b/vlib/v/markused/walker.v @@ -101,6 +101,7 @@ pub fn (mut w Walker) stmt(node_ ast.Stmt) { mut node := unsafe { node_ } match mut node { ast.EmptyStmt {} + ast.DebuggerStmt {} ast.AsmStmt { w.asm_io(node.output) w.asm_io(node.input) diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 2c36936535..576c42b4e9 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -1058,12 +1058,17 @@ fn (mut p Parser) stmt(is_top_level bool) ast.Stmt { return p.comptime_for() } .name { - mut pos := p.tok.pos() - expr := p.expr(0) - pos.update_last_line(p.prev_tok.line_nr) - return ast.ExprStmt{ - expr: expr - pos: pos + // handles $dbg directly without registering token + if p.peek_tok.lit == 'dbg' { + return p.dbg_stmt() + } else { + mut pos := p.tok.pos() + expr := p.expr(0) + pos.update_last_line(p.prev_tok.line_nr) + return ast.ExprStmt{ + expr: expr + pos: pos + } } } else { @@ -1149,6 +1154,16 @@ fn (mut p Parser) stmt(is_top_level bool) ast.Stmt { } } +fn (mut p Parser) dbg_stmt() ast.DebuggerStmt { + pos := p.tok.pos() + p.check(.dollar) + p.check(.name) + p.register_auto_import('v.debug') + return ast.DebuggerStmt{ + pos: pos + } +} + fn (mut p Parser) semicolon_stmt() ast.SemicolonStmt { pos := p.tok.pos() p.check(.semicolon) diff --git a/vlib/v/transformer/transformer.v b/vlib/v/transformer/transformer.v index c4959e9f53..3f6565b333 100644 --- a/vlib/v/transformer/transformer.v +++ b/vlib/v/transformer/transformer.v @@ -178,6 +178,7 @@ pub fn (mut t Transformer) stmt(mut node ast.Stmt) ast.Stmt { ast.EmptyStmt {} ast.NodeError {} ast.AsmStmt {} + ast.DebuggerStmt {} ast.AssertStmt { return t.assert_stmt(mut node) }