From 10402927cdc37b9f49efd23281646c173e67c84c Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Fri, 31 May 2024 09:54:41 +0200 Subject: [PATCH 01/11] vm: reliably stop execution if inner function invoked exit() When an inner function invoked exit() in a ucode > native > ucode call stack situation, the VM did end up with an empty callframe stack after the native function returned, leading to subsequent invalid memory accesses. Properly deal with situation similar to how we also check for an empty call stack after processing I_RETURN and additionally translate the current exception type to a status return value. Signed-off-by: Jo-Philipp Wich --- vm.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/vm.c b/vm.c index fc322c5d..8933c6b4 100644 --- a/vm.c +++ b/vm.c @@ -2768,6 +2768,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) { @@ -2968,6 +2979,10 @@ uc_vm_execute_chunk(uc_vm_t *vm) case I_CALL: case I_QCALL: 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; @@ -2975,6 +2990,10 @@ uc_vm_execute_chunk(uc_vm_t *vm) case I_MCALL: case I_QMCALL: uc_vm_insn_mcall(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; From 69a7dc6c006bf19e92664170f3a986185cd58e4d Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Thu, 20 Jun 2024 16:07:34 +0200 Subject: [PATCH 02/11] platform: add SIGWINCH signal name Add missing definition for the SIGWINCH signal number in order to allow ucode programs to subscribe to terminal size change signals. Signed-off-by: Jo-Philipp Wich --- platform.c | 3 +++ 1 file changed, 3 insertions(+) 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 }; From 0fbf2a3621d6a08cd07419c753174afb9ca9afca Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Wed, 22 May 2024 21:59:53 +0200 Subject: [PATCH 03/11] chunk: optimize offset info encoding Use a two byte structure to encode { byte offset, insns count } offset info tuples and introduce additional flags plus related api functions to encode statement start- and end boundaries in offsetinfo. The new encoding format more than halves the required memory footprint in many cases, especially for whitespace- and comment heavy sources. Signed-off-by: Jo-Philipp Wich --- chunk.c | 108 +++++++++++++++++++++++++++--------------- include/ucode/chunk.h | 5 +- include/ucode/types.h | 7 ++- 3 files changed, 79 insertions(+), 41 deletions(-) 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/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/types.h b/include/ucode/types.h index f149b98b..15209c7a 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; From 2da83520720fcc738347bd38798c3b6e93d2ba5c Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Thu, 20 Jun 2024 16:32:13 +0200 Subject: [PATCH 04/11] compiler: encode statement boundaries in offset info Utilize the new chunk offset info facilities to encode the start and end offsets of logical statements in the offset information vector of the debug data area. This information can be used later by debuggers to step through logical statements instead of single instructions. Signed-off-by: Jo-Philipp Wich --- compiler.c | 136 ++++++++++++++++++++++++++------------- include/ucode/compiler.h | 3 +- 2 files changed, 95 insertions(+), 44 deletions(-) diff --git a/compiler.c b/compiler.c index 6e417d2b..b9a13303 100644 --- a/compiler.c +++ b/compiler.c @@ -240,6 +240,7 @@ uc_compiler_parse_advance(uc_compiler_t *compiler) { ucv_put(compiler->parser->prev.uv); compiler->parser->prev = compiler->parser->curr; + compiler->parser->prev_endpos = compiler->parser->curr_endpos; while (true) { uc_token_t *tok = uc_lexer_next_token(&compiler->parser->lex); @@ -253,6 +254,7 @@ uc_compiler_parse_advance(uc_compiler_t *compiler) } compiler->parser->curr = *tok; + compiler->parser->curr_endpos = compiler->parser->lex.source->off; if (compiler->parser->curr.type != TK_ERROR) break; @@ -465,6 +467,22 @@ uc_compiler_reladdr(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_endpos)); +} + static size_t uc_compiler_emit_insn(uc_compiler_t *compiler, size_t srcpos, uc_vm_insn_t insn) { @@ -1309,9 +1327,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 @@ -1406,8 +1430,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 */ @@ -1581,7 +1607,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; @@ -1751,7 +1777,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); } @@ -1764,7 +1790,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); } @@ -1944,7 +1970,7 @@ uc_compiler_compile_subscript(uc_compiler_t *compiler) compiler->exprstack->flags |= optional_chaining ? F_OPTCHAINING : 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; @@ -2227,15 +2253,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 @@ -2273,7 +2303,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 */ @@ -2297,7 +2327,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); @@ -2382,7 +2412,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 */ @@ -2444,7 +2474,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); @@ -2561,7 +2592,7 @@ uc_compiler_compile_for_count(uc_compiler_t *compiler, bool local, uc_token_t *v } /* ... otherwise an unrelated expression */ else { - uc_compiler_compile_expression(compiler); + uc_compiler_compile_expression(compiler, false); uc_compiler_emit_insn(compiler, 0, I_POP); } } @@ -2570,10 +2601,11 @@ uc_compiler_compile_for_count(uc_compiler_t *compiler, bool local, uc_token_t *v } /* ... 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); @@ -2581,7 +2613,7 @@ uc_compiler_compile_for_count(uc_compiler_t *compiler, bool local, uc_token_t *v 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); } @@ -2596,7 +2628,7 @@ uc_compiler_compile_for_count(uc_compiler_t *compiler, bool local, uc_token_t *v 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); } @@ -2647,6 +2679,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 */ @@ -2707,7 +2741,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); @@ -2753,7 +2787,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: @@ -3003,7 +3037,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); @@ -3046,7 +3080,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) { @@ -3087,32 +3121,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; @@ -3201,8 +3242,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; } @@ -3214,7 +3258,7 @@ uc_compiler_compile_export(uc_compiler_t *compiler) else if (uc_compiler_parse_match(compiler, TK_FUNC)) uc_compiler_compile_funcdecl(compiler); 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 '{'"); @@ -3236,6 +3280,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 * @@ -3640,6 +3686,8 @@ uc_compiler_compile_import(uc_compiler_t *compiler) return; } + uc_compiler_emit_stmt_start(compiler, &compiler->parser->prev); + /* import { ... } from */ if (uc_compiler_parse_match(compiler, TK_LBRACE)) { uc_compiler_compile_importlist(compiler, namelist); @@ -3696,6 +3744,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); } diff --git a/include/ucode/compiler.h b/include/ucode/compiler.h index db3d6768..376b9baf 100644 --- a/include/ucode/compiler.h +++ b/include/ucode/compiler.h @@ -101,8 +101,9 @@ typedef struct { uc_parse_config_t *config; uc_lexer_t lex; uc_token_t prev, curr; - bool synchronizing; uc_stringbuf_t *error; + size_t prev_endpos, curr_endpos; + bool synchronizing; } uc_parser_t; typedef struct uc_compiler { From f9c27bdfcd8012ba8824ee10ce199b596c094bc5 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Thu, 20 Jun 2024 16:36:33 +0200 Subject: [PATCH 05/11] vm: add breakpoint primitives Introduce low level facilities for registering breakpoints in the running VM context. The breakpoint primitives allow invoking provided callback functions when the VM reaches an associated instruction address. This functionality provides the foundation for building more thorough interactive debug functionality on top. Signed-off-by: Jo-Philipp Wich --- include/ucode/types.h | 8 +++++++- vm.c | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/include/ucode/types.h b/include/ucode/types.h index 15209c7a..0fc92630 100644 --- a/include/ucode/types.h +++ b/include/ucode/types.h @@ -310,8 +310,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; @@ -328,7 +334,7 @@ struct uc_vm { uc_source_t *sources; uc_weakref_t values; uc_resource_types_t restypes; - char _reserved[sizeof(uc_modexports_t)]; + uc_breakpoints_t breakpoints; union { uint32_t u32; int32_t s32; diff --git a/vm.c b/vm.c index 8933c6b4..cf3f5933 100644 --- a/vm.c +++ b/vm.c @@ -271,6 +271,11 @@ void uc_vm_free(uc_vm_t *vm) free(vm->restypes.entries[i]); uc_vector_clear(&vm->restypes); + + for (i = 0; i < vm->breakpoints.count; i++) + free(vm->breakpoints.entries[i]); + + uc_vector_clear(&vm->breakpoints); } static uc_chunk_t * @@ -312,6 +317,7 @@ uc_vm_is_strict(uc_vm_t *vm) static uc_vm_insn_t uc_vm_decode_insn(uc_vm_t *vm, uc_callframe_t *frame, uc_chunk_t *chunk) { + uc_breakpoints_t *bks = &vm->breakpoints; uc_vm_insn_t insn; #ifndef NDEBUG @@ -320,6 +326,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 < bks->count; i++) { + uc_breakpoint_t *bk = bks->entries[i]; + + if (bk != NULL && (bk->ip == NULL || bk->ip == frame->ip)) + bk->cb(vm, bk); + } + insn = frame->ip[0]; frame->ip++; From cf8efdc3ecfbcb53e2060e54ef974ec053503847 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Thu, 20 Jun 2024 16:51:01 +0200 Subject: [PATCH 06/11] vm: export insn format table Publicly export the instruction format table in order to make it useable for libucode.so users, such as dynamically loaded debug libraries. Signed-off-by: Jo-Philipp Wich --- include/ucode/vm.h | 2 +- vm.c | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/include/ucode/vm.h b/include/ucode/vm.h index 3cb6dc07..2d8b9dc3 100644 --- a/include/ucode/vm.h +++ b/include/ucode/vm.h @@ -122,7 +122,7 @@ typedef enum { #define GC_DEFAULT_INTERVAL 1000 -extern uint32_t insns[__I_MAX]; +extern const int8_t 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); diff --git a/vm.c b/vm.c index cf3f5933..ee4cae30 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, @@ -336,9 +336,9 @@ uc_vm_decode_insn(uc_vm_t *vm, uc_callframe_t *frame, uc_chunk_t *chunk) insn = frame->ip[0]; frame->ip++; - assert(frame->ip + abs(insn_operand_bytes[insn]) <= end); + assert(frame->ip + abs(uc_vm_insn_format[insn]) <= end); - switch (insn_operand_bytes[insn]) { + switch (uc_vm_insn_format[insn]) { case 0: break; @@ -376,7 +376,7 @@ uc_vm_decode_insn(uc_vm_t *vm, uc_callframe_t *frame, uc_chunk_t *chunk) break; default: - fprintf(stderr, "Unhandled operand format: %" PRId8 "\n", insn_operand_bytes[insn]); + fprintf(stderr, "Unhandled operand format: %" PRId8 "\n", uc_vm_insn_format[insn]); abort(); } @@ -730,7 +730,7 @@ uc_dump_insn(uc_vm_t *vm, uint8_t *pos, uc_vm_insn_t insn) fprintf(stderr, "%08zx %s", pos - chunk->entries, insn_names[insn]); - switch (insn_operand_bytes[insn]) { + switch (uc_vm_insn_format[insn]) { case 0: break; @@ -763,7 +763,7 @@ uc_dump_insn(uc_vm_t *vm, uint8_t *pos, uc_vm_insn_t insn) break; default: - fprintf(stderr, " (unknown operand format: %" PRId8 ")", insn_operand_bytes[insn]); + fprintf(stderr, " (unknown operand format: %" PRId8 ")", uc_vm_insn_format[insn]); break; } From 8af6d7fff97222893def54a612c273f27e21c494 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Thu, 20 Jun 2024 16:53:28 +0200 Subject: [PATCH 07/11] lib: fix potential invalid memory access in uc_require_ucode() When additional ucode scripts are loaded through require() or similar means, and the required code invokes exit(), the VM will clear the stack before returning, so we must ensure that the stack size matches our expectation before we're trying to pop values from it after returning from require. Signed-off-by: Jo-Philipp Wich --- lib.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib.c b/lib.c index 6415e3c7..98277b4e 100644 --- a/lib.c +++ b/lib.c @@ -2709,9 +2709,11 @@ uc_require_ucode(uc_vm_t *vm, const char *path, uc_value_t *scope, uc_value_t ** *res = uc_callfunc(vm, 3); - 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); + } } vm->config = prev_config; From f2dd117cef0d6a1011f7e2f6e7728670030e4e87 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Thu, 20 Jun 2024 23:04:43 +0200 Subject: [PATCH 08/11] debug: add interactive command line debugger Implement an interactive command line debugger within the debug module which can be started by invoking the `debugger()` function. The debugger offers common features such as the ability to set breakpoints, stepping through commands, examining call stacks and variables as well as byte code disassembly source code highlighting. Signed-off-by: Jo-Philipp Wich --- lib/debug.c | 4896 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 4883 insertions(+), 13 deletions(-) diff --git a/lib/debug.c b/lib/debug.c index 0b227d9f..2782ee98 100644 --- a/lib/debug.c +++ b/lib/debug.c @@ -60,7 +60,13 @@ #include #include #include +#include #include +#include +#include +#include +#include +#include #ifdef HAVE_ULOOP #include @@ -71,6 +77,7 @@ #include "ucode/module.h" #include "ucode/platform.h" +#include "ucode/compiler.h" static char *memdump_signal = "USR2"; @@ -615,6 +622,7 @@ debug_setup_memdump(uc_vm_t *vm) { uc_cfn_ptr_t ucsignal = uc_stdlib_function("signal"); uc_value_t *memdump = ucv_cfunction_new("memdump", debug_handle_memdump); + uc_value_t *handler; char *ev; ev = getenv("UCODE_DEBUG_MEMDUMP_PATH"); @@ -628,11 +636,14 @@ debug_setup_memdump(uc_vm_t *vm) uc_vm_stack_push(vm, ucv_string_new(memdump_signal)); uc_vm_stack_push(vm, memdump); - if (ucsignal(vm, 2) != memdump) + handler = ucsignal(vm, 2); + + if (handler != memdump) fprintf(stderr, "Unable to install debug signal handler\n"); ucv_put(uc_vm_stack_pop(vm)); ucv_put(uc_vm_stack_pop(vm)); + ucv_put(handler); } static void @@ -1614,20 +1625,4879 @@ 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 enum { + BK_ONCE, + BK_USER, + BK_STEP, + BK_CATCH, +} debug_breakpoint_kind_t; + +typedef struct debug_breakpoint { + uc_breakpoint_t bk; + uc_function_t *fn; + size_t depth; + debug_breakpoint_kind_t kind; +} debug_breakpoint_t; + +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; + +typedef enum { + ARGTYPE_NONE, + ARGTYPE_ERROR, + ARGTYPE_STRING, + ARGTYPE_NUMBER, +} argtype_t; + +typedef struct { + argtype_t type; + size_t off; + size_t nv; + char *sv; +} arg_t; + +typedef struct { + size_t count; + char **entries; +} suggestions_t; + +typedef struct { + size_t pos, len, size, width; + uint32_t *chars; +} termline_t; + +static struct { + bool initialized; + char data[128]; + size_t pos, fill; + size_t rows, cols, col_offset; + struct termios orig_settings, curr_settings; + struct { + size_t count; + termline_t *entries; + } history; + struct { + size_t count; + regex_t *entries; + } patterns; +} termstate; + +enum { + HOME_KEY = 0x110000, + END_KEY, + DEL_KEY, + PAGE_UP, + PAGE_DOWN, + ARROW_UP, + ARROW_DOWN, + ARROW_LEFT, + ARROW_RIGHT, + CTRL_UP, + CTRL_DOWN, + CTRL_LEFT, + CTRL_RIGHT, }; -void uc_module_init(uc_vm_t *vm, uc_value_t *scope) +#define HISTORY_SIZE 100 + +enum { + BOLD = (1 << 0), + FAINT = (1 << 1), + ULINE = (1 << 2), +}; + +typedef enum { + 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_BLACK = 40, + BG_GRAY = 100, +} bg_color_t; + +typedef struct { + fg_color_t fg; + bg_color_t bg; + uint32_t styles; +} style_t; + +#define uc_vector_add(vec, ...) ({ \ + uc_vector_push((vec), ((typeof((vec)->entries[0]))__VA_ARGS__)); \ + uc_vector_last(vec); \ +}) + +static void +cs(uc_stringbuf_t *sb, style_t *style) { - uc_function_list_register(scope, debug_fns); + int codes[8] = { 0 }; + size_t i = 0; - debug_setup(vm); + if (style == NULL) { + printbuf_strappend(sb, "\033[0m"); + 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; + + printbuf_strappend(sb, "\033["); + + for (size_t n = 0; n < i; n++) + sprintbuf(sb, "%s%d", n ? ";" : "", codes[n]); + + printbuf_strappend(sb, "m"); +} + +static uc_callframe_t * +uc_debug_curr_frame(uc_vm_t *vm, size_t off) +{ + if (off > vm->callframes.count) + return NULL; + + for (size_t i = vm->callframes.count - off; i > 0; i--) + if (vm->callframes.entries[i-1].closure) + return &vm->callframes.entries[i-1]; + + return NULL; +} + +static bool cmd_help(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); +static bool cmd_break(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); +static bool cmd_delete(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); +static bool cmd_list(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); +static bool cmd_next(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); +static bool cmd_step(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); +static bool cmd_continue(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); +static bool cmd_return(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); +static bool cmd_backtrace(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); +static bool cmd_variables(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); +static bool cmd_sources(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); +static bool cmd_quit(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); +static bool cmd_print(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); +static bool cmd_lines(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); +static bool cmd_throw(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); +static bool cmd_disasm(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv); + +static const struct { + const char *command; + bool (*cb)(uc_vm_t *, debug_breakpoint_t *, size_t, arg_t *); + const char *help; +} commands[] = { + { "help\0", cmd_help, + "Print help information." }, + { "break\0", cmd_break, + "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\0", cmd_delete, + "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", cmd_list, + "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\0", cmd_next, + "Execute the next statement and stop again." + }, + { "step\0", cmd_step, + "Execute the next statement, in case of function calls step into the " + "called function and stop there." + }, + { "continue\0", cmd_continue, + "Continue execution until the next breakpoint or end of program." + }, + { "return\0", cmd_return, + "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", cmd_backtrace, + "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\0", cmd_variables, + "Print local variables and their contents for the current execution " + "context. Internal variables which are unreachable by script code " + "are colored grey, upvalues (variables captured from parent scopes) " + "are colored blue and ordinary variables use the default color.\n\n" + "If the optional 'full' argument is specified, the complete value for " + "each variable is shown, instead of an abbreviated line truncated to " + "the current terminal width.\n\n" + "Examples:\n" + " variables # Print local variables\n" + " variables full # Print variables with complete content" + }, + { "sources\0src\0", cmd_sources, + "Print a list of loaded source buffers.\n" + }, + { "print\0", cmd_print, + "Evaluate an ucode expression and print the resulting value.\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" + }, + { "lines\0ln\0", cmd_lines, + "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 preceeding and following lines to print may be " + "specified as second and third argument respecitely. 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", cmd_throw, + "Raise an exception at the current instruction offset.\n\n" + "Examples:\n" + " throw \"Message\" # Throw exception with given message" + }, + { "disassemble\0disasm\0", cmd_disasm, + "Disassembe 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\n" + }, + { "quit\0", cmd_quit, + "Forcibly terminate the currently running program. The termination " + "happens in the same manner as if 'exit()' has been called from " + "script code." + } +}; + +/* -- 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 size_t +printbuf_cs(uc_stringbuf_t *sb, const char *fmt, ...) +{ + uc_stringbuf_t fmtbuf = { 0 }; + style_t *styles[8] = { 0 }; + uint8_t nstyles = 0; + va_list ap, ap1; + + for (const char *p = fmt; *p; p++) + if (*p >= '\1' && *p <= '\7' && *p > nstyles) + nstyles = *p; + + va_start(ap, fmt); + + for (uint8_t i = 0; i < nstyles; i++) + styles[i] = va_arg(ap, style_t *); + + const char *p, *l; + + for (p = l = fmt; *p; p++) { + if ((*p >= '\1' && *p <= '\7') || *p == '\177') { + printbuf_memappend_fast((&fmtbuf), l, p - l); + cs(&fmtbuf, (*p <= '\7' ? styles[(size_t)*p - 1] : NULL)); + l = p + 1; + } + } + + printbuf_memappend_fast((&fmtbuf), l, p - l); + + va_copy(ap1, ap); + int len = vsnprintf(NULL, 0, fmtbuf.buf, ap1); + va_end(ap1); + + if (len > 0) { + printbuf_memset(sb, sb->bpos + len - 1, '\0', 1); + vsnprintf(sb->buf + sb->bpos - len, len + 1, fmtbuf.buf, ap); + } + + va_end(ap); + + free(fmtbuf.buf); + + return (len > 0) ? len : 0; +} + + +static void +bk_enter_cli(uc_vm_t *vm, uc_breakpoint_t *bk); + +static void +bk_handle_catch(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; +} + +static bool +free_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--; + + free(bk); + + return found; +} + +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_cli; + 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 false; +} + +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 void +term_dimensions(void) +{ + struct winsize w; + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) { + termstate.rows = w.ws_row; + termstate.cols = w.ws_col; + } + else { + termstate.rows = 26; + termstate.cols = 80; + } +} + +static size_t +term_width(void) +{ + if (termstate.cols == 0) + term_dimensions(); + + return termstate.cols; +} + +static void +term_reset(void) +{ + if (tcsetattr(STDOUT_FILENO, TCSAFLUSH, &termstate.orig_settings) == -1) + fprintf(stderr, "tcsetattr(): %m\n"); + + while (termstate.patterns.count > 0) { + regex_t *re = &termstate.patterns.entries[--termstate.patterns.count]; + if (re) regfree(re); + } + + while (termstate.history.count > 0) + free(termstate.history.entries[--termstate.history.count].chars); + + uc_vector_clear(&termstate.patterns); + uc_vector_clear(&termstate.history); +} + +static bool +term_raw(void) +{ + if (tcgetattr(STDOUT_FILENO, &termstate.orig_settings) == -1) { + fprintf(stderr, "tcgetattr(): %m\n"); + + return false; + } + + atexit(term_reset); + + termstate.curr_settings = termstate.orig_settings; + + termstate.curr_settings.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + termstate.curr_settings.c_cflag |= (CS8); + termstate.curr_settings.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + termstate.curr_settings.c_cc[VMIN] = 0; + termstate.curr_settings.c_cc[VTIME] = 1; + + if (tcsetattr(STDOUT_FILENO, TCSAFLUSH, &termstate.curr_settings) == -1) { + fprintf(stderr, "tcsetattr(): %m\n"); + + return false; + } + + return true; +} + +static bool +term_isig(bool enable) +{ + struct termios t; + + if (tcgetattr(STDOUT_FILENO, &t) == -1) { + fprintf(stderr, "tcgetattr(): %m\n"); + + return false; + } + + if (enable) + t.c_lflag |= ISIG; + else + t.c_lflag &= ~ISIG; + + if (tcsetattr(STDOUT_FILENO, TCSAFLUSH, &t) == -1) { + fprintf(stderr, "tcsetattr(): %m\n"); + + return false; + } + + return true; +} + +static ssize_t +fgetline(FILE *stream, char **buf, size_t *bufsize) +{ + ssize_t n = 0; + + while (true) { + n = getline(buf, bufsize, stream); + + if (n == -1 && errno == EINTR) { + clearerr(stream); + continue; + } + + break; + } + + return n; +} + +static int +term_getc_raw(void) +{ + ssize_t rlen; + + if (termstate.pos >= termstate.fill) { + while (true) { + rlen = read(STDIN_FILENO, termstate.data, sizeof(termstate.data)); + + if (rlen == -1) { + if (errno == EINTR) + continue; + + return -1; + } + + if (rlen == 0) + continue; + + termstate.fill = rlen; + termstate.pos = 0; + break; + } + } + + return termstate.data[termstate.pos++]; +} + +static bool is_utf8_2b(char c) { return (c & 0xe0) == 0xc0; } +static bool is_utf8_3b(char c) { return (c & 0xf0) == 0xe0; } +static bool is_utf8_4b(char c) { return (c & 0xf8) == 0xf0; } +static bool is_utf8_ct(char c) { return (c & 0xc0) == 0x80; } + +static int +term_getc(void) +{ + int chr = term_getc_raw(); + int seq[5]; + + /* escape sequence */ + if (chr == '\033') { + if ((seq[0] = term_getc_raw()) == -1) return '\033'; + if ((seq[1] = term_getc_raw()) == -1) return '\033'; + + switch (seq[0]) { + case '[': + switch (seq[1]) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + if ((seq[2] = term_getc_raw()) == -1) return '\033'; + + switch (seq[2]) { + case '~': + switch (seq[1]) { + case '1': return HOME_KEY; + case '3': return DEL_KEY; + case '4': return END_KEY; + case '5': return PAGE_UP; + case '6': return PAGE_DOWN; + case '7': return HOME_KEY; + case '8': return END_KEY; + } + break; + + case ';': + if ((seq[3] = term_getc_raw()) == -1) return '\033'; + + switch (seq[3]) { + case '5': + if ((seq[4] = term_getc_raw()) == -1) return '\033'; + + switch (seq[4]) { + case 'A': return CTRL_UP; + case 'B': return CTRL_DOWN; + case 'C': return CTRL_RIGHT; + case 'D': return CTRL_LEFT; + } + break; + } + break; + } + break; + + case 'A': return ARROW_UP; + case 'B': return ARROW_DOWN; + case 'C': return ARROW_RIGHT; + case 'D': return ARROW_LEFT; + case 'H': return HOME_KEY; + case 'F': return END_KEY; + } + break; + + case 'O': + switch (seq[1]) { + case 'H': return HOME_KEY; + case 'F': return END_KEY; + } + break; + } + + return '\033'; + } + + /* two byte utf-8 sequence */ + if (is_utf8_2b(chr) && + is_utf8_ct(seq[0] = term_getc_raw())) + { + return ((chr & 0x1f) << 6) | + (seq[0] & 0x3f); + } + + /* three byte utf-8 sequence */ + if (is_utf8_3b(chr) && + is_utf8_ct(seq[0] = term_getc_raw()) && + is_utf8_ct(seq[1] = term_getc_raw())) + { + return ((chr & 0x0f) << 12) | + ((seq[0] & 0x3f) << 6) | + (seq[1] & 0x3f); + } + + /* four byte utf-8 sequence */ + if (is_utf8_4b(chr) && + is_utf8_ct(seq[0] = term_getc_raw()) && + is_utf8_ct(seq[1] = term_getc_raw()) && + is_utf8_ct(seq[2] = term_getc_raw())) + { + return ((chr & 0x07) << 18) | + ((seq[0] & 0x3f) << 12) | + ((seq[1] & 0x3f) << 6) | + (seq[2] & 0x3f); + } + + return chr; +} + +static bool +term_write(const char *s, size_t len) +{ + ssize_t wlen = write(STDOUT_FILENO, s, len); + + return (wlen > -1 && (size_t)wlen == len); +} + +#define term_print(x) term_write(x, sizeof(x) - 1) +#define term_printf(fmt, ...) dprintf(STDOUT_FILENO, fmt, __VA_ARGS__) + +static void +uc_vector_addcp(void *vec, uint32_t cp) +{ + struct { size_t count; char *entries; } *v = vec; + + if (cp <= 0x7F) { + uc_vector_add(v, cp); + } + else if (cp <= 0x7FF) { + uc_vector_add(v, ((cp >> 6) & 0x1F) | 0xC0); + uc_vector_add(v, ( cp & 0x3F) | 0x80); + } + else if (cp <= 0xFFFF) { + uc_vector_add(v, ((cp >> 12) & 0x0F) | 0xE0); + uc_vector_add(v, ((cp >> 6) & 0x3F) | 0x80); + uc_vector_add(v, ( cp & 0x3F) | 0x80); + } + else if (cp <= 0x10FFFF) { + uc_vector_add(v, ((cp >> 18) & 0x07) | 0xF0); + uc_vector_add(v, ((cp >> 12) & 0x3F) | 0x80); + uc_vector_add(v, ((cp >> 6) & 0x3F) | 0x80); + uc_vector_add(v, ( cp & 0x3F) | 0x80); + } +} + +static bool +term_line_parsearg(termline_t *line, size_t *off, arg_t *arg, bool silent) +{ + struct { size_t count; char *entries; } buf = { 0 }, nesting = { 0 }; + uint32_t *end, *cp, q; + unsigned long n; + bool esc; + + if (line == NULL || *off >= line->width) { + arg->type = ARGTYPE_NONE; + arg->off = line->width; + arg->sv = NULL; + arg->nv = 0; + + return false; + } + + end = line->chars + line->width; + cp = line->chars + *off; + + while (cp < end && strchr(" \t\r\n", *cp) != NULL) + cp++; + + arg->off = cp - line->chars; + + if (cp < end && strchr("\"'", *cp) != NULL) { + for (esc = false, q = *cp++; cp < end; cp++) { + if (esc) { + if (cp[0] >= '0' && cp[0] <= '7') { + int n = cp[0] - '0'; + int i = 0; + + if (cp[1] >= '0' && cp[1] <= '7') { + n = n * 8 + (cp[1] - '0'); + i++; + + if (cp[2] >= '0' && cp[2] <= '7') { + n = n * 8 + (cp[2] - '0'); + i++; + } + } + + if (n <= 255) { + uc_vector_addcp(&buf, n); + } + else { + uc_vector_add(&buf, cp[-1]); + uc_vector_add(&buf, cp[0]); + if (i > 0) uc_vector_addcp(&buf, cp[1]); + if (i > 1) uc_vector_addcp(&buf, cp[2]); + } + + cp += i; + } + else if (cp[0] == 'x') { + char c = cp[1]|32; + char d = c ? cp[2]|32 : 0; + + if (((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) && + ((d >= '0' && d <= '9') || (d >= 'a' && d <= 'f'))) + { + uc_vector_add(&buf, + (c > '9' ? 10 + c - 'a' : c - '0') * 16 + + (d > '9' ? 10 + d - 'a' : d - '0')); + } + else { + uc_vector_add(&buf, cp[-1]); + uc_vector_add(&buf, cp[0]); + if (c) uc_vector_addcp(&buf, cp[1]); + if (d) uc_vector_addcp(&buf, cp[2]); + } + + cp += !!c + !!d; + } + else { + switch (cp[0]) { + case 'n': uc_vector_add(&buf, '\n'); break; + case 't': uc_vector_add(&buf, '\t'); break; + case 'r': uc_vector_add(&buf, '\r'); break; + case 'b': uc_vector_add(&buf, '\b'); break; + default: uc_vector_addcp(&buf, *cp); break; + } + } + + esc = false; + continue; + } + + if (*cp == '\\') { + esc = true; + continue; + } + + if (*cp == q) + break; + + uc_vector_addcp(&buf, *cp); + } + + *off = cp - line->chars; + + uc_vector_add(&buf, 0); + + arg->sv = buf.entries, buf.entries = NULL; + arg->nv = buf.count; + + uc_vector_clear(&buf); + + if (esc == true || cp == end || *cp != q) { + if (!silent) + term_print("Unterminated string\n"); + + arg->type = ARGTYPE_ERROR; + } + else { + arg->type = ARGTYPE_STRING; + } + + return true; + } + + for (n = 0; cp < end && *cp >= '0' && *cp <= '9'; cp++) { + uint32_t d = *cp - '0'; + + uc_vector_add(&buf, *cp); + + if (n > ULONG_MAX / 10) { + n = ULONG_MAX; + continue; + } + + n *= 10; + + if (n > ULONG_MAX - d) { + n = ULONG_MAX; + continue; + } + + n += d; + } + + *off = cp - line->chars; + + if (buf.count > 0 && (cp == end || strchr(" \t\r\n", *cp) != NULL)) { + uc_vector_add(&buf, 0); + + arg->type = ARGTYPE_NUMBER; + arg->sv = buf.entries, buf.entries = NULL; + arg->nv = n; + + uc_vector_clear(&buf); + + return true; + } + + for (esc = false, q = 0; cp < end; cp++) { + if (esc) { + esc = false; + } + else if (*cp == '\\') { + esc = true; + } + else if (q != 0 && *cp == q) { + q = 0; + } + else if (q == 0) { + switch (*cp) { + case '(': uc_vector_push(&nesting, ')'); break; + case '{': uc_vector_push(&nesting, '}'); break; + case '[': uc_vector_push(&nesting, ']'); break; + + case '"': + case '\'': + q = *cp; + break; + + case ']': + case '}': + case ')': + if (nesting.count > 0 && *uc_vector_last(&nesting) == (char)*cp) + nesting.count--; + + break; + } + } + + if (strchr(" \t\r\n", *cp) && nesting.count == 0 && esc == false) + break; + + uc_vector_addcp(&buf, *cp); + } + + uc_vector_clear(&nesting); + + *off = cp - line->chars; + + n = buf.count; + + uc_vector_add(&buf, 0); + + arg->sv = buf.entries, buf.entries = NULL; + arg->nv = n; + + uc_vector_clear(&buf); + + if (esc == true || q != 0) { + if (!silent) + term_print("Unterminated string\n"); + + arg->type = ARGTYPE_ERROR; + } + else if (n > 0 || silent == true) { + arg->type = ARGTYPE_STRING; + } + else { + free(arg->sv); + + arg->type = ARGTYPE_NONE; + arg->sv = NULL; + + return false; + } + + return true; +} + +static size_t +term_line_toargv(termline_t *line, bool silent, arg_t **argp) +{ + struct { size_t count; arg_t *entries; } argv = { 0 }; + size_t off = 0; + + while (true) { + arg_t arg; + argtype_t t = term_line_parsearg(line, &off, &arg, silent); + + if (t == ARGTYPE_NONE) + break; + + uc_vector_add(&argv, arg); + } + + *argp = argv.entries; + + return argv.count; +} + +static size_t +term_line_fromstr(termline_t *line, size_t from, char *s, size_t len) +{ + size_t needed = 0; + + for (const char *p = s, *e = s + len; p < e; needed++) { + if (is_utf8_2b(p[0]) && is_utf8_ct(p[1])) + p += 2; + else if (is_utf8_3b(p[0]) && is_utf8_ct(p[1]) && + is_utf8_ct(p[2])) + p += 3; + else if (is_utf8_4b(p[0]) && is_utf8_ct(p[1]) && + is_utf8_ct(p[2]) && is_utf8_ct(p[3])) + p += 4; + else + p++; + } + + if (from + needed > line->size) { + line->size = ((from + needed + 127) >> 7) << 7; + line->chars = xrealloc(line->chars, line->size * sizeof(*line->chars)); + } + + uint32_t *cp = line->chars + from; + + for (const char *p = s, *e = s + len; p < e; cp++) { + if (is_utf8_2b(p[0]) && is_utf8_ct(p[1])) { + *cp = ((*p++ & 0x1f) << 6); + *cp |= (*p++ & 0x3f); + } + else if (is_utf8_3b(p[0]) && is_utf8_ct(p[1]) && + is_utf8_ct(p[2])) { + *cp = ((*p++ & 0x0f) << 12); + *cp |= ((*p++ & 0x3f) << 6); + *cp |= (*p++ & 0x3f); + } + else if (is_utf8_4b(p[0]) && is_utf8_ct(p[1]) && + is_utf8_ct(p[2]) && is_utf8_ct(p[3])) { + *cp = ((*p++ & 0x07) << 18); + *cp |= ((*p++ & 0x3f) << 12); + *cp |= ((*p++ & 0x3f) << 6); + *cp |= (*p++ & 0x3f); + } + else { + *cp = *p++; + } + } + + line->width = cp - line->chars; + + return line->width; +} + +static bool +term_line_setcur(termline_t *line, size_t pos) +{ + size_t columns = term_width(); + size_t from_row = (termstate.col_offset + line->pos) / columns; + size_t from_col = ((termstate.col_offset + line->pos) % columns) + 1; + size_t to_row = (termstate.col_offset + pos) / columns; + size_t to_col = ((termstate.col_offset + pos) % columns) + 1; + size_t len = 0; + char buf[64]; + + if (from_row > to_row) + len = snprintf(buf, sizeof(buf), "\033[%zuA", from_row - to_row); + else if (from_row < to_row) + len = snprintf(buf, sizeof(buf), "\033[%zuB", to_row - from_row); + + if (from_col > to_col) + len += snprintf(buf + len, sizeof(buf) - len, + "\033[%zuD", from_col - to_col); + else if (from_col < to_col) + len += snprintf(buf + len, sizeof(buf) - len, + "\033[%zuC", to_col - from_col); + + line->pos = pos; + + return (len == 0 || term_write(buf, len) == true); +} + +static bool +term_line_clear(termline_t *line, size_t from) +{ + /* move cursor to initial position, erase screen after curser */ + return term_line_setcur(line, from) && term_print("\033[0J"); +} + +static bool +term_line_needlf(termline_t *line) +{ + return (((termstate.col_offset + line->width) % term_width()) == 0); +} + +static bool +term_line_write(termline_t *line, size_t off) +{ + struct { size_t count; char *entries; } buf = { 0 }; + bool ret; + + for (size_t i = off; i < line->width; i++) + uc_vector_addcp(&buf, line->chars[i]); + + ret = (buf.count == 0 || term_write(buf.entries, buf.count) == true); + + uc_vector_clear(&buf); + + /* if the printed string filled the entire line then print one more + character and erase it again in order to force scrolling to the next + line */ + if (term_line_needlf(line)) + ret &= term_print(" \033[1D\033[0K"); + + line->pos = line->width; + + return ret; +} + +static bool +term_line_cancel(termline_t *line) +{ + term_print("^C"); + term_line_setcur(line, line->width); + term_print("\n"); + + return true; +} + +static bool +term_line_prevword(termline_t *line) +{ + if (line->width == 0) + return true; + + /* find offset */ + size_t off = (line->pos < line->width) ? line->pos : line->width - 1; + + /* skip spaces before cursor */ + while (off > 0 && strchr(" \t", line->chars[off - 1]) != NULL) + off--; + + /* skip non-whitespace before cursor */ + while (off > 0 && strchr(" \t", line->chars[off - 1]) == NULL) + off--; + + return term_line_setcur(line, off); +} + +static bool +term_line_nextword(termline_t *line) +{ + if (line->width == 0) + return true; + + /* find offset */ + size_t off = (line->pos < line->width) ? line->pos : line->width - 1; + + /* skip spaces after cursor */ + while (off < line->width && strchr(" \t", line->chars[off]) != NULL) + off++; + + /* skip non-whitespace after cursor */ + while (off < line->width && strchr(" \t", line->chars[off]) == NULL) + off++; + + return term_line_setcur(line, off); +} + +static bool +term_line_delchr(termline_t *line) +{ + if (line->pos >= line->width || line->width == 0) + return true; + + /* remember original position */ + size_t pos = line->pos; + + /* move cursor before last char, erase */ + term_line_setcur(line, line->width - 1); + term_print("\033[0K"); + + /* move cursor to original position */ + term_line_setcur(line, pos); + + /* rearrange char buffer */ + for (size_t i = pos + 1; i < line->width; i++) + line->chars[i-1] = line->chars[i]; + + line->width--; + + /* re-write tail, will move cursor to eol */ + term_line_write(line, pos); + + /* reset cursor to original position */ + term_line_setcur(line, pos); + + return true; +} + +static bool +term_line_delword(termline_t *line) +{ + if (line->width == 0) + return true; + + /* find offset */ + size_t off = (line->pos < line->width) ? line->pos : line->width - 1; + + /* skip spaces before cursor */ + while (off > 0 && strchr(" \t", line->chars[off - 1]) != NULL) + off--; + + /* skip non-whitespace before cursor */ + while (off > 0 && strchr(" \t", line->chars[off - 1]) == NULL) + off--; + + /* calculate shift offset */ + size_t shift = line->pos - off; + + if (shift > 0) { + /* erase everything after offset */ + term_line_clear(line, off); + + /* rearrange char buffer */ + for (size_t i = off + shift; i < line->width; i++) + line->chars[i - shift] = line->chars[i]; + + line->width -= shift; + + /* re-write tail, will move cursor to eol */ + term_line_write(line, off); + + /* reset cursor to offset position */ + term_line_setcur(line, off); + } + + return true; +} + +static bool +term_line_addchr(termline_t *line, uint32_t chr) +{ + if (line->width == line->size) { + line->size += 128; + line->chars = xrealloc(line->chars, line->size * sizeof(*line->chars)); + } + + size_t pos = line->pos; + + for (size_t i = line->width; i > pos; i--) + line->chars[i] = line->chars[i-1]; + + line->chars[pos] = chr; + line->width++; + + /* write tail, will move cursor to eol */ + term_line_write(line, pos); + + /* restore cursor to original position + 1 */ + term_line_setcur(line, pos + 1); + + return true; +} + +static int +qsort_strcmp(const void *a, const void *b) +{ + return strcmp(*(const char **)a, *(const char **)b); +} + +static char * +common_prefix(suggestions_t *suggests) +{ + if (!suggests || suggests->count == 0 || *suggests->entries[0] == '\0') + return NULL; + + char *prefix = xstrdup(suggests->entries[0]); + size_t prefixlen = strlen(prefix); + + for (size_t i = 1; i < suggests->count; i++) { + while (strncmp(suggests->entries[i], prefix, prefixlen) != 0) { + prefix[--prefixlen] = '\0'; + + if (prefixlen == 0) { + free(prefix); + + return NULL; + } + } + } + + return prefix; +} + +static void +term_line_tabcomplete(termline_t *line, const char *prompt, + void (*cb)(size_t, arg_t *, suggestions_t *, void *), + void *ud) +{ + arg_t *argv = NULL; + size_t argc = term_line_toargv(line, true, &argv); + + suggestions_t suggests = { 0 }; + + cb(argc, argv, &suggests, ud); + + if (suggests.count > 1) { + size_t longest = 0; + + for (size_t i = 0; i < suggests.count; i++) { + size_t itemlen = strlen(suggests.entries[i]) + 2; + + if (itemlen > longest) + longest = itemlen; + } + + size_t cols = term_width() / longest; + + if (cols == 0) + cols = 1; + + qsort(suggests.entries, suggests.count, + sizeof(suggests.entries[0]), qsort_strcmp); + + term_print("\n"); + + for (size_t row = 0; row < suggests.count; row += cols) { + for (size_t col = 0; col < cols && row + col < suggests.count; col++) + term_printf("%-*s", (int)longest, suggests.entries[row + col]); + + term_print("\n"); + } + + fflush(stdout); + + if (prompt) + term_write(prompt, termstate.col_offset); + } + else { + term_line_clear(line, 0); + } + + char *prefix = common_prefix(&suggests); + + if (prefix) { + if (argc > 0) { + arg_t *partial = argv + argc - 1; + + term_line_fromstr(line, partial->off, prefix, strlen(prefix)); + } + else { + term_line_fromstr(line, line->width, prefix, strlen(prefix)); + } + + if (suggests.count == 1) + term_line_fromstr(line, line->width, " ", 1); + + free(prefix); + } + + term_line_write(line, 0); + + for (size_t i = 0; i < suggests.count; i++) + free(suggests.entries[i]); + + uc_vector_clear(&suggests); + + for (size_t i = 0; i < argc; i++) + free(argv[i].sv); + + free(argv); +} + +static ssize_t +term_getline(const char *prompt, arg_t **argv, + void (*completion_cb)(size_t, arg_t *, suggestions_t *, void *), + void *ud) +{ + termline_t line = { 0 }; + termline_t *curr_line = &line; + termline_t *next_line; + + if (prompt != NULL) { + termstate.col_offset = strwidth(prompt); + term_write(prompt, termstate.col_offset); + } + else { + termstate.col_offset = 0; + } + + while (true) { + int chr = term_getc(); + + switch (chr) { + case HOME_KEY: + case CTRL_UP: + term_line_setcur(curr_line, 0); + break; + + case END_KEY: + case CTRL_DOWN: + term_line_setcur(curr_line, curr_line->width); + break; + + case DEL_KEY: + term_line_delchr(curr_line); + break; + + case PAGE_UP: + case ARROW_UP: + if (termstate.history.count > 0 && + curr_line != uc_vector_first(&termstate.history)) { + + if (curr_line == &line) + next_line = uc_vector_last(&termstate.history); + else + next_line = curr_line - 1; + + term_line_clear(curr_line, 0); + term_line_write(next_line, 0); + curr_line = next_line; + } + break; + + case PAGE_DOWN: + case ARROW_DOWN: + if (termstate.history.count > 0 && curr_line != &line) { + if (curr_line == uc_vector_last(&termstate.history)) + next_line = &line; + else + next_line = curr_line + 1; + + term_line_clear(curr_line, 0); + term_line_write(next_line, 0); + curr_line = next_line; + } + break; + + case ARROW_LEFT: + if (curr_line->pos > 0) + term_line_setcur(curr_line, curr_line->pos - 1); + break; + + case ARROW_RIGHT: + if (curr_line->pos < curr_line->width) + term_line_setcur(curr_line, curr_line->pos + 1); + break; + + case CTRL_LEFT: + term_line_prevword(curr_line); + break; + + case CTRL_RIGHT: + term_line_nextword(curr_line); + break; + + case '\3': /* Ctrl-C */ + term_line_cancel(curr_line); + + *argv = NULL; + + return 0; + + case '\11': /* tab */ + if (completion_cb != NULL) + term_line_tabcomplete(curr_line, prompt, completion_cb, ud); + break; + + case '\15': /* carriage return */ + /* save to history if no other line was selected */ + if (curr_line == &line && curr_line->width > 0) { + if (termstate.history.count >= HISTORY_SIZE) { + free(termstate.history.entries[0].chars); + + for (size_t i = 1; i < termstate.history.count; i++) + termstate.history.entries[i-1] = + termstate.history.entries[i]; + + termstate.history.count--; + } + + uc_vector_push(&termstate.history, line); + } + + term_print("\n"); + + return term_line_toargv(curr_line, false, argv); + + case '\27': /* Ctrl-W */ + term_line_delword(curr_line); + break; + + case '\177': /* backspace */ + if (curr_line->pos > 0) { + term_line_setcur(curr_line, curr_line->pos - 1); + term_line_delchr(curr_line); + } + break; + + default: + if (chr >= ' ') + term_line_addchr(curr_line, chr); + break; + } + } + + *argv = NULL; + + return -1; +} + +static uc_value_t * +uc_debug_sigint_handler(uc_vm_t *vm, size_t nargs); + +static size_t +format_context_breadcrumb(uc_stringbuf_t *sb, uc_vm_t *vm, size_t maxcols) +{ + int off = sb->bpos; + + for (size_t i = 0; i < vm->callframes.count; i++) { + uc_callframe_t *frame = &vm->callframes.entries[i]; + + if (frame->cfunction != NULL && + frame->cfunction->cfn == uc_debug_sigint_handler) + continue; + + if (sb->bpos > off) + printbuf_strappend(sb, " » "); + + printbuf_append_function(sb, vm, + frame->closure + ? &frame->closure->header : &frame->cfunction->header, + NULL, SIZE_MAX); + } + + return printbuf_truncate(sb, off, maxcols, false); +} + +static void +format_context_header_backtrace(uc_stringbuf_t *sb, uc_vm_t *vm) +{ + size_t columns = term_width(); + size_t filename_width = (columns >= 42) ? (columns - 2) / 4 : columns - 2; + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_source_t *source = uc_program_function_source(frame->closure->function); + size_t printed = 0; + + cs(sb, &((style_t){ FG_BWHITE, BG_GRAY, 0 })); + + printbuf_strappend(sb, "["); + printed += 2 + printbuf_append_srcpath(sb, source, filename_width); + printbuf_strappend(sb, "]"); + + if (columns - printed - 2 > 10) { + printbuf_strappend(sb, " "); + printed += 2 + format_context_breadcrumb(sb, vm, columns - printed - 2); + printbuf_strappend(sb, " "); + } + + printbuf_memset(sb, -1, ' ', columns - printed); + + cs(sb, NULL); +} + +static void +format_context_header_callframe(uc_stringbuf_t *sb, uc_vm_t *vm, + uc_callframe_t *frame, size_t left_pad) +{ + size_t columns = term_width() - left_pad; + size_t filename_width = (columns >= 42) ? (columns - 2) / 4 : columns - 2; + size_t printed = 0; + + printbuf_memset(sb, -1, ' ', left_pad); + + cs(sb, &((style_t){ FG_BWHITE, BG_GRAY, 0 })); + + if (frame->closure) { + uc_source_t *source = uc_program_function_source(frame->closure->function); + + printbuf_strappend(sb, "["); + printed += 2 + printbuf_append_srcpath(sb, source, filename_width); + printbuf_strappend(sb, "]"); + } + else { + printbuf_strappend(sb, "[C]"); + printed += 3; + } + + if (columns - printed - 2 > 10) { + printbuf_strappend(sb, " "); + printed += 2 + printbuf_append_function(sb, vm, + frame->closure ? &frame->closure->header : &frame->cfunction->header, + frame, columns - printed - 2); + printbuf_strappend(sb, " "); + } + + printbuf_memset(sb, -1, ' ', columns - printed); + + cs(sb, NULL); +} + +static bool have_highlighting = false; + +static struct { + fg_color_t color; + 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 }, +}; + +static bool +compile_patterns(void) +{ + regex_t *re = NULL; + int err = 0; + + if (termstate.patterns.count > 0) + return true; + + for (size_t i = 0; i < ARRAY_SIZE(highlight_rules); i++) { + re = uc_vector_add(&termstate.patterns, { 0 }); + err = regcomp(re, highlight_rules[i].start, REG_EXTENDED); + + if (err != 0) + goto err; + + re = uc_vector_add(&termstate.patterns, { 0 }); + + if (highlight_rules[i].end) { + err = regcomp(re, highlight_rules[i].end, REG_EXTENDED); + + if (err != 0) + goto err; + } + } + + return true; + +err: + char errbuf[128]; + regerror(err, re, errbuf, sizeof(errbuf)); + fprintf(stderr, "Regex error: %s\n", errbuf); + + for (size_t i = 0; i < termstate.patterns.count; i++) { + regex_t *re = &termstate.patterns.entries[i]; + if (re) regfree(re); + } + + uc_vector_clear(&termstate.patterns); + + return false; +} + +typedef struct { + uint32_t style; + size_t from, to; +} style_range_t; + +typedef struct { + size_t count; + style_range_t *entries; +} style_ranges_t; + +typedef struct { + size_t from, to; +} line_range_t; + +static void +print_source_location(uc_stringbuf_t *sb, uc_vm_t *vm, uc_source_t *source, + size_t nranges, line_range_t *ranges, insn_span_t *hl, + size_t left_pad) +{ + size_t columns = term_width() - left_pad; + off_t offset = ftello(source->fp); + + fseeko(source->fp, 0, SEEK_SET); + + size_t linesize = 0, byte_pos = 0, start_line = SIZE_MAX, end_line = 0; + size_t hl_start = hl ? hl->pos_start : SIZE_MAX; + size_t cursor_pos = hl ? hl->pos_ip : SIZE_MAX; + size_t hl_end = hl ? hl->pos_end : SIZE_MAX; + style_t style = { FG_BWHITE, BG_BLACK, 0 }; + regex_t *ml_rule_re_end = NULL; + uint32_t ml_rule_color = 0; + ssize_t last_indent = -1; + char *linestr = NULL; + + for (size_t i = 0; i < nranges; i++) { + if (ranges[i].from == 0 || ranges[i].to == 0) + continue; + + if (ranges[i].from < start_line) + start_line = ranges[i].from; + + if (ranges[i].to > end_line) + end_line = ranges[i].to; + } + + for (size_t linenum = 1; linenum <= end_line; linenum++) { + ssize_t linelen = fgetline(source->fp, &linestr, &linesize); + + struct { + size_t count; + struct { fg_color_t color; ssize_t from, to; } *entries; + } colors = { 0 }; + + if (linelen == -1) + break; + + /* apply highlighting rules */ + if (have_highlighting) { + size_t ml_rule_from; + regmatch_t m; + char *p; + int rf; + + /* apply single line matches */ + for (size_t i = 0; i < ARRAY_SIZE(highlight_rules); i++) { + regex_t *re = &termstate.patterns.entries[i * 2]; + + /* only consider single line matches */ + 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) + { + uc_vector_add(&colors, { + .color = highlight_rules[i].color, + .from = p + m.rm_so - linestr, + .to = p + m.rm_eo - linestr + }); + } + } + + /* apply multi line matches */ + for (rf = 0, p = linestr, ml_rule_from = 0; + rf == 0 || ml_rule_re_end != NULL; + rf = REG_NOTBOL) { + + /* handle unterminated multiline matches */ + if (ml_rule_re_end != NULL) { + /* end match found on this line, colorize until match */ + if (regexec(ml_rule_re_end, p, 1, &m, 0) == 0) { + uc_vector_add(&colors, { + .color = ml_rule_color, + .from = ml_rule_from, + .to = p + m.rm_eo - linestr + }); + + ml_rule_re_end = NULL; + ml_rule_color = 0; + ml_rule_from = 0; + p += m.rm_eo; + } + + /* no end match, colorize entire remainder and skip rest */ + else { + uc_vector_add(&colors, { + .color = ml_rule_color, + .from = ml_rule_from, + .to = linelen + }); + + break; + } + } + + /* look for next multiline start match */ + for (size_t i = 0; i < ARRAY_SIZE(highlight_rules); i++) { + regex_t *re_start = &termstate.patterns.entries[i * 2]; + regex_t *re_end = &termstate.patterns.entries[i * 2 + 1]; + + /* only consider multi line rules */ + if (highlight_rules[i].end == NULL) + continue; + + /* found another multi line start */ + 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 = p + m.rm_so - linestr; + p += m.rm_eo; + break; + } + } + } + } + + bool print_line = false, more_lines = false; + + for (size_t i = 0; i < nranges; i++) { + if (ranges[i].from == 0 || ranges[i].to == 0) + continue; + + print_line |= (linenum >= ranges[i].from && linenum <= ranges[i].to); + more_lines |= (ranges[i].from > start_line && ranges[i].from == linenum + 1); + } + + if (!print_line) { + uc_vector_clear(&colors); + byte_pos += linelen; + + if (more_lines) { + printbuf_memset(sb, -1, ' ', left_pad); + cs(sb, &((style_t){ FG_GRAY, BG_BLACK, FAINT })); + printbuf_strappend(sb, " … "); + printbuf_memset(sb, -1, ' ', last_indent); + printbuf_strappend(sb, "…"); + printbuf_memset(sb, -1, ' ', columns - 6 - last_indent); + cs(sb, &((style_t){ FG_BWHITE, BG_BLACK, 0 })); + printbuf_strappend(sb, "\n"); + } + + continue; + } + + if (linelen > 0 && linestr[linelen - 1] == '\n') + linelen--; + + size_t trunc = 0; + + /* determine display width of line and whether it is too long */ + for (size_t i = 0, c = 0; i < (size_t)linelen; i++) { + c += (linestr[i] == '\t') ? 4 : 1; + + if (columns > 6 && c > columns - 6) { + trunc = linelen - i; + linelen = i; + break; + } + } + + size_t linecols = 0; + + printbuf_memset(sb, -1, ' ', left_pad); + cs(sb, &((style_t){ FG_GRAY, BG_BLACK, FAINT })); + sprintbuf(sb, "%4zu ", linenum); + cs(sb, &style); + + last_indent = -1; + + /* format line (substitute tabs and ctrls with placeholders) */ + for (ssize_t i = 0; i < linelen; i++, byte_pos++) { + style_t newstyle = { + .fg = FG_BWHITE, + .bg = (byte_pos >= hl_start && byte_pos < hl_end) + ? BG_GRAY : BG_BLACK, + .styles = (cursor_pos == byte_pos) ? ULINE : 0 + }; + + for (size_t j = 0; j < colors.count; j++) + if (colors.entries[j].from <= i && colors.entries[j].to > i) + newstyle.fg = colors.entries[j].color; + + if (memcmp(&style, &newstyle, sizeof(style))) { + style = newstyle; + cs(sb, &style); + } + + if (linestr[i] == '\t') { + linecols += 4; + cs(sb, &((style_t){ FG_BBLACK, style.bg, FAINT })); + printbuf_strappend(sb, "<-> "); + cs(sb, &style); + } + else if (linestr[i] < ' ' || linestr[i] == 0x7f) { + linecols++; + cs(sb, &((style_t){ FG_BBLACK, style.bg, FAINT })); + printbuf_strappend(sb, "."); + cs(sb, &style); + } + else { + if (last_indent == -1) + last_indent = linecols; + + linecols++; + printbuf_memappend_fast(sb, linestr + i, 1); + } + } + + /* reset char styles */ + style.styles = 0; + style.bg = (byte_pos >= hl_start && + byte_pos + trunc <= hl_end) ? BG_GRAY : BG_BLACK; + cs(sb, &style); + + /* if truncated, add ellipsis */ + if (trunc > 0) { + if (linecols < columns - 6) + printbuf_memset(sb, -1, ' ', (columns - 6) - linecols); + + printbuf_strappend(sb, "…"); + byte_pos += trunc; + } + + /* if shorter than display width, pad with trailing spaces */ + else if (linecols < columns - 5) { + if (linestr[linelen] == '\n') { + printbuf_memset(sb, -1, ' ', 1); + linecols++; + } + + if (style.bg != BG_BLACK) { + style.bg = BG_BLACK; + cs(sb, &style); + } + + printbuf_memset(sb, -1, ' ', (columns - 5) - linecols); + } + + cs(sb, &((style_t){ 0, 0, 0 })); + printbuf_strappend(sb, "\n"); + + uc_vector_clear(&colors); + + byte_pos++; + } + + free(linestr); + + fseeko(source->fp, offset, SEEK_SET); +} + +static void +format_context_statement(uc_stringbuf_t *sb, uc_vm_t *vm, + uc_function_t *fn, insn_span_t *stmt, + size_t ctx_before, size_t ctx_after, size_t left_pad) +{ + size_t beg_line = 1, beg_off = 0, end_line = 1, end_off = 0, ip_line = 1; + uc_source_t *source = uc_program_function_source(fn); + uc_lineinfo_t *lines = &source->lineinfo; + + /* determine start and end byte position of first and last statement line */ + for (size_t i = 0, lineoff = 0; i < lines->count; i++) { + // FIXME: >= stmt->pos_start ? + if (end_off <= stmt->pos_start && + end_off + (lines->entries[i] & 0x7f) > stmt->pos_start) + { + beg_line = end_line; + beg_off = lineoff; + } + + if (end_off <= stmt->pos_ip && + end_off + (lines->entries[i] & 0x7f) >= stmt->pos_ip) + { + ip_line = end_line; + } + + if (i > 0 && lines->entries[i] & 0x80) { + end_line++; + end_off++; + lineoff = end_off; + + if (end_off >= stmt->pos_end) + break; + } + + end_off += lines->entries[i] & 0x7f; + } + + if (beg_off >= end_off) + return; + + line_range_t ranges[3] = { 0 }; + + if (end_line - beg_line <= 4) { + ranges[0].from = beg_line; + ranges[0].to = end_line; + } + else { + if (ip_line - beg_line <= (ctx_before + ctx_after + 2)) { + ranges[1].from = beg_line; + } + else { + ranges[0].from = beg_line; + ranges[0].to = beg_line + ctx_after; + + ranges[1].from = ip_line - ctx_before; + } + + if (end_line - ip_line <= (ctx_before + ctx_after + 2)) { + ranges[1].to = end_line; + } + else { + ranges[1].to = ip_line + ctx_after; + + ranges[2].from = end_line - ctx_before; + ranges[2].to = end_line; + } + } + + print_source_location(sb, vm, source, 3, ranges, stmt, left_pad); +} + +static void +format_context_cfunction(uc_stringbuf_t *sb, uc_vm_t *vm, + uc_cfunction_t *cfn, size_t left_pad) +{ + void *loadaddr = NULL, *symaddr = NULL; + const char *filename = "Not available"; + const char *symname = "Not available"; + size_t columns = term_width() - left_pad; + Dl_info dli; + int n; + + if (dladdr(cfn->cfn, &dli)) { + if (dli.dli_fname) + filename = dli.dli_fname; + + if (dli.dli_sname) + symname = dli.dli_sname; + + loadaddr = dli.dli_fbase; + symaddr = dli.dli_saddr; + } + + printbuf_memset(sb, -1, ' ', left_pad); + cs(sb, &((style_t){ FG_BWHITE, BG_BLACK, FAINT })); + n = sprintbuf(sb, " Dynamic library: %s (%p)", filename, loadaddr); + printbuf_memset(sb, -1, ' ', columns - n); + cs(sb, NULL); + printbuf_strappend(sb, "\n"); + + printbuf_memset(sb, -1, ' ', left_pad); + cs(sb, &((style_t){ FG_BWHITE, BG_BLACK, FAINT })); + n = sprintbuf(sb, " Symbol name: %s (%p)", symname, symaddr); + printbuf_memset(sb, -1, ' ', columns - n); + cs(sb, NULL); + printbuf_strappend(sb, "\n"); +} + +// 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 || *ip == I_QCALL || *ip == I_MCALL || *ip == I_QMCALL) + return 5 + insn_u16(ip + 1) * 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_MCALL || *ip == I_QMCALL) { + argspec = insn_u32(ip + 1); + + size_t nargs = argspec & 0xffff; + + if (nargs + 2 < vm->stack.count) { + uc_value_t *ctx = vm->stack.entries[vm->stack.count - nargs - 2]; + uc_value_t *key = vm->stack.entries[vm->stack.count - nargs - 1]; + uc_value_t *fno = ucv_key_get(vm, ctx, key); + + ucv_put(fno); /* ucv_get_get() increases refcount */ + + if (ucv_type(fno) == UC_UPVALUE) { + uc_upvalref_t *ref = (uc_upvalref_t *)fno; + + if (ref->closed) + fno = ref->value; + else + fno = vm->stack.entries[ref->slot]; + } + + if (ucv_type(fno) == UC_CLOSURE) { + uc_function_t *fn = ((uc_closure_t *)fno)->function; + + dbk->bk.cb = bk_enter_cli; + dbk->bk.ip = fn->chunk.entries; + dbk->depth = 1; + dbk->fn = fn; + enter = true; + } + } + } + else if (*ip == I_CALL || *ip == I_QCALL) { + 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_cli; + dbk->bk.ip = fn->chunk.entries; + dbk->depth = 1; + dbk->fn = fn; + enter = true; + } + } + } + + if (!enter) { + dbk->bk.cb = bk_enter_cli; + 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); + + term_print("Leaving function!\n"); + + if (!frame) + return; + + dbk->bk.cb = bk_enter_cli; + 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)) + { + term_print("Jump target out of range\n"); + 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_cli; + + 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) +{ +#define exname(x) [EXCEPTION_##x] = "EXCEPTION_" #x + const char *exnames[] = { + exname(NONE), + exname(SYNTAX), + exname(RUNTIME), + exname(TYPE), + exname(REFERENCE), + exname(USER), + exname(EXIT) + }; +#undef exname + + term_print("Exception occurred!\n"); + term_printf("Type: %s\n", exnames[vm->exception.type]); + term_printf("Message: %s\n", vm->exception.message); + + bk_enter_cli(vm, bk); +} + +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: + case I_QCALL: + case I_MCALL: + case I_QMCALL: + if (single) { + update_breakpoint(vm, BK_STEP, bk_enter_function, p, *fnp, 0); + + return NULL; + } + + break; + + case I_RETURN: + if (single) { + 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; + } + } + + term_print("No next statement, continuing in parent\n"); + + *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; +} + +static void +print_variables(uc_stringbuf_t *buf, uc_vm_t *vm, uc_callframe_t *frame, + bool verbose, const char *indent) +{ + uc_chunk_t *chunk = &frame->closure->function->chunk; + uc_variables_t *decls = &chunk->debuginfo.variables; + uc_value_list_t *names = &chunk->debuginfo.varnames; + size_t columns = term_width() - strlen(indent); + size_t pos = frame->ip - chunk->entries; + + if (frame->ctx) { + printbuf_memappend_fast(buf, indent, strlen(indent)); + + cs(buf, &((style_t){ FG_BWHITE, 0, FAINT })); + printbuf_strappend(buf, "(this) : "); + cs(buf, NULL); + + if (verbose) + ucv_to_stringbuf_formatted(vm, buf, frame->ctx, 0, ' ', 2); + else + printbuf_append_uv(buf, vm, frame->ctx, columns - 19); + + printbuf_strappend(buf, "\n"); + } + + for (size_t i = 0; i < decls->count; i++) { + if (decls->entries[i].from > pos || decls->entries[i].to < pos) + continue; + + uc_value_t *vname = load_constval(names, decls->entries[i].nameidx); + size_t slot = decls->entries[i].slot; + + printbuf_memappend_fast(buf, indent, strlen(indent)); + + /* is local variable */ + if (slot < (size_t)-1 / 2) { + bool is_internal = (vname && *ucv_string_get(vname) == '('); + + if (is_internal) + cs(buf, &((style_t){ FG_BWHITE, 0, FAINT })); + + int n, off = buf->bpos; + + if (vname) + n = sprintbuf(buf, "%s", ucv_string_get(vname)); + else + n = sprintbuf(buf, "$%zu", slot); + + printbuf_truncate(buf, off, 16, true); + + if (is_internal) + cs(buf, NULL); + + if (n < 16) + printbuf_memset(buf, -1, ' ', 16 - n); + + cs(buf, &((style_t){ FG_BWHITE, 0, FAINT })); + printbuf_strappend(buf, " : "); + cs(buf, NULL); + + if (frame->stackframe + slot < vm->stack.count) { + uc_value_t *vval = vm->stack.entries[frame->stackframe + slot]; + + if (verbose) + ucv_to_stringbuf_formatted(vm, buf, vval, 0, ' ', 2); + else + printbuf_append_uv(buf, vm, vval, columns - 19); + } + else { + cs(buf, &((style_t){ FG_RED, 0, BOLD })); + printbuf_strappend(buf, ""); + cs(buf, NULL); + } + } + + /* is upvalue */ + else { + cs(buf, &((style_t){ FG_CYAN, 0, BOLD })); + + int n, off = buf->bpos; + + if (vname) + n = sprintbuf(buf, "%s", ucv_string_get(vname)); + else + n = sprintbuf(buf, "$%zu", slot); + + printbuf_truncate(buf, off, 16, true); + cs(buf, NULL); + + if (n < 16) + printbuf_memset(buf, -1, ' ', 16 - n); + + cs(buf, &((style_t){ FG_BWHITE, 0, FAINT })); + printbuf_strappend(buf, " : "); + cs(buf, NULL); + + slot -= ((size_t)-1 / 2); + + if (slot < frame->closure->function->nupvals) { + uc_upvalref_t *ref = frame->closure->upvals[slot]; + + if (!ref) { + cs(buf, &((style_t){ FG_BWHITE, 0, FAINT })); + printbuf_strappend(buf, ""); + cs(buf, NULL); + } + else if (ref->closed) { + uc_value_t *vval = ref->value; + + if (verbose) + ucv_to_stringbuf_formatted(vm, buf, vval, 0, ' ', 2); + else + printbuf_append_uv(buf, vm, vval, columns - 19); + } + else if (ref->slot < vm->stack.count) { + uc_value_t *vval = vm->stack.entries[ref->slot]; + + if (verbose) + ucv_to_stringbuf_formatted(vm, buf, vval, 0, ' ', 2); + else + printbuf_append_uv(buf, vm, vval, columns - 19); + } + else { + cs(buf, &((style_t){ FG_RED, 0, BOLD })); + printbuf_strappend(buf, ""); + cs(buf, NULL); + } + } + else { + cs(buf, &((style_t){ FG_RED, 0, BOLD })); + printbuf_strappend(buf, ""); + cs(buf, NULL); + } + } + + ucv_put(vname); + + printbuf_strappend(buf, "\n"); + } +} + +static bool +eval_expr(uc_vm_t *vm, uc_callframe_t *frame, char *expr, uc_value_t **res) +{ + 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; + char *err = NULL; + + uc_source_t *source = + uc_source_new_buffer("[eval expression]", xstrdup(expr), strlen(expr)); + + uc_parse_config_t conf = { .raw_mode = true }; + uc_program_t *prog = uc_compile(&conf, source, &err); + + uc_source_put(source); + + if (!prog) { + term_printf("%s", err); + free(err); + *res = NULL; + + return false; + } + + uc_value_t *exprfn = ucv_closure_new(vm, uc_program_entry(prog), false); + uc_chunk_t *chunk = &((uc_closure_t *)exprfn)->function->chunk; + + if (chunk->entries[0] != I_LVAR && chunk->entries[0] != I_LTHIS) { + term_print("Expecting expression\n"); + uc_program_put(prog); + ucv_put(exprfn); + *res = NULL; + + return false; + } + + uc_value_t *scope = ucv_object_new(NULL); + + /* determine referenced variables */ + for (size_t i = 0; i < chunk->count; i += insn_length(&chunk->entries[i], prog)) { + if (chunk->entries[i] != I_LVAR) + continue; + + uc_value_t *varname = load_constval( + &prog->constants, + insn_u32(chunk->entries + i + 1)); + + if (!varname) + continue; + + uc_value_t *varval = NULL; + + for (size_t j = 0; !varval && j < decls->count; j++) { + if (decls->entries[j].from > pos || decls->entries[j].to < pos) + continue; + + uc_value_t *vname = load_constval(names, decls->entries[j].nameidx); + bool match = ucv_is_equal(varname, vname); + + ucv_put(vname); + + if (!match) + continue; + + size_t slot = decls->entries[j].slot; + + /* is local var */ + if (slot < (size_t)-1 / 2) { + slot += frame->stackframe; + + if (slot < vm->stack.count) + varval = ucv_get(vm->stack.entries[slot]); + } + + /* is upvalue */ + else { + slot -= ((size_t)-1 / 2); + + if (slot < frame->closure->function->nupvals) { + uc_upvalref_t *ref = frame->closure->upvals[slot]; + + if (ref && ref->closed) + varval = ucv_get(ref->value); + else if (ref && ref->slot < vm->stack.count) + varval = ucv_get(vm->stack.entries[ref->slot]); + } + } + } + + if (varval) + ucv_object_add(scope, ucv_string_get(varname), varval); + + ucv_put(varname); + } + + 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; + + if (uc_vm_call(vm, true, 0) == EXCEPTION_NONE) { + *res = uc_vm_stack_pop(vm); + rv = true; + } + else { + term_printf("Exception: %s\n", 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; + + uc_vm_scope_set(vm, prev_scope); + uc_program_put(prog); + ucv_put(exprfn); + + return rv; +} + +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) { + update_breakpoint(vm, BK_CATCH, bk_handle_catch, + fn->chunk.entries + eh->entries[i].target, fn, 0); + + break; + } + } +} + +static void +print_location(uc_vm_t *vm, const char *prefix, debug_breakpoint_t *dbk) +{ + uc_callframe_t *topframe = NULL, *funframe = NULL; + size_t depth = dbk->depth; + + 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]; + + /* Update location in automatic function breakpoint */ + if (dbk->fn == NULL) { + dbk->fn = funframe->closure->function; + dbk->bk.ip = funframe->ip; + } + + /* Update exception catch point */ + update_catchpoint(vm, funframe->closure->function, funframe->ip); + break; + } + } + + uc_stringbuf_t *pb = xprintbuf_new(); + + printbuf_memappend_fast(pb, prefix, strlen(prefix)); + + 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)) { + size_t byte = stmt.pos_start; + size_t line = uc_source_get_line(source, &byte); + + sprintbuf(pb, "%s, line %zu:%zu\n", + source->filename, line, byte); + + format_context_header_backtrace(pb, vm); + format_context_statement(pb, vm, function, &stmt, 2, 2, 0); + } + } + else if (topframe) { + if (topframe->cfunction->name[0]) + sprintbuf(pb, "native function %s()\n", topframe->cfunction->name); + else + printbuf_strappend(pb, "unnamed native function\n"); + } + else { + printbuf_strappend(pb, "[unknown location]\n"); + } + + printbuf_strappend(pb, "\n"); + term_write(pb->buf, pb->bpos); + + printbuf_free(pb); +} + +static bool +cmd_help(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + char *cmd = (argc > 1) ? argv[1].sv : NULL; + size_t columns = term_width(); + + for (size_t i = 0; i < ARRAY_SIZE(commands); i++) { + bool match = !cmd; + + if (cmd) { + for (const char *c = commands[i].command; *c; c += strlen(c) + 1) { + if (str_startswith(c, argv[1].sv)) { + match = true; + break; + } + } + } + + if (match == false) + continue; + + term_printf("\033[1m%s\033[0m\n\n", commands[i].command); + + const char *p = commands[i].help; + + while (*p != '\0') { + size_t pad = strspn(p, " "); + size_t len = strcspn(p, "\r\n") - pad; + + if (pad + len <= columns) { + term_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) { + term_printf("%.*s", (int)pad, p); + + for (size_t j = columns - pad; j > 0; j--) { + if (l[j-1] == ' ') { + term_printf("%.*s\n", (int)j, l); + l += j; + len -= j; + break; + } + } + } + + term_printf("%.*s", (int)pad, p); + term_printf("%.*s\n", (int)len, l); + p = l + len + (l[len] == '\n'); + } + } + + term_print("\n\n"); + } + + return true; +} + +static bool +cmd_break(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + char *spec = (argc == 2) ? argv[1].sv : NULL; + size_t id = 0; + + if (spec == NULL || *spec == '\0') { + term_print("Usage:\n"); + term_print(" break path[:line[:offset]]\n"); + term_print(" break expr\n"); + + return true; + } + + /* path spec */ + if ((strchr(spec, '/') || strchr(spec, ':') || + (*spec >= '0' && *spec <= '9')) && *spec != '(') { + + char *path, *line, *byte; + + if (*spec == ':' || (*spec >= '0' && *spec <= '9')) { + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_function_t *function = frame->closure->function; + + path = uc_program_function_source(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) { + term_print("Usage: break path[:line[:offset]]\n"); + + return true; + } + + id = add_breakpoint(vm, path, + line ? strtoul(line, NULL, 10) : 0, + byte ? strtoul(byte, NULL, 10) : 0, + BK_USER); + } + + /* expression spec or function name */ + else { + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_value_t *val = NULL; + + /* Before evaluating as code, try looking up function name directly. */ + if (frame != NULL) { + uc_program_function_foreach(frame->closure->function->program, fn) { + if (!strcmp(fn->name, spec)) { + id = patch_breakpoint(vm, fn, 0, BK_USER, 1); + break; + } + } + } + + if (id == 0 && frame != NULL && eval_expr(vm, frame, spec, &val)) { + if (ucv_type(val) == UC_CLOSURE) { + id = patch_breakpoint(vm, + ((uc_closure_t *)val)->function, 0, BK_USER, 1); + } + else { + char *s = ucv_to_string(vm, val); + int len = strlen(s); + + term_printf("Value `%s` (%.*s%s) is not a function\n", + spec, + len > 32 ? 31 : len, + s, + len > 32 ? "…" : ""); + } + + ucv_put(val); + } + } + + if (id) + term_printf("Breakpoint #%zu added\n", id); + else + term_print("Unable to resolve source location\n"); + + return true; +} + +static bool +cmd_delete(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + uc_breakpoints_t *bks = &vm->breakpoints; + + if (argc > 2 || (argc == 2 && argv[1].type != ARGTYPE_NUMBER)) { + term_print("Usage: delete [id]\n"); + } + else if (argc == 2) { + size_t n = 0; + + for (size_t i = 0; i < bks->count; i++) { + debug_breakpoint_t *dbk = (debug_breakpoint_t *)bks->entries[i]; + + if (dbk == NULL || dbk->kind != BK_USER) + continue; + + if (++n == argv[1].nv) { + free_breakpoint(vm, &dbk->bk); + + return term_printf("Breakpoint #%zu deleted\n", argv[1].nv); + } + } + + term_printf("No breakpoint #%zu set\n", argv[1].nv); + } + else { + if (dbk->kind == BK_USER) { + free_breakpoint(vm, &dbk->bk); + term_print("Current breakpoint deleted\n"); + } + else { + term_print("Automatic breakpoint cannot be deleted\n"); + } + } + + return true; +} + +static bool +cmd_list(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + uc_breakpoints_t *bks = &vm->breakpoints; + uc_stringbuf_t buf = { 0 }; + size_t n = 0; + + const char *kinds[] = { + [BK_ONCE] = "(once)", + [BK_USER] = "(user)", + [BK_STEP] = "(step)", + [BK_CATCH] = "(catch)", + }; + + 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; + + if (p->kind == BK_USER) + sprintbuf(&buf, "#%-6zu ", ++n); + else + sprintbuf(&buf, "%-7s ", kinds[p->kind]); + + 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); + + if (source) + printbuf_append_srcpath(&buf, source, SIZE_MAX); + else + printbuf_strappend(&buf, "[unknown source]"); + + sprintbuf(&buf, ":%zu:%zu - ", line, byte > 1 ? byte : 1); + + uc_closure_t cl = { + .header = { .type = UC_CLOSURE }, + .function = p->fn + }; + + printbuf_append_function(&buf, vm, &cl.header, NULL, SIZE_MAX); + + + } + else { + printbuf_strappend(&buf, ""); + } + + printbuf_strappend(&buf, "\n"); + term_write(buf.buf, buf.bpos); + printbuf_reset(&buf); + } + } + + if (n == 0) + term_print("No user breakpoints set\n"); + + free(buf.buf); + + return true; +} + +static bool +cmd_step_common(uc_vm_t *vm, debug_breakpoint_t *dbk, bool single) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + + if (!frame) + return false; + + uc_function_t *fn = frame->closure->function; + size_t depth = dbk->depth; + uint8_t *nextinsn = next_step(vm, &fn, frame->ip, single, &depth); + + /* no next instruction, run until completion */ + if (!nextinsn) + return false; + + uc_source_t *source = uc_program_function_source(fn); + + size_t byte = uc_program_function_srcpos(fn, + nextinsn - fn->chunk.entries); + + size_t line = uc_source_get_line( + uc_program_function_source(fn), &byte); + + if (fn != frame->closure->function) + term_printf("Entering %s()...\n", + fn->name[0] + ? fn->name : fn->arrow + ? "[arrow function]" : "[unnamed function]"); + else + term_printf("Continuing in %s:%zu:%zu...\n", + source->filename, line, byte); + + update_breakpoint(vm, BK_STEP, bk_enter_cli, nextinsn, fn, depth); + + return false; +} + +static bool +cmd_next(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + return cmd_step_common(vm, dbk, false); +} + +static bool +cmd_step(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + return cmd_step_common(vm, dbk, true); +} + +static bool +cmd_continue(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + term_print("Continuing...\n"); + + return false; +} + +static bool +cmd_return(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 1); + + if (frame) { + update_breakpoint(vm, BK_STEP, bk_enter_cli, frame->ip, + frame->closure->function, 0); /* XXX: fixup depth? */ + } + else { + term_print("In topmost function, running until completion...\n"); + } + + return false; +} + +static bool +cmd_backtrace(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + bool verbose = false; + + if (argc > 3 || (argc == 2 && argv[1].type != ARGTYPE_STRING)) + return term_print("Usage: backtrace [full]\n"); + + if (argc == 2 && str_startswith("full", argv[1].sv)) + verbose = true; + + uc_stringbuf_t buf = { 0 }; + uc_function_t *function; + uc_callframe_t *frame; + bool adjust_ip = true; + size_t i; + + for (i = vm->callframes.count; i > 0; i--) { + frame = &vm->callframes.entries[i - 1]; + + if (frame->closure) { + function = frame->closure->function; + + printbuf_cs(&buf, "\1#%-2zu\177 in ", + &((style_t){ 0, 0, BOLD }), + i); + + printbuf_append_srcpath(&buf, + uc_program_function_source(function), SIZE_MAX); + + size_t insn = frame->ip - function->chunk.entries; + size_t byte = insn; + size_t line = insnoff_to_srcpos(function, &byte); + + sprintbuf(&buf, ":%zu:%zu at insn #%zu in ", line, byte, insn); + + cs(&buf, &((style_t){ 0, 0, BOLD })); + printbuf_append_funcname(&buf, + vm, &frame->closure->header, SIZE_MAX); + printbuf_strappend(&buf, "()\n"); + cs(&buf, NULL); + + uint8_t *ip = frame->ip; + insn_span_t stmt; + + if (adjust_ip && i < vm->callframes.count) + ip -= 5 - 2 * (vm->arg.u32 >> 16); + + if (find_statement_boundaries(function, ip, 0, &stmt)) { + format_context_header_callframe(&buf, vm, frame, 2); + format_context_statement(&buf, vm, function, &stmt, 2, 2, 2); + } + + if (verbose) { + printbuf_cs(&buf, "\n \1Local variables:\177\n", + &((style_t){ 0, 0, BOLD })); + + print_variables(&buf, vm, frame, false, " - "); + } + } + else if (frame->cfunction) { + uc_cfunction_t *cfn = frame->cfunction; + Dl_info dli; + + printbuf_cs(&buf, "\1#%-2zu\177 in ", + &((style_t){ 0, 0, BOLD }), + i); + + if (dladdr(cfn->cfn, &dli) != 0 && dli.dli_fname != NULL) + printbuf_memappend_fast((&buf), + dli.dli_fname, strlen(dli.dli_fname)); + else + printbuf_strappend(&buf, "[unknown shared object]"); + + printbuf_strappend(&buf, ", function "); + + cs(&buf, &((style_t){ 0, 0, BOLD })); + printbuf_append_funcname(&buf, vm, &cfn->header, SIZE_MAX); + printbuf_strappend(&buf, "()\n"); + cs(&buf, NULL); + + format_context_header_callframe(&buf, vm, frame, 2); + format_context_cfunction(&buf, vm, cfn, 2); + + adjust_ip = false; + } + + printbuf_strappend(&buf, "\n"); + term_write(buf.buf, buf.bpos); + printbuf_reset(&buf); + } + + free(buf.buf); + + return true; +} + +static bool +cmd_variables(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_stringbuf_t buf = { 0 }; + bool verbose = false; + + if (argc > 3 || (argc == 2 && argv[1].type != ARGTYPE_STRING)) + return term_print("Usage: backtrace [full]\n"); + + if (argc == 2 && str_startswith("full", argv[1].sv)) + verbose = true; + + if (!frame) + return term_print("No local variables in current context\n"); + + print_variables(&buf, vm, frame, verbose, ""); + + term_write(buf.buf, buf.bpos); + free(buf.buf); + + return true; +} + +static bool +cmd_sources(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + struct lh_table *sources = lh_kptr_table_new(16, NULL); + uc_weakref_t *ref; + + for (ref = vm->values.next; ref != &vm->values; ref = ref->next) { + uc_closure_t *uc = + (uc_closure_t *)((uintptr_t)ref - offsetof(uc_closure_t, ref)); + + if (uc->header.type != UC_CLOSURE) + continue; + + if (!uc->function || !uc->function->program) + continue; + + for (size_t i = 0; i < uc->function->program->sources.count; i++) { + uc_source_t *source = uc->function->program->sources.entries[i]; + unsigned long hash = lh_get_hash(sources, source); + + if (!lh_table_lookup_entry_w_hash(sources, source, hash)) + lh_table_insert_w_hash(sources, source, NULL, hash, 0); + } + } + + struct lh_entry *e; + size_t i = 0; + + lh_foreach(sources, e) { + uc_source_t *source = lh_entry_k(e); + + term_printf("#%2zu %s\n", i++, source->filename); + } + + lh_table_free(sources); + + return true; +} + +static bool +cmd_print(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_stringbuf_t buf = { 0 }; + + if (argc < 2) + return term_print("Usage: print expr\n"); + + for (size_t i = 1; i < argc; i++) { + if (i > 1) + printbuf_strappend(&buf, " "); + + printbuf_memappend_fast((&buf), argv[i].sv, strlen(argv[i].sv)); + } + + uc_value_t *res = NULL; + + if (eval_expr(vm, frame, buf.buf, &res)) { + printbuf_reset(&buf); + ucv_to_stringbuf_formatted(vm, &buf, res, 0, ' ', 2); + printbuf_strappend(&buf, "\n"); + + ucv_put(res); + + term_write(buf.buf, buf.bpos); + } + + free(buf.buf); + + return true; +} + +static bool +cmd_lines(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_function_t *fn = frame->closure->function; + size_t insn = frame->ip - fn->chunk.entries; + bool ctx_is_range = false; + size_t ctx_before = 2; + size_t ctx_after = 2; + + location_t loc = { + .program = fn->program, + .source = uc_program_function_source(fn), + .function = fn, + .offset = uc_program_function_srcpos(fn, insn), + }; + + insn_span_t stmt = { + .pos_start = SIZE_MAX, + .pos_end = SIZE_MAX, + .pos_ip = SIZE_MAX + }; + + /* no argument */ + if (argc == 1) { + 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); + } + + /* absolute line number */ + else if (argc >= 2 && argv[1].type == ARGTYPE_NUMBER) { + loc.line = (argv[1].nv > 0) ? argv[1].nv : 1; + } + + /* line or instruction offset */ + else if (argc >= 2 && argv[1].type == ARGTYPE_STRING && + strchr("+-#", argv[1].sv[0]) != NULL && + argv[1].sv[1] >= '0' && argv[1].sv[1] <= '9') { + char *e; + unsigned long n = strtoul(argv[1].sv + 1, &e, 0); + + if (*e != '\0') + return term_print("Invalid offset\n"); + + if (argv[1].sv[0] == '+') { + loc.line += n; + } + else if (argv[1].sv[0] == '-') { + loc.line = (n < loc.line) ? loc.line - n : 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; + } + } + + /* source path, function name or function expression */ + else if (argc >= 2 && argv[1].type == ARGTYPE_STRING) { + bool found = false; + + if (argv[1].sv[0] != '(') { + loc = ((location_t){ + .path = argv[1].sv, + .line = 1, + .column = 1 + }); + + found = lookup_source(vm, &loc); + ctx_is_range = true; + + if (!found) { + uc_program_function_foreach(fn->program, pfn) { + if (!strcmp(pfn->name, argv[1].sv)) { + loc = ((location_t){ .function = pfn }); + found = true; + ctx_is_range = false; + break; + } + } + } + } + + if (!found) { + uc_value_t *val; + + if (!eval_expr(vm, frame, argv[1].sv, &val)) + return true; + + if (ucv_type(val) != UC_CLOSURE) { + char *s = ucv_to_string(vm, val); + int len = strlen(s); + + term_printf("Value `%s` (%.*s%s) is not a function\n", + argv[1].sv, + len > 32 ? 31 : len, + s, + len > 32 ? "…" : ""); + + ucv_put(val); + free(s); + + return true; + } + + loc = ((location_t){ .function = ((uc_closure_t *)val)->function }); + found = true; + ctx_is_range = false; + + ucv_put(val); + } + + if (loc.function) { + size_t beg = uc_program_function_srcpos(loc.function, 0); + size_t end = 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, &end) + 2 - loc.line; + } + else { + ctx_before = 0; + ctx_after = 5; + } + } + + if (argc >= 3 && argv[2].type != ARGTYPE_NUMBER) + return term_print("Invalid amount of context lines\n"); + + if (argc >= 4 && argv[3].type != ARGTYPE_NUMBER) + return term_print("Invalid amount of following context lines\n"); + + if (argc >= 4) { + if (ctx_is_range) { + loc.line = argv[2].nv > 0 ? argv[2].nv : 1; + ctx_before = 0; + ctx_after = (argv[3].nv > argv[2].nv) ? argv[3].nv - argv[2].nv : 1; + } + else { + ctx_before = argv[2].nv; + ctx_after = argv[3].nv; + } + } + else if (argc >= 3) { + ctx_before = 0, ctx_after = argv[2].nv; + } + + if (!lookup_function(vm, &loc)) + return term_print("Unable to resolve source code location\n"); + + uc_stringbuf_t buf = { 0 }; + + line_range_t lines = { + .from = (loc.line > ctx_before) ? loc.line - ctx_before : 1, + .to = loc.line + ctx_after + + }; + + print_source_location(&buf, vm, loc.source, 1, &lines, + (loc.source == uc_program_function_source(fn)) ? &stmt : NULL, 0); + + printbuf_strappend(&buf, "\n"); + + term_write(buf.buf, buf.bpos); + free(buf.buf); + + return true; +} + +static bool +cmd_throw(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + uc_exception_type_t et = EXCEPTION_USER; + + if (argc < 2 || argv[1].type != ARGTYPE_STRING || argc > 3) + return term_print("Usage: throw [type] message\n"); + + if (argc == 3) { + if (str_startswith("syntax", argv[1].sv)) + et = EXCEPTION_SYNTAX; + else if (str_startswith("runtime", argv[1].sv)) + et = EXCEPTION_RUNTIME; + else if (str_startswith("type", argv[1].sv)) + et = EXCEPTION_TYPE; + else if (str_startswith("reference", argv[1].sv)) + et = EXCEPTION_REFERENCE; + else if (str_startswith("user", argv[1].sv)) + et = EXCEPTION_USER; + else if (str_startswith("exit", argv[1].sv)) + et = EXCEPTION_EXIT; + else + return term_printf("Unrecognized exception type '%s'\n", argv[1].sv); + } + + uc_vm_raise_exception(vm, et, "%s", argv[argc - 1].sv); + + return true; +} + +#undef __insn +#define __insn(_name) #_name, + +static const char *insn_names[__I_MAX] = { + __insns +}; + +static bool +cmd_disasm(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + uc_function_t *target = NULL; + uc_stringbuf_t buf = { 0 }; + uc_program_t *prog = NULL; + size_t from = 0, to = 0; + size_t columns = term_width(); + + if (argc > 2 || (argc == 2 && argv[1].type != ARGTYPE_STRING)) + return term_print("Usage: disassemble [target]\n"); + + if (argc == 2) { + if (*argv[1].sv == '#') { + char *e; + + from = strtoul(argv[1].sv + 1, &e, 10); + + if (*e == '-') { + to = strtoul(e + 1, &e, 10); + + if (*e != '\0' || to < from) + return term_printf("Invalid instruction range '%s'\n", argv[1].sv); + } + else if (*e == '+') { + to = from + strtoul(e + 1, &e, 10); + + if (*e != '\0') + return term_printf("Invalid instruction count '%s'\n", argv[1].sv); + } + else if (*e == '\0') { + to = from; + } + else { + return term_printf("Invalid instruction offset '%s'\n", argv[1].sv); + } + + target = frame->closure->function; + + if (from >= target->chunk.count || to >= target->chunk.count) + return term_printf("Instruction offset '%s' out of range 0..%zu\n", + argv[1].sv, target->chunk.count - 1); + } + else if (*argv[1].sv == '(') { + uc_parse_config_t conf = { .raw_mode = true }; + uc_source_t *source = uc_source_new_buffer("[disasm expression]", + xstrdup(argv[1].sv), strlen(argv[1].sv)); + + char *err; + prog = uc_compile(&conf, source, &err); + + uc_source_put(source); + + if (!prog) { + term_write(err, strlen(err)); + free(err); + + return term_print("Invalid expression\n"); + } + + target = uc_program_entry(prog); + from = 0; + to = target->chunk.count - 1; + } + else { + char *p = strchr(argv[1].sv, '+'); + size_t limit = SIZE_MAX; + + if (p) { + char *e; + limit = strtoul(p + 1, &e, 10); + + if (e == p + 1 || *e != '\0' || limit == 0) + return term_printf("Invalid instruction count '%s'\n", p + 1); + + *p++ = 0; + } + + uc_program_function_foreach(frame->closure->function->program, fn) { + if (!strcmp(fn->name, argv[1].sv)) { + target = fn; + from = 0; + to = (limit < target->chunk.count) + ? limit : target->chunk.count - 1; + break; + } + } + + if (!target) + return term_printf("Unable to find function '%s'\n", argv[1].sv); + } + } + else { + insn_span_t stmt; + + target = frame->closure->function; + + if (!find_statement_boundaries(target, frame->ip, 0, &stmt)) + return term_print("Unable to determine current statement boundaries\n"); + + from = stmt.ip_start - target->chunk.entries; + to = (stmt.ip_end - target->chunk.entries) - 1; + } + + uint8_t *bytecode = target->chunk.entries; + + /* find nearest instruction start */ + 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; + } + + for (size_t i = from; i <= to; ) { + union { uint8_t u8; uint16_t u16; uint32_t u32; int32_t s32; } arg; + size_t n = insn_length(bytecode + i, target->program); + uint8_t insn = bytecode[i]; + int off = buf.bpos; + fg_color_t color; + + sprintbuf(&buf, "%06zu:", i); + + for (size_t j = 0; j <= (size_t)abs(uc_vm_insn_format[insn]); j++) { + if (j == 0) + color = 0; + else if (j <= (size_t)abs(uc_vm_insn_format[insn])) + color = FG_BMAGENT; + else + color = FG_BYELLOW; + + printbuf_cs(&buf, " \001%02hhx\177", + &((style_t){ color, 0, 0 }), + bytecode[i + j]); + } + + printbuf_memset(&buf, -1, ' ', 3 * (4 - abs(uc_vm_insn_format[insn]))); + + sprintbuf(&buf, " %7s", insn_names[insn]); + + switch (uc_vm_insn_format[insn]) { + case 0: + break; + + case -4: + arg.s32 = insn_s32(bytecode + i + 1); + + printbuf_cs(&buf, " {\1%c0x%x\177}", + &((style_t){ FG_BMAGENT, 0, 0 }), + arg.s32 < 0 ? '-' : '+', + (uint32_t)(arg.s32 < 0 ? -arg.s32 : arg.s32)); + + break; + + case 1: + arg.u8 = bytecode[i + 1]; + + printbuf_cs(&buf, " {\1%hhu\177}", + &((style_t){ FG_BMAGENT, 0, 0 }), + arg.u8); + + break; + + case 2: + arg.u16 = insn_u16(bytecode + i + 1); + + printbuf_cs(&buf, " {\0010x%hx\177}", + &((style_t){ FG_BMAGENT, 0, 0 }), + arg.u16); + + break; + + case 4: + arg.u32 = insn_u32(bytecode + i + 1); + + if (insn == I_LOAD) { + uc_value_t *cv = load_constval(&target->program->constants, arg.u32); + + char *s = ucv_to_jsonstring(vm, cv); + printbuf_cs(&buf, " {\0010x%x\177 : \002%s\177}", + &((style_t){ FG_BMAGENT, 0, 0 }), + &((style_t){ ucv_type(cv) == UC_STRING ? FG_BMAGENT : FG_CYAN, 0, 0 }), + arg.u32, s); + free(s); + } + 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); + + printbuf_cs(&buf, " {\0010x%x\177 : %s \002%s\177}", + &((style_t){ FG_BMAGENT, 0, 0 }), + &((style_t){ upval ? FG_CYAN : FG_BWHITE, 0, 0 }), + arg.u32, upval ? "upval" : "local", + 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); + + printbuf_cs(&buf, " {\0010x%x\177 : global \002%s\177}", + &((style_t){ FG_BMAGENT, 0, 0 }), + &((style_t){ FG_BWHITE, 0, 0 }), + arg.u32, vn ? ucv_string_get(vn) : "(unknown)"); + } + else if (insn == I_CLFN || insn == I_ARFN) { + printbuf_cs(&buf, " {\0010x%x\177 : %s \001#%u\177}", + &((style_t){ FG_BMAGENT, 0, 0 }), + arg.u32, + (insn == I_CLFN) ? "closure" : "arrow", + arg.u32); + } + else { + printbuf_cs(&buf, " {\0010x%x\177}", + &((style_t){ FG_BMAGENT, 0, 0 }), + arg.u32); + } + + break; + + default: + printbuf_cs(&buf, " \1(unknown operand format: %hhu)\177", + &((style_t){ FG_RED, 0, 0 }), + uc_vm_insn_format[insn]); + + break; + } + + printbuf_truncate(&buf, off, columns, true); + cs(&buf, NULL); + + printbuf_strappend(&buf, "\n"); + + if (insn == I_CLFN || insn == I_ARFN) { + size_t id = 1, nupvals = 0; + 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); + + int off = buf.bpos; + + printbuf_cs(&buf, " … \1%02hhx %02hhx %02hhx %02hhx\177", + &((style_t){ FG_YELLOW, 0, 0 }), + bytecode[i + 5 + j * 4 + 0], bytecode[i + 5 + j * 4 + 1], + bytecode[i + 5 + j * 4 + 2], bytecode[i + 5 + j * 4 + 3]); + + printbuf_cs(&buf, + " capture {\001%c0x%x\177 : %s \002%s\177}", + &((style_t){ FG_YELLOW, 0, 0 }), + &((style_t){ upval ? FG_CYAN : FG_BWHITE, 0, 0 }), + (slot < 0) ? '-' : '+', + (slot < 0) ? -slot : slot, + upval ? "upval" : "local", + vn ? ucv_string_get(vn) : "(unknown)"); + + printbuf_truncate(&buf, off, columns, true); + printbuf_strappend(&buf, "\n"); + } + } + else if (insn == I_CALL || insn == I_QCALL || insn == I_MCALL || insn == I_QMCALL) { + for (size_t j = 0; j < arg.u32 >> 16; j++) { + uint16_t slot = insn_u16(bytecode + i + 5 + j * 2); + int off = buf.bpos; + + printbuf_cs(&buf, " … \1%02hhx %02hhx\177", + &((style_t){ FG_YELLOW, 0, 0 }), + bytecode[i + 5 + j * 2 + 0], bytecode[i + 5 + j * 2 + 1]); + + printbuf_cs(&buf, + " unpack {\0010x%hx\177 : stack slot \002-%hx\177}", + &((style_t){ FG_YELLOW, 0, 0 }), + &((style_t){ FG_BMAGENT, 0, 0 }), + slot, slot + 1); + + printbuf_truncate(&buf, off, columns, true); + printbuf_strappend(&buf, "\n"); + } + } + + term_write(buf.buf, buf.bpos); + printbuf_reset(&buf); + + i += n; + } + + free(buf.buf); + + return true; +} + +static bool +cmd_quit(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) +{ + bool proceed = true; + ssize_t c; + arg_t *v; + + while ((c = term_getline("Terminate program? (y/n) > ", &v, NULL, NULL)) != -1) { + if (c > 0 && v[0].sv[0] == 'y') { + vm->arg.s32 = -1; + uc_vm_raise_exception(vm, EXCEPTION_EXIT, "Terminated"); + proceed = false; + break; + } + + if (c > 0 && v[0].sv[0] == 'n') + break; + } + + while (c > 0) + free(v[--c].sv); + + free(v); + + return proceed; +} + +static void +cli_tab_complete(size_t nargs, arg_t *args, suggestions_t *suggests, void *ud) +{ + uc_vm_t *vm = ud; + uc_callframe_t *frame = uc_debug_curr_frame(vm, 0); + char *cmd = (nargs > 0) ? args[0].sv : NULL; + + /* no completions beyond first arg */ + if (nargs > 2) + return; + + /* no completions without stackframe info */ + if (frame == NULL) + return; + + /* complete command itself */ + if (nargs <= 1) { + for (size_t i = 0; i < ARRAY_SIZE(commands); i++) + if (nargs == 0 || str_startswith(commands[i].command, args[0].sv)) + uc_vector_add(suggests, xstrdup(commands[i].command)); + + return; + } + + /* completions for `break` and `disassemble` */ + if (str_startswith("break", cmd) || + str_startswith("disasm", cmd) || + str_startswith("disassemble", cmd)) { + + size_t len = strlen(args[1].sv); + + uc_program_function_foreach(frame->closure->function->program, fn) { + if (fn->name[0] == '\0') + continue; + + if (len > 0 && strncmp(fn->name, args[1].sv, len) != 0) + continue; + + uc_vector_add(suggests, xstrdup(fn->name)); + } + + return; + } + + /* completions for `lines` */ + if (str_startswith("lines", cmd) || str_startswith("ln", cmd)) { + uc_program_t *prog = frame->closure->function->program; + size_t len = strlen(args[1].sv); + + /* suggest function names */ + uc_program_function_foreach(prog, fn) { + if (fn->name[0] == '\0') + continue; + + if (len > 0 && strncmp(fn->name, args[1].sv, len) != 0) + continue; + + uc_vector_add(suggests, xstrdup(fn->name)); + } + + /* suggest file names */ + for (size_t i = 0; i < prog->sources.count; i++) { + uc_stringbuf_t buf = { 0 }; + printbuf_append_srcpath(&buf, prog->sources.entries[i], SIZE_MAX); + + if (len > 0 && strncmp(buf.buf, args[1].sv, len) != 0) { + free(buf.buf); + continue; + } + + uc_vector_add(suggests, buf.buf); + } + + return; + } + + /* completions for `help` */ + if (str_startswith("help", cmd)) { + size_t len = strlen(args[1].sv); + + /* suggest command names */ + for (size_t i = 0; i < ARRAY_SIZE(commands); i++) { + if (len > 0 && strncmp(commands[i].command, args[1].sv, len) != 0) + continue; + + uc_vector_add(suggests, xstrdup(commands[i].command)); + } + + return; + } + + /* completions for `print` */ + if (str_startswith("print", cmd)) { + 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 len = strlen(args[1].sv); + + /* suggest local variable names */ + for (size_t i = 0; i < decls->count; i++) { + uc_value_t *vname = load_constval(names, decls->entries[i].nameidx); + char *s = ucv_string_get(vname); + + if (*s != '(' && (len == 0 || strncmp(s, args[1].sv, len) == 0)) + uc_vector_add(suggests, xstrdup(s)); + + ucv_put(vname); + } + + /* suggest global variables */ + ucv_object_foreach(uc_vm_scope_get(vm), k, v) { + /* skip functions */ + if (ucv_is_callable(v)) + continue; + + if (len > 0 && strncmp(k, args[1].sv, len) != 0) + continue; + + uc_vector_add(suggests, xstrdup(k)); + } + } +} + +static void +bk_enter_cli(uc_vm_t *vm, uc_breakpoint_t *bk) +{ + debug_breakpoint_t *dbk = (debug_breakpoint_t *)bk; + arg_t *argv = NULL; + ssize_t argc = 0; + + term_isig(false); + print_location(vm, "Paused execution in ", dbk); + + while ((argc = term_getline("dbg > ", &argv, cli_tab_complete, vm)) > -1) { + size_t l = (argc > 0) ? strlen(argv[0].sv) : 0, i; + bool proceed = true; + + for (i = 0; l > 0 && i < ARRAY_SIZE(commands); i++) { + bool match = false; + + for (const char *c = commands[i].command; *c; c += strlen(c) + 1) { + if (strncmp(c, argv[0].sv, l) == 0) { + match = true; + break; + } + } + + if (!match) + continue; + + proceed = commands[i].cb(vm, dbk, argc, argv); + break; + } + + if (l > 0 && i == ARRAY_SIZE(commands)) + term_printf("Unrecognized command '%s'\n", argv[0].sv); + + while (argc > 0) + free(argv[--argc].sv); + + free(argv); + + if (!proceed) + break; + } + + if (dbk->kind == BK_ONCE) + free_breakpoint(vm, &dbk->bk); + + term_isig(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_cli(vm, &dbk.bk); + + uc_value_t *sigint_handler = + uc_vm_registry_get(vm, "debug.orig_int_signal"); + + if (ucv_is_callable(sigint_handler)) { + uc_vm_stack_push(vm, ucv_get(sigint_handler)); + uc_vm_stack_push(vm, ucv_get(uc_fn_arg(0))); + + if (uc_vm_call(vm, false, 1) == EXCEPTION_NONE) + return uc_vm_stack_pop(vm); + } + + return NULL; +} + +static uc_value_t * +uc_debug_sigwinch_handler(uc_vm_t *vm, size_t nargs) +{ + term_dimensions(); + + uc_value_t *sigwinch_handler = + uc_vm_registry_get(vm, "debug.orig_winch_signal"); + + if (ucv_is_callable(sigwinch_handler)) { + uc_vm_stack_push(vm, ucv_get(sigwinch_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(…)` + */ +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 (termstate.initialized == false) { + 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)); + + uc_vm_stack_push(vm, ucv_string_new("SIGWINCH")); + uc_vm_registry_set(vm, "debug.orig_winch_signal", ucsignal(vm, 1)); + ucv_put(uc_vm_stack_pop(vm)); + + uc_vm_stack_push(vm, ucv_string_new("SIGWINCH")); + uc_vm_stack_push(vm, + ucv_cfunction_new("debug_sigwinch_handler", uc_debug_sigwinch_handler)); + ucv_put(ucsignal(vm, 2)); + ucv_put(uc_vm_stack_pop(vm)); + ucv_put(uc_vm_stack_pop(vm)); + + term_raw(); + term_isig(true); + + termstate.initialized = true; + } + + if (ucv_type(mainfn) == UC_CLOSURE) { + uc_function_t *fn = ((uc_closure_t *)mainfn)->function; + update_breakpoint(vm, BK_STEP, bk_enter_cli, 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_cli(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 }, +}; + +void uc_module_init(uc_vm_t *vm, uc_value_t *scope) +{ + uc_function_list_register(scope, debug_fns); + + debug_setup(vm); + + have_highlighting = compile_patterns(); } From 780fce8e9f095f1c72f46aafc74f809e69acc396 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Thu, 20 Jun 2024 23:52:55 +0200 Subject: [PATCH 09/11] main: add command line switch to start debug mode Introduce a new command line switch `-x` which loads the debug module and launches the given program or expression within the interactive debugger. Signed-off-by: Jo-Philipp Wich --- main.c | 44 ++++++++++++++++++++++++++++++++++++----- tests/cram/test_basic.t | 3 +++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/main.c b/main.c index 1faaba5e..7389de94 100644 --- a/main.c +++ b/main.c @@ -106,13 +106,19 @@ 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\n" + " Start program in interactive debugger.\n\n", app); } +static bool +parse_library_load(char *opt, uc_vm_t *vm); static int -compile(uc_vm_t *vm, uc_source_t *src, FILE *precompile, bool strip, char *interp, bool print_result) +compile(uc_vm_t *vm, uc_source_t *src, FILE *precompile, bool strip, + char *interp, bool print_result, bool debugger) { uc_value_t *res = NULL; uc_program_t *program; @@ -140,6 +146,30 @@ compile(uc_vm_t *vm, uc_source_t *src, FILE *precompile, bool strip, char *inter if (vm->gc_interval) uc_vm_gc_start(vm, vm->gc_interval); + if (debugger) { + if (!parse_library_load("debug", vm)) { + fprintf(stderr, "Unable to load debug module\n"); + rc = -2; + goto out; + } + + uc_value_t *dbgmod = ucv_object_get(uc_vm_scope_get(vm), "debug", NULL); + uc_value_t *dbgfn = ucv_object_get(dbgmod, "debugger", NULL); + + if (ucv_type(dbgfn) != UC_CFUNCTION) { + fprintf(stderr, "Unable to locate debugger function\n"); + rc = -2; + goto out; + } + + uc_vm_stack_push(vm, ucv_get(dbgfn)); + uc_vm_stack_push(vm, + ucv_closure_new(vm, uc_program_entry(program), false)); + + if (uc_vm_call(vm, false, 1) == EXCEPTION_NONE) + ucv_put(uc_vm_stack_pop(vm)); + } + rc = uc_vm_execute(vm, program, &res); switch (rc) { @@ -499,8 +529,8 @@ appname(const char *argv0) int main(int argc, char **argv) { - const char *optspec = "he:p:tg:ST::RD:F:U:l:L:c::o:s"; - bool strip = false, print_result = false; + const char *optspec = "he:p:tg:ST::RD:F:U:l:L:c::o:sx"; + bool strip = false, print_result = false, debugger = false; char *interp = "/usr/bin/env ucode"; uc_source_t *source = NULL; FILE *precompile = NULL; @@ -635,6 +665,10 @@ main(int argc, char **argv) case 'o': outfile = optarg; break; + + case 'x': + debugger = true; + break; } } @@ -684,7 +718,7 @@ main(int argc, char **argv) ucv_put(o); - rv = compile(&vm, source, precompile, strip, interp, print_result); + rv = compile(&vm, source, precompile, strip, interp, print_result, debugger); out: uc_search_path_free(&config.module_search_path); diff --git a/tests/cram/test_basic.t b/tests/cram/test_basic.t index 798436e3..8547547c 100644 --- a/tests/cram/test_basic.t +++ b/tests/cram/test_basic.t @@ -79,6 +79,9 @@ 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. + check that ucode prints greetings: From 28f36af48e4e619bf0c0d215dfb47480964cbdc1 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Thu, 19 Dec 2024 14:14:14 +0100 Subject: [PATCH 10/11] compiler: simplify source position tracking Signed-off-by: Jo-Philipp Wich --- compiler.c | 4 +--- include/ucode/compiler.h | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/compiler.c b/compiler.c index b9a13303..dafec632 100644 --- a/compiler.c +++ b/compiler.c @@ -240,7 +240,6 @@ uc_compiler_parse_advance(uc_compiler_t *compiler) { ucv_put(compiler->parser->prev.uv); compiler->parser->prev = compiler->parser->curr; - compiler->parser->prev_endpos = compiler->parser->curr_endpos; while (true) { uc_token_t *tok = uc_lexer_next_token(&compiler->parser->lex); @@ -254,7 +253,6 @@ uc_compiler_parse_advance(uc_compiler_t *compiler) } compiler->parser->curr = *tok; - compiler->parser->curr_endpos = compiler->parser->lex.source->off; if (compiler->parser->curr.type != TK_ERROR) break; @@ -480,7 +478,7 @@ 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_endpos)); + uc_compiler_set_srcpos(compiler, compiler->parser->prev.end)); } static size_t diff --git a/include/ucode/compiler.h b/include/ucode/compiler.h index 376b9baf..f6e4bfd6 100644 --- a/include/ucode/compiler.h +++ b/include/ucode/compiler.h @@ -102,7 +102,6 @@ typedef struct { uc_lexer_t lex; uc_token_t prev, curr; uc_stringbuf_t *error; - size_t prev_endpos, curr_endpos; bool synchronizing; } uc_parser_t; From 568f414344210a37419ba1ca40a97d60eb58d32c Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Thu, 19 Dec 2024 14:59:36 +0100 Subject: [PATCH 11/11] debug: fix relative line context argument processing Fix processing single argument `lines +n` and `lines -n` commands. Signed-off-by: Jo-Philipp Wich --- lib/debug.c | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/debug.c b/lib/debug.c index 2782ee98..06f65b66 100644 --- a/lib/debug.c +++ b/lib/debug.c @@ -5676,11 +5676,19 @@ cmd_lines(uc_vm_t *vm, debug_breakpoint_t *dbk, size_t argc, arg_t *argv) if (*e != '\0') return term_print("Invalid offset\n"); - if (argv[1].sv[0] == '+') { - loc.line += n; - } - else if (argv[1].sv[0] == '-') { - loc.line = (n < loc.line) ? loc.line - n : 1; + if (argv[1].sv[0] == '+' || argv[1].sv[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 (argv[1].sv[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);