398 lines
15 KiB
C
398 lines
15 KiB
C
/* spl_syscall.c — built-in syscall implementations + registration
|
|
*
|
|
* All syscalls validate nargs, cast spl_val_t args to the expected C types,
|
|
* execute, and return the result as spl_val_t.
|
|
*
|
|
* Simple syscalls use the SYSCALL_N macros; complex ones are written
|
|
* manually but follow the same template.
|
|
*/
|
|
|
|
#include "spl_syscall.h"
|
|
#include "include/core_map.h"
|
|
#include "include/core_vec.h"
|
|
#include "spl_ir.h"
|
|
#include "spl_vm.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* =================================================================
|
|
* Macro templates
|
|
*
|
|
* Each SYSCALL_N macro:
|
|
* 1. Validates nargs (prints error and returns -1 on mismatch)
|
|
* 2. Casts args[n] from spl_val_t to the specified C type via
|
|
* (type)(uintptr_t) — this handles both integer and pointer types
|
|
* 3. Evaluates the expression and returns the result as spl_val_t
|
|
* ================================================================= */
|
|
|
|
#define CHECK_NARGS(fname, expected) \
|
|
do { \
|
|
if (nargs != (expected)) { \
|
|
fprintf(stderr, fname ": expected " #expected " args, got %d\n", nargs); \
|
|
return -1; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define SYSCALL_0(name, ret_expr) \
|
|
static spl_val_t name(int nargs, spl_val_t *args) { \
|
|
CHECK_NARGS(#name, 0); \
|
|
(void)args; \
|
|
return (spl_val_t)(uintptr_t)(ret_expr); \
|
|
}
|
|
|
|
#define SYSCALL_1(name, t1, ret_expr) \
|
|
static spl_val_t name(int nargs, spl_val_t *args) { \
|
|
CHECK_NARGS(#name, 1); \
|
|
t1 a1 = (t1)(uintptr_t)args[0]; \
|
|
return (spl_val_t)(uintptr_t)(ret_expr); \
|
|
}
|
|
|
|
#define SYSCALL_2(name, t1, t2, ret_expr) \
|
|
static spl_val_t name(int nargs, spl_val_t *args) { \
|
|
CHECK_NARGS(#name, 2); \
|
|
t1 a1 = (t1)(uintptr_t)args[0]; \
|
|
t2 a2 = (t2)(uintptr_t)args[1]; \
|
|
return (spl_val_t)(uintptr_t)(ret_expr); \
|
|
}
|
|
|
|
#define SYSCALL_3(name, t1, t2, t3, ret_expr) \
|
|
static spl_val_t name(int nargs, spl_val_t *args) { \
|
|
CHECK_NARGS(#name, 3); \
|
|
t1 a1 = (t1)(uintptr_t)args[0]; \
|
|
t2 a2 = (t2)(uintptr_t)args[1]; \
|
|
t3 a3 = (t3)(uintptr_t)args[2]; \
|
|
return (spl_val_t)(uintptr_t)(ret_expr); \
|
|
}
|
|
|
|
#define SYSCALL_4(name, t1, t2, t3, t4, ret_expr) \
|
|
static spl_val_t name(int nargs, spl_val_t *args) { \
|
|
CHECK_NARGS(#name, 4); \
|
|
t1 a1 = (t1)(uintptr_t)args[0]; \
|
|
t2 a2 = (t2)(uintptr_t)args[1]; \
|
|
t3 a3 = (t3)(uintptr_t)args[2]; \
|
|
t4 a4 = (t4)(uintptr_t)args[3]; \
|
|
return (spl_val_t)(uintptr_t)(ret_expr); \
|
|
}
|
|
|
|
/* =================================================================
|
|
* OS syscalls
|
|
* ================================================================= */
|
|
|
|
/* vm_exit — terminate process with the given exit code */
|
|
SYSCALL_1(vm_exit, int, (exit(a1), (spl_val_t)0))
|
|
|
|
/* vm_putchar — write a single character to stdout */
|
|
SYSCALL_1(vm_putchar, int, putchar(a1))
|
|
|
|
/* vm_getchar — read a single character from stdin */
|
|
SYSCALL_0(vm_getchar, getchar())
|
|
|
|
/* vm_putint — print an integer to stdout */
|
|
SYSCALL_1(vm_putint, int, (fprintf(stdout, "%d", a1), (spl_val_t)0))
|
|
|
|
/* vm_putstr — print a string to stdout (no trailing newline) */
|
|
SYSCALL_1(vm_putstr, const char *, (fputs(a1, stdout), (spl_val_t)0))
|
|
|
|
/* vm_fopen — open a file, returns FILE* as spl_val_t */
|
|
SYSCALL_2(vm_fopen, const char *, const char *, (uintptr_t)fopen(a1, a2))
|
|
|
|
/* vm_fclose — close a file, returns 0 on success */
|
|
SYSCALL_1(vm_fclose, FILE *, fclose(a1))
|
|
|
|
/* vm_fread — read from file, returns number of items read */
|
|
SYSCALL_4(vm_fread, void *, size_t, size_t, FILE *, fread(a1, a2, a3, a4))
|
|
|
|
/* vm_fwrite — write to file, returns number of items written */
|
|
SYSCALL_4(vm_fwrite, const void *, size_t, size_t, FILE *, fwrite(a1, a2, a3, a4))
|
|
|
|
/* vm_fsize — get file size in bytes */
|
|
static spl_val_t vm_fsize(int nargs, spl_val_t *args) {
|
|
CHECK_NARGS("vm_fsize", 1);
|
|
FILE *f = (FILE *)(uintptr_t)args[0];
|
|
long cur = ftell(f);
|
|
fseek(f, 0, SEEK_END);
|
|
long sz = ftell(f);
|
|
fseek(f, cur, SEEK_SET);
|
|
return (spl_val_t)(uintptr_t)sz;
|
|
}
|
|
|
|
SYSCALL_0(vm_stdin, stdin)
|
|
SYSCALL_0(vm_stdout, stdout)
|
|
SYSCALL_0(vm_stderr, stderr)
|
|
|
|
/* vm_read_file — read entire file into a malloc'd, null-terminated buffer */
|
|
static spl_val_t vm_read_file(int nargs, spl_val_t *args) {
|
|
CHECK_NARGS("vm_read_file", 1);
|
|
const char *path = (const char *)(uintptr_t)args[0];
|
|
if (path == nullptr) {
|
|
fprintf(stderr, "filepath can't be null");
|
|
return 1;
|
|
}
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) {
|
|
fprintf(stderr, "filepath %s can't be open", path);
|
|
return 1;
|
|
}
|
|
fseek(f, 0, SEEK_END);
|
|
long sz = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
char *buf = (char *)malloc((size_t)sz + 1);
|
|
if (!buf) {
|
|
fclose(f);
|
|
return 0;
|
|
}
|
|
size_t nread = fread(buf, 1, (size_t)sz, f);
|
|
fclose(f);
|
|
buf[nread] = '\0';
|
|
return (spl_val_t)(uintptr_t)buf;
|
|
}
|
|
|
|
/* vm_alloc — allocate memory (malloc) */
|
|
SYSCALL_1(vm_alloc, size_t, (uintptr_t)malloc(a1))
|
|
|
|
/* vm_free — free memory */
|
|
SYSCALL_1(vm_free, void *, (free(a1), (spl_val_t)0))
|
|
|
|
/* vm_realloc — reallocate memory (realloc) */
|
|
SYSCALL_2(vm_realloc, void *, size_t, (uintptr_t)realloc(a1, a2))
|
|
|
|
/* vm_strlen — get string length */
|
|
SYSCALL_1(vm_strlen, const char *, strlen(a1))
|
|
|
|
/* vm_strcmp — compare two strings */
|
|
SYSCALL_2(vm_strcmp, const char *, const char *, strcmp(a1, a2))
|
|
|
|
/* vm_memcpy — copy memory, returns dst */
|
|
SYSCALL_3(vm_memcpy, void *, const void *, size_t, (memcpy(a1, a2, a3), (uintptr_t)a1))
|
|
|
|
static spl_val_t vm_printf(int nargs, spl_val_t *args) {
|
|
(void)args;
|
|
if (nargs <= 0) {
|
|
return 0;
|
|
}
|
|
const char *fmt = (const char *)args[0];
|
|
usize fmt_len = strlen(fmt);
|
|
typedef VEC(char) string_builder_t;
|
|
string_builder_t buffer;
|
|
vec_init(buffer);
|
|
if (nargs <= 1) {
|
|
printf("%s", fmt);
|
|
return nargs;
|
|
}
|
|
int arg_idx = 0;
|
|
char tmp_buf[32];
|
|
memset(tmp_buf, 0, sizeof(tmp_buf));
|
|
for (usize i = 0; i < fmt_len; ++i) {
|
|
if (fmt[i] != '%') {
|
|
vec_push(buffer, fmt[i]);
|
|
continue;
|
|
}
|
|
arg_idx += 1;
|
|
if (++i >= fmt_len) {
|
|
continue;
|
|
}
|
|
switch (fmt[i]) {
|
|
case 'd':
|
|
snprintf(tmp_buf, sizeof(tmp_buf), "%zd", args[arg_idx]);
|
|
for (usize j = 0; j < strlen(tmp_buf); ++j) {
|
|
vec_push(buffer, tmp_buf[j]);
|
|
}
|
|
break;
|
|
case 'c':
|
|
vec_push(buffer, (char)args[arg_idx]);
|
|
break;
|
|
case 's':
|
|
for (usize j = 0; j < strlen((const char *)args[arg_idx]); ++j) {
|
|
vec_push(buffer, ((const char *)args[arg_idx])[j]);
|
|
}
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
}
|
|
vec_push(buffer, '\0');
|
|
printf("%s", buffer.data);
|
|
vec_free(buffer);
|
|
return nargs;
|
|
}
|
|
|
|
/* =================================================================
|
|
* VM syscalls (for comptime — compile-time code execution)
|
|
*
|
|
* These let SPL code create isolated sub-VMs, load compiled .sir
|
|
* programs into them, push arguments, call functions, and run.
|
|
* ================================================================= */
|
|
|
|
/* vm_new — create a new sub-VM instance, returns spl_vm_t* */
|
|
static spl_val_t vm_new(int nargs, spl_val_t *args) {
|
|
CHECK_NARGS("vm_new", 0);
|
|
(void)args;
|
|
spl_vm_t *vm = (spl_vm_t *)malloc(sizeof(spl_vm_t));
|
|
if (!vm)
|
|
return 0;
|
|
spl_vm_init(vm);
|
|
return (spl_val_t)(uintptr_t)vm;
|
|
}
|
|
|
|
/* vm_drop — destroy a sub-VM and its loaded program */
|
|
static spl_val_t vm_drop(int nargs, spl_val_t *args) {
|
|
CHECK_NARGS("vm_drop", 1);
|
|
spl_vm_t *vm = (spl_vm_t *)(uintptr_t)args[0];
|
|
if (!vm)
|
|
return 0;
|
|
if (vm->prog) {
|
|
spl_prog_drop(vm->prog);
|
|
free(vm->prog);
|
|
vm->prog = NULL;
|
|
}
|
|
spl_vm_drop(vm);
|
|
free(vm);
|
|
return 0;
|
|
}
|
|
|
|
/* vm_load — load a .sir file, register syscalls, return spl_prog_t* */
|
|
static spl_val_t vm_load(int nargs, spl_val_t *args) {
|
|
CHECK_NARGS("vm_load", 2);
|
|
spl_vm_t *vm = (spl_vm_t *)(uintptr_t)args[0];
|
|
const char *path = (const char *)(uintptr_t)args[1];
|
|
if (!vm || !path)
|
|
return -1;
|
|
|
|
spl_prog_t *prog = (spl_prog_t *)malloc(sizeof(spl_prog_t));
|
|
if (!prog)
|
|
return -1;
|
|
if (spl_prog_load_from_file(path, prog) != 0) {
|
|
free(prog);
|
|
return -1;
|
|
}
|
|
spl_syscall_register(prog);
|
|
if (spl_vm_load_prog(vm, prog) != 0) {
|
|
spl_prog_drop(prog);
|
|
free(prog);
|
|
return -1;
|
|
}
|
|
return (spl_val_t)(uintptr_t)prog;
|
|
}
|
|
|
|
/* vm_push — push a value onto the sub-VM's stack (for passing arguments) */
|
|
static spl_val_t vm_push(int nargs, spl_val_t *args) {
|
|
CHECK_NARGS("vm_push", 2);
|
|
spl_vm_t *vm = (spl_vm_t *)(uintptr_t)args[0];
|
|
spl_val_t val = args[1];
|
|
if (!vm)
|
|
return -1;
|
|
if (vm->sp >= vm->config.max_stack_depth) {
|
|
fprintf(stderr, "vm_push: stack overflow\n");
|
|
return -1;
|
|
}
|
|
vm->stacks.data[vm->sp++] = val;
|
|
return 0;
|
|
}
|
|
|
|
/* vm_call — call a function by name with pre-pushed arguments
|
|
*
|
|
* Args (3): vm, function_name, nargs
|
|
* The arguments should already be on the sub-VM's stack via vm_push.
|
|
*
|
|
* Mirrors the CALL instruction's frame setup:
|
|
* - Pushes a sentinel frame (saved_ip=-1) if cp==0 so RET halts properly
|
|
* - Pushes the actual call frame
|
|
* - Sets fp = sp - nargs (so arg0 = data[fp], arg1 = data[fp+1], ...)
|
|
* - Sets ip to the function address
|
|
*/
|
|
static spl_val_t vm_call(int nargs, spl_val_t *args) {
|
|
CHECK_NARGS("vm_call", 3);
|
|
spl_vm_t *vm = (spl_vm_t *)(uintptr_t)args[0];
|
|
const char *name = (const char *)(uintptr_t)args[1];
|
|
spl_val_t call_nargs = args[2];
|
|
if (!vm || !name)
|
|
return -1;
|
|
|
|
spl_func_t *func = spl_prog_get_func(vm->prog, name);
|
|
if (!func) {
|
|
fprintf(stderr, "vm_call: function '%s' not found\n", name);
|
|
return -1;
|
|
}
|
|
if (vm->cp >= vm->config.max_call_depth - 1) {
|
|
fprintf(stderr, "vm_call: call stack overflow\n");
|
|
return -1;
|
|
}
|
|
|
|
/* Sentinel frame so the entry function's RET halts cleanly */
|
|
if (vm->cp == 0) {
|
|
vm->frames.data[vm->cp].saved_fp = 0;
|
|
vm->frames.data[vm->cp].saved_ip = (uintptr_t)-1;
|
|
vm->frames.data[vm->cp].nargs = 0;
|
|
vm->cp++;
|
|
}
|
|
|
|
/* Actual call frame (same logic as the CALL instruction) */
|
|
vm->frames.data[vm->cp].saved_fp = vm->fp;
|
|
vm->frames.data[vm->cp].saved_ip = vm->ip;
|
|
vm->frames.data[vm->cp].nargs = call_nargs;
|
|
vm->cp++;
|
|
vm->fp = vm->sp - call_nargs;
|
|
vm->ip = (uintptr_t)func->address;
|
|
return 0;
|
|
}
|
|
|
|
/* vm_run — run a sub-VM to completion, returns exit_code */
|
|
SYSCALL_1(vm_run, spl_vm_t *, spl_vm_run_until(a1, 0))
|
|
|
|
/* =================================================================
|
|
* Registration
|
|
*
|
|
* Called after loading a .sir file. Matches native declarations in
|
|
* prog->natives against known syscall names and hooks up impl_fn.
|
|
* ================================================================= */
|
|
|
|
void spl_syscall_register(spl_prog_t *prog) {
|
|
static spl_native_t table[] = {
|
|
/* OS operations */
|
|
{"vm_exit", 0, vm_exit},
|
|
{"vm_putchar", 0, vm_putchar},
|
|
{"vm_getchar", 0, vm_getchar},
|
|
{"vm_putint", 0, vm_putint},
|
|
{"vm_putstr", 0, vm_putstr},
|
|
{"vm_fopen", 0, vm_fopen},
|
|
{"vm_fclose", 0, vm_fclose},
|
|
{"vm_fread", 0, vm_fread},
|
|
{"vm_fwrite", 0, vm_fwrite},
|
|
{"vm_fsize", 0, vm_fsize},
|
|
{"vm_stdin", 0, vm_stdin},
|
|
{"vm_stdout", 0, vm_stdout},
|
|
{"vm_stderr", 0, vm_stderr},
|
|
{"vm_read_file", 0, vm_read_file},
|
|
{"vm_alloc", 0, vm_alloc},
|
|
{"vm_free", 0, vm_free},
|
|
{"vm_realloc", 0, vm_realloc},
|
|
{"vm_strlen", 0, vm_strlen},
|
|
{"vm_strcmp", 0, vm_strcmp},
|
|
{"vm_memcpy", 0, vm_memcpy},
|
|
{"vm_printf", 0, vm_printf},
|
|
/* VM operations (comptime) */
|
|
{"vm_new", 0, vm_new},
|
|
{"vm_drop", 0, vm_drop},
|
|
{"vm_load", 0, vm_load},
|
|
{"vm_push", 0, vm_push},
|
|
{"vm_call", 0, vm_call},
|
|
{"vm_run", 0, vm_run},
|
|
};
|
|
int n = (int)(sizeof(table) / sizeof(table[0]));
|
|
if (!prog)
|
|
return;
|
|
vec_for(prog->natives, i) {
|
|
spl_native_t *nat = &vec_at(prog->natives, i);
|
|
if (!nat->name || nat->impl_fn)
|
|
continue;
|
|
for (int j = 0; j < n; j++) {
|
|
if (strcmp(nat->name, table[j].name) == 0) {
|
|
nat->impl_fn = table[j].impl_fn;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|