diff --git a/CMakeLists.txt b/CMakeLists.txt index 1bfb05e6..d6cedb99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -228,13 +228,14 @@ set(LIBRARIES "") if(DEBUG_SUPPORT) set(LIBRARIES ${LIBRARIES} debug_lib) - add_library(debug_lib MODULE lib/debug.c) + add_library(debug_lib MODULE lib/debug.c lib/debug_remote.c lib/debug_proto.c) set_target_properties(debug_lib PROPERTIES OUTPUT_NAME debug PREFIX "") target_link_options(debug_lib PRIVATE ${UCODE_MODULE_LINK_OPTIONS}) + target_link_libraries(debug_lib PRIVATE libucode) if(libubox) find_path(uloop_include_dir NAMES libubox/uloop.h) include_directories(${uloop_include_dir}) - target_link_libraries(debug_lib ${libubox} ${libucode}) + target_link_libraries(debug_lib PRIVATE ${libubox} ${libucode}) target_compile_definitions(debug_lib PRIVATE HAVE_ULOOP) endif() endif() @@ -451,7 +452,9 @@ if(UNIT_TESTING) endif() endif() -install(TARGETS ucode RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +add_executable(udbg udbg.c debug_highlight.c debug_lineedit.c) +target_link_libraries(udbg PRIVATE libucode ${JSONC_LINK_LIBRARIES}) +install(TARGETS ucode udbg RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(TARGETS libucode LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) install(TARGETS ${LIBRARIES} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/ucode) diff --git a/chunk.c b/chunk.c index 63a70648..92c7efb7 100644 --- a/chunk.c +++ b/chunk.c @@ -20,14 +20,11 @@ #include "ucode/types.h" #include "ucode/util.h" -#define OFFSETINFO_BITS (sizeof(((uc_offsetinfo_t *)NULL)->entries[0]) * 8) -#define OFFSETINFO_BYTE_BITS 3 -#define OFFSETINFO_INSN_BITS (OFFSETINFO_BITS - OFFSETINFO_BYTE_BITS) -#define OFFSETINFO_MAX_BYTES ((1 << OFFSETINFO_BYTE_BITS) - 1) -#define OFFSETINFO_MAX_INSNS ((1 << OFFSETINFO_INSN_BITS) - 1) -#define OFFSETINFO_NUM_BYTES(n) ((n) & OFFSETINFO_MAX_BYTES) -#define OFFSETINFO_NUM_INSNS(n) ((n) >> OFFSETINFO_BYTE_BITS) -#define OFFSETINFO_ENCODE(line, insns) ((line & OFFSETINFO_MAX_BYTES) | (((insns) << OFFSETINFO_BYTE_BITS) & ~OFFSETINFO_MAX_BYTES)) +#define OFFSETINFO_MAX_BYTES 127 +#define OFFSETINFO_MAX_INSNS 127 +#define OFFSETINFO_NUM_BYTES(o) ((o)->bytes & OFFSETINFO_MAX_BYTES) +#define OFFSETINFO_NUM_INSNS(o) ((o)->insns & OFFSETINFO_MAX_INSNS) +#define OFFSETINFO_IS_END(o) ((o)->insns & 0x80) void @@ -69,38 +66,39 @@ uc_chunk_add(uc_chunk_t *chunk, uint8_t byte, size_t offset) uc_vector_push(chunk, byte); - /* offset info is encoded in bytes, for each byte, the first three bits - * specify the number of source text bytes to advance since the last entry - * and the remaining five bits specify the amount of instructions belonging - * to any given source text offset */ + /* Offset info is encoded in byte pairs, the first byte specifies the number + * of source text bytes to advance since the last entry and the second byte + * specifies the amount of instructions belonging to the source text offset. + * Byte and instruction count values are limited to 7 bits (0x00..0x7f), + * the most significant bit in each byte is reserved as flag value; if the + * bit is set in the first byte, it signals the begin of a logical statement + * while a set bit in the second byte denotes the end of the statement. */ if (offset > 0 || offsets->count == 0) { - /* if this offset is farther than seven (2 ** 3 - 1) bytes apart from + /* If this offset is farther than 127 (2 ** 7 - 1) bytes apart from * the last one, we need to emit intermediate "jump" bytes with zero * instructions each */ for (i = offset; i > OFFSETINFO_MAX_BYTES; i -= OFFSETINFO_MAX_BYTES) { - /* advance by 7 bytes */ - uc_vector_push(offsets, OFFSETINFO_ENCODE(OFFSETINFO_MAX_BYTES, 0)); + /* advance by 127 bytes */ + uc_vector_push(offsets, { OFFSETINFO_MAX_BYTES, 0 }); } /* advance by `i` bytes, count one instruction */ - uc_vector_push(offsets, OFFSETINFO_ENCODE(i, 1)); + uc_vector_push(offsets, { i, 1 }); } /* update instruction count at current offset entry */ else { - /* since we encode the per-offset instruction count in five bits, we - * can only count up to 31 instructions. If we exceed that limit, - * emit another offset entry with the initial three bits set to zero */ - if (OFFSETINFO_NUM_INSNS(offsets->entries[offsets->count - 1]) >= OFFSETINFO_MAX_INSNS) { + uc_offset_t *o = uc_vector_last(offsets); + + /* since we encode the per-offset instruction count in seven bits, we + * can only count up to 127 instructions. If we exceed that limit, + * emit another offset entry with the byte offset set to zero */ + if (OFFSETINFO_NUM_INSNS(o) >= OFFSETINFO_MAX_INSNS) { /* advance by 0 bytes, count one instruction */ - uc_vector_push(offsets, OFFSETINFO_ENCODE(0, 1)); + uc_vector_push(offsets, { 0, 1 }); } else { - uint8_t *prev = uc_vector_last(offsets); - - *prev = OFFSETINFO_ENCODE( - OFFSETINFO_NUM_BYTES(*prev), - OFFSETINFO_NUM_INSNS(*prev) + 1); + o->insns++; } } @@ -108,24 +106,56 @@ uc_chunk_add(uc_chunk_t *chunk, uint8_t byte, size_t offset) } void -uc_chunk_pop(uc_chunk_t *chunk) +uc_chunk_stmt_start(uc_chunk_t *chunk, size_t offset) { uc_offsetinfo_t *offsets = &chunk->debuginfo.offsets; - int n_insns; + size_t i; - assert(chunk->count > 0); + for (i = offset; i > OFFSETINFO_MAX_BYTES; i -= OFFSETINFO_MAX_BYTES) { + /* advance by 127 bytes */ + uc_vector_push(offsets, { OFFSETINFO_MAX_BYTES, 0 }); + } - chunk->count--; + /* advance by `i` bytes, set start of statement flag */ + uc_vector_push(offsets, { i | 0x80, 0 }); +} - n_insns = OFFSETINFO_NUM_INSNS(offsets->entries[offsets->count - 1]); +void +uc_chunk_stmt_end(uc_chunk_t *chunk, size_t offset) +{ + uc_offsetinfo_t *offsets = &chunk->debuginfo.offsets; + uc_offset_t *o = offsets->count ? uc_vector_last(offsets) : NULL; + size_t i; - if (n_insns > 0) { - uint8_t *prev = uc_vector_last(offsets); + for (i = offset; i > OFFSETINFO_MAX_BYTES; i -= OFFSETINFO_MAX_BYTES) { + /* advance by 127 bytes */ + uc_vector_push(offsets, { OFFSETINFO_MAX_BYTES, 0 }); + } - *prev = OFFSETINFO_ENCODE(OFFSETINFO_NUM_BYTES(*prev), n_insns - 1); + if (i > 0 || o == NULL || OFFSETINFO_IS_END(o)) { + /* advance by `i` bytes, set start of statement flag */ + uc_vector_push(offsets, { i, 0x80 }); } else { - offsets->count--; + /* set end flag on last offset entry */ + o->insns |= 0x80; + } +} + +void +uc_chunk_pop(uc_chunk_t *chunk) +{ + assert(chunk->count > 0); + + chunk->count--; + + for (size_t i = chunk->debuginfo.offsets.count; i > 0; i--) { + uc_offset_t *o = &chunk->debuginfo.offsets.entries[i - 1]; + + if (o->insns & 127) { + o->insns = ((o->insns & 127) - 1) | (o->insns & 128); + break; + } } } @@ -133,17 +163,17 @@ size_t uc_chunk_debug_get_srcpos(uc_chunk_t *chunk, size_t off) { uc_offsetinfo_t *offsets = &chunk->debuginfo.offsets; - size_t i, inum = 0, lnum = 0; + size_t i, inum = 0, bnum = 0; if (!offsets->count) return 0; for (i = 0; i < offsets->count && inum < off; i++) { - lnum += OFFSETINFO_NUM_BYTES(offsets->entries[i]); - inum += OFFSETINFO_NUM_INSNS(offsets->entries[i]); + bnum += OFFSETINFO_NUM_BYTES(&offsets->entries[i]); + inum += OFFSETINFO_NUM_INSNS(&offsets->entries[i]); } - return lnum; + return bnum; } void diff --git a/compiler.c b/compiler.c index 1f468cac..a752da59 100644 --- a/compiler.c +++ b/compiler.c @@ -480,6 +480,22 @@ uc_compiler_reladdr32(uc_compiler_t *compiler, size_t from, size_t to) return (size_t)(delta + 0x7fffffff); } +static void +uc_compiler_emit_stmt_start(uc_compiler_t *compiler, uc_token_t *tok) +{ + uc_chunk_stmt_start( + uc_compiler_current_chunk(compiler), + uc_compiler_set_srcpos(compiler, tok->pos)); +} + +static void +uc_compiler_emit_stmt_end(uc_compiler_t *compiler) +{ + uc_chunk_stmt_end( + uc_compiler_current_chunk(compiler), + uc_compiler_set_srcpos(compiler, compiler->parser->prev.end)); +} + static size_t uc_compiler_reladdr16(uc_compiler_t *compiler, size_t from, size_t to) { @@ -1413,9 +1429,15 @@ uc_compiler_compile_nullish_assignment(uc_compiler_t *compiler, uc_value_t *var) } static void -uc_compiler_compile_expression(uc_compiler_t *compiler) +uc_compiler_compile_expression(uc_compiler_t *compiler, bool tag_stmt) { + if (tag_stmt) + uc_compiler_emit_stmt_start(compiler, &compiler->parser->curr); + uc_compiler_parse_precedence(compiler, P_COMMA); + + if (tag_stmt) + uc_compiler_emit_stmt_end(compiler); } static bool @@ -1518,8 +1540,10 @@ uc_compiler_compile_arrowfn(uc_compiler_t *compiler, uc_value_t *args, bool rest } } else { + uc_compiler_emit_stmt_start(&fncompiler, &compiler->parser->curr); uc_compiler_parse_precedence(&fncompiler, P_ASSIGN); uc_compiler_emit_insn(&fncompiler, 0, I_RETURN); + uc_compiler_emit_stmt_end(&fncompiler); } /* emit load instruction for function value */ @@ -1693,7 +1717,7 @@ uc_compiler_compile_paren(uc_compiler_t *compiler) * expression or reached the closing paren. If neither applies, we have a * syntax error. */ if (!uc_compiler_parse_check(compiler, TK_RPAREN)) - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, false); /* A subsequent slash cannot be a regular expression literal */ compiler->parser->lex.no_regexp = true; @@ -1867,7 +1891,7 @@ uc_compiler_compile_template(uc_compiler_t *compiler) uc_compiler_emit_insn(compiler, 0, I_ADD); } else if (uc_compiler_parse_match(compiler, TK_PLACEH)) { - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, true); uc_compiler_emit_insn(compiler, 0, I_ADD); uc_compiler_parse_consume(compiler, TK_RBRACE); } @@ -1880,7 +1904,7 @@ uc_compiler_compile_template(uc_compiler_t *compiler) static void uc_compiler_compile_comma(uc_compiler_t *compiler) { - uc_compiler_emit_insn(compiler, 0, I_POP); + uc_compiler_emit_insn(compiler, compiler->parser->curr.pos, I_POP); uc_compiler_parse_precedence(compiler, P_ASSIGN); } @@ -2114,7 +2138,7 @@ uc_compiler_compile_subscript(uc_compiler_t *compiler) (1u << UC_ARRAY) | (1u << UC_OBJECT) | (1u << UC_RESOURCE), 0); /* compile lhs */ - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, false); /* no regexp literal possible after computed property access */ compiler->parser->lex.no_regexp = true; @@ -2490,15 +2514,19 @@ uc_compiler_compile_declexpr(uc_compiler_t *compiler, bool constant) static void uc_compiler_compile_local(uc_compiler_t *compiler) { + uc_compiler_emit_stmt_start(compiler, &compiler->parser->prev); uc_compiler_compile_declexpr(compiler, false); uc_compiler_parse_consume(compiler, TK_SCOL); + uc_compiler_emit_stmt_end(compiler); } static void uc_compiler_compile_const(uc_compiler_t *compiler) { + uc_compiler_emit_stmt_start(compiler, &compiler->parser->prev); uc_compiler_compile_declexpr(compiler, true); uc_compiler_parse_consume(compiler, TK_SCOL); + uc_compiler_emit_stmt_end(compiler); } static uc_tokentype_t @@ -2536,7 +2564,7 @@ uc_compiler_compile_if(uc_compiler_t *compiler) /* parse & compile condition expression */ uc_compiler_parse_consume(compiler, TK_LPAREN); - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, true); uc_compiler_parse_consume(compiler, TK_RPAREN); /* conditional jump to else/elif branch */ @@ -2560,7 +2588,7 @@ uc_compiler_compile_if(uc_compiler_t *compiler) /* parse & compile elsif condition */ uc_compiler_parse_advance(compiler); uc_compiler_parse_consume(compiler, TK_LPAREN); - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, true); uc_compiler_parse_consume(compiler, TK_RPAREN); uc_compiler_parse_consume(compiler, TK_COLON); @@ -2645,7 +2673,7 @@ uc_compiler_compile_while(uc_compiler_t *compiler) /* parse & compile loop condition */ uc_compiler_parse_consume(compiler, TK_LPAREN); - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, true); uc_compiler_parse_consume(compiler, TK_RPAREN); /* conditional jump to end */ @@ -2707,7 +2735,8 @@ uc_compiler_compile_for_in(uc_compiler_t *compiler, bool local, uc_token_t *kvar } /* value to iterate */ - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, false); + uc_compiler_emit_stmt_end(compiler); uc_compiler_parse_consume(compiler, TK_RPAREN); uc_compiler_emit_insn(compiler, 0, I_SLOC); uc_compiler_emit_u32(compiler, 0, val_slot); @@ -2829,7 +2858,7 @@ uc_compiler_compile_for_count(uc_compiler_t *compiler, bool local, uc_token_t *p } /* ... otherwise an unrelated expression */ else { - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, false); uc_compiler_emit_insn(compiler, 0, I_POP); } } @@ -2838,10 +2867,11 @@ uc_compiler_compile_for_count(uc_compiler_t *compiler, bool local, uc_token_t *p } /* ... otherwise try parsing an entire expression (which might be absent) */ else if (!uc_compiler_parse_check(compiler, TK_SCOL)) { - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, false); uc_compiler_emit_insn(compiler, 0, I_POP); } + uc_compiler_emit_stmt_end(compiler); uc_compiler_parse_consume(compiler, TK_SCOL); @@ -2849,7 +2879,7 @@ uc_compiler_compile_for_count(uc_compiler_t *compiler, bool local, uc_token_t *p if (!uc_compiler_parse_check(compiler, TK_SCOL)) { cond_off = chunk->count; - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, true); test_off = uc_compiler_emit_jmpz(compiler, 0); } @@ -2864,7 +2894,7 @@ uc_compiler_compile_for_count(uc_compiler_t *compiler, bool local, uc_token_t *p incr_off = chunk->count; if (!uc_compiler_parse_check(compiler, TK_RPAREN)) { - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, true); uc_compiler_emit_insn(compiler, 0, I_POP); } @@ -2915,6 +2945,8 @@ uc_compiler_compile_for(uc_compiler_t *compiler) uc_compiler_parse_consume(compiler, TK_LPAREN); + uc_compiler_emit_stmt_start(compiler, &compiler->parser->curr); + /* check the next few tokens and see if we have either a * `let x in` / `let x, y` expression or an ordinary initializer * statement */ @@ -2979,7 +3011,7 @@ uc_compiler_compile_switch(uc_compiler_t *compiler) /* parse and compile match value */ uc_compiler_parse_consume(compiler, TK_LPAREN); - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, true); uc_compiler_parse_consume(compiler, TK_RPAREN); uc_compiler_parse_consume(compiler, TK_LBRACE); @@ -3025,7 +3057,7 @@ uc_compiler_compile_switch(uc_compiler_t *compiler) skip_jmp = uc_compiler_emit_jmp(compiler, 0); /* compile case value expression */ - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, false); uc_compiler_parse_consume(compiler, TK_COLON); /* Store three values in case offset list: @@ -3275,7 +3307,7 @@ uc_compiler_compile_tplexp(uc_compiler_t *compiler) uc_chunk_t *chunk = uc_compiler_current_chunk(compiler); size_t off = chunk->count; - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, true); /* XXX: the lexer currently emits a superfluous trailing semicolon... */ uc_compiler_parse_match(compiler, TK_SCOL); @@ -3318,7 +3350,7 @@ uc_compiler_compile_expstmt(uc_compiler_t *compiler) if (uc_compiler_parse_match(compiler, TK_SCOL)) return TK_NULL; - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, false); /* allow omitting final semicolon */ switch (compiler->parser->curr.type) { @@ -3359,32 +3391,39 @@ uc_compiler_compile_statement(uc_compiler_t *compiler) compiler->exprstack = &expr; - if (uc_compiler_parse_match(compiler, TK_IF)) - uc_compiler_compile_if(compiler); - else if (uc_compiler_parse_match(compiler, TK_WHILE)) - uc_compiler_compile_while(compiler); - else if (uc_compiler_parse_match(compiler, TK_FOR)) - uc_compiler_compile_for(compiler); - else if (uc_compiler_parse_match(compiler, TK_SWITCH)) - uc_compiler_compile_switch(compiler); - else if (uc_compiler_parse_match(compiler, TK_TRY)) - uc_compiler_compile_try(compiler); - else if (uc_compiler_parse_match(compiler, TK_FUNC)) - uc_compiler_compile_funcdecl(compiler); - else if (uc_compiler_parse_match(compiler, TK_BREAK)) - uc_compiler_compile_control(compiler); - else if (uc_compiler_parse_match(compiler, TK_CONTINUE)) - uc_compiler_compile_control(compiler); - else if (uc_compiler_parse_match(compiler, TK_RETURN)) - uc_compiler_compile_return(compiler); - else if (uc_compiler_parse_match(compiler, TK_TEXT)) - uc_compiler_compile_text(compiler); - else if (uc_compiler_parse_match(compiler, TK_LEXP)) - uc_compiler_compile_tplexp(compiler); - else if (uc_compiler_parse_match(compiler, TK_LBRACE)) + if (uc_compiler_parse_match(compiler, TK_LBRACE)) { last_statement_type = uc_compiler_compile_block(compiler); - else - last_statement_type = uc_compiler_compile_expstmt(compiler); + } + else { + uc_compiler_emit_stmt_start(compiler, &compiler->parser->curr); + + if (uc_compiler_parse_match(compiler, TK_IF)) + uc_compiler_compile_if(compiler); + else if (uc_compiler_parse_match(compiler, TK_WHILE)) + uc_compiler_compile_while(compiler); + else if (uc_compiler_parse_match(compiler, TK_FOR)) + uc_compiler_compile_for(compiler); + else if (uc_compiler_parse_match(compiler, TK_SWITCH)) + uc_compiler_compile_switch(compiler); + else if (uc_compiler_parse_match(compiler, TK_TRY)) + uc_compiler_compile_try(compiler); + else if (uc_compiler_parse_match(compiler, TK_FUNC)) + uc_compiler_compile_funcdecl(compiler); + else if (uc_compiler_parse_match(compiler, TK_BREAK)) + uc_compiler_compile_control(compiler); + else if (uc_compiler_parse_match(compiler, TK_CONTINUE)) + uc_compiler_compile_control(compiler); + else if (uc_compiler_parse_match(compiler, TK_RETURN)) + uc_compiler_compile_return(compiler); + else if (uc_compiler_parse_match(compiler, TK_TEXT)) + uc_compiler_compile_text(compiler); + else if (uc_compiler_parse_match(compiler, TK_LEXP)) + uc_compiler_compile_tplexp(compiler); + else + last_statement_type = uc_compiler_compile_expstmt(compiler); + + uc_compiler_emit_stmt_end(compiler); + } compiler->exprstack = expr.parent; @@ -3474,8 +3513,11 @@ uc_compiler_compile_export(uc_compiler_t *compiler) return; } + uc_compiler_emit_stmt_start(compiler, &compiler->parser->prev); + if (uc_compiler_parse_match(compiler, TK_LBRACE)) { uc_compiler_compile_exportlist(compiler); + uc_compiler_emit_stmt_end(compiler); return; } @@ -3493,7 +3535,7 @@ uc_compiler_compile_export(uc_compiler_t *compiler) return; } else if (uc_compiler_parse_match(compiler, TK_DEFAULT)) - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, false); else uc_compiler_syntax_error(compiler, compiler->parser->curr.pos, "Unexpected token\nExpecting 'let', 'const', 'function', 'default' or '{'"); @@ -3515,6 +3557,8 @@ uc_compiler_compile_export(uc_compiler_t *compiler) } uc_compiler_parse_consume(compiler, TK_SCOL); + + uc_compiler_emit_stmt_end(compiler); } static uc_program_t * @@ -3937,7 +3981,7 @@ uc_compiler_compile_importcall(uc_compiler_t *compiler) static uc_tokentype_t uc_compiler_compile_import(uc_compiler_t *compiler) { - uc_value_t *namelist; + uc_value_t *namelist = ucv_array_new(NULL); /* import(...) */ if (uc_compiler_parse_check(compiler, TK_LPAREN)) { @@ -3953,10 +3997,12 @@ uc_compiler_compile_import(uc_compiler_t *compiler) uc_compiler_syntax_error(compiler, compiler->parser->prev.pos, "Imports may only appear at top level"); + ucv_put(namelist); + return TK_IMPORT; } - namelist = ucv_array_new(NULL); + uc_compiler_emit_stmt_start(compiler, &compiler->parser->prev); /* import { ... } from */ if (uc_compiler_parse_match(compiler, TK_LBRACE)) { @@ -4014,6 +4060,8 @@ uc_compiler_compile_import(uc_compiler_t *compiler) uc_compiler_parse_consume(compiler, TK_SCOL); + uc_compiler_emit_stmt_end(compiler); + ucv_put(namelist); return TK_IMPORT; diff --git a/debug_highlight.c b/debug_highlight.c new file mode 100644 index 00000000..9a66400d --- /dev/null +++ b/debug_highlight.c @@ -0,0 +1,1029 @@ +/* + * Copyright (C) 2026 Jo-Philipp Wich + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ANY IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include + +#include "debug_highlight.h" + +/* -- styling, ported verbatim from the pre-protocol lib/debug.c ---------- */ + +enum { + BOLD = (1 << 0), + FAINT = (1 << 1), + ULINE = (1 << 2), +}; + +typedef enum { + FG_NONE = 0, + FG_BLACK = 30, + FG_RED = 31, + FG_GREEN = 32, + FG_YELLOW = 33, + FG_BLUE = 34, + FG_MAGENTA = 35, + FG_CYAN = 36, + FG_GRAY = 37, + FG_BBLACK = 90, + FG_BRED = 91, + FG_BGREEN = 92, + FG_BYELLOW = 93, + FG_BBLUE = 94, + FG_BMAGENT = 95, + FG_BCYAN = 96, + FG_BWHITE = 97, +} fg_color_t; + +typedef enum { + BG_NONE = 0, + BG_BLACK = 40, + BG_GRAY = 100, +} bg_color_t; + +typedef struct { + fg_color_t fg; + bg_color_t bg; + unsigned int styles; +} style_t; + +static void +cs(FILE *out, const style_t *style) +{ + int codes[8] = { 0 }; + size_t i = 0; + + if (style == NULL) { + fputs("\033[0m", out); + return; + } + + if ((style->styles & (BOLD | FAINT | ULINE)) == 0) + codes[i++] = 0; + + if (style->styles & BOLD) codes[i++] = 1; + if (style->styles & FAINT) codes[i++] = 2; + if (style->styles & ULINE) codes[i++] = 4; + + codes[i++] = style->fg ? style->fg : 39; + codes[i++] = style->bg ? style->bg : 49; + + fputs("\033[", out); + + for (size_t n = 0; n < i; n++) + fprintf(out, "%s%d", n ? ";" : "", codes[n]); + + fputc('m', out); +} + +/* -- syntax highlighting rules, ported verbatim -------------------------- */ + +static struct { + fg_color_t color; + const char *start, *end; +} highlight_rules[] = { + { FG_GRAY, "^#!.*", NULL }, + + /* declarations */ + { FG_GREEN, "\\<(let|const|function|this)\\>", NULL }, + + /* arrow functions */ + { FG_GREEN, "(\\<\\w+\\>|\\([[:alnum:][:space:]_,.]*\\))[[:space:]]*=>", NULL }, + + /* flow control */ + { FG_BYELLOW, "\\<(while|if|else|elif|switch|case|default|for|in|endif|endfor|endwhile|endfunction)\\>", NULL }, + + /* keywords */ + { FG_BYELLOW, "\\<(export|import|try|catch|delete)\\>", NULL }, + + /* exit points */ + { FG_MAGENTA, "\\<(break|continue|return)\\>", NULL }, + + /* numeric literals */ + { FG_CYAN, "\\<([0-9]+\\.[0-9]+([eE][+-]?[0-9]+)?|[0-9]+[eE][+-]?[0-9]+)\\>", NULL }, + { FG_CYAN, "\\<0[xX][[:xdigit:]]+(\\.[[:xdigit:]]+)?\\>", NULL }, + { FG_CYAN, "\\<(0[oO][0-7]+|0[bB][01]+|[0-9]+)\\>", NULL }, + + /* special values */ + { FG_CYAN, "\\<(true|false|null|NaN|Infinity)\\>", NULL }, + + /* strings */ + { FG_BMAGENT, "\"([^\"\\{%#}]|\\\\.|\\{[^\"\\{%#]|[%#}][^\"\\}]|[{%#}]\\\\.)*[{%#}]?\"", NULL }, + { FG_BMAGENT, "'([^'\\{%#}]|\\\\.|\\{[^'\\{%#]|[%#}][^'\\}]|[{%#}]\\\\.)*[{%#}]?'", NULL }, + { FG_BMAGENT, "`([^`\\{%#}]|\\\\.|\\{[^`\\{%#]|[%#}][^`\\}]|[{%#}]\\\\.)*[{%#}]?`", NULL }, + + /* template string expressions */ + { FG_BWHITE, "\\$\\{", "}" }, + + /* comments */ + { FG_BBLUE, "(^|[[:blank:]])//.*", NULL }, + { FG_BBLUE, "(^|[[:space:]])/\\*", "\\*/" }, + { FG_BBLUE, "\\{#", "#\\}" }, + + /* text outside template directives */ + { FG_GRAY, "[}%#]\\}", "\\{[{%#]" }, + { FG_GRAY, "^#!.*(\\|[[:space:]]-[[:alnum:]]*T[[:alnum:]]*\\>)", "\\{[{%#]" }, + { FG_GRAY, "^([^{%#}]|\\{[^{%#]|[%#}][^}])+\\{[{%#]", NULL }, + + /* template tags */ + { FG_BWHITE, "\\{[{%][+-]?|-?[%}]\\}", NULL }, + { FG_BBLUE, "\\{#[+-]?|-?#\\}", NULL }, +}; + +#define NRULES (sizeof(highlight_rules) / sizeof(highlight_rules[0])) + +static regex_t compiled_patterns[NRULES * 2]; +static bool have_highlighting = false; +static bool init_attempted = false; + +bool +debug_highlight_init(void) +{ + regex_t *re = NULL; + int err = 0; + size_t i; + + if (init_attempted) + return have_highlighting; + + init_attempted = true; + + for (i = 0; i < NRULES; i++) { + re = &compiled_patterns[i * 2]; + err = regcomp(re, highlight_rules[i].start, REG_EXTENDED); + + if (err != 0) + goto err; + + re = &compiled_patterns[i * 2 + 1]; + + if (highlight_rules[i].end) { + err = regcomp(re, highlight_rules[i].end, REG_EXTENDED); + + if (err != 0) + goto err; + } + } + + have_highlighting = true; + + return true; + +err: + { + char errbuf[128]; + + regerror(err, re, errbuf, sizeof(errbuf)); + fprintf(stderr, "debug_highlight: regex error: %s\n", errbuf); + } + + for (i = 0; i < NRULES * 2; i++) + regfree(&compiled_patterns[i]); + + have_highlighting = false; + + return false; +} + +/* -- source rendering, ported from print_source_location() -------------- + * + * The original computed hl_start/hl_end/cursor_pos as byte offsets into + * the whole source file (it read lines off a live, seekable FILE*, so a + * single running byte counter was the natural coordinate space). This + * version instead receives an already-split line array and a per-line + * column range (`hl`, in the debug protocol's own {line, col} terms), so + * the equivalent bounds are recomputed per line instead of accumulated + * globally - the rendering logic itself (per-character style diffing, tab/ + * control-char placeholders, truncation, background shading) is otherwise + * unchanged. The single ULINE-underlined "current instruction" character + * the original also drew is dropped: the protocol only ever hands clients + * a statement *range*, not that finer-grained instruction position. */ + +typedef struct { + fg_color_t color; + ssize_t from, to; +} color_span_t; + +static color_span_t * +colors_grow(color_span_t *colors, size_t *count, size_t *cap) +{ + if (*count >= *cap) { + size_t newcap = *cap ? *cap * 2 : 16; + color_span_t *p = realloc(colors, newcap * sizeof(*p)); + + if (!p) + return colors; + + colors = p; + *cap = newcap; + } + + return colors; +} + +void +debug_highlight_print_source(FILE *out, char **lines, size_t nlines, + size_t from, size_t to, + const debug_highlight_span_t *hl, + size_t left_pad, size_t columns) +{ + debug_highlight_range_t range = { from, to }; + + debug_highlight_print_source_ranges(out, lines, nlines, 1, &range, hl, left_pad, columns); +} + +void +debug_highlight_print_source_ranges(FILE *out, char **lines, size_t nlines, + size_t nranges, + const debug_highlight_range_t *ranges, + const debug_highlight_span_t *hl, + size_t left_pad, size_t columns) +{ + color_span_t *colors = NULL; + size_t colors_count = 0, colors_cap = 0; + regex_t *ml_rule_re_end = NULL; + fg_color_t ml_rule_color = FG_NONE; + style_t style = { FG_BWHITE, BG_BLACK, 0 }; + size_t linenum, start_line = SIZE_MAX, end_line = 0; + ssize_t last_indent = -1; + size_t r; + + for (r = 0; r < nranges; r++) { + if (ranges[r].from == 0 || ranges[r].to == 0) + continue; + + if (ranges[r].from < start_line) + start_line = ranges[r].from; + + if (ranges[r].to > end_line) + end_line = ranges[r].to; + } + + if (end_line > nlines) + end_line = nlines; + + for (linenum = 1; linenum <= end_line; linenum++) { + const char *linestr = lines[linenum - 1]; + ssize_t linelen = (ssize_t)strlen(linestr); + size_t ml_rule_from = 0; + size_t line_hl_from = SIZE_MAX, line_hl_to = SIZE_MAX; + regmatch_t m; + const char *p; + int rf; + + colors_count = 0; + + /* apply highlighting rules */ + if (have_highlighting) { + size_t i; + + /* single line matches */ + for (i = 0; i < NRULES; i++) { + regex_t *re = &compiled_patterns[i * 2]; + + if (highlight_rules[i].end != NULL) + continue; + + for (rf = 0, p = linestr; + regexec(re, p, 1, &m, rf) == 0; + rf = REG_NOTBOL, p += m.rm_eo) { + colors = colors_grow(colors, &colors_count, &colors_cap); + colors[colors_count++] = (color_span_t){ + .color = highlight_rules[i].color, + .from = p + m.rm_so - linestr, + .to = p + m.rm_eo - linestr + }; + + if (m.rm_eo == m.rm_so) + break; + } + } + + /* multi line matches */ + for (rf = 0, p = linestr, ml_rule_from = 0; + rf == 0 || ml_rule_re_end != NULL; + rf = REG_NOTBOL) { + + if (ml_rule_re_end != NULL) { + if (regexec(ml_rule_re_end, p, 1, &m, 0) == 0) { + colors = colors_grow(colors, &colors_count, &colors_cap); + colors[colors_count++] = (color_span_t){ + .color = ml_rule_color, + .from = (ssize_t)ml_rule_from, + .to = p + m.rm_eo - linestr + }; + + ml_rule_re_end = NULL; + ml_rule_color = FG_NONE; + ml_rule_from = 0; + p += m.rm_eo; + } + else { + colors = colors_grow(colors, &colors_count, &colors_cap); + colors[colors_count++] = (color_span_t){ + .color = ml_rule_color, + .from = (ssize_t)ml_rule_from, + .to = linelen + }; + + break; + } + } + + { + size_t i; + bool found = false; + + for (i = 0; i < NRULES; i++) { + regex_t *re_start = &compiled_patterns[i * 2]; + regex_t *re_end = &compiled_patterns[i * 2 + 1]; + + if (highlight_rules[i].end == NULL) + continue; + + if (regexec(re_start, p, 1, &m, rf) == 0) { + ml_rule_re_end = re_end; + ml_rule_color = highlight_rules[i].color; + ml_rule_from = (size_t)(p + m.rm_so - linestr); + p += m.rm_eo; + found = true; + break; + } + } + + if (!found && ml_rule_re_end == NULL) + break; + } + } + } + + { + bool print_line = false, more_lines = false; + + for (r = 0; r < nranges; r++) { + if (ranges[r].from == 0 || ranges[r].to == 0) + continue; + + print_line |= (linenum >= ranges[r].from && linenum <= ranges[r].to); + more_lines |= (ranges[r].from > start_line && ranges[r].from == linenum + 1); + } + + if (!print_line) { + if (more_lines) { + size_t pad = (size_t)(last_indent < 0 ? 0 : last_indent); + size_t i; + + for (i = 0; i < left_pad; i++) + fputc(' ', out); + + cs(out, &((style_t){ FG_GRAY, BG_BLACK, FAINT })); + fputs(" \xe2\x80\xa6 " /* " … " */, out); + + for (i = 0; i < pad; i++) + fputc(' ', out); + + fputs("\xe2\x80\xa6", out); + + if (columns > 6 + pad) + for (i = 0; i < columns - 6 - pad; i++) + fputc(' ', out); + + cs(out, NULL); + fputc('\n', out); + } + + continue; + } + } + + /* per-line highlight bounds, translated from the {line,col} + * range (see comment above) */ + if (hl && hl->from_line > 0 && linenum >= hl->from_line && linenum <= hl->to_line) { + line_hl_from = (linenum == hl->from_line) ? hl->from_col : 0; + line_hl_to = (linenum == hl->to_line) ? hl->to_col : SIZE_MAX; + } + + size_t trunc = 0; + + /* determine display width of line and whether it is too long */ + if (columns > 6) { + size_t c; + ssize_t i; + + for (i = 0, c = 0; i < linelen; i++) { + c += (linestr[i] == '\t') ? 4 : 1; + + if (c > columns - 6) { + trunc = (size_t)(linelen - i); + linelen = i; + break; + } + } + } + + size_t linecols = 0; + ssize_t i; + + for (i = 0; i < (ssize_t)left_pad; i++) + fputc(' ', out); + + cs(out, &((style_t){ FG_GRAY, BG_BLACK, FAINT })); + fprintf(out, "%4zu ", linenum); + cs(out, &style); + + for (i = 0; i < linelen; i++) { + style_t newstyle = { + .fg = FG_BWHITE, + .bg = ((size_t)i >= line_hl_from && (size_t)i < line_hl_to) + ? BG_GRAY : BG_BLACK, + .styles = (hl && hl->have_ip && linenum == hl->ip_line && + (size_t)i == hl->ip_col) ? ULINE : 0 + }; + size_t j; + + for (j = 0; j < colors_count; j++) + if (colors[j].from <= i && colors[j].to > i) + newstyle.fg = colors[j].color; + + if (memcmp(&style, &newstyle, sizeof(style))) { + style = newstyle; + cs(out, &style); + } + + if (linestr[i] == '\t') { + linecols += 4; + cs(out, &((style_t){ FG_BBLACK, style.bg, FAINT })); + fputs("<-> ", out); + cs(out, &style); + } + else if (linestr[i] < ' ' || linestr[i] == 0x7f) { + linecols++; + cs(out, &((style_t){ FG_BBLACK, style.bg, FAINT })); + fputc('.', out); + cs(out, &style); + } + else { + if (last_indent == -1) + last_indent = (ssize_t)linecols; + + linecols++; + fputc(linestr[i], out); + } + } + + /* reset char styles */ + style.styles = 0; + style.bg = ((size_t)linelen >= line_hl_from && (size_t)(linelen) + trunc <= line_hl_to) + ? BG_GRAY : BG_BLACK; + cs(out, &style); + + if (trunc > 0) { + if (columns > 6 && linecols < columns - 6) + for (i = 0; i < (ssize_t)((columns - 6) - linecols); i++) + fputc(' ', out); + + fputs("\xe2\x80\xa6" /* U+2026 HORIZONTAL ELLIPSIS */, out); + } + else if (columns > 5 && linecols < columns - 5) { + if (style.bg != BG_BLACK) { + style.bg = BG_BLACK; + cs(out, &style); + } + + for (i = 0; i < (ssize_t)((columns - 5) - linecols); i++) + fputc(' ', out); + } + + cs(out, &((style_t){ FG_NONE, BG_NONE, 0 })); + fputc('\n', out); + } + + free(colors); +} + +/* -- header bar, ported from format_context_header_backtrace()/ + * format_context_header_callframe() -------------------------------------- */ + +/* Elide the front of `s` (in place) down to at most `maxcols` bytes, + * prefixing a horizontal-ellipsis marker, so the *tail* stays visible - + * matches the original's choice for both filenames (basename matters more + * than the leading directories) and call breadcrumbs (the innermost/ + * current frame matters more than the outermost). Byte-based rather than + * the original's UTF-8/ANSI-escape-aware column counting - a reasonable + * simplification for what is normally short, plain ASCII text (paths, + * identifiers). */ +static char * +truncate_head(const char *s, size_t maxcols) +{ + static const char ellipsis[] = "\xe2\x80\xa6"; /* U+2026, 1 column, 3 bytes */ + size_t len = strlen(s); + char *out; + + if (maxcols == 0 || len <= maxcols) + return strdup(s); + + if (maxcols <= 1) + return strdup(ellipsis); + + out = malloc(sizeof(ellipsis) - 1 + (maxcols - 1) + 1); + memcpy(out, ellipsis, sizeof(ellipsis) - 1); + memcpy(out + sizeof(ellipsis) - 1, s + (len - (maxcols - 1)), maxcols - 1); + out[sizeof(ellipsis) - 1 + (maxcols - 1)] = '\0'; + + return out; +} + +void +debug_highlight_print_header_bar(FILE *out, const char *bracket, const char *rest, + size_t left_pad, size_t columns) +{ + size_t columns_avail = (columns > left_pad) ? columns - left_pad : 0; + size_t bracket_width = (columns_avail >= 42) ? (columns_avail - 2) / 4 : columns_avail; + char *bracket_trunc = columns_avail ? truncate_head(bracket, bracket_width) : strdup(bracket); + size_t printed = 2 + strlen(bracket_trunc); + size_t i; + + for (i = 0; i < left_pad; i++) + fputc(' ', out); + + cs(out, &((style_t){ FG_BWHITE, BG_GRAY, 0 })); + fprintf(out, "[%s]", bracket_trunc); + free(bracket_trunc); + + if (rest && *rest && (!columns_avail || columns_avail > printed + 2 + 10)) { + size_t rest_width = columns_avail ? columns_avail - printed - 2 : 0; + char *rest_trunc = columns_avail ? truncate_head(rest, rest_width) : strdup(rest); + + fprintf(out, " %s ", rest_trunc); + printed += 2 + strlen(rest_trunc); + free(rest_trunc); + } + + if (columns_avail > printed) + for (i = 0; i < columns_avail - printed; i++) + fputc(' ', out); + + cs(out, NULL); + fputc('\n', out); +} + +/* -- disassembly, ported from cmd_disasm() (formerly lib/debug.c) -------- */ + +/* Minimal growable byte buffer, used to build one disassembly line at a + * time (with real "\033[...m" sequences already embedded) so it can be + * measured and truncated to `columns` before being written out - mirrors + * what the pre-protocol version did with a uc_stringbuf_t. */ +typedef struct { + char *buf; + size_t len, cap; +} dbuf_t; + +static void +dbuf_reserve(dbuf_t *b, size_t extra) +{ + if (b->len + extra + 1 > b->cap) { + size_t newcap = b->cap ? b->cap * 2 : 128; + + while (newcap < b->len + extra + 1) + newcap *= 2; + + b->buf = realloc(b->buf, newcap); + b->cap = newcap; + } +} + +static void +dbuf_style(dbuf_t *b, const style_t *style) +{ + char tmp[32]; + int codes[8] = { 0 }; + size_t i = 0, n = 0; + + if (style == NULL) { + dbuf_reserve(b, 4); + memcpy(b->buf + b->len, "\033[0m", 4); + b->len += 4; + return; + } + + if ((style->styles & (BOLD | FAINT | ULINE)) == 0) + codes[i++] = 0; + + if (style->styles & BOLD) codes[i++] = 1; + if (style->styles & FAINT) codes[i++] = 2; + if (style->styles & ULINE) codes[i++] = 4; + + codes[i++] = style->fg ? style->fg : 39; + codes[i++] = style->bg ? style->bg : 49; + + n += sprintf(tmp + n, "\033["); + + for (size_t k = 0; k < i; k++) + n += sprintf(tmp + n, "%s%d", k ? ";" : "", codes[k]); + + n += sprintf(tmp + n, "m"); + + dbuf_reserve(b, n); + memcpy(b->buf + b->len, tmp, n); + b->len += n; +} + +static void +dbuf_printf(dbuf_t *b, const char *fmt, ...) +{ + va_list ap, ap2; + int n; + + va_start(ap, fmt); + va_copy(ap2, ap); + n = vsnprintf(NULL, 0, fmt, ap2); + va_end(ap2); + + if (n > 0) { + dbuf_reserve(b, (size_t)n); + vsnprintf(b->buf + b->len, (size_t)n + 1, fmt, ap); + b->len += (size_t)n; + } + + va_end(ap); +} + +/* Byte-based visible-width truncation with a trailing ellipsis, skipping + * embedded "\033[...m" escape sequences when counting columns - the same + * "reasonable simplification" truncate_head() above documents, since + * disassembly text (mnemonics, hex, identifiers) is normally plain ASCII. */ +static void +dbuf_truncate_tail(dbuf_t *b, size_t maxcols) +{ + size_t col = 0, i = 0, cut = SIZE_MAX; + + if (maxcols == 0) + return; + + while (i < b->len) { + if (b->buf[i] == '\033') { + size_t j = i + 1; + + if (j < b->len && b->buf[j] == '[') { + j++; + + while (j < b->len && b->buf[j] != 'm') + j++; + + if (j < b->len) + j++; + } + + i = j; + continue; + } + + if (col + 1 == maxcols && cut == SIZE_MAX) + cut = i; + + col++; + i++; + } + + if (col > maxcols && cut != SIZE_MAX) { + b->len = cut; + dbuf_printf(b, "\xe2\x80\xa6" /* U+2026 HORIZONTAL ELLIPSIS */); + } +} + +static void +dbuf_flush(dbuf_t *b, FILE *out, size_t columns) +{ + dbuf_truncate_tail(b, columns); + dbuf_style(b, NULL); + fwrite(b->buf, 1, b->len, out); + fputc('\n', out); + b->len = 0; +} + +void +debug_highlight_print_disassembly(FILE *out, const char *function, + const debug_disasm_insn_t *insns, + size_t ninsns, size_t columns) +{ + dbuf_t line = { 0 }; + static const style_t st_none = { FG_NONE, 0, 0 }; + static const style_t st_op = { FG_BMAGENT, 0, 0 }; + static const style_t st_cyan = { FG_CYAN, 0, 0 }; + static const style_t st_white = { FG_BWHITE, 0, 0 }; + static const style_t st_yellow = { FG_YELLOW, 0, 0 }; + static const style_t st_red = { FG_RED, 0, 0 }; + + fprintf(out, "Function: %s\n", function ? function : "?"); + + for (size_t idx = 0; idx < ninsns; idx++) { + const debug_disasm_insn_t *ins = &insns[idx]; + int fmt = ins->format; + int absfmt = (fmt < 0) ? -fmt : fmt; + + if (absfmt > 4) + absfmt = 4; + + dbuf_printf(&line, "%06zu:", ins->offset); + + /* Only the base instruction (opcode + its fixed-width operand, per + * `format`) is shown here - CLFN/ARFN's per-upvalue-capture bytes + * and CALL's per-argument unpack bytes that may follow in `bytes` + * (uc_vm_insn_call() needs the *full* instruction length to skip + * over them) get their own indented hex dump below instead. */ + for (size_t j = 0; j < ins->nbytes && j <= (size_t)absfmt; j++) { + dbuf_printf(&line, " "); + dbuf_style(&line, (j == 0) ? &st_none : &st_op); + dbuf_printf(&line, "%02x", ins->bytes[j]); + dbuf_style(&line, NULL); + } + + for (int j = 0; j < 3 * (4 - absfmt); j++) + dbuf_printf(&line, " "); + + dbuf_printf(&line, " %7s", ins->mnemonic ? ins->mnemonic : "?"); + + switch (fmt) { + case 0: + break; + + case -4: { + int64_t v = ins->operand; + uint32_t mag = (uint32_t)((v < 0) ? -v : v); + + dbuf_printf(&line, " {"); + dbuf_style(&line, &st_op); + dbuf_printf(&line, "%c0x%x", (v < 0) ? '-' : '+', mag); + dbuf_style(&line, NULL); + dbuf_printf(&line, "}"); + break; + } + + case 1: + dbuf_printf(&line, " {"); + dbuf_style(&line, &st_op); + dbuf_printf(&line, "%" PRIu64, (uint64_t)ins->operand); + dbuf_style(&line, NULL); + dbuf_printf(&line, "}"); + break; + + case 2: + dbuf_printf(&line, " {"); + dbuf_style(&line, &st_op); + dbuf_printf(&line, "0x%" PRIx64, (uint64_t)ins->operand); + dbuf_style(&line, NULL); + dbuf_printf(&line, "}"); + break; + + case 4: + dbuf_printf(&line, " {"); + dbuf_style(&line, &st_op); + dbuf_printf(&line, "0x%" PRIx64, (uint64_t)ins->operand); + dbuf_style(&line, NULL); + + if (ins->have_constant) { + dbuf_printf(&line, " : "); + dbuf_style(&line, ins->constant_is_string ? &st_op : &st_cyan); + dbuf_printf(&line, "%s", ins->constant_repr ? ins->constant_repr : "null"); + dbuf_style(&line, NULL); + } + else if (ins->variable_kind && strcmp(ins->variable_kind, "global") == 0) { + dbuf_printf(&line, " : global "); + dbuf_style(&line, &st_white); + dbuf_printf(&line, "%s", ins->variable_name ? ins->variable_name : "(unknown)"); + dbuf_style(&line, NULL); + } + else if (ins->variable_kind) { + bool upval = !strcmp(ins->variable_kind, "upval"); + + dbuf_printf(&line, " : %s ", ins->variable_kind); + dbuf_style(&line, upval ? &st_cyan : &st_white); + dbuf_printf(&line, "%s", ins->variable_name ? ins->variable_name : "(unknown)"); + dbuf_style(&line, NULL); + } + else if (ins->have_closure) { + dbuf_printf(&line, " : %s ", ins->closure_kind ? ins->closure_kind : "closure"); + dbuf_style(&line, &st_op); + dbuf_printf(&line, "#%" PRIu32, ins->closure_index); + dbuf_style(&line, NULL); + } + else if (ins->have_call) { + dbuf_printf(&line, " : "); + + if (ins->call_mcall) + dbuf_printf(&line, "mcall, "); + + dbuf_style(&line, &st_op); + dbuf_printf(&line, "%" PRIu32, ins->call_nargs); + dbuf_style(&line, NULL); + dbuf_printf(&line, " arg%s", (ins->call_nargs == 1) ? "" : "s"); + } + + dbuf_printf(&line, "}"); + break; + + default: + dbuf_style(&line, &st_red); + dbuf_printf(&line, " (unknown operand format: %d)", fmt); + dbuf_style(&line, NULL); + break; + } + + dbuf_flush(&line, out, columns); + + for (size_t j = 0; j < ins->ncaptures; j++) { + bool upval = ins->captures[j].upval; + int64_t slot = ins->captures[j].slot; + uint32_t mag = (uint32_t)((slot < 0) ? -slot : slot); + + dbuf_printf(&line, " \xe2\x80\xa6 " /* " … " */); + dbuf_style(&line, &st_yellow); + + for (size_t k = 0; k < 4; k++) + dbuf_printf(&line, "%s%02x", k ? " " : "", ins->captures[j].bytes[k]); + + dbuf_style(&line, NULL); + dbuf_printf(&line, " capture {"); + dbuf_style(&line, &st_yellow); + dbuf_printf(&line, "%c0x%x", (slot < 0) ? '-' : '+', mag); + dbuf_style(&line, NULL); + dbuf_printf(&line, " : %s ", upval ? "upval" : "local"); + dbuf_style(&line, upval ? &st_cyan : &st_white); + dbuf_printf(&line, "%s", ins->captures[j].name ? ins->captures[j].name : "(unknown)"); + dbuf_style(&line, NULL); + dbuf_printf(&line, "}"); + + dbuf_flush(&line, out, columns); + } + + for (size_t j = 0; j < ins->nunpacks; j++) { + uint16_t slot = ins->unpacks[j].slot; + + dbuf_printf(&line, " \xe2\x80\xa6 " /* " … " */); + dbuf_style(&line, &st_yellow); + dbuf_printf(&line, "%02x %02x", ins->unpacks[j].bytes[0], ins->unpacks[j].bytes[1]); + dbuf_style(&line, NULL); + dbuf_printf(&line, " unpack {"); + dbuf_style(&line, &st_yellow); + dbuf_printf(&line, "0x%x", slot); + dbuf_style(&line, NULL); + dbuf_printf(&line, " : stack slot "); + dbuf_style(&line, &st_op); + dbuf_printf(&line, "-0x%x", (unsigned)(slot + 1)); + dbuf_style(&line, NULL); + dbuf_printf(&line, "}"); + + dbuf_flush(&line, out, columns); + } + } + + free(line.buf); +} + +/* -- variables listing, ported from print_variables() (formerly + * lib/debug.c) ------------------------------------------------------------ */ + +/* Like dbuf_truncate_tail(), but for a compact JSON-ish value repr: places + * the ellipsis just before a synthetic closing bracket/quote so a truncated + * object/array/string still visually reads as one and stays on a single + * line, matching printbuf_append_uv()'s (formerly lib/debug.c) truncation + * exactly. Byte-based rather than UTF-8-aware, the same simplification + * truncate_head() above documents. */ +static void +dbuf_truncate_value(dbuf_t *b, size_t maxcols) +{ + const char *end; + size_t keep; + + if (maxcols == 0 || b->len <= maxcols) + return; + + switch (b->buf[0]) { + case '{': keep = (maxcols > 3) ? maxcols - 3 : 0; end = "\xe2\x80\xa6 }"; break; + case '[': keep = (maxcols > 3) ? maxcols - 3 : 0; end = "\xe2\x80\xa6 ]"; break; + case '"': keep = (maxcols > 2) ? maxcols - 2 : 0; end = "\xe2\x80\xa6\""; break; + default: keep = (maxcols > 1) ? maxcols - 1 : 0; end = "\xe2\x80\xa6"; break; + } + + b->len = keep; + dbuf_printf(b, "%s", end); +} + +void +debug_highlight_print_variables(FILE *out, const debug_variable_t *vars, + size_t nvars, const char *indent, + size_t columns) +{ + static const style_t st_upval = { FG_CYAN, 0, BOLD }; + static const style_t st_faint = { FG_BWHITE, 0, FAINT }; + static const style_t st_err = { FG_RED, 0, BOLD }; + static const char shadowed_suffix[] = " (shadowed)"; + size_t indent_len = indent ? strlen(indent) : 0; + size_t value_cols = 0; + dbuf_t namebuf = { 0 }, valuebuf = { 0 }; + + if (columns > indent_len + 19) + value_cols = columns - indent_len - 19; + + for (size_t i = 0; i < nvars; i++) { + const debug_variable_t *v = &vars[i]; + const char *kind = v->kind ? v->kind : ""; + const char *name = v->name ? v->name : "?"; + const char *repr = v->value_repr ? v->value_repr : ""; + bool upval = !strcmp(kind, "upvalue"); + bool faint = v->shadowed || !strcmp(kind, "this") || !strcmp(kind, "internal"); + bool err = !strcmp(repr, ""); + size_t namelen; + + namebuf.len = 0; + valuebuf.len = 0; + + dbuf_printf(&namebuf, "%s", name); + namelen = namebuf.len; + dbuf_truncate_tail(&namebuf, 16); + + if (indent) + fputs(indent, out); + + /* A shadowed entry is rendered faint throughout, taking priority + * over its own kind's usual color (still cyan/upvalue matters + * far less than "this isn't what the name resolves to anymore"). */ + if (v->shadowed) + cs(out, &st_faint); + else if (upval) + cs(out, &st_upval); + else if (faint) + cs(out, &st_faint); + + fwrite(namebuf.buf, 1, namebuf.len, out); + + if (v->shadowed || upval || faint) + cs(out, NULL); + + for (; namelen < 16; namelen++) + fputc(' ', out); + + cs(out, &st_faint); + fputs(" : ", out); + cs(out, NULL); + + if (err) { + cs(out, &st_err); + fputs(repr, out); + cs(out, NULL); + } + else { + size_t this_value_cols = value_cols; + + /* Reserve room for the trailing "(shadowed)" marker printed + * below, or it doesn't count against the line's width budget + * and can push the whole line past `columns`, wrapping. */ + if (v->shadowed && this_value_cols > sizeof(shadowed_suffix) - 1) + this_value_cols -= sizeof(shadowed_suffix) - 1; + + if (v->shadowed) + cs(out, &st_faint); + + dbuf_printf(&valuebuf, "%s", repr); + + /* value_repr is always the compact, single-line repr (see + * build_variables_json() in lib/debug.c) - guard against a + * literal embedded newline anyway, since byte-counting + * truncation across one would garble rather than shorten it. */ + if (columns > 0 && !strchr(repr, '\n')) + dbuf_truncate_value(&valuebuf, this_value_cols); + + fwrite(valuebuf.buf, 1, valuebuf.len, out); + + if (v->shadowed) + cs(out, NULL); + } + + if (v->shadowed) { + cs(out, &st_faint); + fputs(shadowed_suffix, out); + cs(out, NULL); + } + + fputc('\n', out); + } + + free(namebuf.buf); + free(valuebuf.buf); +} diff --git a/debug_highlight.h b/debug_highlight.h new file mode 100644 index 00000000..7e888d51 --- /dev/null +++ b/debug_highlight.h @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2026 Jo-Philipp Wich + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ANY IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * --- + * + * ucode/utpl syntax highlighting and ANSI source rendering, ported from the + * pre-protocol interactive debugger (formerly lib/debug.c's + * highlight_rules[]/compile_patterns()/print_source_location()) so it can + * be reused by any client speaking the line-based debug protocol - or + * anything else that wants to print ucode source with the same styling. + * + * This module is intentionally standalone: no ucode headers, no protocol + * knowledge, just POSIX regex + stdio. A caller supplies already-split + * source lines and, optionally, a single line/column range to shade as the + * "current statement" - the multi-range/ellipsis-gap layout the original + * server-side renderer supported for very large statements is not ported, + * since every client of the debug protocol only ever receives one + * contiguous range at a time (see SOURCE_RANGE in lib/debug_proto.h). + */ + +#ifndef _DEBUG_HIGHLIGHT_H +#define _DEBUG_HIGHLIGHT_H + +#include +#include +#include +#include + +/* A statement span to shade, in 1-based line numbers and 0-based byte + * columns within those lines (matching the debug protocol's "col" fields). + * Set from_line to 0 for "no highlight". `have_ip` additionally underlines + * the single character at {ip_line, ip_col} - the exact current + * instruction position, as opposed to {from,to} which mark the enclosing + * statement's extent. */ +typedef struct { + size_t from_line, from_col; + size_t to_line, to_col; + bool have_ip; + size_t ip_line, ip_col; +} debug_highlight_span_t; + +/* Compile the highlight regexes once; safe to call repeatedly. Returns + * false (and prints a diagnostic to stderr) on a regex compile error, in + * which case debug_highlight_print_source() below still works, just + * without coloring. */ +bool debug_highlight_init(void); + +/* Print source lines [from, to] (1-based, inclusive, clamped to + * [1, nlines]) from the `nlines`-element `lines` array (as produced by + * splitting raw source text on '\n', with no trailing newlines) to `out`, + * applying ucode/utpl syntax highlighting and, if `hl` is non-NULL, shading + * the statement range it describes. Every printed line is prefixed with + * `left_pad` blank columns plus a right-aligned line number gutter. + * `columns` is the terminal width to wrap/pad to (pass 0 for "don't know", + * which disables truncation and trailing-space padding). */ +void debug_highlight_print_source(FILE *out, char **lines, size_t nlines, + size_t from, size_t to, + const debug_highlight_span_t *hl, + size_t left_pad, size_t columns); + +/* A single [from, to] (1-based, inclusive) line range, for the multi-range + * form below. */ +typedef struct { + size_t from, to; +} debug_highlight_range_t; + +/* Like debug_highlight_print_source(), but for up to `nranges` disjoint + * ranges at once - lines that fall in a gap between two ranges are skipped + * with a single " … " ellipsis marker rather than printed, matching the + * original format_context_statement()'s handling of a statement too long + * to show in full: a window of context at its start, a gap, and a window + * around the current instruction/its end. Ranges need not be sorted; a + * {0, 0} entry is ignored (so callers can pass a fixed-size array without + * always filling every slot). */ +void debug_highlight_print_source_ranges(FILE *out, char **lines, size_t nlines, + size_t nranges, + const debug_highlight_range_t *ranges, + const debug_highlight_span_t *hl, + size_t left_pad, size_t columns); + +/* Print a full-width "[bracket] rest " status bar to `out` on a solid + * background, ported from the original format_context_header_backtrace()/ + * format_context_header_callframe() (the bar shown above a paused + * location's or a backtrace frame's source snippet) - `bracket` is the + * source file (or "C" for a native frame), `rest` the call breadcrumb or + * frame signature. Long `rest` values are elided from the front (ellipsis + * first, keeping the tail - the original's choice, since the innermost/ + * current part of a chain matters more than the outermost when both don't + * fit) if `columns` is nonzero; pass 0 to disable width awareness (no + * truncation, no trailing padding). */ +void debug_highlight_print_header_bar(FILE *out, const char *bracket, + const char *rest, + size_t left_pad, size_t columns); + +/* A single decoded bytecode instruction, as reported by the DISASSEMBLE + * protocol response's "instructions" array, for + * debug_highlight_print_disassembly() below. Which of the optional fields + * are populated selects what annotation (if any) is shown after the raw + * operand - the renderer itself has no notion of opcode names or their + * meaning, it only reacts to which fields the caller filled in. */ +typedef struct { + size_t offset; + const char *mnemonic; + int format; /* uc_vm_insn_format[] value: 0, 1, 2, 4 or -4 */ + const unsigned char *bytes; size_t nbytes; /* raw instruction bytes */ + int64_t operand; /* decoded operand; sign only meaningful for format -4; + * unused when format == 0 */ + + bool have_constant; + const char *constant_repr; /* JSON text of the constant value */ + bool constant_is_string; + + const char *variable_kind; /* "local", "upval", "global", or NULL */ + const char *variable_name; + + bool have_closure; + const char *closure_kind; /* "closure" or "arrow" */ + uint32_t closure_index; + + bool have_call; + bool call_mcall; /* method call: an implicit `this` arg follows */ + uint32_t call_nargs; + + struct { + int64_t slot; + bool upval; + const char *name; + unsigned char bytes[4]; + } *captures; size_t ncaptures; + + struct { + uint16_t slot; + unsigned char bytes[2]; + } *unpacks; size_t nunpacks; +} debug_disasm_insn_t; + +/* Print a disassembly listing exactly as the pre-protocol interactive + * debugger's `disassemble` command did: address, a color-coded raw byte + * dump (opcode byte plain, operand bytes bright magenta), the mnemonic, + * the decoded operand, and - when the caller supplied it - a semantic + * annotation (constant value, local/upval/global name, closure/arrow + * index) plus extra indented lines for closure upvalue captures or call + * argument unpacks. `columns` is the terminal width to wrap to (pass 0 + * for "don't know", which disables truncation). */ +void debug_highlight_print_disassembly(FILE *out, const char *function, + const debug_disasm_insn_t *insns, + size_t ninsns, size_t columns); + +/* One entry of a VARIABLES (or a BACKTRACE frame's inline "variables") + * protocol response, for debug_highlight_print_variables() below. `kind` + * is one of "this", "local", "internal" (a synthetic, parenthesized slot + * name such as a `for`-loop's hidden iterator) or "upvalue". `shadowed` + * marks a same-named, less-nested declaration that a more-nested one + * currently hides - still a real, live slot, just not what plain script + * code resolves this name to right now. */ +typedef struct { + const char *name; + const char *kind; + const char *value_repr; + bool shadowed; +} debug_variable_t; + +/* Print a "name : value" variable listing exactly as the pre-protocol + * interactive debugger's print_variables() did: the name in a fixed + * 16-column field (tail-truncated with an ellipsis if longer), styled + * bold cyan for an upvalue or faint white for "this"/an internal slot + * (plain otherwise), a faint " : " separator, then the value - styled + * bold red instead of truncated when it is the literal sentinel + * "". A shadowed entry (see above - not part of the + * original pre-protocol listing, which never showed more than one + * variable per name to begin with) is rendered faint throughout with a + * trailing "(shadowed)" marker. Every line is prefixed with `indent`. + * `columns` is the terminal width the value is truncated to fit (pass 0 + * for "don't know", which disables value truncation only - the name + * field is always truncated to 16 regardless). */ +void debug_highlight_print_variables(FILE *out, const debug_variable_t *vars, + size_t nvars, const char *indent, + size_t columns); + +#endif diff --git a/debug_lineedit.c b/debug_lineedit.c new file mode 100644 index 00000000..9e21c34a --- /dev/null +++ b/debug_lineedit.c @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2026 Jo-Philipp Wich + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ANY IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "debug_lineedit.h" + +#define EDITBUF_SIZE 4096 +#define HISTORY_SIZE 100 + +/* -- raw terminal mode ----------------------------------------------------- */ + +static struct termios orig_termios; +static int orig_flags = -1; +static bool raw_active = false; + +static void +raw_mode_disable(void) +{ + if (!raw_active) + return; + + tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios); + + if (orig_flags != -1) + fcntl(STDIN_FILENO, F_SETFL, orig_flags); + + raw_active = false; +} + +void +lineedit_init(void) +{ + struct termios raw; + + if (raw_active || !isatty(STDIN_FILENO)) + return; + + if (tcgetattr(STDIN_FILENO, &orig_termios) != 0) + return; + + raw = orig_termios; + + /* ISIG is deliberately cleared too: Ctrl-C is handled below as "cancel + * the current line" (matching the original), not as SIGINT - this + * client has no separate signal-based break-into-debugger path of its + * own to preserve that for. + * + * VMIN/VTIME are deliberately left alone: with ICANON off, setting + * VMIN=0/VTIME=0 makes every read() with nothing available return 0 + * immediately - indistinguishable from real EOF, which getc_nb() below + * needs to detect. O_NONBLOCK (via fcntl, right below) already gives + * the same "don't block" behavior while keeping that distinction: a + * non-blocking read() returns -1/EAGAIN for "nothing yet" and only 0 + * for an actual EOF. */ + raw.c_lflag &= (tcflag_t)~(ICANON | ECHO | ISIG); + + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) != 0) + return; + + orig_flags = fcntl(STDIN_FILENO, F_GETFL); + + /* Non-blocking so a read() for the tail of an escape sequence can never + * stall the process if, in some rare split-input scenario (e.g. a slow + * network terminal), the rest hasn't arrived yet - see read_key(). */ + if (orig_flags != -1) + fcntl(STDIN_FILENO, F_SETFL, orig_flags | O_NONBLOCK); + + raw_active = true; + atexit(raw_mode_disable); +} + +bool +lineedit_active(void) +{ + return raw_active; +} + +void +lineedit_suspend(void) +{ + raw_mode_disable(); +} + +void +lineedit_resume(void) +{ + lineedit_init(); +} + +/* -- non-blocking key decoding, ported from term_getc()/term_getc_raw() + * (formerly lib/debug.c) ---------------------------------------------------- */ + +enum { + LE_NODATA = -1, /* nothing available right now - stop reading */ + LE_EOF = -2, /* stdin hit real EOF (terminal hung up) */ + + KEY_HOME = 0x110000, + KEY_END, + KEY_DEL, + KEY_ARROW_UP, + KEY_ARROW_DOWN, + KEY_ARROW_LEFT, + KEY_ARROW_RIGHT, + KEY_CTRL_LEFT, + KEY_CTRL_RIGHT, +}; + +static int +getc_nb(void) +{ + unsigned char c; + ssize_t n = read(STDIN_FILENO, &c, 1); + + if (n == 1) + return c; + + if (n == 0) + return LE_EOF; + + return LE_NODATA; +} + +/* Decode one keypress, including multi-byte escape sequences for arrow/ + * home/end/delete keys. If a sequence is only partially available, it + * degrades to a bare ESC (0x1b) rather than blocking or losing the bytes + * already read - see the header comment on why that's an acceptable + * simplification here. */ +static int +read_key(void) +{ + int c = getc_nb(); + int seq[3]; + + if (c != 0x1b) + return c; + + if ((seq[0] = getc_nb()) < 0) return 0x1b; + if ((seq[1] = getc_nb()) < 0) return 0x1b; + + if (seq[0] == '[') { + if (seq[1] >= '0' && seq[1] <= '9') { + if ((seq[2] = getc_nb()) < 0) return 0x1b; + + if (seq[2] == '~') { + switch (seq[1]) { + case '1': case '7': return KEY_HOME; + case '3': return KEY_DEL; + case '4': case '8': return KEY_END; + } + } + else if (seq[2] == ';') { + int mod = getc_nb(); + int fin = (mod < 0) ? LE_NODATA : getc_nb(); + + if (mod == '5') { + switch (fin) { + case 'C': return KEY_CTRL_RIGHT; + case 'D': return KEY_CTRL_LEFT; + } + } + } + + return LE_NODATA; /* unrecognized sequence, swallow it */ + } + + switch (seq[1]) { + case 'A': return KEY_ARROW_UP; + case 'B': return KEY_ARROW_DOWN; + case 'C': return KEY_ARROW_RIGHT; + case 'D': return KEY_ARROW_LEFT; + case 'H': return KEY_HOME; + case 'F': return KEY_END; + } + } + else if (seq[0] == 'O') { + switch (seq[1]) { + case 'H': return KEY_HOME; + case 'F': return KEY_END; + } + } + + return LE_NODATA; +} + +/* -- line buffer + cursor --------------------------------------------------- */ + +static char linebuf[EDITBUF_SIZE]; +static size_t linelen = 0, cursor = 0; +static char cur_prompt[64]; + +static void +buf_insert(const char *s, size_t n) +{ + if (linelen + n >= sizeof(linebuf)) + n = sizeof(linebuf) - 1 - linelen; + + if (!n) + return; + + memmove(linebuf + cursor + n, linebuf + cursor, linelen - cursor); + memcpy(linebuf + cursor, s, n); + linelen += n; + cursor += n; +} + +static void +buf_delete(size_t from, size_t to) +{ + if (to > linelen) + to = linelen; + + if (from >= to) + return; + + memmove(linebuf + from, linebuf + to, linelen - to); + linelen -= (to - from); + + if (cursor > from) + cursor = (cursor >= to) ? cursor - (to - from) : from; +} + +static size_t +word_left(size_t pos) +{ + while (pos > 0 && isspace((unsigned char)linebuf[pos - 1])) pos--; + while (pos > 0 && !isspace((unsigned char)linebuf[pos - 1])) pos--; + + return pos; +} + +static size_t +word_right(size_t pos) +{ + while (pos < linelen && isspace((unsigned char)linebuf[pos])) pos++; + while (pos < linelen && !isspace((unsigned char)linebuf[pos])) pos++; + + return pos; +} + +static void +redraw(void) +{ + printf("\r\033[K%s%.*s", cur_prompt, (int)linelen, linebuf); + + if (cursor < linelen) + printf("\033[%zuD", linelen - cursor); + + fflush(stdout); +} + +/* -- history, ported from termstate.history/HISTORY_SIZE (formerly + * lib/debug.c) -------------------------------------------------------------- */ + +static char *history[HISTORY_SIZE]; +static size_t history_count = 0; +static size_t history_browse = 0; /* == history_count: editing the live line */ +static char history_saved[EDITBUF_SIZE]; + +static void +history_push(const char *line) +{ + if (!*line) + return; + + if (history_count > 0 && !strcmp(history[history_count - 1], line)) + return; + + if (history_count >= HISTORY_SIZE) { + free(history[0]); + memmove(&history[0], &history[1], (HISTORY_SIZE - 1) * sizeof(history[0])); + history_count--; + } + + history[history_count++] = strdup(line); +} + +/* -- Tab completion, ported from term_line_tabcomplete() (formerly + * lib/debug.c), restricted to command-name completion only ----------------- + * (the original also completed breakpoint specs/function names/file paths + * depending on argument position - that needs live data from the server + * and is future work, not something this port takes on). */ + +static const lineedit_completion_t *completions = NULL; +static size_t ncompletions = 0; + +void +lineedit_set_completions(const lineedit_completion_t *c, size_t n) +{ + completions = c; + ncompletions = n; +} + +static void +try_complete(void) +{ + const char *matches[64]; + size_t nmatch = 0, maxlen = 0, wend = 0, i; + + while (wend < linelen && !isspace((unsigned char)linebuf[wend])) + wend++; + + /* only complete the command word itself, not its arguments */ + if (!completions || cursor != wend || wend == 0) + return; + + for (i = 0; i < ncompletions; i++) { + const char *c; + + for (c = completions[i].names; *c; c += strlen(c) + 1) { + size_t len = strlen(c); + + if (len >= wend && !strncmp(c, linebuf, wend)) { + if (nmatch < sizeof(matches) / sizeof(matches[0])) + matches[nmatch++] = c; + + if (len > maxlen) + maxlen = len; + } + } + } + + if (nmatch == 0) + return; + + if (nmatch == 1) { + buf_delete(0, wend); + cursor = 0; + buf_insert(matches[0], strlen(matches[0])); + buf_insert(" ", 1); + } + else { + printf("\n"); + + for (i = 0; i < nmatch; i++) + printf("%-*s ", (int)maxlen, matches[i]); + + printf("\n"); + } +} + +/* -- prompt + feed ----------------------------------------------------------- */ + +void +lineedit_begin(const char *prompt) +{ + snprintf(cur_prompt, sizeof(cur_prompt), "%s", prompt); + linelen = cursor = 0; + history_browse = history_count; + + if (raw_active) { + redraw(); + } + else { + fputs(prompt, stdout); + fflush(stdout); + } +} + +bool +lineedit_feed(char *out, size_t outsz, bool *eof) +{ + int key; + + *eof = false; + + /* Non-interactive input (piped/scripted, or stdin isn't a tty): no + * editing possible or needed, just read one line the plain way. */ + if (!raw_active) { + if (!fgets(out, (int)outsz, stdin)) { + *eof = true; + return false; + } + + out[strcspn(out, "\n")] = '\0'; + + return true; + } + + while ((key = read_key()) != LE_NODATA) { + if (key == LE_EOF) { + *eof = true; + return false; + } + + switch (key) { + case '\r': case '\n': + printf("\n"); + snprintf(out, outsz, "%.*s", (int)linelen, linebuf); + history_push(out); + + return true; + + case 3: /* Ctrl-C: cancel the line in place, like the original */ + linelen = cursor = 0; + history_browse = history_count; + break; + + case 127: case 8: /* backspace */ + if (cursor > 0) + buf_delete(cursor - 1, cursor); + + break; + + case KEY_DEL: + buf_delete(cursor, cursor + 1); + break; + + case KEY_HOME: + cursor = 0; + break; + + case KEY_END: + cursor = linelen; + break; + + case KEY_ARROW_LEFT: + if (cursor > 0) + cursor--; + + break; + + case KEY_ARROW_RIGHT: + if (cursor < linelen) + cursor++; + + break; + + case KEY_CTRL_LEFT: + cursor = word_left(cursor); + break; + + case KEY_CTRL_RIGHT: + cursor = word_right(cursor); + break; + + case KEY_ARROW_UP: + if (history_browse > 0) { + if (history_browse == history_count) { + linebuf[linelen] = '\0'; + snprintf(history_saved, sizeof(history_saved), "%s", linebuf); + } + + history_browse--; + snprintf(linebuf, sizeof(linebuf), "%s", history[history_browse]); + linelen = cursor = strlen(linebuf); + } + + break; + + case KEY_ARROW_DOWN: + if (history_browse < history_count) { + history_browse++; + + if (history_browse == history_count) + snprintf(linebuf, sizeof(linebuf), "%s", history_saved); + else + snprintf(linebuf, sizeof(linebuf), "%s", history[history_browse]); + + linelen = cursor = strlen(linebuf); + } + + break; + + case 23: /* Ctrl-W */ + buf_delete(word_left(cursor), cursor); + break; + + case 9: /* Tab */ + try_complete(); + break; + + default: + if (key >= ' ' && key < 127) { + char c = (char)key; + + buf_insert(&c, 1); + } + + break; + } + + redraw(); + } + + return false; +} diff --git a/debug_lineedit.h b/debug_lineedit.h new file mode 100644 index 00000000..98984c7a --- /dev/null +++ b/debug_lineedit.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2026 Jo-Philipp Wich + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ANY IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * --- + * + * Interactive line editing, history and command-name completion for udbg's + * "dbg > " prompt, ported from the pre-protocol interactive debugger's + * hand-rolled terminal line editor (formerly lib/debug.c's termline_t/ + * term_getc()/term_getline()/term_line_tabcomplete()) - no external + * readline/editline dependency, matching the original's choice not to take + * on one either. + * + * The one architectural change the port needed: the original owned a + * dedicated, blocking input loop (it was a synchronous, in-process + * debugger), whereas udbg is driven by a single select() loop that also has + * to watch the server socket for async EVENTs - so every function here is + * non-blocking and consumes only bytes already available, meant to be + * called each time select()/poll() reports STDIN_FILENO readable. + */ + +#ifndef _DEBUG_LINEEDIT_H +#define _DEBUG_LINEEDIT_H + +#include +#include + +/* One command-name completion candidate set for Tab, e.g. a CLI's own + * verb/alias table - `names` is a NUL-separated list of aliases (primary + * name first), itself NUL-terminated, the same shape already used for + * udbg's own help table. Only ever matched against the line's first + * (unterminated-by-space) word - this module has no notion of per-argument + * completion (function names, file paths, ...). */ +typedef struct { + const char *names; +} lineedit_completion_t; + +/* Try to put STDIN_FILENO into raw, non-blocking mode for interactive + * editing. No-op if stdin isn't a terminal (piped/scripted input, the + * common case when testing) - callers must check lineedit_active() and + * fall back to plain fgets()-based reads in that case, since nothing below + * does anything useful without raw mode. Registers an atexit() handler to + * restore the original terminal settings; safe to call more than once. */ +void lineedit_init(void); + +/* True if lineedit_init() actually engaged raw mode. */ +bool lineedit_active(void); + +/* Temporarily restore the original (cooked, blocking) terminal mode - for a + * one-off plain fgets()-based prompt elsewhere (e.g. a yes/no confirmation) + * that needs normal line buffering and echo. Pair with lineedit_resume(). */ +void lineedit_suspend(void); + +/* Re-engage raw mode after lineedit_suspend(), if it was active before. */ +void lineedit_resume(void); + +/* Install the Tab completion candidate table. Optional - skip the call to + * disable completion entirely. `completions` must outlive any subsequent + * lineedit_feed() call. */ +void lineedit_set_completions(const lineedit_completion_t *completions, size_t n); + +/* Print `prompt` and start a fresh, empty line - call this whenever the + * caller (re)enters a state where it wants to accept a new command, i.e. + * the one place that used to just printf() the prompt directly. */ +void lineedit_begin(const char *prompt); + +/* Consume whatever is currently available on STDIN_FILENO. Never blocks. + * Returns true exactly once a line has been submitted (Enter), copied + * NUL-terminated into `out` (truncated to fit `outsz`); *eof is set to true + * if the terminal hung up (read() saw EOF) rather than a line being ready. + * Redraws the prompt/line itself as needed - callers only need to react to + * a completed line or *eof, not to intermediate keystrokes. */ +bool lineedit_feed(char *out, size_t outsz, bool *eof); + +#endif diff --git a/docs/debugger.md b/docs/debugger.md new file mode 100644 index 00000000..f4110f66 --- /dev/null +++ b/docs/debugger.md @@ -0,0 +1,270 @@ +# UCode Debugger + +## Overview + +The ucode interpreter includes source-level debugging support: breakpoints, +stepping, stack inspection, and runtime expression evaluation. The +implementation is split into a **server** (the debug core, `lib/debug.c` + +`lib/debug_remote.c` + `lib/debug_proto.c`, loaded as the `debug` module) and +a **client** (`udbg`, at the repository root) that talks to it over a simple, +line-based text protocol. + +The server never renders anything - no ANSI escapes, no syntax highlighting, +no formatted columns. It only emits and consumes structured protocol +messages (see "Wire Protocol" below). All rendering, source buffer handling +and interactive line editing live in the client. This split exists so that +alternative clients - IDE integrations, editor plugins, other tooling - can +drive the exact same debug core without reimplementing any of its logic, and +so the client can be tested and evolved independently of the VM-side +breakpoint machinery. + +There is exactly one way a session is driven, regardless of how it was +reached: a connected file descriptor is handed to `bk_enter_session()` +(`lib/debug.c`), which writes a `PAUSED` message and then reads and dispatches +protocol commands until the client tells it to resume or quit. Three things +differ only in *how that fd is obtained*: + +- **Local (`ucode -x script.uc`)** - `uc_debugger()` creates a `socketpair()`, + forks, and execs `udbg --fd 3` in the child with one end of the pair on fd + 3; the parent (running the script) keeps the other end as the session fd. + The child owns the real controlling terminal and is the interactive + client; the parent never touches its own stdin/stdout for protocol + traffic. +- **Remote, explicit path (`debug.listen(path)`)** - accepts a single + connection on an arbitrary, caller-chosen Unix domain socket path and hands + it to the same session driver. +- **Remote, SIGUSR1 attach (`ucode -X`, `debug.attach()`, `debug.listen()` + with no path)** - arms a breakpoint and/or a `SIGUSR1` handler; once + triggered, waits (up to 30s) for a client to connect to the PID-derived + attach socket `/tmp/ucode-debug-.sock`, then hands off the accepted fd + the same way. `udbg ` automates sending the signal and connecting. + +--- + +## Wire Protocol + +One message per line, `\n`-terminated: an uppercase **VERB**, optionally +followed by a single space and a JSON object payload. + +``` +PAUSED {"reason":"breakpoint","file":"script.uc","line":12,"col":3,"function":"main","breakpoint_id":1} +BREAK {"spec":"script.uc:12"} +BREAKPOINT_ADDED {"id":1} +``` + +A payload, when present, is always a JSON *object* (never a bare +array/string/number), so new fields can be added without breaking existing +clients. `file` fields are the source's display path exactly as the server +resolves it (repository-relative when the source lives under the current +working directory, absolute otherwise) - clients should treat it as an +opaque key for the `SOURCE` verb, not derive anything from its shape. + +### Client → server commands + +| Verb | Payload | Response | +|---|---|---| +| `BREAK` | `{"spec":"path[:line[:col]]"\|"expr"}` | `BREAKPOINT_ADDED {"id"}` or `ERROR` | +| `DELETE` | `{"id":N}` (omit for the current breakpoint) | `OK` or `ERROR` | +| `LIST_BREAKPOINTS` | none | `BREAKPOINTS {"items":[{"id"?,"kind","file"?,"line"?,"col"?,"function"?}]}` | +| `NEXT` | none | none synchronously - see "No synchronous step acks" below | +| `STEP` | none | none synchronously | +| `CONTINUE` | none | none synchronously | +| `RETURN` | none | none synchronously | +| `BACKTRACE` | `{"full":bool}` | `BACKTRACE {"frames":[...]}` (see below) | +| `VARIABLES` | none | `VARIABLES {"vars":[...]}` (see below) | +| `SOURCES` | none | `SOURCES {"items":[{"index","file"}]}` | +| `PRINT` | `{"expr":"..."}` | `VALUE {"repr"}` or `ERROR` | +| `LINES` | `{"spec"?,"before"?,"after"?}` | `SOURCE_RANGE {"file","from","to","cursor"?}` - no source text, see "Source Resolution" | +| `THROW` | `{"type"?,"message"}` | raises the exception; no direct response | +| `DISASSEMBLE` | `{"spec"?}` | `DISASSEMBLY {"function","instructions":[...]}` | +| `SOURCE` | `{"file"}` | `SOURCE {"file","text"\|null,"error"?}` | +| `HELP` | `{"command"?}` | `HELP {"commands":[{"verb","help"}]}` | +| `QUIT` | none | terminates the debugged program (like `exit()`); no confirmation prompt - a client that wants one must ask the user itself before sending this | + +`BACKTRACE` frame shape: `{"kind":"script"|"native","index","file"?,"line"?, +"col"?,"insn"?,"function"?,"module"?,"variables"?}` - `variables` is only +present when `full:true` was requested, and has the same shape as +`VARIABLES`'s `vars` array. + +`VARIABLES`/backtrace-`variables` entry shape: `{"name","kind":"this"| +"local"|"internal"|"upvalue","value_repr"}` - `value_repr` is a pre-rendered +string (via the same formatter `print()`/`printf()` use) since ucode values +include closures, resources and regexes that don't round-trip through JSON; +there is no separate machine-typed `value` field. + +### Server → client events + +| Verb | Payload | +|---|---| +| `PAUSED` | `{"reason":"entry"\|"breakpoint"\|"step"\|"exception"\|"uncaught","file"?,"line"?,"col"?,"function"?,"breakpoint_id"?,"exception_type"?,"exception_message"?}` | +| `EVENT` | `{"event":"exception"\|"exit"\|"signal", ...}` - unsolicited, can arrive at any time (e.g. right before the process exits) | +| `ERROR` | `{"message"}` - the uniform failure shape for every command above | + +### No synchronous step acks + +`NEXT`/`STEP`/`CONTINUE`/`RETURN` do not get an immediate acknowledgement. +The next thing a client sees is whatever actually happens next: a new +`PAUSED` if execution hits another breakpoint/step boundary, an `EVENT` +carrying `"event":"exit"` if the program ends, or nothing further for a +while if it just keeps running. This mirrors the real control flow exactly +- there is no "done stepping" moment to report before that. + +### Source resolution + +The server resolves `{file, line, col}` locations from the running program's +debug info, but **never sends rendered or highlighted source text** for a +`PAUSED`/`SOURCE_RANGE`/backtrace frame - only the coordinates. A client +that wants to display source has two options: + +- **It already has the file** - the common case either way debugging is + actually done: fully locally (client and target share a filesystem, e.g. + `-x`/`udbg ` on the same box) or from a development checkout against + a remote target (the *client*, not the target, has the real/better + source access - think a stripped production device). Either way the + client should try reading the file itself first, keyed by the `file` + string from any location payload, and never needs a round-trip to the + server for it. `udbg` does this (see `-s`/`--srcdir` below for path + mapping when the reported path doesn't exist as-is locally). +- **It doesn't** (no local access at all) - send `SOURCE {"file":"..."}` + and use the returned raw `text`. If the server itself has no source + available either (running precompiled bytecode with no embedded source + and no matching local file), `text` is `null` and `error` explains why. + +`udbg` implements this as: try the exact reported path; if that fails and +`-s DIR`/`--srcdir DIR` was given, try `DIR/`; only then fall back to asking the server. + +--- + +## Debugger API (`module:debug`) + +| Function | Description | +|----------|-------------| +| `debug.memdump(path)` | Dump VM heap state to file for analysis | +| `debug.traceback([level])` | Get current call stack trace (structured data, not the CLI's `BACKTRACE` output) | +| `debug.sourcepos()` | Get current source position (filename, line, byte) | +| `debug.getinfo(value)` | Query internal value information | +| `debug.getlocal(level, var)` / `debug.setlocal(level, var, value)` | Get/set a local variable | +| `debug.getupval(target, var)` / `debug.setupval(target, var, value)` | Get/set an upvalue | +| `debug.debugger([target])` | Local interactive session: forks and execs `udbg --fd N` over a socketpair, then pauses (immediately, or at entry to `target` if given) | +| `debug.attach(mainfn)` | Arm `SIGUSR1`-triggered attach and break on entry to `mainfn` | +| `debug.break()` | Pause execution right here, waiting for an attach-socket client | +| `debug.breakpoint(spec[, mainfn])` | Install a breakpoint from a location spec, usable before the program starts running | +| `debug.listen([wait\|path])` | Enable remote debugging - explicit path, `SIGUSR1`-armed, or block-until-attached (see below) | + +`debug.listen()` usage: + +```ucode +import { listen } from 'debug'; + +// Arm SIGUSR1-triggered remote debugging on /tmp/ucode-debug-.sock, +// matching what -X and `udbg ` expect, and keep running. +listen(); + +// ...or pause right here, synchronously, until a debugger attaches (or a +// 30s timeout elapses) - also arms SIGUSR1 for later, same as above. +listen(true); + +// ...or bind an arbitrary, caller-chosen socket path and block +// indefinitely until a client connects on it, independent of SIGUSR1. +listen("/tmp/ucode-debug.sock"); +``` + +This is the primary way to enable remote debugging in a host application +that embeds the ucode VM directly (uhttpd, uwsd, ...) and therefore has no +`-X` flag of its own. `debug.listen()`'s `SIGUSR1` handling is dispatched +through ucode's own `signal()` builtin (`uc_vm_signal_dispatch()`, itself +only ever called from *within* the VM's per-instruction loop) rather than +the `-X` flag's `uc_vm_break_request()`/`STATUS_BREAK` mechanism, since the +latter unwinds the *entire* C call stack back to whoever called +`uc_vm_execute()` - fine for `main.c`'s own `-X` loop, but not safe for a +host calling `uc_vm_call()` from its own request-handling code, which would +have no way to handle an unexpected `STATUS_BREAK` bubbling out of what it +thought was a normal call. + +--- + +## `udbg` Client + +`udbg` is a typed-command protocol client with ANSI source rendering (the +original interactive debugger's exact ucode/utpl syntax highlighter and +statement/header-bar styling, ported into `debug_highlight.c` - see below) +but no line-editing or history yet; that's follow-up work that can be built +against this same protocol without touching the server again. + +``` +udbg [-s DIR] # SIGUSR1-attach to a running `-X` process, gdb -p style +udbg [-s DIR] # connect to an explicit debug.listen(path) socket +udbg [-s DIR] --fd # use an inherited, already-connected fd (internal, used by `-x`) +``` + +`-s DIR`/`--srcdir DIR` gives a local directory to also look for source +files under (by basename) when the server-reported path doesn't exist +as-is on this machine - see "Source resolution" above. + +Typed commands at the `dbg >` prompt map directly onto the protocol verbs +above (`break `, `delete [id]`, `list`, `next`, `step`, `continue`, +`return`, `backtrace [full]`, `variables`, `sources`, `print `, +`lines [spec] [before] [after]`, `throw [type] `, `disassemble +[spec]`, `source `, `help [verb]`, `quit`). + +--- + +## Breakpoint Location Syntax + +Used by `BREAK`'s `spec` field, `debug.breakpoint()`, and the `-x`/`-X` +command-line breakpoint argument: + +``` +path[:line[:col]] # File and line number (path optional if a frame is active) +line[:col] # Line in the current file (requires an active frame) +expression # ucode expression evaluating to a function (e.g. obj.method) +(expression) # Parens to disambiguate an expression from a bare path +``` + +A `path`/`line` spec resolves to the next real bytecode statement at or +after that position - breaking on a comment-only or blank line lands on the +next actual statement, not an error. + +--- + +## Building on the Protocol: Local `-x` Wiring + +`uc_debugger()` (`lib/debug.c`) does the following once, on first call: + +1. `socketpair(AF_UNIX, SOCK_STREAM, 0, sv)`. +2. `fork()`; the child `dup2(sv[1], 3)` and `execlp("udbg", "udbg", "--fd", + "3", NULL)`. +3. The parent closes its copy of `sv[1]`, keeps `sv[0]` as the session fd + (`debug_remote_set_active_fd()`), and proceeds exactly like the + remote-attach case from here on. + +Neither process ever manipulates the *debuggee's* own stdin/stdout for +protocol traffic - the child (client) inherits the real controlling +terminal for its own I/O, and the parent (VM) only ever reads/writes the +socketpair fd. If the client process dies or disconnects, this is treated +like a remote client dropping the connection: the script is resumed +unattended rather than left hanging. + +--- + +## Testing + +`tests/custom/99_debugger/run_debugger_tests.uc` is a standalone (non-cram) +integration suite that starts real target scripts via `ucode -X:1` +(the same attach-socket mechanism `-X`/`udbg` use), connects to the +resulting PID-derived Unix domain socket with the `socket` module, sends +batches of protocol messages, and asserts on the *parsed* JSON responses +and/or the target script's own stdout - never on rendered text, since +nothing is rendered server-side. Run it directly with: + +```bash +UCODE_BIN=/path/to/build/ucode ./build/ucode -L build tests/custom/99_debugger/run_debugger_tests.uc +``` + +Note for anyone writing new cases: a bare line-number `BREAK`/`-X` spec +against a script that is *only* variable declarations (no function calls or +other statements) is a narrow, pre-existing edge case in +`resolve_breakpoint()`/`lookup_stmt_boundary()` that doesn't always resolve +reliably - prefer `STEP` to advance past declarations, or target a function +name instead, both of which are unaffected. diff --git a/include/ucode/chunk.h b/include/ucode/chunk.h index 1e6ab1f9..804a1225 100644 --- a/include/ucode/chunk.h +++ b/include/ucode/chunk.h @@ -26,10 +26,13 @@ __hidden void uc_chunk_init(uc_chunk_t *chunk); __hidden void uc_chunk_free(uc_chunk_t *chunk); -__hidden size_t uc_chunk_add(uc_chunk_t *chunk, uint8_t byte, size_t line); +__hidden size_t uc_chunk_add(uc_chunk_t *chunk, uint8_t byte, size_t offset); __hidden void uc_chunk_pop(uc_chunk_t *chunk); +__hidden void uc_chunk_stmt_start(uc_chunk_t *chunk, size_t offset); +__hidden void uc_chunk_stmt_end(uc_chunk_t *chunk, size_t offset); + size_t uc_chunk_debug_get_srcpos(uc_chunk_t *chunk, size_t offset); __hidden void uc_chunk_debug_add_variable(uc_chunk_t *chunk, size_t from, size_t to, size_t slot, bool upval, uc_value_t *name); uc_value_t *uc_chunk_debug_get_variable(uc_chunk_t *chunk, size_t offset, size_t slot, bool upval); diff --git a/include/ucode/compiler.h b/include/ucode/compiler.h index a90d6717..d0d83def 100644 --- a/include/ucode/compiler.h +++ b/include/ucode/compiler.h @@ -101,8 +101,8 @@ typedef struct { uc_parse_config_t *config; uc_lexer_t lex; uc_token_t prev, curr; - bool synchronizing; uc_stringbuf_t *error; + bool synchronizing; } uc_parser_t; typedef struct uc_compiler { diff --git a/include/ucode/types.h b/include/ucode/types.h index 23137fad..a5ec3cd6 100644 --- a/include/ucode/types.h +++ b/include/ucode/types.h @@ -91,9 +91,14 @@ typedef struct { size_t from, to, slot, nameidx; } uc_varrange_t; +typedef struct { + uint8_t bytes; + uint8_t insns; +} uc_offset_t; + uc_declare_vector(uc_ehranges_t, uc_ehrange_t); uc_declare_vector(uc_variables_t, uc_varrange_t); -uc_declare_vector(uc_offsetinfo_t, uint8_t); +uc_declare_vector(uc_offsetinfo_t, uc_offset_t); typedef struct { size_t count; @@ -323,8 +328,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; @@ -341,7 +352,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; @@ -363,6 +374,8 @@ struct uc_vm { struct sigaction sa; int sigpipe[2]; } signal; + bool break_requested; + int break_notifyfd[2]; }; @@ -440,6 +453,40 @@ size_t ucv_object_length(uc_value_t *); : 0); \ entry##key = entry_next##key) +/* dict (value-key object) detection via hash table equal_fn sentinel */ +extern int uc_dict_equal(const void *k1, const void *k2); + +static inline bool +ucv_is_dict(uc_value_t *uv) +{ + uc_object_t *obj; + + if (((uintptr_t)uv & 3) != 0 || uv == NULL || uv->type != UC_OBJECT) + return false; + + obj = (uc_object_t *)uv; + + return (obj->table->equal_fn == uc_dict_equal); +} + +#define ucv_dict_foreach(dict, key, val) \ + uc_value_t *key = NULL; \ + uc_value_t *val = NULL; \ + struct lh_entry *entry##key; \ + struct lh_entry *entry_next##key = NULL; \ + for (entry##key = (ucv_type(dict) == UC_OBJECT) ? ((uc_object_t *)dict)->table->head : NULL; \ + (entry##key ? (key = (uc_value_t *)lh_entry_k(entry##key), \ + val = (uc_value_t *)lh_entry_v(entry##key), \ + entry_next##key = entry##key->next, entry##key) \ + : 0); \ + entry##key = entry_next##key) + +uc_value_t *ucv_dict_new(uc_vm_t *, uc_value_t *src); +uc_value_t *ucv_dict_get(uc_vm_t *, uc_value_t *, uc_value_t *); +uc_value_t *ucv_dict_set(uc_vm_t *, uc_value_t *, uc_value_t *, uc_value_t *); +bool ucv_dict_delete(uc_vm_t *, uc_value_t *, uc_value_t *); +size_t ucv_dict_length(uc_value_t *); + uc_value_t *ucv_cfunction_new(const char *, uc_cfn_ptr_t); uc_value_t *ucv_closure_new(uc_vm_t *, uc_function_t *, bool); diff --git a/include/ucode/vm.h b/include/ucode/vm.h index 53e4aaed..febd447a 100644 --- a/include/ucode/vm.h +++ b/include/ucode/vm.h @@ -112,6 +112,7 @@ typedef enum { typedef enum { STATUS_OK, STATUS_EXIT, + STATUS_BREAK, ERROR_COMPILE, ERROR_RUNTIME } uc_vm_status_t; @@ -122,7 +123,7 @@ typedef enum { #define GC_DEFAULT_INTERVAL 1000 -extern uint32_t insns[__I_MAX]; +extern const int8_t uc_vm_insn_format[__I_MAX]; void uc_vm_init(uc_vm_t *vm, uc_parse_config_t *config); void uc_vm_free(uc_vm_t *vm); @@ -161,4 +162,34 @@ uc_exception_type_t uc_vm_signal_dispatch(uc_vm_t *vm); void uc_vm_signal_raise(uc_vm_t *vm, int signo); int uc_vm_signal_notifyfd(uc_vm_t *vm); +/* Lazily wire up the self-pipe/handler array needed for the signal() + * builtin to work, independent of whether the embedding host opted into + * this via uc_parse_config_t.setup_signal_handlers. Without this, a VM + * initialized with that flag left unset (e.g. uc_vm_init(vm, NULL)) would + * silently install a NULL/SIG_DFL signal disposition the first time + * script code calls signal() with a callable handler - terminating the + * process on the next occurrence of that signal instead of invoking the + * handler. Call this before relying on signal() from C code that doesn't + * control how the VM was initialized (see lib/debug.c). Safe to call more + * than once. */ +void uc_vm_signal_handlers_ensure(uc_vm_t *vm); + +bool uc_vm_break_requested(uc_vm_t *vm); +void uc_vm_break_request(uc_vm_t *vm); +int uc_vm_break_notifyfd(uc_vm_t *vm); +void uc_vm_break_init(uc_vm_t *vm); +void uc_vm_break_cleanup(uc_vm_t *vm); + +uc_vm_status_t uc_vm_resume(uc_vm_t *vm); + +int8_t uc_vm_insn_to_argtype(uc_vm_insn_t insn); + +/* Well-known sentinel `uc_breakpoint_t.ip` value identifying the dedicated + * "break on uncaught exception" system breakpoint. Not a real bytecode + * address - install a breakpoint with this as its `ip` (and any `cb`) to + * have it invoked, with callframes fully intact, right before an exception + * that nothing would catch starts unwinding the stack. See the comment on + * uc_vm_exception_would_be_caught() in vm.c for the exact semantics. */ +extern uint8_t *const UC_BREAKPOINT_UNCAUGHT_EXCEPTION; + #endif /* UCODE_VM_H */ diff --git a/lib.c b/lib.c index a3d11d8d..a0b5682f 100644 --- a/lib.c +++ b/lib.c @@ -383,7 +383,9 @@ uc_length(uc_vm_t *vm, size_t nargs) switch (ucv_type(arg)) { case UC_OBJECT: - return ucv_int64_new(ucv_object_length(arg)); + return ucv_int64_new(ucv_is_dict(arg) + ? ucv_dict_length(arg) + : ucv_object_length(arg)); case UC_ARRAY: return ucv_int64_new(ucv_array_length(arg)); @@ -769,6 +771,58 @@ uc_die(uc_vm_t *vm, size_t nargs) return NULL; } +/** + * Create a dictionary object with arbitrary value keys. + * + * Unlike regular objects which are limited to string keys, dictionary objects + * allow any ucode value as a key. Key uniqueness follows the same semantics as + * the {@link module:core#uniq|uniq()} function: + * + * - Scalar values (null, boolean, integer, double, string): compared by value + * - Non-scalar values (arrays, objects, resources, closures): compared by + * reference (pointer equality) + * - NaN doubles are treated as equal + * + * If an existing object, dict or array is passed as argument, its entries + * are copied into the new dictionary: + * + * - Objects: string keys become string-value keys in the dict + * - Dicts: value keys are copied as-is + * - Arrays: numeric indices become integer-value keys + * + * @function module:core#dict + * + * @param {?*} [src=null] + * An optional source object, dict, or array to initialize from. + * + * @returns {Object} + * A new dictionary object. + * + * @example + * let d = dict(); + * d[true] = "yes"; + * d[false] = "no"; + * d[42] = "answer"; + * d["foo"] = "bar"; + * + * // keys() returns actual key values + * keys(d); // [true, false, 42, "foo"] + * + * // initialize from an existing object + * let d2 = dict({ a: 1, b: 2 }); + * d2["a"]; // 1 + * + * // spread dict into regular object (keys converted to strings) + * let obj = { ...d }; + */ +static uc_value_t * +uc_dict(uc_vm_t *vm, size_t nargs) +{ + uc_value_t *src = uc_fn_arg(0); + + return ucv_dict_new(vm, src); +} + /** * Check whether the given key exists within the given object value. * @@ -798,16 +852,24 @@ uc_exists(uc_vm_t *vm, size_t nargs) uc_value_t *key = uc_fn_arg(1); bool found, freeable; char *k; + uc_value_t *v; if (ucv_type(obj) != UC_OBJECT) return ucv_boolean_new(false); - k = uc_cast_string(vm, &key, &freeable); + if (ucv_is_dict(obj)) { + v = ucv_dict_get(vm, obj, key); + found = (v != NULL); + if (v) + ucv_put(v); + } else { + k = uc_cast_string(vm, &key, &freeable); - ucv_object_get(obj, k, &found); + ucv_object_get(obj, k, &found); - if (freeable) - free(k); + if (freeable) + free(k); + } return ucv_boolean_new(found); } @@ -1110,9 +1172,17 @@ uc_keys(uc_vm_t *vm, size_t nargs) arr = ucv_array_new(vm); - ucv_object_foreach(obj, key, val) { - (void)val; - ucv_array_push(arr, ucv_string_new(key)); + if (ucv_is_dict(obj)) { + /* dict keys are values, return them directly */ + ucv_dict_foreach(obj, key, val) { + (void)val; + ucv_array_push(arr, ucv_get(key)); + } + } else { + ucv_object_foreach(obj, key, val) { + (void)val; + ucv_array_push(arr, ucv_string_new(key)); + } } return arr; @@ -2119,9 +2189,16 @@ uc_values(uc_vm_t *vm, size_t nargs) arr = ucv_array_new(vm); - ucv_object_foreach(obj, key, val) { - (void)key; - ucv_array_push(arr, ucv_get(val)); + if (ucv_is_dict(obj)) { + ucv_dict_foreach(obj, key, val) { + (void)key; + ucv_array_push(arr, ucv_get(val)); + } + } else { + ucv_object_foreach(obj, key, val) { + (void)key; + ucv_array_push(arr, ucv_get(val)); + } } return arr; @@ -2748,9 +2825,11 @@ uc_require_ucode(uc_vm_t *vm, const char *path, uc_value_t *scope, uc_value_t ** *res = uc_require_imports(vm, closure); } - uc_vm_stack_pop(vm); - uc_vm_stack_pop(vm); - uc_vm_stack_pop(vm); + if (vm->stack.count >= 3) { + uc_vm_stack_pop(vm); + uc_vm_stack_pop(vm); + uc_vm_stack_pop(vm); + } } } @@ -5949,6 +6028,7 @@ uc_signal(uc_vm_t *vm, size_t nargs) const uc_function_list_t uc_stdlib_functions[] = { { "chr", uc_chr }, { "die", uc_die }, + { "dict", uc_dict }, { "exists", uc_exists }, { "exit", uc_exit }, { "filter", uc_filter }, diff --git a/lib/debug.c b/lib/debug.c index 5c9949a5..f7d448a0 100644 --- a/lib/debug.c +++ b/lib/debug.c @@ -60,7 +60,21 @@ #include #include #include +#include +#include +#include #include +#include +#include +#include +#include +#include + +#include "debug_remote.h" +#include "debug_proto.h" + +/* Forward declarations from debug_remote.c */ +extern bool debug_remote_has_active_connection(void); #ifdef HAVE_ULOOP #include @@ -71,6 +85,8 @@ #include "ucode/module.h" #include "ucode/platform.h" +#include "ucode/compiler.h" +#include "ucode/vm.h" static char *memdump_signal = "USR2"; @@ -586,6 +602,71 @@ static struct { uc_vm_t *vm; } signal_handle; +static struct { + struct uloop_fd ufd; + uc_vm_t *vm; +} break_handle; + +static bool debug_attach_mode = false; + +typedef enum { + BK_ONCE, + BK_USER, + BK_STEP, + BK_CATCH, + /* Dedicated system breakpoint firing once per raise, right before an + * exception that nothing would catch starts unwinding the stack - see + * UC_BREAKPOINT_UNCAUGHT_EXCEPTION in vm.c. Unlike BK_STEP/BK_CATCH it + * isn't tied to a concrete instruction address (dbk->bk.ip is instead + * the UC_BREAKPOINT_UNCAUGHT_EXCEPTION sentinel), and unlike BK_USER + * it's armed automatically for the lifetime of the debug session, not + * by an explicit `break` command. */ + BK_UNCAUGHT, + /* Dedicated system breakpoint for an async "pause now" request from an + * already-attached client (SIGUSR1 while running - see + * debug_break_signal_handler()) - the connected-client counterpart to + * `-X`'s bare SIGUSR1 attach for a not-yet-attached one. Its struct is + * pre-created (see install_debug_system_breakpoints()) and never + * freed, specifically so the signal handler only ever has to write + * already-allocated fields (dbk->bk.ip/cb) - genuinely + * async-signal-safe, unlike get_breakpoint()'s malloc path. Between + * requests dbk->bk.ip sits at debug_interrupt_disarmed_marker (a + * dedicated inert sentinel, not NULL - a NULL ip is itself the + * "fire on every single instruction" convention the generic + * per-instruction breakpoint check in vm.c uses, the opposite of + * idle); the handler arms it by pointing ip at NULL, and + * bk_handle_interrupt() immediately disarms it again (back to the + * inert marker) before entering the session, so it fires exactly + * once per request instead of on every instruction from then on. */ + BK_INTERRUPT, +} debug_breakpoint_kind_t; + +typedef struct debug_breakpoint { + uc_breakpoint_t bk; + uc_function_t *fn; + size_t depth; + debug_breakpoint_kind_t kind; + /* Set instead of actually freeing the struct when "delete" removes the + * breakpoint bk_enter_session() is *currently* handling: that C stack frame + * still holds this pointer and keeps handling further commands (and, + * for "next"/"step", keeps reading ->depth) for the rest of the CLI + * session, so freeing it there and then would be a use-after-free the + * moment the next command runs, and a double free once bk_enter_session()'s + * own end-of-session cleanup runs free_breakpoint() on it again. The + * breakpoint is unlinked from vm->breakpoints immediately either way + * (so it can't fire again); only the free() of the struct itself is + * deferred until bk_enter_session() is done with it. */ + bool deleted; +} debug_breakpoint_t; + +/* Dedicated inert marker for BK_INTERRUPT - see its debug_breakpoint_kind_t + * comment above for why this can't just be NULL. Declared this early so + * debug_break_signal_handler() below can reference it. */ +static uint8_t debug_interrupt_disarmed_marker; + +static void bk_enter_session(uc_vm_t *vm, uc_breakpoint_t *bk); +static uc_callframe_t *uc_debug_curr_frame(uc_vm_t *vm, size_t off); + static void uc_uloop_signal_cb(struct uloop_fd *ufd, unsigned int events) { @@ -593,28 +674,125 @@ uc_uloop_signal_cb(struct uloop_fd *ufd, unsigned int events) uloop_end(); } +static void +uc_uloop_break_cb(struct uloop_fd *ufd, unsigned int events) +{ + char c; + while (read(break_handle.ufd.fd, &c, 1) > 0) { + /* break requested */ + } + + /* In attach mode, launch the debugger CLI immediately */ + if (debug_attach_mode) { + uc_vm_t *vm = break_handle.vm; + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + + if (frame) { + debug_breakpoint_t dbk = { + .bk = { .ip = frame->ip }, + .fn = frame->closure->function, + .kind = BK_USER + }; + + bk_enter_session(vm, &dbk.bk); + } + } +} + static void debug_setup_uloop(uc_vm_t *vm) { int signal_fd = uc_vm_signal_notifyfd(vm); + int break_fd = uc_vm_break_notifyfd(vm); + + if (uloop_init() < 0) + return; - if (signal_fd != -1 && uloop_init() == 0) { + if (signal_fd != -1) { signal_handle.vm = vm; signal_handle.ufd.cb = uc_uloop_signal_cb; signal_handle.ufd.fd = signal_fd; uloop_fd_add(&signal_handle.ufd, ULOOP_READ); } + + if (break_fd != -1) { + break_handle.vm = vm; + break_handle.ufd.cb = uc_uloop_break_cb; + break_handle.ufd.fd = break_fd; + + uloop_fd_add(&break_handle.ufd, ULOOP_READ); + } } #else static void debug_setup_uloop(uc_vm_t *vm) {} #endif +/* Global vm pointer for SIGUSR1 handler */ +static uc_vm_t *debug_break_vm = NULL; + +static void +debug_break_signal_handler(int sig) +{ + /* A debugger is already attached - SIGUSR1 arriving here could be the + * attached client's own Ctrl-C-while-running interrupt request (see + * udbg.c), or an unrelated external sender; there's no way to tell + * which, and this deliberately no longer distinguishes them (that + * used to just forward an "already attached, ignoring" notification - + * debug_remote_notify_signal(), removed - without actually pausing + * anything): any SIGUSR1 while attached now arms a real break, the + * same as it would for the not-yet-attached case just below, since + * "someone sent SIGUSR1 to a debugged process" is a deliberate act + * either way and "pause for inspection" is the reasonable universal + * response to it. Arm BK_INTERRUPT so it fires on the very next + * instruction (see its debug_breakpoint_kind_t comment) rather than + * the VM API break used below for the not-yet-attached case: that one + * unwinds the whole C call stack back to -X's own main loop (see + * uc_vm_break_request()'s doc comment), which would tear down the + * live session instead of pausing it. Only a direct field write - dbk + * was pre-created specifically so this never has to call + * get_breakpoint()'s malloc path from signal-handler context. */ + if (debug_remote_has_active_connection()) { + for (size_t i = 0; i < debug_break_vm->breakpoints.count; i++) { + debug_breakpoint_t *dbk = (debug_breakpoint_t *)debug_break_vm->breakpoints.entries[i]; + + if (dbk && dbk->kind == BK_INTERRUPT) { + dbk->bk.ip = NULL; + break; + } + } + + return; + } + + /* Signal handler - request break via VM API + * The actual break will be processed by uloop or the VM */ + if (debug_break_vm) + uc_vm_break_request(debug_break_vm); +} + +static void +debug_setup_break_signal(uc_vm_t *vm) +{ + struct sigaction sa = { 0 }; + + debug_break_vm = vm; + + sa.sa_handler = debug_break_signal_handler; + sa.sa_flags = SA_RESTART; + sigemptyset(&sa.sa_mask); + + /* Only install if not already handled by debug module */ + if (sigaction(SIGUSR1, &sa, NULL) != 0) + fprintf(stderr, "SIGUSR1 handler installation failed: %s\n", strerror(errno)); +} + static void 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 +806,35 @@ 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 uc_exception_handler_t *debug_prev_exhandler = NULL; + +static void +debug_exception_notify_handler(uc_vm_t *vm, uc_exception_t *ex) +{ + /* Forward uncaught exceptions to an attached remote debugger client, + * in addition to whatever the previously installed handler does + * (normally printing to stderr). No-op when nobody is attached. + * vm->output (stdout) is fully block-buffered once it's a socket + * rather than a tty, while the notification itself goes out via a raw + * write() - flush first, or the event can overtake not-yet-flushed + * script output the target already produced earlier. */ + if (debug_remote_has_active_connection()) { + fflush(vm->output); + debug_remote_notify_exception(vm, ex); + } + + if (debug_prev_exhandler) + debug_prev_exhandler(vm, ex); } static void @@ -640,10 +842,26 @@ debug_setup(uc_vm_t *vm) { char *ev; + /* Make sure the ucode-level signal() builtin actually works, + * regardless of whether the embedding host opted into + * uc_parse_config_t.setup_signal_handlers - debug_setup_memdump() + * below and debug.attach()/debug.listen()/debug.debugger() all rely + * on it, and a host that simply calls uc_vm_init(vm, NULL) (uwsd, + * uhttpd) gets that flag unset by default. Without this, installing + * one of those handlers would silently end up with a NULL/SIG_DFL + * disposition, terminating the process on the next occurrence of the + * signal instead of invoking the handler. */ + uc_vm_signal_handlers_ensure(vm); + ev = getenv("UCODE_DEBUG_MEMDUMP_ENABLED"); if (!ev || !strcmp(ev, "1") || !strcmp(ev, "yes") || !strcmp(ev, "true")) debug_setup_memdump(vm); + + debug_setup_break_signal(vm); + + debug_prev_exhandler = uc_vm_exception_handler_get(vm); + uc_vm_exception_handler_set(vm, debug_exception_notify_handler); } @@ -1652,20 +1870,3696 @@ uc_setupval(uc_vm_t *vm, size_t nargs) } -static const uc_function_list_t debug_fns[] = { - { "memdump", uc_memdump }, - { "traceback", uc_traceback }, - { "sourcepos", uc_sourcepos }, - { "getinfo", uc_getinfo }, - { "getlocal", uc_getlocal }, - { "setlocal", uc_setlocal }, - { "getupval", uc_getupval }, - { "setupval", uc_setupval }, -}; +/* ========================================================================== */ +/* Interactive debugger implementation follows */ +/* ========================================================================== */ + +typedef struct { + size_t nesting; + size_t off_start, off_end; + size_t pos_start, pos_end, pos_ip; + uint8_t *ip_start, *ip_end; +} insn_span_t; + +typedef struct { + const char *path; + size_t line; + size_t column; + size_t offset; + uc_program_t *program; + uc_source_t *source; + uc_function_t *function; +} location_t; + +#define uc_vector_add(vec, ...) ({ \ + uc_vector_push((vec), ((typeof((vec)->entries[0]))__VA_ARGS__)); \ + uc_vector_last(vec); \ +}) -void uc_module_init(uc_vm_t *vm, uc_value_t *scope) +static uc_callframe_t * +uc_debug_curr_frame(uc_vm_t *vm, size_t off) { - uc_function_list_register(scope, debug_fns); + if (off > vm->callframes.count) + return NULL; - debug_setup(vm); + 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; +} + +/* -- convert file path to module name -------------------------------------- */ +static char * +filename_to_modulename(uc_vm_t *vm, const char *filename) +{ + char *module_path = realpath(filename, NULL); + char *rv = NULL; + + if (!module_path) + module_path = (char *)filename; + + size_t len_module_path = strlen(module_path); + + uc_value_t *search = + ucv_object_get(uc_vm_scope_get(vm), "REQUIRE_SEARCH_PATH", NULL); + + for (size_t i = 0; rv == NULL && i < ucv_array_length(search); i++) { + uc_value_t *p = ucv_array_get(search, i); + + if (ucv_type(p) != UC_STRING) + continue; + + char *search_spec = xstrdup(ucv_string_get(p)); + char *search_ext = strchr(search_spec, '*'); + + if (!search_ext) { + free(search_spec); + continue; + } + + *search_ext++ = 0; + + char *search_path = realpath(search_spec, NULL); + + if (!search_path) { + free(search_spec); + continue; + } + + size_t len_search_path = strlen(search_path); + size_t len_search_ext = strlen(search_ext); + + if (!strncmp(module_path, search_path, len_search_path) && + module_path[len_search_path] == '/' && + len_module_path > len_search_ext && + !strcmp(module_path + len_module_path - len_search_ext, search_ext)) + { + xasprintf(&rv, "%.*s", + (int)(len_module_path - (len_search_path + 1 + len_search_ext)), + module_path + len_search_path + 1); + + for (char *p = rv; *p; p++) + if (*p == '/') + *p = '.'; + } + + free(search_spec); + free(search_path); + } + + free(module_path); + + return rv; +} + +/* -- helper routines to deal with print buffers ---------------------------- */ +static size_t +utf8_sequence_length(const char *s) +{ + const uint8_t *c = (const uint8_t *)s; + + if ((c[0] & 0xe0) == 0xc0 && + (c[1] & 0xc0) == 0x80) + return 2; + + if ((c[0] & 0xf0) == 0xe0 && + (c[1] & 0xc0) == 0x80 && + (c[2] & 0xc0) == 0x80) + return 3; + + if ((c[0] & 0xf8) == 0xf0 && + (c[1] & 0xc0) == 0x80 && + (c[2] & 0xc0) == 0x80 && + (c[3] & 0xc0) == 0x80) + return 4; + + return (*c != 0); +} + +static size_t +esc_sequence_length(const char *s) +{ + if (s[0] == '\033' && s[1] == '[') { + size_t i = 2; + + while (s[i] != '\0' && s[i] != 'm') + i++; + + return i + (s[i] == 'm'); + } + + return 0; +} + +static size_t +strwidth(const char *s) +{ + size_t len = 0; + + while (*s) { + s += esc_sequence_length(s); + + size_t n = utf8_sequence_length(s); + + if (n) { + s += n; + len++; + } + } + + return len; +} + +static bool +str_startswith(const char *s, const char *substr) +{ + if (substr == NULL) + return true; + + return strncmp(s, substr, strlen(substr)) == 0; +} + +static size_t +printbuf_truncate(uc_stringbuf_t *sb, size_t off, size_t maxcols, bool tail) +{ + if (maxcols == 0) { + sb->bpos = off; + sb->buf[off] = 0; + + return 0; + } + + size_t len = strwidth(sb->buf + off); + char *s = sb->buf + off; + + if (tail == false && len > maxcols) { + for (size_t i = 0; i < len - maxcols + 1; i++) { + s += esc_sequence_length(s); + s += utf8_sequence_length(s); + } + + size_t keeplen = (sb->buf + sb->bpos) - s; + size_t trunclen = s - (sb->buf + off); + size_t elliplen = sizeof("…") - 1; + + /* Reserve enough additional space for ellipsis mb sequence. */ + if (trunclen < elliplen) + printbuf_memset(sb, -1, ' ', elliplen - trunclen); + + memmove(sb->buf + off + elliplen, s, keeplen); + memcpy(sb->buf + off, "…", elliplen); + + sb->bpos += elliplen; + sb->bpos -= trunclen; + sb->buf[sb->bpos] = 0; + + return maxcols; + } + + if (tail == true && len > maxcols) { + for (size_t i = 0; i < maxcols - 1; i++) { + s += esc_sequence_length(s); + s += utf8_sequence_length(s); + } + + sb->bpos = s - sb->buf; + printbuf_strappend(sb, "…"); + + return maxcols; + } + + return len; +} + +static size_t +printbuf_append_uv(uc_stringbuf_t *sb, uc_vm_t *vm, uc_value_t *val, + size_t maxcols) +{ + int pos = sb->bpos; + const char *end; + size_t len; + + ucv_to_stringbuf(vm, sb, val, false); + + len = strwidth(sb->buf + pos); + + if (len > maxcols) { + switch (sb->buf[pos]) { + case '{': len = maxcols - 3; end = "… }"; break; + case '[': len = maxcols - 3; end = "… ]"; break; + case '"': len = maxcols - 2; end = "…\""; break; + default: len = maxcols - 1; end = "…"; break; + } + + for (sb->bpos = pos; len > 0; len--) + sb->bpos += utf8_sequence_length(sb->buf + sb->bpos); + + printbuf_memappend_fast(sb, end, strlen(end)); + + return maxcols; + } + + return len; +} + +static size_t +printbuf_append_funcname(uc_stringbuf_t *sb, uc_vm_t *vm, uc_value_t *val, + size_t maxcols) +{ + char *placeholder = NULL; + int off = sb->bpos; + + 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) { + printbuf_memappend_fast(sb, rt->name, strlen(rt->name)); + printbuf_strappend(sb, "#"); + goto name; + } + } + } + + 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) { + printbuf_memappend_fast(sb, modname, strlen(modname)); + printbuf_strappend(sb, "."); + goto name; + } + } + } + +name: + if (ucv_type(val) == UC_CLOSURE) { + uc_function_t *fn = ((uc_closure_t *)val)->function; + + if (fn->name[0]) { + printbuf_memappend_fast(sb, fn->name, strlen(fn->name)); + goto done; + } + + placeholder = fn->arrow ? "λ" : "𝑓"; + } + else if (ucv_type(val) == UC_CFUNCTION) { + uc_cfunction_t *cf = (uc_cfunction_t *)val; + + if (cf->name[0]) { + printbuf_memappend_fast(sb, cf->name, strlen(cf->name)); + goto done; + } + + placeholder = "𝑓"; + } + else { + return 0; + } + + /* no prefix and no name yet, try to name by containing property name */ + for (uc_weakref_t *ref = vm->values.next; + ref != &vm->values && sb->bpos == off; + ref = ref->next) + { + uc_object_t *obj = + (uc_object_t *)((char *)ref - offsetof(uc_object_t, ref)); + + if (obj->header.type != UC_OBJECT) + continue; + + ucv_object_foreach(&obj->header, k, v) { + if (v == val) { + printbuf_memappend_fast(sb, k, strlen(k)); + printbuf_strappend(sb, ":"); + break; + } + } + } + + printbuf_memappend_fast(sb, placeholder, strlen(placeholder)); + +done: + return printbuf_truncate(sb, off, maxcols, true); +} + +static size_t +printbuf_append_function(uc_stringbuf_t *sb, uc_vm_t *vm, uc_value_t *val, + uc_callframe_t *frame, size_t maxcols) +{ + uc_type_t t = ucv_type(val); + int off = sb->bpos; + + if (t == UC_CFUNCTION) { + printbuf_append_funcname(sb, vm, val, SIZE_MAX); + printbuf_strappend(sb, "("); + + if (frame) { + size_t prev_frame = vm->stack.count; + + for (size_t i = vm->callframes.count; i > 0; i--) { + if (&vm->callframes.entries[i - 1] == frame) + break; + + prev_frame = vm->callframes.entries[i - 1].stackframe; + } + + for (size_t j = 1; j < prev_frame - frame->stackframe; j++) { + if (j > 1) + printbuf_strappend(sb, ", "); + + uc_value_t *argval = + (frame->stackframe + j < vm->stack.count) + ? vm->stack.entries[frame->stackframe + j] + : NULL; + + printbuf_append_uv(sb, vm, argval, 32); + } + } + + printbuf_strappend(sb, ")"); + } + else if (t == UC_CLOSURE) { + uc_closure_t *cl = (uc_closure_t *)val; + uc_source_t *source = uc_program_function_source(cl->function); + + if (cl->function->module) { + char *s = filename_to_modulename(vm, source->filename); + sprintbuf(sb, "module(%s)", s ? s : ""); + free(s); + } + else { + printbuf_append_funcname(sb, vm, val, SIZE_MAX); + printbuf_strappend(sb, "("); + + if (frame) { + for (size_t i = 0; i < cl->function->nargs; i++) { + uc_value_t *argname = uc_chunk_debug_get_variable( + &cl->function->chunk, i, i + 1, false); + + if (i > 0) + printbuf_strappend(sb, ", "); + + if (i + 1 == cl->function->nargs && cl->function->vararg) + printbuf_strappend(sb, "..."); + + if (argname) { + printbuf_memappend_fast(sb, + ucv_string_get(argname), + ucv_string_length(argname)); + + printbuf_strappend(sb, "="); + ucv_put(argname); + } + else { + sprintbuf(sb, "$%zu=", i + 1); + } + + uc_value_t *argval = + (frame->stackframe + i + 1 < vm->stack.count) + ? vm->stack.entries[frame->stackframe + i + 1] + : NULL; + + printbuf_append_uv(sb, vm, argval, 32); + } + } + + printbuf_strappend(sb, ")"); + } + } + + return printbuf_truncate(sb, off, maxcols, true); +} + +static size_t +printbuf_append_srcpath(uc_stringbuf_t *sb, uc_source_t *source, size_t maxcols) +{ + int off = sb->bpos; + + printbuf_memset(sb, off + PATH_MAX, 0, 1); + + if (realpath(source->filename, sb->buf + off)) { + size_t pathlen = strlen(sb->buf + off); + char cwd[PATH_MAX]; + + if (getcwd(cwd, sizeof(cwd))) { + size_t cwdlen = strlen(cwd); + + if (strncmp(sb->buf + off, cwd, cwdlen) == 0 && + sb->buf[off + cwdlen] == '/') + { + pathlen -= cwdlen + 1; + memmove(sb->buf + off, sb->buf + off + cwdlen + 1, pathlen); + } + } + + sb->bpos = off + pathlen; + sb->buf[sb->bpos] = 0; + } + else { + sb->bpos = off; + printbuf_memappend_fast(sb, + source->filename, strlen(source->filename)); + } + + return printbuf_truncate(sb, off, maxcols, false); +} + +static void +bk_enter_session(uc_vm_t *vm, uc_breakpoint_t *bk); + +static void +bk_handle_catch(uc_vm_t *vm, uc_breakpoint_t *bk); + +static void +bk_handle_uncaught(uc_vm_t *vm, uc_breakpoint_t *bk); + +static void +bk_handle_interrupt(uc_vm_t *vm, uc_breakpoint_t *bk); + +static debug_breakpoint_t * +get_breakpoint(uc_vm_t *vm, debug_breakpoint_kind_t kind) +{ + debug_breakpoint_t *dbk; + + for (size_t i = 0; i < vm->breakpoints.count; i++) { + dbk = (debug_breakpoint_t *)vm->breakpoints.entries[i]; + + if (dbk != NULL && dbk->kind == kind) + return dbk; + } + + dbk = xalloc(sizeof(*dbk)); + dbk->kind = kind; + uc_vector_push(&vm->breakpoints, &dbk->bk); + + return dbk; +} + +static void +update_breakpoint(uc_vm_t *vm, debug_breakpoint_kind_t kind, + void (*cb)(uc_vm_t *, uc_breakpoint_t *), uint8_t *ip, + uc_function_t *fn, size_t depth) +{ + debug_breakpoint_t *dbk = get_breakpoint(vm, kind); + + dbk->bk.cb = cb; + dbk->depth = depth; + dbk->fn = fn; + + /* If the target instruction is the same then invoke handler directly */ + if (dbk->bk.ip == ip) + dbk->bk.cb(vm, &dbk->bk); + else + dbk->bk.ip = ip; +} + +/* Remove a breakpoint from vm->breakpoints so it can no longer fire, without + * freeing its backing memory - see the `deleted` field comment above for why + * these two steps sometimes need to happen at different times. */ +static bool +unlink_breakpoint(uc_vm_t *vm, uc_breakpoint_t *bk) +{ + uc_breakpoints_t *bks = &vm->breakpoints; + bool found = false; + + /* Blank out breakpoint slot */ + for (size_t i = bks->count; i > 0; i--) { + if (bks->entries[i - 1] == bk) { + bks->entries[i - 1] = NULL; + found = true; + break; + } + } + + /* Cleanup empty tail of the breakpoint vector */ + while (bks->count > 0 && bks->entries[bks->count - 1] == NULL) + bks->count--; + + return found; +} + +static bool +free_breakpoint(uc_vm_t *vm, uc_breakpoint_t *bk) +{ + bool found = unlink_breakpoint(vm, bk); + + free(bk); + + return found; +} + +/* Delete a breakpoint via the "delete" CLI command. `dbk` is the one to + * remove; `current` is the breakpoint bk_enter_session() is presently handling + * (its `dbk` parameter), still alive on that C stack frame and still going + * to be dereferenced by further commands in this same session (and, for + * BK_STEP, possibly by bk_enter_session()'s own end-of-session cleanup). If + * they're the same object, only unlink it now and mark it `deleted` so + * bk_enter_session() frees it once it's actually done with it; otherwise it's + * safe to free it outright. */ +static void +delete_breakpoint(uc_vm_t *vm, debug_breakpoint_t *dbk, debug_breakpoint_t *current) +{ + if (dbk == current) { + unlink_breakpoint(vm, &dbk->bk); + dbk->deleted = true; + } + else { + free_breakpoint(vm, &dbk->bk); + } +} + +/* Arm the dedicated "break on uncaught exception" system breakpoint (see + * UC_BREAKPOINT_UNCAUGHT_EXCEPTION in vm.c), and pre-create (inert - see + * BK_INTERRUPT's debug_breakpoint_kind_t comment) the async "pause now" + * one, for the lifetime of the debug session. Idempotent - safe to call + * from every entry point that can start a session (uc_debugger(), + * uc_debug_attach(), uc_debug_listen()), each of which only runs its + * one-time setup once anyway, but this keeps that invariant local rather + * than relying on the caller not to double-arm it. */ +static void +install_debug_system_breakpoints(uc_vm_t *vm) +{ + debug_breakpoint_t *dbk = get_breakpoint(vm, BK_UNCAUGHT); + + dbk->bk.cb = bk_handle_uncaught; + dbk->bk.ip = UC_BREAKPOINT_UNCAUGHT_EXCEPTION; + + dbk = get_breakpoint(vm, BK_INTERRUPT); + dbk->bk.cb = bk_handle_interrupt; + dbk->bk.ip = &debug_interrupt_disarmed_marker; +} + +static size_t +patch_breakpoint(uc_vm_t *vm, uc_function_t *fn, size_t insnoff, + debug_breakpoint_kind_t kind, size_t depth) +{ + debug_breakpoint_t *dbk = xalloc(sizeof(debug_breakpoint_t)); + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_breakpoints_t *bks = &vm->breakpoints; + + dbk->bk.ip = fn ? &fn->chunk.entries[insnoff] : NULL; + dbk->bk.cb = bk_enter_session; + dbk->fn = fn; + dbk->kind = kind; + dbk->depth = depth; + + /* When the user breakpoint to be installed is at the same instruction + offset as the current VM instruction pointer then ensure to append it + to the breakpoint stack, otherwise reclaim free entry. */ + if (frame == NULL || frame->ip != dbk->bk.ip) { + for (size_t i = 0; i < bks->count; i++) { + if (bks->entries[i] == NULL) { + bks->entries[i] = &dbk->bk; + + return i + 1; + } + } + } + + uc_vector_push(bks, &dbk->bk); + + return bks->count; +} + +static bool +filename_matches_pattern(const char *filename, const char *pattern) +{ + if (strchr(pattern, '/') || strchr(pattern, '*')) + return (fnmatch(filename, pattern, 0) == 0); + + const char *basename = strrchr(filename, '/'); + + if (basename) + return (strcmp(basename + 1, pattern) == 0); + + return (strcmp(filename, pattern) == 0); +} + +static bool +lookup_source(uc_vm_t *vm, location_t *loc) +{ + uc_stringbuf_t pattern = { 0 }, filename = { 0 }; + uc_weakref_t *ref; + uc_closure_t *uc; + + if (loc->program != NULL && loc->source != NULL) + return true; + + if (loc->path == NULL) + return false; + + printbuf_append_srcpath(&pattern, + &((uc_source_t){ .filename = (char *)loc->path }), SIZE_MAX); + + /* iterate all existing closures to find programs */ + 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 *program = uc->function->program; + + /* iterate all program sources looking for a patchname match */ + for (size_t i = 0; i < program->sources.count; i++) { + uc_source_t *source = program->sources.entries[i]; + + printbuf_append_srcpath(&filename, source, SIZE_MAX); + + if (filename_matches_pattern(filename.buf, pattern.buf)) { + size_t col = (loc->column > 0) ? loc->column - 1 : 0; + size_t rem = (loc->line > 0) ? loc->line - 1 : 0; + uc_lineinfo_t *lines = &source->lineinfo; + + /* iterate line lengths looking for exact offset */ + for (size_t j = 0, llen = 0, off = 0; j < lines->count; j++) { + size_t bytes = lines->entries[j] & 0x7f; + + if (rem == 0 && col >= llen && col <= llen + bytes) { + loc->program = program; + loc->source = source; + loc->offset = off + llen + col; + + free(filename.buf); + free(pattern.buf); + + return true; + } + + llen += bytes; + + if (j > 0 && lines->entries[j] & 0x80) { + off += llen + 1; + llen = 0; + rem--; + } + } + } + + printbuf_reset(&filename); + } + } + + free(filename.buf); + free(pattern.buf); + + return false; +} + +static bool +lookup_offset(uc_vm_t *vm, location_t *loc) +{ + if (!lookup_source(vm, loc)) + return false; + + size_t column = (loc->column > 0) ? loc->column - 1 : 0; + size_t remaining = (loc->line > 0) ? loc->line - 1 : 0; + uc_lineinfo_t *lines = &loc->source->lineinfo; + + /* iterate line lengths looking for exact offset */ + for (size_t j = 0, linelen = 0, offset = 0; j < lines->count; j++) { + size_t bytes = lines->entries[j] & 0x7f; + + if (remaining == 0 && column >= linelen && column <= linelen + bytes) { + loc->offset = offset + linelen + column; + + return true; + } + + linelen += bytes; + + if (j > 0 && lines->entries[j] & 0x80) { + offset += linelen + 1; + linelen = 0; + remaining--; + } + } + + return false; +} + +static bool +lookup_function(uc_vm_t *vm, location_t *loc) +{ + if (loc->function != NULL) + return true; + + if (!lookup_offset(vm, loc)) + return false; + + uc_program_function_foreach(loc->program, fn) { + if (uc_program_function_source(fn) != loc->source) + continue; + + size_t beg = uc_program_function_srcpos(fn, 0); + size_t end = uc_program_function_srcpos(fn, SIZE_MAX); + + if (beg <= loc->offset && end >= loc->offset) { + loc->function = fn; + + return true; + } + } + + return false; +} + +static bool +lookup_stmt_boundary(uc_vm_t *vm, location_t *loc, insn_span_t *sp) +{ + if (!lookup_function(vm, loc)) + return false; + + struct { insn_span_t *entries; size_t count; } sp_stack = { 0 }; + uc_chunk_t *chunk = &loc->function->chunk; + uc_offsetinfo_t *offsets = &chunk->debuginfo.offsets; + size_t bytes = loc->function->srcpos; + insn_span_t *s = NULL; + + for (size_t i = 0, insns = 0; i < offsets->count; i++) { + uc_offset_t *o = &offsets->entries[i]; + + if (o->bytes & 0x80) { + size_t nesting = sp_stack.count + 1; + + s = uc_vector_add(&sp_stack, { + .nesting = nesting, + .off_start = i, + .pos_start = bytes, + .pos_ip = bytes, + .ip_start = chunk->entries + insns + }); + } + + bytes += o->bytes & 0x7f; + insns += o->insns & 0x7f; + + if (insns > chunk->count) + goto not_found; /* out of range / invalid offset coding */ + + if (o->insns & 0x80) { + if (sp_stack.count == 0) + goto not_found; /* invalid offset coding */ + + s->off_end = i; + s->pos_end = bytes; + s->ip_end = chunk->entries + insns; + + if (s->pos_start <= loc->offset && s->pos_end >= loc->offset) + goto found; + + s = --sp_stack.count ? uc_vector_last(&sp_stack) : NULL; + } + + if (bytes > loc->offset && s == NULL) + goto not_found; /* past searched offset w/o matching range start */ + } + +not_found: + memset(sp, 0, sizeof(*sp)); + uc_vector_clear(&sp_stack); + + return false; + +found: + *sp = *uc_vector_last(&sp_stack); + uc_vector_clear(&sp_stack); + + return true; +} + +static size_t +add_breakpoint(uc_vm_t *vm, const char *path, size_t line, size_t byte, + debug_breakpoint_kind_t kind) +{ + location_t loc = { .path = path, .line = line, .column = byte }; + insn_span_t stmt; + + if (!lookup_stmt_boundary(vm, &loc, &stmt)) + return 0; + + return patch_breakpoint(vm, loc.function, + stmt.ip_start - loc.function->chunk.entries, kind, stmt.nesting); +} + +static uint8_t * +next_parent(uc_vm_t *vm, uc_function_t **fnp) +{ + for (size_t i = vm->callframes.count - 1; i > 0; i--) { + uc_callframe_t *pframe = &vm->callframes.entries[i - 1]; + + if (!pframe->closure) + continue; + + *fnp = pframe->closure->function; + + return pframe->ip; + } + + return NULL; +} + +static bool +find_statement_boundaries(uc_function_t *fn, uint8_t *ip, size_t depth, insn_span_t *sp) +{ + struct { insn_span_t *entries; size_t count; } sp_stack = { 0 }; + uc_offsetinfo_t *offsets = &fn->chunk.debuginfo.offsets; + size_t off = ip - fn->chunk.entries; + size_t i = 0, bytes = 0, insns = 0; + insn_span_t *s = NULL; + + for (i = 0; i < offsets->count; i++) { + uc_offset_t *o = &offsets->entries[i]; + + bytes += o->bytes & 0x7f; + + if (o->bytes & 0x80) { + size_t nesting = sp_stack.count + 1; + + s = uc_vector_add(&sp_stack, { + .nesting = nesting, + .off_start = i, + .pos_start = fn->srcpos + bytes, + .pos_ip = fn->srcpos + bytes, + .ip_start = &fn->chunk.entries[insns] + }); + } + + if (insns <= off && insns + (o->insns & 0x7f) > off && s != NULL) + s->pos_ip = fn->srcpos + bytes; + + insns += o->insns & 0x7f; + + if (insns > fn->chunk.count) + goto not_found; /* out of range / invalid offset codiing */ + + if (o->insns & 0x80) { + if (sp_stack.count == 0) + goto not_found; /* invalid offset coding */ + + if (depth == 0 || sp_stack.count == depth) { + s->off_end = i; + s->pos_end = fn->srcpos + bytes; + s->ip_end = &fn->chunk.entries[insns]; + + if (s->ip_start <= ip && s->ip_end > ip) + goto found; + } + + s = --sp_stack.count ? uc_vector_last(&sp_stack) : NULL; + } + + if (insns > off && s == NULL) + goto not_found; /* past searched offset w/o matching range start */ + } + +not_found: + memset(sp, 0, sizeof(*sp)); + uc_vector_clear(&sp_stack); + + return false; + +found: + *sp = *uc_vector_last(&sp_stack); + uc_vector_clear(&sp_stack); + + return true; +} + + +static uc_value_t * +uc_debug_sigint_handler(uc_vm_t *vm, size_t nargs); + + +// FIXME: read beyond end of array +static int32_t +insn_s32(uint8_t *ip) +{ + return ( + ip[0] * 0x1000000UL + + ip[1] * 0x10000UL + + ip[2] * 0x100UL + + ip[3] + ) - 0x7fffffff; +} + +static uint32_t +insn_u32(uint8_t *ip) +{ + return ( + ip[0] * 0x1000000UL + + ip[1] * 0x10000UL + + ip[2] * 0x100UL + + ip[3] + ); +} + +static uint32_t +insn_u16(uint8_t *ip) +{ + return ( + ip[0] * 0x100UL + + ip[1] + ); +} + +static size_t +insn_length(uint8_t *ip, uc_program_t *prog) +{ + if (*ip == I_CALL) + return 5 + ((insn_u32(ip + 1) >> 16) & 0x7fff) * 2; + + if (*ip == I_CLFN || *ip == I_ARFN) { + uint32_t u32 = insn_u32(ip + 1); + size_t i = 1; + uc_program_function_foreach(prog, fn) + if (i++ == u32) + return 5 + fn->nupvals * 4; + } + + return 1 + abs(uc_vm_insn_format[*ip]); +} + +static void +bk_enter_function(uc_vm_t *vm, uc_breakpoint_t *bk) +{ + debug_breakpoint_t *dbk = (debug_breakpoint_t *)bk; + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uint8_t *ip = frame->ip; + uint32_t argspec = 0; + bool enter = false; + + assert(dbk->kind == BK_STEP); + + if (*ip == I_CALL) { + argspec = insn_u32(ip + 1); + + size_t nargs = argspec & 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_function_t *fn = ((uc_closure_t *)fno)->function; + + dbk->bk.cb = bk_enter_session; + dbk->bk.ip = fn->chunk.entries; + dbk->depth = 1; + dbk->fn = fn; + enter = true; + } + } + } + + if (!enter) { + dbk->bk.cb = bk_enter_session; + dbk->bk.ip = NULL; + dbk->depth = 0; + dbk->fn = NULL; + } +} + +static void +bk_leave_function(uc_vm_t *vm, uc_breakpoint_t *bk) +{ + debug_breakpoint_t *dbk = (debug_breakpoint_t *)bk; + uc_callframe_t *frame = uc_debug_curr_frame(vm, 1); + + assert(dbk->kind == BK_STEP); + + if (!frame) + return; + + dbk->bk.cb = bk_enter_session; + dbk->bk.ip = frame->ip; + dbk->depth = 0; + dbk->fn = frame->closure->function; +} + +static void +bk_follow_jump(uc_vm_t *vm, uc_breakpoint_t *bk) +{ + debug_breakpoint_t *dbk = (debug_breakpoint_t *)bk; + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_program_t *prog = frame->closure->function->program; + uc_chunk_t *chunk = &frame->closure->function->chunk; + size_t off = frame->ip - chunk->entries; + uint8_t *ip = frame->ip; + + assert(dbk->kind == BK_STEP); + + /* skip conditional jmpz if conditition is true */ + if (*ip == I_JMPZ && ucv_is_truish(uc_vm_stack_peek(vm, 0))) { + off += insn_length(ip, prog); + } + + /* otherwise follow jump */ + else { + int32_t addr = insn_s32(ip + 1); + + if ((addr < 0 && (size_t)-addr > off) || + (addr >= 0 && (size_t)addr >= chunk->count)) + { + off += insn_length(ip, prog); + } + else { + off += addr; + } + } + + /* if the next offset is a jump instruction as well, then don't install + interactive breakpoint but re-invoke this breakpoint handler */ + if (chunk->entries[off] == I_JMP || chunk->entries[off] == I_JMPZ) + dbk->bk.cb = bk_follow_jump; + else + dbk->bk.cb = bk_enter_session; + + dbk->bk.ip = chunk->entries + off; + dbk->depth = 0; + dbk->fn = frame->closure->function; +} + +static void +bk_handle_catch(uc_vm_t *vm, uc_breakpoint_t *bk) +{ + bk_enter_session(vm, bk); +} + +/* cb for the dedicated BK_UNCAUGHT system breakpoint (see + * install_debug_system_breakpoints() / UC_BREAKPOINT_UNCAUGHT_EXCEPTION + * in vm.c). Invoked directly from vm.c's exception label, before any + * unwinding happens, so vm->exception and the full callframe stack are + * still exactly as they were at the point of the raise. */ +static void +bk_handle_uncaught(uc_vm_t *vm, uc_breakpoint_t *bk) +{ + bk_enter_session(vm, bk); +} + +/* cb for the dedicated BK_INTERRUPT system breakpoint (see its + * debug_breakpoint_kind_t comment and debug_break_signal_handler()). + * Disarms itself (back to the inert marker) *before* entering the + * session: it's invoked via the generic per-instruction ip==NULL "fire on + * every instruction" check in vm.c, so leaving it armed would make it + * fire again on the very next instruction once this session ends (e.g. + * from "continue"), forever, instead of just the one time the interrupt + * request asked for. */ +static void +bk_handle_interrupt(uc_vm_t *vm, uc_breakpoint_t *bk) +{ + bk->ip = &debug_interrupt_disarmed_marker; + + bk_enter_session(vm, bk); +} + +/* Sentinel returned by next_step() to mean "stay paused right where we + * are" - distinct from a real instruction address and from NULL (which + * means "no next instruction, resume unattended"). Used for the case where + * a single-step would return from the outermost callframe: there is no + * parent frame left for bk_leave_function() to arm a breakpoint in and no + * further instruction will ever be decoded once RETURN executes (the + * program terminates), so silently resuming would blow past the debugger + * entirely instead of stopping. See cmd_step_common(). + * + * Reuses the vm pointer itself as the sentinel value rather than a + * dedicated static byte: vm is already available at both the producing and + * consuming end, points at an object entirely disjoint from any bytecode + * ip, and needs no allocation to obtain (unlike e.g. the address of the + * BK_STEP breakpoint struct, which would force get_breakpoint() to + * lazily xalloc() it just to manufacture a comparison value). */ +#define STEP_STAY_PAUSED(vm) ((uint8_t *)(vm)) + +static uint8_t * +next_step(uc_vm_t *vm, uc_function_t **fnp, uint8_t *ip, bool single, size_t *depthp) +{ + insn_span_t stmt, next; + + if (find_statement_boundaries(*fnp, ip, 0, &stmt)) { + uc_program_t *prog = (*fnp)->program; + + for (uint8_t *p = ip; p < stmt.ip_end; p += insn_length(p, prog)) { + switch (*p) { + case I_CALL: + if (single) { + update_breakpoint(vm, BK_STEP, bk_enter_function, p, *fnp, 0); + + return NULL; + } + + break; + + case I_RETURN: + if (single) { + if (!uc_debug_curr_frame(vm, 1)) + return STEP_STAY_PAUSED(vm); + + update_breakpoint(vm, BK_STEP, bk_leave_function, p, *fnp, 0); + + return NULL; + } + + break; + + case I_JMP: + case I_JMPZ: + update_breakpoint(vm, BK_STEP, bk_follow_jump, p, *fnp, 0); + + return NULL; + } + } + + while (find_statement_boundaries(*fnp, stmt.ip_end, 0, &next)) { + /* if next statement fully contains our statement, continue */ + if (next.ip_start <= stmt.ip_start && next.ip_end >= stmt.ip_end) { + fprintf(stderr, "Redo %zu..%zu -> %zu..%zu\n", + stmt.pos_start, stmt.pos_end, next.pos_start, next.pos_end); + stmt = next; + continue; + } + + *depthp = next.nesting; + + return next.ip_start; + } + } + + *depthp = 0; + + return next_parent(vm, fnp); +} + +static uc_value_t * +load_constval(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); + } + } + else if (t == TAG_DBL) { + size_t off = (vallist->index[cidx] >> 3); + + if (off + sizeof(double) <= vallist->dsize) + return ucv_double_new(uc_double_unpack(vallist->data + off, false)); + } + else if (t == TAG_NUM) { + return ucv_uint64_new(vallist->index[cidx] >> 3); + } + else if (t == TAG_LNUM) { + size_t off = (vallist->index[cidx] >> 3); + + if (off + sizeof(uint64_t) <= vallist->dsize) + return ucv_uint64_new(be64toh(*(uint64_t *)(vallist->data + off))); + } + + return NULL; +} + +/* Data-only extraction of local variables/upvalues for the current frame, + * for the VARIABLES protocol response and BACKTRACE's optional per-frame + * variable dump - the client owns all rendering (column widths, colors, + * truncation), so this only ever emits raw name/kind/value_repr data. */ +static uc_value_t * +build_variables_json(uc_vm_t *vm, uc_callframe_t *frame) +{ + 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; + uc_value_t *arr = ucv_array_new(vm); + + if (frame->ctx) { + uc_value_t *item = ucv_object_new(vm); + uc_stringbuf_t vb = { 0 }; + + /* Compact, single-line repr, matching the pre-protocol variables + * listing's default (non-"full") mode - the client truncates long + * aggregate values rather than ever wrapping them onto several + * lines, so pretty-printing here would defeat that. */ + ucv_to_stringbuf(vm, &vb, frame->ctx, false); + + ucv_object_add(item, "name", ucv_string_new("this")); + ucv_object_add(item, "kind", ucv_string_new("this")); + ucv_object_add(item, "value_repr", ucv_string_new_length(vb.buf, vb.bpos)); + + free(vb.buf); + ucv_array_push(arr, item); + } + + 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_constval(names, decls->entries[i].nameidx); + size_t slot = decls->entries[i].slot; + bool is_upval = slot >= (size_t)-1 / 2; + uc_value_t *item = ucv_object_new(vm); + uc_value_t *vval = NULL; + bool shadowed = false; + + /* decls entries are recorded innermost-scope-first (a nested + * block's own locals close, and get their debug range added, as + * soon as *that* block ends - see uc_compiler_leave_scope() - + * strictly before the enclosing scope's own locals do, whenever + * that later happens to be) - so among entries whose range covers + * `pos` (i.e. genuinely simultaneously in scope here, not just + * same-named siblings in two different, mutually exclusive + * branches), an earlier index is always the more-nested one: the + * one real script code actually resolves this name to right now. + * A same-named *later* entry is a shadowed outer declaration - + * still shown (its stack slot is real and still holds a value), + * just flagged so the listing doesn't look like a duplicate. */ + if (vname) { + for (size_t j = 0; j < i; j++) { + if (decls->entries[j].from > pos || decls->entries[j].to < pos) + continue; + + uc_value_t *other = load_constval(names, decls->entries[j].nameidx); + bool same = other && ucv_is_equal(vname, other); + + ucv_put(other); + + if (same) { + shadowed = true; + break; + } + } + } + + if (vname) { + ucv_object_add(item, "name", ucv_get(vname)); + } + else { + char buf[32]; + snprintf(buf, sizeof(buf), "$%zu", slot); + ucv_object_add(item, "name", ucv_string_new(buf)); + } + + if (shadowed) + ucv_object_add(item, "shadowed", ucv_boolean_new(true)); + + if (!is_upval) { + bool is_internal = (vname && *ucv_string_get(vname) == '('); + + ucv_object_add(item, "kind", ucv_string_new(is_internal ? "internal" : "local")); + + if (frame->stackframe + slot < vm->stack.count) + vval = vm->stack.entries[frame->stackframe + slot]; + } + else { + size_t upslot = slot - ((size_t)-1 / 2); + + ucv_object_add(item, "kind", ucv_string_new("upvalue")); + + if (upslot < frame->closure->function->nupvals) { + uc_upvalref_t *ref = frame->closure->upvals[upslot]; + + if (ref) { + if (ref->closed) + vval = ref->value; + else if (ref->slot < vm->stack.count) + vval = vm->stack.entries[ref->slot]; + } + } + } + + if (vval) { + uc_stringbuf_t vb = { 0 }; + + ucv_to_stringbuf(vm, &vb, vval, false); + ucv_object_add(item, "value_repr", ucv_string_new_length(vb.buf, vb.bpos)); + free(vb.buf); + } + else { + ucv_object_add(item, "value_repr", ucv_string_new("")); + } + + ucv_put(vname); + ucv_array_push(arr, item); + } + + return arr; +} + +/* Resolve a breakpoint location specification of the form + * "path[:line[:offset]]", a bare function name, or a ucode expression that + * evaluates to a function, and install a breakpoint of the given kind. + * + * `frame` may be NULL when there is no active script call frame yet, e.g. + * when installing a breakpoint before the program has started running (the + * `-x `/`-X ` command line options) - in that case, `program` + * must be given explicitly to resolve bare function names against; a `:line` + * spec cannot default its path from a current file and arbitrary expressions + * cannot be evaluated, so both are reported as unsupported instead. + * + * Returns the installed breakpoint id, or 0 on failure. On failure, `*errmsg` + * is set to a newly allocated diagnostic string the caller must free(), or to + * NULL if the caller should fall back to a generic message. */ +static bool eval_expr(uc_vm_t *vm, uc_callframe_t *frame, char *expr, + uc_value_t **res, char **errmsg); + +static size_t +resolve_breakpoint(uc_vm_t *vm, uc_callframe_t *frame, uc_program_t *program, + char *spec, debug_breakpoint_kind_t kind, char **errmsg) +{ + size_t id = 0; + + *errmsg = NULL; + + if (spec == NULL || *spec == '\0') { + xasprintf(errmsg, "Usage: path[:line[:offset]] | expr"); + + return 0; + } + + /* path spec */ + if ((strchr(spec, '/') || strchr(spec, ':') || + (*spec >= '0' && *spec <= '9')) && *spec != '(') { + + char *path, *line, *byte; + + if (*spec == ':' || (*spec >= '0' && *spec <= '9')) { + if (frame == NULL) { + xasprintf(errmsg, + "No active source file to default path from"); + + return 0; + } + + path = uc_program_function_source(frame->closure->function)->filename; + line = strtok(spec, ": \t"); + byte = strtok(NULL, ": \t"); + } + else { + path = strtok(spec, ": \t"); + line = strtok(NULL, ": \t"); + byte = strtok(NULL, ": \t"); + } + + if (!path && !line && !byte) { + xasprintf(errmsg, "Usage: path[:line[:offset]]"); + + return 0; + } + + id = add_breakpoint(vm, path, + line ? strtoul(line, NULL, 10) : 0, + byte ? strtoul(byte, NULL, 10) : 0, + kind); + } + + /* expression spec or function name */ + else { + uc_program_t *prog = frame ? frame->closure->function->program : program; + uc_value_t *val = NULL; + + /* Before evaluating as code, try looking up function name directly. */ + if (prog != NULL) { + uc_program_function_foreach(prog, fn) { + if (!strcmp(fn->name, spec)) { + id = patch_breakpoint(vm, fn, 0, kind, 1); + break; + } + } + } + + if (id == 0 && frame != NULL) { + char *errmsg2 = NULL; + + if (eval_expr(vm, frame, spec, &val, &errmsg2)) { + if (ucv_type(val) == UC_CLOSURE) { + id = patch_breakpoint(vm, + ((uc_closure_t *)val)->function, 0, kind, 1); + } + else { + char *s = ucv_to_string(vm, val); + int len = strlen(s); + + xasprintf(errmsg, "Value `%s` (%.*s%s) is not a function", + spec, + len > 32 ? 31 : len, + s, + len > 32 ? "…" : ""); + + free(s); + } + + ucv_put(val); + } + + free(errmsg2); + } + else if (id == 0 && frame == NULL) { + xasprintf(errmsg, + "No function named `%s` found " + "(expressions require an active frame)", spec); + } + } + + return id; +} + +static void +send_error(int fd, uc_vm_t *vm, const char *msg) +{ + uc_value_t *obj = ucv_object_new(vm); + + ucv_object_add(obj, "message", ucv_string_new(msg)); + debug_proto_write(fd, vm, "ERROR", obj); + ucv_put(obj); +} + +/* eval_expr() below runs the compiled expression through the exact same + * instruction dispatch loop as normal script code, with its callframes/ + * stack swapped out for a fresh, empty set (see uc_vm_call() there) - so + * from the dispatch loop's point of view, an exception raised inside it + * looks exactly like *the whole program* running out of callframes to + * unwind to, which is precisely the condition the debugger's dedicated + * "pause on uncaught exception" system breakpoint (BK_UNCAUGHT, see + * install_debug_system_breakpoints()) exists to catch. Left armed, + * a throwing PRINT/EVAL expression would pause into a confusing nested + * debug session (with a fake "[eval expression]" frame) instead of just + * being reported back as part of that command's own reply, the way + * eval_expr()'s caller (and its own EXCEPTION_NONE check just below) + * already expects. + * + * eval_sandbox_enter()/_leave() bracket the call to temporarily disarm + * that breakpoint - and, defensively, BK_CATCH's currently-armed + * catchpoint too, even though it targets a real instruction address + * within the *original* paused frame's function and so could only ever + * spuriously match here by an astronomically unlikely pointer collision + * with the expression's own freshly compiled chunk - plus BK_INTERRUPT, + * which *can* legitimately be armed here: an async "pause now" request + * (see debug_break_signal_handler()) fires on the very next instruction + * dispatched, whichever that happens to be, so it's just as capable of + * firing mid-eval as BK_UNCAUGHT is. Restored (not consumed) on leave, + * so a request that arrived during eval still fires on the first real + * instruction afterwards instead of being silently dropped. Sandboxing + * this way + * only touches which breakpoints can fire; it does not change what the + * expression itself is allowed to do (see the PRINT/EVAL help text on + * that - this is not a security boundary, just about not derailing the + * command's own request/response shape). + * + * Disarming means pointing `bk.ip` at eval_sandbox_disabled_marker's + * address, *not* NULL: uc_vm_decode_insn()'s generic per-instruction + * breakpoint check (vm.c) treats a NULL ip as "fire on every single + * instruction" (that's how BK_STEP free-runs until it decides to stop), + * the opposite of disabled - so a real, otherwise-unused address is + * needed as the inert value instead, the same trick + * UC_BREAKPOINT_UNCAUGHT_EXCEPTION itself uses to guarantee it never + * collides with an actual bytecode address. */ +static uint8_t eval_sandbox_disabled_marker; + +typedef struct { + uint8_t *uncaught_ip; + uint8_t *catch_ip; + uint8_t *interrupt_ip; +} eval_sandbox_t; + +static eval_sandbox_t +eval_sandbox_enter(uc_vm_t *vm) +{ + eval_sandbox_t saved = { 0 }; + + for (size_t i = 0; i < vm->breakpoints.count; i++) { + debug_breakpoint_t *dbk = (debug_breakpoint_t *)vm->breakpoints.entries[i]; + + if (!dbk) + continue; + + if (dbk->kind == BK_UNCAUGHT) { + saved.uncaught_ip = dbk->bk.ip; + dbk->bk.ip = &eval_sandbox_disabled_marker; + } + else if (dbk->kind == BK_CATCH) { + saved.catch_ip = dbk->bk.ip; + dbk->bk.ip = &eval_sandbox_disabled_marker; + } + else if (dbk->kind == BK_INTERRUPT) { + saved.interrupt_ip = dbk->bk.ip; + dbk->bk.ip = &eval_sandbox_disabled_marker; + } + } + + return saved; +} + +static void +eval_sandbox_leave(uc_vm_t *vm, eval_sandbox_t saved) +{ + for (size_t i = 0; i < vm->breakpoints.count; i++) { + debug_breakpoint_t *dbk = (debug_breakpoint_t *)vm->breakpoints.entries[i]; + + if (!dbk) + continue; + + if (dbk->kind == BK_UNCAUGHT) + dbk->bk.ip = saved.uncaught_ip; + else if (dbk->kind == BK_INTERRUPT) { + /* Unlike BK_UNCAUGHT/BK_CATCH, BK_INTERRUPT can legitimately + * change *while* sandboxed: the async signal handler writes + * NULL to it directly, with no notion of eval_expr() being + * mid-call. Only restore the pre-sandbox value if nothing + * did that - otherwise keep the freshly armed request so it + * still fires on the first real instruction after this + * returns, instead of eval_expr() silently discarding it. */ + if (dbk->bk.ip == &eval_sandbox_disabled_marker) + dbk->bk.ip = saved.interrupt_ip; + } + else if (dbk->kind == BK_CATCH) + dbk->bk.ip = saved.catch_ip; + } +} + +static bool +eval_expr(uc_vm_t *vm, uc_callframe_t *frame, char *expr, uc_value_t **res, + char **errmsg) +{ + uc_chunk_t *caller_chunk = &frame->closure->function->chunk; + uc_variables_t *decls = &caller_chunk->debuginfo.variables; + uc_value_list_t *names = &caller_chunk->debuginfo.varnames; + size_t pos = frame->ip - caller_chunk->entries; + + *errmsg = NULL; + + uc_source_t *source = + uc_source_new_buffer("[eval expression]", xstrdup(expr), strlen(expr)); + + uc_parse_config_t conf = { .raw_mode = true }; + char *err = NULL; + uc_program_t *prog = uc_compile(&conf, source, &err); + + uc_source_put(source); + + if (!prog) { + *errmsg = err; + *res = NULL; + + return false; + } + + uc_value_t *exprfn = ucv_closure_new(vm, uc_program_entry(prog), false); + + /* No restriction on the compiled shape here: raw_mode compiles `expr` + * as an ordinary sequence of ucode statements, so a bare literal + * ("1+2", "[1,2,3]", "\"hi\"") is just as valid as an identifier-rooted + * one ("varname", "myobj.prop") - either way, calling the compiled + * entry below always leaves *some* value on the stack to report back + * (the closing statement's value, or null for a plain statement with + * none). */ + uc_value_t *scope = ucv_object_new(NULL); + + /* Pre-populate `scope` with *every* local/upvalue declared in the + * paused frame's current scope - not just ones this expression + * happens to read - so a bare assignment like "x = 1" resolves + * directly against `scope` too, not only a read like "x" or "x + 1". + * The compiler emits a plain assignment as a bare I_SVAR with no + * preceding I_LVAR at all (there's nothing to read first), and + * I_SVAR's undeclared-variable fallback (uc_vm_insn_store_var() in + * vm.c) only walks *past* `scope` onto the real enclosing scope chain + * - in the worst case all the way to the real vm->globals, silently + * creating an unwanted genuine global - when `scope` doesn't already + * have the name as an *own* property; whether the expression read it + * first is irrelevant to that check. Earlier (i.e. more specific, in + * the case of shadowing) declarations win: stop at the first match + * per name rather than letting a later, less-specific entry overwrite + * it, matching normal scoping. */ + 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_constval(names, decls->entries[i].nameidx); + bool already; + + if (!vname) + continue; + + ucv_object_get(scope, ucv_string_get(vname), &already); + + if (already) { + ucv_put(vname); + continue; + } + + size_t slot = decls->entries[i].slot; + uc_value_t *varval = NULL; + + /* 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 && ref->closed) + varval = ucv_get(ref->value); + else if (ref && ref->slot < vm->stack.count) + varval = ucv_get(vm->stack.entries[ref->slot]); + } + } + + ucv_object_add(scope, ucv_string_get(vname), varval); + ucv_put(vname); + } + + 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); + + /* Save VM callframes and stack */ + uc_upvalref_t *upvals = vm->open_upvals; + uc_callframes_t frames = vm->callframes; + uc_stack_t stack = vm->stack; + + vm->open_upvals = NULL; + + vm->callframes.count = 0; + vm->callframes.entries = NULL; + + vm->stack.count = 0; + vm->stack.entries = NULL; + + uc_vm_stack_push(vm, ucv_get(frame->ctx)); + uc_vm_stack_push(vm, ucv_get(exprfn)); + + bool rv; + eval_sandbox_t sandbox = eval_sandbox_enter(vm); + uc_exception_type_t ex = uc_vm_call(vm, true, 0); + + eval_sandbox_leave(vm, sandbox); + + if (ex == EXCEPTION_NONE) { + *res = uc_vm_stack_pop(vm); + rv = true; + } + else { + xasprintf(errmsg, "Exception: %s", vm->exception.message); + vm->exception.type = EXCEPTION_NONE; + *res = NULL; + rv = false; + } + + uc_vector_clear(&vm->callframes); + uc_vector_clear(&vm->stack); + + /* Restore VM callframes and stack */ + vm->open_upvals = upvals; + vm->callframes = frames; + vm->stack = stack; + + /* `scope` only ever held independent *copies* of the locals/upvalues + * collected above (global references need no such handling: their + * value already lives in prev_scope itself, scope's prototype, which + * assignment inside the expression reaches directly) - so "x = 1" or + * "x.y = 1" mutated the copy, not the paused frame's real stack slot/ + * upvalue, on its own. Write any of them back now that the real stack + * is back in place, the same way I_SLOC/I_SUPV do (see + * uc_vm_insn_store_local()/_store_upval() in vm.c). Unconditional, + * regardless of `rv`: a later statement throwing doesn't undo an + * earlier one's already-applied assignment in ordinary script + * execution either, so eval shouldn't behave differently just because + * it happens to run in a temporary scope. Must run before + * uc_vm_scope_set() below, which drops the last reference to `scope`. */ + 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_constval(names, decls->entries[i].nameidx); + bool exists = false; + uc_value_t *newval = vname + ? ucv_object_get(scope, ucv_string_get(vname), &exists) : NULL; + + ucv_put(vname); + + if (!exists) + continue; + + size_t slot = decls->entries[i].slot; + + /* is local variable */ + if (slot < (size_t)-1 / 2) { + slot += frame->stackframe; + + if (slot < vm->stack.count) { + ucv_put(vm->stack.entries[slot]); + vm->stack.entries[slot] = ucv_get(newval); + } + } + + /* is upvalue */ + else { + slot -= ((size_t)-1 / 2); + + if (slot < frame->closure->function->nupvals) { + uc_upvalref_t *ref = frame->closure->upvals[slot]; + + if (ref) { + if (ref->closed) { + ucv_put(ref->value); + ref->value = ucv_get(newval); + } + else if (ref->slot < vm->stack.count) { + ucv_put(vm->stack.entries[ref->slot]); + vm->stack.entries[ref->slot] = ucv_get(newval); + } + } + } + } + } + + uc_vm_scope_set(vm, prev_scope); + uc_program_put(prog); + ucv_put(exprfn); + + return rv; +} + +/* Arm/refresh the BK_CATCH breakpoint for the innermost exception handler + * range (if any) covering `ip` within `fn`, so a subsequent exception raised + * there is intercepted by bk_handle_catch() before normal unwinding. */ +static void +update_catchpoint(uc_vm_t *vm, uc_function_t *fn, uint8_t *ip) +{ + uc_ehranges_t *eh = &fn->chunk.ehranges; + size_t off = ip - fn->chunk.entries; + + for (size_t i = 0; i < eh->count; i++) { + if (off >= eh->entries[i].from && off < eh->entries[i].to) { + debug_breakpoint_t *dbk = get_breakpoint(vm, BK_CATCH); + + /* Just (re)arm the real bytecode breakpoint so the VM's own + * dispatch loop fires it when execution actually reaches the + * catch handler - never invoke the handler synchronously from + * here via update_breakpoint()'s "already armed to this ip -> + * fire now" shortcut. This runs on every pause, and the target + * is often unchanged across pauses (e.g. a whole function body + * wrapped in one try/catch), so that shortcut would otherwise + * trigger bk_handle_catch() -> bk_enter_session() recursively + * without the VM ever advancing, overflowing the stack. */ + dbk->bk.cb = bk_handle_catch; + dbk->bk.ip = fn->chunk.entries + eh->entries[i].target; + dbk->fn = fn; + dbk->depth = 0; + + break; + } + } +} + +/* Build the PAUSED event payload: resolve the current source location (and, + * as a side effect, finish arming the breakpoint's fn/ip and the exception + * catchpoint - see the original print_location() this replaces), then emit + * {reason, file, line, col, function, breakpoint_id} with no rendering. */ +static const char * +paused_reason_name(debug_breakpoint_kind_t kind) +{ + switch (kind) { + case BK_ONCE: return "entry"; + case BK_USER: return "breakpoint"; + case BK_STEP: return "step"; + case BK_CATCH: return "exception"; + case BK_UNCAUGHT: return "uncaught"; + case BK_INTERRUPT: return "interrupt"; + default: return "unknown"; + } +} + +static uc_value_t * +build_paused_payload(uc_vm_t *vm, debug_breakpoint_t *dbk) +{ + uc_callframe_t *topframe = NULL, *funframe = NULL; + size_t depth = dbk->depth; + uc_value_t *obj = ucv_object_new(vm); + + 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]; + + if (dbk->fn == NULL && dbk->kind != BK_UNCAUGHT) { + dbk->fn = funframe->closure->function; + dbk->bk.ip = funframe->ip; + } + + update_catchpoint(vm, funframe->closure->function, funframe->ip); + break; + } + } + + ucv_object_add(obj, "reason", ucv_string_new(paused_reason_name(dbk->kind))); + + /* Full call chain, outermost first, for the client's header bar (see + * the original format_context_breadcrumb() this replaces) - skips the + * SIGINT handler's own native frame, which would otherwise show up as + * a spurious innermost entry whenever paused via Ctrl-C. */ + { + uc_value_t *breadcrumb = ucv_array_new(vm); + + for (size_t i = 0; i < vm->callframes.count; i++) { + uc_callframe_t *frame = &vm->callframes.entries[i]; + uc_stringbuf_t namebuf = { 0 }; + + if (frame->cfunction != NULL && + frame->cfunction->cfn == uc_debug_sigint_handler) + continue; + + printbuf_append_funcname(&namebuf, vm, + frame->closure ? &frame->closure->header : &frame->cfunction->header, + SIZE_MAX); + + ucv_array_push(breadcrumb, ucv_string_new_length(namebuf.buf, namebuf.bpos)); + free(namebuf.buf); + } + + ucv_object_add(obj, "breadcrumb", breadcrumb); + } + + if (funframe) { + uc_function_t *function = funframe->closure->function; + uc_source_t *source = uc_program_function_source(function); + insn_span_t stmt; + + if (find_statement_boundaries(function, funframe->ip, depth, &stmt)) { + uc_stringbuf_t pathbuf = { 0 }; + size_t byte = stmt.pos_start; + size_t line = uc_source_get_line(source, &byte); + + printbuf_append_srcpath(&pathbuf, source, SIZE_MAX); + + ucv_object_add(obj, "file", ucv_string_new_length(pathbuf.buf, pathbuf.bpos)); + ucv_object_add(obj, "line", ucv_uint64_new(line)); + ucv_object_add(obj, "col", ucv_uint64_new(byte)); + + free(pathbuf.buf); + } + + uc_stringbuf_t fnbuf = { 0 }; + printbuf_append_funcname(&fnbuf, vm, &funframe->closure->header, SIZE_MAX); + ucv_object_add(obj, "function", ucv_string_new_length(fnbuf.buf, fnbuf.bpos)); + free(fnbuf.buf); + } + else if (topframe && topframe->cfunction) { + ucv_object_add(obj, "function", ucv_string_new( + topframe->cfunction->name[0] + ? topframe->cfunction->name : "[native function]")); + } + + if ((dbk->kind == BK_CATCH || dbk->kind == BK_UNCAUGHT) && + vm->exception.type != EXCEPTION_NONE) { + ucv_object_add(obj, "exception_type", + ucv_uint64_new(vm->exception.type)); + ucv_object_add(obj, "exception_message", + ucv_string_new(vm->exception.message)); + } + + if (dbk->kind == BK_USER) { + size_t n = 0; + + for (size_t i = 0; i < vm->breakpoints.count; i++) { + debug_breakpoint_t *p = (debug_breakpoint_t *)vm->breakpoints.entries[i]; + + if (p == NULL || p->kind != BK_USER) + continue; + + n++; + + if (p == dbk) { + ucv_object_add(obj, "breakpoint_id", ucv_uint64_new(n)); + break; + } + } + } + + return obj; +} + +static void +proto_cmd_help(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + static const struct { + const char *verb; + const char *help; + } help_table[] = { + { "BREAK", + "Set a breakpoint. Payload: {\"spec\":\"path[:line[:offset]]\"|\"expr\"}. " + "Response: BREAKPOINT_ADDED {\"id\"} or ERROR." }, + { "DELETE", + "Delete a breakpoint. Payload: {\"id\":N} or omitted for the current one." }, + { "LIST_BREAKPOINTS", + "List all currently set breakpoints. Response: BREAKPOINTS {\"items\"}." }, + { "NEXT", + "Execute the next statement and stop again." }, + { "STEP", + "Execute the next statement, stepping into calls." }, + { "CONTINUE", + "Continue execution until the next breakpoint or end of program." }, + { "RETURN", + "Run until the current function returns." }, + { "BACKTRACE", + "Print a trace of the current callstack. Payload: {\"full\":bool}." }, + { "VARIABLES", + "List local variables for the current context." }, + { "SOURCES", + "List loaded source buffers." }, + { "PRINT", + "Evaluate an expression and report its result. Payload: " + "{\"expr\":\"...\"}. Response: VALUE {\"repr\"} or ERROR." }, + { "EVAL", + "Like PRINT, but discard the expression's result instead of " + "reporting it back - for expressions run for their side effect " + "(assignment, delete, ...). Payload: {\"expr\":\"...\"}. " + "Response: OK or ERROR." }, + { "LINES", + "Resolve a source range. Payload: {\"spec\",\"before\",\"after\"}." }, + { "THROW", + "Raise an exception. Payload: {\"type\",\"message\"}." }, + { "DISASSEMBLE", + "Disassemble a function or statement. Payload: {\"spec\"}." }, + { "SOURCE", + "Fetch raw source text for a file. Payload: {\"file\"}." }, + { "QUIT", + "Terminate the debugged program." }, + }; + + uc_value_t *cmdv = ucv_object_get(payload, "command", NULL); + const char *filter = (ucv_type(cmdv) == UC_STRING) ? ucv_string_get(cmdv) : NULL; + uc_value_t *items = ucv_array_new(vm); + + for (size_t i = 0; i < ARRAY_SIZE(help_table); i++) { + if (filter && !str_startswith(help_table[i].verb, filter)) + continue; + + uc_value_t *item = ucv_object_new(vm); + + ucv_object_add(item, "verb", ucv_string_new(help_table[i].verb)); + ucv_object_add(item, "help", ucv_string_new(help_table[i].help)); + ucv_array_push(items, item); + } + + uc_value_t *obj = ucv_object_new(vm); + ucv_object_add(obj, "commands", items); + debug_proto_write(fd, vm, "HELP", obj); + ucv_put(obj); +} + +static void +proto_cmd_break(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + uc_value_t *specv = ucv_object_get(payload, "spec", NULL); + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + char *errmsg = NULL; + char *spec; + size_t id; + + if (ucv_type(specv) != UC_STRING) { + send_error(fd, vm, "Usage: BREAK {\"spec\":\"path[:line[:offset]]\"|\"expr\"}"); + return; + } + + spec = xstrdup(ucv_string_get(specv)); + + id = resolve_breakpoint(vm, frame, + frame ? frame->closure->function->program : NULL, + spec, BK_USER, &errmsg); + + free(spec); + + if (id) { + uc_value_t *obj = ucv_object_new(vm); + + ucv_object_add(obj, "id", ucv_uint64_new(id)); + debug_proto_write(fd, vm, "BREAKPOINT_ADDED", obj); + ucv_put(obj); + } + else { + send_error(fd, vm, errmsg ? errmsg : "Unable to resolve source location"); + } + + free(errmsg); +} + +static void +proto_cmd_delete(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + uc_breakpoints_t *bks = &vm->breakpoints; + uc_value_t *idv = ucv_object_get(payload, "id", NULL); + + if (idv) { + size_t want, n = 0; + + if (ucv_type(idv) != UC_INTEGER && ucv_type(idv) != UC_DOUBLE) { + send_error(fd, vm, "Usage: DELETE {\"id\":N}"); + return; + } + + want = (size_t)ucv_int64_get(idv); + + for (size_t i = 0; i < bks->count; i++) { + debug_breakpoint_t *target = (debug_breakpoint_t *)bks->entries[i]; + + if (target == NULL || target->kind != BK_USER) + continue; + + if (++n == want) { + delete_breakpoint(vm, target, dbk); + debug_proto_write(fd, vm, "OK", NULL); + return; + } + } + + char msg[64]; + snprintf(msg, sizeof(msg), "No breakpoint #%zu set", want); + send_error(fd, vm, msg); + } + else if (dbk->kind == BK_USER) { + delete_breakpoint(vm, dbk, dbk); + debug_proto_write(fd, vm, "OK", NULL); + } + else { + send_error(fd, vm, "Automatic breakpoint cannot be deleted"); + } +} + +static void +proto_cmd_list(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + uc_breakpoints_t *bks = &vm->breakpoints; + uc_value_t *items = ucv_array_new(vm); + size_t n = 0; + + const char *kinds[] = { + [BK_ONCE] = "once", + [BK_USER] = "user", + [BK_STEP] = "step", + [BK_CATCH] = "catch", + [BK_UNCAUGHT] = "uncaught", + }; + + for (size_t i = 0; i < ARRAY_SIZE(kinds); i++) { + for (size_t j = 0; j < bks->count; j++) { + debug_breakpoint_t *p = (debug_breakpoint_t *)bks->entries[j]; + + if (p == NULL || p->kind != i) + continue; + + uc_value_t *item = ucv_object_new(vm); + + ucv_object_add(item, "kind", ucv_string_new(kinds[p->kind])); + + if (p->kind == BK_USER) + ucv_object_add(item, "id", ucv_uint64_new(++n)); + + if (p->fn) { + 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); + uc_stringbuf_t pathbuf = { 0 }, fnbuf = { 0 }; + uc_closure_t cl = { + .header = { .type = UC_CLOSURE }, + .function = p->fn + }; + + printbuf_append_srcpath(&pathbuf, source, SIZE_MAX); + printbuf_append_function(&fnbuf, vm, &cl.header, NULL, SIZE_MAX); + + ucv_object_add(item, "file", ucv_string_new_length(pathbuf.buf, pathbuf.bpos)); + ucv_object_add(item, "line", ucv_uint64_new(line)); + ucv_object_add(item, "col", ucv_uint64_new(byte > 1 ? byte : 1)); + ucv_object_add(item, "function", ucv_string_new_length(fnbuf.buf, fnbuf.bpos)); + + free(pathbuf.buf); + free(fnbuf.buf); + } + + ucv_array_push(items, item); + } + } + + uc_value_t *obj = ucv_object_new(vm); + ucv_object_add(obj, "items", items); + debug_proto_write(fd, vm, "BREAKPOINTS", obj); + ucv_put(obj); +} + +static void +cmd_step_common(uc_vm_t *vm, debug_breakpoint_t *dbk, bool single, int fd, bool *proceed) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_function_t *fn; + size_t depth; + uint8_t *nextinsn; + + if (!frame) { + *proceed = false; + return; + } + + fn = frame->closure->function; + depth = dbk->depth; + nextinsn = next_step(vm, &fn, frame->ip, single, &depth); + + /* Returning from the outermost frame - nothing further to step to and + * the program is about to terminate. Stay paused instead of resuming + * unattended (see STEP_STAY_PAUSED comment). */ + if (nextinsn == STEP_STAY_PAUSED(vm)) { + send_error(fd, vm, "No next instruction - program will terminate on 'continue'"); + *proceed = true; + return; + } + + /* no next instruction, run until completion */ + if (!nextinsn) { + *proceed = false; + return; + } + + update_breakpoint(vm, BK_STEP, bk_enter_session, nextinsn, fn, depth); + + *proceed = false; +} + +static void +proto_cmd_next(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + cmd_step_common(vm, dbk, false, fd, proceed); +} + +static void +proto_cmd_step(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + cmd_step_common(vm, dbk, true, fd, proceed); +} + +static void +proto_cmd_continue(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + *proceed = false; +} + +static void +proto_cmd_return(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 1); + + if (frame) { + update_breakpoint(vm, BK_STEP, bk_enter_session, frame->ip, + frame->closure->function, 0); /* XXX: fixup depth? */ + } + + *proceed = false; +} + +static void +proto_cmd_backtrace(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + bool verbose = ucv_is_truish(ucv_object_get(payload, "full", NULL)); + uc_value_t *frames = ucv_array_new(vm); + + for (size_t i = vm->callframes.count; i > 0; i--) { + uc_callframe_t *frame = &vm->callframes.entries[i - 1]; + uc_value_t *item; + + if (frame->closure) { + uc_function_t *function = frame->closure->function; + uc_source_t *source = uc_program_function_source(function); + size_t insn = frame->ip - function->chunk.entries; + size_t byte = insn; + size_t line = insnoff_to_srcpos(function, &byte); + uc_stringbuf_t pathbuf = { 0 }, fnbuf = { 0 }; + + item = ucv_object_new(vm); + + printbuf_append_srcpath(&pathbuf, source, SIZE_MAX); + printbuf_append_funcname(&fnbuf, vm, &frame->closure->header, SIZE_MAX); + + ucv_object_add(item, "kind", ucv_string_new("script")); + ucv_object_add(item, "index", ucv_uint64_new(i)); + ucv_object_add(item, "file", ucv_string_new_length(pathbuf.buf, pathbuf.bpos)); + ucv_object_add(item, "line", ucv_uint64_new(line)); + ucv_object_add(item, "col", ucv_uint64_new(byte)); + ucv_object_add(item, "insn", ucv_uint64_new(insn)); + ucv_object_add(item, "function", ucv_string_new_length(fnbuf.buf, fnbuf.bpos)); + + free(pathbuf.buf); + free(fnbuf.buf); + + if (verbose) + ucv_object_add(item, "variables", build_variables_json(vm, frame)); + } + else if (frame->cfunction) { + uc_cfunction_t *cfn = frame->cfunction; + uc_stringbuf_t fnbuf = { 0 }; + Dl_info dli; + + item = ucv_object_new(vm); + + printbuf_append_funcname(&fnbuf, vm, &cfn->header, SIZE_MAX); + + ucv_object_add(item, "kind", ucv_string_new("native")); + ucv_object_add(item, "index", ucv_uint64_new(i)); + + if (dladdr(cfn->cfn, &dli) != 0 && dli.dli_fname != NULL) + ucv_object_add(item, "module", ucv_string_new(dli.dli_fname)); + + ucv_object_add(item, "function", ucv_string_new_length(fnbuf.buf, fnbuf.bpos)); + + free(fnbuf.buf); + } + else { + continue; + } + + ucv_array_push(frames, item); + } + + uc_value_t *obj = ucv_object_new(vm); + ucv_object_add(obj, "frames", frames); + debug_proto_write(fd, vm, "BACKTRACE", obj); + ucv_put(obj); +} + +static void +proto_cmd_variables(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_value_t *obj; + + if (!frame) { + send_error(fd, vm, "No local variables in current context"); + return; + } + + obj = ucv_object_new(vm); + ucv_object_add(obj, "vars", build_variables_json(vm, frame)); + debug_proto_write(fd, vm, "VARIABLES", obj); + ucv_put(obj); +} + +static void +proto_cmd_sources(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + struct lh_table *sources = lh_kptr_table_new(16, NULL); + uc_value_t *items = ucv_array_new(vm); + struct lh_entry *e; + uc_weakref_t *ref; + size_t i = 0; + + 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 j = 0; j < uc->function->program->sources.count; j++) { + uc_source_t *source = uc->function->program->sources.entries[j]; + 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); + } + } + + lh_foreach(sources, e) { + uc_source_t *source = lh_entry_k(e); + uc_value_t *item = ucv_object_new(vm); + + ucv_object_add(item, "index", ucv_uint64_new(i++)); + ucv_object_add(item, "file", ucv_string_new(source->filename)); + ucv_array_push(items, item); + } + + lh_table_free(sources); + + uc_value_t *obj = ucv_object_new(vm); + ucv_object_add(obj, "items", items); + debug_proto_write(fd, vm, "SOURCES", obj); + ucv_put(obj); +} + +static void +proto_cmd_print(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_value_t *exprv = ucv_object_get(payload, "expr", NULL); + uc_value_t *res = NULL; + char *errmsg = NULL; + + if (ucv_type(exprv) != UC_STRING) { + send_error(fd, vm, "Usage: PRINT {\"expr\":\"...\"}"); + return; + } + + if (eval_expr(vm, frame, ucv_string_get(exprv), &res, &errmsg)) { + uc_stringbuf_t vb = { 0 }; + uc_value_t *obj = ucv_object_new(vm); + + ucv_to_stringbuf_formatted(vm, &vb, res, 0, ' ', 2); + + ucv_object_add(obj, "repr", ucv_string_new_length(vb.buf, vb.bpos)); + debug_proto_write(fd, vm, "VALUE", obj); + + ucv_put(obj); + ucv_put(res); + free(vb.buf); + } + else { + send_error(fd, vm, errmsg ? errmsg : "Evaluation failed"); + } + + free(errmsg); +} + +/* Like PRINT, but for an expression run for its side effect (assignment, + * delete, a mutating call, ...) rather than its value - mirrors the ucode + * CLI's -e/-p distinction (uc_compile()'s two entry points in main.c). + * "set x.y 1" is just "eval x.y = 1" - ordinary assignment syntax handles + * plain variables, property paths and array indices alike, so there is no + * separate name-resolution/slot-writing logic here at all, unlike an + * earlier, since-removed dedicated SET command had. */ +static void +proto_cmd_eval(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_value_t *exprv = ucv_object_get(payload, "expr", NULL); + uc_value_t *res = NULL; + char *errmsg = NULL; + + if (ucv_type(exprv) != UC_STRING) { + send_error(fd, vm, "Usage: EVAL {\"expr\":\"...\"}"); + return; + } + + if (eval_expr(vm, frame, ucv_string_get(exprv), &res, &errmsg)) { + ucv_put(res); + debug_proto_write(fd, vm, "OK", NULL); + } + else { + send_error(fd, vm, errmsg ? errmsg : "Evaluation failed"); + } + + free(errmsg); +} + +static void +proto_cmd_lines(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_function_t *fn = frame ? frame->closure->function : NULL; + uc_value_t *specv = ucv_object_get(payload, "spec", NULL); + uc_value_t *beforev = ucv_object_get(payload, "before", NULL); + uc_value_t *afterv = ucv_object_get(payload, "after", NULL); + const char *spec = (ucv_type(specv) == UC_STRING) ? ucv_string_get(specv) : NULL; + size_t ctx_before = (ucv_type(beforev) == UC_INTEGER || ucv_type(beforev) == UC_DOUBLE) + ? (size_t)ucv_int64_get(beforev) : 2; + size_t ctx_after = (ucv_type(afterv) == UC_INTEGER || ucv_type(afterv) == UC_DOUBLE) + ? (size_t)ucv_int64_get(afterv) : 2; + insn_span_t stmt = { .pos_start = SIZE_MAX, .pos_end = SIZE_MAX, .pos_ip = SIZE_MAX }; + location_t loc; + size_t insn, from, to; + uc_value_t *obj; + + if (!fn) { + send_error(fd, vm, "No active source location"); + return; + } + + insn = frame->ip - fn->chunk.entries; + + loc = (location_t){ + .program = fn->program, + .source = uc_program_function_source(fn), + .function = fn, + .offset = uc_program_function_srcpos(fn, insn), + }; + + if (!spec) { + if (find_statement_boundaries(fn, frame->ip, 0, &stmt)) + loc.offset = stmt.pos_start; + + loc.column = loc.offset; + loc.line = uc_source_get_line(loc.source, &loc.column); + } + else if (spec[0] >= '0' && spec[0] <= '9') { + char *end; + unsigned long n = strtoul(spec, &end, 10); + + if (*end != '\0') { + send_error(fd, vm, "Invalid line number"); + return; + } + + loc.line = (n > 0) ? n : 1; + } + else if (strchr("+-#", spec[0]) != NULL && spec[1] >= '0' && spec[1] <= '9') { + char *end; + unsigned long n = strtoul(spec + 1, &end, 0); + + if (*end != '\0') { + send_error(fd, vm, "Invalid offset"); + return; + } + + if (spec[0] == '+' || spec[0] == '-') { + if (find_statement_boundaries(fn, frame->ip, 0, &stmt)) + loc.offset = stmt.pos_start; + + loc.column = loc.offset; + loc.line = uc_source_get_line(loc.source, &loc.column); + + if (spec[0] == '+') + loc.line += n; + else if (n < loc.line) + loc.line -= n; + else + loc.line = 1; + } + else { + loc.offset = uc_program_function_srcpos(fn, n); + loc.column = loc.offset; + loc.line = uc_source_get_line(loc.source, &loc.column); + stmt.pos_ip = loc.offset; + } + } + else { + bool found = false; + + if (spec[0] != '(') { + loc = (location_t){ .path = spec, .line = 1, .column = 1 }; + found = lookup_source(vm, &loc); + + if (!found) { + uc_program_function_foreach(fn->program, pfn) { + if (!strcmp(pfn->name, spec)) { + loc = (location_t){ .function = pfn }; + found = true; + break; + } + } + } + } + + if (!found) { + uc_value_t *val = NULL; + char *errmsg = NULL; + char *specdup = xstrdup(spec); + bool ok = eval_expr(vm, frame, specdup, &val, &errmsg); + + free(specdup); + + if (!ok) { + send_error(fd, vm, errmsg ? errmsg : "Evaluation failed"); + free(errmsg); + return; + } + + free(errmsg); + + if (ucv_type(val) != UC_CLOSURE) { + ucv_put(val); + send_error(fd, vm, "Value is not a function"); + return; + } + + loc = (location_t){ .function = ((uc_closure_t *)val)->function }; + ucv_put(val); + } + + if (loc.function) { + size_t beg = uc_program_function_srcpos(loc.function, 0); + size_t end2 = uc_program_function_srcpos(loc.function, SIZE_MAX); + + loc.program = loc.function->program; + loc.source = uc_program_function_source(loc.function); + loc.offset = loc.column = beg; + loc.line = uc_source_get_line(loc.source, &loc.column); + + ctx_before = 1; + ctx_after = uc_source_get_line(loc.source, &end2) + 2 - loc.line; + } + } + + if (!lookup_function(vm, &loc)) { + send_error(fd, vm, "Unable to resolve source code location"); + return; + } + + from = (loc.line > ctx_before) ? loc.line - ctx_before : 1; + to = loc.line + ctx_after; + + obj = ucv_object_new(vm); + + { + uc_stringbuf_t pathbuf = { 0 }; + + printbuf_append_srcpath(&pathbuf, loc.source, SIZE_MAX); + ucv_object_add(obj, "file", ucv_string_new_length(pathbuf.buf, pathbuf.bpos)); + free(pathbuf.buf); + } + + ucv_object_add(obj, "from", ucv_uint64_new(from)); + ucv_object_add(obj, "to", ucv_uint64_new(to)); + + if (loc.source == uc_program_function_source(fn) && stmt.pos_start != SIZE_MAX) { + size_t sline_byte = stmt.pos_start; + size_t sline = uc_source_get_line(loc.source, &sline_byte); + size_t eline_byte = stmt.pos_end; + size_t eline = uc_source_get_line(loc.source, &eline_byte); + uc_value_t *cursor = ucv_object_new(vm); + + ucv_object_add(cursor, "from_line", ucv_uint64_new(sline)); + ucv_object_add(cursor, "from_col", ucv_uint64_new(sline_byte)); + ucv_object_add(cursor, "to_line", ucv_uint64_new(eline)); + ucv_object_add(cursor, "to_col", ucv_uint64_new(eline_byte)); + ucv_object_add(obj, "cursor", cursor); + } + + debug_proto_write(fd, vm, "SOURCE_RANGE", obj); + ucv_put(obj); +} + +static void +proto_cmd_throw(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + uc_value_t *typev = ucv_object_get(payload, "type", NULL); + uc_value_t *msgv = ucv_object_get(payload, "message", NULL); + uc_exception_type_t et = EXCEPTION_USER; + + if (ucv_type(msgv) != UC_STRING) { + send_error(fd, vm, "Usage: THROW {\"message\":\"...\"}"); + return; + } + + if (ucv_type(typev) == UC_STRING) { + const char *t = ucv_string_get(typev); + + if (str_startswith("syntax", t)) et = EXCEPTION_SYNTAX; + else if (str_startswith("runtime", t)) et = EXCEPTION_RUNTIME; + else if (str_startswith("type", t)) et = EXCEPTION_TYPE; + else if (str_startswith("reference", t)) et = EXCEPTION_REFERENCE; + else if (str_startswith("user", t)) et = EXCEPTION_USER; + else if (str_startswith("exit", t)) et = EXCEPTION_EXIT; + else { + char msg[128]; + snprintf(msg, sizeof(msg), "Unrecognized exception type '%s'", t); + send_error(fd, vm, msg); + return; + } + } + + uc_vm_raise_exception(vm, et, "%s", ucv_string_get(msgv)); +} + +static const char *insn_names[__I_MAX] = { +#undef __insn +#define __insn(_name) [I_##_name] = #_name, + __insns +}; + +static void +proto_cmd_disasm(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_value_t *specv = ucv_object_get(payload, "spec", NULL); + const char *spec = (ucv_type(specv) == UC_STRING) ? ucv_string_get(specv) : NULL; + uc_function_t *target = NULL; + uc_program_t *prog = NULL; + size_t from = 0, to = 0; + uc_value_t *insns, *obj; + uint8_t *bytecode; + + if (!frame) { + send_error(fd, vm, "No active call frame"); + return; + } + + if (spec) { + if (*spec == '#') { + char *e; + + from = strtoul(spec + 1, &e, 10); + + if (*e == '-') { + to = strtoul(e + 1, &e, 10); + + if (*e != '\0' || to < from) { + send_error(fd, vm, "Invalid instruction range"); + return; + } + } + else if (*e == '+') { + to = from + strtoul(e + 1, &e, 10); + + if (*e != '\0') { + send_error(fd, vm, "Invalid instruction count"); + return; + } + } + else if (*e == '\0') { + to = from; + } + else { + send_error(fd, vm, "Invalid instruction offset"); + return; + } + + target = frame->closure->function; + + if (from >= target->chunk.count || to >= target->chunk.count) { + send_error(fd, vm, "Instruction offset out of range"); + return; + } + } + else if (*spec == '(') { + uc_parse_config_t conf = { .raw_mode = true }; + uc_source_t *source = uc_source_new_buffer("[disasm expression]", + xstrdup(spec), strlen(spec)); + char *err = NULL; + + prog = uc_compile(&conf, source, &err); + + uc_source_put(source); + + if (!prog) { + send_error(fd, vm, err ? err : "Invalid expression"); + free(err); + return; + } + + target = uc_program_entry(prog); + from = 0; + to = target->chunk.count - 1; + } + else { + char *dup = xstrdup(spec); + char *p = strchr(dup, '+'); + size_t limit = SIZE_MAX; + + if (p) { + char *e; + + limit = strtoul(p + 1, &e, 10); + + if (e == p + 1 || *e != '\0' || limit == 0) { + send_error(fd, vm, "Invalid instruction count"); + free(dup); + return; + } + + *p = 0; + } + + uc_program_function_foreach(frame->closure->function->program, fn) { + if (!strcmp(fn->name, dup)) { + target = fn; + from = 0; + to = (limit < target->chunk.count) ? limit : target->chunk.count - 1; + break; + } + } + + if (!target) { + send_error(fd, vm, "Unable to find function"); + free(dup); + return; + } + + free(dup); + } + } + else { + insn_span_t stmt; + + target = frame->closure->function; + + if (!find_statement_boundaries(target, frame->ip, 0, &stmt)) { + send_error(fd, vm, "Unable to determine current statement boundaries"); + return; + } + + from = stmt.ip_start - target->chunk.entries; + to = (stmt.ip_end - target->chunk.entries) - 1; + } + + bytecode = target->chunk.entries; + + for (size_t i = 0; i < target->chunk.count; ) { + size_t len = insn_length(bytecode + i, target->program); + + if (i + len > from) { + from = i; + break; + } + + i += len; + } + + insns = ucv_array_new(vm); + + for (size_t i = from; i <= to; ) { + union { uint8_t u8; uint16_t u16; uint32_t u32; int32_t s32; } arg = { 0 }; + size_t n = insn_length(bytecode + i, target->program); + uint8_t insn = bytecode[i]; + uc_value_t *item = ucv_object_new(vm); + uc_value_t *operand = NULL; + uc_value_t *rawbytes = ucv_array_new_length(vm, n); + + for (size_t j = 0; j < n; j++) + ucv_array_push(rawbytes, ucv_uint64_new(bytecode[i + j])); + + ucv_object_add(item, "offset", ucv_uint64_new(i)); + ucv_object_add(item, "mnemonic", ucv_string_new(insn_names[insn])); + ucv_object_add(item, "format", ucv_int64_new(uc_vm_insn_format[insn])); + ucv_object_add(item, "bytes", rawbytes); + + switch (uc_vm_insn_format[insn]) { + case 0: + break; + + case -4: + arg.s32 = insn_s32(bytecode + i + 1); + operand = ucv_int64_new(arg.s32); + break; + + case 1: + arg.u8 = bytecode[i + 1]; + operand = ucv_uint64_new(arg.u8); + break; + + case 2: + arg.u16 = insn_u16(bytecode + i + 1); + operand = ucv_uint64_new(arg.u16); + break; + + case 4: + arg.u32 = insn_u32(bytecode + i + 1); + operand = ucv_uint64_new(arg.u32); + + if (insn == I_LOAD) { + uc_value_t *cv = load_constval(&target->program->constants, arg.u32); + ucv_object_add(item, "constant", cv); + } + else if (insn == I_LLOC || insn == I_SLOC || insn == I_LUPV || insn == I_SUPV) { + bool upval = (insn == I_LUPV || insn == I_SUPV); + uc_value_t *vn = uc_chunk_debug_get_variable( + &target->chunk, i, arg.u32, upval); + + ucv_object_add(item, "variable_kind", ucv_string_new(upval ? "upval" : "local")); + ucv_object_add(item, "variable_name", + ucv_string_new(vn ? ucv_string_get(vn) : "(unknown)")); + } + else if (insn == I_LVAR || insn == I_SVAR) { + uc_value_t *vn = load_constval(&target->program->constants, arg.u32); + + ucv_object_add(item, "variable_kind", ucv_string_new("global")); + ucv_object_add(item, "variable_name", + ucv_string_new(vn ? ucv_string_get(vn) : "(unknown)")); + ucv_put(vn); + } + else if (insn == I_CLFN || insn == I_ARFN) { + ucv_object_add(item, "closure_index", ucv_uint64_new(arg.u32)); + ucv_object_add(item, "closure_kind", + ucv_string_new((insn == I_CLFN) ? "closure" : "arrow")); + } + else if (insn == I_CALL) { + /* See uc_vm_insn_call() in vm.c: top bit is the method-call + * flag (this-context passed as an implicit extra argument + * below the callee on the stack), low 16 bits are the + * argument count. */ + ucv_object_add(item, "call_mcall", + ucv_boolean_new((arg.u32 & 0x80000000) != 0)); + ucv_object_add(item, "call_nargs", + ucv_uint64_new(arg.u32 & 0xffff)); + } + + break; + + default: + break; + } + + if (operand) + ucv_object_add(item, "operand", operand); + + if (insn == I_CLFN || insn == I_ARFN) { + size_t id = 1, nupvals = 0; + uc_value_t *captures = ucv_array_new(vm); + + uc_program_function_foreach(target->program, fn) { + if (id++ == arg.u32) { + nupvals = fn->nupvals; + break; + } + } + + for (size_t j = 0; j < nupvals; j++) { + int32_t slot = insn_s32(bytecode + i + 5 + j * 4); + bool upval = (slot >= 0); + uc_value_t *vn = uc_chunk_debug_get_variable( + &target->chunk, i, (slot < 0) ? -(slot + 1) : slot, upval); + uc_value_t *cap = ucv_object_new(vm); + uc_value_t *capbytes = ucv_array_new_length(vm, 4); + + for (size_t k = 0; k < 4; k++) + ucv_array_push(capbytes, ucv_uint64_new(bytecode[i + 5 + j * 4 + k])); + + ucv_object_add(cap, "slot", ucv_int64_new(slot)); + ucv_object_add(cap, "kind", ucv_string_new(upval ? "upval" : "local")); + ucv_object_add(cap, "name", + ucv_string_new(vn ? ucv_string_get(vn) : "(unknown)")); + ucv_object_add(cap, "bytes", capbytes); + ucv_array_push(captures, cap); + } + + ucv_object_add(item, "captures", captures); + } + else if (insn == I_CALL) { + uc_value_t *unpacks = ucv_array_new(vm); + + for (size_t j = 0; j < ((arg.u32 >> 16) & 0x7fff); j++) { + uint16_t slot = insn_u16(bytecode + i + 5 + j * 2); + uc_value_t *u = ucv_object_new(vm); + uc_value_t *ubytes = ucv_array_new_length(vm, 2); + + for (size_t k = 0; k < 2; k++) + ucv_array_push(ubytes, ucv_uint64_new(bytecode[i + 5 + j * 2 + k])); + + ucv_object_add(u, "slot", ucv_uint64_new(slot)); + ucv_object_add(u, "stack_slot", ucv_int64_new(-(int64_t)(slot + 1))); + ucv_object_add(u, "bytes", ubytes); + ucv_array_push(unpacks, u); + } + + ucv_object_add(item, "unpacks", unpacks); + } + + ucv_array_push(insns, item); + i += n; + } + + if (prog) + uc_program_put(prog); + + obj = ucv_object_new(vm); + ucv_object_add(obj, "function", ucv_string_new(target->name)); + ucv_object_add(obj, "instructions", insns); + debug_proto_write(fd, vm, "DISASSEMBLY", obj); + ucv_put(obj); +} + +static void +proto_cmd_source(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + uc_value_t *filev = ucv_object_get(payload, "file", NULL); + location_t loc = { 0 }; + uc_value_t *obj; + + if (ucv_type(filev) != UC_STRING) { + send_error(fd, vm, "Usage: SOURCE {\"file\":\"...\"}"); + return; + } + + loc.path = ucv_string_get(filev); + loc.line = 1; + loc.column = 1; + + obj = ucv_object_new(vm); + ucv_object_add(obj, "file", ucv_get(filev)); + + if (!lookup_source(vm, &loc)) { + ucv_object_add(obj, "text", NULL); + ucv_object_add(obj, "error", ucv_string_new("source not available on server")); + } + else { + uc_stringbuf_t text = { 0 }; + char buf[4096]; + size_t n; + + fseeko(loc.source->fp, 0, SEEK_SET); + + while ((n = fread(buf, 1, sizeof(buf), loc.source->fp)) > 0) + printbuf_memappend_fast((&text), buf, n); + + ucv_object_add(obj, "text", ucv_string_new_length(text.buf, text.bpos)); + free(text.buf); + } + + debug_proto_write(fd, vm, "SOURCE", obj); + ucv_put(obj); +} + +static void +proto_cmd_quit(uc_vm_t *vm, debug_breakpoint_t *dbk, uc_value_t *payload, int fd, bool *proceed) +{ + vm->arg.s32 = -1; + uc_vm_raise_exception(vm, EXCEPTION_EXIT, "Terminated"); + *proceed = false; +} + +static const struct { + const char *verb; + void (*cb)(uc_vm_t *, debug_breakpoint_t *, uc_value_t *, int, bool *); +} proto_commands[] = { + { "BREAK", proto_cmd_break }, + { "DELETE", proto_cmd_delete }, + { "LIST_BREAKPOINTS", proto_cmd_list }, + { "NEXT", proto_cmd_next }, + { "STEP", proto_cmd_step }, + { "CONTINUE", proto_cmd_continue }, + { "RETURN", proto_cmd_return }, + { "BACKTRACE", proto_cmd_backtrace }, + { "VARIABLES", proto_cmd_variables }, + { "SOURCES", proto_cmd_sources }, + { "PRINT", proto_cmd_print }, + { "EVAL", proto_cmd_eval }, + { "LINES", proto_cmd_lines }, + { "THROW", proto_cmd_throw }, + { "DISASSEMBLE", proto_cmd_disasm }, + { "SOURCE", proto_cmd_source }, + { "HELP", proto_cmd_help }, + { "QUIT", proto_cmd_quit }, +}; + +/* Incremental read buffer for the current session connection - must persist + * across separate bk_enter_session() calls (one per breakpoint hit) for the + * same connection, exactly like the connection fd itself + * (debug_remote_{set,get}_active_fd()), since a single logical debug session + * spans many such calls (one per "next"/"step"/breakpoint hit). */ +static debug_proto_buf_t session_buf; + +static void +bk_enter_session(uc_vm_t *vm, uc_breakpoint_t *bk) +{ + debug_breakpoint_t *dbk = (debug_breakpoint_t *)bk; + uint8_t *entry_ip = bk->ip; + uc_value_t *paused; + int fd; + + /* If a remote client is already connected - either because this is a + * BK_STEP breakpoint hit during an ongoing "next"/"step" sequence, or a + * reentrant SIGUSR1 while already attached - reuse it as-is instead of + * tearing it down to accept a redundant second connection, which would + * just disconnect the live one mid-session. */ + if (debug_attach_mode && !debug_remote_has_active_connection()) { + int client_fd = debug_remote_handle_break(vm); + + if (client_fd < 0) + return; /* timeout or fatal error - resume unattended */ + + debug_remote_set_active_fd(client_fd); + debug_proto_buf_init(&session_buf); + + fprintf(stderr, "Connected to ucode debugger\n\n"); + } + + fd = debug_remote_get_active_fd(); + + /* No session fd available yet (e.g. local `-x` mode before its client + * has been spawned) - nothing to do. */ + if (fd < 0) + return; + + paused = build_paused_payload(vm, dbk); + debug_proto_write(fd, vm, "PAUSED", paused); + ucv_put(paused); + + for (;;) { + char *verb = NULL; + uc_value_t *payload = NULL; + int rv = debug_proto_read(fd, &session_buf, vm, &verb, &payload); + bool proceed = true; + bool handled = false; + + if (rv <= 0) { + bool exiting = (vm->exception.type == EXCEPTION_EXIT); + + free(verb); + ucv_put(payload); + + close(fd); + debug_remote_set_active_fd(-1); + + if (debug_attach_mode && !exiting) { + /* The client dropped the connection without an explicit + * QUIT - tear the dead connection down and go back to + * waiting for a fresh one, rather than silently resuming + * the paused script and losing the session for good. */ + bk_enter_session(vm, bk); + return; + } + + if (debug_attach_mode) + debug_remote_cleanup_attach_socket(); + + break; + } + + for (size_t i = 0; i < ARRAY_SIZE(proto_commands); i++) { + if (!strcmp(proto_commands[i].verb, verb)) { + proto_commands[i].cb(vm, dbk, payload, fd, &proceed); + handled = true; + break; + } + } + + if (!handled) { + char msg[128]; + + snprintf(msg, sizeof(msg), "Unrecognized command '%s'", verb); + send_error(fd, vm, msg); + } + + free(verb); + ucv_put(payload); + + if (!proceed) + break; + } + + /* If "DELETE" removed this very breakpoint during the session above, it + * only unlinked it and deferred the actual free() until now - see the + * `deleted` field comment. Do that first and skip the kind-based checks + * below entirely: dbk was already unlinked, so free_breakpoint() here + * just frees the struct without touching vm->breakpoints again. */ + if (dbk->deleted) { + free_breakpoint(vm, &dbk->bk); + } + /* BK_STEP is a single, reused breakpoint object (see get_breakpoint()): + * a "next"/"step" command handled above may have already re-armed it + * in place, via update_breakpoint(), to a new target instruction so a + * later hit can continue the stepping sequence - in that case dbk->bk.ip + * no longer matches the instruction we were entered for and freeing it + * here would silently cancel that re-arm before it ever fires, letting + * the script run to completion instead of stopping at the next step. + * Only free it when it's still pointing at the same place we started + * at, i.e. nothing re-armed it (e.g. plain "continue"). */ + else if (dbk->kind == BK_ONCE || (dbk->kind == BK_STEP && dbk->bk.ip == entry_ip)) { + free_breakpoint(vm, &dbk->bk); + } +} + +/* Run a full interactive debugger session over an already-connected remote + * client socket, reusing the exact same command set as the local session - + * see bk_enter_session() above. */ +void +debug_run_session(uc_vm_t *vm, int client_fd) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + debug_breakpoint_t dbk; + + if (!frame) { + close(client_fd); + return; + } + + debug_remote_set_active_fd(client_fd); + debug_proto_buf_init(&session_buf); + + dbk = (debug_breakpoint_t){ + .bk = { .ip = frame->ip }, + .fn = frame->closure->function, + .kind = BK_USER + }; + + bk_enter_session(vm, &dbk.bk); + + /* Unless "QUIT" was issued (which already raised EXCEPTION_EXIT), + * resume script execution; further breakpoints hit during this call + * reenter bk_enter_session() directly, still using the fd set up + * above. */ + if (vm->exception.type != EXCEPTION_EXIT) + uc_vm_resume(vm); + + debug_remote_set_active_fd(-1); + close(client_fd); + + /* No-op unless this session came from the SIGUSR1 attach socket. */ + debug_remote_cleanup_attach_socket(); +} + +static uc_value_t *uc_debug_sigint_handler(uc_vm_t *vm, size_t nargs); + +static uc_value_t * +uc_debug_sigusr1_attach_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_USER + }; + + bk_enter_session(vm, &dbk.bk); + + return NULL; +} + +static bool debug_attach_initialized = false; + +static uc_value_t * +uc_debug_attach(uc_vm_t *vm, size_t nargs) +{ + uc_cfn_ptr_t ucsignal = uc_stdlib_function("signal"); + uc_value_t *mainfn = uc_fn_arg(0); + + debug_attach_mode = true; + + if (!debug_attach_initialized) { + 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)); + + /* For attach mode, SIGUSR1 launches the debugger CLI directly */ + uc_vm_stack_push(vm, ucv_string_new("SIGUSR1")); + uc_vm_stack_push(vm, + ucv_cfunction_new("debug_sigusr1_attach_handler", uc_debug_sigusr1_attach_handler)); + ucv_put(ucsignal(vm, 2)); + ucv_put(uc_vm_stack_pop(vm)); + ucv_put(uc_vm_stack_pop(vm)); + + /* Attach mode never actually interacts over the target's own + * stdin/stdout - the debug session only ever runs over the client + * fd accepted once a remote debugger connects (see + * bk_enter_session()), so there is no local tty state to set up + * here at all. */ + + install_debug_system_breakpoints(vm); + + debug_attach_initialized = true; + } + + if (ucv_type(mainfn) == UC_CLOSURE) { + uc_function_t *fn = ((uc_closure_t *)mainfn)->function; + update_breakpoint(vm, BK_STEP, bk_enter_session, fn->chunk.entries, fn, 1); + } + + return ucv_boolean_new(true); +} + +static uc_value_t * +uc_debug_break(uc_vm_t *vm, size_t nargs) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + + if (!frame) + return ucv_boolean_new(false); + + debug_breakpoint_t dbk = { + .bk = { .ip = frame->ip }, + .fn = frame->closure->function, + .kind = BK_USER + }; + + bk_enter_session(vm, &dbk.bk); + + return ucv_boolean_new(true); +} + +/** + * Install a user breakpoint from a location specification, using the exact + * same grammar as the interactive `break` CLI command (`path[:line[:offset]]`, + * a bare function name, or a ucode expression evaluating to a function). + * + * Unlike the `break` CLI command, this may be called before the program has + * started running and thus without any active script call frame - e.g. by + * the `-x `/`-X ` command line options, which use this function + * to resolve their argument early, before `uc_vm_execute()` is even called. + * In that case, `mainfn` is used to resolve bare function names instead of + * the (nonexistent) current frame; a `:line` spec without an explicit path, + * or an arbitrary expression, cannot be resolved without a frame and are + * reported as an error. + * + * @function module:debug#breakpoint + * + * @param {string} spec + * The breakpoint location specification. + * + * @param {function} [mainfn] + * The program entry function, used to resolve bare function names when + * there is no active call frame yet. + * + * @returns {number|boolean} + * The installed breakpoint id, or `false` on failure. + */ +static uc_value_t * +uc_debug_breakpoint(uc_vm_t *vm, size_t nargs) +{ + uc_value_t *specarg = uc_fn_arg(0); + uc_value_t *mainfn = uc_fn_arg(1); + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_program_t *program = NULL; + char *spec, *errmsg = NULL; + size_t id; + + if (ucv_type(specarg) != UC_STRING) + return ucv_boolean_new(false); + + if (!frame && ucv_type(mainfn) == UC_CLOSURE) + program = ((uc_closure_t *)mainfn)->function->program; + + spec = xstrdup(ucv_string_get(specarg)); + id = resolve_breakpoint(vm, frame, program, spec, BK_USER, &errmsg); + free(spec); + + if (!id) { + if (errmsg) + fprintf(stderr, "%s\n", errmsg); + + free(errmsg); + + return ucv_boolean_new(false); + } + + return ucv_uint64_new(id); +} + +/** + * Notify an attached remote debugger client, if any, that the target is + * about to exit, with the final VM status (successful completion, + * `exit()`/`quit`, or an uncaught error). A no-op when nobody is attached, + * or for the local interactive debugger, where the exit is immediately + * visible on the same terminal. + * + * Called by main.c right after `uc_vm_execute()` returns, passing its raw + * `uc_vm_status_t` return value plus the corresponding detail (exit code, or + * an exception object), so a remote client learns the final outcome as an + * explicit event instead of only noticing sometime later that the + * connection dropped, with no indication of why. + * + * The detail arguments must be passed in explicitly by the caller rather + * than read off the vm here: by the time this C function body runs, + * uc_vm_call() has already cleared vm->exception as its own first action + * (a normal safety reset for ordinary calls), so main.c has to snapshot + * vm->arg.s32 / call uc_vm_exception_object() into locals before making + * this call. + * + * @function module:debug#notifyExit + * + * @param {number} status + * The `uc_vm_status_t` value `uc_vm_execute()` returned. + * + * @param {number} exitCode + * `vm->arg.s32` at the time `status` was returned, meaningful only for + * `STATUS_EXIT`. + * + * @param {object} [exception] + * `uc_vm_exception_object(vm)` at the time `status` was returned - the same + * `{type, message, stacktrace}` shape script code sees via try/catch. + * Meaningful only for `ERROR_COMPILE`/`ERROR_RUNTIME`. + */ +static uc_value_t * +uc_debug_notify_exit(uc_vm_t *vm, size_t nargs) +{ + uc_value_t *status = uc_fn_arg(0); + uc_value_t *exit_code = uc_fn_arg(1); + uc_value_t *exception_obj = uc_fn_arg(2); + + debug_remote_notify_exit(vm, + (ucv_type(status) == UC_INTEGER) ? (uc_vm_status_t)ucv_int64_get(status) : STATUS_OK, + (ucv_type(exit_code) == UC_INTEGER) ? (int32_t)ucv_int64_get(exit_code) : 0, + exception_obj); + + return NULL; +} + +static bool debug_remote_listen_armed = false; + +/* Registered as a ucode-level SIGUSR1 handler via the builtin signal() + * function, exactly like uc_debug_sigusr1_attach_handler() above. This + * matters for embedding: ucode-level signal handlers are invoked from + * uc_vm_signal_dispatch(), which is only ever called from inside + * uc_vm_execute_chunk()'s own per-instruction loop (see vm.c) - so this + * runs nested within whatever uc_vm_call()/uc_vm_execute() invocation the + * host application (uhttpd, uwsd, ...) is currently making, and returns + * normally once the debug session ends. It never unwinds the host's own + * C call stack the way the -X flag's raw POSIX SIGUSR1 handler does via + * uc_vm_break_request()/STATUS_BREAK, which a host application that embeds + * the VM directly (rather than driving it through ucode's own -X main + * loop) would have no way to handle. */ +static uc_value_t * +uc_debug_listen_sigusr1_handler(uc_vm_t *vm, size_t nargs) +{ + int client_fd = debug_remote_handle_break(vm); + + if (client_fd >= 0) + debug_run_session(vm, client_fd); + + return NULL; +} + +/** + * Listen for a remote debugger connection. + * + * With no argument (or a boolean), this arms `SIGUSR1`-triggered remote + * debugging on the PID-derived attach socket `/tmp/ucode-debug-.sock` + * - the same socket `-X` and `udbg ` use. This is the counterpart to + * the `-X` command line flag for scripts running inside a host application + * that embeds the ucode VM directly (e.g. uhttpd or uwsd) and therefore has + * no `-X` flag or `SIGUSR1`-triggered break infrastructure of its own. Once + * armed, sending `SIGUSR1` to the process makes it pause at the next + * instruction boundary, open the attach socket and hand off to the very + * same interactive CLI session used locally or via `-X` - the exact same + * command set, tab completion and ANSI rendering. + * + * With a string argument, it instead binds the given Unix domain socket + * path and blocks immediately (right here, synchronously, indefinitely) + * until a client connects on that path - independent of `SIGUSR1` and of + * the PID-derived attach socket. This is useful for host applications that + * want to expose the debugger on a well-known path of their own choosing. + * + * @param {boolean|string} [wait] + * If a string, treated as a socket path to bind and block on (see above). + * If truish (and not a string), block immediately, right here, until a + * debugger client connects on the PID-derived attach socket or a 30 second + * timeout elapses, exactly as if `SIGUSR1` had just been received - in + * addition to arming `SIGUSR1` for later. If omitted or falsy, only arm the + * `SIGUSR1` handler and return immediately; the process keeps running + * normally until a signal is actually sent. + * + * @returns {boolean} + * `true` on success, `false` if binding an explicit socket path failed. + * + * @example + * import { listen } from 'debug'; + * + * // Arm SIGUSR1-triggered remote debugging, keep running + * listen(); + * + * // ... or pause right here until a debugger attaches + * listen(true); + * + * // ... or listen on an explicit, caller-chosen socket path + * listen("/tmp/ucode-debug.sock"); + */ +static uc_value_t * +uc_debug_listen(uc_vm_t *vm, size_t nargs) +{ + uc_value_t *arg = uc_fn_arg(0); + + if (ucv_type(arg) == UC_STRING) { + int client_fd = debug_remote_accept_on_path(ucv_string_get(arg)); + + if (client_fd < 0) + return ucv_boolean_new(false); + + debug_run_session(vm, client_fd); + + return ucv_boolean_new(true); + } + + if (!debug_remote_listen_armed) { + uc_cfn_ptr_t ucsignal = uc_stdlib_function("signal"); + + uc_vm_stack_push(vm, ucv_string_new("SIGUSR1")); + uc_vm_stack_push(vm, + ucv_cfunction_new("debug_listen_sigusr1_handler", uc_debug_listen_sigusr1_handler)); + ucv_put(ucsignal(vm, 2)); + ucv_put(uc_vm_stack_pop(vm)); + ucv_put(uc_vm_stack_pop(vm)); + + install_debug_system_breakpoints(vm); + + debug_remote_listen_armed = true; + } + + if (ucv_is_truish(arg)) { + int client_fd = debug_remote_handle_break(vm); + + if (client_fd >= 0) + debug_run_session(vm, client_fd); + } + + return ucv_boolean_new(true); +} + +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_USER + }; + + bk_enter_session(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; +} + +/** + * Initialize interactive debugger. + * + * The `debugger()` function sets up the interactive command line debugger and + * immediately starts it, or - when a function argument is provided - defers the + * debugger invocation until the given function is called. + * + * This function does not return any value. + * + * @function module:debug#debugger + * + * @param {function} [target] + * An optional function to attach the debugger to. When provided, a debug + * breakpoint is installed at the first instruction of the given function, + * causing the debug cli to get launched as soon as this function is entered. + * + * @example + * // Launch debugger immediately + * debug.debugger(); + * + * + * // Attach debugger to function + * function test(a, b) { + * print(`Result is ${a * b}\n`); + * } + * + * debug.debugger(test); // Install debug breakpoint in `test()` function + * test(); // Starts debugger, breaking before `print(…)` + */ +/* Fork a co-process running the interactive protocol client (the `udbg` + * binary, in its `--fd` mode) connected to us via a socketpair, and make it + * the current session connection - the local `-x` CLI counterpart to a + * remote `debug.listen()`/`-X` connection being accepted. The child owns the + * real controlling terminal (it never touches the inherited fd 3 for + * anything but the protocol connection, so its own stdin/stdout still are + * whatever tty invoked `ucode -x`); the parent (this process, running the + * debugged script) never sets up any tty state of its own and only ever + * speaks the line protocol over the session fd, exactly like the remote + * case. */ +static bool +spawn_local_client(void) +{ + int sv[2]; + pid_t pid; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) + return false; + + pid = fork(); + + if (pid < 0) { + close(sv[0]); + close(sv[1]); + + return false; + } + + if (pid == 0) { + close(sv[0]); + + if (sv[1] != 3) { + dup2(sv[1], 3); + close(sv[1]); + } + + execlp("udbg", "udbg", "--fd", "3", NULL); + _exit(127); + } + + close(sv[1]); + + debug_remote_set_active_fd(sv[0]); + debug_proto_buf_init(&session_buf); + + return true; +} + +static bool debug_local_initialized = false; + +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 (!debug_local_initialized) { + if (!spawn_local_client()) { + fprintf(stderr, "Failed to launch debugger client (udbg)\n"); + + 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)); + + install_debug_system_breakpoints(vm); + + debug_local_initialized = true; + } + + if (ucv_type(mainfn) == UC_CLOSURE) { + uc_function_t *fn = ((uc_closure_t *)mainfn)->function; + update_breakpoint(vm, BK_STEP, bk_enter_session, fn->chunk.entries, fn, 1); + } + else { + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + + if (frame) { + debug_breakpoint_t dbk = { + .bk = { .ip = frame->ip }, + .fn = frame->closure->function, + .kind = BK_USER + }; + + bk_enter_session(vm, &dbk.bk); + } + } + + return NULL; +} + + +static const uc_function_list_t debug_fns[] = { + { "memdump", uc_memdump }, + { "traceback", uc_traceback }, + { "sourcepos", uc_sourcepos }, + { "getinfo", uc_getinfo }, + { "getlocal", uc_getlocal }, + { "setlocal", uc_setlocal }, + { "getupval", uc_getupval }, + { "setupval", uc_setupval }, + { "debugger", uc_debugger }, + { "attach", uc_debug_attach }, + { "break", uc_debug_break }, + { "breakpoint", uc_debug_breakpoint }, + { "listen", uc_debug_listen }, + { "notifyExit", uc_debug_notify_exit }, +}; + +/* Callback invoked by main.c when STATUS_BREAK is returned in -X mode. + * debug_remote_handle_break() (in debug_remote.c) only deals with socket + * transport: it creates the attach socket and waits for a udbg client to + * connect, returning the accepted client fd, -1 on timeout/no client, or + * -2 on a fatal socket error. On success, the full interactive CLI session + * is driven by debug_run_session() above, which also resumes + * script execution once the session ends. + * Returns 0 if execution should resume unattended, 1 if the program has + * already finished or should exit. */ +static int +debug_server_handle_break(uc_vm_t *vm) +{ + int client_fd = debug_remote_handle_break(vm); + + if (client_fd == -1) + return 0; + + if (client_fd < 0) + return 1; + + debug_run_session(vm, client_fd); + + return 1; +} + +void +uc_module_init(uc_vm_t *vm, uc_value_t *scope) +{ + uc_function_list_register(scope, debug_fns); + + debug_setup(vm); + + + /* Register break handler so main.c can find it via registry */ + uc_vm_registry_set(vm, "debug.server_handle_break", + ucv_resource_new(NULL, (void *)(uintptr_t)debug_server_handle_break)); } diff --git a/lib/debug_proto.c b/lib/debug_proto.c new file mode 100644 index 00000000..26a7e41e --- /dev/null +++ b/lib/debug_proto.c @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2026 Jo-Philipp Wich + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ANY IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include + +#include "ucode/util.h" +#include "debug_proto.h" + +#define DEBUG_PROTO_READ_CHUNK 1024 + +void +debug_proto_buf_init(debug_proto_buf_t *buf) +{ + buf->data = NULL; + buf->len = 0; + buf->cap = 0; +} + +void +debug_proto_buf_free(debug_proto_buf_t *buf) +{ + free(buf->data); + buf->data = NULL; + buf->len = 0; + buf->cap = 0; +} + +void +debug_proto_write(int fd, uc_vm_t *vm, const char *verb, uc_value_t *payload) +{ + uc_stringbuf_t *sb = xprintbuf_new(); + const char *p; + size_t remaining; + ssize_t n; + char *json; + + printbuf_memappend_fast(sb, verb, (int)strlen(verb)); + + if (payload) { + json = ucv_to_jsonstring(vm, payload); + + if (json) { + printbuf_memappend_fast(sb, " ", 1); + printbuf_memappend_fast(sb, json, (int)strlen(json)); + free(json); + } + } + + printbuf_memappend_fast(sb, "\n", 1); + + p = sb->buf; + remaining = (size_t)printbuf_length(sb); + + while (remaining > 0) { + n = write(fd, p, remaining); + + if (n < 0) { + if (errno == EINTR) + continue; + + break; + } + + p += n; + remaining -= (size_t)n; + } + + printbuf_free(sb); +} + +/* Append `len` bytes to the tail of buf, growing its backing storage as + * needed. Returns false on allocation failure (buf is left unchanged). */ +static bool +buf_append(debug_proto_buf_t *buf, const char *data, size_t len) +{ + char *newdata; + size_t newcap; + + if (buf->len + len > buf->cap) { + newcap = buf->cap ? buf->cap : DEBUG_PROTO_READ_CHUNK; + + while (newcap < buf->len + len) + newcap *= 2; + + newdata = realloc(buf->data, newcap); + + if (!newdata) + return false; + + buf->data = newdata; + buf->cap = newcap; + } + + memcpy(buf->data + buf->len, data, len); + buf->len += len; + + return true; +} + +/* Drop the first `n` bytes already consumed as a message from the front of + * buf, shifting any remaining buffered bytes down. */ +static void +buf_consume(debug_proto_buf_t *buf, size_t n) +{ + memmove(buf->data, buf->data + n, buf->len - n); + buf->len -= n; +} + +int +debug_proto_read(int fd, debug_proto_buf_t *buf, uc_vm_t *vm, + char **verb_out, uc_value_t **payload_out) +{ + char chunk[DEBUG_PROTO_READ_CHUNK]; + char *line, *sp, *verb; + struct json_tokener *tok; + json_object *jso; + size_t line_len, verb_len, json_len; + ssize_t n; + + *verb_out = NULL; + *payload_out = NULL; + + for (;;) { + char *nl = memchr(buf->data, '\n', buf->len); + + if (nl) { + line_len = (size_t)(nl - buf->data); + break; + } + + n = read(fd, chunk, sizeof(chunk)); + + if (n < 0) { + if (errno == EINTR) + continue; + + return -1; + } + + if (n == 0) + return 0; + + if (!buf_append(buf, chunk, (size_t)n)) + return -1; + } + + line = malloc(line_len + 1); + + if (!line) + return -1; + + memcpy(line, buf->data, line_len); + line[line_len] = '\0'; + buf_consume(buf, line_len + 1); + + if (line_len > 0 && line[line_len - 1] == '\r') + line[--line_len] = '\0'; + + sp = memchr(line, ' ', line_len); + verb_len = sp ? (size_t)(sp - line) : line_len; + + verb = malloc(verb_len + 1); + + if (!verb) { + free(line); + + return -1; + } + + memcpy(verb, line, verb_len); + verb[verb_len] = '\0'; + + json_len = sp ? line_len - verb_len - 1 : 0; + + if (json_len > 0) { + tok = xjs_new_tokener(); + + /* len + 1 to include the trailing NUL: works around json-c + * treating a lone atomic value (e.g. `true`) as incomplete + * without a following delimiter, see json-c issue #681. */ + jso = json_tokener_parse_ex(tok, sp + 1, (int)json_len + 1); + + if (json_tokener_get_error(tok) != json_tokener_success) { + json_tokener_free(tok); + json_object_put(jso); + free(verb); + free(line); + + return -2; + } + + *payload_out = ucv_from_json(vm, jso); + + json_tokener_free(tok); + json_object_put(jso); + } + + free(line); + *verb_out = verb; + + return 1; +} diff --git a/lib/debug_proto.h b/lib/debug_proto.h new file mode 100644 index 00000000..03fd3a8d --- /dev/null +++ b/lib/debug_proto.h @@ -0,0 +1,51 @@ +#ifndef _UCODE_DEBUG_PROTO_H +#define _UCODE_DEBUG_PROTO_H + +#include + +#include +#include + +/* + * Line-based debug protocol framing. + * + * One message per line: an uppercase VERB, optionally followed by a single + * space and a JSON-encoded object payload, terminated by '\n'. Used for both + * directions of traffic (client commands and server responses/events), and + * by every transport (local socketpair, remote Unix domain socket). + */ + +/* Incremental read buffer, one per connection. Zero-initialize (or use + * debug_proto_buf_init()) before first use; release with + * debug_proto_buf_free() once the connection is done. */ +typedef struct { + char *data; + size_t len; + size_t cap; +} debug_proto_buf_t; + +void debug_proto_buf_init(debug_proto_buf_t *buf); +void debug_proto_buf_free(debug_proto_buf_t *buf); + +/* Write a single "VERB json\n" (or "VERB\n" if payload is NULL) message to + * fd. Best-effort: I/O errors are silently swallowed, matching the previous + * debug_write_response() semantics - a dead/blocked peer must never be fatal + * to the caller. payload is not consumed/freed. */ +void debug_proto_write(int fd, uc_vm_t *vm, const char *verb, uc_value_t *payload); + +/* Read a single message from fd, using buf to hold data already read from + * fd but not yet consumed as a full line (refilled from fd as needed). + * Blocks until a full line is available, EOF, or an I/O error occurs. + * + * On success (return 1), *verb_out is set to a newly heap-allocated, + * NUL-terminated verb string (caller must free()) and *payload_out to the + * parsed JSON payload, or NULL if the line carried no payload. + * + * Returns 0 on clean EOF, -1 on I/O error, -2 if the line's payload could + * not be parsed as JSON (verb/payload are left untouched in both error + * cases). + */ +int debug_proto_read(int fd, debug_proto_buf_t *buf, uc_vm_t *vm, + char **verb_out, uc_value_t **payload_out); + +#endif diff --git a/lib/debug_remote.c b/lib/debug_remote.c new file mode 100644 index 00000000..79ae7163 --- /dev/null +++ b/lib/debug_remote.c @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2026 Jo-Philipp Wich + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ANY IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * Remote debugger socket transport. + * + * This file only deals with the socket transport: creating Unix domain + * sockets (both the PID-derived SIGUSR1 attach socket and arbitrary + * caller-supplied paths for debug.listen()), accepting a `udbg` client + * connection, and pushing asynchronous "EVENT " notifications to an + * attached client. The script-facing debug.listen() API and the actual + * interactive command session - which reuses the exact same command set, + * tab completion and readline-style editing as the local terminal debugger + * - live in debug.c (see uc_debug_listen() / debug_cli_run_remote_session()), + * once a client fd has been accepted here. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ucode/lib.h" +#include "ucode/util.h" +#include "ucode/vm.h" +#include "debug_remote.h" +#include "debug_proto.h" + +static int remote_debug_fd = -1; + + +void +debug_remote_set_active_fd(int fd) +{ + remote_debug_fd = fd; +} + +bool +debug_remote_has_active_connection(void) +{ + return remote_debug_fd >= 0; +} + +int +debug_remote_get_active_fd(void) +{ + return remote_debug_fd; +} + + +/* Wait for a udbg client to connect to the SIGUSR1 attach socket, with a + * 30s timeout. Returns the accepted client fd on success, -1 on timeout + * (caller should resume execution unattended), or -2 on a fatal error. */ +int +debug_remote_handle_break(uc_vm_t *vm) +{ + int listen_fd = debug_remote_create_attach_socket(); + fd_set readfds; + struct timeval tv; + int ret, client_fd; + + if (listen_fd < 0) { + fprintf(stderr, "Failed to create attach socket: %s\n", strerror(errno)); + return -2; + } + + fprintf(stderr, "Debugger socket ready, waiting for connection...\n"); + + for (;;) { + FD_ZERO(&readfds); + FD_SET(listen_fd, &readfds); + tv.tv_sec = 30; + tv.tv_usec = 0; + + ret = select(listen_fd + 1, &readfds, NULL, NULL, &tv); + + if (ret < 0 && errno == EINTR) + continue; + + break; + } + + if (ret <= 0) { + close(listen_fd); + debug_remote_cleanup_attach_socket(); + + if (ret == 0) + fprintf(stderr, "Timeout waiting for debugger connection - continuing execution\n"); + else + fprintf(stderr, "Error waiting for debugger connection: %s\n", strerror(errno)); + + return -1; + } + + client_fd = accept(listen_fd, NULL, NULL); + close(listen_fd); + + if (client_fd < 0) { + debug_remote_cleanup_attach_socket(); + return -1; + } + + return client_fd; +} + + +/* Create, bind (mode 0600) and listen on a Unix domain socket at the given + * path, removing any stale socket file first. Shared by both the + * SIGUSR1-triggered attach socket (fixed, PID-derived path) and + * debug.listen() (arbitrary caller-supplied path). Returns the listening + * fd, or -1 on error. */ +static int +debug_remote_bind_and_listen(const char *path) +{ + struct sockaddr_un addr = { 0 }; + int listen_fd; + socklen_t addrlen; + mode_t old_umask; + + listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (listen_fd < 0) + return -1; + + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); + addr.sun_path[sizeof(addr.sun_path) - 1] = '\0'; + addrlen = sizeof(sa_family_t) + strlen(path) + 1; + + unlink(path); + + old_umask = umask(077); + + if (bind(listen_fd, (struct sockaddr *)&addr, addrlen) < 0) { + umask(old_umask); + close(listen_fd); + return -1; + } + + umask(old_umask); + + if (listen(listen_fd, 1) < 0) { + close(listen_fd); + return -1; + } + + return listen_fd; +} + + +/* Global socket path for SIGUSR1-triggered attach */ +static char attach_socket_path[1024] = { 0 }; + +int +debug_remote_create_attach_socket(void) +{ + pid_t pid = getpid(); + + snprintf(attach_socket_path, sizeof(attach_socket_path), + "/tmp/ucode-debug-%d.sock", pid); + + return debug_remote_bind_and_listen(attach_socket_path); +} + +const char * +debug_remote_get_socket_path(void) +{ + return attach_socket_path[0] ? attach_socket_path : NULL; +} + +void +debug_remote_cleanup_attach_socket(void) +{ + if (attach_socket_path[0] != '\0') { + unlink(attach_socket_path); + attach_socket_path[0] = '\0'; + } +} + +/* Bind, listen on and accept a single connection on an arbitrary, + * caller-supplied Unix domain socket path, blocking indefinitely. Returns + * the accepted client fd, or -1 on error. Used by debug.listen(path) (see + * debug.c) for the explicit-path case, as opposed to the PID-derived attach + * socket used for the SIGUSR1/-X flow above. */ +int +debug_remote_accept_on_path(const char *path) +{ + int listen_fd, client_fd; + + listen_fd = debug_remote_bind_and_listen(path); + if (listen_fd < 0) + return -1; + + client_fd = accept(listen_fd, NULL, NULL); + close(listen_fd); + + /* The socket file is no longer needed once accepted (or on error) - + * the connection itself doesn't depend on the path persisting. */ + unlink(path); + + return client_fd; +} + + +/* Shallow-copy a plain object's own keys into a fresh object with no + * prototype - values are shared (ucv_get()'d, not deep-cloned). + * + * Used to defuse uc_vm_exception_object()'s tostring() prototype method + * (attached for script-facing try/catch ergonomics, so `catch (e) { + * print(e) }` prints the message) before JSON-serializing it: ucv_to_json + * string() invokes tostring() if present instead of serializing the + * object's own fields, which would collapse the whole thing down to just + * the message string. Worse, invoking it runs through the VM's own call + * machinery, which calls uc_vm_clear_exception() as a side effect - wiping + * out vm->exception (including freeing ->message) out from under whatever + * runs next. Copying rather than mutating the prototype in place on the + * original object avoids surprising a caller who still holds a reference + * to it for other purposes. */ +static uc_value_t * +object_shallow_copy_no_proto(uc_vm_t *vm, uc_value_t *obj) +{ + uc_value_t *copy = ucv_object_new(vm); + + ucv_object_foreach(obj, k, v) + ucv_object_add(copy, k, ucv_get(v)); + + return copy; +} + +/* Push an unsolicited exception notification to the connected debugger + * client, if any, as the same JSON exception object shape script code sees + * via try/catch ({type, message, stacktrace} - see uc_vm_exception_object() + * in vm.c) - safe to call unconditionally from the VM's exception handler + * chain; a no-op when nobody is attached. `ex` is expected to still be + * `&vm->exception` at this point (true for the exception handler chain, + * which runs synchronously before anything gets cleared), since the actual + * object is built from vm->exception directly. */ +void +debug_remote_notify_exception(uc_vm_t *vm, uc_exception_t *ex) +{ + uc_value_t *exo, *plain, *evo; + + (void)ex; + + if (remote_debug_fd < 0) + return; + + exo = uc_vm_exception_object(vm); + plain = object_shallow_copy_no_proto(vm, exo); + ucv_put(exo); + + evo = ucv_object_new(vm); + ucv_object_add(evo, "event", ucv_string_new("exception")); + ucv_object_add(evo, "exception", plain); + + debug_proto_write(remote_debug_fd, vm, "EVENT", evo); + ucv_put(evo); +} + +static const char * +vm_status_name(uc_vm_status_t status) +{ + switch (status) { + case STATUS_OK: return "OK"; + case STATUS_EXIT: return "EXIT"; + case STATUS_BREAK: return "BREAK"; + case ERROR_COMPILE: return "ERROR_COMPILE"; + case ERROR_RUNTIME: return "ERROR_RUNTIME"; + default: return "UNKNOWN"; + } +} + +/* Push a final "the target is going away" notification to the connected + * debugger client, if any, as a JSON object describing the full final VM + * state - {status}, plus {code} for STATUS_EXIT or the same {type, message, + * stacktrace} exception object shape used above for ERROR_COMPILE/ + * ERROR_RUNTIME. Called from main.c right after uc_vm_execute() returns, + * before the process actually exits and the connection drops - without + * this, a client only finds out the target is gone once the socket EOFs, + * with no indication of why. + * + * Takes the raw uc_vm_status_t rather than main.c's own CLI exit-code + * translation (which flattens both ERROR_COMPILE and ERROR_RUNTIME to the + * same -2 and loses the actual exception), plus exit_code and a + * pre-built exception object (or NULL). Both must be supplied by the + * caller rather than read off the vm here: by the time this runs + * (dispatched through a ucode-level call), uc_vm_call() has already + * cleared vm->exception as its own first action, so main.c has to + * snapshot vm->arg.s32 / call uc_vm_exception_object() *before* making + * this call. */ +void +debug_remote_notify_exit(uc_vm_t *vm, uc_vm_status_t status, int32_t exit_code, + uc_value_t *exception_obj) +{ + uc_value_t *evo; + + if (remote_debug_fd < 0) + return; + + evo = ucv_object_new(vm); + + ucv_object_add(evo, "event", ucv_string_new("exit")); + ucv_object_add(evo, "status", ucv_string_new(vm_status_name(status))); + + if (status == STATUS_EXIT) + ucv_object_add(evo, "code", ucv_int64_new(exit_code)); + else if (exception_obj) { + /* Copy without the tostring() prototype uc_vm_exception_object() + * attaches (for script-facing try/catch ergonomics) before nesting + * it - see the comment on object_shallow_copy_no_proto() above: + * otherwise ucv_to_jsonstring() below would invoke it and collapse + * this down to just the message string instead of serializing + * {type, message, stacktrace}. */ + uc_value_t *plain = object_shallow_copy_no_proto(vm, exception_obj); + + ucv_object_add(evo, "exception", plain); + } + + debug_proto_write(remote_debug_fd, vm, "EVENT", evo); + ucv_put(evo); +} diff --git a/lib/debug_remote.h b/lib/debug_remote.h new file mode 100644 index 00000000..7f32fd28 --- /dev/null +++ b/lib/debug_remote.h @@ -0,0 +1,44 @@ +#ifndef _UCODE_DEBUG_REMOTE_H +#define _UCODE_DEBUG_REMOTE_H + +#include +#include + +int debug_remote_create_attach_socket(void); +const char *debug_remote_get_socket_path(void); +void debug_remote_cleanup_attach_socket(void); + +/* Accept a single connection on an arbitrary, caller-supplied Unix domain + * socket path, blocking indefinitely. Returns the accepted client fd, or -1 + * on error. Used by debug.listen(path) for the explicit-path case. */ +int debug_remote_accept_on_path(const char *path); + +/* Push unsolicited notifications to a connected debugger client, if any, as + * "EVENT {json}" protocol messages (see debug_proto.h) - the JSON payload + * always carries a discriminating "event" field ("exception"/"exit"). */ +void debug_remote_notify_exception(uc_vm_t *vm, uc_exception_t *ex); +void debug_remote_notify_exit(uc_vm_t *vm, uc_vm_status_t status, int32_t exit_code, + uc_value_t *exception_obj); + +/* Mark the given fd as the currently attached debugger session connection + * (or -1 for none), used by debug_remote_has_active_connection() and the + * notify helpers above - shared by both the remote and local (-x) cases, + * since both ultimately just hand a connected fd to bk_enter_session(). + * Owned by whoever is currently driving the session (debug_run_session()). */ +void debug_remote_set_active_fd(int fd); +bool debug_remote_has_active_connection(void); +int debug_remote_get_active_fd(void); + +/* Wait for a udbg client to connect to the SIGUSR1 attach socket, with a + * 30s timeout. Returns the accepted client fd on success, -1 on timeout or + * disconnect (caller should resume execution unattended), or -2 on a fatal + * socket error (caller should give up). */ +int debug_remote_handle_break(uc_vm_t *vm); + +/* Provided by debug.c: run a full interactive debugger session over an + * already-connected client socket, speaking the line-based debug protocol + * (see debug_proto.h). Takes ownership of client_fd (closes it) and resumes + * script execution before returning. */ +void debug_run_session(uc_vm_t *vm, int client_fd); + +#endif diff --git a/main.c b/main.c index adce3f9c..d9c85112 100644 --- a/main.c +++ b/main.c @@ -113,15 +113,61 @@ print_usage(const char *app) "-s\n" " Omit (strip) debug information when compiling files.\n" - " Only meaningful in conjunction with `-c`.\n\n", + " Only meaningful in conjunction with `-c`.\n\n" + + "-x[expr]\n" + " Start program in interactive debugger. If given, stop at the location\n" + " described by `expr` (a function name, `path:line[:offset]`, or a ucode\n" + " expression evaluating to a function - the same grammar the `break`\n" + " debugger CLI command accepts) instead of the first instruction.\n\n" + "-X[expr]\n" + " Enable debugger infrastructure (SIGUSR1 break, uloop) without\n" + " launching the interactive debugger automatically. If given, `expr` is\n" + " resolved the same way as for `-x` and a breakpoint is installed at\n" + " that location; once hit, execution pauses and waits for a remote\n" + " debugger to attach, the same way the SIGUSR1 break does.\n\n", app); } +static bool +parse_library_load(char *opt, uc_vm_t *vm); + +static uc_value_t * +debug_lookup_fn(uc_vm_t *vm, const char *name) +{ + uc_value_t *dbgmod = ucv_object_get(uc_vm_scope_get(vm), "debug", NULL); + uc_value_t *fn = ucv_object_get(dbgmod, name, NULL); + + if (ucv_type(fn) != UC_CFUNCTION) { + fprintf(stderr, "Unable to locate debug.%s() function\n", name); + + return NULL; + } + + return fn; +} + +typedef enum { + DEBUG_MODE_NONE, /* neither -x nor -X given */ + DEBUG_MODE_LOCAL, /* -x: launch local interactive debugger */ + DEBUG_MODE_REMOTE, /* -X: enable break infrastructure for a remote debugger */ +} debug_mode_t; + +typedef struct { + bool strip; + char *interpreter; + bool autoprint; + debug_mode_t debug; + char *breakpoint; +} compile_opts_t; 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, const compile_opts_t *opts) { uc_value_t *res = NULL; + bool strip = opts->strip, autoprint = opts->autoprint; + debug_mode_t debug = opts->debug; + char *interp = opts->interpreter, *breakpoint = opts->breakpoint; uc_program_t *program; int rc = 0; char *err; @@ -147,11 +193,94 @@ 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); - rc = uc_vm_execute(vm, program, &res); + if (debug != DEBUG_MODE_NONE) { + uc_value_t *entryfn; + + if (!parse_library_load("debug", vm)) { + fprintf(stderr, "Unable to load debug module\n"); + rc = -2; + goto out; + } + + entryfn = ucv_closure_new(vm, uc_program_entry(program), false); + + /* -x: launch local debugger, breaking at `breakpoint` if given, + * else at the first instruction. + * -X: just enable break infrastructure; if `breakpoint` is given, + * additionally arm attach mode and break at that location instead + * of waiting for a plain SIGUSR1. */ + if (debug == DEBUG_MODE_LOCAL) { + uc_value_t *dbgfn = debug_lookup_fn(vm, "debugger"); + + if (!dbgfn) { + ucv_put(entryfn); + rc = -2; + goto out; + } + + uc_vm_stack_push(vm, ucv_get(dbgfn)); + uc_vm_stack_push(vm, breakpoint ? NULL : ucv_get(entryfn)); + + if (uc_vm_call(vm, false, 1) == EXCEPTION_NONE) + ucv_put(uc_vm_stack_pop(vm)); + } + else if (breakpoint) { + uc_value_t *attachfn = debug_lookup_fn(vm, "attach"); + + if (!attachfn) { + ucv_put(entryfn); + rc = -2; + goto out; + } + + uc_vm_stack_push(vm, ucv_get(attachfn)); + uc_vm_stack_push(vm, NULL); + + if (uc_vm_call(vm, false, 1) == EXCEPTION_NONE) + ucv_put(uc_vm_stack_pop(vm)); + } + + if (breakpoint) { + uc_value_t *bkfn = debug_lookup_fn(vm, "breakpoint"); + uc_value_t *id; + + if (!bkfn) { + ucv_put(entryfn); + rc = -2; + goto out; + } + + uc_vm_stack_push(vm, ucv_get(bkfn)); + uc_vm_stack_push(vm, ucv_string_new(breakpoint)); + uc_vm_stack_push(vm, ucv_get(entryfn)); + + if (uc_vm_call(vm, false, 2) == EXCEPTION_NONE) { + id = uc_vm_stack_pop(vm); + + if (!ucv_is_truish(id)) { + fprintf(stderr, + "Unable to resolve breakpoint location '%s'\n", + breakpoint); + ucv_put(id); + ucv_put(entryfn); + rc = -2; + goto out; + } + + ucv_put(id); + } + } + + ucv_put(entryfn); + } + + uc_vm_status_t status = uc_vm_execute(vm, program, &res); + + rc = status; switch (rc) { case STATUS_OK: - if (print_result) { + if (autoprint) { if (ucv_type(res) == UC_STRING) { fwrite(ucv_string_get(res), ucv_string_length(res), 1, stdout); } @@ -171,6 +300,14 @@ compile(uc_vm_t *vm, uc_source_t *src, FILE *precompile, bool strip, char *inter rc = (int)ucv_int64_get(res); break; + case STATUS_BREAK: + /* Break requested - in remote debug mode, continue running */ + if (debug == DEBUG_MODE_REMOTE) + rc = 0; + else + rc = -2; + break; + case ERROR_COMPILE: rc = -1; break; @@ -180,6 +317,48 @@ compile(uc_vm_t *vm, uc_source_t *src, FILE *precompile, bool strip, char *inter break; } + /* Let an attached remote debugger client know the target is going away + * and why, instead of it only finding out once the connection drops. + * Pass the raw VM status rather than the CLI's own exit-code + * translation above (which flattens both error kinds to the same -2 + * and loses the actual exception), plus the exit code / a full + * exception object (same {type, message, stacktrace} shape script + * code sees via try/catch) it corresponds to. + * + * These have to be snapshotted into locals *before* the uc_vm_call() + * below, not read from vm->exception/vm->arg by the callee once + * inside it: uc_vm_call() unconditionally calls + * uc_vm_clear_exception() as its very first action (a normal safety + * reset for ordinary calls, which also frees vm->exception.message/ + * ->stacktrace), which would wipe vm->exception out from under us + * before debug.notifyExit() ever got to look at it. + * + * vm->output (stdout) is fully block-buffered once it's a socket + * rather than a tty, while the notification itself goes out via a raw + * write() - flush first, or the event can overtake not-yet-flushed + * script output that was already written earlier. */ + if (debug != DEBUG_MODE_NONE) { + uc_value_t *notifyfn = debug_lookup_fn(vm, "notifyExit"); + int32_t exit_code = vm->arg.s32; + uc_value_t *exception_obj = (status == ERROR_COMPILE || status == ERROR_RUNTIME) + ? uc_vm_exception_object(vm) : NULL; + + fflush(vm->output); + + if (notifyfn) { + uc_vm_stack_push(vm, ucv_get(notifyfn)); + uc_vm_stack_push(vm, ucv_int64_new(status)); + uc_vm_stack_push(vm, ucv_int64_new(exit_code)); + uc_vm_stack_push(vm, exception_obj); + + if (uc_vm_call(vm, false, 3) == EXCEPTION_NONE) + ucv_put(uc_vm_stack_pop(vm)); + } + else { + ucv_put(exception_obj); + } + } + out: uc_program_put(program); ucv_put(res); @@ -513,8 +692,10 @@ appname(const char *argv0) int main(int argc, char **argv) { - const char *optspec = POSIXLY_CORRECT_FLAG "he:p:tg:ST::RD:F:U:l:L:c::o:s"; + const char *optspec = POSIXLY_CORRECT_FLAG "he:p:tg:ST::RD:F:U:l:L:c::o:sx::X::"; bool strip = false, print_result = false; + debug_mode_t debug = DEBUG_MODE_NONE; + char *breakpoint = NULL; char *interp = "/usr/bin/env ucode"; uc_source_t *source = NULL; FILE *precompile = NULL; @@ -654,6 +835,16 @@ main(int argc, char **argv) case 'o': outfile = optarg; break; + + case 'x': + debug = DEBUG_MODE_LOCAL; + breakpoint = optarg; + break; + + case 'X': + debug = DEBUG_MODE_REMOTE; + breakpoint = optarg; + break; } } @@ -703,7 +894,13 @@ main(int argc, char **argv) ucv_put(o); - rv = compile(&vm, source, precompile, strip, interp, print_result); + rv = compile(&vm, source, precompile, &((compile_opts_t){ + .strip = strip, + .interpreter = interp, + .autoprint = print_result, + .debug = debug, + .breakpoint = breakpoint, + })); out: uc_search_path_free(&config.module_search_path); diff --git a/platform.c b/platform.c index 63a79d4f..6a42672e 100644 --- a/platform.c +++ b/platform.c @@ -109,6 +109,9 @@ const char *uc_system_signal_names[UC_SYSTEM_SIGNAL_COUNT] = { #if defined(SIGUSR2) [SIGUSR2] = "USR2", #endif +#if defined(SIGWINCH) + [SIGWINCH] = "WINCH", +#endif }; diff --git a/tests/cram/test_basic.t b/tests/cram/test_basic.t index b9cd95ab..56679a94 100644 --- a/tests/cram/test_basic.t +++ b/tests/cram/test_basic.t @@ -80,6 +80,15 @@ check that ucode provides exepected help: Omit (strip) debug information when compiling files. Only meaningful in conjunction with `-c`. + -x + Start program in interactive debugger. + + -X + Enable debugger infrastructure (SIGUSR1 break, uloop) without + launching the interactive debugger automatically. + + + check that ucode prints greetings: diff --git a/tests/custom/03_stdlib/69_dict b/tests/custom/03_stdlib/69_dict new file mode 100644 index 00000000..ed92561b --- /dev/null +++ b/tests/custom/03_stdlib/69_dict @@ -0,0 +1,600 @@ +The `dict()` function creates a dictionary with arbitrary ucode value keys, +instead of being limited to string keys as regular objects are. + +Key uniqueness follows `uc_uniq()` semantics: +- Scalars (null, bool, int, double, string) are compared by value +- Non-scalars (arrays, objects, etc.) are compared by pointer equality +- NaN doubles are treated as equal + +1. Create an empty dictionary. + +-- Testcase -- +{% + let d = dict(); + printf("%.J\n", [ type(d), length(d) ]); +%} +-- End -- + +-- Expect stdout -- +[ + "object", + 0 +] +-- End -- + + +2. Create dictionary from an object (string keys become string value keys). + +-- Testcase -- +{% + let d = dict({ "a": 1, "b": 2 }); + printf("%.J\n", [ length(d), d["a"], d["b"] ]); +%} +-- End -- + +-- Expect stdout -- +[ + 2, + 1, + 2 +] +-- End -- + + +3. Create dictionary from an array (numeric indices become integer keys). + +-- Testcase -- +{% + let d = dict([ 10, 20, 30 ]); + printf("%.J\n", [ length(d), d[0], d[1], d[2] ]); +%} +-- End -- + +-- Expect stdout -- +[ + 3, + 10, + 20, + 30 +] +-- End -- + + +4. Create dictionary from another dict. + +-- Testcase -- +{% + let d1 = dict(); + d1[42] = "answer"; + d1[true] = "yes"; + let d2 = dict(d1); + printf("%.J\n", [ length(d2), d2[42], d2[true] ]); +%} +-- End -- + +-- Expect stdout -- +[ + 2, + "answer", + "yes" +] +-- End -- + + +5. Dict with various scalar key types (bool, int, float, null, string). + +-- Testcase -- +{% + let d = dict(); + d[true] = "yes"; + d[false] = "no"; + d[42] = "answer"; + d[-1] = "neg"; + d[0] = "zero"; + d[3.14] = "pi"; + d[null] = "nil"; + d["foo"] = "bar"; + + printf("%.J\n", [ + length(d), + d[true], d[false], + d[42], d[-1], d[0], + d[3.14], + d[null], + d["foo"] + ]); +%} +-- End -- + +-- Expect stdout -- +[ + 8, + "yes", + "no", + "answer", + "neg", + "zero", + "pi", + "nil", + "bar" +] +-- End -- + + +6. NaN keys are treated as equal (single NaN slot). + +-- Testcase -- +{% + let d = dict(); + let n1 = json("NaN"); + let n2 = json("NaN"); + d[n1] = "first"; + d[n2] = "second"; + printf("%.J\n", [ length(d), d[n1], d[n2] ]); +%} +-- End -- + +-- Expect stdout -- +[ + 1, + "second", + "second" +] +-- End -- + + +7. Array keys use pointer equality (different array instances = different keys). + +-- Testcase -- +{% + let d = dict(); + let a1 = [ 1, 2, 3 ]; + let a2 = [ 1, 2, 3 ]; + d[a1] = "first"; + d[a2] = "second"; + printf("%.J\n", [ length(d), d[a1], d[a2] ]); +%} +-- End -- + +-- Expect stdout -- +[ + 2, + "first", + "second" +] +-- End -- + + +8. Array keys use pointer equality (same reference = same key). + +-- Testcase -- +{% + let d = dict(); + let a = [ 1, 2, 3 ]; + d[a] = "first"; + d[a] = "second"; + printf("%.J\n", [ length(d), d[a] ]); +%} +-- End -- + +-- Expect stdout -- +[ + 1, + "second" +] +-- End -- + + +9. Object keys use pointer equality. + +-- Testcase -- +{% + let d = dict(); + let o1 = { x: 1 }; + let o2 = { x: 1 }; + d[o1] = "first"; + d[o2] = "second"; + printf("%.J\n", [ length(d), d[o1], d[o2] ]); +%} +-- End -- + +-- Expect stdout -- +[ + 2, + "first", + "second" +] +-- End -- + + +10. Update existing key preserves key count. + +-- Testcase -- +{% + let d = dict(); + d[1] = "a"; + d[2] = "b"; + d[3] = "c"; + d[2] = "updated"; + printf("%.J\n", [ length(d), d[2] ]); +%} +-- End -- + +-- Expect stdout -- +[ + 3, + "updated" +] +-- End -- + + +11. Delete key from dict. + +-- Testcase -- +{% + let d = dict(); + d["a"] = 1; + d["b"] = 2; + d["c"] = 3; + delete d["b"]; + printf("%.J\n", [ length(d), d["a"], d["b"], d["c"] ]); +%} +-- End -- + +-- Expect stdout -- +[ + 2, + 1, + null, + 3 +] +-- End -- + + +12. keys() on dict returns actual value keys. + +-- Testcase -- +{% + let d = dict(); + d[42] = "x"; + d[true] = "y"; + d["foo"] = "z"; + let k = keys(d); + printf("%.J\n", [ length(k), type(k[0]), type(k[1]), type(k[2]) ]); +%} +-- End -- + +-- Expect stdout -- +[ + 3, + "int", + "bool", + "string" +] +-- End -- + + +13. values() on dict. + +-- Testcase -- +{% + let d = dict(); + d[1] = "a"; + d[2] = "b"; + d[3] = "c"; + printf("%.J\n", values(d)); +%} +-- End -- + +-- Expect stdout -- +[ + "a", + "b", + "c" +] +-- End -- + + +14. exists() on dict with value keys. + +-- Testcase -- +{% + let d = dict(); + d[42] = "answer"; + d["foo"] = "bar"; + d[true] = "yes"; + printf("%.J\n", [ + exists(d, 42), + exists(d, "foo"), + exists(d, true), + exists(d, "missing") + ]); +%} +-- End -- + +-- Expect stdout -- +[ + true, + true, + true, + false +] +-- End -- + + +15. length() on dict. + +-- Testcase -- +{% + let d = dict(); + print(length(d), "\n"); + d[1] = "a"; + print(length(d), "\n"); + d[2] = "b"; + d[3] = "c"; + print(length(d), "\n"); +%} +-- End -- + +-- Expect stdout -- +0 +1 +3 +-- End -- + + +16. Spread dict into object (value keys converted to strings). + +-- Testcase -- +{% + let d = dict(); + d[true] = "yes"; + d[42] = "answer"; + d["foo"] = "bar"; + let o = { prefix: 1, ...d, suffix: 2 }; + printf("%.J\n", [ + o["true"], o["42"], o["foo"], + o["prefix"], o["suffix"] + ]); +%} +-- End -- + +-- Expect stdout -- +[ + "yes", + "answer", + "bar", + 1, + 2 +] +-- End -- + + +17. Spread object into dict (string keys preserved as string values). + +-- Testcase -- +{% + let o = { a: 1, b: 2, c: 3 }; + let d = dict(o); + printf("%.J\n", [ length(d), d["a"], d["b"], d["c"] ]); +%} +-- End -- + +-- Expect stdout -- +[ + 3, + 1, + 2, + 3 +] +-- End -- + + +18. Spread dict into dict. + +-- Testcase -- +{% + let d1 = dict(); + d1[99] = "ninety-nine"; + d1[true] = "yes"; + let d2 = dict(d1); + d2[100] = "hundred"; + printf("%.J\n", [ length(d2), d2[99], d2[true], d2[100] ]); +%} +-- End -- + +-- Expect stdout -- +[ + 3, + "ninety-nine", + "yes", + "hundred" +] +-- End -- + + +19. for...in iteration yields value keys. + +-- Testcase -- +{% + let d = dict(); + d["a"] = 1; + d["b"] = 2; + d["c"] = 3; + let collected = []; + + for (let k in d) + push(collected, k); + + printf("%.J\n", collected); +%} +-- End -- + +-- Expect stdout -- +[ + "a", + "b", + "c" +] +-- End -- + + +20. for...in iteration with non-string keys. + +-- Testcase -- +{% + let d = dict(); + d[10] = "x"; + d[20] = "y"; + let sum = 0; + + for (let k in d) + sum += k; + + print(sum, "\n"); +%} +-- End -- + +-- Expect stdout -- +30 +-- End -- + + +21. Dict with prototype chain (regular object proto). + +-- Testcase -- +{% + let d = dict(); + let p = { shared: "value" }; + proto(d, p); + print(d.shared, "\n"); +%} +-- End -- + +-- Expect stdout -- +value +-- End -- + + +22. Dict with dict prototype. + +-- Testcase -- +{% + let pd = dict(); + pd[true] = "from-prototype"; + pd[42] = "answer"; + let d = dict(); + proto(d, pd); + printf("%.J\n", [ d[true], d[42], length(d) ]); +%} +-- End -- + +-- Expect stdout -- +[ + "from-prototype", + "answer", + 0 +] +-- End -- + + +23. Dict value increment and decrement. + +-- Testcase -- +{% + let d = dict(); + d["n"] = 10; + d["n"]++; + d["n"] += 5; + d["n"]--; + d["n"] -= 3; + print(d["n"], "\n"); +%} +-- End -- + +-- Expect stdout -- +12 +-- End -- + + +24. Dict GC stress test (no leaks with many dicts and keys). + +-- Testcase -- +{% + for (let i = 0; i < 1000; i++) { + let d = dict(); + for (let j = 0; j < 50; j++) { + d[j] = j * 2; + d["k" + j] = "str-" + j; + d[true] = "bool"; + } + } + + gc(); + print("ok\n"); +%} +-- End -- + +-- Expect stdout -- +ok +-- End -- + + +25. Dict JSON serialization (value keys converted to JSON strings). + +-- Testcase -- +{% + let d = dict(); + d[true] = "yes"; + d[42] = "answer"; + d["foo"] = "bar"; + d[null] = "nil"; + printf("%.J\n", d); +%} +-- End -- + +-- Expect stdout -- +{ + "true": "yes", + "42": "answer", + "foo": "bar", + "null": "nil" +} +-- End -- + + +26. Dict plain stringification (computed property expressions). + +-- Testcase -- +{% + let d = dict(); + d[true] = "yes"; + d[42] = "answer"; + d["foo"] = "bar"; + print(d, "\n"); +%} +-- End -- + +-- Expect stdout -- +{ [true]: "yes", [42]: "answer", ["foo"]: "bar" } +-- End -- + + +27. Dict with complex keys serializes correctly. + +-- Testcase -- +{% + let d = dict(); + let a = [ 1, 2 ]; + d[a] = "array-key"; + d[3.14] = "pi"; + printf("%.J\n", d); +%} +-- End -- + +-- Expect stdout -- +{ + "[ 1, 2 ]": "array-key", + "3.14": "pi" +} +-- End -- diff --git a/tests/custom/99_debugger/run_debugger_tests.uc b/tests/custom/99_debugger/run_debugger_tests.uc new file mode 100644 index 00000000..5547b584 --- /dev/null +++ b/tests/custom/99_debugger/run_debugger_tests.uc @@ -0,0 +1,874 @@ +#!/usr/bin/env -S ucode -S + +// Debugger Protocol Test Runner (Standalone) +// =========================================== +// Standalone test runner for the debugger's line-based protocol +// (see lib/debug_proto.h) that doesn't rely on cram-style infrastructure. +// +// Each test starts the target script unmodified via `-X1` (attach mode with +// an initial breakpoint at line 1 - the same mechanism the interactive `-x` +// CLI and SIGUSR1 attach use), connects to the resulting PID-derived attach +// socket directly with the `socket` module, sends a batch of protocol +// messages, and asserts on the *parsed* response messages and/or the target +// script's own stdout - not on rendered ANSI text, since the server no +// longer renders anything. +// +// `-X1` (rather than `debug.listen(path)`, which drives the whole session +// from *inside* a nested native call and needs `uc_vm_resume()` to hand +// control back to the VM's bytecode loop) matters here, not just for +// realism: breakpoints set *during* a debug.listen() session and hit via a +// later CONTINUE don't reliably re-fire through that nested-resume path, +// whereas `-X`/`-x`'s breakpoint-loop-driven pause (bk_enter_session invoked +// directly from the VM's per-instruction dispatch, see vm.c +// uc_vm_decode_insn()) does not have this problem - every test below that +// needs a *second* pause after a CONTINUE relies on this. + +import * as fs from 'fs'; +import * as sock from 'socket'; + +let testdir = sourcepath(0, true); +let topdir = fs.realpath(`${testdir}/..`); +let tmpdir = '/tmp/debugger_test.' + system('echo $$'); + +// UCODE_BIN may be a plain executable path, or (as set by the "custom"/ +// "debugger" ctest targets) a full command line like +// "valgrind --quiet --leak-check=full /path/to/ucode" that needs to be +// invoked word-split, not treated as a single path - so fs.dirname(ucode_bin) +// would be nonsense for such a value. UCODE_LIB is set alongside it by those +// same ctest targets specifically to give the correct library directory +// without needing to parse UCODE_BIN at all. +let ucode_bin = getenv('UCODE_BIN') || '/home/jow/devel/ucode.git/build/ucode'; +let libdir = getenv('UCODE_LIB') || fs.dirname(ucode_bin); + +function shq(s) { + return `'${replace(s, "'", "'\\''")}'`; +} + +let n_tests = 0; +let n_passed = 0; +let n_failed = 0; +let n_crashed = 0; +let n_timeout = 0; + +// Generous enough to tolerate running under valgrind --leak-check=full +// (the "custom"/"debugger" ctest targets always do), which can slow +// process startup and breakpoint hits down substantially. +let TEST_TIMEOUT = 30; + +function mkdir_p(path) { + let parts = split(rtrim(path, '/') || '/', /\/+/); + let current = ''; + for (let part in parts) { + current += part + '/'; + if (!fs.access(current)) { + fs.mkdir(current); + } + } +} + +// Connect to the given Unix domain socket path, retrying for a bit while +// the target process is still starting up / hasn't armed its attach socket +// yet. +function connect_retry(path, timeout_sec) { + let deadline = time() + timeout_sec; + + while (time() < deadline) { + let conn = sock.connect({ family: sock.AF_UNIX, path }); + + if (conn) { + // Bound recv() below so a quiet socket never blocks forever. + conn.setopt(sock.SOL_SOCKET, sock.SO_RCVTIMEO, { sec: 0, usec: 20000 }); + return conn; + } + + system('sleep 0.05'); + } + + return null; +} + +// Read every complete "VERB [json]" line already available on `conn` right +// now (non-blocking-ish: short poll loop), parsing each into +// { verb, payload }. Stops once nothing new arrives for a short quiet +// period, since responses may legitimately be a variable-length burst +// (e.g. BREAK + BREAKPOINT_ADDED, then a later EVENT exit). +function drain_messages(conn, quiet_ms) { + let messages = []; + let buf = ''; + let idle = 0; + + while (idle < quiet_ms) { + let chunk = conn.recv(65536); + + if (chunk == null || chunk == '') { + idle += 20; + system('sleep 0.02'); + continue; + } + + idle = 0; + buf += chunk; + + let nl; + while ((nl = index(buf, "\n")) >= 0) { + let line = substr(buf, 0, nl); + buf = substr(buf, nl + 1); + + if (line == '') + continue; + + let sp = index(line, ' '); + let verb = (sp >= 0) ? substr(line, 0, sp) : line; + let payload = (sp >= 0) ? json(substr(line, sp + 1)) : null; + + push(messages, { verb, payload }); + } + } + + return messages; +} + +// Run `source_code` unmodified via `-X1` (attach mode, breaking at line 1) +// with a batch of protocol messages sent all at once - pacing doesn't +// matter since the server processes them strictly in order off its +// blocking read loop regardless of when they were written, and these tests +// only care about the final observable state (script stdout + which +// responses came back), not interactive timing. +function run_debugger(source_code, steps, timeout_sec) { + if (timeout_sec == null) timeout_sec = TEST_TIMEOUT; + mkdir_p(tmpdir); + + let source_file = `${tmpdir}/source.uc`; + let stdout_file = `${tmpdir}/stdout.out`; + let stderr_file = `${tmpdir}/stderr.err`; + let wrapper_file = `${tmpdir}/wrapper.sh`; + let pid_file = `${tmpdir}/pid`; + + fs.writefile(source_file, source_code); + fs.unlink(pid_file); + + // Runs the target in the background (recording its real PID, *not* + // some wrapping shell's) and waits for it, so this test process can + // concurrently drive the PID-derived attach socket while the target is + // paused at its line-1 breakpoint. Enforces its own timeout by killing + // the PID directly rather than via `timeout`, since `timeout` would be + // the one owning the PID `$!` reports otherwise. + // resolve_breakpoint() can't default the path from a current frame + // before the program has started running (there is none yet), so the + // pre-execution `-X` breakpoint spec needs an explicit "path:line" + // rather than a bare line number. + // `ucode_bin` is deliberately left unquoted below: under the "custom"/ + // "debugger" ctest targets it is itself a multi-word command + // ("valgrind --quiet --leak-check=full /path/ucode") that needs to be + // word-split so valgrind sees its own flags, not one opaque argument. + fs.writefile(wrapper_file, sprintf( + 'cd %s\n' + + 'export LD_LIBRARY_PATH=%s\n' + + '%s -L %s -X%s:1 %s > %s 2> %s &\n' + + 'echo $! > %s\n' + + 'wait $!\n' + + 'echo "EXIT:$?"\n', + shq(topdir), shq(libdir), ucode_bin, shq(libdir), shq(source_file), shq(source_file), + shq(stdout_file), shq(stderr_file), shq(pid_file) + )); + + let proc = fs.popen(`sh ${wrapper_file}`, 'r'); + let deadline = time() + timeout_sec; + let pid = null; + + while (time() < deadline && !pid) { + if (fs.access(pid_file)) { + let s = trim(fs.readfile(pid_file) ?? ''); + if (s != '') + pid = int(s); + } + + if (!pid) + system('sleep 0.02'); + } + + let sock_path = pid ? sprintf('/tmp/ucode-debug-%d.sock', pid) : null; + let conn = sock_path ? connect_retry(sock_path, timeout_sec) : null; + let messages = []; + + if (conn) { + // First message is always the initial PAUSED (from the -X1 + // breakpoint at line 1). + for (let msg in drain_messages(conn, 400)) + push(messages, msg); + + let lines = []; + for (let step in steps) + push(lines, step); + // Let anything already paused (including the initial -X1 pause, + // for tests with no steps of their own) run to completion first; + // QUIT is just a safety net in case something is still paused + // afterward (a harmless no-op otherwise, since the connection is + // already gone by the time it'd be read). + push(lines, 'CONTINUE'); + push(lines, 'QUIT'); + + conn.send(join("\n", lines) + "\n"); + + for (let msg in drain_messages(conn, 800)) + push(messages, msg); + + conn.close(); + } + else if (pid) { + // Never connected (e.g. no attach socket appeared) - don't leave + // the target hanging around forever. + system(`kill -9 ${pid} 2>/dev/null`); + } + + let wrapper_out = proc.read('all') ?? ''; + proc.close(); + + let exitcode = -1; + let m = match(wrapper_out, /EXIT:(-?[0-9]+)/); + if (m) exitcode = int(m[1]); + + let stdout = fs.access(stdout_file) ? fs.readfile(stdout_file) ?? '' : ''; + let stderr = fs.access(stderr_file) ? fs.readfile(stderr_file) ?? '' : ''; + + let timed_out = (exitcode == -1 && !conn); + + return { stdout, stderr, exitcode, timed_out, messages, connected: !!conn }; +} + +// True if any received message has the given verb (optionally further +// filtered by a predicate over its payload). +function has_message(messages, verb, pred) { + for (let msg in messages) { + if (msg.verb != verb) + continue; + + if (!pred || pred(msg.payload)) + return true; + } + + return false; +} + +function run_test(name, source_code, steps, expectations) { + n_tests++; + + let result = run_debugger(source_code, steps); + let failed = false; + let exp = expectations ?? {}; + + if (exp.must_connect && !result.connected) { + printf("FAIL %s: could not connect to debug socket\n", name); + n_failed++; + n_crashed++; + return false; + } + + if (result.exitcode < 0 && result.exitcode != -1) { + if (exp.no_crash) { + printf("FAIL %s: crashed (exit code %d)\n", name, result.exitcode); + printf(" stderr: %s\n", substr(result.stderr, 0, 200)); + n_failed++; + n_crashed++; + return false; + } + } + + if (result.timed_out) { + if (exp.no_timeout) { + printf("FAIL %s: timed out after %ds\n", name, TEST_TIMEOUT); + n_failed++; + n_timeout++; + return false; + } + } + + if (exp.stdout_contains) { + for (let pattern in exp.stdout_contains) { + let re = (type(pattern) == 'string') ? regexp(pattern) : pattern; + if (!match(result.stdout, re)) { + printf("FAIL %s: stdout does not contain '%s'\n", name, pattern); + printf(" Got: %s\n", substr(result.stdout, 0, 200)); + failed = true; + } + } + } + + if (exp.messages_contain) { + for (let verb in exp.messages_contain) { + if (!has_message(result.messages, verb)) { + printf("FAIL %s: no '%s' response received\n", name, verb); + printf(" Got verbs: %s\n", join(', ', map(result.messages, (m) => m.verb))); + failed = true; + } + } + } + + if (exp.check) { + let msg = exp.check(result); + if (msg) { + printf("FAIL %s: %s\n", name, msg); + failed = true; + } + } + + if (!failed) { + printf("PASS %s\n", name); + n_passed++; + return true; + } + + n_failed++; + return false; +} + +// ============================================================================ +// TEST SUITES +// ============================================================================ + +function test_basic_breakpoint() { + printf("\n## Basic Breakpoint Tests\n\n"); + + run_test("break_at_line", + `print("hello"); +print("world"); +print("done");`, + ['BREAK {"spec":"2"}', 'CONTINUE'], + { messages_contain: ['PAUSED', 'BREAKPOINT_ADDED'], no_crash: true } + ); + + run_test("break_function", + `function test() { + print("in test"); +} +test();`, + ['BREAK {"spec":"test"}', 'CONTINUE'], + { messages_contain: ['PAUSED', 'BREAKPOINT_ADDED'], no_crash: true } + ); + + run_test("break_multiple", + `print("a"); +print("b"); +print("c");`, + ['BREAK {"spec":"1"}', 'BREAK {"spec":"2"}', 'LIST_BREAKPOINTS', 'CONTINUE'], + { + messages_contain: ['BREAKPOINTS'], + no_crash: true, + check: (r) => { + let bp = null; + for (let m in r.messages) + if (m.verb == 'BREAKPOINTS') bp = m.payload; + if (!bp || length(bp.items) < 2) + return "expected at least 2 breakpoints listed"; + return null; + } + } + ); + + run_test("delete_breakpoint", + `print("a"); +print("b");`, + ['BREAK {"spec":"1"}', 'DELETE {"id":1}', 'LIST_BREAKPOINTS', 'CONTINUE'], + { messages_contain: ['OK'], no_crash: true } + ); +} + +function test_execution_control() { + printf("\n## Execution Control Tests\n\n"); + + run_test("step_command", + `let x = 1; +let y = 2; +let z = x + y;`, + ['STEP', 'STEP', 'STEP', 'CONTINUE'], + { must_connect: true, no_crash: true } + ); + + run_test("next_command", + `function inner() { return 1; } +function outer() { return inner() + 1; } +outer();`, + ['BREAK {"spec":"outer"}', 'CONTINUE', 'NEXT', 'NEXT', 'CONTINUE'], + { must_connect: true, no_crash: true } + ); + + run_test("continue_command", + `print("a"); +print("b"); +print("c");`, + ['BREAK {"spec":"2"}', 'CONTINUE', 'CONTINUE'], + { stdout_contains: ['a', 'b', 'c'], no_crash: true } + ); + + run_test("return_command", + `function inner() { return 1; } +function outer() { return inner() + 1; } +outer();`, + ['BREAK {"spec":"inner"}', 'CONTINUE', 'RETURN', 'CONTINUE'], + { must_connect: true, no_crash: true } + ); + + run_test("quit_command", + `print("a"); +print("b"); +print("c");`, + [], + { no_crash: true } + ); +} + +function test_variable_inspection() { + printf("\n## Variable Inspection Tests\n\n"); + + // STEP (rather than BREAK+CONTINUE to a line number) advances past the + // declarations here: resolving a bare line-number breakpoint against a + // script that is only variable declarations is a pre-existing, narrow + // edge case in resolve_breakpoint()/lookup_stmt_boundary() (unrelated + // to the protocol) that single-instruction stepping avoids entirely. + run_test("print_simple_var", + `let x = 42; +let y = "hello"; +1;`, + ['STEP', 'STEP', 'PRINT {"expr":"x"}', 'PRINT {"expr":"y"}', 'CONTINUE'], + { + no_crash: true, + check: (r) => { + let vals = []; + for (let m in r.messages) + if (m.verb == 'VALUE') push(vals, m.payload.repr); + if (!vals[0] || index(vals[0], '42') < 0) return "expected 42 in first VALUE"; + if (!vals[1] || index(vals[1], 'hello') < 0) return "expected hello in second VALUE"; + return null; + } + } + ); + + run_test("print_expression", + `let a = 10; +let b = 20; +print(a + b);`, + ['STEP', 'STEP', 'PRINT {"expr":"a + b"}', 'CONTINUE'], + { + no_crash: true, + check: (r) => { + for (let m in r.messages) + if (m.verb == 'VALUE' && index(m.payload.repr, '30') >= 0) return null; + return "expected VALUE containing 30"; + } + } + ); + + run_test("print_object", + `let obj = { foo: "bar", num: 123 }; +1;`, + ['STEP', 'PRINT {"expr":"obj"}', 'CONTINUE'], + { + no_crash: true, + check: (r) => { + for (let m in r.messages) + if (m.verb == 'VALUE' && index(m.payload.repr, 'foo') >= 0 && index(m.payload.repr, 'bar') >= 0) + return null; + return "expected VALUE containing foo/bar"; + } + } + ); + + run_test("print_array", + `let arr = [1, 2, 3, 4, 5]; +1;`, + ['STEP', 'PRINT {"expr":"arr"}', 'CONTINUE'], + { + no_crash: true, + check: (r) => { + for (let m in r.messages) + if (m.verb == 'VALUE' && index(m.payload.repr, '1') >= 0 && index(m.payload.repr, '3') >= 0) + return null; + return "expected VALUE containing array elements"; + } + } + ); + + run_test("variables_command", + `let x = 1; +let y = 2; +let z = 3;`, + ['VARIABLES'], + { messages_contain: ['VARIABLES'], no_crash: true } + ); + + run_test("print_nested", + `let obj = { nested: { deep: "value" } }; +1;`, + ['STEP', 'PRINT {"expr":"obj.nested.deep"}', 'CONTINUE'], + { + no_crash: true, + check: (r) => { + for (let m in r.messages) + if (m.verb == 'VALUE' && index(m.payload.repr, 'value') >= 0) return null; + return "expected VALUE containing 'value'"; + } + } + ); +} + +function test_stack_tracing() { + printf("\n## Stack Tracing Tests\n\n"); + + run_test("backtrace_simple", + `function level3() { return 3; } +function level2() { return level3(); } +function level1() { return level2(); } +level1();`, + ['BREAK {"spec":"level3"}', 'CONTINUE', 'BACKTRACE {}', 'CONTINUE'], + { + no_crash: true, + check: (r) => { + for (let m in r.messages) { + if (m.verb != 'BACKTRACE') continue; + let names = join(',', map(m.payload.frames, (f) => f.function ?? '')); + if (index(names, 'level3') >= 0 && index(names, 'level2') >= 0 && index(names, 'level1') >= 0) + return null; + } + return "expected backtrace with level1/2/3"; + } + } + ); + + run_test("backtrace_full", + `function callee() { return 1; } +function caller() { return callee(); } +caller();`, + ['BREAK {"spec":"callee"}', 'CONTINUE', 'BACKTRACE {"full":true}', 'CONTINUE'], + { + no_crash: true, + check: (r) => { + for (let m in r.messages) { + if (m.verb != 'BACKTRACE') continue; + let names = join(',', map(m.payload.frames, (f) => f.function ?? '')); + if (index(names, 'callee') >= 0 && index(names, 'caller') >= 0) + return null; + } + return "expected backtrace with callee/caller"; + } + } + ); + + run_test("bt_alias", + `print("test");`, + ['BREAK {"spec":"1"}', 'CONTINUE', 'BACKTRACE {}', 'CONTINUE'], + { messages_contain: ['BACKTRACE'], no_crash: true } + ); +} + +function test_source_view() { + printf("\n## Source Viewing Tests\n\n"); + + run_test("lines_current", + `// line 1 +// line 2 +// line 3 +print("test");`, + // A bare, comment-only ":line" spec resolves to the next real + // statement (there is no bytecode to break on within a comment), + // exactly like -X1's own initial breakpoint already demonstrates. + ['BREAK {"spec":"1"}', 'CONTINUE', 'LINES {}', 'CONTINUE'], + { messages_contain: ['SOURCE_RANGE'], no_crash: true } + ); + + run_test("lines_with_context", + `// 1 +// 2 +// 3 +// 4 +// 5 +print("test");`, + ['BREAK {"spec":"1"}', 'CONTINUE', 'LINES {"before":3,"after":1}', 'CONTINUE'], + { messages_contain: ['SOURCE_RANGE'], no_crash: true } + ); + + run_test("sources_command", + `print("test");`, + ['BREAK {"spec":"1"}', 'CONTINUE', 'SOURCES', 'CONTINUE'], + { messages_contain: ['SOURCES'], no_crash: true } + ); + + run_test("source_fetch", + `print("test");`, + ['LINES {}'], + { + no_crash: true, + check: (r) => { + let file = null; + for (let m in r.messages) + if (m.verb == 'SOURCE_RANGE') file = m.payload.file; + if (!file) return "no SOURCE_RANGE received"; + return null; + } + } + ); +} + +function test_disassembly() { + printf("\n## Disassembly Tests\n\n"); + + run_test("disasm_current", + `let x = 1 + 2;`, + ['BREAK {"spec":"1"}', 'CONTINUE', 'DISASSEMBLE', 'CONTINUE'], + { + no_crash: true, + check: (r) => { + for (let m in r.messages) + if (m.verb == 'DISASSEMBLY' && length(m.payload.instructions) > 0) return null; + return "expected non-empty DISASSEMBLY"; + } + } + ); + + run_test("disasm_function", + `function test() { + return 42; +} +test();`, + ['BREAK {"spec":"test"}', 'CONTINUE', 'DISASSEMBLE {"spec":"test"}', 'CONTINUE'], + { + no_crash: true, + check: (r) => { + for (let m in r.messages) + if (m.verb == 'DISASSEMBLY' && m.payload.function == 'test') return null; + return "expected DISASSEMBLY for function 'test'"; + } + } + ); +} + +function test_help_and_misc() { + printf("\n## Help and Miscellaneous Tests\n\n"); + + run_test("help_command", + `print("test");`, + ['BREAK {"spec":"1"}', 'CONTINUE', 'HELP', 'CONTINUE'], + { + no_crash: true, + check: (r) => { + for (let m in r.messages) { + if (m.verb != 'HELP') continue; + let verbs = join(',', map(m.payload.commands, (c) => c.verb)); + if (index(verbs, 'BREAK') >= 0 && index(verbs, 'CONTINUE') >= 0 && + index(verbs, 'STEP') >= 0 && index(verbs, 'NEXT') >= 0) + return null; + } + return "expected HELP listing BREAK/CONTINUE/STEP/NEXT"; + } + } + ); + + run_test("list_command", + `print("a"); +print("b");`, + ['BREAK {"spec":"1"}', 'BREAK {"spec":"2"}', 'LIST_BREAKPOINTS', 'CONTINUE'], + { + no_crash: true, + check: (r) => { + for (let m in r.messages) + if (m.verb == 'BREAKPOINTS' && length(m.payload.items) >= 2) return null; + return "expected at least 2 listed breakpoints"; + } + } + ); + + run_test("invalid_command", + `print("test");`, + ['BREAK {"spec":"1"}', 'CONTINUE', 'BOGUS', 'CONTINUE'], + { messages_contain: ['ERROR'], no_crash: true } + ); +} + +function test_debug_api() { + printf("\n## Debug API Tests\n\n"); + + run_test("traceback_function", + `function level3() { return debug.traceback(); } +function level2() { return level3(); } +function level1() { return level2(); } +let result = level1(); +print("done"); +print(result);`, + [], + { stdout_contains: ['done', 'level3', 'level2', 'level1'], no_crash: true } + ); + + run_test("sourcepos_function", + `function test() { + let pos = debug.sourcepos(); + print("line", pos.line); +} +test();`, + [], + { stdout_contains: ['line'], no_crash: true } + ); + + run_test("getinfo_function", + `function test() { return 1; } +let info = debug.getinfo(test); +print("done");`, + [], + { stdout_contains: ['done'], no_crash: true } + ); + + run_test("debugger_api", + `function test() { + print("inside test"); +} +test(); +print("after");`, + ['BREAK {"spec":"test"}', 'CONTINUE', 'CONTINUE'], + { stdout_contains: ['inside test', 'after'], no_crash: true } + ); +} + +function test_edge_cases() { + printf("\n## Edge Cases and Bug Tests\n\n"); + + run_test("empty_commands", + `print("test");`, + [], + { no_crash: true } + ); + + run_test("rapid_breakpoints", + `print("a"); +print("b"); +print("c"); +print("d"); +print("e");`, + ['BREAK {"spec":"1"}', 'BREAK {"spec":"2"}', 'BREAK {"spec":"3"}', 'BREAK {"spec":"4"}', + 'BREAK {"spec":"5"}', 'LIST_BREAKPOINTS', 'CONTINUE'], + { no_crash: true } + ); + + run_test("invalid_breakpoint", + `print("test");`, + ['BREAK {"spec":"999"}', 'CONTINUE'], + { no_crash: true } + ); + + run_test("delete_invalid", + `print("test");`, + ['DELETE {"id":999}', 'CONTINUE'], + { messages_contain: ['ERROR'], no_crash: true } + ); + + run_test("print_undefined", + `print("test");`, + ['BREAK {"spec":"1"}', 'CONTINUE', 'PRINT {"expr":"undefined_var"}', 'CONTINUE'], + { no_crash: true } + ); + + run_test("deep_recursion", + `function recurse(n) { + if (n <= 0) return 0; + return recurse(n - 1) + 1; +} +recurse(100);`, + ['BREAK {"spec":"recurse"}', 'CONTINUE'], + { no_crash: true, no_timeout: true } + ); + + run_test("large_object", + `let obj = {}; +for (let i = 0; i < 100; i++) { + obj["key" + i] = i; +}`, + ['BREAK {"spec":"1"}', 'CONTINUE', 'PRINT {"expr":"obj"}', 'CONTINUE'], + { no_crash: true } + ); + + run_test("closure_upvalues", + `function makeCounter() { + let count = 0; + return function() { count++; return count; }; +} +let counter = makeCounter(); +counter();`, + ['BREAK {"spec":"counter"}', 'CONTINUE', 'PRINT {"expr":"counter()"}', 'CONTINUE'], + { no_crash: true } + ); + + run_test("exception_in_debug", + `try { + die("test error"); +} catch (e) { + print("caught"); +}`, + // One CONTINUE for the initial -X1 pause, one more for the + // automatic BK_CATCH pause the try/catch's die() triggers. + ['CONTINUE'], + { stdout_contains: ['caught'], no_crash: true } + ); +} + +function test_memory_safety() { + printf("\n## Memory Safety Tests\n\n"); + + run_test("repeated_inspection", + `let x = 1; +let y = 2; +let z = 3;`, + ['BREAK {"spec":"1"}', 'CONTINUE', 'PRINT {"expr":"x"}', 'PRINT {"expr":"y"}', + 'PRINT {"expr":"z"}', 'PRINT {"expr":"x"}', 'PRINT {"expr":"y"}', 'CONTINUE'], + { no_crash: true } + ); + + run_test("disasm_variants", + `let a = 1; +let b = "str"; +let c = [1, 2, 3]; +let d = { x: 1 }; +function f() { return 1; }`, + ['BREAK {"spec":"1"}', 'CONTINUE', 'DISASSEMBLE', 'DISASSEMBLE {"spec":"f"}', 'CONTINUE'], + { no_crash: true } + ); + + run_test("mixed_frames", + `replace("test", "t", function(m) { + return m.toUpperCase(); +});`, + [], + { no_crash: true } + ); +} + +// ============================================================================ +// MAIN +// ============================================================================ + +printf('\n##\n## Running Debugger Tests\n##\n\n'); + +try { + mkdir_p(tmpdir); + + test_basic_breakpoint(); + test_execution_control(); + test_variable_inspection(); + test_stack_tracing(); + test_source_view(); + test_disassembly(); + test_help_and_misc(); + test_debug_api(); + test_edge_cases(); + test_memory_safety(); +} +catch (e) { + warn(`Test runner error: ${e.type}: ${e.message}\n${e.stacktrace[0].context}\n`); +} + +// Cleanup +system(['rm', '-rf', tmpdir]); + +printf('\n##\n## Test Summary\n##\n\n'); +printf('Ran %d tests: %d passed, %d failed', n_tests, n_passed, n_failed); +if (n_crashed > 0) printf(' (%d crashes)', n_crashed); +if (n_timeout > 0) printf(' (%d timeouts)', n_timeout); +printf('\n'); + +exit(n_failed > 0 ? 1 : 0); diff --git a/tests/custom/CMakeLists.txt b/tests/custom/CMakeLists.txt index c94278e0..6907956d 100644 --- a/tests/custom/CMakeLists.txt +++ b/tests/custom/CMakeLists.txt @@ -20,3 +20,27 @@ IF(CMAKE_C_COMPILER_ID STREQUAL "Clang") "UCODE_LIB=${CMAKE_BINARY_DIR}" ) ENDIF() + +# Debugger tests +ADD_TEST( + NAME debugger + COMMAND $ -L $/*.so -S 99_debugger/run_debugger_tests.uc + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) +SET_PROPERTY(TEST debugger APPEND PROPERTY ENVIRONMENT + "UCODE_BIN=valgrind --quiet --leak-check=full $" + "UCODE_LIB=${CMAKE_BINARY_DIR}" +) + +IF(CMAKE_C_COMPILER_ID STREQUAL "Clang") + ADD_TEST( + NAME debugger-san + COMMAND $ -L $/*.so -S 99_debugger/run_debugger_tests.uc + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + + SET_PROPERTY(TEST debugger-san APPEND PROPERTY ENVIRONMENT + "UCODE_BIN=$" + "UCODE_LIB=${CMAKE_BINARY_DIR}" + ) +ENDIF() diff --git a/types.c b/types.c index f66ef40a..6ebeb86c 100644 --- a/types.c +++ b/types.c @@ -188,8 +188,16 @@ ucv_gc_mark(uc_value_t *uv) ucv_gc_mark(object->proto); - lh_foreach(object->table, entry) - ucv_gc_mark((uc_value_t *)lh_entry_v(entry)); + if (ucv_is_dict(uv)) { + /* dict keys are uc_value_t* and must be GC'd */ + lh_foreach(object->table, entry) { + ucv_gc_mark((uc_value_t *)lh_entry_k(entry)); + ucv_gc_mark((uc_value_t *)lh_entry_v(entry)); + } + } else { + lh_foreach(object->table, entry) + ucv_gc_mark((uc_value_t *)lh_entry_v(entry)); + } break; @@ -1193,6 +1201,299 @@ ucv_object_length(uc_value_t *uv) } +/* --------------------------------------------------------------------------- + * Dict (value-key object) implementation + * + * Dicts are objects where keys are arbitrary ucode values rather than + * null-terminated strings. Uniqueness semantics follow uc_uniq(): + * - Scalars (null, bool, int, double, string): value equality + * - Non-scalars (arrays, objects, resources, closures): pointer equality + * - NaN doubles are treated as equal for hashing purposes + * --------------------------------------------------------------------------- */ + +static void +ucv_free_dict_entry(struct lh_entry *entry) +{ + /* update iterator positions affected by entry deletion */ + uc_list_foreach(item, &uc_thread_context_get()->object_iterators) { + uc_object_iterator_t *iter = (uc_object_iterator_t *)item; + + if (iter->u.pos == entry) + iter->u.pos = entry->next; + } + + /* keys are uc_value_t pointers — release the reference */ + ucv_put((uc_value_t *)lh_entry_k(entry)); + ucv_put(lh_entry_v(entry)); +} + +static unsigned long +uc_dict_hash(const void *k) +{ + union { double d; int64_t i; uint64_t u; } conv; + uc_value_t *uv = (uc_value_t *)k; + unsigned int h; + uint8_t *u8; + size_t len; + + h = ucv_type(uv); + + switch (h) { + case UC_STRING: + u8 = (uint8_t *)ucv_string_get(uv); + len = ucv_string_length(uv); + if (!u8) + len = 0; + break; + + case UC_INTEGER: + conv.i = ucv_int64_get(uv); + + if (errno == ERANGE) { + h *= 2; + conv.u = ucv_uint64_get(uv); + } + + u8 = (uint8_t *)&conv.u; + len = sizeof(conv.u); + break; + + case UC_DOUBLE: + conv.d = ucv_double_get(uv); + + u8 = (uint8_t *)&conv.u; + len = sizeof(conv.u); + break; + + default: + u8 = (uint8_t *)&uv; + len = sizeof(uv); + break; + } + + while (len > 0) { + h = h * 129 + (*u8++) + LH_PRIME; + len--; + } + + return h; +} + +int +uc_dict_equal(const void *k1, const void *k2) +{ + uc_value_t *uv1 = (uc_value_t *)k1; + uc_value_t *uv2 = (uc_value_t *)k2; + + /* non-scalar keys use pointer equality */ + if (!ucv_is_scalar(uv1) && !ucv_is_scalar(uv2)) + return (uv1 == uv2); + + /* treat two NaNs as equal for dict key lookup */ + if (ucv_type(uv1) == UC_DOUBLE && ucv_type(uv2) == UC_DOUBLE && + isnan(ucv_double_get(uv1)) && isnan(ucv_double_get(uv2))) + return true; + + return ucv_is_equal(uv1, uv2); +} + +uc_value_t * +ucv_dict_new(uc_vm_t *vm, uc_value_t *src) +{ + struct lh_table *table; + uc_object_t *dict; + unsigned long hash; + size_t i; + + table = lh_table_new(16, ucv_free_dict_entry, uc_dict_hash, uc_dict_equal); + + if (!table) { + fprintf(stderr, "Out of memory\n"); + abort(); + } + + dict = xalloc(sizeof(*dict)); + dict->header.type = UC_OBJECT; + dict->header.refcount = 1; + dict->table = table; + dict->proto = NULL; + dict->ref.prev = NULL; + dict->ref.next = NULL; + + /* initialize from source object or dict */ + if (src) { + if (ucv_is_dict(src)) { + ucv_dict_foreach(src, k, v) { + hash = lh_get_hash(dict->table, k); + lh_table_insert_w_hash(dict->table, ucv_get(k), ucv_get(v), hash, 0); + } + } else if (ucv_type(src) == UC_OBJECT) { + ucv_object_foreach(src, k, v) { + uc_value_t *key = ucv_string_new(k); + + hash = lh_get_hash(dict->table, key); + lh_table_insert_w_hash(dict->table, key, ucv_get(v), hash, 0); + } + } else if (ucv_type(src) == UC_ARRAY) { + for (i = 0; i < ucv_array_length(src); i++) { + uc_value_t *key = ucv_int64_new((int64_t)i); + uc_value_t *val = ucv_get(ucv_array_get(src, i)); + + hash = lh_get_hash(dict->table, key); + lh_table_insert_w_hash(dict->table, key, val, hash, 0); + } + } + } + + if (vm) { + ucv_ref(&vm->values, &dict->ref); + vm->alloc_refs++; + } + + return &dict->header; +} + +uc_value_t * +ucv_dict_get(uc_vm_t *vm, uc_value_t *dict, uc_value_t *key) +{ + uc_object_t *obj; + uc_value_t *val = NULL; + bool found; + + if (!ucv_is_dict(dict)) + return NULL; + + obj = (uc_object_t *)dict; + + /* try dict itself first */ + found = lh_table_lookup_ex(obj->table, key, (void **)&val); + + /* walk prototype chain if not found */ + if (!found) { + uc_value_t *proto; + + for (proto = obj->proto; proto; proto = ucv_prototype_get(proto)) { + if (ucv_type(proto) != UC_OBJECT) + continue; + + if (ucv_is_dict(proto)) { + uc_object_t *pro = (uc_object_t *)proto; + + if (lh_table_lookup_ex(pro->table, key, (void **)&val)) + break; + } else { + /* convert key to string for regular object lookup */ + char *s = ucv_to_string(vm, key); + + val = ucv_object_get(proto, s ? s : "", &found); + if (found) + break; + free(s); + } + } + } + + if (!val) + return NULL; + + return ucv_get(val); +} + +uc_value_t * +ucv_dict_set(uc_vm_t *vm, uc_value_t *dict, uc_value_t *key, uc_value_t *val) +{ + uc_object_t *obj; + struct lh_entry *existing; + unsigned long hash; + bool rehash; + (void)vm; + + if (!ucv_is_dict(dict)) + return NULL; + + if (ucv_is_constant(dict)) + return NULL; + + obj = (uc_object_t *)dict; + hash = lh_get_hash(obj->table, key); + existing = lh_table_lookup_entry_w_hash(obj->table, key, hash); + + if (existing) { + ucv_put((uc_value_t *)existing->v); + existing->v = val; + } else { + rehash = (obj->table->count >= obj->table->size * LH_LOAD_FACTOR); + + /* backup iterator states before potential rehash */ + if (rehash) { + uc_list_foreach(item, &uc_thread_context_get()->object_iterators) { + uc_object_iterator_t *iter = (uc_object_iterator_t *)item; + + if (iter->table != obj->table) + continue; + + if (iter->u.pos == NULL) + continue; + + iter->u.kh.k = iter->u.pos->k; + iter->u.kh.hash = lh_get_hash(iter->table, iter->u.kh.k); + } + } + + lh_table_insert_w_hash(obj->table, ucv_get(key), val, hash, 0); + + /* restore iterator states after rehash */ + if (rehash) { + uc_list_foreach(item, &uc_thread_context_get()->object_iterators) { + uc_object_iterator_t *iter = (uc_object_iterator_t *)item; + + if (iter->table != obj->table) + continue; + + if (iter->u.kh.k == NULL) + continue; + + iter->u.pos = lh_table_lookup_entry_w_hash(iter->table, + iter->u.kh.k, + iter->u.kh.hash); + } + } + } + + return ucv_get(val); +} + +bool +ucv_dict_delete(uc_vm_t *vm, uc_value_t *dict, uc_value_t *key) +{ + uc_object_t *obj; + (void)vm; + + if (!ucv_is_dict(dict)) + return false; + + if (ucv_is_constant(dict)) + return false; + + obj = (uc_object_t *)dict; + + return (lh_table_delete(obj->table, key) == 0); +} + +size_t +ucv_dict_length(uc_value_t *dict) +{ + uc_object_t *obj; + + if (!ucv_is_dict(dict)) + return 0; + + obj = (uc_object_t *)dict; + + return lh_table_length(obj->table); +} + + uc_value_t * ucv_cfunction_new(const char *name, uc_cfn_ptr_t fptr) { @@ -1635,8 +1936,17 @@ ucv_to_json(uc_value_t *uv) case UC_OBJECT: jso = json_object_new_object(); - ucv_object_foreach(uv, key, val) - json_object_object_add(jso, key, ucv_to_json(val)); + if (ucv_is_dict(uv)) { + ucv_dict_foreach(uv, key, val) { + char *s = ucv_to_string(NULL, key); + + json_object_object_add(jso, s ? s : "", ucv_to_json(val)); + free(s); + } + } else { + ucv_object_foreach(uv, key, val) + json_object_object_add(jso, key, ucv_to_json(val)); + } return jso; @@ -1897,14 +2207,37 @@ ucv_to_stringbuf_formatted(uc_vm_t *vm, uc_stringbuf_t *pb, uc_value_t *uv, size ucv_stringbuf_append(pb, "{"); i = 0; - ucv_object_foreach(uv, key, val) { - if (i++) - ucv_stringbuf_append(pb, ","); - - ucv_to_stringbuf_add_padding(pb, pad_char, (depth + 1) * pad_size); - ucv_to_string_json_encoded(pb, key, strlen(key), false); - ucv_stringbuf_append(pb, ": "); - ucv_to_stringbuf_formatted(vm, pb, val, depth + 1, pad_char ? pad_char : '\1', pad_size); + if (ucv_is_dict(uv)) { + ucv_dict_foreach(uv, key, val) { + if (i++) + ucv_stringbuf_append(pb, ","); + + ucv_to_stringbuf_add_padding(pb, pad_char, (depth + 1) * pad_size); + if (json) { + /* JSON mode: stringify value key to a JSON string */ + s = ucv_to_string(vm, key); + l = s ? strlen(s) : 0; + ucv_to_string_json_encoded(pb, s, l, false); + free(s); + } else { + /* plain mode: emit key as a computed property expression */ + ucv_stringbuf_append(pb, "["); + ucv_to_stringbuf_formatted(vm, pb, key, depth + 1, pad_char ? pad_char : '\1', pad_size); + ucv_stringbuf_append(pb, "]"); + } + ucv_stringbuf_append(pb, ": "); + ucv_to_stringbuf_formatted(vm, pb, val, depth + 1, pad_char ? pad_char : '\1', pad_size); + } + } else { + ucv_object_foreach(uv, key, val) { + if (i++) + ucv_stringbuf_append(pb, ","); + + ucv_to_stringbuf_add_padding(pb, pad_char, (depth + 1) * pad_size); + ucv_to_string_json_encoded(pb, key, strlen(key), false); + ucv_stringbuf_append(pb, ": "); + ucv_to_stringbuf_formatted(vm, pb, val, depth + 1, pad_char ? pad_char : '\1', pad_size); + } } ucv_to_stringbuf_add_padding(pb, pad_char, depth * pad_size); diff --git a/udbg.c b/udbg.c new file mode 100644 index 00000000..a645d03e --- /dev/null +++ b/udbg.c @@ -0,0 +1,1950 @@ +/* + * udbg - ucode debugger client + * + * Copyright (C) 2026 Jo-Philipp Wich + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ANY IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * --- + * + * Interactive client for ucode's line-based debug protocol (one uppercase + * VERB, optionally followed by a space and a JSON object, per '\n'-terminated + * line - see lib/debug_proto.h). This client owns all user-facing rendering: + * the server-side debug core never emits ANSI or formatted columns, only + * structured data (plus, where a rendering-rich port needed more than the + * original data model had - e.g. DISASSEMBLE's raw instruction bytes - a + * small additive extension of that same structured data, never markup). + * + * Three ways to obtain a connection: + * udbg - SIGUSR1-attach to a running `-X` process (gdb -p style) + * udbg - connect to an explicit debug.listen(path) socket + * udbg --fd N - use an already-connected, inherited fd N (used + * internally by the local `-x` CLI, which forks this + * binary with one end of a socketpair on fd 3) + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "debug_highlight.h" +#include "debug_lineedit.h" + +/* -- ANSI colors ----------------------------------------------------------- */ + +#define C_RESET "\033[0m" +#define C_DIM "\033[2m" +#define C_BOLD "\033[1m" +#define C_RED "\033[31m" +#define C_GREEN "\033[32m" +#define C_YELLOW "\033[33m" +#define C_BLUE "\033[34m" +#define C_MAGENTA "\033[35m" +#define C_CYAN "\033[36m" +#define C_EVENT "\033[2;3m" /* faint + italic, for async server events */ + +#define MAX_LINE 65536 +#define DEFAULT_SOCKET_DIR "/tmp" +#define MAX_WAIT_TIME 30 + +/* -- wire framing ---------------------------------------------------------- + * + * Mirrors lib/debug_proto.c's framing without depending on it: this client + * has no ucode VM of its own to hand `ucv_*` helpers, so it talks the wire + * format directly in terms of json-c objects instead. */ + +static void +proto_write(int fd, const char *verb, struct json_object *payload) +{ + const char *json; + char *line; + size_t len; + ssize_t n; + const char *p; + + if (payload) { + json = json_object_to_json_string_ext(payload, JSON_C_TO_STRING_PLAIN); + len = strlen(verb) + 1 + strlen(json) + 1; + line = malloc(len + 1); + snprintf(line, len + 1, "%s %s\n", verb, json); + } + else { + len = strlen(verb) + 1; + line = malloc(len + 1); + snprintf(line, len + 1, "%s\n", verb); + } + + p = line; + + while (len > 0) { + n = write(fd, p, len); + + if (n < 0) { + if (errno == EINTR) + continue; + + break; + } + + p += n; + len -= (size_t)n; + } + + free(line); +} + +/* Growable line-buffered reader, one instance per connection. */ +typedef struct { + char *data; + size_t len, cap; +} linebuf_t; + +static bool +linebuf_append(linebuf_t *lb, const char *data, size_t n) +{ + if (lb->len + n > lb->cap) { + size_t newcap = lb->cap ? lb->cap : 4096; + + while (newcap < lb->len + n) + newcap *= 2; + + char *p = realloc(lb->data, newcap); + + if (!p) + return false; + + lb->data = p; + lb->cap = newcap; + } + + memcpy(lb->data + lb->len, data, n); + lb->len += n; + + return true; +} + +/* Extract one already-buffered "VERB [json]" line, if any, without touching + * the fd. Returns false if no full line is buffered yet. */ +static bool +linebuf_pop(linebuf_t *lb, char **verb_out, struct json_object **payload_out) +{ + char *nl = memchr(lb->data, '\n', lb->len); + size_t linelen, verblen; + char *line, *sp; + + if (!nl) + return false; + + linelen = (size_t)(nl - lb->data); + line = malloc(linelen + 1); + memcpy(line, lb->data, linelen); + line[linelen] = '\0'; + + memmove(lb->data, lb->data + linelen + 1, lb->len - linelen - 1); + lb->len -= linelen + 1; + + if (linelen > 0 && line[linelen - 1] == '\r') + line[--linelen] = '\0'; + + sp = memchr(line, ' ', linelen); + verblen = sp ? (size_t)(sp - line) : linelen; + + *verb_out = malloc(verblen + 1); + memcpy(*verb_out, line, verblen); + (*verb_out)[verblen] = '\0'; + + *payload_out = NULL; + + if (sp && *(sp + 1)) + *payload_out = json_tokener_parse(sp + 1); + + free(line); + + return true; +} + +/* Block until a full message is available on `fd`/`lb` and pop it - used for + * the synchronous SOURCE request/response round-trip triggered from within + * rendering. Only safe to call while the session is paused and no other + * request is outstanding (true for every call site below): the server only + * ever answers strictly in request order while paused, so the first message + * to arrive is the one we asked for, barring the rare case of an async + * EVENT interleaving, which is not handled specially here. */ +static const char * +jstr(struct json_object *obj, const char *key, const char *dflt) +{ + struct json_object *v; + + if (obj && json_object_object_get_ex(obj, key, &v) && json_object_is_type(v, json_type_string)) + return json_object_get_string(v); + + return dflt; +} + +static int64_t +jint(struct json_object *obj, const char *key, int64_t dflt) +{ + struct json_object *v; + + if (obj && json_object_object_get_ex(obj, key, &v)) + return json_object_get_int64(v); + + return dflt; +} + +/* Every "col"/"from_col"/"to_col" field the protocol sends is 1-based (see + * uc_source_get_line() in source.c), meant for human-readable "line:col" + * display - debug_highlight's span/ip columns are 0-based byte indices + * into the line string, so any such field needs this before being used as + * one. */ +static size_t +col0(int64_t col) +{ + return (col > 0) ? (size_t)(col - 1) : 0; +} + +/* -- source cache & syntax highlighting ----------------------------------- */ + +typedef struct source_cache_entry { + char *file; + char **lines; + size_t nlines; + struct source_cache_entry *next; +} source_cache_entry_t; + +static source_cache_entry_t *source_cache = NULL; + +static char ** +split_lines(const char *text, size_t *nlines_out) +{ + size_t count = 1, i; + char **lines; + const char *p, *start; + + for (p = text; *p; p++) + if (*p == '\n') + count++; + + lines = calloc(count, sizeof(char *)); + i = 0; + start = text; + + for (p = text; ; p++) { + if (*p == '\n' || *p == '\0') { + size_t len = (size_t)(p - start); + + if (len > 0 && start[len - 1] == '\r') + len--; + + lines[i] = malloc(len + 1); + memcpy(lines[i], start, len); + lines[i][len] = '\0'; + i++; + + if (*p == '\0') + break; + + start = p + 1; + } + } + + *nlines_out = i; + + return lines; +} + +static char ** +find_cached_source(const char *file, size_t *nlines_out) +{ + source_cache_entry_t *e; + + for (e = source_cache; e; e = e->next) { + if (!strcmp(e->file, file)) { + *nlines_out = e->nlines; + + return e->lines; + } + } + + return NULL; +} + +/* Split and cache already-known source `text` for `file` (e.g. from a + * SOURCE response the caller already has in hand), so a later + * render_source_lines() call for the same file doesn't re-request it. */ +static char ** +cache_source_text(const char *file, const char *text, size_t *nlines_out) +{ + source_cache_entry_t *e; + char **cached = find_cached_source(file, nlines_out); + + if (cached) + return cached; + + e = malloc(sizeof(source_cache_entry_t)); + e->file = strdup(file); + e->lines = split_lines(text, &e->nlines); + e->next = source_cache; + source_cache = e; + + *nlines_out = e->nlines; + + return e->lines; +} + +/* Local source root override (-s/--srcdir), used when the path the server + * reports doesn't exist as-is on this machine - see try_load_local_file(). */ +static const char *opt_srcdir = NULL; + +static char * +read_whole_file(FILE *fp) +{ + char buf[65536]; + size_t n, cap = 0, len = 0; + char *text = NULL; + + while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) { + if (len + n + 1 > cap) { + cap = cap ? cap * 2 : 65536; + + while (cap < len + n + 1) + cap *= 2; + + text = realloc(text, cap); + } + + memcpy(text + len, buf, n); + len += n; + } + + if (!text) + text = malloc(1); + + text[len] = '\0'; + + return text; +} + +/* Debugging usually either runs fully locally (the client and the debugged + * script share the same filesystem - the common `-x`/`udbg `-on-the- + * same-box case) or from a development checkout against a remote target + * (the *client* has the better/only real source access, not the server) - + * in both cases, the client reading the file itself is at least as likely + * to succeed as asking the server for it, and doesn't need a round trip. + * Only once this fails do callers fall back to requesting SOURCE from the + * server (e.g. the target is a remote embedded device with no shared + * filesystem, or running precompiled bytecode with only embedded source). + * + * Tries the path exactly as the server reported it first (already correct + * for the local case, and for absolute paths that happen to also exist on + * this machine), then, if `-s/--srcdir DIR` was given, DIR joined with + * just the reported path's basename - a simple heuristic for "the server's + * path is from a different checkout/build root than this one". */ +static char ** +try_load_local_file(const char *file, size_t *nlines_out) +{ + FILE *fp = fopen(file, "rb"); + char *joined = NULL; + + if (!fp && opt_srcdir) { + const char *base = strrchr(file, '/'); + + base = base ? base + 1 : file; + joined = malloc(strlen(opt_srcdir) + 1 + strlen(base) + 1); + sprintf(joined, "%s/%s", opt_srcdir, base); + fp = fopen(joined, "rb"); + } + + free(joined); + + if (!fp) + return NULL; + + { + char *text = read_whole_file(fp); + char **lines = cache_source_text(file, text, nlines_out); + + fclose(fp); + free(text); + + return lines; + } +} + +/* Rendering a source range/context needs the actual text, which only ever + * arrives asynchronously as a SOURCE response processed by the normal main + * loop - never via a nested blocking round-trip from inside another + * response's rendering, which would re-enter the single shared connection + * state from two places at once. So when the file isn't cached yet, a + * render call fires off a SOURCE request and remembers what it wanted to + * show as `pending_source`; the main loop's SOURCE handler finishes the + * render once the response actually arrives. */ +typedef struct { + bool active; + char *file; + int64_t from, to; + debug_highlight_span_t hl; + bool have_hl; + size_t left_pad; +} pending_source_t; + +static pending_source_t pending_source = { 0 }; + +static void +request_source(int fd, const char *file, int64_t from, int64_t to, + const debug_highlight_span_t *hl, size_t left_pad) +{ + struct json_object *payload; + + /* A fetch for this exact file is already in flight (e.g. the initial + * PAUSED's own auto-context request hasn't resolved yet when a + * "lines" response also wants it) - don't send a second SOURCE + * request that would only overwrite this same pending_source's + * tracking with no way to reconcile the two, just adopt whichever + * range was asked for most recently and let the one response in + * flight satisfy it. */ + if (pending_source.active && !strcmp(pending_source.file, file)) { + pending_source.from = from; + pending_source.to = to; + pending_source.have_hl = (hl != NULL); + pending_source.left_pad = left_pad; + + if (hl) + pending_source.hl = *hl; + + return; + } + + payload = json_object_new_object(); + json_object_object_add(payload, "file", json_object_new_string(file)); + proto_write(fd, "SOURCE", payload); + json_object_put(payload); + + free(pending_source.file); + pending_source.active = true; + pending_source.file = strdup(file); + pending_source.from = from; + pending_source.to = to; + pending_source.have_hl = (hl != NULL); + pending_source.left_pad = left_pad; + + if (hl) + pending_source.hl = *hl; +} + +/* Current terminal width, for the same wrap/pad behavior + * debug_highlight_print_source()'s ported original had via term_width(). + * Falls back to 80 columns when stdout isn't a tty (e.g. piped output). */ +static size_t +term_columns(void) +{ + struct winsize w; + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0 && w.ws_col > 0) + return w.ws_col; + + return 80; +} + +/* Print source lines [from, to] (1-based, inclusive) from `file`, shading + * the `hl` statement span if given. If the text isn't cached yet, + * asynchronously requests it (see `pending_source` above) and returns + * without printing anything - the main loop's SOURCE response handler + * re-invokes this once the text has actually arrived. */ + +/* Mirrors format_context_statement()'s range-splitting for a statement/ + * function too long to show in full: a window of context around `from`, + * a gap, and a window around the current instruction and/or `to` - using + * the same 2-line-before/2-line-after context radius render_paused()/ + * render_backtrace_final() already use. Returns the number of ranges + * written to `ranges` (1 if the span is short enough to just show whole, + * up to 3 otherwise). Falls back to a single [from, to] range verbatim if + * there's no known "current line" to anchor the split around. */ +static size_t +compute_context_ranges(int64_t from, int64_t to, const debug_highlight_span_t *hl, + debug_highlight_range_t ranges[3]) +{ + const int64_t ctx = 2; + int64_t ip; + debug_highlight_range_t r[3] = { { 0, 0 }, { 0, 0 }, { 0, 0 } }; + size_t n = 0, i; + + if (from < 1) + from = 1; + + if (!hl || !hl->have_ip || to - from <= 4) { + ranges[0] = (debug_highlight_range_t){ (size_t)from, (size_t)to }; + return 1; + } + + ip = (int64_t)hl->ip_line; + + if (ip < from) + ip = from; + + if (ip > to) + ip = to; + + if (ip - from <= (ctx + ctx + 2)) { + r[1].from = (size_t)from; + } + else { + r[0].from = (size_t)from; + r[0].to = (size_t)(from + ctx); + r[1].from = (size_t)(ip - ctx); + } + + if (to - ip <= (ctx + ctx + 2)) { + r[1].to = (size_t)to; + } + else { + r[1].to = (size_t)(ip + ctx); + r[2].from = (size_t)(to - ctx); + r[2].to = (size_t)to; + } + + for (i = 0; i < 3; i++) + if (r[i].from && r[i].to) + ranges[n++] = r[i]; + + return n; +} + +static void +render_source_lines(int fd, const char *file, int64_t from, int64_t to, + const debug_highlight_span_t *hl, size_t left_pad) +{ + size_t nlines; + char **lines = find_cached_source(file, &nlines); + + if (!lines) + lines = try_load_local_file(file, &nlines); + + if (!lines) { + request_source(fd, file, from, to, hl, left_pad); + return; + } + + if (from < 1) + from = 1; + + { + size_t columns = term_columns(); + debug_highlight_range_t ranges[3]; + size_t nranges; + + columns = (columns > left_pad) ? columns - left_pad : 0; + nranges = compute_context_ranges(from, to, hl, ranges); + + debug_highlight_print_source_ranges(stdout, lines, nlines, + nranges, ranges, hl, left_pad, columns); + } +} + +/* -- response rendering ------------------------------------------------- */ + +/* Join a JSON array of strings with " \xc2\xbb " (U+00BB, " » "), matching + * format_context_breadcrumb()'s separator. Caller frees the result. */ +static char * +join_breadcrumb(struct json_object *arr) +{ + static const char sep[] = " \xc2\xbb "; /* U+00BB RIGHT-POINTING GUILLEMET */ + size_t n = arr ? json_object_array_length(arr) : 0; + size_t len = 0, i; + char *out, *p; + + if (n == 0) + return strdup(""); + + for (i = 0; i < n; i++) + len += strlen(json_object_get_string(json_object_array_get_idx(arr, i))); + + len += (n - 1) * (sizeof(sep) - 1); + out = p = malloc(len + 1); + + for (i = 0; i < n; i++) { + const char *s = json_object_get_string(json_object_array_get_idx(arr, i)); + size_t l = strlen(s); + + if (i > 0) { + memcpy(p, sep, sizeof(sep) - 1); + p += sizeof(sep) - 1; + } + + memcpy(p, s, l); + p += l; + } + + *p = '\0'; + + return out; +} + +static void +render_paused(int fd, struct json_object *p) +{ + int64_t line = jint(p, "line", 0); + const char *file = jstr(p, "file", NULL); + + printf(C_BOLD "Paused" C_RESET " (%s) in " C_BOLD "%s()" C_RESET ", %s:%" PRId64 ":%" PRId64 "\n", + jstr(p, "reason", "?"), + jstr(p, "function", "?"), + file ? file : "?", + line, + jint(p, "col", 0)); + + if (json_object_object_get_ex(p, "breakpoint_id", NULL)) + printf(" " C_GREEN "breakpoint #%" PRId64 C_RESET "\n", jint(p, "breakpoint_id", 0)); + + if (json_object_object_get_ex(p, "exception_message", NULL)) + printf(" " C_RED "exception: %s" C_RESET "\n", jstr(p, "exception_message", "")); + + if (file && line > 0) { + struct json_object *breadcrumb_arr = json_object_object_get(p, "breadcrumb"); + char *breadcrumb = join_breadcrumb(breadcrumb_arr); + int64_t col = jint(p, "col", 0); + debug_highlight_span_t hl = { + .from_line = (size_t)line, .from_col = 0, + .to_line = (size_t)line, .to_col = SIZE_MAX, + .have_ip = true, .ip_line = (size_t)line, .ip_col = col0(col) + }; + + debug_highlight_print_header_bar(stdout, file, breadcrumb, 0, term_columns()); + free(breadcrumb); + + render_source_lines(fd, file, line - 2, line + 2, &hl, 0); + } +} + +static void +render_breakpoints(struct json_object *p) +{ + struct json_object *items = NULL; + size_t i, n; + + json_object_object_get_ex(p, "items", &items); + n = items ? json_object_array_length(items) : 0; + + if (n == 0) { + printf("No breakpoints set\n"); + return; + } + + for (i = 0; i < n; i++) { + struct json_object *it = json_object_array_get_idx(items, i); + struct json_object *idv = NULL; + + if (json_object_object_get_ex(it, "id", &idv)) + printf(C_BOLD "#%-4" PRId64 C_RESET " ", json_object_get_int64(idv)); + else + printf(C_DIM "(%-4s)" C_RESET " ", jstr(it, "kind", "?")); + + if (json_object_object_get_ex(it, "file", NULL)) + printf("%s:%" PRId64 ":%" PRId64 " - %s\n", + jstr(it, "file", "?"), jint(it, "line", 0), + jint(it, "col", 0), jstr(it, "function", "?")); + else + printf("\n"); + } +} + +static void +render_variables_array(struct json_object *items, const char *indent) +{ + size_t i, n = items ? json_object_array_length(items) : 0; + debug_variable_t *vars = calloc(n ? n : 1, sizeof(*vars)); + + for (i = 0; i < n; i++) { + struct json_object *it = json_object_array_get_idx(items, i); + struct json_object *shadowed_j = json_object_object_get(it, "shadowed"); + + vars[i].name = jstr(it, "name", "?"); + vars[i].kind = jstr(it, "kind", ""); + vars[i].value_repr = jstr(it, "value_repr", ""); + vars[i].shadowed = shadowed_j && json_object_get_boolean(shadowed_j); + } + + debug_highlight_print_variables(stdout, vars, n, indent, term_columns()); + free(vars); +} + +/* Async multi-file fetch for render_backtrace(): a backtrace can span + * several source files at once (unlike PAUSED/LINES, which only ever need + * one), so a single pending_source-style slot isn't enough - this instead + * queues every file the frames need that isn't cached yet, fetches them + * one at a time, and only actually prints once all of them have arrived. */ +typedef struct { + bool active; + struct json_object *payload; + char **files; + size_t nfiles, next; +} pending_backtrace_t; + +static pending_backtrace_t pending_backtrace = { 0 }; + +static void +request_backtrace_file(int fd, const char *file) +{ + struct json_object *payload = json_object_new_object(); + + json_object_object_add(payload, "file", json_object_new_string(file)); + proto_write(fd, "SOURCE", payload); + json_object_put(payload); +} + +static void +render_backtrace_final(int fd, struct json_object *p) +{ + struct json_object *frames = NULL; + size_t i, n; + + json_object_object_get_ex(p, "frames", &frames); + n = frames ? json_object_array_length(frames) : 0; + + for (i = 0; i < n; i++) { + struct json_object *fr = json_object_array_get_idx(frames, i); + struct json_object *vars = NULL; + const char *file = jstr(fr, "file", NULL); + int64_t line = jint(fr, "line", 0); + int64_t col = jint(fr, "col", 0); + bool native = !strcmp(jstr(fr, "kind", ""), "native"); + char signature[256]; + + char prefix[16]; + size_t prefix_len, columns = term_columns(); + + snprintf(signature, sizeof(signature), "%s()", jstr(fr, "function", "?")); + + /* "#N " is printed right before the header bar, on the same line - + * left_pad itself would make the bar draw *another* copy of that + * indentation (it is meant for a bar that draws its own leading + * blanks, see render_paused() above for that usage), so instead + * just shrink the width budget by the prefix that already went + * out via printf() below, and leave left_pad at 0. Without this, + * the bar is sized for the full terminal width and the combined + * line overflows it by exactly the prefix's length. */ + prefix_len = (size_t)snprintf(prefix, sizeof(prefix), "#%-2" PRId64 " ", jint(fr, "index", 0)); + columns = (columns > prefix_len) ? columns - prefix_len : 0; + + printf(C_BOLD "%s" C_RESET, prefix); + + debug_highlight_print_header_bar(stdout, + native ? "C" : (file ? file : "?"), signature, 0, columns); + + if (!native && file && line > 0) { + debug_highlight_span_t hl = { + .from_line = (size_t)line, .from_col = 0, + .to_line = (size_t)line, .to_col = SIZE_MAX, + .have_ip = true, .ip_line = (size_t)line, .ip_col = col0(col) + }; + + render_source_lines(fd, file, line - 2, line + 2, &hl, 2); + } + + if (json_object_object_get_ex(fr, "variables", &vars)) + render_variables_array(vars, " - "); + + printf("\n"); + } +} + +static void +render_backtrace(int fd, struct json_object *p) +{ + struct json_object *frames = NULL; + size_t i, n; + char **missing = NULL; + size_t n_missing = 0, cap = 0; + + json_object_object_get_ex(p, "frames", &frames); + n = frames ? json_object_array_length(frames) : 0; + + for (i = 0; i < n; i++) { + struct json_object *fr = json_object_array_get_idx(frames, i); + const char *file = jstr(fr, "file", NULL); + size_t dummy; + size_t j; + bool already = false; + + if (!file || strcmp(jstr(fr, "kind", ""), "script")) + continue; + + if (find_cached_source(file, &dummy)) + continue; + + if (try_load_local_file(file, &dummy)) + continue; + + for (j = 0; j < n_missing; j++) + if (!strcmp(missing[j], file)) + already = true; + + if (already) + continue; + + if (n_missing >= cap) { + cap = cap ? cap * 2 : 4; + missing = realloc(missing, cap * sizeof(*missing)); + } + + missing[n_missing++] = strdup(file); + } + + if (n_missing == 0) { + free(missing); + render_backtrace_final(fd, p); + return; + } + + pending_backtrace.active = true; + pending_backtrace.payload = json_object_get(p); + pending_backtrace.files = missing; + pending_backtrace.nfiles = n_missing; + pending_backtrace.next = 0; + + request_backtrace_file(fd, missing[0]); +} + +static void +render_source_range(int fd, struct json_object *p) +{ + const char *file = jstr(p, "file", NULL); + struct json_object *cursor = json_object_object_get(p, "cursor"); + int64_t from = jint(p, "from", 0); + int64_t to = jint(p, "to", 0); + debug_highlight_span_t hl; + + if (!file) { + printf("(no source range)\n"); + return; + } + + if (cursor) { + hl.from_line = (size_t)jint(cursor, "from_line", 0); + hl.from_col = col0(jint(cursor, "from_col", 0)); + hl.to_line = (size_t)jint(cursor, "to_line", 0); + hl.to_col = col0(jint(cursor, "to_col", 0)); + + /* The protocol only gives us the statement's *span*, not the + * exact current instruction position within it (which can differ + * for multi-part expressions) - approximate with the span start, + * which is exact for the common case of a simple statement. */ + hl.have_ip = true; + hl.ip_line = hl.from_line; + hl.ip_col = hl.from_col; + + render_source_lines(fd, file, from, to, &hl, 0); + } + else { + render_source_lines(fd, file, from, to, NULL, 0); + } +} + +/* Fill in the raw byte array fields of a debug_disasm_insn_t (or a + * capture/unpack sub-entry) from a JSON array of small integers. `dst` must + * already point at storage for at least `cap` bytes; only the first + * min(array length, cap) entries are filled. */ +static void +jbytes(struct json_object *arr, unsigned char *dst, size_t cap) +{ + size_t n = arr ? json_object_array_length(arr) : 0; + + if (n > cap) + n = cap; + + for (size_t i = 0; i < n; i++) + dst[i] = (unsigned char)json_object_get_int64(json_object_array_get_idx(arr, i)); +} + +static void +render_disassembly(struct json_object *p) +{ + struct json_object *insns_j = NULL; + debug_disasm_insn_t *insns; + size_t n; + + json_object_object_get_ex(p, "instructions", &insns_j); + n = insns_j ? json_object_array_length(insns_j) : 0; + insns = calloc(n, sizeof(*insns)); + + for (size_t i = 0; i < n; i++) { + struct json_object *ins = json_object_array_get_idx(insns_j, i); + struct json_object *bytes_j = json_object_object_get(ins, "bytes"); + struct json_object *constant_j = NULL; + struct json_object *captures_j = json_object_object_get(ins, "captures"); + struct json_object *unpacks_j = json_object_object_get(ins, "unpacks"); + debug_disasm_insn_t *d = &insns[i]; + size_t nbytes = bytes_j ? json_object_array_length(bytes_j) : 0; + unsigned char *bytes = malloc(nbytes ? nbytes : 1); + + jbytes(bytes_j, bytes, nbytes); + + d->offset = (size_t)jint(ins, "offset", 0); + d->mnemonic = jstr(ins, "mnemonic", "?"); + d->format = (int)jint(ins, "format", 0); + d->bytes = bytes; + d->nbytes = nbytes; + d->operand = jint(ins, "operand", 0); + + if (json_object_object_get_ex(ins, "constant", &constant_j)) { + d->have_constant = true; + d->constant_repr = json_object_to_json_string(constant_j); + d->constant_is_string = (json_object_get_type(constant_j) == json_type_string); + } + + if (json_object_object_get_ex(ins, "variable_kind", NULL)) { + d->variable_kind = jstr(ins, "variable_kind", NULL); + d->variable_name = jstr(ins, "variable_name", NULL); + } + + if (json_object_object_get_ex(ins, "closure_index", NULL)) { + d->have_closure = true; + d->closure_kind = jstr(ins, "closure_kind", "closure"); + d->closure_index = (uint32_t)jint(ins, "closure_index", 0); + } + + if (json_object_object_get_ex(ins, "call_nargs", NULL)) { + struct json_object *mcall_j = json_object_object_get(ins, "call_mcall"); + + d->have_call = true; + d->call_mcall = mcall_j && json_object_get_boolean(mcall_j); + d->call_nargs = (uint32_t)jint(ins, "call_nargs", 0); + } + + d->ncaptures = captures_j ? json_object_array_length(captures_j) : 0; + d->captures = calloc(d->ncaptures ? d->ncaptures : 1, sizeof(*d->captures)); + + for (size_t j = 0; j < d->ncaptures; j++) { + struct json_object *cap = json_object_array_get_idx(captures_j, j); + + d->captures[j].slot = jint(cap, "slot", 0); + d->captures[j].upval = !strcmp(jstr(cap, "kind", ""), "upval"); + d->captures[j].name = jstr(cap, "name", "(unknown)"); + jbytes(json_object_object_get(cap, "bytes"), d->captures[j].bytes, 4); + } + + d->nunpacks = unpacks_j ? json_object_array_length(unpacks_j) : 0; + d->unpacks = calloc(d->nunpacks ? d->nunpacks : 1, sizeof(*d->unpacks)); + + for (size_t j = 0; j < d->nunpacks; j++) { + struct json_object *u = json_object_array_get_idx(unpacks_j, j); + + d->unpacks[j].slot = (uint16_t)jint(u, "slot", 0); + jbytes(json_object_object_get(u, "bytes"), d->unpacks[j].bytes, 2); + } + } + + debug_highlight_print_disassembly(stdout, jstr(p, "function", "?"), insns, n, term_columns()); + + for (size_t i = 0; i < n; i++) { + free((void *)insns[i].bytes); + free(insns[i].captures); + free(insns[i].unpacks); + } + + free(insns); +} + +/* Async server events (see EVENT in lib/debug_proto.h) can land at any + * time, unprompted by anything the user typed - set in a faint italic + * style to visually set them apart from direct command responses. */ +static void +render_event(struct json_object *p) +{ + const char *event = jstr(p, "event", "?"); + + printf(C_EVENT); + + if (!strcmp(event, "exception")) { + struct json_object *exc = json_object_object_get(p, "exception"); + + printf("*** exception: %s: %s ***", jstr(exc, "type", "Error"), + jstr(exc, "message", "?")); + } + else if (!strcmp(event, "exit")) { + const char *status = jstr(p, "status", "?"); + + if (!strcmp(status, "OK")) { + printf("*** program finished ***"); + } + else if (!strcmp(status, "EXIT")) { + printf("*** program exited (code %" PRId64 ") ***", jint(p, "code", 0)); + } + else { + struct json_object *exc = json_object_object_get(p, "exception"); + + if (exc) + printf("*** program terminated: %s: %s ***", + jstr(exc, "type", "Error"), jstr(exc, "message", "?")); + else + printf("*** program terminated (%s) ***", status); + } + } + else { + printf("*** event: %s %s ***", event, + json_object_to_json_string_ext(p, JSON_C_TO_STRING_SPACED)); + } + + printf(C_RESET "\n"); +} + +static void +render_response(int fd, const char *verb, struct json_object *payload) +{ + if (!strcmp(verb, "PAUSED")) + render_paused(fd, payload); + else if (!strcmp(verb, "BREAKPOINTS")) + render_breakpoints(payload); + else if (!strcmp(verb, "VARIABLES")) + render_variables_array(json_object_object_get(payload, "vars"), ""); + else if (!strcmp(verb, "BACKTRACE")) + render_backtrace(fd, payload); + else if (!strcmp(verb, "SOURCE_RANGE")) + render_source_range(fd, payload); + else if (!strcmp(verb, "DISASSEMBLY")) + render_disassembly(payload); + else if (!strcmp(verb, "ERROR")) + printf(C_RED "Error: %s" C_RESET "\n", jstr(payload, "message", "(unknown error)")); + else if (!strcmp(verb, "VALUE")) + printf("%s\n", jstr(payload, "repr", "")); + else if (!strcmp(verb, "BREAKPOINT_ADDED")) + printf(C_GREEN "Breakpoint #%" PRId64 " added" C_RESET "\n", jint(payload, "id", 0)); + else if (!strcmp(verb, "EVENT")) + render_event(payload); + else if (!strcmp(verb, "SOURCE")) { + const char *text = jstr(payload, "text", NULL); + const char *file = jstr(payload, "file", "?"); + + if (text) { + size_t nlines; + + cache_source_text(file, text, &nlines); + + /* A render_backtrace() multi-file fetch takes priority: advance + * its queue and either request the next missing file or, once + * every frame's file is cached, finally print the whole thing. */ + if (pending_backtrace.active && pending_backtrace.next < pending_backtrace.nfiles && + !strcmp(pending_backtrace.files[pending_backtrace.next], file)) { + pending_backtrace.next++; + + if (pending_backtrace.next < pending_backtrace.nfiles) { + request_backtrace_file(fd, pending_backtrace.files[pending_backtrace.next]); + } + else { + size_t i; + + render_backtrace_final(fd, pending_backtrace.payload); + json_object_put(pending_backtrace.payload); + + for (i = 0; i < pending_backtrace.nfiles; i++) + free(pending_backtrace.files[i]); + + free(pending_backtrace.files); + pending_backtrace = (pending_backtrace_t){ 0 }; + } + } + /* Finishing an auto-fetch triggered by render_paused()/ + * render_source_range() (see pending_source) is distinct from + * a direct response to a user-typed "source " command: + * the former re-renders exactly the range that was originally + * requested, the latter shows the whole file. */ + else if (pending_source.active && !strcmp(pending_source.file, file)) { + int64_t from = pending_source.from; + int64_t to = pending_source.to; + bool have_hl = pending_source.have_hl; + debug_highlight_span_t hl = pending_source.hl; + size_t left_pad = pending_source.left_pad; + + pending_source.active = false; + render_source_lines(fd, file, from, to, have_hl ? &hl : NULL, left_pad); + } + else { + printf("--- %s ---\n", file); + render_source_lines(fd, file, 1, (int64_t)nlines, NULL, 0); + } + } + else { + printf("(source unavailable: %s)\n", jstr(payload, "error", "?")); + pending_source.active = false; + + if (pending_backtrace.active) { + size_t i; + + /* Missing source for one frame shouldn't block showing the + * rest - just print what we have (unavailable files will + * fall back to "no snippet" for that frame). */ + render_backtrace_final(fd, pending_backtrace.payload); + json_object_put(pending_backtrace.payload); + + for (i = 0; i < pending_backtrace.nfiles; i++) + free(pending_backtrace.files[i]); + + free(pending_backtrace.files); + pending_backtrace = (pending_backtrace_t){ 0 }; + } + } + } + else if (!strcmp(verb, "HELP")) { + struct json_object *cmds = json_object_object_get(payload, "commands"); + size_t i, n = cmds ? json_object_array_length(cmds) : 0; + + for (i = 0; i < n; i++) { + struct json_object *c = json_object_array_get_idx(cmds, i); + + printf("%-16s %s\n", jstr(c, "verb", "?"), jstr(c, "help", "")); + } + } + else if (!strcmp(verb, "SOURCES")) { + struct json_object *items = json_object_object_get(payload, "items"); + size_t i, n = items ? json_object_array_length(items) : 0; + + for (i = 0; i < n; i++) { + struct json_object *it = json_object_array_get_idx(items, i); + + printf("#%-2" PRId64 " %s\n", jint(it, "index", 0), jstr(it, "file", "?")); + } + } + else if (!strcmp(verb, "OK")) { + printf("OK\n"); + } + else if (!strcmp(verb, "RESUME")) { + printf("(resumed)\n"); + } + else if (payload) { + printf("%s %s\n", verb, json_object_to_json_string_ext(payload, JSON_C_TO_STRING_SPACED)); + } + else { + printf("%s\n", verb); + } +} + +/* -- typed command line -> VERB {payload} translation -------------------- */ + +static char * +trim(char *s) +{ + char *end; + + while (isspace((unsigned char)*s)) + s++; + + end = s + strlen(s); + + while (end > s && isspace((unsigned char)end[-1])) + *--end = '\0'; + + return s; +} + +/* Split off the first whitespace-delimited word from *rest, returning it and + * advancing *rest to the remainder (leading space trimmed). */ +static char * +shift_word(char **rest) +{ + char *p = *rest; + char *word; + + while (isspace((unsigned char)*p)) + p++; + + word = p; + + while (*p && !isspace((unsigned char)*p)) + p++; + + if (*p) { + *p = '\0'; + p++; + + while (isspace((unsigned char)*p)) + p++; + } + + *rest = p; + + return word; +} + +/* True if `typed` is a non-empty prefix of any of the NUL-separated names + * in `names` (e.g. "list\0ls\0") - shortest-unique-prefix command matching, + * same as the original interactive CLI's `commands[]` dispatch. Ambiguous + * prefixes (matching more than one command) resolve to whichever command + * is checked first below, in the same fixed order the original table + * declared them in. */ +static bool +match_cmd(const char *names, const char *typed) +{ + size_t typed_len = strlen(typed); + const char *p = names; + + if (typed_len == 0) + return false; + + while (*p) { + size_t len = strlen(p); + + if (len >= typed_len && !strncmp(p, typed, typed_len)) + return true; + + p += len + 1; + } + + return false; +} + +/* CLI usage documentation, ported verbatim from the pre-protocol interactive + * debugger's `commands[]`/cmd_help() (formerly lib/debug.c) - this describes + * *this client's* typed command syntax, so unlike everything else in this + * file it is never fetched from the server: the server's own HELP verb + * answers a different question (the wire protocol's verbs and payload + * shapes, for anything else that might speak the protocol directly) and + * showing that to an interactive user here just reads as raw protocol + * internals. `names` is a NUL-separated list of aliases, primary name + * first - match_cmd() already implements the exact prefix-matching lookup + * this needs, so it is reused here for `help ` filtering. */ +static const struct { + const char *names; + const char *help; +} cli_help_table[] = { + { "help\0h\0?\0", + "Print help information." }, + { "break\0b\0", + "The break command sets a breakpoint at the given location, " + "instructing the virtual machine to stop execution at this " + "point and handing control to the debugger.\n\n" + "Breakpoint locations may be specified either as filename, " + "line number and optional character offset within the line " + "or as a ucode expression that evaluates to a function in " + "which a breakpoint is set.\n\n" + "Examples:\n" + " break example.uc:13 # Set breakpoint in line 13 of example.uc\n" + " break 4:17 # Break in line in 4, char 17 of current file\n" + " break myobj.method # Break in function `method` of `myobj`\n" + " break (string.uc) # Parens to disambiguate expression from path" + }, + { "delete\0d\0", + "Delete a breakpoint. When no argument is given, the current " + "breakpoint is deleted, otherwise this function deletes the breakpoint " + "with the given index.\n\n" + "Examples:\n" + " delete # Delete current breakpoint\n" + " delete 2 # Delete breakpoint #2" + }, + { "list\0ls\0", + "List all currently set breakpoints. User defined breakpoints are " + "prefixed with a number identifying the breakpoint, internal " + "breakpoints used by the debugger are prefixed with a breakpoint type " + "enclosed in parens, e.g. '(step)'." + }, + { "next\0n\0", + "Execute the next statement and stop again." + }, + { "step\0s\0", + "Execute the next statement, in case of function calls step into the " + "called function and stop there." + }, + { "continue\0c\0", + "Continue execution until the next breakpoint or end of program." + }, + { "return\0", + "Continue executing the current function until it returns, then stop " + "in the calling function. If the current function is the program entry " + "function, then run until the end of the program." + }, + { "backtrace\0bt\0", + "Print a trace of the current callstack, with most recent callframes " + "output first. If the optional 'full' argument is specified, " + "additional information about each call frame is printed.\n\n" + "Examples:\n" + " backtrace # Print backtrace\n" + " backtrace full # Print backtrace with additional information" + }, + { "variables\0vars\0", + "Print local variables and their contents for the current execution " + "context. Internal variables which are unreachable by script code " + "are shown faint, upvalues (variables captured from parent scopes) " + "are shown in bold cyan and ordinary variables use the default color.\n\n" + "Examples:\n" + " variables # Print local variables" + }, + { "sources\0src\0", + "Print a list of loaded source buffers." + }, + { "print\0p\0", + "Evaluate an ucode expression and print the resulting value - like " + "the ucode CLI's `-p`.\n\n" + "Examples:\n" + " print varname # Print value of variable 'varname'\n" + " print myobj.prop # Print `prop` property of `myobj`\n" + " print keys(myobj) # Invoke a stdlib function" + }, + { "eval\0e\0", + "Evaluate an ucode expression, discarding its result instead of " + "printing it - like the ucode CLI's `-e`. The idiomatic way to " + "change a variable's value while paused: assignment is just " + "ordinary expression syntax, so a plain variable, a property path " + "or an array index all work the same way a script would write " + "them, without a separate dedicated command for it.\n\n" + "Examples:\n" + " eval x = 5 # Assign the number 5 to variable 'x'\n" + " eval x.y = 1 # Assign 1 to property 'y' of 'x'\n" + " eval delete foo.bar # Delete property 'bar' of 'foo'" + }, + { "lines\0ln\0", + "Print source code lines surrounding the given location specified " + "either as filename with line number or as expression evaluating to a " + "function value.\n\n" + "The amount of preceding and following lines to print may be " + "specified as second and third argument respectively. By default, two " + "lines of context are printed before and after the location.\n\n" + "Examples:\n" + " lines # Output lines surrounding current line\n" + " lines example.uc # Print first three lines of example.uc\n" + " lines (obj.func) # Parens to disambiguate expression from path\n" + " lines foo 5 8 # Print 5 lines before foo() till 8 lines in\n" + " lines #123 # Print source of instruction offset 123\n" + " lines +0 3 3 # Print 3 lines before and after current line\n" + " lines -5 # Print source 5 lines before current line\n" + " lines +3 # Print source 3 lines after current line" + }, + { "throw\0", + "Raise an exception at the current instruction offset.\n\n" + "Examples:\n" + " throw \"Message\" # Throw exception with given message" + }, + { "disassemble\0disasm\0", + "Disassemble the given function or statement location and output the " + "corresponding byte code in a human readable manner. The location to " + "disassemble may be either a function name, a single instruction " + "offset, an instruction offset range or a ucode expression.\n\n" + "Examples:\n" + " disassemble # Disassemble current statment\n" + " disassemble foo # Disassemble body of foo()\n" + " disassemble foo+100 # Disassemble first 100 byte of function foo()\n" + " disassemble #5 # Disassemble statement containing instruction 5\n" + " disassemble #2-10 # Disassemble instructions 2 to 10\n" + " disassemble #22+100 # Disassemble instructions 22 to 122\n" + " disassemble (12/3*4) # Disassemble ucode expression" + }, + { "source\0", + "Fetch and print the raw source text the server has for a file path, " + "without syntax highlighting - mostly useful to check exactly what " + "the server sees when it differs from the local copy." + }, + { "quit\0q\0", + "Forcibly terminate the currently running program. The termination " + "happens in the same manner as if 'exit()' has been called from " + "script code." + }, +}; + +/* Word-wrap and print one help entry's body to `columns`, preserving + * existing line breaks (so an "Examples:" block's indentation survives) + * and paragraph gaps - ported verbatim from cmd_help(), formerly + * lib/debug.c, with term_printf()/term_print() replaced by printf(). */ +static void +print_help_entry(const char *names, const char *help, size_t columns) +{ + const char *p = help; + + printf(C_BOLD "%s" C_RESET "\n\n", names); + + while (*p != '\0') { + size_t pad = strspn(p, " "); + size_t len = strcspn(p, "\r\n") - pad; + + if (pad + len <= columns) { + printf("%.*s\n", (int)(pad + len), p); + p += pad + len + (p[pad + len] == '\n'); + } + else { + if (pad > columns) + pad = 1; + + const char *l = p + pad; + + while (len > columns - pad) { + printf("%.*s", (int)pad, p); + + for (size_t j = columns - pad; j > 0; j--) { + if (l[j - 1] == ' ') { + printf("%.*s\n", (int)j, l); + l += j; + len -= j; + break; + } + } + } + + printf("%.*s", (int)pad, p); + printf("%.*s\n", (int)len, l); + p = l + len + (l[len] == '\n'); + } + } + + printf("\n\n"); +} + +static void +print_help(const char *cmd) +{ + size_t columns = term_columns(); + size_t n = sizeof(cli_help_table) / sizeof(cli_help_table[0]); + + for (size_t i = 0; i < n; i++) { + if (cmd && *cmd && !match_cmd(cli_help_table[i].names, cmd)) + continue; + + print_help_entry(cli_help_table[i].names, cli_help_table[i].help, columns); + } +} + +static bool +send_command(int fd, char *line, bool *resuming, bool *sent) +{ + char *cmd = shift_word(&line); + struct json_object *payload = NULL; + + *resuming = false; + *sent = true; + + if (!*cmd) { + *sent = false; + return true; + } + + if (match_cmd("help\0h\0?\0", cmd)) { + print_help(*line ? line : NULL); + *sent = false; + } + else if (match_cmd("break\0b\0", cmd)) { + payload = json_object_new_object(); + json_object_object_add(payload, "spec", json_object_new_string(line)); + proto_write(fd, "BREAK", payload); + } + else if (match_cmd("delete\0d\0", cmd)) { + if (*line) { + payload = json_object_new_object(); + json_object_object_add(payload, "id", json_object_new_int64(strtoll(line, NULL, 10))); + } + + proto_write(fd, "DELETE", payload); + } + else if (match_cmd("list\0ls\0", cmd)) { + proto_write(fd, "LIST_BREAKPOINTS", NULL); + } + else if (match_cmd("next\0n\0", cmd)) { + proto_write(fd, "NEXT", NULL); + *resuming = true; + } + else if (match_cmd("step\0s\0", cmd)) { + proto_write(fd, "STEP", NULL); + *resuming = true; + } + else if (match_cmd("continue\0c\0", cmd)) { + proto_write(fd, "CONTINUE", NULL); + *resuming = true; + } + else if (match_cmd("return\0", cmd)) { + proto_write(fd, "RETURN", NULL); + *resuming = true; + } + else if (match_cmd("backtrace\0bt\0", cmd)) { + payload = json_object_new_object(); + json_object_object_add(payload, "full", + json_object_new_boolean(!strcmp(trim(line), "full"))); + proto_write(fd, "BACKTRACE", payload); + } + else if (match_cmd("variables\0vars\0", cmd)) { + proto_write(fd, "VARIABLES", NULL); + } + else if (match_cmd("sources\0src\0", cmd)) { + proto_write(fd, "SOURCES", NULL); + } + else if (match_cmd("print\0p\0", cmd)) { + payload = json_object_new_object(); + json_object_object_add(payload, "expr", json_object_new_string(line)); + proto_write(fd, "PRINT", payload); + } + else if (match_cmd("eval\0e\0", cmd)) { + payload = json_object_new_object(); + json_object_object_add(payload, "expr", json_object_new_string(line)); + proto_write(fd, "EVAL", payload); + } + else if (match_cmd("lines\0ln\0", cmd)) { + char *spec = shift_word(&line); + char *before = shift_word(&line); + char *after = shift_word(&line); + + payload = json_object_new_object(); + + if (*spec) + json_object_object_add(payload, "spec", json_object_new_string(spec)); + + if (*before) + json_object_object_add(payload, "before", json_object_new_int64(strtoll(before, NULL, 10))); + + if (*after) + json_object_object_add(payload, "after", json_object_new_int64(strtoll(after, NULL, 10))); + + proto_write(fd, "LINES", payload); + } + else if (match_cmd("throw\0", cmd)) { + char *first = shift_word(&line); + static const char *types[] = { + "syntax", "runtime", "type", "reference", "user", "exit" + }; + size_t i; + bool is_type = false; + + for (i = 0; i < sizeof(types) / sizeof(types[0]); i++) { + if (!strncmp(types[i], first, strlen(first))) { + is_type = true; + break; + } + } + + payload = json_object_new_object(); + + if (is_type && *line) { + json_object_object_add(payload, "type", json_object_new_string(first)); + json_object_object_add(payload, "message", json_object_new_string(line)); + } + else { + char *msg = *line ? line : first; + + json_object_object_add(payload, "message", json_object_new_string(msg)); + } + + proto_write(fd, "THROW", payload); + } + else if (match_cmd("disassemble\0disasm\0", cmd)) { + if (*line) { + payload = json_object_new_object(); + json_object_object_add(payload, "spec", json_object_new_string(line)); + } + + proto_write(fd, "DISASSEMBLE", payload); + } + else if (match_cmd("source\0", cmd)) { + payload = json_object_new_object(); + json_object_object_add(payload, "file", json_object_new_string(line)); + proto_write(fd, "SOURCE", payload); + } + else if (match_cmd("quit\0q\0", cmd)) { + bool force = !strcmp(trim(line), "-f"); + + if (!force && isatty(STDIN_FILENO)) { + char confirm[16]; + + /* This wants a plain, cooked-mode, blocking fgets() prompt of + * its own - drop out of lineedit's raw/non-blocking mode for + * it, then re-engage before returning. */ + lineedit_suspend(); + + printf("Terminate program? (y/n) > "); + fflush(stdout); + + bool confirmed = fgets(confirm, sizeof(confirm), stdin) && + tolower((unsigned char)confirm[0]) == 'y'; + + lineedit_resume(); + + if (!confirmed) { + *sent = false; + return true; + } + } + + proto_write(fd, "QUIT", NULL); + return false; + } + else { + printf("Unrecognized command '%s' (try 'help')\n", cmd); + *sent = false; + } + + return true; +} + +/* -- connection setup ----------------------------------------------------- */ + +static int +connect_socket(const char *path) +{ + struct sockaddr_un addr; + int fd; + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + + if (fd < 0) + return -1; + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); + + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + close(fd); + + return -1; + } + + return fd; +} + +static char * +get_socket_path_for_pid(pid_t pid) +{ + static char path[256]; + + snprintf(path, sizeof(path), "%s/ucode-debug-%d.sock", DEFAULT_SOCKET_DIR, pid); + + return path; +} + +static int +wait_for_socket(const char *path, int timeout_sec) +{ + int elapsed = 0; + struct stat st; + + while (elapsed < timeout_sec) { + if (stat(path, &st) == 0 && (st.st_mode & S_IFMT) == S_IFSOCK) + return 0; + + sleep(1); + elapsed++; + } + + return -1; +} + +/* The debuggee's PID, for Ctrl-C-while-running (see maybe_send_interrupt() + * below) - resolved once right after connecting, however that happened + * (explicit , a socket path, or an inherited --fd), via SO_PEERCRED: + * works uniformly for all three, since all of them are - or, for --fd, + * were, at the moment the debuggee created it and only then forked - a + * connected AF_UNIX socket. -1 if this somehow couldn't be determined + * (Ctrl-C-while-running is then a no-op; everything else about the + * session is unaffected). */ +static pid_t debuggee_pid = -1; + +static void +resolve_debuggee_pid(int fd) +{ + struct ucred cred; + socklen_t len = sizeof(cred); + + if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == 0) + debuggee_pid = cred.pid; +} + +static void +print_usage(const char *prog) +{ + fprintf(stderr, "Usage: %s [-s DIR] \n", prog); + fprintf(stderr, " %s [-s DIR] \n", prog); + fprintf(stderr, " %s [-s DIR] --fd \n", prog); + fprintf(stderr, "\n"); + fprintf(stderr, "Debugger client for ucode, speaking the line-based debug protocol.\n"); + fprintf(stderr, "\n"); + fprintf(stderr, " SIGUSR1-attach to a running `-X` process, gdb -p style.\n"); + fprintf(stderr, " connect to an explicit debug.listen(path) socket.\n"); + fprintf(stderr, " --fd use an already-connected fd (internal, used by `-x`).\n"); + fprintf(stderr, " -s, --srcdir DIR\n"); + fprintf(stderr, " Local directory to also look for source files under\n"); + fprintf(stderr, " (by basename) when the path the server reports doesn't\n"); + fprintf(stderr, " exist as-is on this machine - e.g. the target runs on a\n"); + fprintf(stderr, " different host/root than this checkout. Source is always\n"); + fprintf(stderr, " tried locally first (at the server's exact reported path)\n"); + fprintf(stderr, " before ever asking the server for it.\n"); +} + +int +main(int argc, char **argv) +{ + int fd; + fd_set readfds; + char buf[MAX_LINE]; + linebuf_t lb = { 0 }; + + signal(SIGPIPE, SIG_IGN); + setvbuf(stdout, NULL, _IOLBF, 0); + debug_highlight_init(); + + { + size_t n = sizeof(cli_help_table) / sizeof(cli_help_table[0]); + static lineedit_completion_t comps[sizeof(cli_help_table) / sizeof(cli_help_table[0])]; + + for (size_t i = 0; i < n; i++) + comps[i].names = cli_help_table[i].names; + + lineedit_set_completions(comps, n); + } + + lineedit_init(); + + /* Pull -s/--srcdir DIR out of argv wherever it appears, leaving the + * rest of argument parsing below untouched. */ + { + int ai = 1; + + while (ai < argc) { + if (!strcmp(argv[ai], "-s") || !strcmp(argv[ai], "--srcdir")) { + if (ai + 1 >= argc) { + print_usage(argv[0]); + + return 1; + } + + opt_srcdir = argv[ai + 1]; + memmove(&argv[ai], &argv[ai + 2], (size_t)(argc - ai - 2) * sizeof(char *)); + argc -= 2; + + continue; + } + + ai++; + } + } + + if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) { + print_usage(argv[0]); + + return (argc < 2) ? 1 : 0; + } + + if (!strcmp(argv[1], "--fd")) { + if (argc < 3) { + print_usage(argv[0]); + + return 1; + } + + fd = atoi(argv[2]); + } + else if (strchr(argv[1], '/')) { + fd = connect_socket(argv[1]); + + if (fd < 0) { + fprintf(stderr, "Failed to connect to %s: %s\n", argv[1], strerror(errno)); + + return 1; + } + } + else { + pid_t pid = atoi(argv[1]); + char *socket_path; + struct stat st; + + if (pid <= 0) { + fprintf(stderr, "Invalid PID: %s\n", argv[1]); + + return 1; + } + + socket_path = get_socket_path_for_pid(pid); + + /* If the attach socket already exists, the target already has a + * session waiting (e.g. `-X `/debug.attach()) - just connect. + * Only send SIGUSR1 for the classic bare `-X` flow, where nothing is + * listening yet until asked to. */ + if (stat(socket_path, &st) == 0 && S_ISSOCK(st.st_mode)) { + fprintf(stderr, "Debugger socket already present, connecting...\n"); + } + else { + if (kill(pid, SIGUSR1) < 0) { + fprintf(stderr, "Failed to send SIGUSR1 to process %d: %s\n", pid, strerror(errno)); + + return 1; + } + + fprintf(stderr, "Sent SIGUSR1 to process %d, waiting for debugger socket...\n", pid); + + if (wait_for_socket(socket_path, MAX_WAIT_TIME) < 0) { + fprintf(stderr, "Timeout waiting for debugger socket at %s\n", socket_path); + + return 1; + } + } + + fd = connect_socket(socket_path); + + if (fd < 0) { + fprintf(stderr, "Failed to connect to %s: %s\n", socket_path, strerror(errno)); + + return 1; + } + } + + resolve_debuggee_pid(fd); + + fprintf(stderr, "Connected to ucode debugger\n\n"); + + bool stdin_done = false; + /* True whenever the session is sitting at a PAUSED prompt waiting for + * a command - i.e. exactly when a "dbg > " prompt should be visible. + * Cleared the instant a resuming command (next/step/continue/return) + * is sent, since there is no synchronous ack for those (see + * lib/debug_proto.h) - the prompt only comes back once a new PAUSED + * (or the connection closing) says so. */ + bool paused = false; + /* True from the moment any command is sent until its response has + * actually been drained and rendered - keeps the prompt from + * reappearing (and racing ahead of) a response that just hasn't + * arrived over the socket yet. */ + bool awaiting_response = false; + /* Tracks whether lineedit_begin() has already been called for the + * current accepting_input span, so the prompt (and a fresh, empty + * edit line) is (re)started exactly once per command, not on every + * select() wakeup while still mid-edit. */ + bool prompt_shown = false; + + for (;;) { + char *verb; + struct json_object *payload; + + while (linebuf_pop(&lb, &verb, &payload)) { + render_response(fd, verb, payload); + awaiting_response = false; + + if (!strcmp(verb, "PAUSED")) + paused = true; + else if (!strcmp(verb, "RESUME") || !strcmp(verb, "EVENT")) + paused = false; + + free(verb); + json_object_put(payload); + } + + /* Only accept (and select on) stdin for actual command input while + * sitting at a prompt: gating this on the exact same condition + * that shows the prompt is what stops a command from racing ahead + * of - and getting interleaved with - the connection's own + * initial PAUSED message or a still-in-flight response to a + * previous command. */ + bool accepting_input = paused && !stdin_done && !awaiting_response + && !pending_source.active && !pending_backtrace.active; + + if (!accepting_input) + prompt_shown = false; + else if (!prompt_shown) { + lineedit_begin("dbg > "); + prompt_shown = true; + } + + FD_ZERO(&readfds); + + /* Outside of accepting_input, stdin is still watched (whenever + * raw-mode editing is active, i.e. a real terminal - piped/ + * scripted input has no Ctrl-C to speak of) purely to catch + * Ctrl-C-while-running: see the interrupt handling below. */ + if (accepting_input || (lineedit_active() && !stdin_done)) + FD_SET(STDIN_FILENO, &readfds); + + FD_SET(fd, &readfds); + + if (select(fd + 1, &readfds, NULL, NULL, NULL) < 0) { + if (errno == EINTR) + continue; + + break; + } + + if (FD_ISSET(fd, &readfds)) { + ssize_t n = read(fd, buf, sizeof(buf)); + + if (n <= 0) { + printf("\nConnection closed\n"); + break; + } + + linebuf_append(&lb, buf, (size_t)n); + } + + if (!stdin_done && FD_ISSET(STDIN_FILENO, &readfds) && !accepting_input) { + /* Not sitting at a prompt (the debuggee is running) - stdin is + * only being watched here for Ctrl-C, not full line editing; + * anything else typed while running had no effect before this + * feature existed either, so it's simply discarded rather + * than queued up to confuse the next prompt. lineedit's raw + * mode (a prerequisite for even reaching this branch, see the + * FD_SET above) already made stdin non-blocking. */ + char ibuf[64]; + ssize_t n = read(STDIN_FILENO, ibuf, sizeof(ibuf)); + + for (ssize_t i = 0; i < n; i++) { + if (ibuf[i] == 3 /* Ctrl-C */ && debuggee_pid > 0) { + kill(debuggee_pid, SIGUSR1); + break; + } + } + } + else if (!stdin_done && FD_ISSET(STDIN_FILENO, &readfds)) { + bool eof = false; + + if (lineedit_feed(buf, sizeof(buf), &eof)) { + prompt_shown = false; + + if (*trim(buf)) { + bool resuming, sent; + bool keep_going = send_command(fd, trim(buf), &resuming, &sent); + + /* An unrecognized/empty command (or "quit" declined at its + * confirmation prompt) never reaches the server, so there + * is no response to wait for - re-show the prompt right + * away instead of waiting forever for one that isn't + * coming. */ + awaiting_response = sent; + + if (resuming) + paused = false; + + if (!keep_going) { + /* QUIT was sent - keep looping (without reading + * further stdin) to drain and render any trailing + * responses (e.g. a final EVENT exit) until the + * server closes the connection, instead of exiting + * immediately and losing output that was already in + * flight. */ + stdin_done = true; + } + } + } + else if (eof) { + proto_write(fd, "QUIT", NULL); + stdin_done = true; + } + } + } + + close(fd); + + return 0; +} diff --git a/vm.c b/vm.c index edcca1d9..4d8f705d 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 uc_vm_insn_format[__I_MAX] = { [I_LOAD] = 4, [I_LOAD8] = 1, [I_LOAD16] = 2, @@ -99,13 +99,13 @@ uc_vm_insn_to_name(uc_vm_insn_t insn) return insn_names[insn]; } -static int8_t +int8_t uc_vm_insn_to_argtype(uc_vm_insn_t insn) { if (insn < 0 || insn >= __I_MAX) return 0; - return insn_operand_bytes[insn]; + return uc_vm_insn_format[insn]; } static void @@ -171,20 +171,14 @@ uc_vm_signal_handler(int sig) uc_vm_signal_raise(vm, sig); } -static void -uc_vm_signal_handlers_setup(uc_vm_t *vm) +/* Actually wire up the self-pipe/handler array/sigaction template needed + * for ucode-level signal() callbacks to work, independent of whether the + * embedding host opted into this via config->setup_signal_handlers. Safe + * to call more than once (a no-op once already set up for this thread). */ +void +uc_vm_signal_handlers_ensure(uc_vm_t *vm) { - uc_thread_context_t *tctx; - - memset(&vm->signal, 0, sizeof(vm->signal)); - - vm->signal.sigpipe[0] = -1; - vm->signal.sigpipe[1] = -1; - - if (!vm->config->setup_signal_handlers) - return; - - tctx = uc_thread_context_get(); + uc_thread_context_t *tctx = uc_thread_context_get(); if (tctx->signal_handler_vm) return; @@ -201,6 +195,20 @@ uc_vm_signal_handlers_setup(uc_vm_t *vm) tctx->signal_handler_vm = vm; } +static void +uc_vm_signal_handlers_setup(uc_vm_t *vm) +{ + memset(&vm->signal, 0, sizeof(vm->signal)); + + vm->signal.sigpipe[0] = -1; + vm->signal.sigpipe[1] = -1; + + if (!vm->config->setup_signal_handlers) + return; + + uc_vm_signal_handlers_ensure(vm); +} + static void uc_vm_signal_handlers_reset(uc_vm_t *vm) { @@ -225,7 +233,27 @@ uc_vm_signal_handlers_reset(uc_vm_t *vm) vm->signal.sigpipe[i] = -1; } - tctx->signal_handler_vm = NULL; + tctx->signal_handler_vm = NULL; +} + +void uc_vm_break_init(uc_vm_t *vm) +{ + vm->break_requested = false; + vm->break_notifyfd[0] = -1; + vm->break_notifyfd[1] = -1; + + if (pipe2(vm->break_notifyfd, O_CLOEXEC | O_NONBLOCK) == 0) { + /* pipe created successfully */ + } +} + +void uc_vm_break_cleanup(uc_vm_t *vm) +{ + for (size_t i = 0; i < ARRAY_SIZE(vm->break_notifyfd); i++) { + if (vm->break_notifyfd[i] > STDERR_FILENO) + close(vm->break_notifyfd[i]); + vm->break_notifyfd[i] = -1; + } } void uc_vm_init(uc_vm_t *vm, uc_parse_config_t *config) @@ -254,6 +282,8 @@ void uc_vm_init(uc_vm_t *vm, uc_parse_config_t *config) uc_vm_signal_handlers_setup(vm); + uc_vm_break_init(vm); + uc_thread_context_get()->refcount++; } @@ -265,6 +295,8 @@ void uc_vm_free(uc_vm_t *vm) uc_vm_signal_handlers_reset(vm); + uc_vm_break_cleanup(vm); + ucv_put(vm->exception.stacktrace); free(vm->exception.message); @@ -294,6 +326,11 @@ void uc_vm_free(uc_vm_t *vm) uc_vector_clear(&vm->restypes); + for (i = 0; i < vm->breakpoints.count; i++) + free(vm->breakpoints.entries[i]); + + uc_vector_clear(&vm->breakpoints); + ctx = uc_thread_context_get(); assert(ctx->refcount > 0); @@ -350,6 +387,13 @@ 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++) { + uc_breakpoint_t *bk = vm->breakpoints.entries[i]; + + if (bk != NULL && (bk->ip == NULL || bk->ip == frame->ip)) + bk->cb(vm, bk); + } + insn = frame->ip[0]; frame->ip++; @@ -897,6 +941,49 @@ uc_vm_clear_exception(uc_vm_t *vm) vm->exception.message = NULL; } +/* Well-known sentinel `uc_breakpoint_t.ip` value identifying the dedicated + * "break on uncaught exception" system breakpoint (see debug.c's BK_UNCAUGHT). + * It deliberately isn't a real bytecode address, so the ordinary + * ip-matching breakpoint dispatch in uc_vm_decode_insn() - which walks + * vm->breakpoints on every single instruction - never fires it by + * accident; it is only ever invoked explicitly, from the exception label in + * uc_vm_execute_chunk() below, at the one moment it actually applies. */ +static uint8_t uc_breakpoint_uncaught_exception_storage; +uint8_t *const UC_BREAKPOINT_UNCAUGHT_EXCEPTION = + &uc_breakpoint_uncaught_exception_storage; + +/* Non-destructively predict whether uc_vm_handle_exception()'s real unwind + * loop (below) would find a handler for the currently raised exception + * anywhere between the current callframe and `caller` (the frame depth this + * uc_vm_execute_chunk() invocation was entered at - the same boundary its + * own unwind loop stops at). Mirrors that loop's exact stopping conditions + * (a native callframe, or reaching `caller`) but only inspects state; nops + * of the stack/exception state, jumping ip. Used to decide whether to break + * into the debugger *before* unwinding starts, while the original throwing + * frame - locals, exact position - is still fully intact, since once + * uc_vm_handle_exception() starts really popping frames that's gone. */ +static bool +uc_vm_exception_would_be_caught(uc_vm_t *vm, size_t caller) +{ + for (size_t i = vm->callframes.count; i > caller; i--) { + uc_callframe_t *frame = &vm->callframes.entries[i - 1]; + + if (!frame->closure) + return false; + + uc_chunk_t *chunk = &frame->closure->function->chunk; + size_t pos = frame->ip - chunk->entries; + + for (size_t j = 0; j < chunk->ehranges.count; j++) { + if (pos >= chunk->ehranges.entries[j].from && + pos < chunk->ehranges.entries[j].to) + return true; + } + } + + return false; +} + static bool uc_vm_handle_exception(uc_vm_t *vm) { @@ -1219,7 +1306,9 @@ uc_vm_insn_load_val(uc_vm_t *vm, uc_vm_insn_t insn) case UC_RESOURCE: case UC_OBJECT: case UC_ARRAY: - uc_vm_stack_push(vm, ucv_key_get(vm, v, k)); + uc_vm_stack_push(vm, ucv_is_dict(v) + ? ucv_dict_get(vm, v, k) + : ucv_key_get(vm, v, k)); break; default: @@ -1244,7 +1333,9 @@ uc_vm_insn_peek_val(uc_vm_t *vm, uc_vm_insn_t insn) case UC_RESOURCE: case UC_OBJECT: case UC_ARRAY: - uc_vm_stack_push(vm, ucv_key_get(vm, v, k)); + uc_vm_stack_push(vm, ucv_is_dict(v) + ? ucv_dict_get(vm, v, k) + : ucv_key_get(vm, v, k)); break; default: @@ -1440,7 +1531,9 @@ uc_vm_insn_store_val(uc_vm_t *vm, uc_vm_insn_t insn) case UC_OBJECT: case UC_ARRAY: if (assert_mutable_value(vm, o)) { - uc_value_t *rv = ucv_key_set(vm, o, k, v); + uc_value_t *rv = ucv_is_dict(o) + ? ucv_dict_set(vm, o, k, v) + : ucv_key_set(vm, o, k, v); /* on success rv is a reference to the stored value that gets * pushed onto the stack; clear v so the cleanup below does not @@ -1923,9 +2016,13 @@ uc_vm_insn_update_val(uc_vm_t *vm, uc_vm_insn_t insn) if (assert_mutable_value(vm, v)) { uc_value_t *nv, *rv; - val = ucv_key_get(vm, v, k); + val = ucv_is_dict(v) + ? ucv_dict_get(vm, v, k) + : ucv_key_get(vm, v, k); nv = uc_vm_value_arith(vm, vm->arg.u8, val, inc); - rv = ucv_key_set(vm, v, k, nv); + rv = ucv_is_dict(v) + ? ucv_dict_set(vm, v, k, nv) + : ucv_key_set(vm, v, k, nv); /* on success rv is a reference to the stored value that gets * pushed onto the stack; on failure nv was not stored, so @@ -2058,10 +2155,17 @@ uc_vm_insn_sobj(uc_vm_t *vm, uc_vm_insn_t insn) uc_value_t *obj = uc_vm_stack_peek(vm, vm->arg.u32); size_t idx; - for (idx = 0; idx < vm->arg.u32; idx += 2) - ucv_key_set(vm, obj, - uc_vm_stack_peek(vm, vm->arg.u32 - idx - 1), - uc_vm_stack_peek(vm, vm->arg.u32 - idx - 2)); + if (ucv_is_dict(obj)) { + for (idx = 0; idx < vm->arg.u32; idx += 2) + ucv_dict_set(vm, obj, + uc_vm_stack_peek(vm, vm->arg.u32 - idx - 1), + uc_vm_stack_peek(vm, vm->arg.u32 - idx - 2)); + } else { + for (idx = 0; idx < vm->arg.u32; idx += 2) + ucv_key_set(vm, obj, + uc_vm_stack_peek(vm, vm->arg.u32 - idx - 1), + uc_vm_stack_peek(vm, vm->arg.u32 - idx - 2)); + } for (idx = 0; idx < vm->arg.u32; idx++) ucv_put(uc_vm_stack_pop(vm)); @@ -2072,23 +2176,51 @@ uc_vm_insn_mobj(uc_vm_t *vm, uc_vm_insn_t insn) { uc_value_t *src = uc_vm_stack_pop(vm); uc_value_t *dst = uc_vm_stack_peek(vm, 0); + bool dst_is_dict = ucv_is_dict(dst); size_t i; char *s; switch (ucv_type(src)) { case UC_OBJECT: - ; /* a label can only be part of a statement and a declaration is not a statement */ - ucv_object_foreach(src, k, v) - ucv_object_add(dst, k, ucv_get(v)); + if (ucv_is_dict(src)) { + /* spread dict into object or dict */ + ucv_dict_foreach(src, k, v) { + if (dst_is_dict) { + ucv_dict_set(vm, dst, k, ucv_get(v)); + } else { + /* convert value key to string for regular object */ + s = ucv_to_string(vm, k); + ucv_object_add(dst, s ? s : "", ucv_get(v)); + free(s); + } + } + } else if (dst_is_dict) { + /* spread regular object into dict — keys become string values */ + ucv_object_foreach(src, k, v) { + uc_value_t *key = ucv_string_new(k); + + ucv_dict_set(vm, dst, key, ucv_get(v)); + } + } else { + /* spread regular object into regular object */ + ucv_object_foreach(src, k, v) + ucv_object_add(dst, k, ucv_get(v)); + } ucv_put(src); break; case UC_ARRAY: for (i = 0; i < ucv_array_length(src); i++) { - xasprintf(&s, "%zu", i); - ucv_object_add(dst, s, ucv_get(ucv_array_get(src, i))); - free(s); + if (dst_is_dict) { + uc_value_t *key = ucv_int64_new((int64_t)i); + + ucv_dict_set(vm, dst, key, ucv_get(ucv_array_get(src, i))); + } else { + xasprintf(&s, "%zu", i); + ucv_object_add(dst, s, ucv_get(ucv_array_get(src, i))); + free(s); + } } ucv_put(src); @@ -2394,6 +2526,7 @@ uc_vm_object_iterator_next(uc_vm_t *vm, uc_vm_insn_t insn, uc_resource_t *res = (uc_resource_t *)k; uc_object_t *obj = (uc_object_t *)v; uc_object_iterator_t *iter; + bool is_dict; if (!res) { /* object is empty */ @@ -2429,7 +2562,12 @@ uc_vm_object_iterator_next(uc_vm_t *vm, uc_vm_insn_t insn, return false; } - uc_vm_stack_push(vm, ucv_string_new(iter->u.pos->k)); + is_dict = (iter->table->equal_fn == uc_dict_equal); + + if (is_dict) + uc_vm_stack_push(vm, ucv_get((uc_value_t *)iter->u.pos->k)); + else + uc_vm_stack_push(vm, ucv_string_new((char *)iter->u.pos->k)); if (insn == I_NEXTKV) uc_vm_stack_push(vm, ucv_get((uc_value_t *)iter->u.pos->v)); @@ -2578,7 +2716,9 @@ uc_vm_insn_delete(uc_vm_t *vm, uc_vm_insn_t insn) switch (ucv_type(v)) { case UC_OBJECT: if (assert_mutable_value(vm, v)) { - rv = ucv_key_delete(vm, v, k); + rv = ucv_is_dict(v) + ? ucv_dict_delete(vm, v, k) + : ucv_key_delete(vm, v, k); uc_vm_stack_push(vm, ucv_boolean_new(rv)); } @@ -2832,7 +2972,12 @@ uc_vm_signal_dispatch(uc_vm_t *vm) size_t i, j; int sig, rv; - if (!vm->config->setup_signal_handlers) + /* Check whether the signal self-pipe was actually set up, rather than + * re-checking config->setup_signal_handlers directly: the pipe may + * have been lazily initialized on demand via + * uc_vm_signal_handlers_ensure() after the fact (see lib/debug.c), + * independent of what the original config requested. */ + if (vm->signal.sigpipe[0] < 0) return EXCEPTION_NONE; for (i = 0; i < ARRAY_SIZE(vm->signal.raised); i++) { @@ -2870,6 +3015,17 @@ uc_vm_signal_dispatch(uc_vm_t *vm) return EXCEPTION_NONE; } +static uc_vm_status_t +uc_vm_exception_type_to_status(uc_vm_t *vm) +{ + switch (vm->exception.type) { + case EXCEPTION_NONE: return STATUS_OK; + case EXCEPTION_EXIT: return STATUS_EXIT; + case EXCEPTION_SYNTAX: return ERROR_COMPILE; + default: return ERROR_RUNTIME; + } +} + static uc_vm_status_t uc_vm_execute_chunk(uc_vm_t *vm) { @@ -3082,6 +3238,12 @@ uc_vm_execute_chunk(uc_vm_t *vm) case I_CALL: uc_vm_insn_call(vm, insn); + + if (vm->callframes.count == 0) + return uc_vm_exception_type_to_status(vm); + + frame = uc_vm_current_frame(vm); + chunk = frame->closure ? uc_vm_frame_chunk(frame) : NULL; break; case I_RETURN: @@ -3128,6 +3290,33 @@ uc_vm_execute_chunk(uc_vm_t *vm) return STATUS_EXIT; } + /* If a debugger has armed the dedicated "break on uncaught + * exception" system breakpoint and nothing between here and + * this invocation's original call depth would actually handle + * this exception, give it a chance to inspect the fully intact + * stack *before* uc_vm_handle_exception()'s loop below starts + * popping frames - once that happens, the original throwing + * frame's locals and exact position are gone for good. */ + if (!uc_vm_exception_would_be_caught(vm, caller)) { + for (size_t i = 0; i < vm->breakpoints.count; i++) { + uc_breakpoint_t *bk = vm->breakpoints.entries[i]; + + if (bk != NULL && bk->ip == UC_BREAKPOINT_UNCAUGHT_EXCEPTION) { + bk->cb(vm, bk); + + /* "quit" was issued from within the breakpoint's + * CLI session */ + if (vm->exception.type == EXCEPTION_EXIT) { + uc_vm_reset_callframes(vm); + + return STATUS_EXIT; + } + + break; + } + } + } + /* walk up callframes until something handles the exception or the original caller is reached */ while (!uc_vm_handle_exception(vm)) { /* no further callframe, report unhandled exception and terminate */ @@ -3150,6 +3339,12 @@ uc_vm_execute_chunk(uc_vm_t *vm) /* run handler for signal(s) delivered during previous instruction */ if (uc_vm_signal_dispatch(vm) != EXCEPTION_NONE) goto exception; + + /* check for break request */ + if (vm->break_requested) { + vm->break_requested = false; + return STATUS_BREAK; + } } return STATUS_OK; @@ -3205,6 +3400,13 @@ uc_vm_execute(uc_vm_t *vm, uc_program_t *program, uc_value_t **retval) break; + case STATUS_BREAK: + /* Break requested - exit gracefully without error */ + if (retval) + *retval = NULL; + + break; + default: if (vm->exhandler) vm->exhandler(vm, &vm->exception); @@ -3385,3 +3587,47 @@ uc_vm_signal_notifyfd(uc_vm_t *vm) { return vm->signal.sigpipe[0]; } + +bool +uc_vm_break_requested(uc_vm_t *vm) +{ + return vm->break_requested; +} + +void +uc_vm_break_request(uc_vm_t *vm) +{ + vm->break_requested = true; + + if (vm->break_notifyfd[1] >= 0) { + char c = 'B'; + if (write(vm->break_notifyfd[1], &c, 1) == -1) {} + } +} + +int +uc_vm_break_notifyfd(uc_vm_t *vm) +{ + return vm->break_notifyfd[0]; +} + +uc_vm_status_t +uc_vm_resume(uc_vm_t *vm) +{ + uc_vm_status_t status = uc_vm_execute_chunk(vm); + + switch (status) { + case STATUS_OK: + case STATUS_EXIT: + case STATUS_BREAK: + break; + + default: + if (vm->exhandler) + vm->exhandler(vm, &vm->exception); + + break; + } + + return status; +}