about summary refs log tree commit diff
path: root/README.md
blob: f35746e33dd133f99721deb6427b1e1e4a05b476 (plain)
1
2
3
# catskill

a c preprocessor with delusions of grandeur.
ref='#n162'>162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
/*
 * a simple routine to call out to a proper
 * compiler with our c source artifacts produced
 * by our bootstrapping transpiler to turn them
 * into real executable code.
 * currently we call out directly to the llvm-based
 * `clang` tool found in the system path to do
 * the real heavy lifting for us. :)
 * the catskill compiler does have an eventual goal
 * to achieve full self-hosting with real assembler
 * and linker implementations, however that is not
 * of interest to our bootstrap phase.
 *
 * Copyright (c) 2026, Mel G. <mel@rnrd.eu>
 *
 * SPDX-License-Identifier: MPL-2.0
 */

#pragma once

#include "catboot.h"

#ifndef MUSL_LIB
#error "MUSL_LIB not defined"
#endif

#ifndef MUSL_DEV
#error "MUSL_DEV not defined"
#endif

bool
create_build_directory(struct String* build_path)
{
    // we can't directly call mkdtemp to create nested directories,
    // so we create the parent /tmp/catskill/ first.
    ascii build_dir_template[] = "/tmp/catskill/build_XXXXXX";

    struct stat st;
    if (stat("/tmp/catskill", &st) == -1) {
        if (mkdir("/tmp/catskill", 0700) == -1) {
            log_error("failed to create /tmp/catskill/ directory\n");
            return false;
        }
    }

    ascii* build_dir_path = mkdtemp(build_dir_template);
    if (!build_dir_path) { return false; }

    *build_path = string_from_c_string(build_dir_path);
    return true;
}

// TODO: clean up build directory after!

struct Compiler_Command_Result
{
    integer exit_code;
    struct String log;
};

struct Compiler_Command_Result
run_compiler_command(const ascii* command)
{
    log_debug("running backend command: %s\n", command);

    FILE* command_pipe = popen(command, "r");
    if (!command_pipe) return (struct Compiler_Command_Result){ .exit_code = -1 };

    uint bytes_read;
    ascii log_buffer[8192] = { 0 };

    while ((bytes_read = fread(log_buffer, 1, ARRAY_SIZE(log_buffer), command_pipe)) > 0) {
        if (bytes_read >= ARRAY_SIZE(log_buffer)) {
            log_error(
                "compiler output surpassed maximum output length... truncating to %lu bytes\n",
                ARRAY_SIZE(log_buffer) - 1);
            break;
        }
    }

    integer exit_code = pclose(command_pipe);
    struct String log = string_from_c_string(log_buffer);

    return (struct Compiler_Command_Result){ .exit_code = exit_code, .log = log };
}

// TODO: move this over to common library
bool
write_build_file(struct String source, struct String input_path)
{
    FILE* input_file = fopen(string_c_str(input_path), "w");
    if (!input_file) { return false; }

    fwrite(source.data, 1, source.length, input_file);
    fclose(input_file);
    return true;
}

bool
copy_runtime_library(struct String build_path)
{
    const ascii core[] = CATBOOT_EMBED("./boot/runtime/core.c");
    const ascii runtime[] = CATBOOT_EMBED("./boot/runtime/runtime.c");
    const ascii stubs[] = CATBOOT_EMBED("./boot/runtime/stubs.c");

    struct String build_runtime_path = string_append_c_str(build_path, "/runtime/");
    if (mkdir(string_c_str(build_runtime_path), 0700) == -1) {
        log_error("failed to create build runtime directory\n");
        return false;
    }

    struct String
        core_path = string_append_c_str(build_runtime_path, "core.c"),
        runtime_path = string_append_c_str(build_runtime_path, "runtime.c"),
        stubs_path = string_append_c_str(build_runtime_path, "stubs.c");

    struct String
        core_source = string_from_c_string(core),
        runtime_source = string_from_c_string(runtime), stubs_source = string_from_c_string(stubs);

    if (!write_build_file(core_source, core_path)) return false;
    if (!write_build_file(runtime_source, runtime_path)) return false;
    if (!write_build_file(stubs_source, stubs_path)) return false;

    return true;
}

bool
copy_file(struct String source_path, struct String dest_path)
{
    FILE* source_file = fopen(string_c_str(source_path), "rb");
    if (!source_file) {
        log_error("failed to open source file for copying: %s\n", source_path);
        return false;
    }

    // create destination file with the right permissions first
    if (!creat(string_c_str(dest_path), 0700)) {
        log_error("failed to create destination file for copying: %s\n", dest_path);
        fclose(source_file);
        return false;
    }
    FILE* dest_file = fopen(string_c_str(dest_path), "wb");
    if (!dest_file) {
        log_error("failed to open destination file for copying: %s\n", dest_path);
        fclose(source_file);
        return false;
    }

    ascii buffer[4096];
    size_t bytes;
    while ((bytes = fread(buffer, 1, sizeof(buffer), source_file)) > 0) {
        fwrite(buffer, 1, bytes, dest_file);
    }
    fclose(source_file);
    fclose(dest_file);
    return true;
}

struct Build_Result
{
    bool success;

    integer compiler_exit_code;
    struct String compiler_log;
    struct String output_path;
};

#ifdef CATBOOT_BACKEND_CLANG
struct Build_Result
compile_using_backend_clang(
    struct String build_path, struct String source_path, struct String output_path)
{
    // probably too many flags for our purposes, but we kind of want
    // this simple bootstrapping output to be the eventual target
    // of our self-hosted compiler stack, so we avoid anything fancy
    // for an easier target to hit.
    struct String runtime_path = string_append_c_str(build_path, "/runtime/");
    struct String stubs_path = string_append_c_str(runtime_path, "stubs.c");
    const ascii* arguments[] = {
        "clang",

        // high-level compiler behavior
        "-O0",
        "-g",
        "-std=c99",

        // low-level compiler settings, which we optimize to generate
        // extremely simple and human-readable assembly in our final
        // executable. we want it to feel almost hand-written.
        "-fno-omit-frame-pointer",
        "-fno-stack-protector",
        "-fno-plt",
        "-fno-builtin",
        "-fno-inline",
        "-fno-common",
        "-fno-ident",
        "-fno-exceptions",
        "-fno-asynchronous-unwind-tables", // we rely on frame pointer instead

        // output options, for integrating clang output into ours
        "-fno-color-diagnostics",
        "-Wall",
        "-Wextra", // with -w flag or on error

        // linker and header options, static compilation w/ musl
        "-static",
        "-nostdlib",
        "-I",
        string_c_str(runtime_path),
        "-isystem",
        MUSL_DEV "/include",
        // linking must follow this exact order
        MUSL_LIB "/lib/crt1.o",
        MUSL_LIB "/lib/crti.o",
        string_c_str(stubs_path), // stub out some software float implementations
        string_c_str(source_path),
        MUSL_LIB "/lib/libc.a",
        MUSL_LIB "/lib/crtn.o",

        "-o",
        string_c_str(output_path),

        "2>&1", // errors are output to stderr, we want to capture them
    };

    // append all flags to single command
    ascii command[1024] = { 0 };
    for (uint i = 0; i < ARRAY_SIZE(arguments); ++i) {
        strcat(command, arguments[i]);
        strcat(command, " ");
    }

    struct Compiler_Command_Result result = run_compiler_command(command);

    return (struct Build_Result){
        .success = result.exit_code == 0,
        .compiler_exit_code = result.exit_code,
        .compiler_log = result.log,
        .output_path = output_path,
    };
}
#endif

#ifdef CATBOOT_BACKEND_TCC

#include <libtcc.h>

#define BUILD_TCC_MAX_ERROR_LOG_LENGTH 65536

static struct String_Buffer tcc_errors;
bool tcc_errors_truncated;

void
build_tcc_on_error(void* something, const ascii* message)
{
    (void)something;
    if (tcc_errors_truncated) return;

    const ascii* log_prefix = ANSI_BOLD " tcc: " ANSI_NO_BOLD;
    const ascii* log_suffix = "\n";

    uint added_length = strlen(message) + strlen(log_prefix) + strlen(log_suffix);
    uint free_space = string_buffer_capacity(&tcc_errors) - string_buffer_length(&tcc_errors);

    if (free_space <= added_length) {
        tcc_errors_truncated = true;
        return;
    }

    string_buffer_append_c_str(&tcc_errors, log_prefix);
    string_buffer_append_c_str(&tcc_errors, message);
    string_buffer_append_c_str(&tcc_errors, log_suffix);
}

void
build_tcc_reset_errors()
{
    if (tcc_errors.data == nil)
        tcc_errors = string_buffer_new(BUILD_TCC_MAX_ERROR_LOG_LENGTH);
    string_buffer_clear(&tcc_errors);
    tcc_errors_truncated = false;
}

struct Build_Result
compile_using_backend_tcc(
    struct String build_path, struct String source_path, struct String output_path)
{
    TCCState* tcc = tcc_new();
    if (!tcc) {
        log_error("failed to initialize backend (TCC) compiler\n");
        goto error;
    }

    build_tcc_reset_errors();
    tcc_set_error_func(tcc, nil, build_tcc_on_error);

    // NOTE: this has to be called prior to `tcc_set_output_type`, apparently setting the output
    // to EXE pulls in some kind of default runtime which declares the _start & co. symbols before we can!
    tcc_set_options(tcc, "-static -nostdlib -nostdinc");

    // we only want a simple executable output.
    // other options can create a dynamic library, or add the output
    // to our memory to run directly, like for a just-in-time compiler, pretty neat!
    tcc_set_output_type(tcc, TCC_OUTPUT_EXE);

    // add necessary include paths
    struct String runtime_path = string_append_c_str(build_path, "/runtime/");
    tcc_add_include_path(tcc, string_c_str(runtime_path));

    tcc_add_sysinclude_path(tcc, MUSL_DEV "/include");

    // linking order, must be exact:
    // crt1.o -> crti.o -> stubs.c -> source.c -> libc.a -> crtn.o
    tcc_add_file(tcc, MUSL_LIB "/lib/crt1.o");
    tcc_add_file(tcc, MUSL_LIB "/lib/crti.o");

    struct String stubs_path = string_append_c_str(runtime_path, "stubs.c");
    tcc_add_file(tcc, string_c_str(stubs_path));

    if (tcc_add_file(tcc, string_c_str(source_path)) == -1) {
        log_error(
            "failed to add transpiled source to backend compiler (TCC), probably malformed?\n");
        goto error;
    }

    tcc_add_file(tcc, MUSL_LIB "/lib/libc.a");
    tcc_add_file(tcc, MUSL_LIB "/lib/crtn.o");

    // now run the compilation, and output!
    if (tcc_output_file(tcc, string_c_str(output_path)) == -1) {
        log_error("error during backend (TCC) compilation!\n");
        goto error;
    }

    tcc_delete(tcc);
    return (struct Build_Result){ .success = true };

error:
    tcc_delete(tcc);
    struct String log = string_buffer_to_string(&tcc_errors);
    return (struct Build_Result){
        .success = false,
        .compiler_exit_code = -1,
        .compiler_log = log,
    };
}
#endif

struct Build_Result
compile_using_backend(
    struct String build_path, struct String source_path, struct String output_path)
{
#if defined(CATBOOT_BACKEND_CLANG)
    log_debug("current backend: LLVM Clang (CLI)\n");
    return compile_using_backend_clang(build_path, source_path, output_path);
#elif defined(CATBOOT_BACKEND_TCC)
    log_debug("current backend: TCC (libtcc)\n");
    return compile_using_backend_tcc(build_path, source_path, output_path);
#else
#error "no backend defined for compilation"
#endif
}

struct Build_Result
build_executable(struct String source, struct String output_path)
{
    struct String build_path;
    if (!create_build_directory(&build_path)) {
        log_error("failed to create build directory\n");
        return (struct Build_Result){};
    }
    log_debug("temporary build directory for compilation: %s\n", build_path);

    // temporary input and output paths
    struct String build_input = string_append_c_str(build_path, "/input.c");
    struct String build_output = string_append_c_str(build_path, "/output");

    if (!write_build_file(source, build_input)) {
        log_error("failed to create input source file\n");
        return (struct Build_Result){};
    }

    // the runtime library files need to be present in the build directory
    // for inclusion within the generated c source.
    if (!copy_runtime_library(build_path)) {
        log_error("failed to copy runtime library files\n");
        return (struct Build_Result){};
    }

    struct Build_Result result = compile_using_backend(build_path, build_input, build_output);
    if (!result.success) { return result; }

    // copy the output executable to the desired location
    if (!copy_file(build_output, output_path)) {
        log_error("failed to copy final executable to %s\n", output_path);
        return (struct Build_Result){};
    }

    result.output_path = output_path;
    return result;
}