1
0
Fork 0
forked from fun/fun

Tons of more debug capabilities.(0.26.0)

This commit is contained in:
Johannes Findeisen 2025-10-06 05:53:30 +02:00
commit a014b963c2
5 changed files with 461 additions and 21 deletions

View file

@ -829,25 +829,38 @@ static int buffer_looks_incomplete(const char *buf) {
static void show_repl_help(void) {
printf("Commands:\n");
printf(" :help Show this help\n");
printf(" :quit | :q | :exit Exit the REPL\n");
printf(" :reset Reset VM state (clears globals)\n");
printf(" :dump | :globals Dump current globals\n");
printf(" :globals [pattern] Dump globals filtering by value substring\n");
printf(" :vars [pattern] Alias for :globals\n");
printf(" :clear Clear current input buffer\n");
printf(" :print Show current buffer\n");
printf(" :run Execute current buffer immediately\n");
printf(" :profile Execute buffer and show timing + instruction count\n");
printf(" :save <file> Save current buffer to file\n");
printf(" :load <file> Load file into buffer (does not run)\n");
printf(" :paste [run] Enter paste mode; end with a single '.' line (optional 'run')\n");
printf(" :history [N] Show last N lines of history (default 50)\n");
printf(" :time on|off|toggle Toggle/enable/disable timing\n");
printf(" :env [NAME[=VALUE]] Get or set environment variable\n");
printf(" :backtrace | :bt Show backtrace of VM frames (most recent first)\n");
printf(" :stack [N] Show top N (default all) stack values\n");
printf(" :locals [FRAME] Show locals of frame (default: current top frame)\n");
printf(" :help Show this help\n");
printf(" :quit | :q | :exit Exit the REPL\n");
printf(" :reset Reset VM state (clears globals)\n");
printf(" :dump | :globals Dump current globals\n");
printf(" :globals [pattern] Dump globals filtering by value substring\n");
printf(" :vars [pattern] Alias for :globals\n");
printf(" :clear Clear current input buffer\n");
printf(" :print Show current buffer\n");
printf(" :run Execute current buffer immediately\n");
printf(" :profile Execute buffer and show timing + instruction count\n");
printf(" :save <file> Save current buffer to file\n");
printf(" :load <file> Load file into buffer (does not run)\n");
printf(" :paste [run] Enter paste mode; end with a single '.' line (optional 'run')\n");
printf(" :history [N] Show last N lines of history (default 50)\n");
printf(" :time on|off|toggle Toggle/enable/disable timing\n");
printf(" :env [NAME[=VALUE]] Get or set environment variable\n");
printf(" :backtrace | :bt Show backtrace of VM frames (most recent first)\n");
printf(" :frame N Select frame N for :locals/:list/:disas (default: top)\n");
printf(" :list [±K] Show K lines of source around current frame line (default 5)\n");
printf(" :disas [±N] Disassemble around current frame ip (default 5)\n");
printf(" :stack [N] Show top N (default all) stack values\n");
printf(" :top Show the top of the VM stack\n");
printf(" :locals [FRAME] Show locals of frame (default: selected frame)\n");
printf(" :printv WHAT Print value: local[i] | stack[i] | global[i]\n");
printf(" :break [file:]line Set a breakpoint (default file = current frame file)\n");
printf(" :info breaks List breakpoints\n");
printf(" :delete ID Delete breakpoint by ID\n");
printf(" :clear breaks Remove all breakpoints\n");
printf(" :cont Continue execution (exit REPL if in debug stop)\n");
printf(" :step Step one instruction\n");
printf(" :next Step over (current frame)\n");
printf(" :finish Run until the current frame returns\n");
}
static char *read_entire_file(const char *path, size_t *out_len) {
@ -930,6 +943,7 @@ static void env_set(const char *name, const char *value) {
int fun_run_repl(VM *vm) {
int repl_timing = 0;
int selected_frame = -1; /* -1 means use current top frame */
printf("Fun %s REPL\n", FUN_VERSION);
printf("Type :help for commands. Submit an empty line to run.\n");
@ -1283,7 +1297,7 @@ int fun_run_repl(VM *vm) {
}
continue;
} else if (strcmp(cmd, "locals") == 0) {
int idx = vm->fp;
int idx = (selected_frame >= 0 && selected_frame <= vm->fp) ? selected_frame : vm->fp;
const char *p = lstrip(arg);
if (p && *p) {
int v = atoi(p);
@ -1304,6 +1318,174 @@ int fun_run_repl(VM *vm) {
}
if (!any) printf(" (no non-nil locals)\n");
continue;
} else if (strcmp(cmd, "frame") == 0) {
const char *p = lstrip(arg);
if (!p || !*p) { printf("Usage: :frame N\n"); continue; }
int v = atoi(p);
if (v < 0 || v > vm->fp) { printf("Invalid frame index. Current top is %d\n", vm->fp); continue; }
selected_frame = v;
Frame *f = &vm->frames[selected_frame];
const char *fname = (f->fn && f->fn->name) ? f->fn->name : "<entry>";
const char *sfile = (f->fn && f->fn->source_file) ? f->fn->source_file : "<unknown>";
printf("Selected frame #%d: %s (%s)\n", selected_frame, fname, sfile);
continue;
} else if (strcmp(cmd, "list") == 0) {
int k = 5;
const char *p = lstrip(arg);
if (p && *p) k = atoi(p);
if (k <= 0) k = 5;
int idx = (selected_frame >= 0 && selected_frame <= vm->fp) ? selected_frame : vm->fp;
if (idx < 0) { printf("(no current frame)\n"); continue; }
Frame *f = &vm->frames[idx];
if (!f->fn || !f->fn->source_file) { printf("(no source info)\n"); continue; }
/* derive current line for this frame by scanning LINE markers up to ip-1 */
int line = vm->current_line;
int upto = f->ip - 1;
if (upto < 0) upto = 0;
for (int i = 0; i <= upto && i < f->fn->instr_count; ++i) {
Instruction ins = f->fn->instructions[i];
if (ins.op == OP_LINE) line = ins.operand;
}
const char *path = f->fn->source_file;
size_t flen = 0;
char *src = read_entire_file(path, &flen);
if (!src) { printf("Unable to read %s\n", path); continue; }
int start = line - k; if (start < 1) start = 1;
int end = line + k;
int cur = 1;
const char *s = src;
while (*s && cur <= end) {
const char *ls = s;
while (*s && *s != '\n') s++;
int print = (cur >= start && cur <= end);
if (print) {
printf("%c %5d | ", (cur == line ? '>' : ' '), cur);
fwrite(ls, 1, (size_t)(s - ls), stdout);
printf("\n");
}
if (*s == '\n') s++;
cur++;
}
free(src);
continue;
} else if (strcmp(cmd, "disas") == 0 || strcmp(cmd, "disassemble") == 0) {
int n = 5;
const char *p = lstrip(arg);
if (p && *p) n = atoi(p);
if (n <= 0) n = 5;
int idx = (selected_frame >= 0 && selected_frame <= vm->fp) ? selected_frame : vm->fp;
if (idx < 0) { printf("(no current frame)\n"); continue; }
Frame *f = &vm->frames[idx];
if (!f->fn) { printf("(no function)\n"); continue; }
int curip = f->ip - 1; if (curip < 0) curip = 0;
int from = curip - n; if (from < 0) from = 0;
int to = curip + n; if (to >= f->fn->instr_count) to = f->fn->instr_count - 1;
for (int i = from; i <= to; ++i) {
Instruction ins = f->fn->instructions[i];
const char *opname = (ins.op >= 0 && ins.op < (int)(sizeof(opcode_names)/sizeof(opcode_names[0])))
? opcode_names[ins.op] : "???";
printf("%c %6d: %-14s %d\n", (i == curip ? '>' : ' '), i, opname, ins.operand);
}
continue;
} else if (strcmp(cmd, "printv") == 0) {
const char *spec = lstrip(arg);
if (!spec || !*spec) { printf("Usage: :printv local[i] | stack[i] | global[i]\n"); continue; }
int idx = -1;
if (sscanf(spec, "local[%d]", &idx) == 1) {
int fidx = (selected_frame >= 0 && selected_frame <= vm->fp) ? selected_frame : vm->fp;
if (fidx < 0 || idx < 0 || idx >= MAX_FRAME_LOCALS) { printf("(out of range)\n"); continue; }
Frame *f = &vm->frames[fidx];
char *sv = value_to_string_alloc(&f->locals[idx]);
printf("%s\n", sv ? sv : "nil");
free(sv);
} else if (sscanf(spec, "stack[%d]", &idx) == 1) {
if (idx < 0 || idx > vm->sp) { printf("(out of range)\n"); continue; }
char *sv = value_to_string_alloc(&vm->stack[idx]);
printf("%s\n", sv ? sv : "nil");
free(sv);
} else if (sscanf(spec, "global[%d]", &idx) == 1) {
if (idx < 0 || idx >= MAX_GLOBALS) { printf("(out of range)\n"); continue; }
char *sv = value_to_string_alloc(&vm->globals[idx]);
printf("%s\n", sv ? sv : "nil");
free(sv);
} else {
printf("Usage: :printv local[i] | stack[i] | global[i]\n");
}
continue;
} else if (strcmp(cmd, "top") == 0) {
if (vm->sp < 0) { printf("(stack empty)\n"); continue; }
char *sv = value_to_string_alloc(&vm->stack[vm->sp]);
printf("%s\n", sv ? sv : "nil");
free(sv);
continue;
} else if (strcmp(cmd, "break") == 0) {
const char *p = lstrip(arg);
if (!p || !*p) { printf("Usage: :break [file:]line\n"); continue; }
const char *colon = strchr(p, ':');
char filebuf[1024];
int line = 0;
if (colon) {
size_t fl = (size_t)(colon - p);
if (fl >= sizeof(filebuf)) fl = sizeof(filebuf) - 1;
memcpy(filebuf, p, fl);
filebuf[fl] = '\0';
line = atoi(colon + 1);
} else {
int idxf = (selected_frame >= 0 && selected_frame <= vm->fp) ? selected_frame : vm->fp;
if (idxf < 0) { printf("(no current frame)\n"); continue; }
Frame *f = &vm->frames[idxf];
const char *sf = (f->fn && f->fn->source_file) ? f->fn->source_file : NULL;
if (!sf) { printf("(no current source file)\n"); continue; }
snprintf(filebuf, sizeof(filebuf), "%s", sf);
line = atoi(p);
}
if (line <= 0) { printf("Invalid line\n"); continue; }
int id = vm_debug_add_breakpoint(vm, filebuf, line);
if (id >= 0) printf("Breakpoint %d set at %s:%d\n", id, filebuf, line);
else printf("Failed to set breakpoint\n");
continue;
} else if (strcmp(cmd, "info") == 0) {
const char *what = lstrip(arg);
if (what && strcmp(what, "breaks") == 0) {
vm_debug_list_breakpoints(vm);
} else {
printf("Usage: :info breaks\n");
}
continue;
} else if (strcmp(cmd, "delete") == 0) {
int id = atoi(lstrip(arg));
if (vm_debug_delete_breakpoint(vm, id)) printf("Deleted breakpoint %d\n", id);
else printf("No such breakpoint %d\n", id);
continue;
} else if (strcmp(cmd, "clear") == 0) {
const char *what = lstrip(arg);
if (what && strcmp(what, "breaks") == 0) {
vm_debug_clear_breakpoints(vm);
printf("Cleared all breakpoints\n");
} else {
printf("Usage: :clear breaks\n");
}
continue;
} else if (strcmp(cmd, "cont") == 0 || strcmp(cmd, "continue") == 0) {
vm_debug_request_continue(vm);
printf("Continuing...\n");
if (vm->on_error_repl) return 0; /* exit REPL to continue execution */
continue;
} else if (strcmp(cmd, "step") == 0) {
vm_debug_request_step(vm);
printf("Stepping one instruction...\n");
if (vm->on_error_repl) return 0;
continue;
} else if (strcmp(cmd, "next") == 0) {
vm_debug_request_next(vm);
printf("Stepping over...\n");
if (vm->on_error_repl) return 0;
continue;
} else if (strcmp(cmd, "finish") == 0) {
vm_debug_request_finish(vm);
printf("Running until current frame returns...\n");
if (vm->on_error_repl) return 0;
continue;
} else {
printf("Unknown command. Use :help\n");
continue;

141
src/vm.c
View file

@ -180,6 +180,9 @@ void vm_reset(VM *vm) {
vm_clear_output(vm);
// Reset exit code
vm->exit_code = 0;
// Reset debugger state (breakpoints, stepping)
vm_debug_reset(vm);
}
void vm_dump_globals(VM *vm) {
@ -194,6 +197,88 @@ void vm_dump_globals(VM *vm) {
printf("===============\n");
}
/* --- Debugger API impl --- */
void vm_debug_reset(VM *vm) {
for (int i = 0; i < vm->break_count; ++i) {
if (vm->breakpoints[i].file) {
free(vm->breakpoints[i].file);
vm->breakpoints[i].file = NULL;
}
vm->breakpoints[i].active = 0;
vm->breakpoints[i].line = 0;
}
vm->break_count = 0;
vm->debug_step_mode = 0;
vm->debug_step_target_fp = -1;
vm->debug_step_start_ic = vm->instr_count;
vm->debug_stop_requested = 0;
}
int vm_debug_add_breakpoint(VM *vm, const char *file, int line) {
if (!file || line <= 0) return -1;
if (vm->break_count >= (int)(sizeof(vm->breakpoints)/sizeof(vm->breakpoints[0]))) return -1;
int id = vm->break_count++;
vm->breakpoints[id].file = strdup(file);
vm->breakpoints[id].line = line;
vm->breakpoints[id].active = 1;
return id;
}
int vm_debug_delete_breakpoint(VM *vm, int id) {
if (id < 0 || id >= vm->break_count) return 0;
if (vm->breakpoints[id].file) free(vm->breakpoints[id].file);
for (int i = id + 1; i < vm->break_count; ++i) {
vm->breakpoints[i - 1] = vm->breakpoints[i];
}
vm->break_count--;
if (vm->break_count >= 0) {
vm->breakpoints[vm->break_count].file = NULL;
vm->breakpoints[vm->break_count].line = 0;
vm->breakpoints[vm->break_count].active = 0;
}
return 1;
}
void vm_debug_clear_breakpoints(VM *vm) {
vm_debug_reset(vm);
}
void vm_debug_list_breakpoints(VM *vm) {
if (vm->break_count <= 0) {
printf("(no breakpoints)\n");
return;
}
for (int i = 0; i < vm->break_count; ++i) {
if (!vm->breakpoints[i].active) continue;
printf(" [%d] %s:%d\n", i, vm->breakpoints[i].file ? vm->breakpoints[i].file : "<unknown>", vm->breakpoints[i].line);
}
}
void vm_debug_request_step(VM *vm) {
vm->debug_step_mode = 1; // step
vm->debug_step_start_ic = vm->instr_count;
vm->debug_stop_requested = 0;
}
void vm_debug_request_next(VM *vm) {
vm->debug_step_mode = 2; // next (step over)
vm->debug_step_target_fp = vm->fp;
vm->debug_step_start_ic = vm->instr_count;
vm->debug_stop_requested = 0;
}
void vm_debug_request_finish(VM *vm) {
vm->debug_step_mode = 3; // finish (until return)
vm->debug_step_target_fp = vm->fp;
vm->debug_stop_requested = 0;
}
void vm_debug_request_continue(VM *vm) {
vm->debug_step_mode = 0;
vm->debug_stop_requested = 0;
}
static void push_value(VM *vm, Value v) {
if (vm->sp >= STACK_SIZE - 1) {
fprintf(stderr, "Runtime error: stack overflow\n");
@ -225,6 +310,19 @@ void vm_init(VM *vm) {
vm->trace_enabled = 0;
vm->repl_on_error = 0;
vm->on_error_repl = NULL;
/* Debugger state */
vm->debug_step_mode = 0;
vm->debug_step_target_fp = -1;
vm->debug_step_start_ic = 0;
vm->debug_stop_requested = 0;
vm->break_count = 0;
for (int i = 0; i < (int)(sizeof(vm->breakpoints)/sizeof(vm->breakpoints[0])); ++i) {
vm->breakpoints[i].file = NULL;
vm->breakpoints[i].line = 0;
vm->breakpoints[i].active = 0;
}
for (int i = 0; i < MAX_GLOBALS; ++i)
vm->globals[i] = make_nil();
}
@ -292,6 +390,31 @@ void vm_run(VM *vm, Bytecode *entry) {
while (vm->fp >= 0) {
Frame *f = &vm->frames[vm->fp];
/* Stop conditions at top of loop (stepping/finish) */
if (vm->on_error_repl) {
int should_stop = 0;
if (vm->debug_stop_requested) {
should_stop = 1;
} else if (vm->debug_step_mode == 1 && vm->instr_count > vm->debug_step_start_ic) { /* step */
should_stop = 1;
vm->debug_step_mode = 0;
} else if (vm->debug_step_mode == 2 && vm->instr_count > vm->debug_step_start_ic && vm->fp <= vm->debug_step_target_fp) { /* next */
should_stop = 1;
vm->debug_step_mode = 0;
} else if (vm->debug_step_mode == 3 && vm->fp < vm->debug_step_target_fp) { /* finish */
should_stop = 1;
vm->debug_step_mode = 0;
}
if (should_stop) {
vm->debug_stop_requested = 0;
fprintf(stderr, "Paused (debug)\n");
vm->on_error_repl(vm);
/* Frame pointer might have changed (reset/cont); refresh f */
if (vm->fp < 0) break;
f = &vm->frames[vm->fp];
}
}
if (f->ip < 0 || f->ip >= f->fn->instr_count) {
/* no more instructions in this frame -> implicit return nil */
Value nilv = make_nil();
@ -321,6 +444,24 @@ void vm_run(VM *vm, Bytecode *entry) {
fprintf(stdout, "]\n");
}
/* Breakpoint hit detection: breakpoints are set on source_file:line via LINE markers */
if (vm->on_error_repl && inst.op == OP_LINE && vm->break_count > 0) {
const char *sfile = (f->fn && f->fn->source_file) ? f->fn->source_file : NULL;
int line = inst.operand;
for (int bi = 0; bi < vm->break_count; ++bi) {
if (!vm->breakpoints[bi].active) continue;
if (vm->breakpoints[bi].line != line) continue;
if (!sfile || !vm->breakpoints[bi].file) continue;
if (strcmp(vm->breakpoints[bi].file, sfile) != 0) continue;
fprintf(stderr, "Breakpoint %d hit at %s:%d\n", bi, sfile, line);
vm->on_error_repl(vm);
/* After returning, refresh frame pointer and frame */
if (vm->fp < 0) break;
f = &vm->frames[vm->fp];
break;
}
}
switch (inst.op) {
/* All opcode handlers as .c includes */
#include "vm/arithmetic/add.c"

View file

@ -70,6 +70,19 @@ struct VM {
int trace_enabled; // when non-zero, print executed ops and stack
int repl_on_error; // when non-zero, enter REPL on runtime error (preserve stack)
int (*on_error_repl)(struct VM *vm); // optional hook to run REPL on error
/* --- Debugger state --- */
int debug_step_mode; // 0 none, 1 step, 2 next, 3 finish
int debug_step_target_fp; // target frame pointer for next/finish
long long debug_step_start_ic; // instruction count snapshot when step/next requested
int debug_stop_requested; // force a pause at loop top
struct {
char *file; // strdup'ed file path
int line; // 1-based line
int active; // 1 if active
} breakpoints[64];
int break_count; // number of active breakpoints
};
typedef struct VM VM;
@ -90,6 +103,17 @@ void vm_dump_globals(VM *vm);
// run entry Bytecode (pushes first frame)
void vm_run(VM *vm, Bytecode *entry);
/* --- Debugger API --- */
void vm_debug_reset(VM *vm);
int vm_debug_add_breakpoint(VM *vm, const char *file, int line); // returns id >=0 or -1
int vm_debug_delete_breakpoint(VM *vm, int id); // returns 1 on success
void vm_debug_clear_breakpoints(VM *vm);
void vm_debug_list_breakpoints(VM *vm);
void vm_debug_request_step(VM *vm);
void vm_debug_request_next(VM *vm);
void vm_debug_request_finish(VM *vm);
void vm_debug_request_continue(VM *vm);
static inline int opcode_is_valid(int op) {
return op >= OP_NOP && op <= OP_EXIT; // all current opcodes
}