1
0
Fork 0
forked from fun/fun

Some more internal optimizations. (0.41.15)

This commit is contained in:
Johannes Findeisen 2026-05-26 05:13:04 +02:00
commit a5de8b167a
13 changed files with 194 additions and 25 deletions

View file

@ -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)

View file

@ -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

View file

@ -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")

View file

@ -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}")

View file

@ -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++;

View file

@ -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;

View file

@ -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 <input> for nested bytecodes */
/* Set active source for error mapping/cascade control and current source path to <input> 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("<input>");
}
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;

106
src/vm.c
View file

@ -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 : "<error>");
/* No handler: print annotated message, stack trace, and terminate VM */
Bytecode *bc = f->fn;
const char *fname = (bc && bc->name) ? bc->name : "<anon>";
const char *src = (bc && bc->source_file) ? bc->source_file : "<unknown>";
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 : "<error>");
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 : "<anon>";
const char *src = (bc && bc->source_file) ? bc->source_file : "<unknown>";
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 : "<anon>";
const char *src = (bc && bc->source_file) ? bc->source_file : "<unknown>";
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
}

View file

@ -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.

View file

@ -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)) {

View file

@ -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;

View file

@ -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;

View file

@ -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;