llm
Download
210 lines 6.6 KiBCModified 2026-09-01 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
// Logic for converting user's Tomo names into valid C identifiers

#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>

#include "environment.h"
#include "sha256.h"
#include "stdlib/fail.h"
#include "stdlib/path.h"
#include "stdlib/print.h"
#include "stdlib/text.h"
#include "stdlib/util.h"

public
Text_t build_target_platform = EMPTY_TEXT;

static const char *c_keywords[] = {
    // Maintain sorted order:
    "_Alignas",
    "_Alignof",
    "_Atomic",
    "_BitInt",
    "_Bool",
    "_Complex",
    "_Decimal128",
    "_Decimal32",
    "_Decimal64",
    "_Generic",
    "_Imaginary",
    "_Noreturn",
    "_Static_assert",
    "_Thread_local",
    "alignas",
    "__alignof__",
    "auto",
    "bool",
    "break",
    "case",
    "char",
    "const",
    "constexpr",
    "continue",
    "default",
    "do",
    "double",
    "else",
    "enum",
    "extern",
    "false",
    "float",
    "for",
    "goto",
    "if",
    "inline",
    "int",
    "long",
    "nullptr",
    "register",
    "restrict",
    "return",
    "short",
    "signed",
    "sizeof",
    "static",
    "static_assert",
    "struct",
    "switch",
    "thread_local",
    "true",
    "typedef",
    "typeof",
    "typeof_unqual",
    "union",
    "unsigned",
    "void",
    "volatile",
    "while",
};

static CONSTFUNC bool is_keyword(const char *word, size_t len) {
    int64_t lo = 0, hi = sizeof(c_keywords) / sizeof(c_keywords[0]) - 1;
    while (lo <= hi) {
        int64_t mid = (lo + hi) / 2;
        int32_t cmp = strncmp(word, c_keywords[mid], len + 1);
        if (cmp == 0) return true;
        else if (cmp > 0) lo = mid + 1;
        else if (cmp < 0) hi = mid - 1;
    }
    return false;
}

public
Text_t valid_c_name(const char *name) {
    size_t len = strlen(name);
    size_t trailing_underscores = 0;
    while (trailing_underscores < len && name[len - 1 - trailing_underscores] == '_')
        trailing_underscores += 1;
    if (is_keyword(name, len - trailing_underscores)) {
        return Texts(Text$from_str(name), Text("_"));
    }
    return Text$from_str(name);
}

public
Text_t CONSTFUNC namespace_name(env_t *env, namespace_t *ns, Text_t name) {
    for (; ns; ns = ns->parent) {
        name = Texts(ns->name, "$", name);
    }
    if (env->id_suffix.length > 0) name = Texts(name, env->id_suffix);
    return name;
}

#define BUILD_CACHE_MAX_AGE_SECS (30 * 24 * 3600)

// Same XDG_CACHE_HOME-or-~/.cache logic as download_cache_dir() in
// packages.c and xdg_tomo_dir() in cmd/common.c, duplicated to avoid a
// dependency between core and cmd/:
static Path_t xdg_cache_tomo_dir(void) {
    const char *cache_home = getenv("XDG_CACHE_HOME");
    return (cache_home && cache_home[0] == '/') ? Path$from_str(String(cache_home, "/tomo"))
                                                : Path$expand_home(Path$from_str("~/.cache/tomo"));
}

// Only called right after creating a new build-cache dir, so ordinary
// (non-fallback) builds never pay this cost:
static void prune_stale_build_dirs(Path_t build_cache) {
    OptionalList_t entries = Path$children(build_cache, true);
    if (entries.data == NULL) return; // No build cache yet: nothing to prune
    time_t now = time(NULL);
    for (int64_t i = 0; i < (int64_t)entries.length; i++) {
        Path_t entry = *(Path_t *)(entries.data + i * entries.stride);
        if (!Path$is_directory(entry, true)) continue;
        OptionalInt64_t modified = Path$modified(entry, true);
        if (!modified.has_value) continue;
        if (now - (time_t)modified.value > BUILD_CACHE_MAX_AGE_SECS) Path$remove(entry, true);
    }
}

// The directory to use as `dir`'s ".tomo" root: ordinarily "<dir>/.tomo",
// but redirected under $XDG_CACHE_HOME/tomo/build/<hash>/.tomo when `dir` is
// $HOME or otherwise not writable. Always ends in a literal ".tomo" so
// ancestor-walking checks like package_store_entry() keep working either
// way. Creates the directory before returning.
public
Path_t tomo_root_for(Path_t dir) {
    char real_dir_buf[PATH_MAX];
    const char *real_dir = realpath(dir, real_dir_buf) ? real_dir_buf : dir;

    const char *home = getenv("HOME");
    char real_home_buf[PATH_MAX];
    const char *real_home = (home && realpath(home, real_home_buf)) ? real_home_buf : NULL;

    const char *reason = NULL;
    if (real_home && streq(real_dir, real_home)) reason = "your home directory";
    else if (!Path$can_write(dir)) reason = "not writable";

    if (reason == NULL) {
        Path_t root = Path$child(dir, Text(".tomo"));
        Result_t made = Path$create_directory(root, 0755, true);
        if (made.Failure.reason.tag != TEXT_NONE) fail("Could not make .tomo directory: ", made.Failure.reason);
        return root;
    }

    // 64 bits of the digest is plenty to avoid collisions between project
    // directories without a 64-character directory name; the directory's own
    // name is tacked on too, just so the cache isn't a wall of opaque hashes:
    char hash[SHA256_HEX_SIZE];
    sha256_hex(real_dir, strlen(real_dir), hash);
    hash[16] = '\0';
    Path_t build_cache = Path$child(xdg_cache_tomo_dir(), Text("build"));
    Path_t hash_dir = Path$child(build_cache, Texts(Text$from_str(hash), "-", Path$base_name(dir)));
    Path_t root = Path$child(hash_dir, Text(".tomo"));
    bool already_existed = Path$is_directory(root, true);
    Result_t made = Path$create_directory(root, 0755, true);
    if (made.Failure.reason.tag != TEXT_NONE) fail("Could not create build cache directory: ", made.Failure.reason);
    if (!already_existed) {
        fprint(stderr, "Warning: ", dir, " is ", reason, "; using a temporary build directory instead: ", root, "\n");
        prune_stale_build_dirs(build_cache);
    }
    return root;
}

public
Path_t tm_build_dir(Path_t tm_path) {
    OptionalPath_t tm_dir = Path$sibling(tm_path, Text("."));
    assert(tm_dir); // A .tm file always has a directory
    Path_t build_dir = tomo_root_for(tm_dir);
    if (build_target_platform.length > 0) {
        build_dir = Path$child(build_dir, build_target_platform);
        if (mkdir(Path$as_c_string(build_dir), 0755) != 0) {
            if (!Path$is_directory(build_dir, true)) err(1, "Could not make .tomo directory");
        }
    }
    return build_dir;
}

public
Text_t get_id_suffix(const char *filename) {
    assert(filename);
    Path_t path = Path$from_str(filename);
    Path_t build_dir = tm_build_dir(path);
    Path_t id_file = Path$child(build_dir, Texts(Path$base_name(path), Text$from_str(".id")));
    OptionalText_t id = Path$read(id_file);
    if (id.tag == TEXT_NONE) err(1, "Could not read ID file: %s", Path$as_c_string(id_file));
    id = Text$trim(id, Text(" \r\n"), true, true);
    return Texts("$", id);
}