From bb9d0a9f18f746f7074c44e453b14a8c86989ea7 Mon Sep 17 00:00:00 2001 From: zzy <2450266535@qq.com> Date: Fri, 18 Sep 2026 19:40:10 +0800 Subject: [PATCH] =?UTF-8?q?stage2=20=E6=8F=90=E4=BE=9B=E8=AF=8D=E6=B3=95?= =?UTF-8?q?=E5=88=86=E6=9E=90=E5=99=A8=E9=80=9A=E8=BF=87=E4=B8=8Estage1?= =?UTF-8?q?=E7=9A=84=E5=B7=AE=E5=BC=82=E6=B5=8B=E8=AF=95=20stage1=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=E6=95=B0=E7=BB=84=E5=88=B0=E5=88=87=E7=89=87=E7=9A=84?= =?UTF-8?q?=E9=9A=90=E5=BC=8F=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gramma.pest | 4 +- nob.c | 131 +++++++++++++++++++- stage0/spl_vm.c | 10 +- stage1/spl_ast2ir.c | 40 +++++++ stage1/spl_sema.c | 9 ++ stage1/splc0.c | 36 ++++++ stage2/lexer.spl | 155 ++++++++++++++++-------- stage2/splc1.spl | 163 +++++++++++++++++++++++++ stage2/token.spl | 218 +++++++++++++++++++++++++++++++++ stage2/utils/byte_view.spl | 16 +++ stage2/utils/fmt.spl | 240 +++++++++++++++++++++++++++++++++++++ stage2/utils/log.spl | 205 +++++++++++++++++++++++++++++++ stage2/utils/value.spl | 100 ++++++++++++++++ 13 files changed, 1275 insertions(+), 52 deletions(-) create mode 100644 stage2/splc1.spl create mode 100644 stage2/utils/log.spl create mode 100644 stage2/utils/value.spl diff --git a/gramma.pest b/gramma.pest index 55669ab..fc0fe8c 100644 --- a/gramma.pest +++ b/gramma.pest @@ -214,7 +214,9 @@ TypePostfixExpr = { } TypePrimary = { - IDENTIFIER + AT ~ IDENTIFIER // @self 等自引用/内置类型 + | ComptimeCallExpr // @import("...") 等导入即类型 + | IDENTIFIER | KEYWORD_i8 | KEYWORD_u8 | KEYWORD_i16 diff --git a/nob.c b/nob.c index 1783d27..1462834 100644 --- a/nob.c +++ b/nob.c @@ -133,12 +133,33 @@ static bool collect_headers(void) { 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; - return collect_headers(); + if (!collect_headers()) + return false; + return collect_spl2(); } static bool build_object(const char *src) { @@ -320,6 +341,105 @@ static bool test_once_with_dump(const char *fname, void *user_data) { 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 ` 与 `spl_cli splc1.sir --dump `, +// 逐字节比较 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; } @@ -481,6 +601,15 @@ int main(int argc, char **argv) { } } + // 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) { diff --git a/stage0/spl_vm.c b/stage0/spl_vm.c index 6c3a0f5..847f59e 100644 --- a/stage0/spl_vm.c +++ b/stage0/spl_vm.c @@ -724,9 +724,15 @@ int spl_vm_prepare(spl_vm_t *vm, const char *entry, int argc, const char **argv, if (!vm || !vm->prog) { return -1; } - spl_vm_func_t *fn = spl_prog_get_func(vm->prog, entry ? entry : "main"); + spl_vm_func_t *fn = NULL; + const char *entry_point = "_start"; + fn = spl_prog_get_func(vm->prog, entry_point); + if (fn == NULL) { + entry_point = entry ? entry : "main"; + fn = spl_prog_get_func(vm->prog, entry_point); + } if (!fn) { - fprintf(stderr, "vm: entry point '%s' not found\n", entry ? entry : "main"); + fprintf(stderr, "vm: entry point '%s' not found\n", entry_point); return -1; } vm->fp = 0; diff --git a/stage1/spl_ast2ir.c b/stage1/spl_ast2ir.c index 079a451..86455c0 100644 --- a/stage1/spl_ast2ir.c +++ b/stage1/spl_ast2ir.c @@ -1138,6 +1138,46 @@ static spl_ir_node_ref_t emit_expr_raw(spl_ast2ir_t *a, spl_ast_node_ref_t ref) usize nelems = vec_size(n->array_literal.elems); + /* [N]T -> []T:物化数组到局部存储,再构造 {ptr, len} 切片。 */ + spl_lang_type_node_t *sln = spl_lang_type_node(a->sema->type, node_lang(a, n)); + if (sln && sln->kind == SPL_LANG_TYPE_SLICE) { + spl_type_id_t elem_ir = to_ir_type(a, sln->extra.elem); + if (!elem_ir) + Panic("array literal slice element has no ir type"); + spl_lang_type_id_t usize_lang = spl_lang_make_int(a->sema->type, sizeof(void *) * 8, 0); + spl_type_id_t usize_ir = to_ir_type(a, usize_lang); + spl_type_id_t ptr_ir = spl_type_builder_ptr(&a->sema->type->ir, elem_ir); + + spl_ir_node_ref_t base = 0; + if (nelems == 0) { + base = spl_ir_builder_type_const_ptr(a->ir, ptr_ir, NULL); + } else { + spl_type_id_t arr_ir = + spl_type_builder_array(&a->sema->type->ir, elem_ir, nelems); + spl_ir_node_ref_t slot = + spl_ir_builder_mem_alloca(a->ir, arr_ir, const_usize(a, 1)); + for (usize i = 0; i < nelems; ++i) { + spl_ir_node_ref_t fptr = + spl_ir_builder_mem_field(a->ir, arr_ir, slot, i); + spl_ir_node_ref_t v = emit_expr(a, vec_at(n->array_literal.elems, i)); + if (!v) + Panic("array element emit failed"); + spl_ir_builder_mem_store(a->ir, elem_ir, fptr, v); + } + base = spl_ir_builder_mem_field(a->ir, arr_ir, slot, 0); + } + if (!base) + Panic("array literal slice base failed"); + + spl_ir_node_ref_t len = spl_ir_builder_type_const_int(a->ir, usize_ir, nelems); + spl_ir_node_ref_t node = spl_ir_builder_agg_construct(a->ir, t); + if (!node) + Panic("agg_construct failed for array-to-slice"); + spl_ir_builder_agg_construct_field(a->ir, node, base); + spl_ir_builder_agg_construct_field(a->ir, node, len); + return node; + } + spl_ir_node_ref_vec_t field_refs; vec_init(field_refs); for (usize i = 0; i < nelems; ++i) { diff --git a/stage1/spl_sema.c b/stage1/spl_sema.c index 9759792..536688b 100644 --- a/stage1/spl_sema.c +++ b/stage1/spl_sema.c @@ -1520,6 +1520,15 @@ static spl_lang_type_id_t sema_coerce(spl_sema_t *sema, spl_ast_node_ref_t ref, n->resolved_lang_tid = expected; return expected; } + /* [N]T -> []T:数组字面量在需要切片处隐式物化为切片。 */ + if (en && en->kind == SPL_LANG_TYPE_SLICE) { + spl_lang_type_id_t et = en->extra.elem; + for (usize i = 0; i < vec_size(n->array_literal.elems); ++i) + if (!sema_coerce(sema, vec_at(n->array_literal.elems, i), et)) + return 0; + n->resolved_lang_tid = expected; + return expected; + } break; } diff --git a/stage1/splc0.c b/stage1/splc0.c index c1476c7..ecb3a19 100644 --- a/stage1/splc0.c +++ b/stage1/splc0.c @@ -81,6 +81,39 @@ static void dump_tokens(spl_tok_vec_t toks) { vec_free(toks); } +/* 折叠重复分隔符并去掉 "." 路径分量,使同一模块的 import 解析结果唯一。 + 就地重写,'\' 统一成 '/';保留前导分隔符(绝对路径)。 */ +static void normalize_import_path(char *p) { + usize n = strlen(p); + char *out = malloc(n + 1); + if (!out) + return; + char *w = out; + if (p[0] == '/' || p[0] == '\\') + *w++ = '/'; + + char *r = p; + while (*r) { + while (*r == '/' || *r == '\\') + r++; + if (!*r) + break; + char *seg = r; + while (*r && *r != '/' && *r != '\\') + r++; + usize len = (usize)(r - seg); + if (len == 1 && seg[0] == '.') + continue; + if (w != out && w[-1] != '/') + *w++ = '/'; + memcpy(w, seg, len); + w += len; + } + *w = 0; + strcpy(p, out); + free(out); +} + /* 把 import_path 拼接到 importing_file 所在目录。 from_fname 为空时按相对 cwd 处理。绝对路径直接透传。 */ static void resolve_relative(const char *from, const char *path, char *out, usize cap) { @@ -91,6 +124,7 @@ static void resolve_relative(const char *from, const char *path, char *out, usiz } if (path[0] == '/' || path[0] == '\\' || (path[0] && path[1] == ':')) { snprintf(out, cap, "%s", path); + normalize_import_path(out); return; } const char *last_sep = NULL; @@ -99,10 +133,12 @@ static void resolve_relative(const char *from, const char *path, char *out, usiz last_sep = p; if (!last_sep) { snprintf(out, cap, "%s", path); + normalize_import_path(out); return; } usize dir_len = (usize)(last_sep - from) + 1; snprintf(out, cap, "%.*s%s", (int)dir_len, from, path); + normalize_import_path(out); } static spl_ast_t *driver_load(void *user, const char *from_fname, const char *path, diff --git a/stage2/lexer.spl b/stage2/lexer.spl index 51fa0fc..87f4274 100644 --- a/stage2/lexer.spl +++ b/stage2/lexer.spl @@ -11,6 +11,8 @@ type .{ .is_digit, .is_alpha, .is_alnum, .is_hex_digit, .digit_value } = @import type .{ .Token, .TokenKind, .Radix, .Keyword, .Punct, .int_value, .keyword_of } = @import("token.spl"); +type .{ .Span, .span_make } = @import("./diag.spl"); +type Diag = @import("./diag.spl"); type Lexer = @self; @@ -18,9 +20,17 @@ src: ByteView, pos: usize, line: usize, col: usize, +diag: *Diag, // 借用;可为 null(合法输入不产生诊断) +file: usize, -fn new(src: ByteView): Lexer { - ret Lexer { .src = src, .pos = 0, .line = 1, .col = 1 }; +fn new(src: ByteView, diag: *Diag, file: usize): Lexer { + ret Lexer { .src = src, .pos = 0, .line = 1, .col = 1, .diag = diag, .file = file }; +} + +// 记录一条词法诊断(非法输入路径)。合法输入不应触发。 +fn report(self: *Lexer, start: usize, end: usize, message: []u8) { + if self.diag == null { ret; } + Diag.err(self.diag, span_make(self.file, start, end), .LexUnexpected, message); } fn is_ident_start(c: u8): bool { ret is_alpha(c) || c == '_'; } @@ -181,6 +191,8 @@ fn scan_int(self: *Lexer, out: *Vec, start: usize, line: usize, col: usize) { fn scan_char(self: *Lexer, out: *Vec, start: usize, line: usize, col: usize) { var p = self.pos + 1; + var closed = false; + var escape_ok = true; if p < self.src.len { if self.src.ptr[p] == '\\' { p = p + 1; @@ -191,14 +203,30 @@ fn scan_char(self: *Lexer, out: *Vec, start: usize, line: usize, col: usize) { p = p + 1; digits = digits + 1; } + if digits == 0 { escape_ok = false; } } else if p < self.src.len { + var e = self.src.ptr[p]; + if !(e == 'n' || e == 't' || e == 'r' || e == '\\' || e == '"' || e == '\'' || + e == '0') { + escape_ok = false; + } p = p + 1; + } else { + escape_ok = false; } } else { p = p + 1; } } - if p < self.src.len && self.src.ptr[p] == '\'' { p = p + 1; } + if p < self.src.len && self.src.ptr[p] == '\'' { + p = p + 1; + closed = true; + } + if !closed { + report(self, start, p, "unterminated character literal"); + } else if !escape_ok { + report(self, start, p, "invalid escape in character literal"); + } emit_to(self, out, .Char(decode_char(sub(self, start, p))), start, p, line, col); } @@ -206,6 +234,7 @@ fn scan_string(self: *Lexer, out: *Vec, start: usize, line: usize, col: usize) { var p = self.pos + 1; var l = line; var cc = col + 1; + var closed = false; while p < self.src.len { var b = self.src.ptr[p]; if b == '\\' { @@ -218,6 +247,7 @@ fn scan_string(self: *Lexer, out: *Vec, start: usize, line: usize, col: usize) { } else if b == '"' { p = p + 1; cc = cc + 1; + closed = true; break; } else if b == '\n' { l = l + 1; @@ -228,6 +258,7 @@ fn scan_string(self: *Lexer, out: *Vec, start: usize, line: usize, col: usize) { cc = cc + 1; } } + if !closed { report(self, start, p, "unterminated string literal"); } emit_to(self, out, .Str, start, p, line, col); self.line = l; self.col = cc; @@ -243,8 +274,8 @@ fn scan_ident(self: *Lexer, out: *Vec, start: usize, line: usize, col: usize) { } } -// 块注释:可嵌套,整体跳过(不产出 token)。 -fn skip_block_comment(self: *Lexer) { +// 块注释:可嵌套,整体跳过(不产出 token)。未闭合时报诊断。 +fn skip_block_comment(self: *Lexer, start: usize) { self.pos = self.pos + 2; self.col = self.col + 2; var depth: usize = 1; @@ -267,6 +298,9 @@ fn skip_block_comment(self: *Lexer) { self.pos = self.pos + 1; } } + if depth > 0 { + report(self, start, self.src.len, "unterminated block comment"); + } } fn run(self: *Lexer, out: *Vec) { @@ -294,7 +328,7 @@ fn run(self: *Lexer, out: *Vec) { continue; } if c == '/' && has2(self, '*') { - skip_block_comment(self); + skip_block_comment(self, start); continue; } if c == '\'' { @@ -318,6 +352,7 @@ fn run(self: *Lexer, out: *Vec) { if has3(self, '-', '>') { op3(self, out, .ArrowBoth, start, line, col); continue; } if has3(self, '=', '>') { op3(self, out, .FatArrowBoth, start, line, col); continue; } if has3(self, '<', '=') { op3(self, out, .LShAssign, start, line, col); continue; } + if has2(self, '<') { op2(self, out, .LSh, start, line, col); continue; } if has2(self, '-') { op2(self, out, .LeftArrow, start, line, col); continue; } if has2(self, '=') { op2(self, out, .Le, start, line, col); continue; } op1(self, out, .Lt, start, line, col); @@ -409,6 +444,7 @@ fn run(self: *Lexer, out: *Vec) { if c == '$' { op1(self, out, .Dollar, start, line, col); continue; } if c == '?' { op1(self, out, .Cond, start, line, col); continue; } + report(self, start, start + 1, "unexpected character"); emit_to(self, out, .Unknown(c), start, start + 1, line, col); } @@ -416,13 +452,13 @@ fn run(self: *Lexer, out: *Vec) { emit_to(self, out, .Eof, end, end, self.line, self.col); } -fn lex(src: ByteView): VecResult { +fn lex(src: ByteView, diag: *Diag, file: usize): VecResult { var out = Vec.blank(@sizeof(Token), @alignof(Token)); match Vec.new(@sizeof(Token), @alignof(Token)) { .Ok(v) => { out = v; }, .Err(error) => { ret .Err(error); }, } - var self = new(src); + var self = new(src, diag, file); run(&self, &out); ret .Ok(out); } @@ -435,7 +471,7 @@ fn view(text: []u8): ByteView { fn lex_ok(src: []u8): Vec { var out = Vec.blank(@sizeof(Token), @alignof(Token)); - match lex(view(src)) { + match lex(view(src), null, 0) { .Ok(v) => { out = v; }, .Err(error) => { @assert(false, "lex failed"); }, } @@ -536,9 +572,9 @@ fn test_basic() { } fn test_operators() { - var v = lex_ok("<-> <- -> <=> <<= >>= ... .. . := == != < <= > >= && || & | ^ + - * / % ~ ! ? # @ $ , ; : ( ) [ ] { }"); + var v = lex_ok("<-> <- -> <=> <<= << >>= ... .. . := == != < <= > >= && || & | ^ + - * / % ~ ! ? # @ $ , ; : ( ) [ ] { }"); defer { Vec.drop(&v); } - @assert(v.len == 42, "operator token count"); + @assert(v.len == 43, "operator token count"); expect_punct(at(&v, 0), .ArrowBoth); expect_text(at(&v, 0), "<->"); @@ -546,43 +582,45 @@ fn test_operators() { expect_punct(at(&v, 2), .RightArrow); expect_punct(at(&v, 3), .FatArrowBoth); expect_punct(at(&v, 4), .LShAssign); - expect_punct(at(&v, 5), .RShAssign); - expect_punct(at(&v, 6), .Ellipsis); - expect_punct(at(&v, 7), .Range); - expect_punct(at(&v, 8), .Dot); - expect_punct(at(&v, 9), .ColonAssign); - expect_punct(at(&v, 10), .Eq); - expect_punct(at(&v, 11), .Neq); - expect_punct(at(&v, 12), .Lt); - expect_punct(at(&v, 13), .Le); - expect_punct(at(&v, 14), .Gt); - expect_punct(at(&v, 15), .Ge); - expect_punct(at(&v, 16), .AndAnd); - expect_punct(at(&v, 17), .OrOr); - expect_punct(at(&v, 18), .And); - expect_punct(at(&v, 19), .Or); - expect_punct(at(&v, 20), .Xor); - expect_punct(at(&v, 21), .Add); - expect_punct(at(&v, 22), .Sub); - expect_punct(at(&v, 23), .Mul); - expect_punct(at(&v, 24), .Div); - expect_punct(at(&v, 25), .Mod); - expect_punct(at(&v, 26), .BitNot); - expect_punct(at(&v, 27), .Not); - expect_punct(at(&v, 28), .Cond); - expect_punct(at(&v, 29), .Sharp); - expect_punct(at(&v, 30), .At); - expect_punct(at(&v, 31), .Dollar); - expect_punct(at(&v, 32), .Comma); - expect_punct(at(&v, 33), .Semicolon); - expect_punct(at(&v, 34), .Colon); - expect_punct(at(&v, 35), .LParen); - expect_punct(at(&v, 36), .RParen); - expect_punct(at(&v, 37), .LBracket); - expect_punct(at(&v, 38), .RBracket); - expect_punct(at(&v, 39), .LBrace); - expect_punct(at(&v, 40), .RBrace); - expect_eof(at(&v, 41)); + expect_punct(at(&v, 5), .LSh); + expect_text(at(&v, 5), "<<"); + expect_punct(at(&v, 6), .RShAssign); + expect_punct(at(&v, 7), .Ellipsis); + expect_punct(at(&v, 8), .Range); + expect_punct(at(&v, 9), .Dot); + expect_punct(at(&v, 10), .ColonAssign); + expect_punct(at(&v, 11), .Eq); + expect_punct(at(&v, 12), .Neq); + expect_punct(at(&v, 13), .Lt); + expect_punct(at(&v, 14), .Le); + expect_punct(at(&v, 15), .Gt); + expect_punct(at(&v, 16), .Ge); + expect_punct(at(&v, 17), .AndAnd); + expect_punct(at(&v, 18), .OrOr); + expect_punct(at(&v, 19), .And); + expect_punct(at(&v, 20), .Or); + expect_punct(at(&v, 21), .Xor); + expect_punct(at(&v, 22), .Add); + expect_punct(at(&v, 23), .Sub); + expect_punct(at(&v, 24), .Mul); + expect_punct(at(&v, 25), .Div); + expect_punct(at(&v, 26), .Mod); + expect_punct(at(&v, 27), .BitNot); + expect_punct(at(&v, 28), .Not); + expect_punct(at(&v, 29), .Cond); + expect_punct(at(&v, 30), .Sharp); + expect_punct(at(&v, 31), .At); + expect_punct(at(&v, 32), .Dollar); + expect_punct(at(&v, 33), .Comma); + expect_punct(at(&v, 34), .Semicolon); + expect_punct(at(&v, 35), .Colon); + expect_punct(at(&v, 36), .LParen); + expect_punct(at(&v, 37), .RParen); + expect_punct(at(&v, 38), .LBracket); + expect_punct(at(&v, 39), .RBracket); + expect_punct(at(&v, 40), .LBrace); + expect_punct(at(&v, 41), .RBrace); + expect_eof(at(&v, 42)); } fn test_comments_and_literals() { @@ -627,8 +665,29 @@ fn test_comments_and_literals() { expect_eof(at(&v, 12)); } +// 非法输入走 diag:仍产出 token 以便继续收集错误,但 diag 必须记录。 +fn test_diag_errors() { + var diag = Diag.new(null); + defer { Diag.drop(&diag); } + + match lex(view("'\\q' \"abc"), &diag, 0) { + .Ok(v) => { Vec.drop(&v); }, + .Err(error) => { @assert(false, "lex should return tokens"); }, + } + @assert(Diag.has_error(diag), "lex errors recorded"); + + var clean = Diag.new(null); + defer { Diag.drop(&clean); } + match lex(view("fn main() { ret 0; }"), &clean, 0) { + .Ok(v) => { Vec.drop(&v); }, + .Err(error) => { @assert(false, "legal lex failed"); }, + } + @assert(!Diag.has_error(clean), "legal input has no diagnostics"); +} + fn main() { test_basic(); test_operators(); test_comments_and_literals(); + test_diag_errors(); } diff --git a/stage2/splc1.spl b/stage2/splc1.spl new file mode 100644 index 0000000..caa2f47 --- /dev/null +++ b/stage2/splc1.spl @@ -0,0 +1,163 @@ +// Expect retcode = 0 + +// splc1 (stage2 自举编译器入口)。 +// 当前仅实现 `--dump lex [out]`:读取源文件,词法分析,按与 stage1 +// splc0 完全一致的格式打印 token 列表,供 diff 测试逐字节比对。 + +type .{ .ByteView } = @import("./utils/byte_view.spl"); +type .{ .ByteBuffer } = @import("./utils/byte_buffer.spl"); +type .{ .Vec } = @import("./utils/vec.spl"); +type .{ .Token, .kind_name } = @import("./token.spl"); +type .{ .Lexer } = @import("./lexer.spl"); +type .{ .append_usize } = @import("./utils/fmt.spl"); +type .{ .read_all, .print, .eprint } = @import("./utils/io.spl"); +type .{ .set_new, .set_add, .set_drop } = @import("./utils/source.spl"); +type Diag = @import("./diag.spl"); +type Logger = @import("./utils/log.spl"); + +fn token_at(tokens: *Vec, index: usize): Token { + match Vec.get(tokens, index) { + .Ok(ptr) => { ret (ptr as *Token).*; }, + .Err(error) => { ret Token.make(.Unknown(0), ByteView.empty(), 0, 0); }, + } + ret Token.make(.Unknown(0), ByteView.empty(), 0, 0); +} + +// 追加成功返回 true;失败(仅 OOM)返回 false。 +fn push(out: *ByteBuffer, bytes: []u8): bool { + match ByteBuffer.append(out, bytes) { + .Ok => { ret true; }, + .Err(error) => { ret false; }, + } + ret false; +} + +fn push_byte(out: *ByteBuffer, byte: u8): bool { + match ByteBuffer.append_byte(out, byte) { + .Ok => { ret true; }, + .Err(error) => { ret false; }, + } + ret false; +} + +fn push_usize(out: *ByteBuffer, value: usize): bool { + match append_usize(out, value, 10, false) { + .Ok => { ret true; }, + .Err(error) => { ret false; }, + } + ret false; +} + +// 输出格式与 splc0.c 的 dump_tokens 一致: +// tokens got (N)\n +// [TOK_TYPE] (line:col)\n +fn dump_tokens(tokens: *Vec): bool { + var out = ByteBuffer.new(); + defer { ByteBuffer.drop(&out); } + + if !push(&out, "tokens got (") { ret false; } + if !push_usize(&out, tokens.len) { ret false; } + if !push(&out, ")\n") { ret false; } + + var i: usize = 0; + while i < tokens.len { + var tok = token_at(tokens, i); + if !push(&out, "[") { ret false; } + if !push(&out, kind_name(tok.kind)) { ret false; } + if !push(&out, "] ") { ret false; } + if !push(&out, ByteView.as_slice(tok.text)) { ret false; } + if !push(&out, " (") { ret false; } + if !push_usize(&out, tok.line) { ret false; } + if !push_byte(&out, ':') { ret false; } + if !push_usize(&out, tok.col) { ret false; } + if !push(&out, ")\n") { ret false; } + i = i + 1; + } + + match print(ByteBuffer.as_slice(out)) { + .Ok => { ret true; }, + .Err(error) => { ret false; }, + } + ret false; +} + +fn dump_lex(log: *Logger, path: ByteView): i32 { + var content = ByteBuffer.new(); + defer { ByteBuffer.drop(&content); } + match read_all(path, &content) { + .Ok => {}, + .Err(error) => { + Logger.error(log, "cannot read input file"); + ret 1; + }, + } + + var text = ByteView.from_slice(ByteBuffer.as_slice(content)); + + var sources = set_new(); + defer { set_drop(&sources); } + match set_add(&sources, path, text) { + .Ok(index) => {}, + .Err(error) => { + Logger.error(log, "cannot register source"); + ret 1; + }, + } + + var diag = Diag.new(&sources); + defer { Diag.drop(&diag); } + + var tokens = Vec.blank(@sizeof(Token), @alignof(Token)); + match Lexer.lex(text, &diag, 0) { + .Ok(v) => { tokens = v; }, + .Err(error) => { + Logger.error(log, "lex failed"); + ret 1; + }, + } + defer { Vec.drop(&tokens); } + + // 非法输入:走诊断(stage1 在此处是宽容无视,stage2 显式报错)。 + if Diag.has_error(diag) { + var rendered = ByteBuffer.new(); + defer { ByteBuffer.drop(&rendered); } + match Diag.render_human(&diag, &rendered) { + .Ok => {}, + .Err(error) => {}, + } + eprint(ByteBuffer.as_slice(rendered)); + ret 1; + } + + if !dump_tokens(&tokens) { + Logger.fatal(log, "out of memory while dumping tokens"); + ret 1; + } + ret 0; +} + +fn main(argc: i32, argv: **u8): i32 { + // 无参数:自测运行(`nob test stage2`)视为成功。 + if argc <= 1 { ret 0; } + + var log = Logger.new(ByteView.from_slice("splc1")); + + if argc < 4 { + Logger.fatal(&log, "usage: splc1 --dump [out]"); + ret 1; + } + + var arg1 = ByteView.from_cstr(argv[1]); + var mode = ByteView.from_cstr(argv[2]); + var path = ByteView.from_cstr(argv[3]); + + if !ByteView.eq(arg1, ByteView.from_slice("--dump")) { + Logger.fatal(&log, "usage: splc1 --dump [out]"); + ret 1; + } + if !ByteView.eq(mode, ByteView.from_slice("lex")) { + Logger.fatal(&log, "dump mode not implemented"); + ret 1; + } + ret dump_lex(&log, path); +} diff --git a/stage2/token.spl b/stage2/token.spl index 893bdc5..fcd231f 100644 --- a/stage2/token.spl +++ b/stage2/token.spl @@ -108,6 +108,120 @@ fn keyword_of(text: ByteView): KeywordResult { ret .No; } +// ---- token 类型名(与 stage1 spl_tok_type_name 逐字一致,供 dump/diff 使用)---- + +fn keyword_name(kw: Keyword): []u8 { + match kw { + .Any => { ret "KW_ANY"; }, + .As => { ret "KW_AS"; }, + .Bool => { ret "KW_BOOL"; }, + .Break => { ret "KW_BREAK"; }, + .Catch => { ret "KW_CATCH"; }, + .Comptime => { ret "KW_COMPTIME"; }, + .Const => { ret "KW_CONST"; }, + .Continue => { ret "KW_CONTINUE"; }, + .Defer => { ret "KW_DEFER"; }, + .Else => { ret "KW_ELSE"; }, + .Enum => { ret "KW_ENUM"; }, + .Errdefer => { ret "KW_ERRDEFER"; }, + .False => { ret "KW_FALSE"; }, + .Fn => { ret "KW_FN"; }, + .For => { ret "KW_FOR"; }, + .If => { ret "KW_IF"; }, + .Loop => { ret "KW_LOOP"; }, + .Match => { ret "KW_MATCH"; }, + .Null => { ret "KW_NULL"; }, + .Ret => { ret "KW_RET"; }, + .Shape => { ret "KW_SHAPE"; }, + .Struct => { ret "KW_STRUCT"; }, + .True => { ret "KW_TRUE"; }, + .Try => { ret "KW_TRY"; }, + .Type => { ret "KW_TYPE"; }, + .Undefined => { ret "KW_UNDEFINED"; }, + .Union => { ret "KW_UNION"; }, + .Var => { ret "KW_VAR"; }, + .While => { ret "KW_WHILE"; }, + .Underscore => { ret "KW_UNDERSCORE"; }, + } + ret "?"; +} + +fn punct_name(punct: Punct): []u8 { + match punct { + .Eq => { ret "TOK_EQ"; }, + .Assign => { ret "TOK_ASSIGN"; }, + .AddAssign => { ret "TOK_ASSIGN_ADD"; }, + .Add => { ret "TOK_ADD"; }, + .SubAssign => { ret "TOK_ASSIGN_SUB"; }, + .Sub => { ret "TOK_SUB"; }, + .MulAssign => { ret "TOK_ASSIGN_MUL"; }, + .Mul => { ret "TOK_MUL"; }, + .DivAssign => { ret "TOK_ASSIGN_DIV"; }, + .Div => { ret "TOK_DIV"; }, + .ModAssign => { ret "TOK_ASSIGN_MOD"; }, + .Mod => { ret "TOK_MOD"; }, + .RightArrow => { ret "TOK_RIGHT_ARROW"; }, + .LeftArrow => { ret "TOK_LEFT_ARROW"; }, + .ArrowBoth => { ret "TOK_ARROW_BOTH"; }, + .FatArrowBoth => { ret "TOK_FAT_ARROW_BOTH"; }, + .RightFatArrow => { ret "TOK_RIGHT_FAT_ARROW"; }, + .AndAnd => { ret "TOK_AND_AND"; }, + .AndAssign => { ret "TOK_ASSIGN_AND"; }, + .And => { ret "TOK_AND"; }, + .OrOr => { ret "TOK_OR_OR"; }, + .OrAssign => { ret "TOK_ASSIGN_OR"; }, + .Or => { ret "TOK_OR"; }, + .XorAssign => { ret "TOK_ASSIGN_XOR"; }, + .Xor => { ret "TOK_XOR"; }, + .LShAssign => { ret "TOK_ASSIGN_L_SH"; }, + .LSh => { ret "TOK_L_SH"; }, + .Le => { ret "TOK_LE"; }, + .Lt => { ret "TOK_LT"; }, + .RShAssign => { ret "TOK_ASSIGN_R_SH"; }, + .RSh => { ret "TOK_R_SH"; }, + .Ge => { ret "TOK_GE"; }, + .Gt => { ret "TOK_GT"; }, + .Not => { ret "TOK_NOT"; }, + .Neq => { ret "TOK_NEQ"; }, + .BitNot => { ret "TOK_BIT_NOT"; }, + .LBracket => { ret "TOK_L_BRACKET"; }, + .RBracket => { ret "TOK_R_BRACKET"; }, + .LParen => { ret "TOK_L_PAREN"; }, + .RParen => { ret "TOK_R_PAREN"; }, + .LBrace => { ret "TOK_L_BRACE"; }, + .RBrace => { ret "TOK_R_BRACE"; }, + .Semicolon => { ret "TOK_SEMICOLON"; }, + .Comma => { ret "TOK_COMMA"; }, + .Colon => { ret "TOK_COLON"; }, + .ColonAssign => { ret "TOK_COLON_ASSIGN"; }, + .Dot => { ret "TOK_DOT"; }, + .Range => { ret "TOK_RANGE"; }, + .Ellipsis => { ret "TOK_ELLIPSIS"; }, + .Cond => { ret "TOK_COND"; }, + .Sharp => { ret "TOK_SHARP"; }, + .At => { ret "TOK_AT"; }, + .Dollar => { ret "TOK_DOLLAR"; }, + } + ret "?"; +} + +fn kind_name(kind: TokenKind): []u8 { + match kind { + .Keyword(kw) => { ret keyword_name(kw); }, + .Punct(punct) => { ret punct_name(punct); }, + .Ident => { ret "TOK_IDENT"; }, + .Int(value) => { ret "TOK_INT_LITERAL"; }, + .Float => { ret "TOK_FLOAT_LITERAL"; }, + .Char(value) => { ret "TOK_CHAR_LITERAL"; }, + .Str => { ret "TOK_STRING_LITERAL"; }, + .LineComment => { ret "TOK_LINE_COMMENT"; }, + .Endline => { ret "TOK_ENDLINE"; }, + .Eof => { ret "TOK_EOF"; }, + .Unknown(value) => { ret "TOK_UNKNOWN"; }, + } + ret "?"; +} + // ---- 测试辅助 ---- fn view(text: []u8): ByteView { @@ -183,8 +297,112 @@ fn test_trivia() { @assert(!is_trivia(.Eof), "eof not trivia"); } +fn expect_kind_name(kind: TokenKind, expected: []u8) { + @assert(ByteView.eq(ByteView.from_slice(kind_name(kind)), + ByteView.from_slice(expected)), + "token kind name"); +} + +fn test_kind_names() { + expect_kind_name(.Keyword(.Any), "KW_ANY"); + expect_kind_name(.Keyword(.As), "KW_AS"); + expect_kind_name(.Keyword(.Bool), "KW_BOOL"); + expect_kind_name(.Keyword(.Break), "KW_BREAK"); + expect_kind_name(.Keyword(.Catch), "KW_CATCH"); + expect_kind_name(.Keyword(.Comptime), "KW_COMPTIME"); + expect_kind_name(.Keyword(.Const), "KW_CONST"); + expect_kind_name(.Keyword(.Continue), "KW_CONTINUE"); + expect_kind_name(.Keyword(.Defer), "KW_DEFER"); + expect_kind_name(.Keyword(.Else), "KW_ELSE"); + expect_kind_name(.Keyword(.Enum), "KW_ENUM"); + expect_kind_name(.Keyword(.Errdefer), "KW_ERRDEFER"); + expect_kind_name(.Keyword(.False), "KW_FALSE"); + expect_kind_name(.Keyword(.Fn), "KW_FN"); + expect_kind_name(.Keyword(.For), "KW_FOR"); + expect_kind_name(.Keyword(.If), "KW_IF"); + expect_kind_name(.Keyword(.Loop), "KW_LOOP"); + expect_kind_name(.Keyword(.Match), "KW_MATCH"); + expect_kind_name(.Keyword(.Null), "KW_NULL"); + expect_kind_name(.Keyword(.Ret), "KW_RET"); + expect_kind_name(.Keyword(.Shape), "KW_SHAPE"); + expect_kind_name(.Keyword(.Struct), "KW_STRUCT"); + expect_kind_name(.Keyword(.True), "KW_TRUE"); + expect_kind_name(.Keyword(.Try), "KW_TRY"); + expect_kind_name(.Keyword(.Type), "KW_TYPE"); + expect_kind_name(.Keyword(.Undefined), "KW_UNDEFINED"); + expect_kind_name(.Keyword(.Union), "KW_UNION"); + expect_kind_name(.Keyword(.Var), "KW_VAR"); + expect_kind_name(.Keyword(.While), "KW_WHILE"); + expect_kind_name(.Keyword(.Underscore), "KW_UNDERSCORE"); + + expect_kind_name(.Punct(.Eq), "TOK_EQ"); + expect_kind_name(.Punct(.Assign), "TOK_ASSIGN"); + expect_kind_name(.Punct(.AddAssign), "TOK_ASSIGN_ADD"); + expect_kind_name(.Punct(.Add), "TOK_ADD"); + expect_kind_name(.Punct(.SubAssign), "TOK_ASSIGN_SUB"); + expect_kind_name(.Punct(.Sub), "TOK_SUB"); + expect_kind_name(.Punct(.MulAssign), "TOK_ASSIGN_MUL"); + expect_kind_name(.Punct(.Mul), "TOK_MUL"); + expect_kind_name(.Punct(.DivAssign), "TOK_ASSIGN_DIV"); + expect_kind_name(.Punct(.Div), "TOK_DIV"); + expect_kind_name(.Punct(.ModAssign), "TOK_ASSIGN_MOD"); + expect_kind_name(.Punct(.Mod), "TOK_MOD"); + expect_kind_name(.Punct(.RightArrow), "TOK_RIGHT_ARROW"); + expect_kind_name(.Punct(.LeftArrow), "TOK_LEFT_ARROW"); + expect_kind_name(.Punct(.ArrowBoth), "TOK_ARROW_BOTH"); + expect_kind_name(.Punct(.FatArrowBoth), "TOK_FAT_ARROW_BOTH"); + expect_kind_name(.Punct(.RightFatArrow), "TOK_RIGHT_FAT_ARROW"); + expect_kind_name(.Punct(.AndAnd), "TOK_AND_AND"); + expect_kind_name(.Punct(.AndAssign), "TOK_ASSIGN_AND"); + expect_kind_name(.Punct(.And), "TOK_AND"); + expect_kind_name(.Punct(.OrOr), "TOK_OR_OR"); + expect_kind_name(.Punct(.OrAssign), "TOK_ASSIGN_OR"); + expect_kind_name(.Punct(.Or), "TOK_OR"); + expect_kind_name(.Punct(.XorAssign), "TOK_ASSIGN_XOR"); + expect_kind_name(.Punct(.Xor), "TOK_XOR"); + expect_kind_name(.Punct(.LShAssign), "TOK_ASSIGN_L_SH"); + expect_kind_name(.Punct(.LSh), "TOK_L_SH"); + expect_kind_name(.Punct(.Le), "TOK_LE"); + expect_kind_name(.Punct(.Lt), "TOK_LT"); + expect_kind_name(.Punct(.RShAssign), "TOK_ASSIGN_R_SH"); + expect_kind_name(.Punct(.RSh), "TOK_R_SH"); + expect_kind_name(.Punct(.Ge), "TOK_GE"); + expect_kind_name(.Punct(.Gt), "TOK_GT"); + expect_kind_name(.Punct(.Not), "TOK_NOT"); + expect_kind_name(.Punct(.Neq), "TOK_NEQ"); + expect_kind_name(.Punct(.BitNot), "TOK_BIT_NOT"); + expect_kind_name(.Punct(.LBracket), "TOK_L_BRACKET"); + expect_kind_name(.Punct(.RBracket), "TOK_R_BRACKET"); + expect_kind_name(.Punct(.LParen), "TOK_L_PAREN"); + expect_kind_name(.Punct(.RParen), "TOK_R_PAREN"); + expect_kind_name(.Punct(.LBrace), "TOK_L_BRACE"); + expect_kind_name(.Punct(.RBrace), "TOK_R_BRACE"); + expect_kind_name(.Punct(.Semicolon), "TOK_SEMICOLON"); + expect_kind_name(.Punct(.Comma), "TOK_COMMA"); + expect_kind_name(.Punct(.Colon), "TOK_COLON"); + expect_kind_name(.Punct(.ColonAssign), "TOK_COLON_ASSIGN"); + expect_kind_name(.Punct(.Dot), "TOK_DOT"); + expect_kind_name(.Punct(.Range), "TOK_RANGE"); + expect_kind_name(.Punct(.Ellipsis), "TOK_ELLIPSIS"); + expect_kind_name(.Punct(.Cond), "TOK_COND"); + expect_kind_name(.Punct(.Sharp), "TOK_SHARP"); + expect_kind_name(.Punct(.At), "TOK_AT"); + expect_kind_name(.Punct(.Dollar), "TOK_DOLLAR"); + + expect_kind_name(.Ident, "TOK_IDENT"); + expect_kind_name(.Int(int_value(0, .Dec)), "TOK_INT_LITERAL"); + expect_kind_name(.Float, "TOK_FLOAT_LITERAL"); + expect_kind_name(.Char('a'), "TOK_CHAR_LITERAL"); + expect_kind_name(.Str, "TOK_STRING_LITERAL"); + expect_kind_name(.LineComment, "TOK_LINE_COMMENT"); + expect_kind_name(.Endline, "TOK_ENDLINE"); + expect_kind_name(.Eof, "TOK_EOF"); + expect_kind_name(.Unknown(0), "TOK_UNKNOWN"); +} + fn main() { test_keywords(); test_adt_payloads(); test_trivia(); + test_kind_names(); } diff --git a/stage2/utils/byte_view.spl b/stage2/utils/byte_view.spl index f9c1ea3..fa365ad 100644 --- a/stage2/utils/byte_view.spl +++ b/stage2/utils/byte_view.spl @@ -35,6 +35,14 @@ fn from_slice(bytes: []u8): ByteView { ret ByteView { .ptr = bytes.ptr, .len = bytes.len }; } +// 以 NUL 结尾的 C 字符串转为视图;不复制、不拥有。null 规范化为空视图。 +fn from_cstr(text: *u8): ByteView { + if text == null { ret empty(); } + var len: usize = 0; + while text[len] != 0 { len = len + 1; } + ret from_slice(.{ .ptr = text, .len = len }); +} + fn as_slice(self: ByteView): []u8 { ret .{ .ptr = self.ptr, .len = self.len }; } @@ -213,6 +221,14 @@ fn test_construction_and_access() { var bytes = as_slice(text); @assert(bytes.ptr == text.ptr && bytes.len == text.len, "slice conversion borrows"); + + var cstr_data: [4]u8 = .[104, 105, 0, 88]; + var cstr = from_cstr(&cstr_data[0]); + @assert(cstr.len == 2 && cstr.ptr[0] == 104 && cstr.ptr[1] == 105, + "from_cstr stops at NUL"); + @assert(from_cstr(&cstr_data[2]).len == 0, "from_cstr at NUL is empty"); + var none = from_cstr(null); + @assert(none.ptr == null && none.len == 0, "from_cstr null normalized"); } fn test_compare_and_slice() { diff --git a/stage2/utils/fmt.spl b/stage2/utils/fmt.spl index 33a5250..aea9b00 100644 --- a/stage2/utils/fmt.spl +++ b/stage2/utils/fmt.spl @@ -5,6 +5,8 @@ type .{ .Error, .Status, .make } = @import("error.spl"); type .{ .ByteBuffer } = @import("byte_buffer.spl"); +type .{ .ByteView } = @import("byte_view.spl"); +type .{ .Value, .int, .uint, .char, .str, .addr } = @import("value.spl"); type .{ .BigInt, .Bits } = @import("mp.spl"); // 注意:radix 边界直接用字面量。stage1 刻意不做常量求值/折叠, @@ -69,6 +71,158 @@ fn append_bool(out: *ByteBuffer, value: bool): Status { ret ByteBuffer.append(out, "false"); } +// ---- 显式装箱值格式化 ---- + +// Int/Uint 落到整数;signed 仅对 Int 的十进制生效,其余按无符号位模式输出。 +fn append_value_int(out: *ByteBuffer, value: Value, radix: u8, uppercase: bool, + signed: bool): Status { + match value { + .Int(v) => { + if signed && radix == 10 { ret append_i64(out, v, radix, uppercase); } + ret append_u64(out, v as u64, radix, uppercase); + }, + .Uint(v) => { ret append_u64(out, v, radix, uppercase); }, + _ => { ret .Err(make(.InvalidFormat)); }, + } +} + +fn append_value_char(out: *ByteBuffer, value: Value): Status { + match value { + .Char(c) => { ret ByteBuffer.append_byte(out, c); }, + _ => { ret .Err(make(.InvalidFormat)); }, + } +} + +fn append_value_str(out: *ByteBuffer, value: Value): Status { + match value { + .Str(view) => { ret ByteBuffer.append(out, ByteView.as_slice(view)); }, + _ => { ret .Err(make(.InvalidFormat)); }, + } +} + +fn append_value_ptr(out: *ByteBuffer, value: Value): Status { + match value { + .Ptr(p) => { + match ByteBuffer.append(out, "0x") { + .Ok => {}, + .Err(error) => { ret .Err(error); }, + } + ret append_u64(out, p as usize, 16, false); + }, + _ => { ret .Err(make(.InvalidFormat)); }, + } +} + +// {} 自动格式化:按 Value 变体选择默认表示。 +fn append_value_auto(out: *ByteBuffer, value: Value): Status { + match value { + .Int(v) => { ret append_i64(out, v, 10, false); }, + .Uint(v) => { ret append_u64(out, v, 10, false); }, + .Bool(v) => { ret append_bool(out, v); }, + .Char(c) => { ret ByteBuffer.append_byte(out, c); }, + .Str(view) => { ret ByteBuffer.append(out, ByteView.as_slice(view)); }, + .Ptr(p) => { + match ByteBuffer.append(out, "0x") { + .Ok => {}, + .Err(error) => { ret .Err(error); }, + } + ret append_u64(out, p as usize, 16, false); + }, + } + ret .Ok; +} + +// {} 风格:{} 自动;{:d} {:i} {:u} {:x} {:X} {:o} {:b} {:c} {:s} {:p} 指定。 +// {{ 与 }} 输出字面量括号。参数按序从 args 消费;数量不足、类型不符、未知转换 +// 或括号不配返回 Err(.InvalidFormat)。失败不产生部分输出:先在临时缓冲完成。 +fn format(out: *ByteBuffer, fmt: []u8, args: []Value): Status { + var temp = ByteBuffer.new(); + defer { ByteBuffer.drop(&temp); } + + var argi: usize = 0; + var i: usize = 0; + while i < fmt.len { + var c = fmt.ptr[i]; + + if c == '{' { + if i + 1 < fmt.len && fmt.ptr[i + 1] == '{' { + match ByteBuffer.append_byte(&temp, '{') { + .Ok => {}, + .Err(error) => { ret .Err(error); }, + } + i = i + 2; + continue; + } + + var j = i + 1; + if j < fmt.len && fmt.ptr[j] == ':' { j = j + 1; } + var has_spec = false; + var spec: u8 = 0; + if j < fmt.len && fmt.ptr[j] != '}' { + has_spec = true; + spec = fmt.ptr[j]; + j = j + 1; + } + if j >= fmt.len || fmt.ptr[j] != '}' { ret .Err(make(.InvalidFormat)); } + i = j + 1; + + if argi >= args.len { ret .Err(make(.InvalidFormat)); } + var value = args.ptr[argi]; + argi = argi + 1; + + var step: Status = .Ok; + if !has_spec { + step = append_value_auto(&temp, value); + } else if spec == 'd' || spec == 'i' { + step = append_value_int(&temp, value, 10, false, true); + } else if spec == 'u' { + step = append_value_int(&temp, value, 10, false, false); + } else if spec == 'x' { + step = append_value_int(&temp, value, 16, false, false); + } else if spec == 'X' { + step = append_value_int(&temp, value, 16, true, false); + } else if spec == 'o' { + step = append_value_int(&temp, value, 8, false, false); + } else if spec == 'b' { + step = append_value_int(&temp, value, 2, false, false); + } else if spec == 'c' { + step = append_value_char(&temp, value); + } else if spec == 's' { + step = append_value_str(&temp, value); + } else if spec == 'p' { + step = append_value_ptr(&temp, value); + } else { + ret .Err(make(.InvalidFormat)); + } + match step { + .Ok => {}, + .Err(error) => { ret .Err(error); }, + } + continue; + } + + if c == '}' { + if i + 1 < fmt.len && fmt.ptr[i + 1] == '}' { + match ByteBuffer.append_byte(&temp, '}') { + .Ok => {}, + .Err(error) => { ret .Err(error); }, + } + i = i + 2; + continue; + } + ret .Err(make(.InvalidFormat)); + } + + match ByteBuffer.append_byte(&temp, c) { + .Ok => {}, + .Err(error) => { ret .Err(error); }, + } + i = i + 1; + } + + ret ByteBuffer.append(out, ByteBuffer.as_slice(temp)); +} + // ---- 测试辅助 ---- fn expect_ok(result: Status) { @@ -262,10 +416,96 @@ fn test_composition_and_growth() { @assert(s.ptr[1] == ',', "0 then comma"); } +fn expect_format(fmt: []u8, args: []Value, expected: []u8) { + var b = ByteBuffer.new(); + defer { ByteBuffer.drop(&b); } + expect_ok(format(&b, fmt, args)); + expect_text(&b, expected); +} + +fn expect_invalid_format(fmt: []u8, args: []Value) { + var b = ByteBuffer.new(); + defer { ByteBuffer.drop(&b); } + match format(&b, fmt, args) { + .Ok => { @assert(false, "expected invalid format"); }, + .Err(error) => { + match error.kind { + .InvalidFormat => {}, + _ => { @assert(false, "expected InvalidFormat kind"); }, + } + }, + } +} + +fn test_format_specs() { + var one: [1]Value = .[int(-5)]; + expect_format("{}", one[0..1], "-5"); + expect_format("{d}", one[0..1], "-5"); + expect_format("{i}", one[0..1], "-5"); + + var u: [1]Value = .[uint(7)]; + expect_format("{}", u[0..1], "7"); + expect_format("{u}", u[0..1], "7"); + expect_format("{x}", u[0..1], "7"); + expect_format("{o}", u[0..1], "7"); + expect_format("{b}", u[0..1], "111"); + + var hex: [1]Value = .[uint(255)]; + expect_format("{x}", hex[0..1], "ff"); + expect_format("{X}", hex[0..1], "FF"); + + var oct: [1]Value = .[uint(8)]; + expect_format("{o}", oct[0..1], "10"); + + var bits: [1]Value = .[uint(5)]; + expect_format("{b}", bits[0..1], "101"); + + var ch: [1]Value = .[char('A')]; + expect_format("{}", ch[0..1], "A"); + expect_format("{c}", ch[0..1], "A"); + + var text: [1]Value = .[str("hi")]; + expect_format("{}", text[0..1], "hi"); + expect_format("{s}", text[0..1], "hi"); + + var nul: [1]Value = .[addr(null)]; + expect_format("{p}", nul[0..1], "0x0"); + + var none: [1]Value = .[int(0)]; + expect_format("100%", none[0..0], "100%"); + expect_format("{{}}", none[0..0], "{}"); + expect_format("plain", none[0..0], "plain"); +} + +fn test_format_mixed_and_failure() { + var args: [3]Value = .[int(42), str("x"), char('!')]; + expect_format("n={} s={} c={}", args[0..3], "n=42 s=x c=!"); + expect_format("n={} s={s} c={c}", args[0..3], "n=42 s=x c=!"); + + // 失败不改动输出,且不产生部分结果:前两个转换成功、第三个参数不足。 + var b = ByteBuffer.new(); + defer { ByteBuffer.drop(&b); } + expect_ok(ByteBuffer.append(&b, "prefix")); + var two: [2]Value = .[int(1), int(2)]; + expect_invalid_format("a={} b={} c={}", two[0..2]); + expect_text(&b, "prefix"); + + var only_int: [1]Value = .[int(1)]; + expect_invalid_format("{} {}", only_int[0..1]); + expect_invalid_format("{}", only_int[0..0]); + expect_invalid_format("trailing {", only_int[0..1]); + expect_invalid_format("dangling }", only_int[0..1]); + expect_invalid_format("{q}", only_int[0..1]); + expect_invalid_format("{s}", only_int[0..1]); + expect_invalid_format("{c}", only_int[0..1]); +} + fn main() { test_unsigned(); test_signed(); test_usize_bool_and_errors(); test_mpa_integer_formatting(); test_composition_and_growth(); + test_format_specs(); + test_format_mixed_and_failure(); } diff --git a/stage2/utils/log.spl b/stage2/utils/log.spl new file mode 100644 index 0000000..5b418f0 --- /dev/null +++ b/stage2/utils/log.spl @@ -0,0 +1,205 @@ +// Expect retcode = 0 + +// 编译器自身日志(区别于 diag:diag 负责带源码位置的诊断)。 +// 输出到 stderr,格式稳定:[LEVEL] name: message\n。 +// 消息为借用;Logger 不拥有 name。fatal 打印后终止进程(退出码 1)。 +// write 返回 Status;OOM 归类为 .Overflow(无独立内存耗尽错误码)。 + +type .{ .Status, .make } = @import("error.spl"); +type .{ .ByteView } = @import("byte_view.spl"); +type .{ .ByteBuffer } = @import("byte_buffer.spl"); +type .{ .format } = @import("fmt.spl"); +type .{ .Value, .int, .str } = @import("value.spl"); +type .{ .eprint } = @import("io.spl"); +type Exit = @import("../sys/exit.spl"); + +type Level = enum { Debug, Info, Warn, Error, Fatal } + +fn level_name(level: Level): []u8 { + match level { + .Debug => { ret "DEBUG"; }, + .Info => { ret "INFO "; }, + .Warn => { ret "WARN "; }, + .Error => { ret "ERROR"; }, + .Fatal => { ret "FATAL"; }, + } + ret "NOTSET"; +} + +type Logger = @self; + +name: ByteView, +min_level: Level, + +fn new(name: ByteView): Logger { + ret Logger { .name = name, .min_level = .Debug }; +} + +fn set_level(self: *Logger, level: Level) { + self.min_level = level; +} + +fn enabled(self: Logger, level: Level): bool { + ret @enum2int(level) >= @enum2int(self.min_level); +} + +// 追加成功返回 true;失败(仅 OOM)返回 false。 +fn push(line: *ByteBuffer, text: []u8): bool { + match ByteBuffer.append(line, text) { + .Ok => { ret true; }, + .Err(error) => { ret false; }, + } + ret false; +} + +// 低于 min_level 的日志不写出,直接成功。 +fn write(self: *Logger, level: Level, message: []u8): Status { + if !enabled(self.*, level) { ret .Ok; } + var line = ByteBuffer.new(); + defer { ByteBuffer.drop(&line); } + if !push(&line, "[") { ret .Err(make(.Overflow)); } + if !push(&line, level_name(level)) { ret .Err(make(.Overflow)); } + if !push(&line, "] ") { ret .Err(make(.Overflow)); } + if !push(&line, ByteView.as_slice(self.name)) { ret .Err(make(.Overflow)); } + if !push(&line, ": ") { ret .Err(make(.Overflow)); } + if !push(&line, message) { ret .Err(make(.Overflow)); } + if !push(&line, "\n") { ret .Err(make(.Overflow)); } + ret eprint(ByteBuffer.as_slice(line)); +} + +fn debug(self: *Logger, message: []u8): Status { + ret write(self, .Debug, message); +} + +fn info(self: *Logger, message: []u8): Status { + ret write(self, .Info, message); +} + +fn warn(self: *Logger, message: []u8): Status { + ret write(self, .Warn, message); +} + +fn error(self: *Logger, message: []u8): Status { + ret write(self, .Error, message); +} + +// 尽力写出后终止;写失败无法挽回,直接退出。 +fn fatal(self: *Logger, message: []u8) { + write(self, .Fatal, message); + Exit.fail(); +} + +// ---- 格式化入口(显式装箱,见 value.spl / fmt.format)---- + +// 先格式化到临时缓冲;格式错误在写出前返回 Err(.InvalidFormat)。 +fn logf(self: *Logger, level: Level, fmt: []u8, args: []Value): Status { + if !enabled(self.*, level) { ret .Ok; } + var message = ByteBuffer.new(); + defer { ByteBuffer.drop(&message); } + match format(&message, fmt, args) { + .Ok => {}, + .Err(error) => { ret .Err(error); }, + } + ret write(self, level, ByteBuffer.as_slice(message)); +} + +fn debugf(self: *Logger, fmt: []u8, args: []Value): Status { + ret logf(self, .Debug, fmt, args); +} + +fn infof(self: *Logger, fmt: []u8, args: []Value): Status { + ret logf(self, .Info, fmt, args); +} + +fn warnf(self: *Logger, fmt: []u8, args: []Value): Status { + ret logf(self, .Warn, fmt, args); +} + +fn errorf(self: *Logger, fmt: []u8, args: []Value): Status { + ret logf(self, .Error, fmt, args); +} + +// 格式化后终止;格式化失败也要退出(避免吞掉致命错误)。 +fn fatalf(self: *Logger, fmt: []u8, args: []Value) { + logf(self, .Fatal, fmt, args); + Exit.fail(); +} + +// ---- 测试 ---- + +fn expect_name(level: Level, expected: []u8) { + @assert(ByteView.eq(ByteView.from_slice(level_name(level)), + ByteView.from_slice(expected)), + "level name"); +} + +fn expect_ok(result: Status) { + match result { + .Ok => {}, + .Err(error) => { @assert(false, "unexpected log error"); }, + } +} + +fn test_level_names() { + expect_name(.Debug, "DEBUG"); + expect_name(.Info, "INFO "); + expect_name(.Warn, "WARN "); + expect_name(.Error, "ERROR"); + expect_name(.Fatal, "FATAL"); + + var debug: Level = .Debug; + var info: Level = .Info; + var warn: Level = .Warn; + var err: Level = .Error; + var fatal: Level = .Fatal; + @assert(@enum2int(fatal) > @enum2int(err), "fatal above error"); + @assert(@enum2int(err) > @enum2int(warn), "error above warn"); + @assert(@enum2int(warn) > @enum2int(info), "warn above info"); + @assert(@enum2int(info) > @enum2int(debug), "info above debug"); +} + +fn test_filter_and_write() { + var log = new(ByteView.from_slice("splc1")); + @assert(enabled(log, .Debug), "default shows debug"); + @assert(enabled(log, .Fatal), "default shows fatal"); + + set_level(&log, .Warn); + @assert(!enabled(log, .Debug), "filtered debug"); + @assert(!enabled(log, .Info), "filtered info"); + @assert(enabled(log, .Warn), "warn passes"); + @assert(enabled(log, .Error), "error passes"); + + // 被过滤的写入不产生输出,且返回成功。 + expect_ok(debug(&log, "hidden")); + expect_ok(write(&log, .Info, "hidden too")); + expect_ok(error(&log, "visible")); +} + +fn test_logf() { + var log = new(ByteView.from_slice("splc1")); + set_level(&log, .Error); + + var args: [2]Value = .[int(3), str("x")]; + + // 被过滤的级别不写出,直接成功。 + expect_ok(infof(&log, "n={} s={}", args[0..2])); + // 通过级别正常写出。 + expect_ok(errorf(&log, "n={} s={}", args[0..2])); + + // 格式化失败向上传播,且不产出消息。 + match errorf(&log, "{} {}", args[0..1]) { + .Ok => { @assert(false, "expected invalid format"); }, + .Err(error) => { + match error.kind { + .InvalidFormat => {}, + _ => { @assert(false, "expected InvalidFormat"); }, + } + }, + } +} + +fn main() { + test_level_names(); + test_filter_and_write(); + test_logf(); +} diff --git a/stage2/utils/value.spl b/stage2/utils/value.spl new file mode 100644 index 0000000..7f781fa --- /dev/null +++ b/stage2/utils/value.spl @@ -0,0 +1,100 @@ +// Expect retcode = 0 + +// 显式装箱值:运行期格式化用的最小 ADT。调用方显式构造(如 Value.int(x)), +// 库内用 match 分派。不做隐式装箱、不带运行期类型系统。 +// Str 借用 ByteView,不拥有存储;Ptr 是类型擦除指针。 + +type .{ .ByteView } = @import("byte_view.spl"); + +type Value = enum { + Int: i64, + Uint: u64, + Bool: bool, + Char: u8, + Str: ByteView, + Ptr: *any, +} + +fn int(value: i64): Value { + ret .Int(value); +} + +fn uint(value: u64): Value { + ret .Uint(value); +} + +fn boolean(value: bool): Value { + ret .Bool(value); +} + +fn char(value: u8): Value { + ret .Char(value); +} + +fn str(bytes: []u8): Value { + ret .Str(ByteView.from_slice(bytes)); +} + +fn str_view(view: ByteView): Value { + ret .Str(view); +} + +fn addr(value: *any): Value { + ret .Ptr(value); +} + +// ---- 测试 ---- + +fn test_int_and_uint() { + match int(-5) { + .Int(v) => { @assert(v == -5, "int payload"); }, + _ => { @assert(false, "expected int"); }, + } + match uint(5) { + .Uint(v) => { @assert(v == 5, "uint payload"); }, + _ => { @assert(false, "expected uint"); }, + } +} + +fn test_scalars() { + match boolean(true) { + .Bool(v) => { @assert(v, "bool payload"); }, + _ => { @assert(false, "expected bool"); }, + } + match char('A') { + .Char(v) => { @assert(v == 'A', "char payload"); }, + _ => { @assert(false, "expected char"); }, + } +} + +fn test_borrowed_str() { + var text: [3]u8 = .[104, 105, 0]; + var s = str(text[0..2]); + match s { + .Str(view) => { + @assert(view.len == 2 && view.ptr[0] == 104 && view.ptr[1] == 105, + "str payload borrows"); + }, + _ => { @assert(false, "expected str"); }, + } + @assert(text[0] == 104, "source untouched"); +} + +fn test_ptr() { + var x: i32 = 7; + match addr(&x as *any) { + .Ptr(p) => { @assert((p as *i32).* == 7, "ptr payload"); }, + _ => { @assert(false, "expected ptr"); }, + } + match addr(null) { + .Ptr(p) => { @assert(p == null, "null ptr payload"); }, + _ => { @assert(false, "expected ptr"); }, + } +} + +fn main() { + test_int_and_uint(); + test_scalars(); + test_borrowed_str(); + test_ptr(); +}