Files
spl/nob.c
T

650 lines
20 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#define SPL_CFLAGS "-Wall", "-Wextra", "-O0", "-g", "-D_CRT_SECURE_NO_WARNINGS"
#define nob_cc_flags(cmd) nob_cmd_append(cmd, SPL_CFLAGS)
#undef UNICODE
#define NOB_IMPLEMENTATION
#include "nob.h"
#undef NOB_IMPLEMENTATION
#include "nob_popen.h"
#ifdef _WIN32
#include <consoleapi2.h>
#endif
#define BUILD_FOLDER "build/"
// Content of the .cflags stamp file; must match SPL_CFLAGS above.
#define CFLAGS_STR "-Wall -Wextra -O0 -g"
// Stamp file whose mtime is refreshed whenever the flags change so that all
// object files get rebuilt.
static const char *CFLAGS_STAMP = BUILD_FOLDER ".cflags";
/* clang-format off */
#define VM_SRCS "stage0/spl_mcode.c", "stage0/spl_syscall.c", "stage0/spl_vm.c",
static const char *spl_cli_srcs[] = {"stage0/spl_cli.c", VM_SRCS};
static const char *test_srcs[] = {"stage0/test_spl_vm.c", VM_SRCS};
static const char *splc0_srcs[] = {
"stage1/splc0.c",
"stage1/spl_diag.c",
"stage1/spl_tok.c",
"stage1/spl_lexer.c",
"stage1/spl_ast.c",
"stage1/spl_parser.c",
"stage1/spl_dumptree.c",
"stage1/spl_type.c",
"stage1/spl_fold.c",
"stage1/spl_lang_type.c",
"stage1/spl_attr.c",
"stage1/spl_scope.c",
"stage1/spl_sema.c",
"stage1/spl_intrin.c",
"stage1/spl_ir.c",
"stage1/spl_ast2ir.c",
"stage1/spl_layout.c",
"stage1/spl_ir2vm.c",
"stage0/spl_mcode.c",
};
/* clang-format on */
typedef struct {
const char *name;
const char **srcs;
size_t srcs_count;
} target_t;
static target_t exe_targets[] = {
{"spl_cli", spl_cli_srcs, NOB_ARRAY_LEN(spl_cli_srcs)},
{"test", test_srcs, NOB_ARRAY_LEN(test_srcs)},
{"splc0", splc0_srcs, NOB_ARRAY_LEN(splc0_srcs)},
};
static const target_t *find_exe_target(const char *name) {
for (size_t i = 0; i < NOB_ARRAY_LEN(exe_targets); ++i) {
if (strcmp(exe_targets[i].name, name) == 0) {
return &exe_targets[i];
}
}
return NULL;
}
static const char *obj_path(const char *src) {
// "stage0/spl_vm.c" -> "build/stage0_spl_vm.o"
size_t prefix_len = strlen(BUILD_FOLDER);
char *out = (char *)nob_temp_alloc(prefix_len + strlen(src) + 2 + 1);
char *w = out;
memcpy(w, BUILD_FOLDER, prefix_len);
w += prefix_len;
for (const char *p = src; *p && *p != '.'; ++p) {
*w++ = (*p == '/' || *p == '\\') ? '_' : *p;
}
memcpy(w, ".o", 3);
return out;
}
// Windows 上 clang/gcc 会输出 build/foo.exe;这里统一带上后缀,
// 否则 nob_needs_rebuild 找不到输出会每次重新链接。
static const char *exe_path(const char *name) {
#ifdef _WIN32
return nob_temp_sprintf(BUILD_FOLDER "%s.exe", name);
#else
return nob_temp_sprintf(BUILD_FOLDER "%s", name);
#endif
}
static bool sync_cflags_stamp(void) {
Nob_String_View cflags = nob_sv_from_cstr(CFLAGS_STR);
Nob_String_Builder old = {0};
bool changed = true;
if (nob_file_exists(CFLAGS_STAMP) && nob_read_entire_file(CFLAGS_STAMP, &old)) {
changed = !nob_sv_eq(nob_sb_to_sv(old), cflags);
}
nob_sb_free(old);
if (changed) {
if (!nob_write_entire_file(CFLAGS_STAMP, cflags.data, cflags.count)) {
return false;
}
}
return true;
}
// 头文件依赖:收集 stage0/stage1 下的所有 .h,作为每个 object 的输入。
// nob 不解析 #include,这里用“任一头文件变化则重建全部 object”的保守策略,
// 避免头文件改动后各 .o 结构体布局/常量不一致导致的隐蔽错误。
static Nob_Files g_headers = {0};
static bool collect_header_entry(Nob_Walk_Entry entry) {
if (entry.type != NOB_FILE_REGULAR)
return true;
const char *dot = strrchr(entry.path, '.');
if (!dot || strcmp(dot, ".h") != 0)
return true;
if (strstr(entry.path, ".old") != NULL)
return true;
da_append(&g_headers, nob_temp_strdup(entry.path));
return true;
}
static bool collect_headers(void) {
if (g_headers.count > 0)
return true;
if (!nob_walk_dir("stage0", collect_header_entry))
return false;
if (!nob_walk_dir("stage1", collect_header_entry))
return false;
return true;
}
// stage2 的所有 .spl:作为 splc1.sir 的依赖(不解析 @import,保守全量重建)。
static Nob_Files g_spl2 = {0};
static bool collect_spl2_entry(Nob_Walk_Entry entry) {
if (entry.type != NOB_FILE_REGULAR)
return true;
const char *dot = strrchr(entry.path, '.');
if (!dot || strcmp(dot, ".spl") != 0)
return true;
da_append(&g_spl2, nob_temp_strdup(entry.path));
return true;
}
static bool collect_spl2(void) {
if (g_spl2.count > 0)
return true;
return nob_walk_dir("stage2", collect_spl2_entry);
}
static bool prepare_build(void) {
if (!nob_mkdir_if_not_exists(BUILD_FOLDER))
return false;
if (!sync_cflags_stamp())
return false;
if (!collect_headers())
return false;
return collect_spl2();
}
static bool build_object(const char *src) {
const char *obj = obj_path(src);
if (!nob_file_exists(src)) {
nob_log(NOB_ERROR, "source file `%s` does not exist", src);
return false;
}
// 依赖:源文件 + 所有头文件 + cflags 戳。
size_t ninputs = g_headers.count + 2;
const char **inputs = (const char **)nob_temp_alloc(sizeof(const char *) * ninputs);
inputs[0] = src;
for (size_t i = 0; i < g_headers.count; ++i) {
inputs[i + 1] = g_headers.items[i];
}
inputs[ninputs - 1] = CFLAGS_STAMP;
int needs = nob_needs_rebuild(obj, inputs, ninputs);
if (needs < 0)
return false;
if (needs == 0)
return true;
Nob_Cmd cmd = {0};
nob_cc(&cmd);
nob_cc_flags(&cmd);
nob_cmd_append(&cmd, "-c");
nob_cc_inputs(&cmd, src);
nob_cc_output(&cmd, obj);
return nob_cmd_run(&cmd);
}
static bool link_exe(const target_t *target) {
const char *exe = exe_path(target->name);
const char **objs = (const char **)nob_temp_alloc(sizeof(const char *) * target->srcs_count);
for (size_t i = 0; i < target->srcs_count; ++i) {
objs[i] = obj_path(target->srcs[i]);
}
int needs = nob_needs_rebuild(exe, objs, target->srcs_count);
if (needs < 0)
return false;
if (needs == 0)
return true;
Nob_Cmd cmd = {0};
nob_cc(&cmd);
nob_cc_flags(&cmd);
for (size_t i = 0; i < target->srcs_count; ++i) {
nob_cc_inputs(&cmd, objs[i]);
}
nob_cc_output(&cmd, exe);
return nob_cmd_run(&cmd);
}
static bool build_target(const char *name) {
const target_t *target = find_exe_target(name);
if (target == NULL) {
nob_log(NOB_ERROR, "unknown target `%s`", name);
return false;
}
for (size_t i = 0; i < target->srcs_count; ++i) {
if (!build_object(target->srcs[i])) {
return false;
}
}
return link_exe(target);
}
static bool build_all(void) {
for (size_t i = 0; i < NOB_ARRAY_LEN(exe_targets); ++i) {
if (!build_target(exe_targets[i].name)) {
return false;
}
}
return true;
}
static void parse_expect(const char *name, Nob_Cmd_Result *expect) {
Nob_String_Builder sb = {0};
nob_read_entire_file(name, &sb);
Nob_String_View sv = nob_sb_to_sv(sb);
while (sv.count > 0) {
Nob_String_View line = nob_sv_chop_by_delim(&sv, '\n');
line = nob_sv_trim_left(line); // 去掉前导空格
if (!nob_sv_starts_with(line, nob_sv_from_cstr("//"))) {
break;
}
Nob_String_View rest = nob_sv_from_parts(line.data + 2, line.count - 2);
rest = nob_sv_trim_left(rest);
if (rest.count == 0)
continue;
if (!nob_sv_starts_with(rest, nob_sv_from_cstr("Expect"))) {
break;
}
rest = nob_sv_from_parts(rest.data + 6, rest.count - 6);
rest = nob_sv_trim_left(rest);
// 按 '=' 分割
Nob_String_View key = nob_sv_chop_by_delim(&rest, '=');
key = nob_sv_trim(key);
Nob_String_View value = nob_sv_trim(rest);
if (nob_sv_eq(key, nob_sv_from_cstr("retcode"))) {
int val = 0;
for (size_t i = 0; i < value.count && isdigit(value.data[i]); ++i) {
val = val * 10 + (value.data[i] - '0');
}
// printf("expected code %d\n", val);
expect->exit_code = val;
} else if (nob_sv_eq(key, nob_sv_from_cstr("stdout"))) {
}
}
}
static bool test_once(const char *fname, void *user_data) {
(void)user_data;
const char *compiler_path = "splc0";
const char *cli_path = "spl_cli";
const char *target_sir = BUILD_FOLDER "test.sir";
Nob_Cmd cmd = {0};
if (!build_target(compiler_path)) {
return 1;
}
cmd.count = 0;
nob_cmd_append(&cmd, exe_path(compiler_path));
nob_cmd_append(&cmd, fname, target_sir);
if (!nob_cmd_run(&cmd)) {
return false;
}
if (!build_target(cli_path)) {
return 1;
}
cmd.count = 0;
nob_cmd_append(&cmd, exe_path(cli_path));
nob_cmd_append(&cmd, target_sir);
// nob_cmd_run(&cmd);
Nob_Cmd_Result result = {0};
Nob_String_Builder cmd_str = {0};
nob_cmd_render(cmd, &cmd_str);
Nob_String_View sv = nob_sb_to_sv(cmd_str);
nob_cmd_capture_popen(nob_temp_sv_to_cstr(sv), &result);
// Expect retcode = number
Nob_Cmd_Result expect = {0};
parse_expect(fname, &expect);
if (expect.exit_code != result.exit_code) {
nob_log(NOB_ERROR, "Expect code `%d`, Result code `%d`", expect.exit_code,
result.exit_code);
return false;
}
nob_cmd_result_free(result);
return true;
}
static bool test_once_with_dump(const char *fname, void *user_data) {
const char *target_name = "splc0";
if (!prepare_build())
return 1;
if (!build_target(target_name))
return 1;
Nob_Cmd cmd = {0};
nob_cmd_append(&cmd, exe_path(target_name));
nob_cmd_append(&cmd, "--dump", (char *)user_data, fname, BUILD_FOLDER "t.sir");
return nob_cmd_run(&cmd);
}
// 用 splc0 编译 stage2/splc1.spl -> build/splc1.sir(依赖 stage2 全部 .spl)。
static bool build_splc1(void) {
if (!build_target("splc0"))
return false;
const char *src = "stage2/splc1.spl";
const char *sir = BUILD_FOLDER "splc1.sir";
size_t ninputs = g_spl2.count + 2;
const char **inputs = (const char **)nob_temp_alloc(sizeof(const char *) * ninputs);
inputs[0] = src;
for (size_t i = 0; i < g_spl2.count; ++i)
inputs[i + 1] = g_spl2.items[i];
inputs[ninputs - 1] = CFLAGS_STAMP;
int needs = nob_needs_rebuild(sir, inputs, ninputs);
if (needs < 0)
return false;
if (needs == 0)
return true;
Nob_Cmd cmd = {0};
nob_cmd_append(&cmd, exe_path("splc0"), src, sir);
return nob_cmd_run(&cmd);
}
static bool run_capture(Nob_Cmd *cmd, Nob_Cmd_Result *result) {
Nob_String_Builder rendered = {0};
nob_cmd_render(*cmd, &rendered);
Nob_String_View sv = nob_sb_to_sv(rendered);
bool ok = nob_cmd_capture_popen(nob_temp_sv_to_cstr(sv), result);
nob_sb_free(rendered);
return ok;
}
// diff 测试:对同一文件跑 `splc0 --dump <mode>` 与 `spl_cli splc1.sir --dump <mode>`,
// 逐字节比较 stdout。
static bool test_once_diff(const char *fname, void *user_data) {
const char *mode = (const char *)user_data;
if (!mode) {
nob_log(NOB_ERROR, "diff test requires a mode: lex|ast|sema|ir|vm");
return false;
}
if (!build_target("splc0"))
return false;
if (!build_target("spl_cli"))
return false;
if (!build_splc1())
return false;
Nob_Cmd c_cmd = {0};
nob_cmd_append(&c_cmd, exe_path("splc0"), "--dump", (char *)mode, (char *)fname,
BUILD_FOLDER "diff_c.sir");
Nob_Cmd_Result c_res = {0};
if (!run_capture(&c_cmd, &c_res)) {
nob_cmd_result_free(c_res);
return false;
}
Nob_Cmd s_cmd = {0};
nob_cmd_append(&s_cmd, exe_path("spl_cli"), BUILD_FOLDER "splc1.sir", "--dump",
(char *)mode, (char *)fname);
Nob_Cmd_Result s_res = {0};
if (!run_capture(&s_cmd, &s_res)) {
nob_cmd_result_free(c_res);
nob_cmd_result_free(s_res);
return false;
}
Nob_String_View c_out = nob_sb_to_sv(c_res.out);
Nob_String_View s_out = nob_sb_to_sv(s_res.out);
bool same = nob_sv_eq(c_out, s_out);
if (!same) {
nob_log(NOB_ERROR, "diff mismatch: %s (c=%zu bytes, spl=%zu bytes)", fname,
c_out.count, s_out.count);
// 打印首个不相同行(按行比较)。
Nob_String_View ca = c_out;
Nob_String_View sa = s_out;
size_t lineno = 1;
while (ca.count > 0 || sa.count > 0) {
Nob_String_View cl = nob_sv_chop_by_delim(&ca, '\n');
Nob_String_View sl = nob_sv_chop_by_delim(&sa, '\n');
if (!nob_sv_eq(cl, sl)) {
nob_log(NOB_ERROR, " line %zu:", lineno);
nob_log(NOB_ERROR, " c : %.*s", (int)cl.count, cl.data);
nob_log(NOB_ERROR, " spl: %.*s", (int)sl.count, sl.data);
break;
}
lineno++;
}
nob_log(NOB_ERROR, "--- c stderr/stdout merged above; exit=%d vs spl exit=%d ---",
c_res.exit_code, s_res.exit_code);
}
nob_cmd_result_free(c_res);
nob_cmd_result_free(s_res);
return same;
}
static bool is_stage_name(const char *s) {
return strcmp(s, "stage1") == 0 || strcmp(s, "stage2") == 0 || strcmp(s, "diff") == 0;
}
static bool is_dump_mode(const char *s) {
return strcmp(s, "lex") == 0 || strcmp(s, "ast") == 0 || strcmp(s, "sema") == 0 ||
strcmp(s, "ir") == 0 || strcmp(s, "vm") == 0;
}
static const char *stage_dir_of(const char *stage) {
if (strcmp(stage, "stage2") == 0)
return "./stage2/";
return "./tests/stage1/";
}
typedef bool (*test_fn_t)(const char *fname, void *user_data);
typedef struct {
Nob_Files *files;
test_fn_t test_once;
void *user_data;
} test_walk_ctx_t;
static bool test_walk_entry(Nob_Walk_Entry entry) {
if (entry.type != NOB_FILE_REGULAR)
return true;
const char *dot = strrchr(entry.path, '.');
if (!dot || strcmp(dot, ".spl") != 0)
return true;
test_walk_ctx_t *ctx = (test_walk_ctx_t *)entry.data;
bool run_all = (ctx->files->count == 0);
bool matched = run_all;
for (int i = 0; !matched && i < ctx->files->count; i += 1) {
if (strstr(entry.path, ctx->files->items[i]))
matched = true;
}
if (!matched)
return true;
printf("--- test %s ---\n", entry.path);
if (ctx->test_once(entry.path, ctx->user_data) == false)
return false;
return true;
}
static bool run_test(const char *stage, Nob_Files *files, test_fn_t test_once, void *user_data) {
const char *test_folder = stage_dir_of(stage);
if (!prepare_build())
return false;
if (!nob_file_exists(test_folder)) {
nob_log(NOB_INFO, "%s does not exist, skip", test_folder);
return true;
}
test_walk_ctx_t ctx = {
.files = files,
.test_once = test_once,
.user_data = user_data,
};
return nob_walk_dir(test_folder, test_walk_entry, .data = &ctx);
}
static bool delete_walk_entry(Nob_Walk_Entry entry) { return nob_delete_file(entry.path); }
static bool delete_directory_recursively(const char *dir_path) {
return nob_walk_dir(dir_path, delete_walk_entry, .post_order = true);
}
static void print_help(const char *program_name) {
nob_log(NOB_INFO, "SPL C-side bootstrap build system (nob)");
nob_log(NOB_INFO, "%s", " ");
nob_log(NOB_INFO, "Usage: %s <command> [args...]", program_name);
nob_log(NOB_INFO, "%s", " ");
nob_log(NOB_INFO, "Commands:");
nob_log(NOB_INFO, " build [targets...] build all targets or the given ones");
nob_log(NOB_INFO, " run <target> [args] build and run a target");
nob_log(NOB_INFO, " test build and run VM unit tests");
nob_log(NOB_INFO, " clean remove build artifacts");
nob_log(NOB_INFO, " list list all targets");
nob_log(NOB_INFO, " help/-h/--help print this help message");
nob_log(NOB_INFO, "%s", " ");
nob_log(NOB_INFO, "Targets:");
for (size_t i = 0; i < NOB_ARRAY_LEN(exe_targets); ++i) {
nob_log(NOB_INFO, " %s", exe_targets[i].name);
}
}
int main(int argc, char **argv) {
#ifdef _WIN32
SetConsoleOutputCP(CP_UTF8);
#endif
NOB_GO_REBUILD_URSELF_PLUS(argc, argv, "nob.h", "nob_popen.h");
set_log_handler(nob_cancer_log_handler);
const char *program_name = nob_shift(argv, argc);
const char *command_name = "build";
if (argc > 0) {
command_name = nob_shift(argv, argc);
}
if (strcmp(command_name, "build") == 0) {
if (!prepare_build())
return 1;
if (argc > 0) {
while (argc > 0) {
const char *target_name = nob_shift(argv, argc);
if (!build_target(target_name))
return 1;
}
} else {
if (!build_all())
return 1;
}
return 0;
}
if (strcmp(command_name, "run") == 0) {
if (argc <= 0) {
nob_log(NOB_ERROR, "usage: %s run <target> [args...]", program_name);
return 1;
}
const char *target_name = nob_shift(argv, argc);
if (!prepare_build())
return 1;
if (!build_target(target_name))
return 1;
Nob_Cmd cmd = {0};
nob_cmd_append(&cmd, exe_path(target_name));
while (argc > 0) {
nob_cmd_append(&cmd, nob_shift(argv, argc));
}
return nob_cmd_run(&cmd) ? 0 : 1;
}
if (strcmp(command_name, "test") == 0) {
// 1. stage(可选,默认 stage1)
const char *stage = "stage1";
if (argc > 0 && is_stage_name(argv[0])) {
stage = nob_shift(argv, argc);
}
// 2. mode(可选)
test_fn_t tfn = test_once;
const char *mode_name = NULL;
if (argc > 0) {
if (is_dump_mode(argv[0])) {
tfn = test_once_with_dump;
mode_name = nob_shift(argv, argc);
} else if (strcmp(argv[0], "run") == 0) {
nob_shift(argv, argc);
// HACK:只对 stage1 有意义,跑上次生成的 build/test.sir
Nob_Cmd cmd = {0};
nob_cmd_append(&cmd, BUILD_FOLDER "spl_cli", "-d");
nob_cmd_append(&cmd, BUILD_FOLDER "test.sir");
return nob_cmd_run(&cmd) ? 0 : 1;
}
}
// diff 需要显式 mode,且使用专门的双编译器比较函数。
if (strcmp(stage, "diff") == 0) {
if (!mode_name) {
nob_log(NOB_ERROR, "diff requires a mode: lex|ast|sema|ir|vm");
return 1;
}
tfn = test_once_diff;
}
// 3. 剩下的都当 pattern
Nob_Files files = {0};
while (argc > 0) {
da_append(&files, nob_shift(argv, argc));
}
return run_test(stage, &files, tfn, (void *)mode_name) ? 0 : 1;
}
if (strcmp(command_name, "clean") == 0) {
if (nob_file_exists(BUILD_FOLDER)) {
if (!delete_directory_recursively(BUILD_FOLDER))
return 1;
}
nob_log(NOB_INFO, "cleaned %s", BUILD_FOLDER);
return 0;
}
if (strcmp(command_name, "list") == 0) {
for (size_t i = 0; i < NOB_ARRAY_LEN(exe_targets); ++i) {
nob_log(NOB_INFO, "%s:", exe_targets[i].name);
for (size_t j = 0; j < exe_targets[i].srcs_count; ++j) {
nob_log(NOB_INFO, " %s", exe_targets[i].srcs[j]);
}
}
return 0;
}
if (strcmp(command_name, "help") == 0 || strcmp(command_name, "-h") == 0 ||
strcmp(command_name, "--help") == 0) {
print_help(program_name);
return 0;
}
nob_log(NOB_ERROR, "unknown command `%s`", command_name);
print_help(program_name);
return 1;
}