mirror of
https://github.com/bearlanguageorg/bear.git
synced 2026-08-26 14:57:18 +00:00
Debugger
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
// dbg.v — the interactive debugger, enabled by `vr debug`.
|
||||
//
|
||||
// While enabled the VM checks dbg_tick before every instruction. When a
|
||||
// breakpoint is hit (or a step/next/finish condition is met) it drops into
|
||||
// dbg_session, a small command loop over stdin:
|
||||
//
|
||||
// c | continue resume until the next breakpoint (or the end)
|
||||
// s | step execute one instruction
|
||||
// n | next run to the next line of the current frame (skip calls)
|
||||
// f | finish run until the current function returns
|
||||
// b [line] set a breakpoint on a source line (no arg: list them)
|
||||
// d <n> delete breakpoint n (1-based)
|
||||
// p <name> print a local variable's value
|
||||
// l | locals list the current function's locals and their values
|
||||
// bt | stack print the call chain
|
||||
// q | quit abort the program
|
||||
//
|
||||
// Reading EOF (e.g. piped input) is treated as `continue`, so scripts can
|
||||
// drive a session non-interactively.
|
||||
module vm
|
||||
|
||||
import os
|
||||
|
||||
// dbg_tick runs once per instruction while the debugger is enabled and
|
||||
// decides whether to stop and open an interactive session.
|
||||
fn (mut v Vm) dbg_tick() ! {
|
||||
line := v.line_at(v.ip)
|
||||
mut stop := false
|
||||
match v.dbg.mode {
|
||||
.step {
|
||||
stop = true
|
||||
}
|
||||
.next {
|
||||
// run until the line changes while still at (or above) the frame
|
||||
// level where next began — calls push deeper frames, which we skip
|
||||
stop = v.bp <= v.dbg.start_bp && line != v.dbg.last_line
|
||||
}
|
||||
.finish {
|
||||
// run until the current frame returns to its caller
|
||||
stop = v.bp < v.dbg.start_bp
|
||||
}
|
||||
.run {
|
||||
// stop at the first instruction of a breakpoint line only, so a
|
||||
// multi-instruction line does not re-trigger mid-line
|
||||
if line in v.dbg.breakpoints {
|
||||
first := v.ip == 0 || v.line_at(v.ip - 1) != line
|
||||
stop = first
|
||||
}
|
||||
}
|
||||
}
|
||||
if stop {
|
||||
v.dbg_session()!
|
||||
}
|
||||
}
|
||||
|
||||
// dbg_session is the interactive command loop. It returns when the user
|
||||
// chooses a resume mode (continue/step/next/finish) or quits.
|
||||
fn (mut v Vm) dbg_session() ! {
|
||||
v.dbg.last_line = v.line_at(v.ip)
|
||||
println('')
|
||||
println('== stopped at ${v.func_at(v.ip)} (line ${v.dbg.last_line}, ip ${v.ip}) — help: h')
|
||||
for {
|
||||
input := os.input_opt('(vr-dbg) ') or { 'c' } // EOF → continue
|
||||
parts := input.trim_space().split(' ')
|
||||
cmd := parts[0]
|
||||
arg := if parts.len > 1 { parts[1] } else { '' }
|
||||
match cmd {
|
||||
'c', 'continue', '' {
|
||||
v.dbg.mode = .run
|
||||
return
|
||||
}
|
||||
's', 'step' {
|
||||
v.dbg.mode = .step
|
||||
return
|
||||
}
|
||||
'n', 'next' {
|
||||
v.dbg.mode = .next
|
||||
v.dbg.start_bp = v.bp
|
||||
return
|
||||
}
|
||||
'f', 'finish' {
|
||||
v.dbg.mode = .finish
|
||||
v.dbg.start_bp = v.bp
|
||||
return
|
||||
}
|
||||
'b', 'break' {
|
||||
if arg == '' {
|
||||
if v.dbg.breakpoints.len == 0 {
|
||||
println(' no breakpoints set')
|
||||
} else {
|
||||
for i, bp in v.dbg.breakpoints {
|
||||
println(' ${i + 1}: line ${bp}')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
line := arg.int()
|
||||
if line <= 0 {
|
||||
println(' usage: b <line>')
|
||||
} else if line !in v.dbg.breakpoints {
|
||||
v.dbg.breakpoints << line
|
||||
println(' breakpoint set at line ${line}')
|
||||
}
|
||||
}
|
||||
}
|
||||
'd', 'delete' {
|
||||
n := arg.int()
|
||||
if n >= 1 && n <= v.dbg.breakpoints.len {
|
||||
v.dbg.breakpoints.delete(n - 1)
|
||||
println(' breakpoint ${n} deleted')
|
||||
} else {
|
||||
println(' usage: d <n> (see `b` for the list)')
|
||||
}
|
||||
}
|
||||
'p', 'print' {
|
||||
if arg == '' {
|
||||
println(' usage: p <name>')
|
||||
} else {
|
||||
v.dbg_print_local(arg)
|
||||
}
|
||||
}
|
||||
'l', 'locals' {
|
||||
v.dbg_list_locals()
|
||||
}
|
||||
'bt', 'stack', 'backtrace' {
|
||||
println(v.stack_trace())
|
||||
}
|
||||
'h', 'help' {
|
||||
println(' c continue · s step · n next · f finish · b [line] · d <n>')
|
||||
println(' p <name> · l locals · bt stack · q quit')
|
||||
}
|
||||
'q', 'quit', 'exit' {
|
||||
v.halted = true
|
||||
return
|
||||
}
|
||||
else {
|
||||
println(' unknown command "${cmd}" — h for help')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dbg_print_local prints the value of one local variable of the current
|
||||
// function, resolving its slot from the debug locals table.
|
||||
fn (mut v Vm) dbg_print_local(name string) {
|
||||
fn_name := v.func_at(v.ip)
|
||||
slot := v.dbg_slot(fn_name, name)
|
||||
if slot < 0 {
|
||||
println(' no local "${name}" in ${fn_name}')
|
||||
return
|
||||
}
|
||||
println(' ${name} = ${v.val_str(v.stack[v.bp + slot], 0)}')
|
||||
}
|
||||
|
||||
// dbg_list_locals prints every named local of the current function with its
|
||||
// current value.
|
||||
fn (mut v Vm) dbg_list_locals() {
|
||||
fn_name := v.func_at(v.ip)
|
||||
mut found := false
|
||||
for l in v.dbg_locals {
|
||||
if l.fn == fn_name {
|
||||
found = true
|
||||
println(' ${l.name} = ${v.val_str(v.stack[v.bp + l.slot], 0)} (slot ${l.slot})')
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
println(' (no named locals for ${fn_name})')
|
||||
}
|
||||
}
|
||||
|
||||
// dbg_slot finds the stack slot of a local by (function, name), or -1.
|
||||
fn (v Vm) dbg_slot(fn_name string, name string) int {
|
||||
for l in v.dbg_locals {
|
||||
if l.fn == fn_name && l.name == name {
|
||||
return l.slot
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
+44
-10
@@ -236,16 +236,11 @@ fn (mut v Vm) native(id int, _argc int) ! {
|
||||
return error('sort() expects an array')
|
||||
}
|
||||
mut a := v.arrays[v.hand(h)]
|
||||
// insertion sort by numeric value
|
||||
for i in 1..a.len {
|
||||
key := a[i]
|
||||
mut j := i - 1
|
||||
for j >= 0 && v.num_gt(a[j], key) {
|
||||
a[j + 1] = a[j]
|
||||
j--
|
||||
}
|
||||
a[j + 1] = key
|
||||
}
|
||||
// merge sort: guaranteed O(n log n) (insertion sort was O(n^2) on
|
||||
// large or reversed inputs) and stable, so equal elements keep
|
||||
// their original order
|
||||
mut tmp := []i64{len: a.len}
|
||||
v.merge_sort(mut a, mut tmp, 0, a.len)
|
||||
v.push(h)!
|
||||
}
|
||||
native_clone {
|
||||
@@ -1099,6 +1094,45 @@ fn (mut v Vm) str_method(name string, argc int) ! {
|
||||
return error('unknown string method "${name}"')
|
||||
}
|
||||
|
||||
// merge_sort sorts a[lo..hi) ascending by numeric value, using tmp as the
|
||||
// scratch buffer (must be at least hi long). It is stable: equal elements
|
||||
// keep their relative order.
|
||||
fn (mut v Vm) merge_sort(mut a []i64, mut tmp []i64, lo int, hi int) {
|
||||
if hi - lo <= 1 {
|
||||
return
|
||||
}
|
||||
mid := lo + (hi - lo) / 2
|
||||
v.merge_sort(mut a, mut tmp, lo, mid)
|
||||
v.merge_sort(mut a, mut tmp, mid, hi)
|
||||
mut i := lo
|
||||
mut j := mid
|
||||
mut k := lo
|
||||
for i < mid && j < hi {
|
||||
if !v.num_gt(a[i], a[j]) {
|
||||
// a[i] <= a[j]: take from the left half (equal -> left, so stable)
|
||||
tmp[k] = a[i]
|
||||
i++
|
||||
} else {
|
||||
tmp[k] = a[j]
|
||||
j++
|
||||
}
|
||||
k++
|
||||
}
|
||||
for i < mid {
|
||||
tmp[k] = a[i]
|
||||
i++
|
||||
k++
|
||||
}
|
||||
for j < hi {
|
||||
tmp[k] = a[j]
|
||||
j++
|
||||
k++
|
||||
}
|
||||
for x in lo..hi {
|
||||
a[x] = tmp[x]
|
||||
}
|
||||
}
|
||||
|
||||
// num_gt compares two values by their numeric value (int or float).
|
||||
fn (mut v Vm) num_gt(x i64, y i64) bool {
|
||||
if v.is_float(x) || v.is_float(y) {
|
||||
|
||||
+32
@@ -34,6 +34,30 @@ struct Handler {
|
||||
sp int // stack pointer right after the handler record
|
||||
}
|
||||
|
||||
// DbgMode says what the debugger should do after an interactive session ends.
|
||||
enum DbgMode {
|
||||
run // keep going until the next breakpoint (or the end)
|
||||
step // stop at the very next instruction
|
||||
next // stop after the current line returns to this frame level
|
||||
finish // stop when the current function returns
|
||||
}
|
||||
|
||||
// DbgState is the interactive debugger's runtime state, checked once per
|
||||
// instruction while enabled.
|
||||
struct DbgState {
|
||||
mut:
|
||||
enabled bool
|
||||
breakpoints []int // source lines to stop at (first instruction of the line)
|
||||
mode DbgMode
|
||||
start_bp int // frame base captured when next/finish began
|
||||
last_line int // line at the moment the session stopped
|
||||
}
|
||||
|
||||
struct FnEntry {
|
||||
idx int
|
||||
entry int
|
||||
}
|
||||
|
||||
struct Vm {
|
||||
mut:
|
||||
code []u8
|
||||
@@ -57,6 +81,14 @@ mut:
|
||||
const_strs int // strings[0..const_strs] are bytecode constants, never collected
|
||||
last_heap int // heap size at the last GC check (allocation trigger)
|
||||
build_root string // directory of the .vrmm build module (build_root() builtin)
|
||||
dbg DbgState // interactive debugger state (vr debug)
|
||||
dbg_locals []obj.DbgLocal // local name -> slot per function (debugger)
|
||||
profiling bool // instruction/call counting (vr run --profile)
|
||||
prof_instr []u64 // instructions executed per function index
|
||||
prof_calls []u64 // calls made per function index
|
||||
fn_of_ip []int // code offset -> function index (for profiling)
|
||||
max_ops i64 // instruction budget; 0 = unlimited (fuzzing safety)
|
||||
ops i64 // instructions executed so far
|
||||
}
|
||||
|
||||
fn bool_i64(b bool) i64 {
|
||||
|
||||
@@ -8,6 +8,19 @@ module vm
|
||||
import obj
|
||||
import math
|
||||
|
||||
// RunOpts configures a VM run: tracing, program arguments, the build root,
|
||||
// the interactive debugger, an instruction budget, and profiling.
|
||||
pub struct RunOpts {
|
||||
pub:
|
||||
trace bool
|
||||
args []string = []
|
||||
root string
|
||||
debug bool // start the interactive debugger (vr debug)
|
||||
breakpoints []int // source lines to stop at; empty + debug = stop at entry
|
||||
max_ops i64 // instruction budget; 0 = unlimited
|
||||
profile bool // count instructions/calls per function
|
||||
}
|
||||
|
||||
// run executes the function named `entry` from the executable `bin` and
|
||||
// returns its return value (0 if it never returns one).
|
||||
pub fn run(bin obj.Bin, entry string, trace bool) !i64 {
|
||||
@@ -17,27 +30,95 @@ pub fn run(bin obj.Bin, entry string, trace bool) !i64 {
|
||||
// run_with_args is run() with command-line arguments exposed to the program
|
||||
// via the `args()` builtin.
|
||||
pub fn run_with_args(bin obj.Bin, entry string, trace bool, args []string) !i64 {
|
||||
return run_internal(bin, entry, trace, args, '')!
|
||||
return run_opts(bin, entry, RunOpts{ trace: trace, args: args })!
|
||||
}
|
||||
|
||||
// run_build executes a .vrmm build module: the entry target receives the
|
||||
// extra CLI arguments via `args()`, and `build_root()` reports the module's
|
||||
// own directory so scripts can find files regardless of the working directory.
|
||||
pub fn run_build(bin obj.Bin, entry string, args []string, root string) !i64 {
|
||||
return run_internal(bin, entry, false, args, root)!
|
||||
return run_opts(bin, entry, RunOpts{ args: args, root: root })!
|
||||
}
|
||||
|
||||
fn run_internal(bin obj.Bin, entry string, trace bool, args []string, root string) !i64 {
|
||||
// run_opts runs the program with full control over the runtime options.
|
||||
pub fn run_opts(bin obj.Bin, entry string, opts RunOpts) !i64 {
|
||||
mut v := new_vm(bin, entry, opts)!
|
||||
return v.run_result()!
|
||||
}
|
||||
|
||||
// run_debug runs the program under the interactive debugger, stopping at the
|
||||
// given source-line breakpoints (or at entry when none are given).
|
||||
pub fn run_debug(bin obj.Bin, entry string, breakpoints []int, args []string) !i64 {
|
||||
return run_opts(bin, entry, RunOpts{ args: args, debug: true, breakpoints: breakpoints })!
|
||||
}
|
||||
|
||||
// ProfileRow is one function's profile totals.
|
||||
pub struct ProfileRow {
|
||||
pub:
|
||||
name string
|
||||
calls u64
|
||||
instr u64
|
||||
}
|
||||
|
||||
// ProfileReport is the result of a profiled run: per-function instruction
|
||||
// and call counts, sorted by instructions executed (hot first).
|
||||
pub struct ProfileReport {
|
||||
pub:
|
||||
rows []ProfileRow
|
||||
total u64 // instructions executed across all functions
|
||||
}
|
||||
|
||||
// run_profiled executes the program counting instructions and calls per
|
||||
// function, and returns the report.
|
||||
pub fn run_profiled(bin obj.Bin, entry string, args []string) !ProfileReport {
|
||||
mut v := new_vm(bin, entry, RunOpts{ args: args, profile: true })!
|
||||
_ = v.run_result()!
|
||||
mut rows := []ProfileRow{}
|
||||
for i in 0..v.fns.len {
|
||||
rows << ProfileRow{ name: v.fns[i].name, calls: v.prof_calls[i], instr: v.prof_instr[i] }
|
||||
}
|
||||
rows.sort_with_compare(fn (a &ProfileRow, b &ProfileRow) int {
|
||||
if a.instr > b.instr {
|
||||
return -1
|
||||
}
|
||||
if a.instr < b.instr {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
mut total := u64(0)
|
||||
for r in rows {
|
||||
total += r.instr
|
||||
}
|
||||
return ProfileReport{ rows: rows, total: total }
|
||||
}
|
||||
|
||||
// new_vm builds a configured Vm for the entry function, pushing the synthetic
|
||||
// entry frame and pointing ip at the entry point.
|
||||
fn new_vm(bin obj.Bin, entry string, opts RunOpts) !Vm {
|
||||
mut v := Vm{
|
||||
code: bin.code
|
||||
strings: bin.strings.clone()
|
||||
stack: []i64{len: stack_cap}
|
||||
trace: trace
|
||||
prog_args: args
|
||||
trace: opts.trace
|
||||
prog_args: opts.args
|
||||
lines: bin.lines
|
||||
fns: bin.fns
|
||||
const_strs: bin.strings.len
|
||||
build_root: root
|
||||
build_root: opts.root
|
||||
dbg_locals: bin.locals
|
||||
max_ops: opts.max_ops
|
||||
}
|
||||
if opts.profile {
|
||||
v.profiling = true
|
||||
v.prof_instr = []u64{len: v.fns.len}
|
||||
v.prof_calls = []u64{len: v.fns.len}
|
||||
v.fn_of_ip = v.build_fn_of_ip()
|
||||
}
|
||||
if opts.debug {
|
||||
v.dbg.enabled = true
|
||||
v.dbg.breakpoints = opts.breakpoints.clone()
|
||||
v.dbg.mode = if opts.breakpoints.len > 0 { DbgMode.run } else { DbgMode.step }
|
||||
}
|
||||
mut entry_ip := -1
|
||||
for f in bin.fns {
|
||||
@@ -61,6 +142,11 @@ fn run_internal(bin obj.Bin, entry string, trace bool, args []string, root strin
|
||||
v.sp++
|
||||
v.bp = v.sp
|
||||
v.ip = entry_ip
|
||||
return v
|
||||
}
|
||||
|
||||
// run_result executes until halt/error and extracts the program's result.
|
||||
fn (mut v Vm) run_result() !i64 {
|
||||
v.exec() or {
|
||||
return error('${err.msg()} at ${v.where()}\n${v.stack_trace()}')
|
||||
}
|
||||
@@ -73,6 +159,29 @@ fn run_internal(bin obj.Bin, entry string, trace bool, args []string, root strin
|
||||
return 0
|
||||
}
|
||||
|
||||
// build_fn_of_ip precomputes, for every code offset, the index of the
|
||||
// function that contains it, so profiling adds one array lookup per opcode.
|
||||
// Function tables are not guaranteed to be in entry order (the linker builds
|
||||
// them from a map), so entries are sorted by offset first.
|
||||
fn (v Vm) build_fn_of_ip() []int {
|
||||
mut out := []int{len: v.code.len}
|
||||
mut fes := []FnEntry{}
|
||||
for i, f in v.fns {
|
||||
fes << FnEntry{ idx: i, entry: f.entry }
|
||||
}
|
||||
fes.sort_with_compare(fn (a &FnEntry, b &FnEntry) int {
|
||||
return a.entry - b.entry
|
||||
})
|
||||
mut fi := 0
|
||||
for ip in 0..v.code.len {
|
||||
for fi + 1 < fes.len && fes[fi + 1].entry <= ip {
|
||||
fi++
|
||||
}
|
||||
out[ip] = fes[fi].idx
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// where returns a source-level location for the current instruction pointer:
|
||||
// `line 12 (ip 345)` when debug info is available, otherwise just `(ip 345)`.
|
||||
fn (v Vm) where() string {
|
||||
@@ -145,6 +254,18 @@ fn (mut v Vm) exec() ! {
|
||||
if v.trace {
|
||||
v.trace_op(op)
|
||||
}
|
||||
if v.profiling {
|
||||
v.prof_instr[v.fn_of_ip[v.ip]]++
|
||||
}
|
||||
if v.max_ops > 0 {
|
||||
v.ops++
|
||||
if v.ops > v.max_ops {
|
||||
return error('max ops exceeded (${v.max_ops}) — possible infinite loop')
|
||||
}
|
||||
}
|
||||
if v.dbg.enabled {
|
||||
v.dbg_tick()!
|
||||
}
|
||||
match op {
|
||||
op_halt {
|
||||
v.halted = true
|
||||
@@ -305,6 +426,9 @@ fn (mut v Vm) exec() ! {
|
||||
v.ip++
|
||||
target := int(v.read_i64())
|
||||
argc := int(v.read_i64())
|
||||
if v.profiling {
|
||||
v.prof_calls[v.fn_of_ip[target]]++
|
||||
}
|
||||
v.call(target, argc)
|
||||
}
|
||||
op_ret {
|
||||
@@ -633,6 +757,9 @@ fn (mut v Vm) exec() ! {
|
||||
v.stack[c + i] = cl.captured[i]
|
||||
}
|
||||
v.sp = c + n + argc
|
||||
if v.profiling {
|
||||
v.prof_calls[v.fn_of_ip[cl.entry]]++
|
||||
}
|
||||
v.call(cl.entry, argc + n)
|
||||
}
|
||||
op_argc {
|
||||
|
||||
Reference in New Issue
Block a user