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;