From a5de8b167a14858dfc69d8402157128e1bd54f36 Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 26 May 2026 05:13:04 +0200 Subject: [PATCH] Some more internal optimizations. (0.41.15) --- CMakeLists.txt | 2 +- Doxyfile | 2 +- cmake/Options.cmake | 3 ++ cmake/Targets.cmake | 5 ++ src/bytecode.c | 22 +++++++-- src/bytecode.h | 3 ++ src/parser.c | 51 ++++++++++++++++++- src/vm.c | 106 ++++++++++++++++++++++++++++++++++++++-- src/vm.h | 9 ++++ src/vm/arithmetic/add.c | 1 + src/vm/core/dup.c | 5 +- src/vm/core/pop.c | 5 +- src/vm/core/swap.c | 5 +- 13 files changed, 194 insertions(+), 25 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b1f0d65..1c754fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.41.14 LANGUAGES C) +project(fun VERSION 0.41.15 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/Doxyfile b/Doxyfile index 429658f..39f0078 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = "Fun API Documentation" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 0.41.10 +PROJECT_NUMBER = 0.41.15 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewers a diff --git a/cmake/Options.cmake b/cmake/Options.cmake index 8ad1fa5..8992b1c 100644 --- a/cmake/Options.cmake +++ b/cmake/Options.cmake @@ -34,6 +34,9 @@ endif() # Debug option to enable verbose parser/VM logging option(FUN_DEBUG "Enable extra debug logging in Fun" OFF) +# Optional: VM trace and opcode counters (disabled by default) +option(FUN_TRACE "Enable VM tracing and opcode execution counters" OFF) + # VM settings (configurable via -D...) set(MAX_FRAMES 128 CACHE STRING "Maximum depth of the call stack (frames)") set(MAX_FRAME_LOCALS 64 CACHE STRING "Maximum number of local variables per frame") diff --git a/cmake/Targets.cmake b/cmake/Targets.cmake index 8ce3db9..534d279 100644 --- a/cmake/Targets.cmake +++ b/cmake/Targets.cmake @@ -21,6 +21,11 @@ if(FUN_DEBUG) target_compile_definitions(fun_core PUBLIC FUN_DEBUG=1) endif() +# Enable VM tracing/counters when requested +if(FUN_TRACE) + target_compile_definitions(fun_core PUBLIC FUN_TRACE=1) +endif() + # Provide default stdlib directory and version to the runtime target_compile_definitions(fun_core PUBLIC FUN_VERSION="${PROJECT_VERSION}") target_compile_definitions(fun_core PUBLIC DEFAULT_LIB_DIR="${DEFAULT_LIB_DIR}") diff --git a/src/bytecode.c b/src/bytecode.c index fd7f95b..27109ef 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -36,16 +36,28 @@ Bytecode *bytecode_new(void) { } /** - * @brief Append a constant to a Bytecode's constant table. + * @brief Append a constant to a Bytecode's constant table with de-duplication. * - * The value is deep-copied into the table; the caller retains ownership of v - * and may free it independently. + * The value is compared against existing constants and if an equal constant is + * already present, its index is returned without modifying the table. Equality + * uses value_equals which supports numeric cross-type (int/float) equality and + * string content equality. The caller retains ownership of @p v in all cases. + * When a new constant is inserted, a deep copy is stored in the table. * * @param bc Target bytecode (must not be NULL). - * @param v Value to store (copied). - * @return The index of the stored constant (zero-based). + * @param v Value to store (copied on insert). + * @return The index of the stored or matched existing constant (zero-based). */ int bytecode_add_constant(Bytecode *bc, Value v) { + /* Linear scan for a match. This is fast enough for small constant pools and + * avoids duplicates (smaller bytecode, better cache locality). */ + for (int i = 0; i < bc->const_count; ++i) { + if (value_equals(&bc->constants[i], &v)) { + return i; + } + } + + /* Not found: append a copy */ bc->constants = (Value *)realloc(bc->constants, sizeof(Value) * (bc->const_count + 1)); bc->constants[bc->const_count] = copy_value(&v); return bc->const_count++; diff --git a/src/bytecode.h b/src/bytecode.h index 8bb4897..42c4aea 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -291,6 +291,9 @@ typedef enum { OP_CPP_ADD, // pops b, a; pushes (a + b) } OpCode; +/* Total number of opcodes (keep in sync with OpCode enum). */ +#define OPCODE_COUNT (OP_CPP_ADD + 1) + typedef struct { OpCode op; int32_t operand; diff --git a/src/parser.c b/src/parser.c index 3346115..eee6572 100644 --- a/src/parser.c +++ b/src/parser.c @@ -56,9 +56,16 @@ extern char *preprocess_includes_with_path(const char *src, const char *current_ static void skip_to_eol(const char *src, size_t len, size_t *pos); static int read_line_start(const char *src, size_t len, size_t *pos, int *out_indent); static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, int current_indent); +static void calc_line_col(const char *src, size_t len, size_t pos, int *out_line, int *out_col); /* ---- parser error state ---- */ static const char *g_current_source_path = NULL; /* for propagating filename into nested bytecodes */ +/* Track active source buffer for error line computation and cascade control */ +static const char *g_active_src = NULL; +static size_t g_active_len = 0; +static int g_last_err_line = -1; +static int g_line_err_count = 0; +static const int G_ERRS_PER_LINE_CAP = 3; /* suppress floods after this many per line */ static int g_has_error = 0; static size_t g_err_pos = 0; static char g_err_msg[256]; @@ -137,6 +144,22 @@ static int fun_debug_enabled(void) { * @param ... Arguments matching fmt. */ static void parser_fail(size_t pos, const char *fmt, ...) { + /* Optional flood guard: suppress excessive errors on the same source line */ + if (g_active_src && g_active_len > 0) { + int line = 0, col = 0; + calc_line_col(g_active_src, g_active_len, pos, &line, &col); + if (line == g_last_err_line) { + if (g_line_err_count >= G_ERRS_PER_LINE_CAP) { + /* Too many errors on this line; ignore to avoid cascades */ + return; + } + g_line_err_count++; + } else { + g_last_err_line = line; + g_line_err_count = 1; + } + } + g_has_error = 1; g_err_pos = pos; va_list ap; @@ -7608,8 +7631,16 @@ Bytecode *parse_file_to_bytecode(const char *path) { g_err_line = 0; g_err_col = 0; - /* Set current source path for nested bytecodes to inherit */ + /* Set active source for error mapping/cascade control and current source path for nested bytecodes */ const char *prev_source = g_current_source_path; + const char *prev_active_src = g_active_src; + size_t prev_active_len = g_active_len; + int prev_last_line = g_last_err_line; + int prev_line_count = g_line_err_count; + g_active_src = compile_src; + g_active_len = compile_len; + g_last_err_line = -1; + g_line_err_count = 0; g_current_source_path = path; Bytecode *bc = compile_minimal(compile_src, compile_len); /* assign debug metadata to module bytecode */ @@ -7624,6 +7655,10 @@ Bytecode *parse_file_to_bytecode(const char *path) { } /* restore previous */ g_current_source_path = prev_source; + g_active_src = prev_active_src; + g_active_len = prev_active_len; + g_last_err_line = prev_last_line; + g_line_err_count = prev_line_count; if (g_has_error) { int line = 1, col = 1; @@ -7791,8 +7826,16 @@ Bytecode *parse_string_to_bytecode(const char *source) { g_err_line = 0; g_err_col = 0; - /* Set current source path to for nested bytecodes */ + /* Set active source for error mapping/cascade control and current source path to for nested bytecodes */ const char *prev_src = g_current_source_path; + const char *prev_active_src = g_active_src; + size_t prev_active_len = g_active_len; + int prev_last_line = g_last_err_line; + int prev_line_count = g_line_err_count; + g_active_src = compile_src; + g_active_len = len; + g_last_err_line = -1; + g_line_err_count = 0; g_current_source_path = NULL; Bytecode *bc = compile_minimal(compile_src, len); if (bc) { @@ -7802,6 +7845,10 @@ Bytecode *parse_string_to_bytecode(const char *source) { bc->name = strdup(""); } g_current_source_path = prev_src; + g_active_src = prev_active_src; + g_active_len = prev_active_len; + g_last_err_line = prev_last_line; + g_line_err_count = prev_line_count; if (g_has_error) { int line = 1, col = 1; diff --git a/src/vm.c b/src/vm.c index 073feb0..f5a3803 100644 --- a/src/vm.c +++ b/src/vm.c @@ -224,6 +224,8 @@ static void fun_vm_exit(int code) { /* Forward decl for stack push used by vm_raise_error */ static void push_value(VM *vm, Value v); +/* Forward decl for diagnostic helper used by vm_raise_error */ +static int vm_ip_to_line(const Bytecode *bc, int ip); /* Raise a runtime error that respects try/catch/finally semantics. * If a handler is installed for the current frame, jump to it and push @@ -259,8 +261,22 @@ void vm_raise_error(VM *vm, const char *msg) { f->ip = target; return; } - /* No handler: print annotated message and terminate VM */ - fprintf(stderr, "Runtime error: %s\n", msg ? msg : ""); + /* No handler: print annotated message, stack trace, and terminate VM */ + Bytecode *bc = f->fn; + const char *fname = (bc && bc->name) ? bc->name : ""; + const char *src = (bc && bc->source_file) ? bc->source_file : ""; + int last_ip = f->ip - 1; + int line = vm_ip_to_line(bc, last_ip); + const char *opname = "OP?"; + if (bc && last_ip >= 0 && last_ip < bc->instr_count) { + int op = bc->instructions[last_ip].op; + if (op >= 0 && op < (int)(sizeof(opcode_names) / sizeof(opcode_names[0]))) { + opname = opcode_names[op]; + } + } + fprintf(stderr, "Runtime error at %s:%d in %s (ip=%d, %s): %s\n", + src, line > 0 ? line : 0, fname, last_ip, opname, msg ? msg : ""); + vm_print_stacktrace(vm); vm->fp = -1; /* stop execution */ } @@ -389,6 +405,11 @@ void vm_reset(VM *vm) { // Reset exit code vm->exit_code = 0; +#ifdef FUN_TRACE + // Reset opcode counters + for (int i = 0; i < OPCODE_COUNT; ++i) vm->op_counts[i] = 0; +#endif + // Reset debugger state (breakpoints, stepping) vm_debug_reset(vm); } @@ -732,6 +753,10 @@ void vm_init(VM *vm) { vm->repl_on_error = 0; vm->on_error_repl = NULL; +#ifdef FUN_TRACE + for (int i = 0; i < OPCODE_COUNT; ++i) vm->op_counts[i] = 0; +#endif + /* Debugger state */ vm->debug_step_mode = 0; vm->debug_step_target_fp = -1; @@ -812,6 +837,60 @@ void vm_print_output(VM *vm) { } } +/* ---- Diagnostics helpers ---- */ +/* Find best-effort source line for given ip by scanning backwards for OP_LINE. */ +static int vm_ip_to_line(const Bytecode *bc, int ip) { + if (!bc || !bc->instructions || ip < 0) return 0; + if (ip >= bc->instr_count) ip = bc->instr_count - 1; + for (int i = ip; i >= 0; --i) { + if (bc->instructions[i].op == OP_LINE) { + return bc->instructions[i].operand; + } + } + return 0; +} + +/* Print a human-readable stack trace (top frame first). */ +void vm_print_stacktrace(VM *vm) { + if (!vm) return; + fprintf(stderr, "Stack trace (most recent call first):\n"); + for (int fp = vm->fp; fp >= 0; --fp) { + Frame *fr = &vm->frames[fp]; + Bytecode *bc = fr->fn; + const char *fname = (bc && bc->name) ? bc->name : ""; + const char *src = (bc && bc->source_file) ? bc->source_file : ""; + int ip = fr->ip - 1; /* last executed */ + int line = vm_ip_to_line(bc, ip); + const char *opname = "OP?"; + if (bc && ip >= 0 && ip < bc->instr_count) { + int op = bc->instructions[ip].op; + if (op >= 0 && op < (int)(sizeof(opcode_names) / sizeof(opcode_names[0]))) { + opname = opcode_names[op]; + } + } + fprintf(stderr, " at %s:%d in %s (ip=%d, %s)\n", src, line > 0 ? line : 0, fname, ip, opname); + } +} + +#ifdef FUN_TRACE +/** + * @brief Dump non-zero opcode execution counters to stdout. + */ +void vm_dump_opcode_counters(VM *vm) { + if (!vm) return; + long long total = 0; + for (int i = 0; i < OPCODE_COUNT; ++i) total += vm->op_counts[i]; + printf("=== opcode counters (total %lld) ===\n", total); + for (int i = 0; i < OPCODE_COUNT; ++i) { + long long c = vm->op_counts[i]; + if (c > 0) { + const char *name = (i >= 0 && i < (int)(sizeof(opcode_names) / sizeof(opcode_names[0]))) ? opcode_names[i] : "OP?"; + printf("%3d %-16s %lld\n", i, name, c); + } + } +} +#endif + /** * @brief Execute a bytecode program starting from the given entry point. * @@ -886,11 +965,25 @@ void vm_run(VM *vm, Bytecode *entry) { /* Validate opcode defensively */ if (!opcode_is_valid(inst.op)) { - fprintf(stderr, "Runtime error: invalid opcode %d at ip=%d\n", inst.op, f->ip - 1); - vm_raise_error(vm, "Invalid opcode"); + char buf[256]; + Bytecode *bc = f->fn; + const char *fname = (bc && bc->name) ? bc->name : ""; + const char *src = (bc && bc->source_file) ? bc->source_file : ""; + int ip = f->ip - 1; + int line = vm_ip_to_line(bc, ip); + const char *opname = (inst.op >= 0 && inst.op < (int)(sizeof(opcode_names)/sizeof(opcode_names[0]))) ? opcode_names[inst.op] : "OP?"; + snprintf(buf, sizeof(buf), "invalid opcode %d (%s) at %s:%d in %s (ip=%d)", inst.op, opname, src, line > 0 ? line : 0, fname, ip); + vm_raise_error(vm, buf); break; } +#ifdef FUN_TRACE + /* Increment per-opcode counter */ + if (inst.op >= 0 && inst.op < OPCODE_COUNT) { + vm->op_counts[inst.op]++; + } +#endif + if (vm->trace_enabled) { const char *opname = (inst.op >= 0 && inst.op < (int)(sizeof(opcode_names) / sizeof(opcode_names[0]))) ? opcode_names[inst.op] @@ -1205,4 +1298,9 @@ void vm_run(VM *vm, Bytecode *entry) { } } g_active_vm = NULL; +#ifdef FUN_TRACE + if (vm->trace_enabled) { + vm_dump_opcode_counters(vm); + } +#endif } diff --git a/src/vm.h b/src/vm.h index 2c021fd..c76de93 100644 --- a/src/vm.h +++ b/src/vm.h @@ -121,6 +121,11 @@ struct VM { int output_is_partial[OUTPUT_SIZE]; // 1 when the corresponding output entry should not end with newline (echo) long long instr_count; // executed instructions in the last vm_run + +#ifdef FUN_TRACE + /* Per-opcode execution counters (enabled when FUN_TRACE=1) */ + long long op_counts[OPCODE_COUNT]; +#endif int current_line; // last executed source line (debug) @@ -163,6 +168,10 @@ void vm_clear_output(VM *vm); * @param vm VM instance. */ void vm_print_output(VM *vm); +/** Dump per-opcode execution counters (when FUN_TRACE enabled). */ +void vm_dump_opcode_counters(VM *vm); +/** Print a human-readable VM stack trace to stderr (top frame first). */ +void vm_print_stacktrace(VM *vm); /** * @brief Free all resources owned by the VM (globals, frames, output buffers). * The VM object itself is not freed when allocated on the stack. diff --git a/src/vm/arithmetic/add.c b/src/vm/arithmetic/add.c index 3da5df2..c31bdeb 100644 --- a/src/vm/arithmetic/add.c +++ b/src/vm/arithmetic/add.c @@ -33,6 +33,7 @@ */ case OP_ADD: { + vm_require_stack(vm, 2); Value b = pop_value(vm); Value a = pop_value(vm); if ((a.type == VAL_INT || a.type == VAL_FLOAT) && (b.type == VAL_INT || b.type == VAL_FLOAT)) { diff --git a/src/vm/core/dup.c b/src/vm/core/dup.c index fc6c95f..606d5a7 100644 --- a/src/vm/core/dup.c +++ b/src/vm/core/dup.c @@ -28,10 +28,7 @@ */ case OP_DUP: { - if (vm->sp < 0) { - fprintf(stderr, "Runtime error: stack underflow for DUP\n"); - exit(1); - } + vm_require_stack(vm, 1); Value top = vm->stack[vm->sp]; push_value(vm, copy_value(&top)); break; diff --git a/src/vm/core/pop.c b/src/vm/core/pop.c index 5cc3d02..ecf96b2 100644 --- a/src/vm/core/pop.c +++ b/src/vm/core/pop.c @@ -26,10 +26,7 @@ */ case OP_POP: { - if (vm->sp < 0) { - fprintf(stderr, "Runtime error: stack underflow for POP\n"); - exit(1); - } + vm_require_stack(vm, 1); Value v = pop_value(vm); free_value(v); break; diff --git a/src/vm/core/swap.c b/src/vm/core/swap.c index 8cf2488..c3df3a7 100644 --- a/src/vm/core/swap.c +++ b/src/vm/core/swap.c @@ -23,10 +23,7 @@ */ case OP_SWAP: { - if (vm->sp < 1) { - fprintf(stderr, "Runtime error: stack underflow for SWAP\n"); - exit(1); - } + vm_require_stack(vm, 2); Value a = vm->stack[vm->sp]; Value b = vm->stack[vm->sp - 1]; vm->stack[vm->sp] = b;