Tons of more debug capabilities.(0.26.0)
This commit is contained in:
parent
c71857a068
commit
a014b963c2
5 changed files with 461 additions and 21 deletions
|
|
@ -1,5 +1,5 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
project(fun VERSION 0.25.0 LANGUAGES C)
|
||||
project(fun VERSION 0.26.0 LANGUAGES C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
|
|
|||
93
examples/debug_reporting.fun
Normal file
93
examples/debug_reporting.fun
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
//!/usr/bin/env fun
|
||||
|
||||
/*
|
||||
* Debugging and error reporting demo for Fun.
|
||||
*
|
||||
* How to run:
|
||||
* fun --repl-on-error examples/debug_reporting.fun
|
||||
*
|
||||
* What happens:
|
||||
* - The program runs compute(), then triggers a runtime error in crash().
|
||||
* - The VM drops you into the REPL at the error site with the stack preserved.
|
||||
*
|
||||
* Try these REPL commands when it stops:
|
||||
* :backtrace # or :bt — show frames (most recent first)
|
||||
* :locals # locals in the current (top) frame
|
||||
* :stack # show the value stack
|
||||
* :top # show the top-of-stack value
|
||||
* :list # show source around the current line
|
||||
* :disas # disassemble around current instruction pointer (ip)
|
||||
* :frame 1 # select a lower frame, then try :locals, :list, :disas again
|
||||
* :printv local[0] # print a single value (also: stack[i], global[i])
|
||||
*
|
||||
* Stepping and breakpoints:
|
||||
* - Before re-running the script, you can set a breakpoint at the marker below.
|
||||
* For example (adjust the line number as needed in your copy):
|
||||
* :break examples/debug_reporting.fun:LINE_BK_1
|
||||
* :info breaks
|
||||
* :cont
|
||||
* On hit, try stepping:
|
||||
* :step # step one instruction
|
||||
* :next # step over
|
||||
* :finish # run until current frame returns
|
||||
* :cont # continue until next stop (breakpoint or error)
|
||||
*
|
||||
* Note: If line numbers drift due to edits, use :list and :backtrace to locate the desired spot,
|
||||
* then set the breakpoint with the correct file:line.
|
||||
*/
|
||||
|
||||
/* A small helper computation to set a breakpoint on */
|
||||
fun add(a, b)
|
||||
// Simple arithmetic to have a few instructions to step over
|
||||
c = a + b
|
||||
print(c)
|
||||
|
||||
/* A function that calls add() and has a good place for a breakpoint */
|
||||
fun compute(n)
|
||||
arr = [10, 20, 30, 40, 50]
|
||||
i = 2
|
||||
// BREAKPOINT CANDIDATE (LINE_BK_1): set a breakpoint on the next line:
|
||||
x = arr[i] + 7
|
||||
add(x, n)
|
||||
|
||||
/* A function that intentionally triggers a runtime error (index out of range) */
|
||||
fun crash()
|
||||
nums = [1, 2, 3]
|
||||
// This out-of-range access triggers an error and drops into the REPL:
|
||||
print(nums[10])
|
||||
|
||||
fun main()
|
||||
compute(3)
|
||||
crash()
|
||||
|
||||
main()
|
||||
|
||||
/* Expected when run with --repl-on-error (your version and line/ip may vary):
|
||||
Runtime error: index out of range
|
||||
(at ./examples/debug_reporting.fun:XX in crash, op INDEX_GET @ip YY)
|
||||
Entering REPL due to runtime error (code 1)
|
||||
Fun VERSION REPL
|
||||
Type :help for commands. Submit an empty line to run.
|
||||
fun> :backtrace
|
||||
Backtrace (most recent call first):
|
||||
#TOP crash at ./examples/debug_reporting.fun ip=... line=...
|
||||
#... main at ./examples/debug_reporting.fun ip=... line=...
|
||||
fun> :list
|
||||
> <current line> ...
|
||||
fun> :locals
|
||||
0: [locals if any]
|
||||
fun> :disas
|
||||
> ip: OP_NAME OPERAND
|
||||
...
|
||||
fun> :stack
|
||||
[... stack values ...]
|
||||
|
||||
Then try breakpoints and stepping on a new run:
|
||||
fun> :break ./examples/debug_reporting.fun:<line for LINE_BK_1>
|
||||
fun> :info breaks
|
||||
fun> :cont
|
||||
... Breakpoint hit ...
|
||||
fun> :next
|
||||
fun> :finish
|
||||
fun> :cont
|
||||
*/
|
||||
222
src/repl.c
222
src/repl.c
|
|
@ -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
141
src/vm.c
|
|
@ -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"
|
||||
|
|
|
|||
24
src/vm.h
24
src/vm.h
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue