321 lines
10 KiB
C
321 lines
10 KiB
C
/**
|
|
* This file is part of the Fun programming language.
|
|
* https://hanez.org/project/fun/
|
|
*
|
|
* Copyright 2025 Johannes Findeisen <you@hanez.org>
|
|
* Licensed under the terms of the ISC license.
|
|
* https://opensource.org/license/isc-license-txt
|
|
*/
|
|
|
|
/* Bring in split-out built-ins without changing the build system yet */
|
|
#include "iter.c"
|
|
#include "map.c"
|
|
#include "string.c"
|
|
#include "vm.h"
|
|
#include "value.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdarg.h>
|
|
|
|
/* Track the currently running VM to annotate error messages */
|
|
static VM *g_active_vm = NULL;
|
|
|
|
/* fprintf wrapper that appends source line info for stderr messages */
|
|
static int fun_vm_vfprintf(FILE *stream, const char *fmt, va_list ap) {
|
|
int written = vfprintf(stream, fmt, ap);
|
|
if (stream == stderr && g_active_vm && g_active_vm->current_line > 0) {
|
|
/* If original message didn't end with newline, add a space first */
|
|
if (written > 0) {
|
|
/* best-effort: do not attempt to inspect fmt string fully; just append line info */
|
|
}
|
|
fprintf(stream == stderr ? stderr : stream, " (line %d)\n", g_active_vm->current_line);
|
|
}
|
|
return written;
|
|
}
|
|
|
|
static int fun_vm_fprintf(FILE *stream, const char *fmt, ...) {
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
int r = fun_vm_vfprintf(stream, fmt, ap);
|
|
va_end(ap);
|
|
return r;
|
|
}
|
|
|
|
/* Redirect fprintf within this translation unit so opcode handlers use our wrapper */
|
|
#define fprintf fun_vm_fprintf
|
|
|
|
/*
|
|
Opcode case include index (vm_case_*.inc):
|
|
- Core/stack/frame:
|
|
vm_case_nop.inc, vm_case_halt.inc,
|
|
vm_case_load_const.inc, vm_case_load_local.inc, vm_case_store_local.inc,
|
|
vm_case_load_global.inc, vm_case_store_global.inc,
|
|
vm_case_pop.inc, vm_case_dup.inc, vm_case_swap.inc,
|
|
vm_case_call.inc, vm_case_return.inc, vm_case_print.inc, vm_case_jump.inc, vm_case_jump_if_false.inc
|
|
- Arithmetic and logic:
|
|
vm_case_add.inc, vm_case_sub.inc, vm_case_mul.inc, vm_case_div.inc,
|
|
vm_case_mod.inc, vm_case_lt.inc, vm_case_lte.inc, vm_case_gt.inc, vm_case_gte.inc,
|
|
vm_case_eq.inc, vm_case_neq.inc, vm_case_and.inc, vm_case_or.inc, vm_case_not.inc
|
|
- Conversions:
|
|
vm_case_to_number.inc, vm_case_to_string.inc
|
|
- Arrays and slices:
|
|
vm_case_make_array.inc, vm_case_len.inc,
|
|
vm_case_index_get.inc, vm_case_index_set.inc,
|
|
vm_case_arr_push.inc, vm_case_arr_pop.inc, vm_case_arr_set.inc, vm_case_arr_insert.inc, vm_case_arr_remove.inc,
|
|
vm_case_slice.inc
|
|
- Strings and iteration helpers:
|
|
vm_case_split.inc, vm_case_join.inc, vm_case_substr.inc, vm_case_find.inc,
|
|
vm_case_enumerate.inc, vm_case_zip.inc
|
|
- Maps and I/O:
|
|
vm_case_make_map.inc, vm_case_keys.inc, vm_case_values.inc, vm_case_has_key.inc,
|
|
vm_case_read_file.inc, vm_case_write_file.inc
|
|
- Math utils / RNG:
|
|
vm_case_min.inc, vm_case_max.inc, vm_case_clamp.inc, vm_case_abs.inc, vm_case_pow.inc,
|
|
vm_case_random_seed.inc, vm_case_random_int.inc
|
|
|
|
Dev tips:
|
|
- When adding a new opcode:
|
|
1) Define OP_<NAME> in bytecode.h and opcode_names[] in vm.h.
|
|
2) Implement its VM handler in src/vm_case_<lowercase>.inc.
|
|
3) Include it in the switch below.
|
|
4) Run scripts/check_op_includes.py to verify coverage.
|
|
- You can run scripts/run_examples.sh to sanity-check examples quickly.
|
|
*/
|
|
|
|
static const char* value_type_name(ValueType t) {
|
|
switch (t) {
|
|
case VAL_FUNCTION: return "function";
|
|
case VAL_INT: return "int";
|
|
case VAL_NIL: return "nil";
|
|
case VAL_STRING: return "string";
|
|
default: return "unknown";
|
|
}
|
|
}
|
|
|
|
void vm_clear_output(VM *vm) {
|
|
for (int i = 0; i < vm->output_count; ++i) {
|
|
free_value(vm->output[i]);
|
|
}
|
|
vm->output_count = 0;
|
|
}
|
|
|
|
void vm_free(VM *vm) {
|
|
// currently nothing persistent allocated inside VM itself
|
|
}
|
|
|
|
/* forward declaration for helper used in vm_reset */
|
|
static void vm_pop_frame(VM *vm);
|
|
|
|
void vm_reset(VM *vm) {
|
|
// Pop all frames (free locals)
|
|
while (vm->fp >= 0) {
|
|
vm_pop_frame(vm);
|
|
}
|
|
// Clear stack
|
|
vm->sp = -1;
|
|
// Free globals
|
|
for (int i = 0; i < MAX_GLOBALS; ++i) {
|
|
free_value(vm->globals[i]);
|
|
vm->globals[i] = make_nil();
|
|
}
|
|
// Clear output buffer
|
|
vm_clear_output(vm);
|
|
}
|
|
|
|
void vm_dump_globals(VM *vm) {
|
|
printf("=== globals ===\n");
|
|
for (int i = 0; i < MAX_GLOBALS; ++i) {
|
|
if (vm->globals[i].type != VAL_NIL) {
|
|
printf("[%d] ", i);
|
|
print_value(&vm->globals[i]);
|
|
printf("\n");
|
|
}
|
|
}
|
|
printf("===============\n");
|
|
}
|
|
|
|
static void push_value(VM *vm, Value v) {
|
|
if (vm->sp >= STACK_SIZE - 1) {
|
|
fprintf(stderr, "Runtime error: stack overflow\n");
|
|
exit(1);
|
|
}
|
|
vm->stack[++vm->sp] = v; /* take ownership of v */
|
|
}
|
|
|
|
static Value pop_value(VM *vm) {
|
|
if (vm->sp < 0) {
|
|
fprintf(stderr, "Runtime error: stack underflow\n");
|
|
exit(1);
|
|
}
|
|
return vm->stack[vm->sp--]; /* caller owns returned Value */
|
|
}
|
|
|
|
static void frame_init(Frame *f) {
|
|
f->fn = NULL;
|
|
f->ip = 0;
|
|
for (int i = 0; i < MAX_FRAME_LOCALS; ++i) f->locals[i] = make_nil();
|
|
}
|
|
|
|
void vm_init(VM *vm) {
|
|
vm->sp = -1;
|
|
vm->fp = -1;
|
|
vm->output_count = 0;
|
|
vm->instr_count = 0;
|
|
for (int i = 0; i < MAX_GLOBALS; ++i)
|
|
vm->globals[i] = make_nil();
|
|
}
|
|
|
|
/* push a new frame, transferring ownership of args[] into frame->locals[0..argc-1] */
|
|
static void vm_push_frame(VM *vm, Bytecode *fn, int argc, Value *args) {
|
|
if (vm->fp >= MAX_FRAMES - 1) {
|
|
fprintf(stderr, "Runtime error: too many frames\n");
|
|
exit(1);
|
|
}
|
|
Frame *f = &vm->frames[++vm->fp];
|
|
frame_init(f);
|
|
f->fn = fn;
|
|
f->ip = 0;
|
|
/* move args into locals 0..argc-1 */
|
|
for (int i = 0; i < argc && i < MAX_FRAME_LOCALS; ++i) {
|
|
f->locals[i] = args[i]; /* transfer ownership */
|
|
}
|
|
}
|
|
|
|
/* pop current frame and free its locals */
|
|
static void vm_pop_frame(VM *vm) {
|
|
if (vm->fp < 0) {
|
|
fprintf(stderr, "Runtime error: pop frame with empty frame stack\n");
|
|
exit(1);
|
|
}
|
|
Frame *f = &vm->frames[vm->fp];
|
|
for (int i = 0; i < MAX_FRAME_LOCALS; ++i) {
|
|
free_value(f->locals[i]);
|
|
f->locals[i] = make_nil();
|
|
}
|
|
vm->fp--;
|
|
}
|
|
|
|
void vm_print_output(VM *vm) {
|
|
for (int i = 0; i < vm->output_count; ++i) {
|
|
print_value(&vm->output[i]);
|
|
printf("\n");
|
|
}
|
|
}
|
|
|
|
void vm_run(VM *vm, Bytecode *entry) {
|
|
/* reset instruction count for this run */
|
|
vm->instr_count = 0;
|
|
vm->current_line = 1;
|
|
g_active_vm = vm;
|
|
|
|
/* start with entry frame (no args) */
|
|
vm_push_frame(vm, entry, 0, NULL);
|
|
|
|
while (vm->fp >= 0) {
|
|
Frame *f = &vm->frames[vm->fp];
|
|
|
|
if (f->ip < 0 || f->ip >= f->fn->instr_count) {
|
|
/* no more instructions in this frame -> implicit return nil */
|
|
Value nilv = make_nil();
|
|
vm_pop_frame(vm);
|
|
push_value(vm, nilv);
|
|
continue;
|
|
}
|
|
|
|
Instruction inst = f->fn->instructions[f->ip++];
|
|
vm->instr_count++; /* count each executed instruction */
|
|
|
|
switch (inst.op) {
|
|
/* All opcode handlers as .c includes */
|
|
#include "vm/arithmetic/add.c"
|
|
#include "vm/arithmetic/div.c"
|
|
#include "vm/arithmetic/mul.c"
|
|
#include "vm/arithmetic/sub.c"
|
|
|
|
#include "vm/arrays/apop.c"
|
|
#include "vm/arrays/clear.c"
|
|
#include "vm/arrays/contains.c"
|
|
#include "vm/arrays/enumerate.c"
|
|
#include "vm/arrays/index_get.c"
|
|
#include "vm/arrays/index_of.c"
|
|
#include "vm/arrays/index_set.c"
|
|
#include "vm/arrays/insert.c"
|
|
#include "vm/arrays/join.c"
|
|
#include "vm/arrays/make_array.c"
|
|
#include "vm/arrays/push.c"
|
|
#include "vm/arrays/remove.c"
|
|
#include "vm/arrays/set.c"
|
|
#include "vm/arrays/slice.c"
|
|
#include "vm/arrays/zip.c"
|
|
|
|
#include "vm/core/call.c"
|
|
#include "vm/core/dup.c"
|
|
#include "vm/core/halt.c"
|
|
#include "vm/core/jump.c"
|
|
#include "vm/core/jump_if_false.c"
|
|
#include "vm/core/load_const.c"
|
|
#include "vm/core/load_global.c"
|
|
#include "vm/core/load_local.c"
|
|
#include "vm/core/nop.c"
|
|
#include "vm/core/pop.c"
|
|
#include "vm/core/return.c"
|
|
#include "vm/core/store_global.c"
|
|
#include "vm/core/store_local.c"
|
|
#include "vm/core/swap.c"
|
|
|
|
#include "vm/io/read_file.c"
|
|
#include "vm/io/write_file.c"
|
|
#include "vm/os/env.c"
|
|
|
|
#include "vm/logic/and.c"
|
|
#include "vm/logic/eq.c"
|
|
#include "vm/logic/gt.c"
|
|
#include "vm/logic/gte.c"
|
|
#include "vm/logic/lt.c"
|
|
#include "vm/logic/lte.c"
|
|
#include "vm/logic/neq.c"
|
|
#include "vm/logic/not.c"
|
|
#include "vm/logic/or.c"
|
|
|
|
#include "vm/maps/has_key.c"
|
|
#include "vm/maps/keys.c"
|
|
#include "vm/maps/make_map.c"
|
|
#include "vm/maps/values.c"
|
|
|
|
#include "vm/math/abs.c"
|
|
#include "vm/math/clamp.c"
|
|
#include "vm/math/max.c"
|
|
#include "vm/math/min.c"
|
|
#include "vm/math/mod.c"
|
|
#include "vm/math/pow.c"
|
|
#include "vm/math/random_int.c"
|
|
#include "vm/math/random_seed.c"
|
|
|
|
#include "vm/strings/find.c"
|
|
#include "vm/strings/split.c"
|
|
#include "vm/strings/substr.c"
|
|
|
|
#include "vm/len.c"
|
|
#include "vm/line.c"
|
|
#include "vm/print.c"
|
|
#include "vm/to_number.c"
|
|
#include "vm/to_string.c"
|
|
#include "vm/typeof.c"
|
|
#include "vm/uclamp.c"
|
|
#include "vm/sclamp.c"
|
|
|
|
default:
|
|
if (!opcode_is_valid(inst.op)) {
|
|
fprintf(stderr, "Runtime error: unknown opcode %d (%s) at instruction %d\n",
|
|
inst.op,
|
|
(inst.op >= 0 && inst.op < sizeof(opcode_names)/sizeof(opcode_names[0]))
|
|
? opcode_names[inst.op] : "???",
|
|
f->ip - 1);
|
|
exit(1);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
g_active_vm = NULL;
|
|
}
|