From a014b963c20f2bca391a57c4c2f0fce857acd9d8 Mon Sep 17 00:00:00 2001 From: hanez Date: Mon, 6 Oct 2025 05:53:30 +0200 Subject: [PATCH] Tons of more debug capabilities.(0.26.0) --- CMakeLists.txt | 2 +- examples/debug_reporting.fun | 93 +++++++++++++++ src/repl.c | 222 +++++++++++++++++++++++++++++++---- src/vm.c | 141 ++++++++++++++++++++++ src/vm.h | 24 ++++ 5 files changed, 461 insertions(+), 21 deletions(-) create mode 100644 examples/debug_reporting.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index bcad443..692d331 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/examples/debug_reporting.fun b/examples/debug_reporting.fun new file mode 100644 index 0000000..9abb9f8 --- /dev/null +++ b/examples/debug_reporting.fun @@ -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 +> ... +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: +fun> :info breaks +fun> :cont +... Breakpoint hit ... +fun> :next +fun> :finish +fun> :cont +*/ diff --git a/src/repl.c b/src/repl.c index 7a4a55f..2771f97 100644 --- a/src/repl.c +++ b/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 Save current buffer to file\n"); - printf(" :load 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 Save current buffer to file\n"); + printf(" :load 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 : ""; + const char *sfile = (f->fn && f->fn->source_file) ? f->fn->source_file : ""; + 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; diff --git a/src/vm.c b/src/vm.c index d58abc1..0101728 100644 --- a/src/vm.c +++ b/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 : "", 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" diff --git a/src/vm.h b/src/vm.h index 6fe27a0..eccbdfc 100644 --- a/src/vm.h +++ b/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 }