diff --git a/include/ucode/types.h b/include/ucode/types.h index c0ccd38e..2ccad695 100644 --- a/include/ucode/types.h +++ b/include/ucode/types.h @@ -290,8 +290,14 @@ typedef struct { bool mcall, strict; } uc_callframe_t; +typedef struct uc_breakpoint { + uint8_t *ip; + void (*cb)(uc_vm_t *, struct uc_breakpoint *); +} uc_breakpoint_t; + uc_declare_vector(uc_callframes_t, uc_callframe_t); uc_declare_vector(uc_stack_t, uc_value_t *); +uc_declare_vector(uc_breakpoints_t, uc_breakpoint_t *); typedef struct printbuf uc_stringbuf_t; @@ -308,7 +314,7 @@ struct uc_vm { uc_source_t *sources; uc_weakref_t values; uc_resource_types_t restypes; - char _reserved[sizeof(uc_modexports_t)]; + uc_breakpoints_t breakpoints; union { uint32_t u32; int32_t s32; diff --git a/include/ucode/vm.h b/include/ucode/vm.h index 3cb6dc07..094e5d59 100644 --- a/include/ucode/vm.h +++ b/include/ucode/vm.h @@ -122,7 +122,7 @@ typedef enum { #define GC_DEFAULT_INTERVAL 1000 -extern uint32_t insns[__I_MAX]; +extern const int8_t insn_operand_bytes[__I_MAX]; void uc_vm_init(uc_vm_t *vm, uc_parse_config_t *config); void uc_vm_free(uc_vm_t *vm); diff --git a/lib/debug.c b/lib/debug.c index 0b227d9f..1cddc471 100644 --- a/lib/debug.c +++ b/lib/debug.c @@ -71,6 +71,7 @@ #include "ucode/module.h" #include "ucode/platform.h" +#include "ucode/compiler.h" static char *memdump_signal = "USR2"; @@ -367,6 +368,88 @@ print_ip_srcpos(FILE *out, uc_callframe_t *frame) uc_program_function_source(function)->filename, line, off); } +static size_t +srcpos_to_srcoff(uc_vm_t *vm, const char *path, size_t line, size_t column, + uc_source_t **sourcep, uc_program_t **programp) +{ + uc_weakref_t *ref; + uc_closure_t *uc; + + if (sourcep) + *sourcep = NULL; + + if (programp) + *programp = NULL; + + /* discover all source buffers */ + for (ref = vm->values.next; ref != &vm->values; ref = ref->next) { + uc = (uc_closure_t *)((uintptr_t)ref - offsetof(uc_closure_t, ref)); + + if (uc->header.type != UC_CLOSURE) + continue; + + if (!uc->function || !uc->function->program) + continue; + + uc_program_t *prog = uc->function->program; + + for (size_t i = 0; i < prog->sources.count; i++) { + uc_source_t *source = prog->sources.entries[i]; + + if (!strcmp(source->filename, path)) { + /* translate line + char to byte offset */ + line = (line < 1) ? 0 : line - 1; + column = (column < 1) ? 0 : column - 1; + + for (size_t i = 0; i < source->lineinfo.count; i++) { + if (i > 0 && source->lineinfo.entries[i] & 0x80) + line--, column++; + + column += (source->lineinfo.entries[i] & 0x7f); + + if (line == 0) { + if (sourcep) + *sourcep = uc_source_get(source); + + if (programp) + *programp = uc_program_get(prog); + + return column; + } + } + + goto out; + } + } + } + +out: + return 0; +} + +static size_t +srcoff_to_insnoff(uc_function_t *fn, size_t off) +{ + uc_offsetinfo_t *offsets = &fn->chunk.debuginfo.offsets; + size_t insn = 0; + + for (size_t i = 0, pos = 0, last_insn = 0; i < offsets->count; i++) + { + if (offsets->entries[i] & 7) { + pos += (offsets->entries[i] & 7); + + if (fn->srcpos + pos > off) + return last_insn; + + last_insn = insn; + } + + insn += (offsets->entries[i] >> 3); + } + + return insn; +} + static void print_memdump(uc_vm_t *vm, FILE *out) { @@ -615,6 +698,7 @@ debug_setup_memdump(uc_vm_t *vm) { uc_cfn_ptr_t ucsignal = uc_stdlib_function("signal"); uc_value_t *memdump = ucv_cfunction_new("memdump", debug_handle_memdump); + uc_value_t *handler; char *ev; ev = getenv("UCODE_DEBUG_MEMDUMP_PATH"); @@ -628,11 +712,14 @@ debug_setup_memdump(uc_vm_t *vm) uc_vm_stack_push(vm, ucv_string_new(memdump_signal)); uc_vm_stack_push(vm, memdump); - if (ucsignal(vm, 2) != memdump) + handler = ucsignal(vm, 2); + + if (handler != memdump) fprintf(stderr, "Unable to install debug signal handler\n"); ucv_put(uc_vm_stack_pop(vm)); ucv_put(uc_vm_stack_pop(vm)); + ucv_put(handler); } static void @@ -1614,6 +1701,1203 @@ uc_setupval(uc_vm_t *vm, size_t nargs) } +typedef enum { + BK_ONCE, + BK_USER, +} debug_breakpoint_kind_t; + +typedef struct { + uc_breakpoint_t bk; + uc_function_t *fn; + debug_breakpoint_kind_t kind; +} debug_breakpoint_t; + +static uc_callframe_t * +uc_debug_curr_frame(uc_vm_t *vm, size_t off) +{ + if (off > vm->callframes.count) + return NULL; + + for (size_t i = vm->callframes.count - off; i > 0; i--) + if (vm->callframes.entries[i-1].closure) + return &vm->callframes.entries[i-1]; + + return NULL; +} + +static bool cmd_break(uc_vm_t *vm, debug_breakpoint_t *dbk); +static bool cmd_delete(uc_vm_t *vm, debug_breakpoint_t *dbk); +static bool cmd_list(uc_vm_t *vm, debug_breakpoint_t *dbk); +static bool cmd_next(uc_vm_t *vm, debug_breakpoint_t *dbk); +static bool cmd_step(uc_vm_t *vm, debug_breakpoint_t *dbk); +static bool cmd_continue(uc_vm_t *vm, debug_breakpoint_t *dbk); +static bool cmd_return(uc_vm_t *vm, debug_breakpoint_t *dbk); +static bool cmd_backtrace(uc_vm_t *vm, debug_breakpoint_t *dbk); +static bool cmd_variables(uc_vm_t *vm, debug_breakpoint_t *dbk); +static bool cmd_sources(uc_vm_t *vm, debug_breakpoint_t *dbk); +static bool cmd_quit(uc_vm_t *vm, debug_breakpoint_t *dbk); +static bool cmd_print(uc_vm_t *vm, debug_breakpoint_t *dbk); + +static const struct { + const char *command; + bool (*cb)(uc_vm_t *, debug_breakpoint_t *); +} commands[] = { + { "break", cmd_break }, + { "delete", cmd_delete }, + { "list", cmd_list }, + { "next", cmd_next }, + { "step", cmd_step }, + { "continue", cmd_continue }, + { "return", cmd_return }, + { "backtrace", cmd_backtrace }, + { "bt", cmd_backtrace }, + { "variables", cmd_variables }, + { "sources", cmd_sources }, + { "print", cmd_print }, + { "quit", cmd_quit }, +}; + +static void +handle_breakpoint(uc_vm_t *vm, uc_breakpoint_t *bk); + +static size_t +patch_breakpoint(uc_vm_t *vm, uc_function_t *fn, size_t insnoff, + debug_breakpoint_kind_t kind) +{ + debug_breakpoint_t *bk = xalloc(sizeof(debug_breakpoint_t)); + + bk->bk.ip = &fn->chunk.entries[insnoff]; + bk->bk.cb = handle_breakpoint; + bk->fn = fn; + bk->kind = kind; + + for (size_t i = 0; i < vm->breakpoints.count; i++) { + if (vm->breakpoints.entries[i] == NULL) { + vm->breakpoints.entries[i] = &bk->bk; + + return i + 1; + } + } + + uc_vector_push(&vm->breakpoints, &bk->bk); + + return vm->breakpoints.count; +} + +static bool +delete_breakpoint(uc_vm_t *vm, debug_breakpoint_t *bk) +{ + for (size_t i = 0; i < vm->breakpoints.count; i++) { + if (vm->breakpoints.entries[i] == &bk->bk) { + vm->breakpoints.entries[i] = NULL; + free(bk); + + return true; + } + } + + return false; +} + +static size_t +add_breakpoint(uc_vm_t *vm, const char *path, size_t line, size_t byte, + debug_breakpoint_kind_t kind) +{ + uc_source_t *source = NULL; + uc_program_t *prog = NULL; + size_t off = srcpos_to_srcoff(vm, path, line, byte, &source, &prog); + + if (!prog) + return 0; + + uc_function_t *bfn = NULL; + size_t insn = 0; + + uc_program_function_foreach(prog, fn) { + if (uc_program_function_source(fn) != source) + continue; + + uc_offsetinfo_t *offsets = &fn->chunk.debuginfo.offsets; + + if (!offsets->count) + continue; + + size_t beg = uc_program_function_srcpos(fn, 0); + size_t end = uc_program_function_srcpos(fn, SIZE_MAX); + + if (off < beg || off > end) + continue; + + bfn = fn; + insn = srcoff_to_insnoff(fn, off); + +#if 0 + fprintf(stderr, + "%s:%zu:%zu -> %zu -> %zu {%s}\n", + path, line, byte, off, insn, + insns[fn->chunk.entries[insn]]); +#endif + } + + if (!bfn) + return 0; + + return patch_breakpoint(vm, bfn, insn, kind); +} + +static uint8_t * +next_instruction(uc_vm_t *vm, uc_function_t **fn, uint8_t *ip, bool single) +{ + if (single) { + if (*ip == I_MCALL || *ip == I_QMCALL) { + size_t nargs = ( + ip[1] * 0x1000000UL + + ip[2] * 0x10000UL + + ip[3] * 0x100UL + + ip[4] + ) & 0xffff; + + if (nargs + 2 < vm->stack.count) { + uc_value_t *ctx = vm->stack.entries[vm->stack.count - nargs - 2]; + uc_value_t *key = vm->stack.entries[vm->stack.count - nargs - 1]; + uc_value_t *fno = ucv_key_get(vm, ctx, key); + + ucv_put(fno); /* ucv_get_get() increases refcount */ + + if (ucv_type(fno) == UC_UPVALUE) { + uc_upvalref_t *ref = (uc_upvalref_t *)fno; + + if (ref->closed) + fno = ref->value; + else + fno = vm->stack.entries[ref->slot]; + } + + if (ucv_type(fno) == UC_CLOSURE) { + uc_closure_t *cl = (uc_closure_t *)fno; + + *fn = cl->function; + + return cl->function->chunk.entries; + } + } + } + else if (*ip == I_CALL || *ip == I_QCALL) { + size_t nargs = ( + ip[1] * 0x1000000UL + + ip[2] * 0x10000UL + + ip[3] * 0x100UL + + ip[4] + ) & 0xffff; + + if (nargs + 1 < vm->stack.count) { + uc_value_t *fno = vm->stack.entries[vm->stack.count - nargs - 1]; + + if (ucv_type(fno) == UC_CLOSURE) { + uc_closure_t *cl = (uc_closure_t *)fno; + + *fn = cl->function; + + return cl->function->chunk.entries; + } + } + } + } + + uint8_t *nextinsn = ip + abs(insn_operand_bytes[*ip]) + 1; + uc_chunk_t *chunk = &(*fn)->chunk; + + if (nextinsn >= chunk->entries + chunk->count) { + for (size_t i = vm->callframes.count - 1; i > 0; i--) { + uc_callframe_t *pframe = &vm->callframes.entries[i-1]; + + if (!pframe->closure) + continue; + + *fn = pframe->closure->function; + + return pframe->ip; + } + + return NULL; + } + + return nextinsn; +} + +static uc_value_t * +load_string(uc_value_list_t *vallist, size_t cidx) +{ + uc_value_type_t t = (cidx < vallist->isize) + ? (vallist->index[cidx] & 7) : TAG_INVAL; + + if (t == TAG_STR) { + char buf[sizeof(vallist->index[0])] = { 0 }; + size_t len = (vallist->index[cidx] >> 3) & 31; + + for (size_t j = 1; j <= len; j++) + buf[j-1] = (vallist->index[cidx] >> (j << 3)); + + return ucv_string_new_length(buf, len); + } + else if (t == TAG_LSTR) { + size_t off = (vallist->index[cidx] >> 3); + + if (off + sizeof(uint32_t) <= vallist->dsize) { + char *p = vallist->data + off; + size_t len = be32toh(*(uint32_t *)p); + + if (off + sizeof(uint32_t) + len <= vallist->dsize) + return ucv_string_new_length(p + sizeof(uint32_t), len); + } + } + + return NULL; +} + +static void +print_value2(uc_vm_t *vm, uc_value_t *val, int limit) +{ + uc_stringbuf_t *sb = xprintbuf_new(); + + ucv_to_stringbuf(vm, sb, val, true); + + if (limit > 4 && sb->bpos > limit) + printf("%.*s...%c", limit - 4, sb->buf, sb->buf[sb->bpos - 1]); + else + printf("%.*s", sb->bpos, sb->buf); + + printbuf_free(sb); +} + +static void +print_value3(uc_vm_t *vm, uc_value_t *val, bool full) +{ + uc_stringbuf_t *sb; + + if (full) { + sb = xprintbuf_new(); + ucv_to_stringbuf_formatted(vm, sb, val, 0, ' ', 2); + printf("%.*s", sb->bpos, sb->buf); + printbuf_free(sb); + } + else { + print_value2(vm, val, 32); + } +} + +static void +format_function(uc_vm_t *vm, uc_stringbuf_t *pb, uc_value_t *val, bool full) { + uc_type_t t = ucv_type(val); + + if (t == UC_CFUNCTION) { + uc_cfunction_t *cfn = (uc_cfunction_t *)val; + + if (!cfn->name[0]) { + sprintbuf(pb, "unnamed native function"); + return; + } + + for (size_t i = 0; i < vm->restypes.count; i++) { + uc_resource_type_t *rt = vm->restypes.entries[i]; + + ucv_object_foreach(rt->proto, k, v) { + (void)k; + + if (v == val) { + if (full) { + char *s = ucv_to_string(vm, val); + sprintbuf(pb, "function %s#%s", + rt->name, s + sizeof("function")); + free(s); + } + else { + sprintbuf(pb, "%s#%s", rt->name, cfn->name); + } + + return; + } + } + } + + uc_value_t *modtable = ucv_object_get(uc_vm_scope_get(vm), "modules", NULL); + + ucv_object_foreach(modtable, modname, modscope) { + ucv_object_foreach(modscope, symname, symval) { + (void)symname; + + if (symval == val) { + if (full) { + char *s = ucv_to_string(vm, val); + sprintbuf(pb, "function %s.%s", + modname, s + sizeof("function")); + free(s); + } + else { + sprintbuf(pb, "%s.%s", modname, cfn->name); + } + + return; + } + } + } + } + else if (t == UC_CLOSURE) { + uc_closure_t *cl = (uc_closure_t *)val; + + if (full) { + if (!cl->function->name[0]) + sprintbuf(pb, "%s ", + cl->function->arrow ? "arrow function" : "unnamed"); + + ucv_to_stringbuf(vm, pb, val, false); + } + else { + if (!cl->function->name[0]) + sprintbuf(pb, "%s", + cl->function->arrow ? "arrow function" : "unnamed function"); + else + sprintbuf(pb, "%s", cl->function->name); + } + } +} + +static void +print_variables(uc_vm_t *vm, uc_callframe_t *frame, + bool verbose, const char *indent) +{ + uc_chunk_t *chunk = &frame->closure->function->chunk; + uc_variables_t *decls = &chunk->debuginfo.variables; + uc_value_list_t *names = &chunk->debuginfo.varnames; + size_t pos = frame->ip - chunk->entries; + + if (frame->ctx) { + printf("%s(this) = ", indent); + print_value3(vm, frame->ctx, verbose); + printf("\n"); + } + + for (size_t i = 0; i < decls->count; i++) { + if (decls->entries[i].from > pos || decls->entries[i].to < pos) + continue; + + uc_value_t *vname = load_string(names, decls->entries[i].nameidx); + size_t slot = decls->entries[i].slot; + + /* is local variable */ + if (slot < (size_t)-1 / 2) { + if (vname) + printf("%s%s = ", indent, ucv_string_get(vname)); + else + printf("%s$%zu = ", indent, slot); + + if (frame->stackframe + slot < vm->stack.count) + print_value3(vm, + vm->stack.entries[frame->stackframe + slot], verbose); + else + printf(""); + } + + /* is upvalue */ + else { + if (vname) + printf("%s^%s = ", indent, ucv_string_get(vname)); + else + printf("%s^$%zu = ", indent, slot); + + slot -= ((size_t)-1 / 2); + + if (slot < frame->closure->function->nupvals) { + uc_upvalref_t *ref = frame->closure->upvals[slot]; + + if (ref->closed) + print_value3(vm, ref->value, verbose); + else if (ref->slot < vm->stack.count) + print_value3(vm, vm->stack.entries[ref->slot], verbose); + else + printf(""); + } + else { + printf(""); + } + } + + ucv_put(vname); + printf("\n"); + } +} + +static bool +eval_expr(uc_vm_t *vm, uc_callframe_t *frame, char *expr, uc_value_t **res) +{ + uc_chunk_t *chunk = &frame->closure->function->chunk; + uc_variables_t *decls = &chunk->debuginfo.variables; + uc_value_list_t *names = &chunk->debuginfo.varnames; + size_t pos = frame->ip - chunk->entries; + char *err = NULL; + + uc_source_t *source = + uc_source_new_buffer("[eval expression]", expr, strlen(expr)); + + uc_parse_config_t conf = { .raw_mode = true }; + uc_program_t *prog = uc_compile(&conf, source, &err); + + if (!prog) { + printf("%s", err); + free(err); + *res = NULL; + + return false; + } + + uc_function_t *fn = uc_program_entry(prog); + + if (fn->chunk.entries[0] != I_LVAR && fn->chunk.entries[0] != I_LTHIS) { + printf("Expecting expression\n"); + uc_program_put(prog); + *res = NULL; + + return false; + } + + uc_value_t *scope = ucv_object_new(NULL); + + /* determine referenced variables */ + for (size_t i = 0; i < fn->chunk.count; i++) { + uint8_t insn = fn->chunk.entries[i]; + + if (insn == I_LVAR) { + uc_value_t *varname = load_string(&prog->constants, + fn->chunk.entries[i + 1] * 0x1000000UL + + fn->chunk.entries[i + 2] * 0x10000UL + + fn->chunk.entries[i + 3] * 0x100UL + + fn->chunk.entries[i + 4]); + + if (!varname) + continue; + + uc_value_t *varval = NULL; + + for (size_t j = 0; !varval && j < decls->count; j++) { + if (decls->entries[j].from > pos || decls->entries[j].to < pos) + continue; + + uc_value_t *vname = load_string(names, decls->entries[j].nameidx); + bool match = ucv_is_equal(varname, vname); + + ucv_put(vname); + + if (!match) + continue; + + size_t slot = decls->entries[j].slot; + + /* is local var */ + if (slot < (size_t)-1 / 2) { + slot += frame->stackframe; + + if (slot < vm->stack.count) + varval = ucv_get(vm->stack.entries[slot]); + } + + /* is upvalue */ + else { + slot -= ((size_t)-1 / 2); + + if (slot < frame->closure->function->nupvals) { + uc_upvalref_t *ref = frame->closure->upvals[slot]; + + if (ref->closed) + varval = ucv_get(ref->value); + else if (ref->slot < vm->stack.count) + varval = ucv_get(vm->stack.entries[ref->slot]); + } + } + } + + if (varval) + ucv_object_add(scope, ucv_string_get(varname), varval); + + ucv_put(varname); + } + + i += insn_operand_bytes[insn]; + } + + uc_value_t *prev_scope = ucv_get(uc_vm_scope_get(vm)); + + ucv_prototype_set(scope, ucv_get(prev_scope)); + + uc_vm_scope_set(vm, scope); + + uc_vm_stack_push(vm, ucv_get(frame->ctx)); + uc_vm_stack_push(vm, ucv_closure_new(vm, fn, false)); + + bool rv; + + if (uc_vm_call(vm, true, 0) == EXCEPTION_NONE) { + *res = uc_vm_stack_pop(vm); + rv = true; + } + else { + printf("Exception: %s\n", vm->exception.message); + vm->exception.type = EXCEPTION_NONE; + *res = NULL; + rv = false; + } + + uc_vm_scope_set(vm, prev_scope); + + uc_program_put(prog); + + return rv; +} + +static uc_value_t * +uc_debug_sigint_handler(uc_vm_t *vm, size_t nargs); + +static void +print_location(uc_vm_t *vm, const char *prefix) +{ + uc_callframe_t *topframe = NULL, *funframe = NULL; + + for (size_t i = vm->callframes.count; i > 0; i--) { + if (!topframe || (topframe->cfunction && + topframe->cfunction->cfn == uc_debug_sigint_handler)) + topframe = &vm->callframes.entries[i - 1]; + + if (vm->callframes.entries[i - 1].closure) { + funframe = &vm->callframes.entries[i - 1]; + break; + } + } + + uc_stringbuf_t *pb = xprintbuf_new(); + + if (topframe != funframe && topframe && topframe->cfunction) { + format_function(vm, pb, &topframe->cfunction->header, true); + + if (funframe) + sprintbuf(pb, ", called by "); + } + + if (funframe) { + format_function(vm, pb, &funframe->closure->header, true); + + uc_function_t *function = funframe->closure->function; + uc_source_t *source = uc_program_function_source(function); + size_t off = (funframe->ip - function->chunk.entries) - 1; + size_t pos = uc_program_function_srcpos(function, off); + size_t byte = pos; + size_t line = uc_source_get_line(source, &byte); + + sprintbuf(pb, "\nat %s", source->filename); + + if (line) + sprintbuf(pb, ", line %zu:%zu", line, byte); + + sprintbuf(pb, "\n"); + uc_source_context_format(pb, source, pos, true); + } + + printf("%s%.*s\n", prefix, pb->bpos, pb->buf); + printbuf_free(pb); +} + +static bool +cmd_break(uc_vm_t *vm, debug_breakpoint_t *dbk) +{ + char *spec = strtok(NULL, "\n"); + size_t id = 0; + + if (!spec) { + printf("Usage:\n"); + printf(" break path[:line[:offset]]\n"); + printf(" break expr\n"); + + return true; + } + + while (*spec == ' ' || *spec == '\t') + spec++; + + /* path spec */ + if (spec && (strchr(spec, '/') || strchr(spec, ':')) && *spec != '(') { + char *path = strtok(spec, ": \t"); + char *line = strtok(NULL, ": \t"); + char *byte = strtok(NULL, ": \t"); + + if (!path) { + printf("Usage: break path[:line[:offset]]\n"); + + return true; + } + + id = add_breakpoint(vm, path, + line ? strtoul(line, NULL, 10) : 0, + byte ? strtoul(byte, NULL, 10) : 0, + BK_USER); + } + + /* expression spec */ + else { + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_value_t *val = NULL; + + if (frame && eval_expr(vm, frame, spec, &val)) { + if (ucv_type(val) == UC_CLOSURE) { + id = patch_breakpoint(vm, + ((uc_closure_t *)val)->function, 0, BK_USER); + } + else { + char *s = ucv_to_string(vm, val); + int len = strlen(s); + + printf("Value `%s` (%.*s%s) is not a function\n", + spec, + len > 32 ? 29 : len, + s, + len > 32 ? "..." : ""); + } + + ucv_put(val); + } + } + + if (id) + printf("Breakpoint #%zu added\n", id); + else + printf("Unable to resolve source location\n"); + + return true; +} + +static bool +cmd_delete(uc_vm_t *vm, debug_breakpoint_t *dbk) +{ + char *p = strtok(NULL, ": \t\r\n"); + unsigned long id = p ? strtoul(p, NULL, 10) : 0; + + if (id < vm->breakpoints.count && vm->breakpoints.entries[id]) { + free(vm->breakpoints.entries[id]); + vm->breakpoints.entries[id] = NULL; + + printf("Breakpoint #%lu deleted\n", id); + } + else { + printf("No breakpoint #%lu set\n", id); + } + + return true; +} + +static bool +cmd_list(uc_vm_t *vm, debug_breakpoint_t *dbk) +{ + bool set = false; + + for (size_t i = 0; i < vm->breakpoints.count; i++) { + if (!vm->breakpoints.entries[i]) + continue; + + debug_breakpoint_t *p = + (debug_breakpoint_t *)vm->breakpoints.entries[i]; + + uc_source_t *source = uc_program_function_source(p->fn); + size_t byte = uc_program_function_srcpos(p->fn, + p->bk.ip - p->fn->chunk.entries); + + size_t line = uc_source_get_line(source, &byte); + + printf("#%2zu %s:%zu:%zu %s()\n", + i, + source ? source->filename : "?", + line, byte > 1 ? byte : 1, + p->fn->name[0] + ? p->fn->name + : p->fn->arrow + ? "[arrow function]" + : "[unnamed function]"); + + set = true; + } + + if (!set) + printf("No breakpoints set\n"); + + return true; +} + +static bool +cmd_step_common(uc_vm_t *vm, debug_breakpoint_t *dbk, bool single) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + + if (!frame) + return false; + + uc_function_t *fn = frame->closure->function; + uint8_t *nextinsn = next_instruction(vm, &fn, frame->ip, single); + + /* no next instruction, run until completion */ + if (!nextinsn) + return false; + + uc_source_t *source = uc_program_function_source(fn); + + size_t byte = uc_program_function_srcpos(fn, + nextinsn - fn->chunk.entries); + + size_t line = uc_source_get_line( + uc_program_function_source(fn), &byte); + + if (fn != frame->closure->function) + printf("Entering %s()...\n", + fn->name[0] + ? fn->name : fn->arrow + ? "[arrow function]" : "[unnamed function]"); + else + printf("Continuing in %s:%zu:%zu...\n", + source->filename, line, byte); + + patch_breakpoint(vm, fn, nextinsn - fn->chunk.entries, BK_ONCE); + + return false; +} + +static bool +cmd_next(uc_vm_t *vm, debug_breakpoint_t *dbk) +{ + return cmd_step_common(vm, dbk, false); +} + +static bool +cmd_step(uc_vm_t *vm, debug_breakpoint_t *dbk) +{ + return cmd_step_common(vm, dbk, true); +} + +static bool +cmd_continue(uc_vm_t *vm, debug_breakpoint_t *dbk) +{ + printf("Continuing...\n"); + + return false; +} + +static bool +cmd_return(uc_vm_t *vm, debug_breakpoint_t *dbk) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 1); + + if (frame) { + patch_breakpoint(vm, frame->closure->function, + frame->ip - frame->closure->function->chunk.entries, + BK_ONCE); + } + else { + printf("In topmost function, running until completion...\n"); + } + + return false; +} + +static bool +cmd_backtrace(uc_vm_t *vm, debug_breakpoint_t *dbk) +{ + char *mode = strtok(NULL, ": \t\r\n"); + bool verbose = false; + + if (mode && !strcmp(mode, "full")) + verbose = true; + + size_t i, off, srcpos, prev_frame; + uc_function_t *function; + uc_callframe_t *frame; + + for (i = vm->callframes.count, prev_frame = vm->stack.count; i > 0; i--) { + frame = &vm->callframes.entries[i - 1]; + + if (frame->closure) { + function = frame->closure->function; + off = (frame->ip - function->chunk.entries) - 1; + srcpos = uc_program_function_srcpos(function, off); + + uc_stringbuf_t *pb = xprintbuf_new(); + format_function(vm, pb, &frame->closure->header, false); + printf("#%-2zu in %.*s(", i, pb->bpos, pb->buf); + printbuf_free(pb); + +#if 0 + // XXX: format + printf("#%-2zu in %s(", i, + function->name[0] + ? function->name : function->arrow + ? "[arrow function]" : "[unnamed function]"); +#endif + + for (size_t j = 0; j < function->nargs; j++) { + uc_value_t *argname = uc_chunk_debug_get_variable( + &function->chunk, j, j + 1, false); + + if (j > 0) + printf(", "); + + if (j + 1 == function->nargs && function->vararg) + printf("..."); + + if (argname) { + printf("%s=", ucv_string_get(argname)); + ucv_put(argname); + } + else { + printf("$%zu=", j + 1); + } + + uc_value_t *argval = + (frame->stackframe + j + 1 < vm->stack.count) + ? vm->stack.entries[frame->stackframe + j + 1] + : NULL; + + print_value2(vm, argval, 32); + } + + printf(")\n"); + + uc_stringbuf_t *context = xprintbuf_new(); + + uc_source_context_format(context, + uc_program_function_source(function), + srcpos, true); + + printf(" %.*s\n", context->bpos, context->buf); + printbuf_free(context); + + if (verbose) { + print_variables(vm, frame, false, " "); + printf("\n"); + } + } + else if (frame->cfunction) { + uc_cfunction_t *cfn = frame->cfunction; + + printf("#%-2zu in %s(", i, + cfn->name[0] ? cfn->name : "[unnamed native function]"); + + for (size_t j = 1; j < prev_frame - frame->stackframe; j++) { + if (j > 1) + printf(", "); + + uc_value_t *argval = + (frame->stackframe + j < vm->stack.count) + ? vm->stack.entries[frame->stackframe + j] + : NULL; + + print_value2(vm, argval, 32); + } + + printf(")\n"); + } + + prev_frame = frame->stackframe; + } + + return true; +} + +static bool +cmd_variables(uc_vm_t *vm, debug_breakpoint_t *dbk) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + char *p = strtok(NULL, " \t\r\n"); + + if (!frame) { + printf("No local variables in current context\n"); + + return true; + } + + print_variables(vm, frame, p && !strcmp(p, "full"), ""); + + return true; +} + +static bool +cmd_sources(uc_vm_t *vm, debug_breakpoint_t *dbk) +{ + struct lh_table *sources = lh_kptr_table_new(16, NULL); + uc_weakref_t *ref; + + for (ref = vm->values.next; ref != &vm->values; ref = ref->next) { + uc_closure_t *uc = + (uc_closure_t *)((uintptr_t)ref - offsetof(uc_closure_t, ref)); + + if (uc->header.type != UC_CLOSURE) + continue; + + if (!uc->function || !uc->function->program) + continue; + + for (size_t i = 0; i < uc->function->program->sources.count; i++) { + uc_source_t *source = uc->function->program->sources.entries[i]; + unsigned long hash = lh_get_hash(sources, source); + + if (!lh_table_lookup_entry_w_hash(sources, source, hash)) + lh_table_insert_w_hash(sources, source, NULL, hash, 0); + } + } + + struct lh_entry *e; + size_t i = 0; + + lh_foreach(sources, e) { + uc_source_t *source = lh_entry_k(e); + + printf("#%2zu %s\n", i++, source->filename); + } + + lh_table_free(sources); + + return true; +} + +static bool +cmd_print(uc_vm_t *vm, debug_breakpoint_t *dbk) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + char *expr = strtok(NULL, "\n"); + + if (!expr || !*expr) { + printf("Usage: print expr\n"); + + return true; + } + + uc_value_t *res = NULL; + + if (eval_expr(vm, frame, expr, &res)) { + print_value3(vm, res, true); + printf("\n"); + + ucv_put(res); + } + + return true; +} + + +static bool +cmd_quit(uc_vm_t *vm, debug_breakpoint_t *dbk) +{ + bool proceed = true; + size_t linelen = 0; + char *line = NULL; + + while (true) { + printf("Terminate program? (y/n) > "); + + if (getline(&line, &linelen, stdin) == -1) + break; + + if (line && *line == 'y') { + vm->arg.s32 = -1; + uc_vm_raise_exception(vm, EXCEPTION_EXIT, "Terminated"); + + proceed = false; + break; + } + else if (line && *line == 'n') { + break; + } + } + + free(line); + + return proceed; +} + +static void +handle_breakpoint(uc_vm_t *vm, uc_breakpoint_t *bk) +{ + uc_callframe_t *frame = uc_vector_last(&vm->callframes); + debug_breakpoint_t *dbk = (debug_breakpoint_t *)bk; + size_t linelen = 0; + char *line = NULL; + + print_location(vm, "Paused execution in "); + + if (frame->closure && 0) { + uc_function_t *function = frame->closure->function; + uc_source_t *source = uc_program_function_source(function); + + size_t off = (frame->ip - function->chunk.entries) - 1; + size_t srcpos = uc_program_function_srcpos(function, off); + + uc_stringbuf_t *context = xprintbuf_new(); + + uc_source_context_format(context, + uc_program_function_source(function), srcpos, true); + + size_t line = uc_source_get_line(source, &srcpos); + + printf("Paused execution in %s, line %zu, byte %zu\n", + source->filename, line, srcpos); + + printf("%.*s\n", context->bpos, context->buf); + + printbuf_free(context); + } + + while (true) { + printf("dbg > "); + + if (getline(&line, &linelen, stdin) == -1) + break; + + char *p = strtok(line, " \t\r\n"); + size_t l = p ? strlen(p) : 0; + size_t i; + + for (i = 0; l > 0 && i < ARRAY_SIZE(commands); i++) { + if (strncmp(commands[i].command, p, l)) + continue; + + if (!commands[i].cb(vm, dbk)) + goto out; + + break; + } + + if (l > 0 && i == ARRAY_SIZE(commands)) + printf("Unrecognized command '%s'\n", p); + } + +out: + free(line); + + if (dbk->kind == BK_ONCE) + delete_breakpoint(vm, dbk); +} + +static uc_value_t * +uc_breakpoint(uc_vm_t *vm, size_t nargs) +{ + uc_value_t *path = uc_fn_arg(0); + uc_value_t *line = uc_fn_arg(1); + uc_value_t *byte = uc_fn_arg(2); + + const char *p = ucv_string_get(path); + size_t lnum = ucv_to_unsigned(line); + size_t bnum = ucv_to_unsigned(byte); + + uc_source_t *source = NULL; + uc_program_t *prog = NULL; + + if (!p) + return NULL; + + size_t off = srcpos_to_srcoff(vm, p, lnum, bnum, &source, &prog); + + uc_value_t *ret = ucv_array_new(vm); + + ucv_array_push(ret, ucv_uint64_new(off)); + ucv_array_push(ret, ucv_string_new(source->filename)); + ucv_array_push(ret, &prog->header); + + if (prog) { + uc_function_t *bfn = NULL; + size_t insn = 0; + + uc_program_function_foreach(prog, fn) { + if (uc_program_function_source(fn) != source) + continue; + + uc_offsetinfo_t *offsets = &fn->chunk.debuginfo.offsets; + + if (!offsets->count) + continue; + + size_t beg = uc_program_function_srcpos(fn, 0); + size_t end = uc_program_function_srcpos(fn, SIZE_MAX); + + if (off < beg || off > end) + continue; + + bfn = fn; + insn = srcoff_to_insnoff(fn, off); + +#if 0 + fprintf(stderr, + "%s:%zu:%zu -> %zu -> %zu {%s}\n", + p, lnum, bnum, off, insn, + insns[fn->chunk.entries[insn]]); +#endif + } + + if (bfn) { + patch_breakpoint(vm, bfn, insn, BK_USER); + } + } + + return ret; +} + +static uc_value_t * +uc_debug_sigint_handler(uc_vm_t *vm, size_t nargs) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + + if (!frame) + return NULL; + + debug_breakpoint_t dbk = { + .bk = { .ip = frame->ip }, + .fn = frame->closure->function, + .kind = BK_ONCE + }; + + handle_breakpoint(vm, &dbk.bk); + + uc_value_t *sigint_handler = + uc_vm_registry_get(vm, "debug.orig_int_signal"); + + if (ucv_is_callable(sigint_handler)) { + uc_vm_stack_push(vm, ucv_get(sigint_handler)); + uc_vm_stack_push(vm, ucv_get(uc_fn_arg(0))); + + if (uc_vm_call(vm, false, 1) == EXCEPTION_NONE) + return uc_vm_stack_pop(vm); + } + + return NULL; +} + +static uc_value_t * +uc_debugger(uc_vm_t *vm, size_t nargs) +{ + uc_cfn_ptr_t ucsignal = uc_stdlib_function("signal"); + uc_value_t *mainfn = uc_fn_arg(0); + + if (ucv_type(mainfn) != UC_CLOSURE) + return NULL; + + uc_vm_stack_push(vm, ucv_string_new("SIGINT")); + uc_vm_registry_set(vm, "debug.orig_int_signal", ucsignal(vm, 1)); + ucv_put(uc_vm_stack_pop(vm)); + + uc_vm_stack_push(vm, ucv_string_new("SIGINT")); + uc_vm_stack_push(vm, + ucv_cfunction_new("debug_sigint_handler", uc_debug_sigint_handler)); + ucv_put(ucsignal(vm, 2)); + ucv_put(uc_vm_stack_pop(vm)); + ucv_put(uc_vm_stack_pop(vm)); + + patch_breakpoint(vm, ((uc_closure_t *)mainfn)->function, 0, BK_ONCE); + + return NULL; +} + + static const uc_function_list_t debug_fns[] = { { "memdump", uc_memdump }, { "traceback", uc_traceback }, @@ -1623,6 +2907,8 @@ static const uc_function_list_t debug_fns[] = { { "setlocal", uc_setlocal }, { "getupval", uc_getupval }, { "setupval", uc_setupval }, + { "breakpoint", uc_breakpoint }, + { "debugger", uc_debugger }, }; void uc_module_init(uc_vm_t *vm, uc_value_t *scope) diff --git a/main.c b/main.c index a51c3707..3fe75786 100644 --- a/main.c +++ b/main.c @@ -110,9 +110,12 @@ print_usage(const char *app) app); } +static bool +parse_library_load(char *opt, uc_vm_t *vm); static int -compile(uc_vm_t *vm, uc_source_t *src, FILE *precompile, bool strip, char *interp, bool print_result) +compile(uc_vm_t *vm, uc_source_t *src, FILE *precompile, bool strip, + char *interp, bool print_result, bool debugger) { uc_value_t *res = NULL; uc_program_t *program; @@ -140,6 +143,30 @@ compile(uc_vm_t *vm, uc_source_t *src, FILE *precompile, bool strip, char *inter if (vm->gc_interval) uc_vm_gc_start(vm, vm->gc_interval); + if (debugger) { + if (!parse_library_load("debug", vm)) { + fprintf(stderr, "Unable to load debug module\n"); + rc = -2; + goto out; + } + + uc_value_t *dbgmod = ucv_object_get(uc_vm_scope_get(vm), "debug", NULL); + uc_value_t *dbgfn = ucv_object_get(dbgmod, "debugger", NULL); + + if (ucv_type(dbgfn) != UC_CFUNCTION) { + fprintf(stderr, "Unable to locate debugger function\n"); + rc = -2; + goto out; + } + + uc_vm_stack_push(vm, ucv_get(dbgfn)); + uc_vm_stack_push(vm, + ucv_closure_new(vm, uc_program_entry(program), false)); + + if (uc_vm_call(vm, false, 1) == EXCEPTION_NONE) + ucv_put(uc_vm_stack_pop(vm)); + } + rc = uc_vm_execute(vm, program, &res); switch (rc) { @@ -486,8 +513,8 @@ appname(const char *argv0) int main(int argc, char **argv) { - const char *optspec = "he:p:tg:ST::RD:F:U:l:L:c::o:s"; - bool strip = false, print_result = false; + const char *optspec = "he:p:tg:ST::RD:F:U:l:L:c::o:sx"; + bool strip = false, print_result = false, debugger = false; char *interp = "/usr/bin/env ucode"; uc_source_t *source = NULL; FILE *precompile = NULL; @@ -622,6 +649,10 @@ main(int argc, char **argv) case 'o': outfile = optarg; break; + + case 'x': + debugger = true; + break; } } @@ -671,7 +702,7 @@ main(int argc, char **argv) ucv_put(o); - rv = compile(&vm, source, precompile, strip, interp, print_result); + rv = compile(&vm, source, precompile, strip, interp, print_result, debugger); out: uc_search_path_free(&config.module_search_path); diff --git a/vm.c b/vm.c index bb6dc2f2..0f9c22ee 100644 --- a/vm.c +++ b/vm.c @@ -37,7 +37,7 @@ static const char *insn_names[__I_MAX] = { __insns }; -static const int8_t insn_operand_bytes[__I_MAX] = { +const int8_t insn_operand_bytes[__I_MAX] = { [I_LOAD] = 4, [I_LOAD8] = 1, [I_LOAD16] = 2, @@ -235,6 +235,11 @@ void uc_vm_free(uc_vm_t *vm) free(vm->restypes.entries[i]); uc_vector_clear(&vm->restypes); + + for (i = 0; i < vm->breakpoints.count; i++) + free(vm->breakpoints.entries[i]); + + uc_vector_clear(&vm->breakpoints); } static uc_chunk_t * @@ -284,6 +289,11 @@ uc_vm_decode_insn(uc_vm_t *vm, uc_callframe_t *frame, uc_chunk_t *chunk) assert(frame->ip < end); + for (size_t i = 0; i < vm->breakpoints.count; i++) + if (vm->breakpoints.entries[i] != 0 && + vm->breakpoints.entries[i]->ip == frame->ip) + vm->breakpoints.entries[i]->cb(vm, vm->breakpoints.entries[i]); + insn = frame->ip[0]; frame->ip++;