llm
Download
391 lines 12.5 KiBCModified 2026-09-01 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
// Built-in functions

#include <stdio.h> // Must be before gmp.h

#include <errno.h>
#include <fcntl.h>
#include <gc.h>
#include <gmp.h>
#include <limits.h>
#include <locale.h>
#include <math.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/param.h>
#include <time.h>
#include <unistd.h>

#include "../config.h"
#include "../util.h"
#include "bool.h"
#include "files.h"
#include "metamethods.h"
#include "number.h"
#include "optionals.h"
#include "path.h"
#include "print.h"
#include "random.h"
#include "siphash.h"
#include "stacktrace.h"
#include "stdlib.h"
#include "text.h"
#include "util.h"

public
bool USE_COLOR;

public
const char *TOMO_PATH = "/usr/local";

public
const char *TOMO_VERSION = "v0";

public
Text_t TOMO_VERSION_TEXT = Text("v0");

#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
#include <dlfcn.h>

static inline const char *get_library_path(void *func) {
    static Dl_info info;
    if (dladdr(func, &info) && info.dli_fname && info.dli_fname[0]) {
        return info.dli_fname; // full path of the library
    }
#if defined(__linux__)
    // In a fully static binary there is no shared object for dladdr() to report,
    // but the code that would live in libtomo is linked directly into the
    // executable, so the executable's own path is the right thing to return.
    static char exe_path[PATH_MAX];
    ssize_t n = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
    if (n > 0) {
        exe_path[n] = '\0';
        return exe_path;
    }
#endif
    return NULL;
}

#elif defined(_WIN32)
#include <windows.h>

static inline const char *get_library_path(void *func) {
    static char path[MAX_PATH];
    HMODULE hm = NULL;
    if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
                          (LPCSTR)func, &hm)) {
        if (GetModuleFileName(hm, path, MAX_PATH)) {
            return path;
        }
    }
    return NULL;
}

#else
#error "Unsupported platform"
#endif

char *find_in_path(const char *name) {
    if (strchr(name, '/')) {
        // name contains a slash → treat as path
        char *abs = realpath(name, NULL);
        if (abs == NULL) fail("Couldn't find real path of: ", name);
        char *ret = String(abs);
        free(abs);
        return ret;
    }

    char *path_env = getenv("PATH");
    if (!path_env) return NULL;

    char *paths = strdup(path_env);
    if (!paths) return NULL;

    char *token = strtok(paths, ":");
    while (token) {
        char candidate[PATH_MAX];
        snprintf(candidate, sizeof(candidate), "%s/%s", token, name);
        if (access(candidate, X_OK) == 0) {
            char *abs = realpath(candidate, NULL);
            free(paths);
            char *ret = String(abs);
            free(abs);
            return ret;
        }
        token = strtok(NULL, ":");
    }

    free(paths);
    return NULL; // not found
}

public
void tomo_configure(void) {
    const char *p = get_library_path(get_library_path);
    p = find_in_path(p);
    Path_t path = Path$from_str(p);
    OptionalPath_t lib_dir = Path$parent(path);
    OptionalPath_t prefix = lib_dir ? Path$parent(lib_dir) : NULL;
    assert(prefix); // Tomo installs as PREFIX/lib/libtomo@<version>.so
    TOMO_PATH = Path$as_c_string(prefix);
    OptionalText_t base_name = Path$base_name(path);
    if (base_name.tag == TEXT_NONE) fail("Tomo is installed at a path that is not valid UTF-8: ", path);
    TOMO_VERSION_TEXT = Text$without_suffix(
        Text$without_prefix(Text$without_prefix(base_name, Text("lib")), Text("tomo@")), Text(".so"));
    TOMO_VERSION = Text$as_c_string(TOMO_VERSION_TEXT);
}

static _Noreturn void signal_handler(int sig, siginfo_t *info, void *userdata) {
    (void)info, (void)userdata;
    if (sig == SIGILL) {
        fflush(stdout);
        if (USE_COLOR) fputs("\x1b[91;7m ===== ILLEGAL INSTRUCTION ===== \n\n\x1b[m", stderr);
        else fputs("===== ILLEGAL INSTRUCTION =====\n\n", stderr);
        print_stacktrace(stderr, 3);
        fflush(stderr);
        tomo_cleanup();
        if (Bool$parse(Text$from_str(getenv("TOMO_CORE_DUMP")), NULL) == true) raise(SIGABRT);
        raise(SIGABRT);
        _exit(1);
    }
    // A crash skips atexit(), so run the cleanups here instead. Draining the
    // list makes this idempotent, which matters because the SIGILL branch
    // above raises SIGABRT and lands back in this handler.
    tomo_cleanup();
    raise(sig);
    _exit(1);
}

// GMP allocates the limb storage behind every bignum through these hooks.
// Route them to the garbage collector so that storage is reclaimed
// automatically: otherwise every mpz result copied into a GC object (see
// Int$from_mpz) leaks its malloc'd limbs, and the temporary mpz_t locals in
// the arithmetic helpers are never cleared and leak outright. Limbs hold no
// pointers, so atomic allocation is both safe and keeps them off the GC's scan
// set (important: limb bit-patterns would otherwise look like pointers and
// falsely retain garbage). GC_REALLOC preserves the atomic kind of the block.
static void *gc_gmp_alloc(size_t size) {
    return GC_MALLOC_ATOMIC(size);
}
static void *gc_gmp_realloc(void *ptr, size_t old_size, size_t new_size) {
    (void)old_size;
    return GC_REALLOC(ptr, new_size);
}
static void gc_gmp_free(void *ptr, size_t size) {
    (void)ptr;
    (void)size; // reclaimed by the GC
}

public
void tomo_init(void) {
    GC_INIT();
    mp_set_memory_functions(gc_gmp_alloc, gc_gmp_realloc, gc_gmp_free);
    tomo_configure();
    const char *color_env = getenv("COLOR");
    USE_COLOR = color_env ? strcmp(color_env, "1") == 0 : isatty(STDOUT_FILENO);
    const char *no_color_env = getenv("NO_COLOR");
    if (no_color_env && no_color_env[0] != '\0') USE_COLOR = false;

    setlocale(LC_ALL, "");
    assert(getrandom(TOMO_HASH_KEY, sizeof(TOMO_HASH_KEY), 0) == sizeof(TOMO_HASH_KEY));

    // Register signal handler for all fatal signals:
    struct sigaction sigact;
    sigact.sa_sigaction = signal_handler;
    sigemptyset(&sigact.sa_mask);
    sigact.sa_flags = SA_SIGINFO | SA_NODEFER | SA_RESETHAND;
    int fatal[] = {SIGHUP,  SIGINT,  SIGQUIT, SIGILL,  SIGTRAP, SIGABRT, SIGBUS,    SIGFPE,  SIGUSR1, SIGSEGV,
                   SIGUSR2, SIGPIPE, SIGALRM, SIGTERM, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGPROF, SIGIO,   SIGSYS};
    for (size_t i = 0; i < sizeof(fatal) / sizeof(fatal[0]); i++) {
        (void)sigaction(fatal[i], &sigact, NULL);
    }

    atexit(tomo_cleanup);
}

static int _inspect_depth = 0;
static file_t *file = NULL;

__attribute__((nonnull)) public
void start_inspect(const char *filename, int64_t start, int64_t end) {
    if (file == NULL || strcmp(file->filename, filename) != 0) file = load_file(filename);

    if (file) {
        size_t first_line_len = strcspn(file->text + start, "\r\n");
        if (first_line_len > (size_t)(end - start)) first_line_len = (size_t)(end - start);
        const char *slash = strrchr(filename, '/');
        const char *file_base = slash ? slash + 1 : filename;

        int64_t line_num = get_line_number(file, file->text + start);
        if (USE_COLOR) {
            print(repeated_char(' ', 3 * _inspect_depth), "\x1b[93;1m>> \x1b[m",
                  string_slice(file->text + start, first_line_len), "   ",
                  repeated_char(' ', MAX(0, 35 - (int64_t)first_line_len - 3 * _inspect_depth)), "\x1b[32;2m[",
                  file_base, ":", line_num, "]\x1b[m");
        } else {
            print(repeated_char(' ', 3 * _inspect_depth), ">> ", string_slice(file->text + start, first_line_len),
                  "   ", repeated_char(' ', MAX(0, 35 - (int64_t)first_line_len - 3 * _inspect_depth)), "[", file_base,
                  ":", line_num, "]");
        }

        // For multi-line expressions, dedent each and print it on a new line with ".. " in front:
        if (end > start + (int64_t)first_line_len) {
            const char *line_start = get_line(file, line_num);
            int64_t indent_len = (int64_t)strspn(line_start, " \t");
            for (const char *line = file->text + start + first_line_len; line < file->text + end;
                 line += strcspn(line, "\r\n")) {
                line += strspn(line, "\r\n");
                if ((int64_t)strspn(line, " \t") >= indent_len) line += indent_len;
                print(repeated_char(' ', 3 * _inspect_depth), USE_COLOR ? "\x1b[33m..\033[m " : ".. ",
                      string_slice(line, strcspn(line, "\r\n")));
            }
        }
    }
    _inspect_depth += 1;
}

public
void end_inspect(const void *expr, const TypeInfo_t *type) {
    _inspect_depth -= 1;

    if (type && type->metamethods.as_text) {
        Text_t expr_text = generic_as_text(expr, USE_COLOR, type);
        Text_t type_name = generic_as_text(NULL, false, type);
        for (int i = 0; i < 3 * _inspect_depth; i++)
            fputc(' ', stdout);
        fprint(stdout, USE_COLOR ? "\x1b[93;1m=\x1b[0m " : "= ", expr_text, USE_COLOR ? " \x1b[33;2m: \x1b[36m" : " : ",
               type_name, USE_COLOR ? "\033[m" : "");
    }
}

public
void say(Text_t text, bool newline) {
    Text$print(stdout, text);
    if (newline) fputc('\n', stdout);
    fflush(stdout);
}

public
_Noreturn void tomo_exit(Text_t text, int32_t status) {
    if (text.length > 0) print(text);
    exit(status);
}

public
OptionalText_t ask(Text_t prompt, bool bold, bool force_tty) {
    OptionalText_t ret = NONE_TEXT;
    FILE *out = stdout;
    FILE *in = stdin;
    bool opened_out = false, opened_in = false;

    char *line = NULL;
    size_t bufsize = 0;
    ssize_t length = 0;
    char *gc_input = NULL;

    if (force_tty && !isatty(STDOUT_FILENO)) {
        out = fopen("/dev/tty", "w");
        if (!out) goto cleanup;
        opened_out = true;
    }

    if (bold) fputs("\x1b[1m", out);
    Text$print(out, prompt);
    if (bold) fputs("\x1b[m", out);
    fflush(out);

    if (force_tty && !isatty(STDIN_FILENO)) {
        in = fopen("/dev/tty", "r");
        if (!in) {
            fputs("\n", out); // finish the line, since the user can't
            goto cleanup;
        }
        opened_in = true;
    }

    length = getline(&line, &bufsize, in);
    if (length == -1) {
        fputs("\n", out); // finish the line, since we didn't get any input
        goto cleanup;
    }

    if (length > 0 && line[length - 1] == '\n') {
        line[length - 1] = '\0';
        --length;
    }

    gc_input = GC_MALLOC_ATOMIC((size_t)(length + 1));
    memcpy(gc_input, line, (size_t)(length + 1));

    ret = Text$from_strn(gc_input, (size_t)(length));

cleanup:
    if (opened_out) fclose(out);
    if (opened_in) fclose(in);
    if (line != NULL) free(line);
    return ret;
}

public
void sleep_seconds(Num_t seconds) {
    double secs = number_to_double(seconds);
    if (secs < 0) fail("Cannot sleep for a negative amount of time: ", secs);
    else if (isnan(secs)) fail("Cannot sleep for a time that is NaN");
    struct timespec ts;
    ts.tv_sec = (time_t)secs;
    ts.tv_nsec = (long)((secs - (double)ts.tv_sec) * 1e9);
    while (nanosleep(&ts, NULL) != 0) {
        if (errno == EINTR) continue;
        fail("Failed to sleep for the requested time (", strerror(errno), ")");
    }
}

public
OptionalText_t getenv_text(Text_t name) {
    const char *val = getenv(Text$as_c_string(name));
    return val ? Text$from_str(val) : NONE_TEXT;
}

public
void setenv_text(Text_t name, OptionalText_t value) {
    int status;
    if (value.tag == TEXT_NONE) {
        status = unsetenv(Text$as_c_string(name));
    } else {
        status = setenv(Text$as_c_string(name), Text$as_c_string(value), 1);
    }
    if (status != 0) {
        if (errno == EINVAL) fail("Invalid environment variable name: ", Text$quoted(name, false, Text("\"")));
        else fail("Failed to set environment variable (", strerror(errno));
    }
}

typedef struct cleanup_s {
    Closure_t cleanup_fn;
    struct cleanup_s *next;
} cleanup_t;

static cleanup_t *cleanups = NULL;

public
void tomo_at_cleanup(Closure_t fn) {
    cleanup_t *new_cleanup = GC_MALLOC(sizeof(cleanup_t));
    new_cleanup->cleanup_fn = fn;
    new_cleanup->next = cleanups;
    cleanups = new_cleanup;
}

public
void tomo_cleanup(void) {
    while (cleanups) {
        // NOTE: we *must* remove the cleanup function from the stack before calling it,
        // otherwise it will cause an infinite loop if the cleanup function fails or exits.
        void (*run_cleanup)(void *) = cleanups->cleanup_fn.fn;
        void *userdata = cleanups->cleanup_fn.userdata;
        cleanups = cleanups->next;
        run_cleanup(userdata);
    }
    fflush(stdout);
    fflush(stderr);
}