Files
five/hbrt/debug.go
Charles KWON OhJun 59568f3301 Five v0.9 — Harbour + Go fusion language
- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline
- Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch
- RTL: 351 Harbour-compatible functions
- RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization
- Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec)
- HB_FUNC API: Full Harbour C API compatible Go bridge
- Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT
- Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST
- Macro Compiler: Runtime AST parsing and evaluation
- Debugger: TUI debugger with source display, breakpoints, stepping
- FRB: Native + Pcode dual mode runtime binary
- Tests: 13 packages ALL PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 09:41:50 +09:00

310 lines
8.0 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Five Debugger — line-level debugging with breakpoints, stepping,
// variable inspection, and call stack display.
//
// Architecture:
// gengo emits t.DebugLine(file, line) at each PRG source line.
// Thread.DebugLine() checks breakpoints and invokes the debugger callback.
// The debugger callback can inspect variables, call stack, and control flow.
//
// Unlike Harbour's 370KB debug system, Five's debugger is ~300 lines of Go
// that leverages Go's runtime introspection.
package hbrt
import (
"fmt"
"strings"
"sync"
)
// DebugMode constants
const (
DbgContinue = 0 // free run, stop at breakpoints only
DbgStepLine = 1 // stop at every line
DbgStepOver = 2 // stop when returning to same or shallower level
DbgStepOut = 3 // stop when returning from current function
DbgToCursor = 4 // run until specific line
)
// Breakpoint represents a source-level breakpoint.
type Breakpoint struct {
Module string // source file name
Line int // line number (1-based)
Function string // optional function name filter
Enabled bool
HitCount int
}
// DebugVarInfo describes a variable visible in the current scope.
type DebugVarInfo struct {
Name string // variable name
Value Value // current value
Scope string // "LOCAL", "STATIC", "PARAM"
Index int // local index (1-based)
}
// DebugStackFrame describes one frame in the call stack.
type DebugStackFrame struct {
Function string // function name
Module string // source file
Line int // current line
Level int // stack depth
}
// DebugCallback is called when the debugger activates.
// The callback can inspect state and return the next debug mode.
type DebugCallback func(info *DebugEvent) int
// DebugEvent contains all information available at a debug stop.
type DebugEvent struct {
Module string // current source file
Line int // current line number
Function string // current function name
Reason string // "breakpoint", "step", "entry"
Thread *Thread // the executing thread
CallStack []DebugStackFrame // call stack
Locals []DebugVarInfo // local variables
Breakpoint *Breakpoint // which breakpoint hit (or nil)
}
// Debugger manages debug state for the VM.
type Debugger struct {
mu sync.Mutex
Enabled bool
Mode int // DbgContinue, DbgStepLine, etc.
Breakpoints []*Breakpoint
Callback DebugCallback // called when debugger activates
StepLevel int // call stack depth for step-over
ToCursorMod string // target module for run-to-cursor
ToCursorLine int // target line for run-to-cursor
// Debug info tables (populated by generated code)
LineInfo map[string]map[int]bool // module → set of valid lines
FuncInfo map[string]string // "MODULE:LINE" → function name
LocalInfo map[string][]string // function → local var names
SourceDir string // base directory for resolving source paths
}
// NewDebugger creates a new debugger instance.
func NewDebugger() *Debugger {
return &Debugger{
Enabled: true,
Mode: DbgStepLine, // start in step mode
LineInfo: make(map[string]map[int]bool),
FuncInfo: make(map[string]string),
LocalInfo: make(map[string][]string),
}
}
// AddBreakpoint adds a breakpoint.
func (d *Debugger) AddBreakpoint(module string, line int) int {
d.mu.Lock()
defer d.mu.Unlock()
bp := &Breakpoint{
Module: strings.ToUpper(module),
Line: line,
Enabled: true,
}
d.Breakpoints = append(d.Breakpoints, bp)
return len(d.Breakpoints) - 1
}
// RemoveBreakpoint removes a breakpoint by index.
func (d *Debugger) RemoveBreakpoint(idx int) {
d.mu.Lock()
defer d.mu.Unlock()
if idx >= 0 && idx < len(d.Breakpoints) {
d.Breakpoints = append(d.Breakpoints[:idx], d.Breakpoints[idx+1:]...)
}
}
// ToggleBreakpoint enables/disables a breakpoint.
func (d *Debugger) ToggleBreakpoint(idx int) {
d.mu.Lock()
defer d.mu.Unlock()
if idx >= 0 && idx < len(d.Breakpoints) {
d.Breakpoints[idx].Enabled = !d.Breakpoints[idx].Enabled
}
}
// FindBreakpoint checks if there's an active breakpoint at module:line.
func (d *Debugger) FindBreakpoint(module string, line int) *Breakpoint {
upper := strings.ToUpper(module)
for _, bp := range d.Breakpoints {
if bp.Enabled && bp.Line == line {
if bp.Module == upper || strings.HasSuffix(upper, bp.Module) {
bp.HitCount++
return bp
}
}
}
return nil
}
// RegisterLine records that a line exists in a module (for valid breakpoint checking).
func (d *Debugger) RegisterLine(module string, line int) {
upper := strings.ToUpper(module)
if d.LineInfo[upper] == nil {
d.LineInfo[upper] = make(map[int]bool)
}
d.LineInfo[upper][line] = true
}
// IsValidLine checks if a line is a valid stop point.
func (d *Debugger) IsValidLine(module string, line int) bool {
upper := strings.ToUpper(module)
if lines, ok := d.LineInfo[upper]; ok {
return lines[line]
}
return false
}
// --- Thread debug integration ---
// DebugLine is called by generated code at each PRG source line.
// This is the main debug hook — gengo emits t.DebugLine("file.prg", 42)
func (t *Thread) DebugLine(module string, line int) {
vm := t.VM()
if vm.Debugger == nil || !vm.Debugger.Enabled {
return
}
dbg := vm.Debugger
dbg.mu.Lock()
mode := dbg.Mode
dbg.mu.Unlock()
shouldStop := false
var hitBP *Breakpoint
reason := ""
switch mode {
case DbgContinue:
// Only stop at breakpoints
hitBP = dbg.FindBreakpoint(module, line)
if hitBP != nil {
shouldStop = true
reason = "breakpoint"
}
case DbgStepLine:
shouldStop = true
reason = "step"
case DbgStepOver:
if t.callSP <= dbg.StepLevel {
shouldStop = true
reason = "step"
} else {
// Check breakpoints even during step-over
hitBP = dbg.FindBreakpoint(module, line)
if hitBP != nil {
shouldStop = true
reason = "breakpoint"
}
}
case DbgStepOut:
if t.callSP < dbg.StepLevel {
shouldStop = true
reason = "step"
}
case DbgToCursor:
if strings.EqualFold(module, dbg.ToCursorMod) && line == dbg.ToCursorLine {
shouldStop = true
reason = "cursor"
} else {
hitBP = dbg.FindBreakpoint(module, line)
if hitBP != nil {
shouldStop = true
reason = "breakpoint"
}
}
}
if !shouldStop {
return
}
// Build debug event
event := &DebugEvent{
Module: module,
Line: line,
Function: t.currentFuncName(),
Reason: reason,
Thread: t,
Breakpoint: hitBP,
}
// Build call stack
event.CallStack = t.DebugCallStack()
// Build locals
event.Locals = t.DebugLocals()
// Invoke callback
if dbg.Callback != nil {
newMode := dbg.Callback(event)
dbg.mu.Lock()
dbg.Mode = newMode
if newMode == DbgStepOver {
dbg.StepLevel = t.callSP
} else if newMode == DbgStepOut {
dbg.StepLevel = t.callSP
}
dbg.mu.Unlock()
}
}
// DebugCallStack returns the current call stack for debugging.
func (t *Thread) DebugCallStack() []DebugStackFrame {
var stack []DebugStackFrame
for i := t.callSP - 1; i >= 0; i-- {
frame := &t.calls[i]
name := "unknown"
if frame.symbol != nil {
name = frame.symbol.Name
}
stack = append(stack, DebugStackFrame{
Function: name,
Level: i,
})
}
return stack
}
// DebugLocals returns local variables for the current frame.
func (t *Thread) DebugLocals() []DebugVarInfo {
if t.curFrame == nil {
return nil
}
var vars []DebugVarInfo
for i := 0; i < t.curFrame.localCount; i++ {
idx := t.curFrame.localBase + i
if idx < len(t.locals) {
scope := "LOCAL"
if i < t.curFrame.paramCount {
scope = "PARAM"
}
vars = append(vars, DebugVarInfo{
Name: fmt.Sprintf("_%d", i+1), // placeholder name
Value: t.locals[idx],
Scope: scope,
Index: i + 1,
})
}
}
return vars
}
// currentFuncName returns the name of the currently executing function.
func (t *Thread) currentFuncName() string {
if t.callSP > 0 {
frame := &t.calls[t.callSP-1]
if frame.symbol != nil {
return frame.symbol.Name
}
}
return "MAIN"
}