- 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>
310 lines
8.0 KiB
Go
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"
|
|
}
|