Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5a7a811b3 | ||
|
|
bb9d0a9f18 | ||
|
|
c30143609a | ||
|
|
b3e38ba7e4 |
No files matched your search
+3
-1
@@ -214,7 +214,9 @@ TypePostfixExpr = {
|
||||
}
|
||||
|
||||
TypePrimary = {
|
||||
IDENTIFIER
|
||||
AT ~ IDENTIFIER // @self 等自引用/内置类型
|
||||
| ComptimeCallExpr // @import("...") 等导入即类型
|
||||
| IDENTIFIER
|
||||
| KEYWORD_i8
|
||||
| KEYWORD_u8
|
||||
| KEYWORD_i16
|
||||
|
||||
@@ -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 <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;
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -489,6 +618,7 @@ int main(int argc, char **argv) {
|
||||
|
||||
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))
|
||||
|
||||
+8
-2
@@ -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;
|
||||
|
||||
+115
-2
@@ -691,6 +691,69 @@ static spl_ir_node_ref_t emit_cast(spl_ast2ir_t *a, spl_ast_node_t *n) {
|
||||
return ast2ir_emit_cast_builtin(a, n, in, ft, tt, v);
|
||||
}
|
||||
|
||||
/* 字段访问的基对象是 rvalue(如函数调用结果)时的回退路径:
|
||||
* 先求值整个聚合值,再用 agg.extract 取出字段值(无需左值/临时槽)。 */
|
||||
static spl_ir_node_ref_t ast2ir_field_from_value(spl_ast2ir_t *a, spl_ast_node_t *n) {
|
||||
spl_ast_node_t *obj = spl_ast_node(a->ast, n->field.obj);
|
||||
if (!obj)
|
||||
return 0;
|
||||
spl_lang_type_id_t obj_lang = node_lang(a, obj);
|
||||
spl_lang_type_node_t *obj_ln = spl_lang_type_node(a->sema->type, obj_lang);
|
||||
if (!obj_ln)
|
||||
return 0;
|
||||
|
||||
spl_type_id_t agg_ir = to_ir_type(a, obj_lang);
|
||||
if (!agg_ir)
|
||||
return 0;
|
||||
usize idx = 0;
|
||||
if (!spl_type_agg_member_idx(&a->sema->type->ir, agg_ir, n->field.field, &idx))
|
||||
return 0;
|
||||
|
||||
usize field_idx = idx;
|
||||
if (obj_ln->kind == SPL_LANG_TYPE_ENUM)
|
||||
field_idx = idx + 1; /* enum: field 0 是 tag */
|
||||
|
||||
spl_type_id_t field_ir = 0;
|
||||
spl_type_node_t *agg_tn = spl_type_node(&a->sema->type->ir, agg_ir);
|
||||
if (agg_tn && field_idx < vec_size(agg_tn->agg_type.variants))
|
||||
field_ir = vec_at(agg_tn->agg_type.variants, field_idx).type_id;
|
||||
|
||||
spl_ir_node_ref_t val = emit_expr(a, n->field.obj);
|
||||
if (!val)
|
||||
return 0;
|
||||
return spl_ir_builder_agg_extract(a->ir, agg_ir, field_ir, field_idx, val);
|
||||
}
|
||||
|
||||
/* 限定枚举变体 Type.Variant:产出枚举常量(tag=变体下标;无载荷变体不带 payload)。 */
|
||||
static spl_ir_node_ref_t ast2ir_enum_variant(spl_ast2ir_t *a, spl_ast_node_t *n) {
|
||||
spl_type_id_t t = node_ir(a, n);
|
||||
if (!t)
|
||||
return 0;
|
||||
spl_type_node_t *tn = spl_type_node(&a->sema->type->ir, t);
|
||||
if (!tn)
|
||||
return 0;
|
||||
|
||||
usize vidx = 0;
|
||||
if (!spl_type_agg_member_idx(&a->sema->type->ir, t, n->field.field, &vidx))
|
||||
return 0;
|
||||
|
||||
spl_type_id_t tag_ir = tn->agg_type.tag_type;
|
||||
spl_ir_node_ref_t eptr = spl_ir_builder_mem_alloca(a->ir, t, const_usize(a, 1));
|
||||
spl_ir_node_ref_t tptr = spl_ir_builder_mem_field(a->ir, t, eptr, SPL_ENUM_TAG_FIELD_IDX);
|
||||
spl_ir_node_ref_t tag_val = spl_ir_builder_type_const_int(a->ir, tag_ir, vidx);
|
||||
spl_ir_builder_mem_store(a->ir, tag_ir, tptr, tag_val);
|
||||
|
||||
if (vidx < vec_size(tn->agg_type.variants)) {
|
||||
spl_type_id_t payload_ir = vec_at(tn->agg_type.variants, vidx).type_id;
|
||||
if (payload_ir) {
|
||||
spl_ir_node_ref_t pptr = spl_ir_builder_mem_field(a->ir, t, eptr, vidx + 1);
|
||||
spl_ir_node_ref_t zero = emit_zero_value(a, payload_ir);
|
||||
spl_ir_builder_mem_store(a->ir, payload_ir, pptr, zero);
|
||||
}
|
||||
}
|
||||
return eptr;
|
||||
}
|
||||
|
||||
static spl_ir_node_ref_t emit_expr(spl_ast2ir_t *a, spl_ast_node_ref_t ref) {
|
||||
spl_ir_node_ref_t val = emit_expr_raw(a, ref);
|
||||
if (!val)
|
||||
@@ -1022,10 +1085,20 @@ static spl_ir_node_ref_t emit_expr_raw(spl_ast2ir_t *a, spl_ast_node_ref_t ref)
|
||||
}
|
||||
}
|
||||
|
||||
/* 限定枚举变体 Type.Variant:基对象是聚合类型名且为 enum。 */
|
||||
spl_ast_node_t *objn = spl_ast_node(a->ast, n->field.obj);
|
||||
if (objn && objn->kind == SPL_AST_EXPR_IDENT) {
|
||||
spl_def_node_t *od = spl_def_node(a->sema->type, objn->resolved_def_id);
|
||||
spl_lang_type_id_t et = node_lang(a, objn);
|
||||
spl_lang_type_node_t *etn = spl_lang_type_node(a->sema->type, et);
|
||||
if (od && od->kind == SPL_DEF_AGG && etn && etn->kind == SPL_LANG_TYPE_ENUM)
|
||||
return ast2ir_enum_variant(a, n);
|
||||
}
|
||||
|
||||
spl_ir_node_ref_t ptr = emit_lvalue(a, ref);
|
||||
if (!ptr)
|
||||
return 0;
|
||||
if (ptr)
|
||||
return spl_ir_builder_mem_load(a->ir, node_ir(a, n), ptr);
|
||||
return ast2ir_field_from_value(a, n);
|
||||
}
|
||||
case SPL_AST_EXPR_INDEX: {
|
||||
spl_ir_node_ref_t ptr = emit_lvalue(a, ref);
|
||||
@@ -1065,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) {
|
||||
|
||||
+1
-2
@@ -411,7 +411,7 @@ spl_tok_vec_t spl_lex(const char *source, const char *fname) {
|
||||
}
|
||||
|
||||
/* Three-char operators first */
|
||||
TRY_OP3('<', '-', '>', TOK_ARROW_BOTH, TOK_RIGHT_ARROW, TOK_LT)
|
||||
TRY_OP3('<', '-', '>', TOK_ARROW_BOTH, TOK_LEFT_ARROW, TOK_LT)
|
||||
TRY_OP3('<', '=', '>', TOK_FAT_ARROW_BOTH, TOK_LE, TOK_LT)
|
||||
TRY_OP3('<', '<', '=', TOK_ASSIGN_L_SH, TOK_L_SH, TOK_LT)
|
||||
TRY_OP3('>', '>', '=', TOK_ASSIGN_R_SH, TOK_R_SH, TOK_GT)
|
||||
@@ -433,7 +433,6 @@ spl_tok_vec_t spl_lex(const char *source, const char *fname) {
|
||||
TRY_OP2('&', '=', TOK_ASSIGN_AND, TOK_AND)
|
||||
TRY_OP2('|', '=', TOK_ASSIGN_OR, TOK_OR)
|
||||
TRY_OP2('^', '=', TOK_ASSIGN_XOR, TOK_XOR)
|
||||
TRY_OP2('<', '-', TOK_LEFT_ARROW, TOK_LT)
|
||||
TRY_OP2('-', '>', TOK_RIGHT_ARROW, TOK_SUB)
|
||||
TRY_OP2(':', '=', TOK_COLON_ASSIGN, TOK_COLON)
|
||||
|
||||
|
||||
@@ -849,6 +849,7 @@ static spl_ast_node_ref_t parse_container_declaration(spl_parser_t *p) {
|
||||
}
|
||||
default:
|
||||
PARSE_ERROR(p, SPL_DIAG_KIND_PARSE_UNRECOGNIZED, t, "unrecognized declaration");
|
||||
advance(p); /* 消费该 token,避免调用方在错误恢复时原地死循环 */
|
||||
vec_free(tag_list);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2024,6 +2033,20 @@ static spl_lang_type_id_t sema_infer(spl_sema_t *sema, spl_ast_node_ref_t ref) {
|
||||
if (!obj)
|
||||
return 0;
|
||||
|
||||
/* 限定枚举变体 Type.Variant:基对象是聚合类型名且类型为 enum。 */
|
||||
spl_ast_node_t *objn = spl_ast_node(sema->ast, n->field.obj);
|
||||
if (objn && objn->kind == SPL_AST_EXPR_IDENT) {
|
||||
spl_def_node_t *od = spl_def_node(sema->type, objn->resolved_def_id);
|
||||
if (od && od->kind == SPL_DEF_AGG && obj->kind == SPL_LANG_TYPE_ENUM) {
|
||||
spl_type_id_t eir = spl_lang_type_get_ir_type(sema->type, obj_tid);
|
||||
usize vidx = 0;
|
||||
if (eir && spl_type_agg_member_idx(&sema->type->ir, eir, n->field.field, &vidx)) {
|
||||
n->resolved_lang_tid = obj_tid;
|
||||
return obj_tid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spl_intrin_t in = spl_intrin_lookup_field(obj->kind, n->field.field);
|
||||
if (in != SPL_INTRIN_NONE) {
|
||||
spl_lang_type_id_t t = sema_intrin_field(sema, n, in);
|
||||
|
||||
@@ -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,
|
||||
|
||||
+642
@@ -0,0 +1,642 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// AST:用 ADT(enum 带载荷)表达节点,节点引用是 arena 序号 usize。
|
||||
// stage1 的 C 结构只是「手写 union 模拟 ADT」;这里直接用 SPL 的 enum 载荷,
|
||||
// 不照抄 union 布局,只要求最终 dump / 语义与 stage1 一致。
|
||||
// 索引 0 为 NONE 哨兵(与 stage1 一致)。
|
||||
|
||||
type .{ .Error } = @import("utils/error.spl");
|
||||
type .{ .Vec } = @import("utils/vec.spl");
|
||||
type .{ .ByteView } = @import("utils/byte_view.spl");
|
||||
type .{ .ByteBuffer } = @import("utils/byte_buffer.spl");
|
||||
type .{ .append_usize } = @import("utils/fmt.spl");
|
||||
type .{ .Punct } = @import("token.spl");
|
||||
type BigInt = @import("utils/mp.spl");
|
||||
type .{ .Span, .span_none, .span_make, .span_is_none } = @import("./diag.spl");
|
||||
type .{ .Sources, .Source, .set_at } = @import("utils/source.spl");
|
||||
|
||||
// 算子不另立 enum:AST 直接存词法 Punct。一元/二元由节点 kind 区分
|
||||
// (一元:Sub=负 / Not=! / BitNot=~ / And=& / Mul=.*;二元即对应 Punct)。
|
||||
|
||||
// ---- 节点体(ADT;variant 名 = stage1 的 kind 名;载荷用内联匿名 struct)----
|
||||
|
||||
type Body = enum {
|
||||
None,
|
||||
Root: struct { members: usize },
|
||||
ContainerMembers: struct { external_tags: Vec, declarations: Vec },
|
||||
ExternalTagItem: struct { is_at: bool, is_external: bool, name: ByteView, args: Vec },
|
||||
TagItem: struct { is_at: bool, is_external: bool, name: ByteView, args: Vec },
|
||||
ComptimeDecl: struct { body: usize },
|
||||
TopLevelBlock: struct { tag_list: Vec, block: usize },
|
||||
FnDecl: struct {
|
||||
tag_list: Vec,
|
||||
is_comptime: bool,
|
||||
name: ByteView,
|
||||
params: Vec,
|
||||
ret_type: usize,
|
||||
body: usize,
|
||||
},
|
||||
VarDecl: struct { attr_list: Vec, pattern: usize, type_expr: usize, init_expr: usize },
|
||||
ConstDecl: struct { attr_list: Vec, pattern: usize, type_expr: usize, init_expr: usize },
|
||||
TypeDecl: struct { attr_list: Vec, pattern: usize, type_expr: usize },
|
||||
MemberDecl: struct {
|
||||
attr_list: Vec,
|
||||
name: ByteView,
|
||||
type_expr: usize,
|
||||
init_expr: usize,
|
||||
},
|
||||
ParamDecl: struct { attr_list: Vec, name: ByteView, type_expr: usize },
|
||||
StatementRet: struct { expr: usize },
|
||||
StatementBreak: struct { label: usize, expr: usize },
|
||||
StatementContinue: struct { label: usize },
|
||||
StatementDefer: struct { body: usize },
|
||||
StatementComptime: struct { body: usize },
|
||||
StatementDecl: struct { decl: usize },
|
||||
StatementExpr: struct { expr: usize },
|
||||
AggregateMember: struct {
|
||||
attr_list: Vec,
|
||||
name: ByteView,
|
||||
type_expr: usize,
|
||||
init_expr: usize,
|
||||
},
|
||||
StructInitItem: struct { name: ByteView, expr: usize },
|
||||
MatchArm: struct { pattern: usize, body: usize },
|
||||
Label: struct { name: ByteView },
|
||||
ExprIntLit: struct { value: BigInt }, // 任意宽度(mp),解析后由 Ast 拥有
|
||||
ExprFloatLit: struct { text: ByteView },
|
||||
ExprCharLit: struct { value: u8 },
|
||||
ExprStringLit: struct { data: ByteView },
|
||||
ExprBoolLit: struct { value: bool },
|
||||
ExprNullLit,
|
||||
ExprUndefinedLit,
|
||||
ExprIdent: struct { name: ByteView },
|
||||
ExprUnary: struct { op: Punct, operand: usize },
|
||||
ExprBinary: struct { op: Punct, left: usize, right: usize },
|
||||
ExprCall: struct { callee: usize, args: Vec },
|
||||
ExprField: struct { obj: usize, name: ByteView },
|
||||
ExprIndex: struct { obj: usize, index: usize },
|
||||
ExprSlice: struct { obj: usize, begin: usize, end: usize },
|
||||
ExprCast: struct { expr: usize, type_expr: usize },
|
||||
ExprTuple: struct { items: Vec },
|
||||
ExprArrayLit: struct {
|
||||
has_len: bool,
|
||||
len: usize,
|
||||
is_underscore_len: bool,
|
||||
elem_type: usize,
|
||||
elems: Vec,
|
||||
},
|
||||
ExprAggregateLit: struct { name: ByteView, fields: Vec },
|
||||
ExprComptimeCall: struct { name: ByteView, args: Vec },
|
||||
ExprBlock: struct { stmts: Vec, tail_expr: usize },
|
||||
ExprIf: struct { cond: usize, then_block: usize, else_branch: usize },
|
||||
ExprIfvar: struct { var_decl: usize, then_block: usize, else_branch: usize },
|
||||
ExprWhile: struct { label: usize, cond: usize, body: usize, else_branch: usize },
|
||||
ExprLoop: struct { label: usize, body: usize },
|
||||
ExprFor: struct {
|
||||
label: usize,
|
||||
iter_exprs: Vec,
|
||||
pattern: usize,
|
||||
body: usize,
|
||||
else_branch: usize,
|
||||
},
|
||||
ExprMatch: struct { matched_expr: usize, arms: Vec },
|
||||
ExprFnType: struct { is_comptime: bool, params: Vec, ret_type: usize },
|
||||
TypePath: struct { name: ByteView, generic_args: Vec },
|
||||
TypePointer: struct { pointee: usize },
|
||||
TypeArray: struct {
|
||||
has_len: bool,
|
||||
len: isize,
|
||||
is_underscore_len: bool,
|
||||
elem_type: usize,
|
||||
},
|
||||
TypeSlice: struct { elem_type: usize },
|
||||
TypeFn: struct { is_comptime: bool, params: Vec, ret_type: usize },
|
||||
TypeParen: struct { types: Vec },
|
||||
TypeAggregate: struct { kind_name: ByteView, members: Vec },
|
||||
TypeComptimeCall: struct { name: ByteView, args: Vec },
|
||||
PatternIdent: struct { name: ByteView },
|
||||
PatternUnderscore,
|
||||
PatternDot: struct { name: ByteView, sub_pattern: usize },
|
||||
PatternTuple: struct { items: Vec },
|
||||
PatternList: struct { items: Vec },
|
||||
PatternAggregate: struct { items: Vec },
|
||||
PatternField: struct { name: ByteView, pattern: usize },
|
||||
}
|
||||
|
||||
fn kind_name(body: Body): []u8 {
|
||||
match body {
|
||||
.None => { ret "none"; },
|
||||
.Root(_) => { ret "root"; },
|
||||
.ContainerMembers(_) => { ret "container_members"; },
|
||||
.ExternalTagItem(_) => { ret "external_tag_item"; },
|
||||
.TagItem(_) => { ret "tag_item"; },
|
||||
.ComptimeDecl(_) => { ret "comptime_decl"; },
|
||||
.TopLevelBlock(_) => { ret "top_level_block"; },
|
||||
.FnDecl(_) => { ret "fn_decl"; },
|
||||
.VarDecl(_) => { ret "var_decl"; },
|
||||
.ConstDecl(_) => { ret "const_decl"; },
|
||||
.TypeDecl(_) => { ret "type_decl"; },
|
||||
.MemberDecl(_) => { ret "member_decl"; },
|
||||
.ParamDecl(_) => { ret "param_decl"; },
|
||||
.StatementRet(_) => { ret "statement_ret"; },
|
||||
.StatementBreak(_) => { ret "statement_break"; },
|
||||
.StatementContinue(_) => { ret "statement_continue"; },
|
||||
.StatementDefer(_) => { ret "statement_defer"; },
|
||||
.StatementComptime(_) => { ret "statement_comptime"; },
|
||||
.StatementDecl(_) => { ret "statement_decl"; },
|
||||
.StatementExpr(_) => { ret "statement_expr"; },
|
||||
.AggregateMember(_) => { ret "aggregate_member"; },
|
||||
.StructInitItem(_) => { ret "struct_init_item"; },
|
||||
.MatchArm(_) => { ret "match_arm"; },
|
||||
.Label(_) => { ret "label"; },
|
||||
.ExprIntLit(_) => { ret "int_lit"; },
|
||||
.ExprFloatLit(_) => { ret "float_lit"; },
|
||||
.ExprCharLit(_) => { ret "char_lit"; },
|
||||
.ExprStringLit(_) => { ret "string_lit"; },
|
||||
.ExprBoolLit(_) => { ret "bool_lit"; },
|
||||
.ExprNullLit => { ret "null_lit"; },
|
||||
.ExprUndefinedLit => { ret "undefined_lit"; },
|
||||
.ExprIdent(_) => { ret "ident"; },
|
||||
.ExprUnary(_) => { ret "unary"; },
|
||||
.ExprBinary(_) => { ret "binary"; },
|
||||
.ExprCall(_) => { ret "call"; },
|
||||
.ExprField(_) => { ret "field"; },
|
||||
.ExprIndex(_) => { ret "index"; },
|
||||
.ExprSlice(_) => { ret "slice"; },
|
||||
.ExprCast(_) => { ret "cast"; },
|
||||
.ExprTuple(_) => { ret "tuple"; },
|
||||
.ExprArrayLit(_) => { ret "array_literal"; },
|
||||
.ExprAggregateLit(_) => { ret "aggregate_literal"; },
|
||||
.ExprComptimeCall(_) => { ret "comptime_call"; },
|
||||
.ExprBlock(_) => { ret "block"; },
|
||||
.ExprIf(_) => { ret "if_expr"; },
|
||||
.ExprIfvar(_) => { ret "ifvar_expr"; },
|
||||
.ExprWhile(_) => { ret "while_expr"; },
|
||||
.ExprLoop(_) => { ret "loop_expr"; },
|
||||
.ExprFor(_) => { ret "for_expr"; },
|
||||
.ExprMatch(_) => { ret "match_expr"; },
|
||||
.ExprFnType(_) => { ret "fn_type"; },
|
||||
.TypePath(_) => { ret "type_path"; },
|
||||
.TypePointer(_) => { ret "type_pointer"; },
|
||||
.TypeArray(_) => { ret "type_array"; },
|
||||
.TypeSlice(_) => { ret "type_slice"; },
|
||||
.TypeFn(_) => { ret "type_fn"; },
|
||||
.TypeParen(_) => { ret "type_paren"; },
|
||||
.TypeAggregate(_) => { ret "type_aggregate"; },
|
||||
.TypeComptimeCall(_) => { ret "type_comptime_call"; },
|
||||
.PatternIdent(_) => { ret "pattern_ident"; },
|
||||
.PatternUnderscore => { ret "pattern_underscore"; },
|
||||
.PatternDot(_) => { ret "pattern_dot"; },
|
||||
.PatternTuple(_) => { ret "pattern_tuple"; },
|
||||
.PatternList(_) => { ret "pattern_list"; },
|
||||
.PatternAggregate(_) => { ret "pattern_aggregate"; },
|
||||
.PatternField(_) => { ret "pattern_field"; },
|
||||
}
|
||||
ret "?";
|
||||
}
|
||||
|
||||
type Node = struct {
|
||||
span: Span,
|
||||
resolved_def_id: usize,
|
||||
resolved_lang_tid: usize,
|
||||
expected_lang_tid: usize,
|
||||
attrs: Vec,
|
||||
body: Body,
|
||||
}
|
||||
|
||||
type Ast = @self;
|
||||
|
||||
nodes: Vec, // Node
|
||||
root: usize,
|
||||
sources: *Sources, // 借用;可为 null(dump 时不解析行列)
|
||||
|
||||
type AstResult = enum {
|
||||
Ok: Ast,
|
||||
Err: Error,
|
||||
}
|
||||
|
||||
fn new_node(body: Body, span: Span): Node {
|
||||
ret Node {
|
||||
.span = span,
|
||||
.resolved_def_id = 0,
|
||||
.resolved_lang_tid = 0,
|
||||
.expected_lang_tid = 0,
|
||||
.attrs = Vec.blank(@sizeof(usize), @alignof(usize)),
|
||||
.body = body,
|
||||
};
|
||||
}
|
||||
|
||||
fn new(sources: *Sources): AstResult {
|
||||
var ast = Ast {
|
||||
.nodes = Vec.blank(@sizeof(Node), @alignof(Node)),
|
||||
.root = 0,
|
||||
.sources = sources,
|
||||
};
|
||||
var sentinel = new_node(.None, span_none());
|
||||
match Vec.push(&ast.nodes, &sentinel as *u8) {
|
||||
.Ok => {},
|
||||
.Err(error) => {
|
||||
Vec.drop(&ast.nodes);
|
||||
ret .Err(error);
|
||||
},
|
||||
}
|
||||
ret .Ok(ast);
|
||||
}
|
||||
|
||||
fn drop(self: *Ast) {
|
||||
var i: usize = 0;
|
||||
while i < self.nodes.len {
|
||||
var n = node(self, i);
|
||||
if n != null {
|
||||
Vec.drop(&n.attrs);
|
||||
match n.body {
|
||||
.ExprIntLit(x) => { BigInt.drop(&x.value); },
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
Vec.drop(&self.nodes);
|
||||
self.root = 0;
|
||||
}
|
||||
|
||||
fn node(self: *Ast, ref: usize): *Node {
|
||||
match Vec.get(&self.nodes, ref) {
|
||||
.Ok(ptr) => { ret ptr as *Node; },
|
||||
.Err(error) => { ret null; },
|
||||
}
|
||||
ret null;
|
||||
}
|
||||
|
||||
// 追加节点,返回其引用(失败返回 0)。
|
||||
fn alloc(self: *Ast, body: Body, span: Span): usize {
|
||||
var n = new_node(body, span);
|
||||
match Vec.push(&self.nodes, &n as *u8) {
|
||||
.Ok => {},
|
||||
.Err(error) => {
|
||||
Vec.drop(&n.attrs);
|
||||
ret 0;
|
||||
},
|
||||
}
|
||||
ret self.nodes.len - 1;
|
||||
}
|
||||
|
||||
fn count(self: *Ast): usize {
|
||||
ret self.nodes.len;
|
||||
}
|
||||
|
||||
fn ref_vec(): Vec {
|
||||
ret Vec.blank(@sizeof(usize), @alignof(usize));
|
||||
}
|
||||
|
||||
// ---- dump(镜像 stage1 spl_ast_dump)----
|
||||
//
|
||||
// AST:
|
||||
// <indent>`-/|-kind #ref [line:col]
|
||||
// indent:栈上每层 last 为真输出 " ",否则 "| "。
|
||||
|
||||
fn push(buf: *ByteBuffer, bytes: []u8): bool {
|
||||
match ByteBuffer.append(buf, bytes) {
|
||||
.Ok => { ret true; },
|
||||
.Err(error) => { ret false; },
|
||||
}
|
||||
ret false;
|
||||
}
|
||||
|
||||
fn push_byte(buf: *ByteBuffer, byte: u8): bool {
|
||||
match ByteBuffer.append_byte(buf, byte) {
|
||||
.Ok => { ret true; },
|
||||
.Err(error) => { ret false; },
|
||||
}
|
||||
ret false;
|
||||
}
|
||||
|
||||
fn push_usize(buf: *ByteBuffer, value: usize): bool {
|
||||
match append_usize(buf, value, 10, false) {
|
||||
.Ok => { ret true; },
|
||||
.Err(error) => { ret false; },
|
||||
}
|
||||
ret false;
|
||||
}
|
||||
|
||||
fn ref_at(list: Vec, index: usize): usize {
|
||||
match Vec.get(&list, index) {
|
||||
.Ok(ptr) => { ret (ptr as *usize).*; },
|
||||
.Err(error) => { ret 0; },
|
||||
}
|
||||
ret 0;
|
||||
}
|
||||
|
||||
fn dump_opt(ast: *Ast, buf: *ByteBuffer, stack: *Vec, ref: usize, last: bool): bool {
|
||||
if ref == 0 { ret true; }
|
||||
ret dump_node(ast, buf, stack, ref, last);
|
||||
}
|
||||
|
||||
fn dump_list(ast: *Ast, buf: *ByteBuffer, stack: *Vec, list: Vec): bool {
|
||||
var i: usize = 0;
|
||||
while i < list.len {
|
||||
var last = (i + 1 == list.len);
|
||||
if !dump_node(ast, buf, stack, ref_at(list, i), last) { ret false; }
|
||||
i = i + 1;
|
||||
}
|
||||
ret true;
|
||||
}
|
||||
|
||||
fn dump_node(ast: *Ast, buf: *ByteBuffer, stack: *Vec, ref: usize, last: bool): bool {
|
||||
var n = node(ast, ref);
|
||||
if n == null { ret false; }
|
||||
|
||||
var i: usize = 0;
|
||||
while i < stack.len {
|
||||
var flag: u8 = 0;
|
||||
match Vec.get(stack, i) {
|
||||
.Ok(ptr) => { flag = ptr.*; },
|
||||
.Err(error) => { ret false; },
|
||||
}
|
||||
if !push(buf, if flag != 0 { " " } else { "| " }) { ret false; }
|
||||
i = i + 1;
|
||||
}
|
||||
if !push(buf, if last { "`-" } else { "|-" }) { ret false; }
|
||||
if !push(buf, kind_name(n.body)) { ret false; }
|
||||
if !push(buf, " #") { ret false; }
|
||||
if !push_usize(buf, ref) { ret false; }
|
||||
if !span_is_none(n.span) && ast.sources != null {
|
||||
var src = set_at(ast.sources, n.span.file);
|
||||
if src != null {
|
||||
var pos = Source.line_col(src.*, n.span.start);
|
||||
if !push(buf, " [") { ret false; }
|
||||
if !push_usize(buf, pos.line) { ret false; }
|
||||
if !push_byte(buf, ':') { ret false; }
|
||||
if !push_usize(buf, pos.col) { ret false; }
|
||||
if !push_byte(buf, ']') { ret false; }
|
||||
}
|
||||
}
|
||||
if !push_byte(buf, '\n') { ret false; }
|
||||
|
||||
var l: u8 = 0;
|
||||
if last { l = 1; }
|
||||
match Vec.push(stack, &l as *u8) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret false; },
|
||||
}
|
||||
|
||||
var ok = true;
|
||||
match n.body {
|
||||
.None => {},
|
||||
.Root(r) => { ok = dump_opt(ast, buf, stack, r.members, true); },
|
||||
.ContainerMembers(c) => {
|
||||
ok = dump_list(ast, buf, stack, c.external_tags) &&
|
||||
dump_list(ast, buf, stack, c.declarations);
|
||||
},
|
||||
.ExternalTagItem(t) => { ok = dump_list(ast, buf, stack, t.args); },
|
||||
.TagItem(t) => { ok = dump_list(ast, buf, stack, t.args); },
|
||||
.ComptimeDecl(x) => { ok = dump_opt(ast, buf, stack, x.body, true); },
|
||||
.TopLevelBlock(t) => {
|
||||
ok = dump_list(ast, buf, stack, t.tag_list) &&
|
||||
dump_opt(ast, buf, stack, t.block, true);
|
||||
},
|
||||
.FnDecl(f) => {
|
||||
ok = dump_list(ast, buf, stack, f.tag_list) &&
|
||||
dump_list(ast, buf, stack, f.params) &&
|
||||
dump_opt(ast, buf, stack, f.ret_type, false) &&
|
||||
dump_opt(ast, buf, stack, f.body, true);
|
||||
},
|
||||
.VarDecl(d) => {
|
||||
ok = dump_list(ast, buf, stack, d.attr_list) &&
|
||||
dump_opt(ast, buf, stack, d.pattern, false) &&
|
||||
dump_opt(ast, buf, stack, d.type_expr, false) &&
|
||||
dump_opt(ast, buf, stack, d.init_expr, true);
|
||||
},
|
||||
.ConstDecl(d) => {
|
||||
ok = dump_list(ast, buf, stack, d.attr_list) &&
|
||||
dump_opt(ast, buf, stack, d.pattern, false) &&
|
||||
dump_opt(ast, buf, stack, d.type_expr, false) &&
|
||||
dump_opt(ast, buf, stack, d.init_expr, true);
|
||||
},
|
||||
.TypeDecl(d) => {
|
||||
ok = dump_list(ast, buf, stack, d.attr_list) &&
|
||||
dump_opt(ast, buf, stack, d.pattern, false) &&
|
||||
dump_opt(ast, buf, stack, d.type_expr, true);
|
||||
},
|
||||
.MemberDecl(d) => {
|
||||
ok = dump_list(ast, buf, stack, d.attr_list) &&
|
||||
dump_opt(ast, buf, stack, d.type_expr, false) &&
|
||||
dump_opt(ast, buf, stack, d.init_expr, true);
|
||||
},
|
||||
.ParamDecl(p) => {
|
||||
ok = dump_list(ast, buf, stack, p.attr_list) &&
|
||||
dump_opt(ast, buf, stack, p.type_expr, true);
|
||||
},
|
||||
.StatementRet(e) => { ok = dump_opt(ast, buf, stack, e.expr, true); },
|
||||
.StatementBreak(b) => {
|
||||
ok = dump_opt(ast, buf, stack, b.label, false) &&
|
||||
dump_opt(ast, buf, stack, b.expr, true);
|
||||
},
|
||||
.StatementContinue(x) => { ok = dump_opt(ast, buf, stack, x.label, true); },
|
||||
.StatementDefer(x) => { ok = dump_opt(ast, buf, stack, x.body, true); },
|
||||
.StatementComptime(x) => { ok = dump_opt(ast, buf, stack, x.body, true); },
|
||||
.StatementDecl(d) => { ok = dump_opt(ast, buf, stack, d.decl, true); },
|
||||
.StatementExpr(e) => { ok = dump_opt(ast, buf, stack, e.expr, true); },
|
||||
.AggregateMember(d) => {
|
||||
ok = dump_list(ast, buf, stack, d.attr_list) &&
|
||||
dump_opt(ast, buf, stack, d.type_expr, false) &&
|
||||
dump_opt(ast, buf, stack, d.init_expr, true);
|
||||
},
|
||||
.StructInitItem(s) => { ok = dump_opt(ast, buf, stack, s.expr, true); },
|
||||
.MatchArm(m) => {
|
||||
ok = dump_opt(ast, buf, stack, m.pattern, false) &&
|
||||
dump_opt(ast, buf, stack, m.body, true);
|
||||
},
|
||||
.Label(x) => {},
|
||||
.ExprIntLit(x) => {},
|
||||
.ExprFloatLit(x) => {},
|
||||
.ExprCharLit(x) => {},
|
||||
.ExprStringLit(x) => {},
|
||||
.ExprBoolLit(x) => {},
|
||||
.ExprNullLit => {},
|
||||
.ExprUndefinedLit => {},
|
||||
.ExprIdent(x) => {},
|
||||
.ExprUnary(u) => { ok = dump_opt(ast, buf, stack, u.operand, true); },
|
||||
.ExprBinary(b) => {
|
||||
ok = dump_opt(ast, buf, stack, b.left, false) &&
|
||||
dump_opt(ast, buf, stack, b.right, true);
|
||||
},
|
||||
.ExprCall(c) => {
|
||||
ok = dump_opt(ast, buf, stack, c.callee, false) &&
|
||||
dump_list(ast, buf, stack, c.args);
|
||||
},
|
||||
.ExprField(f) => { ok = dump_opt(ast, buf, stack, f.obj, true); },
|
||||
.ExprIndex(x) => {
|
||||
ok = dump_opt(ast, buf, stack, x.obj, false) &&
|
||||
dump_opt(ast, buf, stack, x.index, true);
|
||||
},
|
||||
.ExprSlice(s) => {
|
||||
ok = dump_opt(ast, buf, stack, s.obj, false) &&
|
||||
dump_opt(ast, buf, stack, s.begin, false) &&
|
||||
dump_opt(ast, buf, stack, s.end, true);
|
||||
},
|
||||
.ExprCast(c) => {
|
||||
ok = dump_opt(ast, buf, stack, c.expr, false) &&
|
||||
dump_opt(ast, buf, stack, c.type_expr, true);
|
||||
},
|
||||
.ExprTuple(v) => { ok = dump_list(ast, buf, stack, v.items); },
|
||||
.ExprArrayLit(a) => {
|
||||
ok = dump_opt(ast, buf, stack, a.elem_type, false) &&
|
||||
dump_list(ast, buf, stack, a.elems);
|
||||
},
|
||||
.ExprAggregateLit(a) => { ok = dump_list(ast, buf, stack, a.fields); },
|
||||
.ExprComptimeCall(c) => { ok = dump_list(ast, buf, stack, c.args); },
|
||||
.ExprBlock(b) => {
|
||||
ok = dump_list(ast, buf, stack, b.stmts) &&
|
||||
dump_opt(ast, buf, stack, b.tail_expr, true);
|
||||
},
|
||||
.ExprIf(x) => {
|
||||
ok = dump_opt(ast, buf, stack, x.cond, false) &&
|
||||
dump_opt(ast, buf, stack, x.then_block, false) &&
|
||||
dump_opt(ast, buf, stack, x.else_branch, true);
|
||||
},
|
||||
.ExprIfvar(x) => {
|
||||
ok = dump_opt(ast, buf, stack, x.var_decl, false) &&
|
||||
dump_opt(ast, buf, stack, x.then_block, false) &&
|
||||
dump_opt(ast, buf, stack, x.else_branch, true);
|
||||
},
|
||||
.ExprWhile(w) => {
|
||||
ok = dump_opt(ast, buf, stack, w.label, false) &&
|
||||
dump_opt(ast, buf, stack, w.cond, false) &&
|
||||
dump_opt(ast, buf, stack, w.body, false) &&
|
||||
dump_opt(ast, buf, stack, w.else_branch, true);
|
||||
},
|
||||
.ExprLoop(x) => {
|
||||
ok = dump_opt(ast, buf, stack, x.label, false) &&
|
||||
dump_opt(ast, buf, stack, x.body, true);
|
||||
},
|
||||
.ExprFor(f) => {
|
||||
ok = dump_opt(ast, buf, stack, f.label, false) &&
|
||||
dump_list(ast, buf, stack, f.iter_exprs) &&
|
||||
dump_opt(ast, buf, stack, f.pattern, false) &&
|
||||
dump_opt(ast, buf, stack, f.body, false) &&
|
||||
dump_opt(ast, buf, stack, f.else_branch, true);
|
||||
},
|
||||
.ExprMatch(m) => {
|
||||
ok = dump_opt(ast, buf, stack, m.matched_expr, false) &&
|
||||
dump_list(ast, buf, stack, m.arms);
|
||||
},
|
||||
.ExprFnType(f) => {
|
||||
ok = dump_list(ast, buf, stack, f.params) &&
|
||||
dump_opt(ast, buf, stack, f.ret_type, true);
|
||||
},
|
||||
.TypePath(p) => {},
|
||||
.TypePointer(p) => { ok = dump_opt(ast, buf, stack, p.pointee, true); },
|
||||
.TypeArray(a) => { ok = dump_opt(ast, buf, stack, a.elem_type, true); },
|
||||
.TypeSlice(s) => { ok = dump_opt(ast, buf, stack, s.elem_type, true); },
|
||||
.TypeFn(f) => {
|
||||
ok = dump_list(ast, buf, stack, f.params) &&
|
||||
dump_opt(ast, buf, stack, f.ret_type, true);
|
||||
},
|
||||
.TypeParen(p) => { ok = dump_list(ast, buf, stack, p.types); },
|
||||
.TypeAggregate(a) => { ok = dump_list(ast, buf, stack, a.members); },
|
||||
.TypeComptimeCall(c) => { ok = dump_list(ast, buf, stack, c.args); },
|
||||
.PatternIdent(x) => {},
|
||||
.PatternUnderscore => {},
|
||||
.PatternDot(p) => { ok = dump_opt(ast, buf, stack, p.sub_pattern, true); },
|
||||
.PatternTuple(v) => { ok = dump_list(ast, buf, stack, v.items); },
|
||||
.PatternList(v) => { ok = dump_list(ast, buf, stack, v.items); },
|
||||
.PatternAggregate(v) => { ok = dump_list(ast, buf, stack, v.items); },
|
||||
.PatternField(p) => { ok = dump_opt(ast, buf, stack, p.pattern, true); },
|
||||
}
|
||||
|
||||
stack.len = stack.len - 1;
|
||||
ret ok;
|
||||
}
|
||||
|
||||
// 把整棵树 dump 到 out(追加,不 clear)。
|
||||
fn dump(ast: *Ast, out: *ByteBuffer): bool {
|
||||
if !push(out, "AST:\n") { ret false; }
|
||||
var stack = Vec.blank(1, 1);
|
||||
defer { Vec.drop(&stack); }
|
||||
ret dump_node(ast, out, &stack, ast.root, true);
|
||||
}
|
||||
|
||||
// ---- 测试 ----
|
||||
|
||||
fn test_kind_names() {
|
||||
@assert(ByteView.eq(ByteView.from_slice(kind_name(.None)), ByteView.from_slice("none")),
|
||||
"none name");
|
||||
@assert(ByteView.eq(ByteView.from_slice(kind_name(.ExprBinary(.{
|
||||
.op = .Add,
|
||||
.left = 0,
|
||||
.right = 0,
|
||||
}))),
|
||||
ByteView.from_slice("binary")),
|
||||
"binary name");
|
||||
}
|
||||
|
||||
fn test_alloc_and_payload() {
|
||||
var ast: Ast = undefined;
|
||||
match new(null) {
|
||||
.Ok(v) => { ast = v; },
|
||||
.Err(error) => { @assert(false, "ast new failed"); },
|
||||
}
|
||||
defer { drop(&ast); }
|
||||
|
||||
@assert(count(&ast) == 1, "sentinel only");
|
||||
|
||||
var r = alloc(&ast, .ExprIdent(.{ .name = ByteView.from_slice("main") }), span_make(0, 5, 9));
|
||||
@assert(r == 1, "first alloc is 1");
|
||||
|
||||
var n = node(&ast, r);
|
||||
n.resolved_def_id = 99;
|
||||
@assert(n.span.file == 0 && n.span.start == 5 && n.span.end == 9, "span stored");
|
||||
@assert(n.resolved_def_id == 99, "header field");
|
||||
|
||||
match n.body {
|
||||
.ExprIdent(i) => {
|
||||
@assert(ByteView.eq(i.name, ByteView.from_slice("main")), "payload name");
|
||||
},
|
||||
_ => { @assert(false, "expected ident"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_bytes(buf: *ByteBuffer, expected: []u8) {
|
||||
var actual = ByteBuffer.as_slice(buf.*);
|
||||
@assert(actual.len == expected.len, "dump length");
|
||||
var i: usize = 0;
|
||||
while i < expected.len {
|
||||
@assert(actual.ptr[i] == expected.ptr[i], "dump byte");
|
||||
i = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn test_dump() {
|
||||
var ast: Ast = undefined;
|
||||
match new(null) {
|
||||
.Ok(v) => { ast = v; },
|
||||
.Err(error) => { @assert(false, "ast new failed"); },
|
||||
}
|
||||
defer { drop(&ast); }
|
||||
|
||||
var id_ref = alloc(&ast, .ExprIdent(.{ .name = ByteView.from_slice("main") }), span_none()); // #1
|
||||
var decls = ref_vec();
|
||||
match Vec.push(&decls, &id_ref as *u8) {
|
||||
.Ok => {},
|
||||
.Err(error) => { @assert(false, "push decl"); },
|
||||
}
|
||||
var cm_ref = alloc(&ast, .ContainerMembers(.{
|
||||
.external_tags = ref_vec(),
|
||||
.declarations = decls,
|
||||
}), span_none()); // #2
|
||||
var root_ref = alloc(&ast, .Root(.{ .members = cm_ref }), span_none()); // #3
|
||||
ast.root = root_ref;
|
||||
|
||||
var buf = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&buf); }
|
||||
@assert(dump(&ast, &buf), "dump ok");
|
||||
// sources=null → 不解析行列;无 span 时也不打印。
|
||||
expect_bytes(&buf, "AST:\n`-root #3\n `-container_members #2\n `-ident #1\n");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
test_kind_names();
|
||||
test_alloc_and_payload();
|
||||
test_dump();
|
||||
}
|
||||
+791
@@ -0,0 +1,791 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// 丰富诊断(参考 rustc / Clang / Swift 的设计,不照搬 stage1 C 实现):
|
||||
// Severity Note/Help/Warning/Error/Fatal
|
||||
// Code 固定语义枚举 + 稳定 E 码;集中管理,利于 diff
|
||||
// Span 字节区间 {file,start,end};file 是 Sources 的下标,NO_FILE 表示无位置
|
||||
// Label 主/次标签(span + 文本),可多个
|
||||
// Child 子诊断(note/help,可带 span),挂在条目上
|
||||
// 渲染:人类可读(源码行 + caret 下划线)与 JSON(字段稳定)双格式。
|
||||
// 位置只存下标,渲染时经 Sources 解析行列/取行,避免复制文件名。
|
||||
// 消息按拥有型 Str 保存;条目拥有其 labels/children;clear/drop 释放全部内部资源。
|
||||
|
||||
type .{ .Error, .Status, .make } = @import("./utils/error.spl");
|
||||
type .{ .Vec } = @import("./utils/vec.spl");
|
||||
type .{ .ByteBuffer } = @import("./utils/byte_buffer.spl");
|
||||
type .{ .Str } = @import("./utils/str.spl");
|
||||
type .{ .ByteView } = @import("./utils/byte_view.spl");
|
||||
type .{ .append_usize } = @import("./utils/fmt.spl");
|
||||
type .{ .Sources, .Source, .set_new, .set_add, .set_at, .set_drop } = @import("./utils/source.spl");
|
||||
|
||||
type Severity = enum {
|
||||
Note,
|
||||
Help,
|
||||
Warning,
|
||||
Error,
|
||||
Fatal,
|
||||
}
|
||||
|
||||
type Stage = enum {
|
||||
None,
|
||||
Lex,
|
||||
Parse,
|
||||
Sema,
|
||||
Lower,
|
||||
Vm,
|
||||
Driver,
|
||||
}
|
||||
|
||||
type Code = enum {
|
||||
None,
|
||||
// 词法 / 语法
|
||||
LexUnexpected,
|
||||
ParseExpected,
|
||||
ParseUnrecognized,
|
||||
// sema
|
||||
UndefinedSymbol,
|
||||
DuplicateSymbol,
|
||||
TypeMismatch,
|
||||
ArgCountMismatch,
|
||||
NotAFunction,
|
||||
NoField,
|
||||
NoMethod,
|
||||
IndexBaseInvalid,
|
||||
TupleIndexOob,
|
||||
FitsOverflow,
|
||||
CircularDep,
|
||||
BreakOutsideLoop,
|
||||
ContinueOutsideLoop,
|
||||
UndefinedLabel,
|
||||
CastInvalid,
|
||||
UnknownAttr,
|
||||
UnknownIntrin,
|
||||
IntrinArgCount,
|
||||
VarUninitialized,
|
||||
CannotInfer,
|
||||
MustReturnValue,
|
||||
// lowering
|
||||
UnhandledNode,
|
||||
ImplicitCastFail,
|
||||
NoLvalue,
|
||||
BadFieldIndex,
|
||||
BadBlock,
|
||||
// vm
|
||||
IrUnknownKind,
|
||||
FrameOverflow,
|
||||
// 通用
|
||||
Internal,
|
||||
}
|
||||
|
||||
// file 下标不可用时的哨兵。
|
||||
fn no_file(): usize {
|
||||
ret ~(0 as usize);
|
||||
}
|
||||
|
||||
type Span = struct {
|
||||
file: usize,
|
||||
start: usize,
|
||||
end: usize,
|
||||
}
|
||||
|
||||
fn span_none(): Span {
|
||||
ret Span { .file = no_file(), .start = 0, .end = 0 };
|
||||
}
|
||||
|
||||
fn span_make(file: usize, start: usize, end: usize): Span {
|
||||
ret Span { .file = file, .start = start, .end = end };
|
||||
}
|
||||
|
||||
fn span_is_none(span: Span): bool {
|
||||
ret span.file == no_file();
|
||||
}
|
||||
|
||||
type Label = struct {
|
||||
span: Span,
|
||||
message: Str,
|
||||
primary: bool,
|
||||
}
|
||||
|
||||
type Child = struct {
|
||||
severity: Severity,
|
||||
span: Span,
|
||||
message: Str,
|
||||
}
|
||||
|
||||
type Item = struct {
|
||||
severity: Severity,
|
||||
stage: Stage,
|
||||
code: Code,
|
||||
span: Span,
|
||||
message: Str,
|
||||
labels: Vec, // Label
|
||||
children: Vec, // Child
|
||||
}
|
||||
|
||||
type Diag = @self;
|
||||
|
||||
items: Vec, // Item
|
||||
sources: *Sources, // 借用;可为 null(则只输出消息)
|
||||
errors: usize,
|
||||
warnings: usize,
|
||||
fatals: usize,
|
||||
|
||||
fn new(sources: *Sources): Diag {
|
||||
ret Diag {
|
||||
.items = Vec.blank(@sizeof(Item), @alignof(Item)),
|
||||
.sources = sources,
|
||||
.errors = 0,
|
||||
.warnings = 0,
|
||||
.fatals = 0,
|
||||
};
|
||||
}
|
||||
|
||||
fn item_at(self: *Diag, index: usize): *Item {
|
||||
match Vec.get(&self.items, index) {
|
||||
.Ok(ptr) => { ret ptr as *Item; },
|
||||
.Err(error) => { ret null; },
|
||||
}
|
||||
ret null;
|
||||
}
|
||||
|
||||
fn label_at(it: *Item, index: usize): *Label {
|
||||
match Vec.get(&it.labels, index) {
|
||||
.Ok(ptr) => { ret ptr as *Label; },
|
||||
.Err(error) => { ret null; },
|
||||
}
|
||||
ret null;
|
||||
}
|
||||
|
||||
fn child_at(it: *Item, index: usize): *Child {
|
||||
match Vec.get(&it.children, index) {
|
||||
.Ok(ptr) => { ret ptr as *Child; },
|
||||
.Err(error) => { ret null; },
|
||||
}
|
||||
ret null;
|
||||
}
|
||||
|
||||
fn last(self: *Diag): usize {
|
||||
if self.items.len == 0 { ret no_file(); }
|
||||
ret self.items.len - 1;
|
||||
}
|
||||
|
||||
fn count(self: Diag): usize { ret self.items.len; }
|
||||
|
||||
fn error_count(self: Diag): usize { ret self.errors; }
|
||||
|
||||
fn warning_count(self: Diag): usize { ret self.warnings; }
|
||||
|
||||
fn fatal_count(self: Diag): usize { ret self.fatals; }
|
||||
|
||||
fn has_error(self: Diag): bool { ret self.errors > 0; }
|
||||
|
||||
fn has_fatal(self: Diag): bool { ret self.fatals > 0; }
|
||||
|
||||
// ---- 采集 ----
|
||||
|
||||
// 追加一条诊断,返回其下标(失败返回 no_file())。span 为主位置。
|
||||
fn emit(self: *Diag, severity: Severity, stage: Stage, code: Code, span: Span,
|
||||
msg: []u8): usize {
|
||||
var message = Str.new();
|
||||
match Str.from_slice(msg) {
|
||||
.Ok(v) => { message = v; },
|
||||
.Err(error) => { ret no_file(); },
|
||||
}
|
||||
var item = Item {
|
||||
.severity = severity,
|
||||
.stage = stage,
|
||||
.code = code,
|
||||
.span = span,
|
||||
.message = message,
|
||||
.labels = Vec.blank(@sizeof(Label), @alignof(Label)),
|
||||
.children = Vec.blank(@sizeof(Child), @alignof(Child)),
|
||||
};
|
||||
match Vec.push(&self.items, &item as *u8) {
|
||||
.Ok => {},
|
||||
.Err(error) => { Str.drop(&item.message); ret no_file(); },
|
||||
}
|
||||
match severity {
|
||||
.Error => { self.errors = self.errors + 1; },
|
||||
.Fatal => { self.errors = self.errors + 1; self.fatals = self.fatals + 1; },
|
||||
.Warning => { self.warnings = self.warnings + 1; },
|
||||
_ => {},
|
||||
}
|
||||
ret self.items.len - 1;
|
||||
}
|
||||
|
||||
fn err(self: *Diag, span: Span, code: Code, msg: []u8): usize {
|
||||
ret emit(self, .Error, .None, code, span, msg);
|
||||
}
|
||||
|
||||
fn warn(self: *Diag, span: Span, code: Code, msg: []u8): usize {
|
||||
ret emit(self, .Warning, .None, code, span, msg);
|
||||
}
|
||||
|
||||
fn note(self: *Diag, span: Span, code: Code, msg: []u8): usize {
|
||||
ret emit(self, .Note, .None, code, span, msg);
|
||||
}
|
||||
|
||||
// 给最近一条诊断附加标签(primary 决定主/次)。
|
||||
fn attach_label(self: *Diag, span: Span, msg: []u8, primary: bool): Status {
|
||||
var idx = last(self);
|
||||
if idx == no_file() { ret .Err(make(.OutOfRange)); }
|
||||
var it = item_at(self, idx);
|
||||
if it == null { ret .Err(make(.OutOfRange)); }
|
||||
|
||||
var message = Str.new();
|
||||
match Str.from_slice(msg) {
|
||||
.Ok(v) => { message = v; },
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
var lab = Label { .span = span, .message = message, .primary = primary };
|
||||
match Vec.push(&it.labels, &lab as *u8) {
|
||||
.Ok => {},
|
||||
.Err(error) => { Str.drop(&lab.message); ret .Err(error); },
|
||||
}
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
// 给最近一条诊断附加子诊断(note/help 等)。
|
||||
fn attach_child(self: *Diag, severity: Severity, span: Span, msg: []u8): Status {
|
||||
var idx = last(self);
|
||||
if idx == no_file() { ret .Err(make(.OutOfRange)); }
|
||||
var it = item_at(self, idx);
|
||||
if it == null { ret .Err(make(.OutOfRange)); }
|
||||
|
||||
var message = Str.new();
|
||||
match Str.from_slice(msg) {
|
||||
.Ok(v) => { message = v; },
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
var ch = Child { .severity = severity, .span = span, .message = message };
|
||||
match Vec.push(&it.children, &ch as *u8) {
|
||||
.Ok => {},
|
||||
.Err(error) => { Str.drop(&ch.message); ret .Err(error); },
|
||||
}
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
// ---- 名称 ----
|
||||
|
||||
fn severity_name(severity: Severity): []u8 {
|
||||
match severity {
|
||||
.Note => { ret "note"; },
|
||||
.Help => { ret "help"; },
|
||||
.Warning => { ret "warning"; },
|
||||
.Error => { ret "error"; },
|
||||
.Fatal => { ret "fatal"; },
|
||||
}
|
||||
ret "?";
|
||||
}
|
||||
|
||||
fn stage_name(stage: Stage): []u8 {
|
||||
match stage {
|
||||
.None => { ret "none"; },
|
||||
.Lex => { ret "lex"; },
|
||||
.Parse => { ret "parse"; },
|
||||
.Sema => { ret "sema"; },
|
||||
.Lower => { ret "lower"; },
|
||||
.Vm => { ret "vm"; },
|
||||
.Driver => { ret "driver"; },
|
||||
}
|
||||
ret "?";
|
||||
}
|
||||
|
||||
// 稳定 E 码(diff 依赖;新增 Code 时分配新码,勿改旧值)。
|
||||
fn code_str(code: Code): []u8 {
|
||||
match code {
|
||||
.None => { ret "E0000"; },
|
||||
.LexUnexpected => { ret "E0001"; },
|
||||
.ParseExpected => { ret "E0002"; },
|
||||
.ParseUnrecognized => { ret "E0003"; },
|
||||
.UndefinedSymbol => { ret "E0101"; },
|
||||
.DuplicateSymbol => { ret "E0102"; },
|
||||
.TypeMismatch => { ret "E0103"; },
|
||||
.ArgCountMismatch => { ret "E0104"; },
|
||||
.NotAFunction => { ret "E0105"; },
|
||||
.NoField => { ret "E0106"; },
|
||||
.NoMethod => { ret "E0107"; },
|
||||
.IndexBaseInvalid => { ret "E0108"; },
|
||||
.TupleIndexOob => { ret "E0109"; },
|
||||
.FitsOverflow => { ret "E0110"; },
|
||||
.CircularDep => { ret "E0111"; },
|
||||
.BreakOutsideLoop => { ret "E0112"; },
|
||||
.ContinueOutsideLoop => { ret "E0113"; },
|
||||
.UndefinedLabel => { ret "E0114"; },
|
||||
.CastInvalid => { ret "E0115"; },
|
||||
.UnknownAttr => { ret "E0116"; },
|
||||
.UnknownIntrin => { ret "E0117"; },
|
||||
.IntrinArgCount => { ret "E0118"; },
|
||||
.VarUninitialized => { ret "E0119"; },
|
||||
.CannotInfer => { ret "E0120"; },
|
||||
.MustReturnValue => { ret "E0121"; },
|
||||
.UnhandledNode => { ret "E0201"; },
|
||||
.ImplicitCastFail => { ret "E0202"; },
|
||||
.NoLvalue => { ret "E0203"; },
|
||||
.BadFieldIndex => { ret "E0204"; },
|
||||
.BadBlock => { ret "E0205"; },
|
||||
.IrUnknownKind => { ret "E0301"; },
|
||||
.FrameOverflow => { ret "E0302"; },
|
||||
.Internal => { ret "E9000"; },
|
||||
}
|
||||
ret "E0000";
|
||||
}
|
||||
|
||||
fn code_name(code: Code): []u8 {
|
||||
match code {
|
||||
.None => { ret "none"; },
|
||||
.LexUnexpected => { ret "lex_unexpected"; },
|
||||
.ParseExpected => { ret "parse_expected"; },
|
||||
.ParseUnrecognized => { ret "parse_unrecognized"; },
|
||||
.UndefinedSymbol => { ret "undefined_symbol"; },
|
||||
.DuplicateSymbol => { ret "duplicate_symbol"; },
|
||||
.TypeMismatch => { ret "type_mismatch"; },
|
||||
.ArgCountMismatch => { ret "arg_count_mismatch"; },
|
||||
.NotAFunction => { ret "not_a_function"; },
|
||||
.NoField => { ret "no_field"; },
|
||||
.NoMethod => { ret "no_method"; },
|
||||
.IndexBaseInvalid => { ret "index_base_invalid"; },
|
||||
.TupleIndexOob => { ret "tuple_index_oob"; },
|
||||
.FitsOverflow => { ret "fits_overflow"; },
|
||||
.CircularDep => { ret "circular_dep"; },
|
||||
.BreakOutsideLoop => { ret "break_outside_loop"; },
|
||||
.ContinueOutsideLoop => { ret "continue_outside_loop"; },
|
||||
.UndefinedLabel => { ret "undefined_label"; },
|
||||
.CastInvalid => { ret "cast_invalid"; },
|
||||
.UnknownAttr => { ret "unknown_attr"; },
|
||||
.UnknownIntrin => { ret "unknown_intrin"; },
|
||||
.IntrinArgCount => { ret "intrin_arg_count"; },
|
||||
.VarUninitialized => { ret "var_uninitialized"; },
|
||||
.CannotInfer => { ret "cannot_infer"; },
|
||||
.MustReturnValue => { ret "must_return_value"; },
|
||||
.UnhandledNode => { ret "unhandled_node"; },
|
||||
.ImplicitCastFail => { ret "implicit_cast_fail"; },
|
||||
.NoLvalue => { ret "no_lvalue"; },
|
||||
.BadFieldIndex => { ret "bad_field_index"; },
|
||||
.BadBlock => { ret "bad_block"; },
|
||||
.IrUnknownKind => { ret "ir_unknown_kind"; },
|
||||
.FrameOverflow => { ret "frame_overflow"; },
|
||||
.Internal => { ret "internal"; },
|
||||
}
|
||||
ret "none";
|
||||
}
|
||||
|
||||
// ---- 渲染:公共小工具 ----
|
||||
|
||||
fn append_num(out: *ByteBuffer, value: usize): Status {
|
||||
ret append_usize(out, value, 10, false);
|
||||
}
|
||||
|
||||
fn append_str(out: *ByteBuffer, s: *Str): Status {
|
||||
ret ByteBuffer.append(out, Str.as_slice(s));
|
||||
}
|
||||
|
||||
fn append_view(out: *ByteBuffer, view: ByteView): Status {
|
||||
ret ByteBuffer.append(out, ByteView.as_slice(view));
|
||||
}
|
||||
|
||||
type Resolved = struct {
|
||||
ok: bool,
|
||||
fname: ByteView,
|
||||
line: usize,
|
||||
col: usize,
|
||||
text: ByteView,
|
||||
width: usize,
|
||||
}
|
||||
|
||||
fn resolve(self: *Diag, span: Span): Resolved {
|
||||
var r = Resolved {
|
||||
.ok = false,
|
||||
.fname = ByteView.empty(),
|
||||
.line = 0,
|
||||
.col = 0,
|
||||
.text = ByteView.empty(),
|
||||
.width = 1,
|
||||
};
|
||||
if self.sources == null { ret r; }
|
||||
if span_is_none(span) { ret r; }
|
||||
|
||||
var src = set_at(self.sources, span.file);
|
||||
if src == null { ret r; }
|
||||
|
||||
var pos = Source.line_col(src.*, span.start);
|
||||
var width: usize = 1;
|
||||
if span.end > span.start { width = span.end - span.start; }
|
||||
r.ok = true;
|
||||
r.fname = src.fname;
|
||||
r.line = pos.line;
|
||||
r.col = pos.col;
|
||||
r.text = Source.line_text(src.*, pos.line);
|
||||
r.width = width;
|
||||
ret r;
|
||||
}
|
||||
|
||||
fn render_header(it: *Item, r: *Resolved, out: *ByteBuffer): Status {
|
||||
if r.ok {
|
||||
match append_view(out, r.fname) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append_byte(out, ':') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_num(out, r.line) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append_byte(out, ':') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_num(out, r.col) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, ": ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
}
|
||||
match ByteBuffer.append(out, severity_name(it.severity)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append_byte(out, '[') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, code_str(it.code)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, "]: ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_str(out, &it.message) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
ret ByteBuffer.append_byte(out, '\n');
|
||||
}
|
||||
|
||||
// 输出一行源码 + caret 下划线(列范围按字节;制表符不特殊处理)。
|
||||
fn render_span_visual(self: *Diag, span: Span, out: *ByteBuffer): Status {
|
||||
var r = resolve(self, span);
|
||||
if !r.ok { ret .Ok; }
|
||||
|
||||
match ByteBuffer.append(out, " ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_view(out, r.text) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append_byte(out, '\n') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
|
||||
match ByteBuffer.append(out, " ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
var pad: usize = 0;
|
||||
if r.col > 0 { pad = r.col - 1; }
|
||||
match ByteBuffer.append_repeat(out, ' ', pad) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
|
||||
var width = r.width;
|
||||
if width == 0 { width = 1; }
|
||||
if r.col > 0 {
|
||||
var room: usize = 0;
|
||||
if r.text.len >= pad { room = r.text.len - pad; }
|
||||
if room == 0 { room = 1; }
|
||||
if width > room { width = room; }
|
||||
}
|
||||
var k: usize = 0;
|
||||
while k < width {
|
||||
var ch: u8 = '~';
|
||||
if k == 0 { ch = '^'; }
|
||||
match ByteBuffer.append_byte(out, ch) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
k = k + 1;
|
||||
}
|
||||
ret ByteBuffer.append_byte(out, '\n');
|
||||
}
|
||||
|
||||
fn render_child_line(ch: *Child, out: *ByteBuffer): Status {
|
||||
match ByteBuffer.append(out, " ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, severity_name(ch.severity)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, ": ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_str(out, &ch.message) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
ret ByteBuffer.append_byte(out, '\n');
|
||||
}
|
||||
|
||||
fn render_human(self: *Diag, out: *ByteBuffer): Status {
|
||||
var i: usize = 0;
|
||||
while i < self.items.len {
|
||||
var it = item_at(self, i);
|
||||
if it == null { ret .Err(make(.OutOfRange)); }
|
||||
|
||||
var r = resolve(self, it.span);
|
||||
match render_header(it, &r, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match render_span_visual(self, it.span, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
|
||||
var j: usize = 0;
|
||||
while j < it.labels.len {
|
||||
var lab = label_at(it, j);
|
||||
if lab != null {
|
||||
match render_span_visual(self, lab.span, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
if Str.size(lab.message) != 0 {
|
||||
match ByteBuffer.append(out, " ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_str(out, &lab.message) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append_byte(out, '\n') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
}
|
||||
}
|
||||
j = j + 1;
|
||||
}
|
||||
|
||||
var c: usize = 0;
|
||||
while c < it.children.len {
|
||||
var ch = child_at(it, c);
|
||||
if ch != null {
|
||||
match render_child_line(ch, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match render_span_visual(self, ch.span, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
}
|
||||
c = c + 1;
|
||||
}
|
||||
|
||||
if i + 1 < self.items.len {
|
||||
match ByteBuffer.append_byte(out, '\n') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
// ---- JSON 渲染(字段稳定,供 LSP / diff 工具)----
|
||||
|
||||
fn append_json_hex4(out: *ByteBuffer, b: u8): Status {
|
||||
var hex: []u8 = "0123456789abcdef";
|
||||
match ByteBuffer.append(out, "\\u00") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append_byte(out, hex[(b >> 4) as usize]) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
ret ByteBuffer.append_byte(out, hex[(b & 15) as usize]);
|
||||
}
|
||||
|
||||
fn append_json_str(out: *ByteBuffer, bytes: []u8): Status {
|
||||
match ByteBuffer.append_byte(out, '"') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
var i: usize = 0;
|
||||
while i < bytes.len {
|
||||
var b = bytes[i];
|
||||
if b == '"' {
|
||||
match ByteBuffer.append(out, "\\\"") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
} else if b == '\\' {
|
||||
match ByteBuffer.append(out, "\\\\") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
} else if b == '\n' {
|
||||
match ByteBuffer.append(out, "\\n") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
} else if b == '\r' {
|
||||
match ByteBuffer.append(out, "\\r") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
} else if b == '\t' {
|
||||
match ByteBuffer.append(out, "\\t") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
} else if b < ' ' {
|
||||
match append_json_hex4(out, b) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
} else {
|
||||
match ByteBuffer.append_byte(out, b) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
ret ByteBuffer.append_byte(out, '"');
|
||||
}
|
||||
|
||||
fn append_str_json(out: *ByteBuffer, s: *Str): Status {
|
||||
ret append_json_str(out, Str.as_slice(s));
|
||||
}
|
||||
|
||||
fn append_bool(out: *ByteBuffer, value: bool): Status {
|
||||
if value { ret ByteBuffer.append(out, "true"); }
|
||||
ret ByteBuffer.append(out, "false");
|
||||
}
|
||||
|
||||
fn append_loc_json(self: *Diag, span: Span, out: *ByteBuffer): Status {
|
||||
var r = resolve(self, span);
|
||||
match ByteBuffer.append(out, "{\"file\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
if r.ok {
|
||||
match append_json_str(out, ByteView.as_slice(r.fname)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
} else {
|
||||
match ByteBuffer.append(out, "null") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
}
|
||||
match ByteBuffer.append(out, ",\"start\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_num(out, span.start) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, ",\"end\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_num(out, span.end) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, ",\"line\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
if r.ok {
|
||||
match append_num(out, r.line) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
} else {
|
||||
match ByteBuffer.append(out, "null") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
}
|
||||
match ByteBuffer.append(out, ",\"col\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
if r.ok {
|
||||
match append_num(out, r.col) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
} else {
|
||||
match ByteBuffer.append(out, "null") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
}
|
||||
ret ByteBuffer.append_byte(out, '}');
|
||||
}
|
||||
|
||||
fn render_item_json(self: *Diag, it: *Item, out: *ByteBuffer): Status {
|
||||
match ByteBuffer.append(out, "{\"severity\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_json_str(out, severity_name(it.severity)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, ",\"stage\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_json_str(out, stage_name(it.stage)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, ",\"code\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_json_str(out, code_str(it.code)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, ",\"name\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_json_str(out, code_name(it.code)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, ",\"message\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_str_json(out, &it.message) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
|
||||
match ByteBuffer.append(out, ",\"span\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_loc_json(self, it.span, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
|
||||
match ByteBuffer.append(out, ",\"labels\":[") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
var j: usize = 0;
|
||||
while j < it.labels.len {
|
||||
var lab = label_at(it, j);
|
||||
if lab != null {
|
||||
if j != 0 {
|
||||
match ByteBuffer.append_byte(out, ',') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
}
|
||||
match ByteBuffer.append(out, "{\"message\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_str_json(out, &lab.message) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, ",\"primary\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_bool(out, lab.primary) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, ",\"span\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_loc_json(self, lab.span, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append_byte(out, '}') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
}
|
||||
j = j + 1;
|
||||
}
|
||||
match ByteBuffer.append_byte(out, ']') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
|
||||
match ByteBuffer.append(out, ",\"children\":[") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
var c: usize = 0;
|
||||
while c < it.children.len {
|
||||
var ch = child_at(it, c);
|
||||
if ch != null {
|
||||
if c != 0 {
|
||||
match ByteBuffer.append_byte(out, ',') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
}
|
||||
match ByteBuffer.append(out, "{\"severity\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_json_str(out, severity_name(ch.severity)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, ",\"message\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_str_json(out, &ch.message) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, ",\"span\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_loc_json(self, ch.span, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append_byte(out, '}') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
}
|
||||
c = c + 1;
|
||||
}
|
||||
match ByteBuffer.append_byte(out, ']') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
ret ByteBuffer.append_byte(out, '}');
|
||||
}
|
||||
|
||||
fn render_json(self: *Diag, out: *ByteBuffer): Status {
|
||||
match ByteBuffer.append_byte(out, '[') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
var i: usize = 0;
|
||||
while i < self.items.len {
|
||||
var it = item_at(self, i);
|
||||
if it == null { ret .Err(make(.OutOfRange)); }
|
||||
if i != 0 {
|
||||
match ByteBuffer.append_byte(out, ',') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
}
|
||||
match render_item_json(self, it, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
i = i + 1;
|
||||
}
|
||||
ret ByteBuffer.append_byte(out, ']');
|
||||
}
|
||||
|
||||
// ---- 生命周期 ----
|
||||
|
||||
fn drop_item(it: *Item) {
|
||||
Str.drop(&it.message);
|
||||
var j: usize = 0;
|
||||
while j < it.labels.len {
|
||||
var lab = label_at(it, j);
|
||||
if lab != null { Str.drop(&lab.message); }
|
||||
j = j + 1;
|
||||
}
|
||||
Vec.drop(&it.labels);
|
||||
var c: usize = 0;
|
||||
while c < it.children.len {
|
||||
var ch = child_at(it, c);
|
||||
if ch != null { Str.drop(&ch.message); }
|
||||
c = c + 1;
|
||||
}
|
||||
Vec.drop(&it.children);
|
||||
}
|
||||
|
||||
fn clear(self: *Diag) {
|
||||
var i: usize = 0;
|
||||
while i < self.items.len {
|
||||
var it = item_at(self, i);
|
||||
if it != null { drop_item(it); }
|
||||
i = i + 1;
|
||||
}
|
||||
Vec.clear(&self.items);
|
||||
self.errors = 0;
|
||||
self.warnings = 0;
|
||||
self.fatals = 0;
|
||||
}
|
||||
|
||||
fn drop(self: *Diag) {
|
||||
clear(self);
|
||||
Vec.drop(&self.items);
|
||||
self.sources = null;
|
||||
}
|
||||
|
||||
// ---- 测试辅助 ----
|
||||
|
||||
fn view(text: []u8): ByteView {
|
||||
ret ByteView.from_slice(text);
|
||||
}
|
||||
|
||||
fn expect_ok(result: Status) {
|
||||
match result {
|
||||
.Ok => {},
|
||||
.Err(error) => { @assert(false, "unexpected diag error"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn has_bytes(out: *ByteBuffer, needle: []u8): bool {
|
||||
ret ByteView.contains(ByteView.from_slice(ByteBuffer.as_slice(out.*)), view(needle));
|
||||
}
|
||||
|
||||
fn test_collect_render_human() {
|
||||
var sources = set_new();
|
||||
match set_add(&sources, view("a.spl"), view("fn main() {\n y;\n}\n")) {
|
||||
.Ok(index) => {},
|
||||
.Err(error) => { @assert(false, "add source"); },
|
||||
}
|
||||
|
||||
var d = new(&sources);
|
||||
var idx = err(&d, span_make(0, 16, 17), .UndefinedSymbol, "undefined symbol `y`");
|
||||
@assert(idx != no_file(), "emit");
|
||||
expect_ok(attach_label(&d, span_make(0, 16, 17), "not found in scope", false));
|
||||
expect_ok(attach_child(&d, .Note, span_none(), "did you mean `x`?"));
|
||||
|
||||
@assert(count(d) == 1, "count");
|
||||
@assert(error_count(d) == 1 && has_error(d), "errors");
|
||||
@assert(warning_count(d) == 0 && !has_fatal(d), "warnings/fatal");
|
||||
|
||||
var out = ByteBuffer.new();
|
||||
expect_ok(render_human(&d, &out));
|
||||
@assert(has_bytes(&out, "a.spl:2:5: error[E0101]: undefined symbol `y`"), "human header");
|
||||
@assert(has_bytes(&out, " y;"), "human source line");
|
||||
@assert(has_bytes(&out, " ^"), "human caret");
|
||||
@assert(has_bytes(&out, "not found in scope"), "human label");
|
||||
@assert(has_bytes(&out, "note: did you mean `x`?"), "human child");
|
||||
ByteBuffer.drop(&out);
|
||||
|
||||
clear(&d);
|
||||
@assert(count(d) == 0 && error_count(d) == 0, "clear");
|
||||
drop(&d);
|
||||
set_drop(&sources);
|
||||
}
|
||||
|
||||
fn test_render_json() {
|
||||
var sources = set_new();
|
||||
match set_add(&sources, view("b.spl"), view("let z = 1;\n")) {
|
||||
.Ok(index) => {},
|
||||
.Err(error) => { @assert(false, "add source"); },
|
||||
}
|
||||
|
||||
var d = new(&sources);
|
||||
err(&d, span_make(0, 4, 5), .TypeMismatch, "expected i32, got bool");
|
||||
expect_ok(attach_child(&d, .Help, span_none(), "annotate the type"));
|
||||
|
||||
var out = ByteBuffer.new();
|
||||
expect_ok(render_json(&d, &out));
|
||||
@assert(has_bytes(&out, "\"severity\":\"error\""), "json severity");
|
||||
@assert(has_bytes(&out, "\"code\":\"E0103\""), "json code");
|
||||
@assert(has_bytes(&out, "\"name\":\"type_mismatch\""), "json name");
|
||||
@assert(has_bytes(&out, "\"line\":1"), "json line");
|
||||
@assert(has_bytes(&out, "\"col\":5"), "json col");
|
||||
@assert(has_bytes(&out, "\"children\":[{\"severity\":\"help\""), "json child");
|
||||
ByteBuffer.drop(&out);
|
||||
|
||||
drop(&d);
|
||||
set_drop(&sources);
|
||||
}
|
||||
|
||||
fn test_empty_and_headerless() {
|
||||
var d = new(null);
|
||||
err(&d, span_none(), .Internal, "no location");
|
||||
var out = ByteBuffer.new();
|
||||
expect_ok(render_human(&d, &out));
|
||||
@assert(has_bytes(&out, "error[E9000]: no location"), "headerless");
|
||||
ByteBuffer.drop(&out);
|
||||
drop(&d);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
test_collect_render_human();
|
||||
test_render_json();
|
||||
test_empty_and_headerless();
|
||||
}
|
||||
@@ -0,0 +1,719 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// 词法分析器:字节流 -> Vec<Token>。镜像 stage1/spl_lexer.c 的行为:
|
||||
// 行注释作为 token 产出;块注释(可嵌套)整体跳过;换行产出 Endline;EOF 产出 Eof。
|
||||
// 复用 utils/ascii 做字符分类;整型/字符在词法期解码后放进 TokenKind 载荷。
|
||||
// Token.text 借用输入字节,不复制。
|
||||
|
||||
type .{ .Vec, .VecResult } = @import("utils/vec.spl");
|
||||
type .{ .ByteView } = @import("utils/byte_view.spl");
|
||||
type .{ .is_digit, .is_alpha, .is_alnum, .is_hex_digit, .digit_value } = @import("utils/ascii.spl");
|
||||
type .{
|
||||
.Token, .TokenKind, .Radix, .Keyword, .Punct, .keyword_of
|
||||
} = @import("token.spl");
|
||||
type .{ .Span, .span_make } = @import("./diag.spl");
|
||||
type Diag = @import("./diag.spl");
|
||||
|
||||
type Lexer = @self;
|
||||
|
||||
src: ByteView,
|
||||
pos: usize,
|
||||
line: usize,
|
||||
col: usize,
|
||||
diag: *Diag, // 借用;可为 null(合法输入不产生诊断)
|
||||
file: usize,
|
||||
|
||||
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 == '_'; }
|
||||
|
||||
fn is_ident_cont(c: u8): bool { ret is_alnum(c) || c == '_'; }
|
||||
|
||||
fn hex_val(c: u8): u8 {
|
||||
match digit_value(c, 16) {
|
||||
.Ok(v) => { ret v; },
|
||||
.Err(error) => { ret 0; },
|
||||
}
|
||||
ret 0;
|
||||
}
|
||||
|
||||
// 文本形如 'a' / '\n' / '\x41';返回解码后的字节。
|
||||
fn decode_char(text: ByteView): u8 {
|
||||
var start: usize = 1;
|
||||
var end = text.len;
|
||||
if text.len >= 2 && text.ptr[text.len - 1] == '\'' { end = text.len - 1; }
|
||||
if end <= start { ret 0; }
|
||||
|
||||
var b = text.ptr[start];
|
||||
if b != '\\' { ret b; }
|
||||
|
||||
start = start + 1;
|
||||
if start >= end { ret 0; }
|
||||
var e = text.ptr[start];
|
||||
if e == 'n' { ret '\n'; }
|
||||
if e == 't' { ret '\t'; }
|
||||
if e == 'r' { ret '\r'; }
|
||||
if e == '\\' { ret '\\'; }
|
||||
if e == '"' { ret '"'; }
|
||||
if e == '\'' { ret '\''; }
|
||||
if e == '0' { ret 0; }
|
||||
if e == 'x' || e == 'X' {
|
||||
var q = start + 1;
|
||||
var value: u8 = 0;
|
||||
var digits: usize = 0;
|
||||
while q < end && digits < 2 && is_hex_digit(text.ptr[q]) {
|
||||
value = value * 16 + hex_val(text.ptr[q]);
|
||||
q = q + 1;
|
||||
digits = digits + 1;
|
||||
}
|
||||
if digits == 0 { ret e; }
|
||||
ret value;
|
||||
}
|
||||
ret e;
|
||||
}
|
||||
|
||||
// ---- 读取辅助 ----
|
||||
|
||||
fn has2(self: *Lexer, ch: u8): bool {
|
||||
ret self.pos + 1 < self.src.len && self.src.ptr[self.pos + 1] == ch;
|
||||
}
|
||||
|
||||
fn has3(self: *Lexer, ch1: u8, ch2: u8): bool {
|
||||
ret self.pos + 2 < self.src.len && self.src.ptr[self.pos + 1] == ch1 &&
|
||||
self.src.ptr[self.pos + 2] == ch2;
|
||||
}
|
||||
|
||||
fn sub(self: *Lexer, start: usize, end: usize): ByteView {
|
||||
match ByteView.slice(self.src, start, end) {
|
||||
.Ok(v) => { ret v; },
|
||||
.Err(error) => { ret ByteView.empty(); },
|
||||
}
|
||||
ret ByteView.empty();
|
||||
}
|
||||
|
||||
// 产出 token 并把 pos/col 推进到 end。Vec.push 失败视为致命(同 vec.make_vec 风格)。
|
||||
fn emit_to(self: *Lexer, out: *Vec, kind: TokenKind, start: usize, end: usize, line: usize,
|
||||
col: usize) {
|
||||
var t = Token.make(kind, sub(self, start, end), line, col, start);
|
||||
match Vec.push(out, &t as *u8) {
|
||||
.Ok => {},
|
||||
.Err(error) => { @assert(false, "lexer: Vec.push failed"); },
|
||||
}
|
||||
self.pos = end;
|
||||
self.col = col + (end - start);
|
||||
}
|
||||
|
||||
fn op1(self: *Lexer, out: *Vec, punct: Punct, start: usize, line: usize, col: usize) {
|
||||
emit_to(self, out, .Punct(punct), start, start + 1, line, col);
|
||||
}
|
||||
|
||||
fn op2(self: *Lexer, out: *Vec, punct: Punct, start: usize, line: usize, col: usize) {
|
||||
emit_to(self, out, .Punct(punct), start, start + 2, line, col);
|
||||
}
|
||||
|
||||
fn op3(self: *Lexer, out: *Vec, punct: Punct, start: usize, line: usize, col: usize) {
|
||||
emit_to(self, out, .Punct(punct), start, start + 3, line, col);
|
||||
}
|
||||
|
||||
// ---- 复合扫描 ----
|
||||
|
||||
fn scan_int(self: *Lexer, out: *Vec, start: usize, line: usize, col: usize) {
|
||||
var p = self.pos;
|
||||
if self.src.ptr[p] == '0' && p + 1 < self.src.len {
|
||||
var nc = self.src.ptr[p + 1];
|
||||
if nc == 'x' || nc == 'X' {
|
||||
p = p + 2;
|
||||
while p < self.src.len && (is_hex_digit(self.src.ptr[p]) || self.src.ptr[p] == '_') {
|
||||
p = p + 1;
|
||||
}
|
||||
emit_to(self, out, .Int(.Hex), start, p, line, col);
|
||||
ret;
|
||||
}
|
||||
if nc == 'b' || nc == 'B' {
|
||||
p = p + 2;
|
||||
while p < self.src.len && (self.src.ptr[p] == '0' || self.src.ptr[p] == '1' ||
|
||||
self.src.ptr[p] == '_') {
|
||||
p = p + 1;
|
||||
}
|
||||
emit_to(self, out, .Int(.Bin), start, p, line, col);
|
||||
ret;
|
||||
}
|
||||
if nc == 'o' || nc == 'O' {
|
||||
p = p + 2;
|
||||
while p < self.src.len &&
|
||||
((self.src.ptr[p] >= '0' && self.src.ptr[p] <= '7') ||
|
||||
self.src.ptr[p] == '_') {
|
||||
p = p + 1;
|
||||
}
|
||||
emit_to(self, out, .Int(.Oct), start, p, line, col);
|
||||
ret;
|
||||
}
|
||||
}
|
||||
|
||||
while p < self.src.len && (is_digit(self.src.ptr[p]) || self.src.ptr[p] == '_') { p = p + 1; }
|
||||
if p + 1 < self.src.len && self.src.ptr[p] == '.' && is_digit(self.src.ptr[p + 1]) {
|
||||
p = p + 1;
|
||||
// 小数部分允许下划线分隔(Rust 风格)。
|
||||
while p < self.src.len && (is_digit(self.src.ptr[p]) || self.src.ptr[p] == '_') {
|
||||
p = p + 1;
|
||||
}
|
||||
// 可选指数:e/E [+-] 数字(数字允许下划线);仅当确有数字时纳入。
|
||||
if p < self.src.len && (self.src.ptr[p] == 'e' || self.src.ptr[p] == 'E') {
|
||||
var q = p + 1;
|
||||
if q < self.src.len && (self.src.ptr[q] == '+' || self.src.ptr[q] == '-') { q = q + 1; }
|
||||
if q < self.src.len && is_digit(self.src.ptr[q]) {
|
||||
p = q + 1;
|
||||
while p < self.src.len && (is_digit(self.src.ptr[p]) || self.src.ptr[p] == '_') {
|
||||
p = p + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
emit_to(self, out, .Float, start, p, line, col);
|
||||
ret;
|
||||
}
|
||||
emit_to(self, out, .Int(.Dec), start, p, line, col);
|
||||
}
|
||||
|
||||
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;
|
||||
if p < self.src.len && (self.src.ptr[p] == 'x' || self.src.ptr[p] == 'X') {
|
||||
p = p + 1;
|
||||
var digits: usize = 0;
|
||||
while p < self.src.len && digits < 2 && is_hex_digit(self.src.ptr[p]) {
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
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 == '\\' {
|
||||
p = p + 1;
|
||||
cc = cc + 1;
|
||||
if p < self.src.len {
|
||||
p = p + 1;
|
||||
cc = cc + 1;
|
||||
}
|
||||
} else if b == '"' {
|
||||
p = p + 1;
|
||||
cc = cc + 1;
|
||||
closed = true;
|
||||
break;
|
||||
} else if b == '\n' {
|
||||
l = l + 1;
|
||||
cc = 1;
|
||||
p = p + 1;
|
||||
} else {
|
||||
p = p + 1;
|
||||
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;
|
||||
}
|
||||
|
||||
fn scan_ident(self: *Lexer, out: *Vec, start: usize, line: usize, col: usize) {
|
||||
var p = self.pos;
|
||||
while p < self.src.len && is_ident_cont(self.src.ptr[p]) { p = p + 1; }
|
||||
var text = sub(self, start, p);
|
||||
match keyword_of(text) {
|
||||
.Yes(kw) => { emit_to(self, out, .Keyword(kw), start, p, line, col); },
|
||||
.No => { emit_to(self, out, .Ident, start, p, line, col); },
|
||||
}
|
||||
}
|
||||
|
||||
// 块注释:可嵌套,整体跳过(不产出 token)。未闭合时报诊断。
|
||||
fn skip_block_comment(self: *Lexer, start: usize) {
|
||||
self.pos = self.pos + 2;
|
||||
self.col = self.col + 2;
|
||||
var depth: usize = 1;
|
||||
while self.pos + 1 < self.src.len && depth > 0 {
|
||||
if self.src.ptr[self.pos] == '*' && self.src.ptr[self.pos + 1] == '/' {
|
||||
depth = depth - 1;
|
||||
self.pos = self.pos + 2;
|
||||
self.col = self.col + 2;
|
||||
} else if self.src.ptr[self.pos] == '/' && self.src.ptr[self.pos + 1] == '*' {
|
||||
depth = depth + 1;
|
||||
self.pos = self.pos + 2;
|
||||
self.col = self.col + 2;
|
||||
} else {
|
||||
if self.src.ptr[self.pos] == '\n' {
|
||||
self.line = self.line + 1;
|
||||
self.col = 1;
|
||||
} else {
|
||||
self.col = self.col + 1;
|
||||
}
|
||||
self.pos = self.pos + 1;
|
||||
}
|
||||
}
|
||||
if depth > 0 {
|
||||
report(self, start, self.src.len, "unterminated block comment");
|
||||
}
|
||||
}
|
||||
|
||||
fn run(self: *Lexer, out: *Vec) {
|
||||
while self.pos < self.src.len {
|
||||
var start = self.pos;
|
||||
var line = self.line;
|
||||
var col = self.col;
|
||||
var c = self.src.ptr[self.pos];
|
||||
|
||||
if c == ' ' || c == '\t' || c == '\r' {
|
||||
self.pos = self.pos + 1;
|
||||
self.col = self.col + 1;
|
||||
continue;
|
||||
}
|
||||
if c == '\n' {
|
||||
emit_to(self, out, .Endline, start, start + 1, line, col);
|
||||
self.line = line + 1;
|
||||
self.col = 1;
|
||||
continue;
|
||||
}
|
||||
if c == '/' && has2(self, '/') {
|
||||
var p = self.pos;
|
||||
while p < self.src.len && self.src.ptr[p] != '\n' { p = p + 1; }
|
||||
emit_to(self, out, .LineComment, start, p, line, col);
|
||||
continue;
|
||||
}
|
||||
if c == '/' && has2(self, '*') {
|
||||
skip_block_comment(self, start);
|
||||
continue;
|
||||
}
|
||||
if c == '\'' {
|
||||
scan_char(self, out, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if c == '"' {
|
||||
scan_string(self, out, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if is_ident_start(c) {
|
||||
scan_ident(self, out, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if is_digit(c) {
|
||||
scan_int(self, out, start, line, col);
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '<' {
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
if c == '>' {
|
||||
if has3(self, '>', '=') { op3(self, out, .RShAssign, start, line, col); continue; }
|
||||
if has2(self, '>') { op2(self, out, .RSh, start, line, col); continue; }
|
||||
if has2(self, '=') { op2(self, out, .Ge, start, line, col); continue; }
|
||||
op1(self, out, .Gt, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if c == '.' {
|
||||
if has3(self, '.', '.') { op3(self, out, .Ellipsis, start, line, col); continue; }
|
||||
if has2(self, '.') { op2(self, out, .Range, start, line, col); continue; }
|
||||
op1(self, out, .Dot, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if c == '=' {
|
||||
if has2(self, '=') { op2(self, out, .Eq, start, line, col); continue; }
|
||||
if has2(self, '>') { op2(self, out, .RightFatArrow, start, line, col); continue; }
|
||||
op1(self, out, .Assign, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if c == '!' {
|
||||
if has2(self, '=') { op2(self, out, .Neq, start, line, col); continue; }
|
||||
op1(self, out, .Not, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if c == '&' {
|
||||
if has2(self, '&') { op2(self, out, .AndAnd, start, line, col); continue; }
|
||||
if has2(self, '=') { op2(self, out, .AndAssign, start, line, col); continue; }
|
||||
op1(self, out, .And, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if c == '|' {
|
||||
if has2(self, '|') { op2(self, out, .OrOr, start, line, col); continue; }
|
||||
if has2(self, '=') { op2(self, out, .OrAssign, start, line, col); continue; }
|
||||
op1(self, out, .Or, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if c == '+' {
|
||||
if has2(self, '=') { op2(self, out, .AddAssign, start, line, col); continue; }
|
||||
op1(self, out, .Add, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if c == '-' {
|
||||
if has2(self, '=') { op2(self, out, .SubAssign, start, line, col); continue; }
|
||||
if has2(self, '>') { op2(self, out, .RightArrow, start, line, col); continue; }
|
||||
op1(self, out, .Sub, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if c == '*' {
|
||||
if has2(self, '=') { op2(self, out, .MulAssign, start, line, col); continue; }
|
||||
op1(self, out, .Mul, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if c == '/' {
|
||||
if has2(self, '=') { op2(self, out, .DivAssign, start, line, col); continue; }
|
||||
op1(self, out, .Div, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if c == '%' {
|
||||
if has2(self, '=') { op2(self, out, .ModAssign, start, line, col); continue; }
|
||||
op1(self, out, .Mod, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if c == '^' {
|
||||
if has2(self, '=') { op2(self, out, .XorAssign, start, line, col); continue; }
|
||||
op1(self, out, .Xor, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if c == ':' {
|
||||
if has2(self, '=') { op2(self, out, .ColonAssign, start, line, col); continue; }
|
||||
op1(self, out, .Colon, start, line, col);
|
||||
continue;
|
||||
}
|
||||
if c == '~' { op1(self, out, .BitNot, start, line, col); continue; }
|
||||
if c == ',' { op1(self, out, .Comma, start, line, col); continue; }
|
||||
if c == ';' { op1(self, out, .Semicolon, start, line, col); continue; }
|
||||
if c == '(' { op1(self, out, .LParen, start, line, col); continue; }
|
||||
if c == ')' { op1(self, out, .RParen, start, line, col); continue; }
|
||||
if c == '[' { op1(self, out, .LBracket, start, line, col); continue; }
|
||||
if c == ']' { op1(self, out, .RBracket, start, line, col); continue; }
|
||||
if c == '{' { op1(self, out, .LBrace, start, line, col); continue; }
|
||||
if c == '}' { op1(self, out, .RBrace, start, line, col); continue; }
|
||||
if c == '#' { op1(self, out, .Sharp, start, line, col); continue; }
|
||||
if c == '@' { op1(self, out, .At, start, line, col); continue; }
|
||||
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);
|
||||
}
|
||||
|
||||
var end = self.src.len;
|
||||
emit_to(self, out, .Eof, end, end, self.line, self.col);
|
||||
}
|
||||
|
||||
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, diag, file);
|
||||
run(&self, &out);
|
||||
ret .Ok(out);
|
||||
}
|
||||
|
||||
// ---- 测试辅助 ----
|
||||
|
||||
fn view(text: []u8): ByteView {
|
||||
ret ByteView.from_slice(text);
|
||||
}
|
||||
|
||||
fn lex_ok(src: []u8): Vec {
|
||||
var out = Vec.blank(@sizeof(Token), @alignof(Token));
|
||||
match lex(view(src), null, 0) {
|
||||
.Ok(v) => { out = v; },
|
||||
.Err(error) => { @assert(false, "lex failed"); },
|
||||
}
|
||||
ret out;
|
||||
}
|
||||
|
||||
fn at(v: *Vec, index: usize): Token {
|
||||
match Vec.get(v, index) {
|
||||
.Ok(ptr) => { ret (ptr as *Token).*; },
|
||||
.Err(error) => { ret Token.make(.Unknown(0), ByteView.empty(), 0, 0, 0); },
|
||||
}
|
||||
ret Token.make(.Unknown(0), ByteView.empty(), 0, 0, 0);
|
||||
}
|
||||
|
||||
fn expect_punct(tok: Token, expected: Punct) {
|
||||
match tok.kind {
|
||||
.Punct(p) => { @assert(@enum2int(p) == @enum2int(expected), "punct kind"); },
|
||||
_ => { @assert(false, "expected punct"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_keyword(tok: Token, expected: Keyword) {
|
||||
match tok.kind {
|
||||
.Keyword(k) => { @assert(@enum2int(k) == @enum2int(expected), "keyword kind"); },
|
||||
_ => { @assert(false, "expected keyword"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_ident(tok: Token) {
|
||||
match tok.kind {
|
||||
.Ident => {},
|
||||
_ => { @assert(false, "expected ident"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_int(tok: Token, radix: Radix) {
|
||||
match tok.kind {
|
||||
.Int(r) => {
|
||||
@assert(@enum2int(r) == @enum2int(radix), "int radix");
|
||||
},
|
||||
_ => { @assert(false, "expected int"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_char(tok: Token, value: u8) {
|
||||
match tok.kind {
|
||||
.Char(v) => { @assert(v == value, "char value"); },
|
||||
_ => { @assert(false, "expected char"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_endline(tok: Token) {
|
||||
match tok.kind {
|
||||
.Endline => {},
|
||||
_ => { @assert(false, "expected endline"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_eof(tok: Token) {
|
||||
match tok.kind {
|
||||
.Eof => {},
|
||||
_ => { @assert(false, "expected eof"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_text(tok: Token, literal: []u8) {
|
||||
@assert(ByteView.eq(tok.text, view(literal)), "token text");
|
||||
}
|
||||
|
||||
fn expect_pos(tok: Token, line: usize, col: usize) {
|
||||
@assert(tok.line == line && tok.col == col, "token position");
|
||||
}
|
||||
|
||||
fn test_basic() {
|
||||
var v = lex_ok("fn main() {\n ret 0;\n}\n");
|
||||
defer { Vec.drop(&v); }
|
||||
@assert(v.len == 13, "basic token count");
|
||||
|
||||
var t0 = at(&v, 0);
|
||||
expect_keyword(t0, .Fn);
|
||||
expect_text(t0, "fn");
|
||||
expect_pos(t0, 1, 1);
|
||||
expect_ident(at(&v, 1));
|
||||
expect_text(at(&v, 1), "main");
|
||||
expect_punct(at(&v, 2), .LParen);
|
||||
expect_punct(at(&v, 3), .RParen);
|
||||
expect_punct(at(&v, 4), .LBrace);
|
||||
expect_endline(at(&v, 5));
|
||||
expect_keyword(at(&v, 6), .Ret);
|
||||
expect_pos(at(&v, 6), 2, 5);
|
||||
expect_int(at(&v, 7), .Dec);
|
||||
expect_punct(at(&v, 8), .Semicolon);
|
||||
expect_endline(at(&v, 9));
|
||||
expect_punct(at(&v, 10), .RBrace);
|
||||
expect_endline(at(&v, 11));
|
||||
expect_eof(at(&v, 12));
|
||||
}
|
||||
|
||||
fn test_operators() {
|
||||
var v = lex_ok("<-> <- -> <=> <<= << >>= ... .. . := == != < <= > >= && || & | ^ + - * / % ~ ! ? # @ $ , ; : ( ) [ ] { }");
|
||||
defer { Vec.drop(&v); }
|
||||
@assert(v.len == 43, "operator token count");
|
||||
|
||||
expect_punct(at(&v, 0), .ArrowBoth);
|
||||
expect_text(at(&v, 0), "<->");
|
||||
expect_punct(at(&v, 1), .LeftArrow);
|
||||
expect_punct(at(&v, 2), .RightArrow);
|
||||
expect_punct(at(&v, 3), .FatArrowBoth);
|
||||
expect_punct(at(&v, 4), .LShAssign);
|
||||
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() {
|
||||
var v = lex_ok("// c\n/* a /* nested */ b */\n'a' '\\n' '\\x41' \"hi\\n\" 0xFF 0b1010 0o17 1_000 3.14");
|
||||
defer { Vec.drop(&v); }
|
||||
@assert(v.len == 13, "literal token count");
|
||||
|
||||
var comment = at(&v, 0);
|
||||
match comment.kind {
|
||||
.LineComment => {},
|
||||
_ => { @assert(false, "expected line comment"); },
|
||||
}
|
||||
expect_text(comment, "// c");
|
||||
expect_pos(comment, 1, 1);
|
||||
expect_endline(at(&v, 1));
|
||||
expect_endline(at(&v, 2));
|
||||
|
||||
var ch3 = at(&v, 3);
|
||||
expect_char(ch3, 'a');
|
||||
@assert(ByteView.eq(ch3.text, view("'a'")), "char text");
|
||||
expect_pos(ch3, 3, 1);
|
||||
expect_char(at(&v, 4), '\n');
|
||||
expect_char(at(&v, 5), 0x41);
|
||||
|
||||
var s = at(&v, 6);
|
||||
match s.kind {
|
||||
.Str => {},
|
||||
_ => { @assert(false, "expected string"); },
|
||||
}
|
||||
expect_text(s, "\"hi\\n\"");
|
||||
|
||||
expect_int(at(&v, 7), .Hex);
|
||||
expect_int(at(&v, 8), .Bin);
|
||||
expect_int(at(&v, 9), .Oct);
|
||||
expect_int(at(&v, 10), .Dec);
|
||||
|
||||
var fl = at(&v, 11);
|
||||
match fl.kind {
|
||||
.Float => {},
|
||||
_ => { @assert(false, "expected float"); },
|
||||
}
|
||||
expect_eof(at(&v, 12));
|
||||
}
|
||||
|
||||
// 非法输入走 diag:仍产出 token 以便继续收集错误,但 diag 必须记录。
|
||||
// Rust 风格数字字面量:分隔符下划线 + 多进制 + 小数/指数字面量。
|
||||
fn test_number_forms() {
|
||||
var v = lex_ok("1_000_000 0xFF_FF 0x_ff 0b_1010 0o_7 1_000.5 1.5_0 1_000.500 1.5e-3 1_0.5_0E2");
|
||||
defer { Vec.drop(&v); }
|
||||
@assert(v.len == 11, "number token count");
|
||||
|
||||
expect_int(at(&v, 0), .Dec);
|
||||
expect_text(at(&v, 0), "1_000_000");
|
||||
expect_int(at(&v, 1), .Hex);
|
||||
expect_text(at(&v, 1), "0xFF_FF");
|
||||
expect_int(at(&v, 2), .Hex);
|
||||
expect_text(at(&v, 2), "0x_ff");
|
||||
expect_int(at(&v, 3), .Bin);
|
||||
expect_text(at(&v, 3), "0b_1010");
|
||||
expect_int(at(&v, 4), .Oct);
|
||||
expect_text(at(&v, 4), "0o_7");
|
||||
|
||||
var i: usize = 5;
|
||||
while i < 10 {
|
||||
match at(&v, i).kind {
|
||||
.Float => {},
|
||||
_ => { @assert(false, "expected float"); },
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
expect_text(at(&v, 5), "1_000.5");
|
||||
expect_text(at(&v, 6), "1.5_0");
|
||||
expect_text(at(&v, 7), "1_000.500");
|
||||
expect_text(at(&v, 8), "1.5e-3");
|
||||
expect_text(at(&v, 9), "1_0.5_0E2");
|
||||
expect_eof(at(&v, 10));
|
||||
}
|
||||
|
||||
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_number_forms();
|
||||
test_diag_errors();
|
||||
}
|
||||
+2192
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,239 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// splc1 (stage2 自举编译器入口)。
|
||||
// 当前仅实现 `--dump lex <file> [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 Ast = @import("./ast.spl");
|
||||
type Parser = @import("./parser.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, 0); },
|
||||
}
|
||||
ret Token.make(.Unknown(0), ByteView.empty(), 0, 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] <lexeme> (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 dump_ast(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); }
|
||||
|
||||
var ast: Ast = undefined;
|
||||
match Ast.new(&sources) {
|
||||
.Ok(v) => { ast = v; },
|
||||
.Err(error) => {
|
||||
Logger.error(log, "ast init failed");
|
||||
ret 1;
|
||||
},
|
||||
}
|
||||
defer { Ast.drop(&ast); }
|
||||
|
||||
var p = Parser.new(&ast, tokens, &diag);
|
||||
Parser.parse(&p);
|
||||
|
||||
if Diag.has_error(diag) || p.failed {
|
||||
var rendered = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&rendered); }
|
||||
match Diag.render_human(&diag, &rendered) {
|
||||
.Ok => {},
|
||||
.Err(error) => {},
|
||||
}
|
||||
eprint(ByteBuffer.as_slice(rendered));
|
||||
ret 1;
|
||||
}
|
||||
|
||||
var out = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&out); }
|
||||
if !Ast.dump(&ast, &out) {
|
||||
Logger.fatal(log, "out of memory while dumping ast");
|
||||
ret 1;
|
||||
}
|
||||
match print(ByteBuffer.as_slice(out)) {
|
||||
.Ok => {},
|
||||
.Err(error) => { 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 <lex|ast> <file> [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 <lex|ast> <file> [out]");
|
||||
ret 1;
|
||||
}
|
||||
if ByteView.eq(mode, ByteView.from_slice("lex")) {
|
||||
ret dump_lex(&log, path);
|
||||
}
|
||||
if ByteView.eq(mode, ByteView.from_slice("ast")) {
|
||||
ret dump_ast(&log, path);
|
||||
}
|
||||
Logger.fatal(&log, "dump mode not implemented");
|
||||
ret 1;
|
||||
}
|
||||
+2
-2
@@ -34,12 +34,12 @@ fn write_byte(f: *File, b: u8): u64 {
|
||||
ret write(f, &tmp, 1);
|
||||
}
|
||||
|
||||
fn read(f: *File, buf: *u8, n: u64): u64 {
|
||||
fn read(f: *File, buf: *u8, n: usize): usize {
|
||||
if f.handle == null { ret 0; }
|
||||
ret vm_fread(buf as *any, 1, n, f.handle);
|
||||
}
|
||||
|
||||
fn size(f: *File): u64 {
|
||||
fn size(f: *File): usize {
|
||||
if f.handle == null { ret 0; }
|
||||
ret vm_fsize(f.handle);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// 词法单元:用带载荷 ADT 表达 TokenKind;关键词/算符各自独立 enum。
|
||||
// 整型与字符在词法期解码并随载荷携带;文本一律借用源切片(不复制)。
|
||||
|
||||
type .{ .ByteView } = @import("utils/byte_view.spl");
|
||||
|
||||
type Keyword = enum {
|
||||
Any, As, Bool, Break, Catch, Comptime, Const, Continue, Defer, Else, Enum,
|
||||
Errdefer, False, Fn, For, If, Loop, Match, Null, Ret, Shape, Struct, True,
|
||||
Try, Type, Undefined, Union, Var, While, Underscore,
|
||||
}
|
||||
|
||||
type KeywordResult = enum {
|
||||
Yes: Keyword,
|
||||
No,
|
||||
}
|
||||
|
||||
type Punct = enum {
|
||||
Eq, Assign, AddAssign, Add, SubAssign, Sub, MulAssign, Mul, DivAssign, Div,
|
||||
ModAssign, Mod, RightArrow, LeftArrow, ArrowBoth, FatArrowBoth, RightFatArrow,
|
||||
AndAnd, AndAssign, And, OrOr, OrAssign, Or, XorAssign, Xor,
|
||||
LShAssign, LSh, Le, Lt, RShAssign, RSh, Ge, Gt, Not, Neq, BitNot,
|
||||
LBracket, RBracket, LParen, RParen, LBrace, RBrace,
|
||||
Semicolon, Comma, Colon, ColonAssign, Dot, Range, Ellipsis, Cond, Sharp, At, Dollar,
|
||||
}
|
||||
|
||||
type Radix = enum {
|
||||
Dec,
|
||||
Bin,
|
||||
Oct,
|
||||
Hex,
|
||||
}
|
||||
|
||||
type TokenKind = enum {
|
||||
Keyword: Keyword,
|
||||
Punct: Punct,
|
||||
Ident,
|
||||
Int: Radix, // 仅记进制;数值由 parser 按任意宽度(mp)解析
|
||||
Float,
|
||||
Char: u8,
|
||||
Str,
|
||||
LineComment,
|
||||
Endline,
|
||||
Eof,
|
||||
Unknown: u8,
|
||||
}
|
||||
|
||||
type Token = @self;
|
||||
|
||||
kind: TokenKind,
|
||||
text: ByteView,
|
||||
line: usize,
|
||||
col: usize,
|
||||
offset: usize, // 源文件字节偏移(供 AST Span 使用)
|
||||
|
||||
fn make(kind: TokenKind, text: ByteView, line: usize, col: usize, offset: usize): Token {
|
||||
ret Token { .kind = kind, .text = text, .line = line, .col = col, .offset = offset };
|
||||
}
|
||||
|
||||
fn is_trivia(kind: TokenKind): bool {
|
||||
match kind {
|
||||
.Endline => { ret true; },
|
||||
.LineComment => { ret true; },
|
||||
_ => { ret false; },
|
||||
}
|
||||
}
|
||||
|
||||
fn keyword_of(text: ByteView): KeywordResult {
|
||||
if is(text, "any") { ret .Yes(.Any); }
|
||||
if is(text, "as") { ret .Yes(.As); }
|
||||
if is(text, "bool") { ret .Yes(.Bool); }
|
||||
if is(text, "break") { ret .Yes(.Break); }
|
||||
if is(text, "catch") { ret .Yes(.Catch); }
|
||||
if is(text, "comptime") { ret .Yes(.Comptime); }
|
||||
if is(text, "const") { ret .Yes(.Const); }
|
||||
if is(text, "continue") { ret .Yes(.Continue); }
|
||||
if is(text, "defer") { ret .Yes(.Defer); }
|
||||
if is(text, "else") { ret .Yes(.Else); }
|
||||
if is(text, "enum") { ret .Yes(.Enum); }
|
||||
if is(text, "errdefer") { ret .Yes(.Errdefer); }
|
||||
if is(text, "false") { ret .Yes(.False); }
|
||||
if is(text, "fn") { ret .Yes(.Fn); }
|
||||
if is(text, "for") { ret .Yes(.For); }
|
||||
if is(text, "if") { ret .Yes(.If); }
|
||||
if is(text, "loop") { ret .Yes(.Loop); }
|
||||
if is(text, "match") { ret .Yes(.Match); }
|
||||
if is(text, "null") { ret .Yes(.Null); }
|
||||
if is(text, "ret") { ret .Yes(.Ret); }
|
||||
if is(text, "shape") { ret .Yes(.Shape); }
|
||||
if is(text, "struct") { ret .Yes(.Struct); }
|
||||
if is(text, "true") { ret .Yes(.True); }
|
||||
if is(text, "try") { ret .Yes(.Try); }
|
||||
if is(text, "type") { ret .Yes(.Type); }
|
||||
if is(text, "undefined") { ret .Yes(.Undefined); }
|
||||
if is(text, "union") { ret .Yes(.Union); }
|
||||
if is(text, "var") { ret .Yes(.Var); }
|
||||
if is(text, "while") { ret .Yes(.While); }
|
||||
if is(text, "_") { ret .Yes(.Underscore); }
|
||||
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 {
|
||||
ret ByteView.from_slice(text);
|
||||
}
|
||||
|
||||
fn is(text: ByteView, literal: []u8): bool {
|
||||
ret ByteView.eq(text, view(literal));
|
||||
}
|
||||
|
||||
fn expect_keyword(result: KeywordResult, expected: Keyword) {
|
||||
match result {
|
||||
.Yes(kw) => { @assert(@enum2int(kw) == @enum2int(expected), "keyword"); },
|
||||
.No => { @assert(false, "expected keyword"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn test_keywords() {
|
||||
expect_keyword(keyword_of(view("fn")), .Fn);
|
||||
expect_keyword(keyword_of(view("while")), .While);
|
||||
expect_keyword(keyword_of(view("_")), .Underscore);
|
||||
match keyword_of(view("foo")) {
|
||||
.Yes(kw) => { @assert(false, "foo is not a keyword"); },
|
||||
.No => {},
|
||||
}
|
||||
match keyword_of(view("")) {
|
||||
.Yes(kw) => { @assert(false, "empty is not a keyword"); },
|
||||
.No => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn test_adt_payloads() {
|
||||
var t = make(.Int(.Hex), view("0xFF"), 3, 5, 0);
|
||||
@assert(t.line == 3 && t.col == 5, "position");
|
||||
match t.kind {
|
||||
.Int(radix) => {
|
||||
var hex: Radix = .Hex;
|
||||
@assert(@enum2int(radix) == @enum2int(hex), "int radix");
|
||||
},
|
||||
_ => { @assert(false, "expected int"); },
|
||||
}
|
||||
|
||||
var c = make(.Char('a'), view("'a'"), 1, 1, 0);
|
||||
match c.kind {
|
||||
.Char(value) => { @assert(value == 'a', "char value"); },
|
||||
_ => { @assert(false, "expected char"); },
|
||||
}
|
||||
|
||||
var p = make(.Punct(.ArrowBoth), view("<->"), 1, 1, 0);
|
||||
match p.kind {
|
||||
.Punct(punct) => {
|
||||
var both: Punct = .ArrowBoth;
|
||||
@assert(@enum2int(punct) == @enum2int(both), "punct");
|
||||
},
|
||||
_ => { @assert(false, "expected punct"); },
|
||||
}
|
||||
|
||||
var k = make(.Keyword(.Ret), view("ret"), 1, 1, 0);
|
||||
match k.kind {
|
||||
.Keyword(kw) => {
|
||||
var ret_kw: Keyword = .Ret;
|
||||
@assert(@enum2int(kw) == @enum2int(ret_kw), "keyword kind");
|
||||
},
|
||||
_ => { @assert(false, "expected keyword"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn test_trivia() {
|
||||
@assert(is_trivia(.Endline), "endline trivia");
|
||||
@assert(is_trivia(.LineComment), "comment trivia");
|
||||
@assert(!is_trivia(.Ident), "ident not 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(.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();
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// 类型擦除的搜索与稳定归并排序。元素由定长字节表示,比较回调负责解释元素和键;
|
||||
// context 只借用,模块不持有元素、键或上下文。所有尺寸错误都在写入前返回。
|
||||
type .{ .Error, .Status, .make } = @import("error.spl");
|
||||
type .{ .checked_mul } = @import("memory.spl");
|
||||
type .{ .alloc, .copy, .free } = @import("../sys/mem.spl");
|
||||
|
||||
type SearchResult = enum {
|
||||
Found: usize,
|
||||
NotFound,
|
||||
Err: Error,
|
||||
}
|
||||
|
||||
fn validate_range(data: *u8, len: usize, elem_size: usize): Status {
|
||||
if elem_size == 0 { ret .Err(make(.InvalidArgument)); }
|
||||
match checked_mul(len, elem_size) {
|
||||
.Ok(bytes) => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
if len != 0 && data == null { ret .Err(make(.InvalidArgument)); }
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
// 返回首个相等元素。空范围不读取 data、key 或调用 compare。
|
||||
fn linear_search(data: *u8, len: usize, elem_size: usize, key: *u8,
|
||||
context: *u8, compare: fn(*u8, *u8, *u8): i32): SearchResult {
|
||||
match validate_range(data, len, elem_size) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
if len == 0 { ret .NotFound; }
|
||||
if key == null { ret .Err(make(.InvalidArgument)); }
|
||||
|
||||
var i: usize = 0;
|
||||
while i < len {
|
||||
if compare(data + i * elem_size, key, context) == 0 {
|
||||
ret .Found(i);
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
ret .NotFound;
|
||||
}
|
||||
|
||||
// data 必须按 compare 升序排列。存在重复值时返回首个相等元素。
|
||||
fn binary_search(data: *u8, len: usize, elem_size: usize, key: *u8,
|
||||
context: *u8, compare: fn(*u8, *u8, *u8): i32): SearchResult {
|
||||
match validate_range(data, len, elem_size) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
if len == 0 { ret .NotFound; }
|
||||
if key == null { ret .Err(make(.InvalidArgument)); }
|
||||
|
||||
var low: usize = 0;
|
||||
var high = len;
|
||||
while low < high {
|
||||
var mid = low + (high - low) / 2;
|
||||
if compare(data + mid * elem_size, key, context) < 0 {
|
||||
low = mid + 1;
|
||||
} else {
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
if low < len && compare(data + low * elem_size, key, context) == 0 {
|
||||
ret .Found(low);
|
||||
}
|
||||
ret .NotFound;
|
||||
}
|
||||
|
||||
fn min_size(a: usize, b: usize): usize {
|
||||
if a < b { ret a; }
|
||||
ret b;
|
||||
}
|
||||
|
||||
fn merge_run(dst: *u8, src: *u8, elem_size: usize,
|
||||
begin: usize, middle: usize, end: usize,
|
||||
context: *u8, compare: fn(*u8, *u8, *u8): i32) {
|
||||
var left = begin;
|
||||
var right = middle;
|
||||
var output = begin;
|
||||
while left < middle && right < end {
|
||||
var left_ptr = src + left * elem_size;
|
||||
var right_ptr = src + right * elem_size;
|
||||
// 相等时先取左侧元素,由此保证稳定性。
|
||||
if compare(left_ptr, right_ptr, context) <= 0 {
|
||||
copy((dst + output * elem_size) as *any, left_ptr as *any, elem_size);
|
||||
left = left + 1;
|
||||
} else {
|
||||
copy((dst + output * elem_size) as *any, right_ptr as *any, elem_size);
|
||||
right = right + 1;
|
||||
}
|
||||
output = output + 1;
|
||||
}
|
||||
while left < middle {
|
||||
copy((dst + output * elem_size) as *any,
|
||||
(src + left * elem_size) as *any, elem_size);
|
||||
left = left + 1;
|
||||
output = output + 1;
|
||||
}
|
||||
while right < end {
|
||||
copy((dst + output * elem_size) as *any,
|
||||
(src + right * elem_size) as *any, elem_size);
|
||||
right = right + 1;
|
||||
output = output + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 自底向上的稳定归并排序。分配失败及参数错误不改变输入。
|
||||
fn stable_sort(data: *u8, len: usize, elem_size: usize,
|
||||
context: *u8, compare: fn(*u8, *u8, *u8): i32): Status {
|
||||
var bytes: usize = 0;
|
||||
match validate_range(data, len, elem_size) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
match checked_mul(len, elem_size) {
|
||||
.Ok(value) => { bytes = value; },
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
if len < 2 { ret .Ok; }
|
||||
|
||||
var scratch = alloc(bytes) as *u8;
|
||||
if scratch == null { ret .Err(make(.Overflow)); }
|
||||
defer { free(scratch as *any); }
|
||||
|
||||
var source = data;
|
||||
var destination = scratch;
|
||||
var width: usize = 1;
|
||||
while width < len {
|
||||
var begin: usize = 0;
|
||||
while begin < len {
|
||||
var left_width = min_size(width, len - begin);
|
||||
var middle = begin + left_width;
|
||||
var remaining = len - middle;
|
||||
var right_width = min_size(width, remaining);
|
||||
var end = middle + right_width;
|
||||
merge_run(destination, source, elem_size, begin, middle, end,
|
||||
context, compare);
|
||||
begin = end;
|
||||
}
|
||||
var swap = source;
|
||||
source = destination;
|
||||
destination = swap;
|
||||
if width > len / 2 {
|
||||
width = len;
|
||||
} else {
|
||||
width = width * 2;
|
||||
}
|
||||
}
|
||||
|
||||
if source != data {
|
||||
copy(data as *any, source as *any, bytes);
|
||||
}
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
// ---- 测试 ----
|
||||
|
||||
type Item = struct {
|
||||
key: i32,
|
||||
sequence: i32,
|
||||
}
|
||||
|
||||
fn compare_i32(a: *u8, b: *u8, context: *u8): i32 {
|
||||
var av = (a as *i32).*;
|
||||
var bv = (b as *i32).*;
|
||||
if av < bv { ret -1; }
|
||||
if av > bv { ret 1; }
|
||||
ret 0;
|
||||
}
|
||||
|
||||
fn compare_item(a: *u8, b: *u8, context: *u8): i32 {
|
||||
var av = (a as *Item).*.key;
|
||||
var bv = (b as *Item).*.key;
|
||||
var direction: i32 = 1;
|
||||
if context != null { direction = (context as *i32).*; }
|
||||
if av < bv { ret -direction; }
|
||||
if av > bv { ret direction; }
|
||||
ret 0;
|
||||
}
|
||||
|
||||
fn expect_ok(result: Status) {
|
||||
match result {
|
||||
.Ok => {},
|
||||
.Err(error) => { @assert(false, "unexpected algorithm error"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_found(result: SearchResult, expected: usize) {
|
||||
match result {
|
||||
.Found(index) => { @assert(index == expected, "search index"); },
|
||||
.NotFound => { @assert(false, "expected search match"); },
|
||||
.Err(error) => { @assert(false, "unexpected search error"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_missing(result: SearchResult) {
|
||||
match result {
|
||||
.Found(index) => { @assert(false, "unexpected search match"); },
|
||||
.NotFound => {},
|
||||
.Err(error) => { @assert(false, "unexpected search error"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_search_error(result: SearchResult, overflow: bool) {
|
||||
match result {
|
||||
.Found(index) => { @assert(false, "expected search error"); },
|
||||
.NotFound => { @assert(false, "expected search error"); },
|
||||
.Err(error) => {
|
||||
match error.kind {
|
||||
.Overflow => { @assert(overflow); },
|
||||
.InvalidArgument => { @assert(!overflow); },
|
||||
_ => { @assert(false, "unexpected search error kind"); },
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn test_search() {
|
||||
var values: [8]i32 = .[1, 2, 2, 2, 5, 8, 13, 21];
|
||||
var two: i32 = 2;
|
||||
var seven: i32 = 7;
|
||||
expect_found(linear_search(&values[0] as *u8, 8, @sizeof(i32),
|
||||
&two as *u8, null, compare_i32), 1);
|
||||
expect_found(binary_search(&values[0] as *u8, 8, @sizeof(i32),
|
||||
&two as *u8, null, compare_i32), 1);
|
||||
expect_missing(linear_search(&values[0] as *u8, 8, @sizeof(i32),
|
||||
&seven as *u8, null, compare_i32));
|
||||
expect_missing(binary_search(&values[0] as *u8, 8, @sizeof(i32),
|
||||
&seven as *u8, null, compare_i32));
|
||||
|
||||
expect_missing(linear_search(null, 0, @sizeof(i32), null, null, compare_i32));
|
||||
expect_missing(binary_search(null, 0, @sizeof(i32), null, null, compare_i32));
|
||||
expect_search_error(linear_search(&values[0] as *u8, 8, 0,
|
||||
&two as *u8, null, compare_i32), false);
|
||||
expect_search_error(binary_search(null, 1, @sizeof(i32),
|
||||
&two as *u8, null, compare_i32), false);
|
||||
var huge = (~(0 as usize)) / @sizeof(i32) + 1;
|
||||
expect_search_error(linear_search(&values[0] as *u8, huge, @sizeof(i32),
|
||||
&two as *u8, null, compare_i32), true);
|
||||
}
|
||||
|
||||
fn test_stable_sort() {
|
||||
var items: [8]Item = .[
|
||||
.{ .key = 3, .sequence = 0 },
|
||||
.{ .key = 1, .sequence = 1 },
|
||||
.{ .key = 2, .sequence = 2 },
|
||||
.{ .key = 1, .sequence = 3 },
|
||||
.{ .key = 3, .sequence = 4 },
|
||||
.{ .key = 2, .sequence = 5 },
|
||||
.{ .key = 1, .sequence = 6 },
|
||||
.{ .key = 2, .sequence = 7 },
|
||||
];
|
||||
var ascending: i32 = 1;
|
||||
expect_ok(stable_sort(&items[0] as *u8, 8, @sizeof(Item),
|
||||
&ascending as *u8, compare_item));
|
||||
var expected_keys: [8]i32 = .[1, 1, 1, 2, 2, 2, 3, 3];
|
||||
var expected_sequence: [8]i32 = .[1, 3, 6, 2, 5, 7, 0, 4];
|
||||
var i: usize = 0;
|
||||
while i < 8 {
|
||||
@assert(items[i].key == expected_keys[i], "sorted key");
|
||||
@assert(items[i].sequence == expected_sequence[i], "stable equal order");
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
var descending: i32 = -1;
|
||||
expect_ok(stable_sort(&items[0] as *u8, 8, @sizeof(Item),
|
||||
&descending as *u8, compare_item));
|
||||
i = 1;
|
||||
while i < 8 {
|
||||
@assert(items[i - 1].key >= items[i].key, "context controls order");
|
||||
if items[i - 1].key == items[i].key {
|
||||
@assert(items[i - 1].sequence < items[i].sequence,
|
||||
"second stable sort preserves group order");
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
expect_ok(stable_sort(null, 0, @sizeof(Item), null, compare_item));
|
||||
expect_ok(stable_sort(&items[0] as *u8, 1, @sizeof(Item), null, compare_item));
|
||||
|
||||
// 非二次幂长度覆盖末尾不足一个完整归并段的路径。
|
||||
var odd: [9]i32 = .[9, -1, 4, 4, 0, 8, 2, 7, 3];
|
||||
var odd_expected: [9]i32 = .[-1, 0, 2, 3, 4, 4, 7, 8, 9];
|
||||
expect_ok(stable_sort(&odd[0] as *u8, 9, @sizeof(i32), null, compare_i32));
|
||||
i = 0;
|
||||
while i < 9 {
|
||||
@assert(odd[i] == odd_expected[i], "non-power-of-two length");
|
||||
i = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn test_sort_failures_preserve_input() {
|
||||
var values: [3]i32 = .[3, 1, 2];
|
||||
match stable_sort(&values[0] as *u8, 3, 0, null, compare_i32) {
|
||||
.Ok => { @assert(false, "zero element size"); },
|
||||
.Err(error) => {
|
||||
match error.kind {
|
||||
.InvalidArgument => {},
|
||||
_ => { @assert(false); },
|
||||
}
|
||||
},
|
||||
}
|
||||
@assert(values[0] == 3 && values[1] == 1 && values[2] == 2,
|
||||
"validation failure preserves input");
|
||||
|
||||
var huge = (~(0 as usize)) / @sizeof(i32) + 1;
|
||||
match stable_sort(&values[0] as *u8, huge, @sizeof(i32), null, compare_i32) {
|
||||
.Ok => { @assert(false, "size overflow"); },
|
||||
.Err(error) => {
|
||||
match error.kind {
|
||||
.Overflow => {},
|
||||
_ => { @assert(false); },
|
||||
}
|
||||
},
|
||||
}
|
||||
@assert(values[0] == 3 && values[1] == 1 && values[2] == 2,
|
||||
"overflow preserves input");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
test_search();
|
||||
test_stable_sort();
|
||||
test_sort_failures_preserve_input();
|
||||
}
|
||||
+70
-35
@@ -1,9 +1,13 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// 分块 bump 分配器:按块分配、块内顺序推进,只在 drop 时整体释放。
|
||||
// 适合 AST/IR 这类生命周期一致的批量小对象;不支持单独 free。
|
||||
// 失败(内存不足/参数非法)返回 null,不改变已有分配。
|
||||
// 分配失败通过 ArenaResult 区分原因:size==0/非法对齐/内存不足/地址溢出。
|
||||
// 失败的 alloc 不改变已有分配。
|
||||
|
||||
type .{ .Vec } = @import("vec.spl");
|
||||
type .{ .Error, .Kind, .Status } = @import("error.spl");
|
||||
type .{ .checked_add, .align_up } = @import("memory.spl");
|
||||
type Mem = @import("../sys/mem.spl");
|
||||
|
||||
type Chunk = struct {
|
||||
@@ -12,6 +16,11 @@ type Chunk = struct {
|
||||
used: usize,
|
||||
}
|
||||
|
||||
type ArenaResult = enum {
|
||||
Ok: *u8,
|
||||
Err: Error,
|
||||
}
|
||||
|
||||
type Arena = @self;
|
||||
|
||||
chunk_size: usize,
|
||||
@@ -28,61 +37,68 @@ fn new(chunk_size: usize): Arena {
|
||||
};
|
||||
}
|
||||
|
||||
fn is_pow2(x: usize): bool {
|
||||
ret x != 0 && (x & (x - 1)) == 0;
|
||||
}
|
||||
|
||||
fn align_up(x: usize, align: usize): usize {
|
||||
var mask = align - 1;
|
||||
ret (x + mask) & ~mask;
|
||||
}
|
||||
|
||||
fn chunk_at(self: *Arena, index: usize): *Chunk {
|
||||
match Vec.get(&self.chunks, index) {
|
||||
.Ok(ptr) => { ret ptr as *Chunk; },
|
||||
// 越界/无效 Arena 统一返回 null;调用方负责判空,无资源需要释放。
|
||||
.Err(error) => { ret null; },
|
||||
}
|
||||
ret null;
|
||||
}
|
||||
|
||||
fn push_chunk(self: *Arena, size: usize): bool {
|
||||
fn push_chunk(self: *Arena, size: usize): Status {
|
||||
var base = Mem.alloc(size) as *u8;
|
||||
if base == null { ret false; }
|
||||
if base == null { ret .Err(Error.make(.Overflow)); }
|
||||
var c = Chunk { .base = base, .size = size, .used = 0 };
|
||||
match Vec.push(&self.chunks, &c as *u8) {
|
||||
.Ok => {},
|
||||
.Err(error) => { Mem.free(base as *any); ret false; },
|
||||
.Err(error) => { Mem.free(base as *any); ret .Err(error); },
|
||||
}
|
||||
self.current = self.chunks.len - 1;
|
||||
ret true;
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
// 分配 size 字节,地址按 align(非零二次幂)对齐。失败返回 null。
|
||||
fn alloc(self: *Arena, size: usize, align: usize): *u8 {
|
||||
if size == 0 { ret null; }
|
||||
// 分配 size 字节,地址按 align(非零二次幂)对齐。失败返回 ArenaResult.Err。
|
||||
fn alloc(self: *Arena, size: usize, align: usize): ArenaResult {
|
||||
if size == 0 { ret .Err(Error.make(.InvalidArgument)); }
|
||||
if align == 0 { align = 1; }
|
||||
if !is_pow2(align) { ret null; }
|
||||
|
||||
while true {
|
||||
if self.current >= self.chunks.len {
|
||||
var want = self.chunk_size;
|
||||
var need = size + align;
|
||||
var need: usize = 0;
|
||||
match checked_add(size, align) {
|
||||
.Ok(value) => { need = value; },
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
if need > want { want = need; }
|
||||
if !push_chunk(self, want) { ret null; }
|
||||
match push_chunk(self, want) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
}
|
||||
|
||||
var c = chunk_at(self, self.current);
|
||||
if c == null { ret null; }
|
||||
if c == null { ret .Err(Error.make(.InvalidArgument)); }
|
||||
var addr = c.base as usize;
|
||||
var aligned = align_up(addr + c.used, align);
|
||||
var used_addr: usize = 0;
|
||||
match checked_add(addr, c.used) {
|
||||
.Ok(value) => { used_addr = value; },
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
var aligned: usize = 0;
|
||||
match align_up(used_addr, align) {
|
||||
.Ok(value) => { aligned = value; },
|
||||
.Err(error) => { ret .Err(error); }, // 非二次幂对齐或地址溢出
|
||||
}
|
||||
var offset = aligned - addr;
|
||||
if offset <= c.size && size <= c.size - offset {
|
||||
c.used = offset + size;
|
||||
ret aligned as *u8;
|
||||
ret .Ok(aligned as *u8);
|
||||
}
|
||||
self.current = self.current + 1;
|
||||
}
|
||||
ret null;
|
||||
ret .Err(Error.make(.OutOfRange)); // 不可达:循环内所有路径均已返回
|
||||
}
|
||||
|
||||
fn chunk_count(self: Arena): usize {
|
||||
@@ -117,13 +133,31 @@ fn is_aligned(p: *u8, align: usize): bool {
|
||||
ret ((p as usize) & (align - 1)) == 0;
|
||||
}
|
||||
|
||||
fn alloc_ok(a: *Arena, size: usize, align: usize): *u8 {
|
||||
match alloc(a, size, align) {
|
||||
.Ok(ptr) => { ret ptr; },
|
||||
.Err(error) => { @assert(false, "unexpected arena alloc error"); },
|
||||
}
|
||||
ret null;
|
||||
}
|
||||
|
||||
fn expect_alloc_error(result: ArenaResult, expected: Kind) {
|
||||
match result {
|
||||
.Ok(ptr) => { @assert(false, "expected arena alloc error"); },
|
||||
.Err(error) => {
|
||||
var want: Kind = expected;
|
||||
@assert(@enum2int(error.kind) == @enum2int(want), "arena alloc error kind");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn test_alloc_and_alignment() {
|
||||
var a = new(64);
|
||||
@assert(chunk_count(a) == 0, "lazy");
|
||||
|
||||
var p1 = alloc(&a, 1, 1);
|
||||
var p8 = alloc(&a, 16, 8);
|
||||
var p16 = alloc(&a, 1, 16);
|
||||
var p1 = alloc_ok(&a, 1, 1);
|
||||
var p8 = alloc_ok(&a, 16, 8);
|
||||
var p16 = alloc_ok(&a, 1, 16);
|
||||
@assert(p1 != null && p8 != null && p16 != null, "alloc");
|
||||
@assert(p1 != p8 && p8 != p16 && p1 != p16, "distinct");
|
||||
@assert(is_aligned(p8, 8), "align 8");
|
||||
@@ -136,18 +170,20 @@ fn test_alloc_and_alignment() {
|
||||
@assert(chunk_count(a) == 1, "one chunk");
|
||||
reset(&a);
|
||||
@assert(chunk_count(a) == 1, "reset keeps chunks");
|
||||
var q = alloc(&a, 1, 1);
|
||||
var q = alloc_ok(&a, 1, 1);
|
||||
@assert(q == p1, "reset reuses first chunk");
|
||||
|
||||
// drop 后还有断言,故保持手动释放,不改为 defer。
|
||||
drop(&a);
|
||||
@assert(chunk_count(a) == 0, "drop empties");
|
||||
}
|
||||
|
||||
fn test_growth() {
|
||||
var a = new(64);
|
||||
defer { drop(&a); }
|
||||
var i: i32 = 0;
|
||||
while i < 100 {
|
||||
var p = alloc(&a, 16, 8);
|
||||
var p = alloc_ok(&a, 16, 8);
|
||||
@assert(p != null, "grow alloc");
|
||||
@assert(is_aligned(p, 8), "grow align");
|
||||
p[0] = i as u8;
|
||||
@@ -156,18 +192,17 @@ fn test_growth() {
|
||||
@assert(chunk_count(a) > 1, "grew multiple chunks");
|
||||
|
||||
// 超过块大小的单次分配也能成功(新块按需放大)。
|
||||
var big = alloc(&a, 1024, 16);
|
||||
var big = alloc_ok(&a, 1024, 16);
|
||||
@assert(big != null && is_aligned(big, 16), "oversized alloc");
|
||||
drop(&a);
|
||||
}
|
||||
|
||||
fn test_failures() {
|
||||
var a = new(64);
|
||||
@assert(alloc(&a, 0, 1) == null, "zero size");
|
||||
var d = alloc(&a, 8, 0);
|
||||
defer { drop(&a); }
|
||||
expect_alloc_error(alloc(&a, 0, 1), .InvalidArgument);
|
||||
var d = alloc_ok(&a, 8, 0);
|
||||
@assert(d != null && is_aligned(d, 1), "align 0 defaults to 1");
|
||||
@assert(alloc(&a, 8, 3) == null, "non-pow2 align");
|
||||
drop(&a);
|
||||
expect_alloc_error(alloc(&a, 8, 3), .InvalidAlignment);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// ASCII 字符工具。输入和输出都是单个字节,不解释 UTF-8 或本地化规则。
|
||||
|
||||
type .{ .Error, .make } = @import("error.spl");
|
||||
|
||||
@@ -1,871 +0,0 @@
|
||||
// Expect retcode = 0
|
||||
// 任意精度有符号整数。limbs 为小端 u32;零规范为 len=0 且 negative=false。
|
||||
// 普通赋值只借用同一存储;深复制使用 clone,所有权转移使用 take。
|
||||
type .{ .Error, .Status } = @import("error.spl");
|
||||
type .{ .checked_mul } = @import("memory.spl");
|
||||
type .{ .digit_char, .digit_value } = @import("ascii.spl");
|
||||
type .{ .ByteBuffer } = @import("byte_buffer.spl");
|
||||
type .{ .alloc, .copy, .free } = @import("../sys/mem.spl");
|
||||
|
||||
type BigInt = @self;
|
||||
|
||||
limbs: *u32,
|
||||
len: usize,
|
||||
cap: usize,
|
||||
negative: bool,
|
||||
|
||||
fn zero(): BigInt {
|
||||
ret BigInt { .limbs = null, .len = 0, .cap = 0, .negative = false };
|
||||
}
|
||||
|
||||
fn valid(self: BigInt): bool {
|
||||
if self.len > self.cap { ret false; }
|
||||
if self.cap == 0 {
|
||||
ret self.limbs == null && self.len == 0 && !self.negative;
|
||||
}
|
||||
if self.limbs == null { ret false; }
|
||||
if self.len == 0 { ret !self.negative; }
|
||||
ret self.limbs[self.len - 1] != 0;
|
||||
}
|
||||
|
||||
fn is_zero(self: BigInt): bool {
|
||||
ret self.len == 0;
|
||||
}
|
||||
|
||||
fn is_negative(self: BigInt): bool {
|
||||
ret self.negative;
|
||||
}
|
||||
|
||||
fn normalize(self: *BigInt) {
|
||||
while self.len > 0 {
|
||||
if self.limbs[self.len - 1] != 0 { break; }
|
||||
self.len = self.len - 1;
|
||||
}
|
||||
if self.len == 0 { self.negative = false; }
|
||||
}
|
||||
|
||||
fn reserve(self: *BigInt, need: usize): Status {
|
||||
if !valid(self.*) { ret .Err(Error.make(.InvalidArgument)); }
|
||||
if need <= self.cap { ret .Ok; }
|
||||
|
||||
var new_cap = self.cap;
|
||||
if new_cap < 2 { new_cap = 2; }
|
||||
while new_cap < need {
|
||||
if new_cap > (~(0 as usize)) / 2 {
|
||||
new_cap = need;
|
||||
break;
|
||||
}
|
||||
new_cap = new_cap * 2;
|
||||
}
|
||||
|
||||
var byte_count: usize = 0;
|
||||
match checked_mul(new_cap, @sizeof(u32)) {
|
||||
.Ok(value) => { byte_count = value; },
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
var fresh = alloc(byte_count) as *u32;
|
||||
if self.len != 0 {
|
||||
copy(fresh as *any, self.limbs as *any, self.len * @sizeof(u32));
|
||||
}
|
||||
free(self.limbs as *any);
|
||||
self.limbs = fresh;
|
||||
self.cap = new_cap;
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn drop(self: *BigInt) {
|
||||
free(self.limbs as *any);
|
||||
self.* = zero();
|
||||
}
|
||||
|
||||
fn take(source: *BigInt): BigInt {
|
||||
var result = source.*;
|
||||
source.* = zero();
|
||||
ret result;
|
||||
}
|
||||
|
||||
fn replace(out: *BigInt, fresh: *BigInt) {
|
||||
drop(out);
|
||||
out.* = take(fresh);
|
||||
}
|
||||
|
||||
fn clone(out: *BigInt, source: BigInt): Status {
|
||||
if !valid(out.*) || !valid(source) { ret .Err(Error.make(.InvalidArgument)); }
|
||||
var fresh = zero();
|
||||
match reserve(&fresh, source.len) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
if source.len != 0 {
|
||||
copy(fresh.limbs as *any, source.limbs as *any, source.len * @sizeof(u32));
|
||||
}
|
||||
fresh.len = source.len;
|
||||
fresh.negative = source.negative;
|
||||
replace(out, &fresh);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn from_u64(out: *BigInt, value: u64): Status {
|
||||
if !valid(out.*) { ret .Err(Error.make(.InvalidArgument)); }
|
||||
var fresh = zero();
|
||||
if value != 0 {
|
||||
match reserve(&fresh, 2) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
fresh.limbs[0] = value as u32;
|
||||
fresh.limbs[1] = (value >> 32) as u32;
|
||||
fresh.len = 1;
|
||||
if fresh.limbs[1] != 0 { fresh.len = 2; }
|
||||
}
|
||||
replace(out, &fresh);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn from_i64(out: *BigInt, value: i64): Status {
|
||||
var magnitude = value as u64;
|
||||
var negative = value < 0;
|
||||
if negative { magnitude = 0 - magnitude; }
|
||||
match from_u64(out, magnitude) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
if out.len != 0 { out.negative = negative; }
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn cmp_abs(a: BigInt, b: BigInt): i32 {
|
||||
if a.len < b.len { ret -1; }
|
||||
if a.len > b.len { ret 1; }
|
||||
var i = a.len;
|
||||
while i > 0 {
|
||||
i = i - 1;
|
||||
if a.limbs[i] < b.limbs[i] { ret -1; }
|
||||
if a.limbs[i] > b.limbs[i] { ret 1; }
|
||||
}
|
||||
ret 0;
|
||||
}
|
||||
|
||||
fn cmp(a: BigInt, b: BigInt): i32 {
|
||||
if a.negative != b.negative {
|
||||
if a.negative { ret -1; }
|
||||
ret 1;
|
||||
}
|
||||
var order = cmp_abs(a, b);
|
||||
if a.negative { ret 0 - order; }
|
||||
ret order;
|
||||
}
|
||||
|
||||
fn eq(a: BigInt, b: BigInt): bool {
|
||||
if a.negative != b.negative || a.len != b.len { ret false; }
|
||||
var i: usize = 0;
|
||||
while i < a.len {
|
||||
if a.limbs[i] != b.limbs[i] { ret false; }
|
||||
i = i + 1;
|
||||
}
|
||||
ret true;
|
||||
}
|
||||
|
||||
fn add_abs_into(out: *BigInt, a: BigInt, b: BigInt): Status {
|
||||
var count = a.len;
|
||||
if b.len > count { count = b.len; }
|
||||
if count == ~(0 as usize) { ret .Err(Error.make(.Overflow)); }
|
||||
match reserve(out, count + 1) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
var carry: u64 = 0;
|
||||
var i: usize = 0;
|
||||
while i < count {
|
||||
var av: u64 = 0;
|
||||
var bv: u64 = 0;
|
||||
if i < a.len { av = a.limbs[i] as u64; }
|
||||
if i < b.len { bv = b.limbs[i] as u64; }
|
||||
var sum = av + bv + carry;
|
||||
out.limbs[i] = sum as u32;
|
||||
carry = sum >> 32;
|
||||
i = i + 1;
|
||||
}
|
||||
if carry != 0 {
|
||||
out.limbs[count] = carry as u32;
|
||||
count = count + 1;
|
||||
}
|
||||
out.len = count;
|
||||
out.negative = false;
|
||||
normalize(out);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
// 要求 |a| >= |b|。
|
||||
fn sub_abs_into(out: *BigInt, a: BigInt, b: BigInt): Status {
|
||||
match reserve(out, a.len) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
var borrow: u64 = 0;
|
||||
var i: usize = 0;
|
||||
while i < a.len {
|
||||
var av = a.limbs[i] as u64;
|
||||
var bv: u64 = 0;
|
||||
if i < b.len { bv = b.limbs[i] as u64; }
|
||||
var subtrahend = bv + borrow;
|
||||
if av < subtrahend {
|
||||
out.limbs[i] = (4294967296 + av - subtrahend) as u32;
|
||||
borrow = 1;
|
||||
} else {
|
||||
out.limbs[i] = (av - subtrahend) as u32;
|
||||
borrow = 0;
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
out.len = a.len;
|
||||
out.negative = false;
|
||||
normalize(out);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn add(out: *BigInt, a: BigInt, b: BigInt): Status {
|
||||
if !valid(out.*) || !valid(a) || !valid(b) {
|
||||
ret .Err(Error.make(.InvalidArgument));
|
||||
}
|
||||
var fresh = zero();
|
||||
if a.negative == b.negative {
|
||||
match add_abs_into(&fresh, a, b) {
|
||||
.Ok => {}, .Err(error) => { drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
if fresh.len != 0 { fresh.negative = a.negative; }
|
||||
} else {
|
||||
var order = cmp_abs(a, b);
|
||||
if order >= 0 {
|
||||
match sub_abs_into(&fresh, a, b) {
|
||||
.Ok => {}, .Err(error) => { drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
if fresh.len != 0 { fresh.negative = a.negative; }
|
||||
} else {
|
||||
match sub_abs_into(&fresh, b, a) {
|
||||
.Ok => {}, .Err(error) => { drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
if fresh.len != 0 { fresh.negative = b.negative; }
|
||||
}
|
||||
}
|
||||
replace(out, &fresh);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn sub(out: *BigInt, a: BigInt, b: BigInt): Status {
|
||||
var neg_b = b;
|
||||
if neg_b.len != 0 { neg_b.negative = !neg_b.negative; }
|
||||
ret add(out, a, neg_b);
|
||||
}
|
||||
|
||||
fn neg(out: *BigInt, source: BigInt): Status {
|
||||
match clone(out, source) {
|
||||
.Ok => {}, .Err(error) => { ret .Err(error); },
|
||||
}
|
||||
if out.len != 0 { out.negative = !out.negative; }
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn abs(out: *BigInt, source: BigInt): Status {
|
||||
match clone(out, source) {
|
||||
.Ok => {}, .Err(error) => { ret .Err(error); },
|
||||
}
|
||||
out.negative = false;
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn mul(out: *BigInt, a: BigInt, b: BigInt): Status {
|
||||
if !valid(out.*) || !valid(a) || !valid(b) {
|
||||
ret .Err(Error.make(.InvalidArgument));
|
||||
}
|
||||
var fresh = zero();
|
||||
if a.len == 0 || b.len == 0 {
|
||||
replace(out, &fresh);
|
||||
ret .Ok;
|
||||
}
|
||||
if b.len > (~(0 as usize)) - a.len {
|
||||
ret .Err(Error.make(.Overflow));
|
||||
}
|
||||
var count = a.len + b.len;
|
||||
match reserve(&fresh, count) {
|
||||
.Ok => {}, .Err(error) => { ret .Err(error); },
|
||||
}
|
||||
var z: usize = 0;
|
||||
while z < count { fresh.limbs[z] = 0; z = z + 1; }
|
||||
|
||||
var i: usize = 0;
|
||||
while i < a.len {
|
||||
var carry: u64 = 0;
|
||||
var j: usize = 0;
|
||||
while j < b.len {
|
||||
var at = i + j;
|
||||
var product = (a.limbs[i] as u64) * (b.limbs[j] as u64) +
|
||||
(fresh.limbs[at] as u64) + carry;
|
||||
fresh.limbs[at] = product as u32;
|
||||
carry = product >> 32;
|
||||
j = j + 1;
|
||||
}
|
||||
fresh.limbs[i + b.len] = carry as u32;
|
||||
i = i + 1;
|
||||
}
|
||||
fresh.len = count;
|
||||
fresh.negative = a.negative != b.negative;
|
||||
normalize(&fresh);
|
||||
replace(out, &fresh);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn bit_length(out: *usize, self: BigInt): Status {
|
||||
if out == null || !valid(self) { ret .Err(Error.make(.InvalidArgument)); }
|
||||
if self.len == 0 { out.* = 0; ret .Ok; }
|
||||
if self.len - 1 > (~(0 as usize)) / 32 { ret .Err(Error.make(.Overflow)); }
|
||||
var top = self.limbs[self.len - 1];
|
||||
var bits = (self.len - 1) * 32;
|
||||
while top != 0 {
|
||||
bits = bits + 1;
|
||||
top = top >> 1;
|
||||
}
|
||||
out.* = bits;
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn mul_pow2(out: *BigInt, source: BigInt, amount: usize): Status {
|
||||
if !valid(out.*) || !valid(source) { ret .Err(Error.make(.InvalidArgument)); }
|
||||
if source.len == 0 { ret from_u64(out, 0); }
|
||||
var words = amount / 32;
|
||||
var shift = amount % 32;
|
||||
if source.len == ~(0 as usize) || words > (~(0 as usize)) - source.len - 1 {
|
||||
ret .Err(Error.make(.Overflow));
|
||||
}
|
||||
var fresh = zero();
|
||||
var count = source.len + words + 1;
|
||||
match reserve(&fresh, count) {
|
||||
.Ok => {}, .Err(error) => { ret .Err(error); },
|
||||
}
|
||||
var i: usize = 0;
|
||||
while i < words { fresh.limbs[i] = 0; i = i + 1; }
|
||||
var carry: u64 = 0;
|
||||
i = 0;
|
||||
while i < source.len {
|
||||
var value = ((source.limbs[i] as u64) << shift as u32) | carry;
|
||||
fresh.limbs[i + words] = value as u32;
|
||||
carry = value >> 32;
|
||||
i = i + 1;
|
||||
}
|
||||
fresh.len = source.len + words;
|
||||
if carry != 0 {
|
||||
fresh.limbs[fresh.len] = carry as u32;
|
||||
fresh.len = fresh.len + 1;
|
||||
}
|
||||
fresh.negative = source.negative;
|
||||
replace(out, &fresh);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
// 按绝对值右移,等价于向零除以 2^amount。
|
||||
fn div_pow2(out: *BigInt, source: BigInt, amount: usize): Status {
|
||||
if !valid(out.*) || !valid(source) { ret .Err(Error.make(.InvalidArgument)); }
|
||||
var words = amount / 32;
|
||||
var shift = amount % 32;
|
||||
if words >= source.len { ret from_u64(out, 0); }
|
||||
var count = source.len - words;
|
||||
var fresh = zero();
|
||||
match reserve(&fresh, count) {
|
||||
.Ok => {}, .Err(error) => { ret .Err(error); },
|
||||
}
|
||||
var i: usize = 0;
|
||||
while i < count {
|
||||
var src = i + words;
|
||||
var value = (source.limbs[src] as u64) >> shift as u32;
|
||||
if shift != 0 && src + 1 < source.len {
|
||||
value = value | ((source.limbs[src + 1] << (32 - shift) as u32) as u64);
|
||||
}
|
||||
fresh.limbs[i] = value as u32;
|
||||
i = i + 1;
|
||||
}
|
||||
fresh.len = count;
|
||||
fresh.negative = source.negative;
|
||||
normalize(&fresh);
|
||||
replace(out, &fresh);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn shift_one(self: *BigInt) {
|
||||
var carry: u64 = 0;
|
||||
var i: usize = 0;
|
||||
while i < self.len {
|
||||
var value = ((self.limbs[i] as u64) << 1) | carry;
|
||||
self.limbs[i] = value as u32;
|
||||
carry = value >> 32;
|
||||
i = i + 1;
|
||||
}
|
||||
if carry != 0 {
|
||||
self.limbs[self.len] = carry as u32;
|
||||
self.len = self.len + 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn sub_abs_in_place(a: *BigInt, b: BigInt) {
|
||||
var borrow: u64 = 0;
|
||||
var i: usize = 0;
|
||||
while i < a.len {
|
||||
var av = a.limbs[i] as u64;
|
||||
var bv: u64 = 0;
|
||||
if i < b.len { bv = b.limbs[i] as u64; }
|
||||
var subtrahend = bv + borrow;
|
||||
if av < subtrahend {
|
||||
a.limbs[i] = (4294967296 + av - subtrahend) as u32;
|
||||
borrow = 1;
|
||||
} else {
|
||||
a.limbs[i] = (av - subtrahend) as u32;
|
||||
borrow = 0;
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
normalize(a);
|
||||
}
|
||||
|
||||
fn div_rem(quotient: *BigInt, remainder: *BigInt, dividend: BigInt,
|
||||
divisor: BigInt): Status {
|
||||
if (quotient as usize) == (remainder as usize) {
|
||||
ret .Err(Error.make(.InvalidArgument));
|
||||
}
|
||||
if !valid(quotient.*) || !valid(remainder.*) || !valid(dividend) || !valid(divisor) {
|
||||
ret .Err(Error.make(.InvalidArgument));
|
||||
}
|
||||
if divisor.len == 0 { ret .Err(Error.make(.DivisionByZero)); }
|
||||
|
||||
var q = zero();
|
||||
var r = zero();
|
||||
match reserve(&q, dividend.len) {
|
||||
.Ok => {}, .Err(error) => { ret .Err(error); },
|
||||
}
|
||||
if divisor.len == ~(0 as usize) { drop(&q); ret .Err(Error.make(.Overflow)); }
|
||||
match reserve(&r, divisor.len + 1) {
|
||||
.Ok => {}, .Err(error) => { drop(&q); ret .Err(error); },
|
||||
}
|
||||
var i: usize = 0;
|
||||
while i < dividend.len { q.limbs[i] = 0; i = i + 1; }
|
||||
q.len = dividend.len;
|
||||
|
||||
var bit: usize = 0;
|
||||
match bit_length(&bit, dividend) {
|
||||
.Ok => {},
|
||||
.Err(error) => { drop(&q); drop(&r); ret .Err(error); },
|
||||
}
|
||||
while bit > 0 {
|
||||
bit = bit - 1;
|
||||
shift_one(&r);
|
||||
var word = bit / 32;
|
||||
var offset = bit % 32;
|
||||
var incoming = (dividend.limbs[word] >> offset as u32) & 1;
|
||||
if incoming != 0 {
|
||||
if r.len == 0 { r.len = 1; r.limbs[0] = 0; }
|
||||
r.limbs[0] = r.limbs[0] | 1;
|
||||
}
|
||||
if cmp_abs(r, divisor) >= 0 {
|
||||
sub_abs_in_place(&r, divisor);
|
||||
q.limbs[word] = q.limbs[word] | ((1 as u32) << offset as u32);
|
||||
}
|
||||
}
|
||||
q.negative = q.len != 0 && dividend.negative != divisor.negative;
|
||||
r.negative = r.len != 0 && dividend.negative;
|
||||
normalize(&q);
|
||||
normalize(&r);
|
||||
replace(quotient, &q);
|
||||
replace(remainder, &r);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn mul_small(self: *BigInt, value: u32): Status {
|
||||
if self.len == 0 || value == 1 { ret .Ok; }
|
||||
if value == 0 { self.len = 0; self.negative = false; ret .Ok; }
|
||||
if self.len == ~(0 as usize) { ret .Err(Error.make(.Overflow)); }
|
||||
match reserve(self, self.len + 1) {
|
||||
.Ok => {}, .Err(error) => { ret .Err(error); },
|
||||
}
|
||||
var carry: u64 = 0;
|
||||
var i: usize = 0;
|
||||
while i < self.len {
|
||||
var product = (self.limbs[i] as u64) * (value as u64) + carry;
|
||||
self.limbs[i] = product as u32;
|
||||
carry = product >> 32;
|
||||
i = i + 1;
|
||||
}
|
||||
if carry != 0 {
|
||||
self.limbs[self.len] = carry as u32;
|
||||
self.len = self.len + 1;
|
||||
}
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn add_small(self: *BigInt, value: u32): Status {
|
||||
if value == 0 { ret .Ok; }
|
||||
if self.len == ~(0 as usize) { ret .Err(Error.make(.Overflow)); }
|
||||
match reserve(self, self.len + 1) {
|
||||
.Ok => {}, .Err(error) => { ret .Err(error); },
|
||||
}
|
||||
var carry = value as u64;
|
||||
var i: usize = 0;
|
||||
while carry != 0 && i < self.len {
|
||||
var sum = (self.limbs[i] as u64) + carry;
|
||||
self.limbs[i] = sum as u32;
|
||||
carry = sum >> 32;
|
||||
i = i + 1;
|
||||
}
|
||||
if carry != 0 {
|
||||
self.limbs[self.len] = carry as u32;
|
||||
self.len = self.len + 1;
|
||||
}
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn parse(out: *BigInt, bytes: []u8, radix: u8): Status {
|
||||
if !valid(out.*) { ret .Err(Error.make(.InvalidArgument)); }
|
||||
if radix < 2 || radix > 36 { ret .Err(Error.make(.InvalidArgument)); }
|
||||
if bytes.len == 0 { ret .Err(Error.at(.UnexpectedEnd, 0)); }
|
||||
if bytes.ptr == null { ret .Err(Error.make(.InvalidArgument)); }
|
||||
|
||||
var i: usize = 0;
|
||||
var negative = false;
|
||||
if bytes.ptr[0] == '-' || bytes.ptr[0] == '+' {
|
||||
negative = bytes.ptr[0] == '-';
|
||||
i = 1;
|
||||
}
|
||||
if i == bytes.len { ret .Err(Error.at(.UnexpectedEnd, i)); }
|
||||
|
||||
var fresh = zero();
|
||||
while i < bytes.len {
|
||||
var digit: u8 = 0;
|
||||
match digit_value(bytes.ptr[i], radix) {
|
||||
.Ok(value) => { digit = value; },
|
||||
.Err(error) => { drop(&fresh); ret .Err(Error.at(.InvalidDigit, i)); },
|
||||
}
|
||||
match mul_small(&fresh, radix as u32) {
|
||||
.Ok => {}, .Err(error) => { drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
match add_small(&fresh, digit as u32) {
|
||||
.Ok => {}, .Err(error) => { drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
fresh.negative = fresh.len != 0 && negative;
|
||||
replace(out, &fresh);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn div_small(self: *BigInt, radix: u32): u32 {
|
||||
var remainder: u64 = 0;
|
||||
var i = self.len;
|
||||
while i > 0 {
|
||||
i = i - 1;
|
||||
var current = (remainder << 32) | (self.limbs[i] as u64);
|
||||
self.limbs[i] = (current / radix as u64) as u32;
|
||||
remainder = current % radix as u64;
|
||||
}
|
||||
normalize(self);
|
||||
ret remainder as u32;
|
||||
}
|
||||
|
||||
fn format(out: *ByteBuffer, source: BigInt, radix: u8, uppercase: bool): Status {
|
||||
if !valid(source) { ret .Err(Error.make(.InvalidArgument)); }
|
||||
if radix < 2 || radix > 36 { ret .Err(Error.make(.InvalidArgument)); }
|
||||
var reverse = ByteBuffer.new();
|
||||
var fresh = ByteBuffer.new();
|
||||
var magnitude = zero();
|
||||
match clone(&magnitude, source) {
|
||||
.Ok => {}, .Err(error) => { ret .Err(error); },
|
||||
}
|
||||
magnitude.negative = false;
|
||||
if magnitude.len == 0 {
|
||||
match ByteBuffer.append_byte(&reverse, '0') {
|
||||
.Ok => {}, .Err(error) => { drop(&magnitude); ret .Err(error); },
|
||||
}
|
||||
}
|
||||
while magnitude.len != 0 {
|
||||
var remainder = div_small(&magnitude, radix as u32);
|
||||
var byte: u8 = 0;
|
||||
match digit_char(remainder as u8, uppercase) {
|
||||
.Ok(value) => { byte = value; },
|
||||
.Err(error) => { drop(&magnitude); ByteBuffer.drop(&reverse); ret .Err(error); },
|
||||
}
|
||||
match ByteBuffer.append_byte(&reverse, byte) {
|
||||
.Ok => {},
|
||||
.Err(error) => {
|
||||
drop(&magnitude); ByteBuffer.drop(&reverse); ret .Err(error);
|
||||
},
|
||||
}
|
||||
}
|
||||
if source.negative {
|
||||
match ByteBuffer.append_byte(&fresh, '-') {
|
||||
.Ok => {},
|
||||
.Err(error) => {
|
||||
drop(&magnitude); ByteBuffer.drop(&reverse); ret .Err(error);
|
||||
},
|
||||
}
|
||||
}
|
||||
var i = ByteBuffer.size(reverse);
|
||||
var digits = ByteBuffer.as_slice(reverse);
|
||||
while i > 0 {
|
||||
i = i - 1;
|
||||
match ByteBuffer.append_byte(&fresh, digits.ptr[i]) {
|
||||
.Ok => {},
|
||||
.Err(error) => {
|
||||
drop(&magnitude); ByteBuffer.drop(&reverse); ByteBuffer.drop(&fresh);
|
||||
ret .Err(error);
|
||||
},
|
||||
}
|
||||
}
|
||||
drop(&magnitude);
|
||||
ByteBuffer.drop(&reverse);
|
||||
ByteBuffer.drop(out);
|
||||
out.* = ByteBuffer.take(&fresh);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn to_u64(out: *u64, source: BigInt): Status {
|
||||
if out == null || !valid(source) { ret .Err(Error.make(.InvalidArgument)); }
|
||||
if source.negative || source.len > 2 { ret .Err(Error.make(.Overflow)); }
|
||||
var value: u64 = 0;
|
||||
if source.len > 0 { value = source.limbs[0] as u64; }
|
||||
if source.len > 1 { value = value | ((source.limbs[1] as u64) << 32); }
|
||||
out.* = value;
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn to_i64(out: *i64, source: BigInt): Status {
|
||||
if out == null || !valid(source) { ret .Err(Error.make(.InvalidArgument)); }
|
||||
if source.len > 2 { ret .Err(Error.make(.Overflow)); }
|
||||
var magnitude: u64 = 0;
|
||||
if source.len > 0 { magnitude = source.limbs[0] as u64; }
|
||||
if source.len > 1 { magnitude = magnitude | ((source.limbs[1] as u64) << 32); }
|
||||
if !source.negative {
|
||||
if magnitude > 9223372036854775807 { ret .Err(Error.make(.Overflow)); }
|
||||
out.* = magnitude as i64;
|
||||
ret .Ok;
|
||||
}
|
||||
if magnitude > 9223372036854775808 { ret .Err(Error.make(.Overflow)); }
|
||||
if magnitude == 9223372036854775808 {
|
||||
out.* = magnitude as i64;
|
||||
} else {
|
||||
out.* = 0 - magnitude as i64;
|
||||
}
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn to_usize(out: *usize, source: BigInt): Status {
|
||||
if out == null { ret .Err(Error.make(.InvalidArgument)); }
|
||||
var value: u64 = 0;
|
||||
match to_u64(&value, source) {
|
||||
.Ok => {}, .Err(error) => { ret .Err(error); },
|
||||
}
|
||||
if value > (~(0 as usize)) as u64 { ret .Err(Error.make(.Overflow)); }
|
||||
out.* = value as usize;
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn expect_ok(result: Status) {
|
||||
match result { .Ok => {}, .Err(error) => { @assert(false, "unexpected BigInt error"); }, }
|
||||
}
|
||||
|
||||
fn expect_text_radix(value: BigInt, radix: u8, uppercase: bool, expected: []u8) {
|
||||
var buffer = ByteBuffer.new();
|
||||
expect_ok(format(&buffer, value, radix, uppercase));
|
||||
var actual = ByteBuffer.as_slice(buffer);
|
||||
@assert(actual.len == expected.len, "formatted length");
|
||||
var i: usize = 0;
|
||||
while i < expected.len {
|
||||
@assert(actual.ptr[i] == expected.ptr[i], "formatted byte");
|
||||
i = i + 1;
|
||||
}
|
||||
ByteBuffer.drop(&buffer);
|
||||
}
|
||||
|
||||
fn expect_text(value: BigInt, expected: []u8) {
|
||||
expect_text_radix(value, 10, false, expected);
|
||||
}
|
||||
|
||||
fn parsed(text: []u8): BigInt {
|
||||
var value = zero();
|
||||
expect_ok(parse(&value, text, 10));
|
||||
ret value;
|
||||
}
|
||||
|
||||
fn test_arithmetic() {
|
||||
var a = parsed("4294967295");
|
||||
var one = zero();
|
||||
expect_ok(from_u64(&one, 1));
|
||||
expect_ok(add(&a, a, one));
|
||||
expect_text(a, "4294967296");
|
||||
|
||||
var all_ones = zero();
|
||||
expect_ok(parse(&all_ones, "ffffffffffffffffffffffffffffffff", 16));
|
||||
expect_ok(add(&all_ones, all_ones, one));
|
||||
expect_text(all_ones, "340282366920938463463374607431768211456");
|
||||
expect_ok(sub(&all_ones, all_ones, one));
|
||||
expect_text_radix(all_ones, 16, false, "ffffffffffffffffffffffffffffffff");
|
||||
|
||||
var two_64 = parsed("18446744073709551616");
|
||||
var product = zero();
|
||||
expect_ok(mul(&product, two_64, two_64));
|
||||
expect_text(product, "340282366920938463463374607431768211456");
|
||||
|
||||
var difference = zero();
|
||||
expect_ok(sub(&difference, product, two_64));
|
||||
expect_text(difference, "340282366920938463444927863358058659840");
|
||||
|
||||
var shifted = zero();
|
||||
expect_ok(mul_pow2(&shifted, one, 257));
|
||||
var bits: usize = 0;
|
||||
expect_ok(bit_length(&bits, shifted));
|
||||
@assert(bits == 258);
|
||||
expect_ok(div_pow2(&shifted, shifted, 257));
|
||||
@assert(eq(shifted, one));
|
||||
|
||||
drop(&shifted); drop(&difference); drop(&product); drop(&two_64);
|
||||
drop(&all_ones); drop(&one); drop(&a);
|
||||
}
|
||||
|
||||
fn test_division_and_sign() {
|
||||
var dividend = parsed("340282366920938463463374607431768211456");
|
||||
var divisor = parsed("18446744073709551616");
|
||||
var quotient = zero();
|
||||
var remainder = zero();
|
||||
expect_ok(div_rem("ient, &remainder, dividend, divisor));
|
||||
@assert(eq(quotient, divisor));
|
||||
@assert(is_zero(remainder));
|
||||
|
||||
var long_dividend = parsed("123456789012345678901234567890");
|
||||
var long_divisor = parsed("9876543210");
|
||||
expect_ok(div_rem("ient, &remainder, long_dividend, long_divisor));
|
||||
expect_text(quotient, "12499999887343749990");
|
||||
expect_text(remainder, "1562499990");
|
||||
|
||||
var negative = zero();
|
||||
var seven = zero();
|
||||
expect_ok(from_i64(&negative, -100));
|
||||
expect_ok(from_u64(&seven, 7));
|
||||
expect_ok(div_rem(&negative, &remainder, negative, seven));
|
||||
expect_text(negative, "-14");
|
||||
expect_text(remainder, "-2");
|
||||
|
||||
var reconstructed = zero();
|
||||
expect_ok(mul(&reconstructed, negative, seven));
|
||||
expect_ok(add(&reconstructed, reconstructed, remainder));
|
||||
expect_text(reconstructed, "-100");
|
||||
|
||||
var saved = parsed("123");
|
||||
var zero_divisor = zero();
|
||||
match div_rem(&saved, &remainder, dividend, zero_divisor) {
|
||||
.Ok => { @assert(false, "division by zero"); },
|
||||
.Err(error) => { match error.kind { .DivisionByZero => {}, _ => { @assert(false); }, } },
|
||||
}
|
||||
expect_text(saved, "123");
|
||||
|
||||
drop(&zero_divisor); drop(&saved); drop(&reconstructed); drop(&seven); drop(&negative);
|
||||
drop(&long_divisor); drop(&long_dividend);
|
||||
drop(&remainder); drop("ient); drop(&divisor); drop(÷nd);
|
||||
}
|
||||
|
||||
fn test_parse_format_and_conversion() {
|
||||
var value = zero();
|
||||
expect_ok(parse(&value, "-deadBEEF", 16));
|
||||
var output = ByteBuffer.new();
|
||||
expect_ok(format(&output, value, 16, false));
|
||||
var text = ByteBuffer.as_slice(output);
|
||||
@assert(text.len == 9 && text.ptr[0] == '-' && text.ptr[1] == 'd' && text.ptr[8] == 'f');
|
||||
expect_text_radix(value, 16, true, "-DEADBEEF");
|
||||
|
||||
var base36 = zero();
|
||||
expect_ok(parse(&base36, "zzzzzzzzzz", 36));
|
||||
expect_text_radix(base36, 36, false, "zzzzzzzzzz");
|
||||
|
||||
var preserved = parsed("77");
|
||||
match parse(&preserved, "12x", 10) {
|
||||
.Ok => { @assert(false, "invalid digit"); },
|
||||
.Err(error) => {
|
||||
@assert(error.has_position && error.position == 2);
|
||||
match error.kind { .InvalidDigit => {}, _ => { @assert(false); }, }
|
||||
},
|
||||
}
|
||||
expect_text(preserved, "77");
|
||||
|
||||
var max = parsed("18446744073709551615");
|
||||
var converted: u64 = 0;
|
||||
expect_ok(to_u64(&converted, max));
|
||||
@assert(converted == 18446744073709551615);
|
||||
var over = parsed("18446744073709551616");
|
||||
converted = 17;
|
||||
match to_u64(&converted, over) {
|
||||
.Ok => { @assert(false); },
|
||||
.Err(error) => { match error.kind { .Overflow => {}, _ => { @assert(false); }, } },
|
||||
}
|
||||
@assert(converted == 17, "failed conversion preserves output");
|
||||
|
||||
var minimum = parsed("-9223372036854775808");
|
||||
var signed_value: i64 = 0;
|
||||
expect_ok(to_i64(&signed_value, minimum));
|
||||
@assert(signed_value == (9223372036854775808 as u64) as i64);
|
||||
var signed_over = parsed("9223372036854775808");
|
||||
signed_value = 23;
|
||||
match to_i64(&signed_value, signed_over) {
|
||||
.Ok => { @assert(false); },
|
||||
.Err(error) => { match error.kind { .Overflow => {}, _ => { @assert(false); }, } },
|
||||
}
|
||||
@assert(signed_value == 23, "failed signed conversion preserves output");
|
||||
|
||||
var copied = zero();
|
||||
expect_ok(clone(&copied, max));
|
||||
var one_more = parsed("1");
|
||||
expect_ok(add(&max, max, one_more));
|
||||
expect_text(copied, "18446744073709551615");
|
||||
var moved = take(&copied);
|
||||
@assert(copied.limbs == null && copied.len == 0 && copied.cap == 0);
|
||||
expect_text(moved, "18446744073709551615");
|
||||
|
||||
drop(&moved); drop(&copied); drop(&one_more); drop(&signed_over); drop(&minimum);
|
||||
drop(&over); drop(&max); drop(&preserved); drop(&base36);
|
||||
ByteBuffer.drop(&output); drop(&value);
|
||||
}
|
||||
|
||||
fn expect_i64_value(value: BigInt, expected: i64) {
|
||||
var actual: i64 = 0;
|
||||
expect_ok(to_i64(&actual, value));
|
||||
@assert(actual == expected, "small integer reference result");
|
||||
}
|
||||
|
||||
fn test_small_reference() {
|
||||
var left = zero();
|
||||
var right = zero();
|
||||
var result = zero();
|
||||
var remainder = zero();
|
||||
var a: i64 = 0;
|
||||
while a <= 0 {
|
||||
var b: i64 = 2;
|
||||
while b <= 2 {
|
||||
expect_ok(from_i64(&left, a));
|
||||
expect_ok(from_i64(&right, b));
|
||||
expect_ok(add(&result, left, right));
|
||||
expect_i64_value(result, a + b);
|
||||
expect_ok(sub(&result, left, right));
|
||||
expect_i64_value(result, a - b);
|
||||
expect_ok(mul(&result, left, right));
|
||||
expect_i64_value(result, a * b);
|
||||
if b != 0 {
|
||||
expect_ok(div_rem(&result, &remainder, left, right));
|
||||
expect_i64_value(result, a / b);
|
||||
expect_i64_value(remainder, a % b);
|
||||
}
|
||||
b = b + 1;
|
||||
}
|
||||
a = a + 1;
|
||||
}
|
||||
drop(&remainder); drop(&result); drop(&right); drop(&left);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
test_arithmetic();
|
||||
test_division_and_sign();
|
||||
test_parse_format_and_conversion();
|
||||
test_small_reference();
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// 拥有字节存储的动态缓冲区。普通赋值会复制所有权,转移必须使用 take。
|
||||
// 借用切片和 z_ptr 返回的指针在下一次可能扩容的操作或 drop 后失效。
|
||||
|
||||
@@ -205,6 +206,7 @@ fn expect_z(self: *ByteBuffer) {
|
||||
|
||||
fn test_append_and_resize() {
|
||||
var buffer = new();
|
||||
defer { drop(&buffer); }
|
||||
@assert(is_empty(buffer));
|
||||
@assert(size(buffer) == 0 && capacity(buffer) == 0);
|
||||
expect_z(&buffer);
|
||||
@@ -232,11 +234,11 @@ fn test_append_and_resize() {
|
||||
clear(&buffer);
|
||||
@assert(is_empty(buffer) && capacity(buffer) != 0, "clear retains allocation");
|
||||
expect_z(&buffer);
|
||||
drop(&buffer);
|
||||
}
|
||||
|
||||
fn test_growth_and_self_append() {
|
||||
var buffer = new();
|
||||
defer { drop(&buffer); }
|
||||
var i: usize = 0;
|
||||
while i < 8 {
|
||||
expect_ok(append_byte(&buffer, (65 + i) as u8));
|
||||
@@ -253,14 +255,15 @@ fn test_growth_and_self_append() {
|
||||
i = i + 1;
|
||||
}
|
||||
expect_z(&buffer);
|
||||
drop(&buffer);
|
||||
}
|
||||
|
||||
fn test_ownership_and_failures() {
|
||||
var source = new();
|
||||
defer { drop(&source); }
|
||||
expect_ok(append(&source, "owned"));
|
||||
|
||||
var copy = new();
|
||||
defer { drop(©); }
|
||||
expect_ok(clone(©, source));
|
||||
@assert(copy.storage.len == 5, "clone logical length");
|
||||
source.storage.data[0] = 79;
|
||||
@@ -293,6 +296,7 @@ fn test_ownership_and_failures() {
|
||||
expect_bytes(copy, "wne");
|
||||
|
||||
var moved = take(©);
|
||||
defer { drop(&moved); }
|
||||
@assert(copy.storage.raw == null && copy.storage.len == 0 && copy.storage.cap == 0);
|
||||
expect_bytes(moved, "wne");
|
||||
|
||||
@@ -304,10 +308,6 @@ fn test_ownership_and_failures() {
|
||||
moved.storage.cap == old_cap,
|
||||
"overflow preserves byte buffer");
|
||||
expect_bytes(moved, "wne");
|
||||
|
||||
drop(&moved);
|
||||
drop(©);
|
||||
drop(&source);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// 借用的字节字符串视图;不拥有、不分配也不释放 ptr 指向的存储。
|
||||
// 非空视图要求 ptr 在整个借用期内可读。空视图统一表示为 null/0。
|
||||
|
||||
@@ -34,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 };
|
||||
}
|
||||
@@ -212,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() {
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
// Expect retcode = 0
|
||||
// 诊断收集与渲染:severity + 文件/行/列 + 消息;统计错误/警告数。
|
||||
// 消息与文件名按拥有型 Str 保存(深拷贝),Diag 拥有它们直到 clear/drop。
|
||||
// 渲染输出到 ByteBuffer;不直接写文件,便于 LSP/测试。
|
||||
|
||||
type .{ .Error, .Status, .make } = @import("error.spl");
|
||||
type .{ .Vec } = @import("vec.spl");
|
||||
type .{ .ByteBuffer } = @import("byte_buffer.spl");
|
||||
type .{ .Str } = @import("str.spl");
|
||||
type .{ .append_usize } = @import("fmt.spl");
|
||||
|
||||
type Severity = enum {
|
||||
Debug,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Fatal,
|
||||
}
|
||||
|
||||
type DiagItem = struct {
|
||||
severity: Severity,
|
||||
line: usize,
|
||||
col: usize,
|
||||
fname: Str,
|
||||
message: Str,
|
||||
}
|
||||
|
||||
type Diag = @self;
|
||||
|
||||
items: Vec, // 元素类型 DiagItem
|
||||
errors: usize,
|
||||
warnings: usize,
|
||||
|
||||
fn new(): Diag {
|
||||
ret Diag {
|
||||
.items = Vec.blank(@sizeof(DiagItem), 8),
|
||||
.errors = 0,
|
||||
.warnings = 0,
|
||||
};
|
||||
}
|
||||
|
||||
fn item_at(self: *Diag, index: usize): *DiagItem {
|
||||
match Vec.get(&self.items, index) {
|
||||
.Ok(ptr) => { ret ptr as *DiagItem; },
|
||||
.Err(error) => { ret null; },
|
||||
}
|
||||
ret null;
|
||||
}
|
||||
|
||||
fn count(self: Diag): usize {
|
||||
ret self.items.len;
|
||||
}
|
||||
|
||||
fn error_count(self: Diag): usize {
|
||||
ret self.errors;
|
||||
}
|
||||
|
||||
fn warning_count(self: Diag): usize {
|
||||
ret self.warnings;
|
||||
}
|
||||
|
||||
fn has_error(self: Diag): bool {
|
||||
ret self.errors > 0;
|
||||
}
|
||||
|
||||
// 添加一条诊断;失败不改变已有内容。
|
||||
fn emit(self: *Diag, fname: []u8, line: usize, col: usize, severity: Severity,
|
||||
msg: []u8): Status {
|
||||
var item = DiagItem {
|
||||
.severity = severity,
|
||||
.line = line,
|
||||
.col = col,
|
||||
.fname = Str.new(),
|
||||
.message = Str.new(),
|
||||
};
|
||||
|
||||
match Str.from_slice(fname) {
|
||||
.Ok(v) => { item.fname = v; },
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
match Str.from_slice(msg) {
|
||||
.Ok(v) => { item.message = v; },
|
||||
.Err(error) => {
|
||||
Str.drop(&item.fname);
|
||||
ret .Err(error);
|
||||
},
|
||||
}
|
||||
match Vec.push(&self.items, &item as *u8) {
|
||||
.Ok => {},
|
||||
.Err(error) => {
|
||||
Str.drop(&item.fname);
|
||||
Str.drop(&item.message);
|
||||
ret .Err(error);
|
||||
},
|
||||
}
|
||||
|
||||
match severity {
|
||||
.Error => { self.errors = self.errors + 1; },
|
||||
.Fatal => { self.errors = self.errors + 1; },
|
||||
.Warning => { self.warnings = self.warnings + 1; },
|
||||
_ => {},
|
||||
}
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn err(self: *Diag, fname: []u8, line: usize, col: usize, msg: []u8): Status {
|
||||
ret emit(self, fname, line, col, .Error, msg);
|
||||
}
|
||||
|
||||
fn warn(self: *Diag, fname: []u8, line: usize, col: usize, msg: []u8): Status {
|
||||
ret emit(self, fname, line, col, .Warning, msg);
|
||||
}
|
||||
|
||||
fn info(self: *Diag, fname: []u8, line: usize, col: usize, msg: []u8): Status {
|
||||
ret emit(self, fname, line, col, .Info, msg);
|
||||
}
|
||||
|
||||
fn level_name(severity: Severity): []u8 {
|
||||
match severity {
|
||||
.Debug => { ret "debug"; },
|
||||
.Info => { ret "info"; },
|
||||
.Warning => { ret "warning"; },
|
||||
.Error => { ret "error"; },
|
||||
.Fatal => { ret "fatal"; },
|
||||
}
|
||||
ret "?";
|
||||
}
|
||||
|
||||
fn append_str(out: *ByteBuffer, s: *Str): Status {
|
||||
ret ByteBuffer.append(out, Str.as_slice(s));
|
||||
}
|
||||
|
||||
// 渲染为 "file:line:col: level: message\n"(file 为空时省略前缀)。
|
||||
fn render(self: *Diag, out: *ByteBuffer): Status {
|
||||
var i: usize = 0;
|
||||
while i < self.items.len {
|
||||
var it = item_at(self, i);
|
||||
if it == null { ret .Err(make(.OutOfRange)); }
|
||||
|
||||
if Str.size(it.fname) != 0 {
|
||||
match append_str(out, &it.fname) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append_byte(out, ':') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
}
|
||||
match append_usize(out, it.line, 10, false) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append_byte(out, ':') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_usize(out, it.col, 10, false) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, ": ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, level_name(it.severity)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(out, ": ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match append_str(out, &it.message) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append_byte(out, '\n') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
i = i + 1;
|
||||
}
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
// 丢弃所有条目内容但保留缓冲区。
|
||||
fn clear(self: *Diag) {
|
||||
var i: usize = 0;
|
||||
while i < self.items.len {
|
||||
var it = item_at(self, i);
|
||||
if it != null {
|
||||
Str.drop(&it.fname);
|
||||
Str.drop(&it.message);
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
Vec.clear(&self.items);
|
||||
self.errors = 0;
|
||||
self.warnings = 0;
|
||||
}
|
||||
|
||||
fn drop(self: *Diag) {
|
||||
var i: usize = 0;
|
||||
while i < self.items.len {
|
||||
var it = item_at(self, i);
|
||||
if it != null {
|
||||
Str.drop(&it.fname);
|
||||
Str.drop(&it.message);
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
Vec.drop(&self.items);
|
||||
self.errors = 0;
|
||||
self.warnings = 0;
|
||||
}
|
||||
|
||||
// ---- 测试辅助 ----
|
||||
|
||||
fn expect_ok(result: Status) {
|
||||
match result {
|
||||
.Ok => {},
|
||||
.Err(error) => { @assert(false, "unexpected diag error"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_bytes(out: *ByteBuffer, expected: []u8) {
|
||||
var actual = ByteBuffer.as_slice(out.*);
|
||||
@assert(actual.len == expected.len, "render length");
|
||||
var i: usize = 0;
|
||||
while i < expected.len {
|
||||
@assert(actual.ptr[i] == expected.ptr[i], "render byte");
|
||||
i = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn test_collect_and_count() {
|
||||
var d = new();
|
||||
@assert(count(d) == 0 && !has_error(d), "empty");
|
||||
|
||||
expect_ok(err(&d, "a.spl", 3, 5, "undefined identifier `x`"));
|
||||
expect_ok(warn(&d, "a.spl", 7, 1, "unused variable"));
|
||||
expect_ok(info(&d, "a.spl", 9, 2, "note"));
|
||||
expect_ok(err(&d, "", 1, 1, "fatal-ish"));
|
||||
|
||||
@assert(count(d) == 4, "count");
|
||||
@assert(error_count(d) == 2, "errors");
|
||||
@assert(warning_count(d) == 1, "warnings");
|
||||
@assert(has_error(d), "has_error");
|
||||
|
||||
var out = ByteBuffer.new();
|
||||
expect_ok(render(&d, &out));
|
||||
expect_bytes(&out, "a.spl:3:5: error: undefined identifier `x`\na.spl:7:1: warning: unused variable\na.spl:9:2: info: note\n1:1: error: fatal-ish\n");
|
||||
ByteBuffer.drop(&out);
|
||||
drop(&d);
|
||||
}
|
||||
|
||||
fn test_clear_and_reuse() {
|
||||
var d = new();
|
||||
expect_ok(err(&d, "f", 1, 1, "one"));
|
||||
expect_ok(warn(&d, "f", 2, 2, "two"));
|
||||
clear(&d);
|
||||
@assert(count(d) == 0 && !has_error(d) && warning_count(d) == 0, "cleared");
|
||||
|
||||
expect_ok(err(&d, "f", 3, 3, "three"));
|
||||
@assert(count(d) == 1 && error_count(d) == 1, "reuse");
|
||||
drop(&d);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
test_collect_and_count();
|
||||
test_clear_and_reuse();
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// 公共错误值不持有资源,可按值复制;不分配错误消息字符串。
|
||||
// position 是调用方输入中的从零开始的位置;仅在 has_position 时有效。
|
||||
// 未定位错误的 position 规范为 0,不能用 position == 0 判断是否有位置。
|
||||
|
||||
+314
-44
@@ -1,10 +1,13 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// 数值/布尔格式化,写入 ByteBuffer。radix 取值 2..36;uppercase 只影响 10..35 的字母形式。
|
||||
// 不分配额外内存(除目标 ByteBuffer 自身扩容),失败不产生部分输出。
|
||||
// 整数转换统一复用 MPA。先在临时缓冲区完成转换,再一次性追加,失败不产生部分输出。
|
||||
|
||||
type .{ .Error, .Status, .make } = @import("error.spl");
|
||||
type .{ .ByteBuffer } = @import("byte_buffer.spl");
|
||||
type .{ .digit_char } = @import("ascii.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 刻意不做常量求值/折叠,
|
||||
// 顶层 const 不应用于运行时表达式。
|
||||
@@ -12,53 +15,47 @@ fn valid_radix(radix: u8): bool {
|
||||
ret radix >= 2 && radix <= 36;
|
||||
}
|
||||
|
||||
fn append_u64(out: *ByteBuffer, value: u64, radix: u8, uppercase: bool): Status {
|
||||
fn append_big_int(out: *ByteBuffer, value: BigInt,
|
||||
radix: u8, uppercase: bool): Status {
|
||||
if !valid_radix(radix) { ret .Err(make(.InvalidArgument)); }
|
||||
|
||||
var digits: [64]u8 = undefined;
|
||||
var count: usize = 0;
|
||||
|
||||
if value == 0 {
|
||||
digits[0] = '0';
|
||||
count = 1;
|
||||
} else {
|
||||
var v = value;
|
||||
var base = radix as u64;
|
||||
while v != 0 {
|
||||
var d = (v % base) as u8;
|
||||
match digit_char(d, uppercase) {
|
||||
.Ok(c) => { digits[count] = c; },
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
count = count + 1;
|
||||
v = v / base;
|
||||
}
|
||||
}
|
||||
|
||||
// digits 是低位在前,反向写入得到正常顺序。
|
||||
var i = count;
|
||||
while i > 0 {
|
||||
i = i - 1;
|
||||
match ByteBuffer.append_byte(out, digits[i]) {
|
||||
var text = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&text); }
|
||||
match BigInt.format(&text, value, radix, uppercase) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
ret ByteBuffer.append(out, ByteBuffer.as_slice(text));
|
||||
}
|
||||
ret .Ok;
|
||||
|
||||
fn append_bits(out: *ByteBuffer, value: Bits, signed: bool,
|
||||
radix: u8, uppercase: bool): Status {
|
||||
var integer = BigInt.zero();
|
||||
defer { BigInt.drop(&integer); }
|
||||
match Bits.to_big(&integer, value, signed) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
ret append_big_int(out, integer, radix, uppercase);
|
||||
}
|
||||
|
||||
fn append_u64(out: *ByteBuffer, value: u64, radix: u8, uppercase: bool): Status {
|
||||
var integer = BigInt.zero();
|
||||
defer { BigInt.drop(&integer); }
|
||||
match BigInt.from_u64(&integer, value) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
ret append_big_int(out, integer, radix, uppercase);
|
||||
}
|
||||
|
||||
fn append_i64(out: *ByteBuffer, value: i64, radix: u8, uppercase: bool): Status {
|
||||
if !valid_radix(radix) { ret .Err(make(.InvalidArgument)); }
|
||||
if value < 0 {
|
||||
match ByteBuffer.append_byte(out, '-') {
|
||||
var integer = BigInt.zero();
|
||||
defer { BigInt.drop(&integer); }
|
||||
match BigInt.from_i64(&integer, value) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
// value 为补码,(0 - (value as u64)) 得到绝对值(含 i64 最小值)。
|
||||
var magnitude = 0 - (value as u64);
|
||||
ret append_u64(out, magnitude, radix, uppercase);
|
||||
}
|
||||
ret append_u64(out, value as u64, radix, uppercase);
|
||||
ret append_big_int(out, integer, radix, uppercase);
|
||||
}
|
||||
|
||||
fn append_usize(out: *ByteBuffer, value: usize, radix: u8, uppercase: bool): Status {
|
||||
@@ -74,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) {
|
||||
@@ -107,6 +256,7 @@ fn expect_text(buffer: *ByteBuffer, expected: []u8) {
|
||||
|
||||
fn test_unsigned() {
|
||||
var b = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&b); }
|
||||
|
||||
expect_ok(append_u64(&b, 0, 10, false));
|
||||
expect_text(&b, "0");
|
||||
@@ -147,12 +297,11 @@ fn test_unsigned() {
|
||||
expect_ok(append_u64(&b, 18446744073709551615, 2, false));
|
||||
expect_text(&b, "1111111111111111111111111111111111111111111111111111111111111111");
|
||||
ByteBuffer.clear(&b);
|
||||
|
||||
ByteBuffer.drop(&b);
|
||||
}
|
||||
|
||||
fn test_signed() {
|
||||
var b = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&b); }
|
||||
|
||||
expect_ok(append_i64(&b, 0, 10, false));
|
||||
expect_text(&b, "0");
|
||||
@@ -177,12 +326,11 @@ fn test_signed() {
|
||||
expect_ok(append_u64(&b, 48879, 16, true));
|
||||
expect_text(&b, "BEEF");
|
||||
ByteBuffer.clear(&b);
|
||||
|
||||
ByteBuffer.drop(&b);
|
||||
}
|
||||
|
||||
fn test_usize_bool_and_errors() {
|
||||
var b = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&b); }
|
||||
|
||||
expect_ok(append_usize(&b, 1000 as usize, 10, false));
|
||||
expect_text(&b, "1000");
|
||||
@@ -202,11 +350,48 @@ fn test_usize_bool_and_errors() {
|
||||
expect_invalid(append_i64(&b, -1, 0, false));
|
||||
@assert(ByteBuffer.size(b) == 0, "invalid radix writes nothing");
|
||||
|
||||
ByteBuffer.drop(&b);
|
||||
expect_ok(ByteBuffer.append(&b, "prefix"));
|
||||
expect_invalid(append_i64(&b, -123, 1, false));
|
||||
expect_text(&b, "prefix");
|
||||
}
|
||||
|
||||
fn test_mpa_integer_formatting() {
|
||||
var b = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&b); }
|
||||
|
||||
var huge = BigInt.zero();
|
||||
defer { BigInt.drop(&huge); }
|
||||
expect_ok(BigInt.parse(&huge, "1234567890123456789012345678901234567890", 10));
|
||||
expect_ok(append_big_int(&b, huge, 10, false));
|
||||
expect_text(&b, "1234567890123456789012345678901234567890");
|
||||
ByteBuffer.clear(&b);
|
||||
|
||||
expect_ok(BigInt.parse(&huge, "-ffffffffffffffffffffffffffffffff", 16));
|
||||
expect_ok(append_big_int(&b, huge, 16, true));
|
||||
expect_text(&b, "-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
|
||||
ByteBuffer.clear(&b);
|
||||
|
||||
var raw = BigInt.zero();
|
||||
defer { BigInt.drop(&raw); }
|
||||
expect_ok(BigInt.from_u64(&raw, 255));
|
||||
var bits: Bits = undefined;
|
||||
match Bits.new(8) {
|
||||
.Ok(value) => { bits = value; },
|
||||
.Err(error) => { @assert(false, "Bits construction"); },
|
||||
}
|
||||
defer { Bits.drop(&bits); }
|
||||
expect_ok(Bits.from_big_checked(&bits, raw, 8, false));
|
||||
|
||||
expect_ok(append_bits(&b, bits, false, 10, false));
|
||||
expect_text(&b, "255");
|
||||
ByteBuffer.clear(&b);
|
||||
expect_ok(append_bits(&b, bits, true, 10, false));
|
||||
expect_text(&b, "-1");
|
||||
}
|
||||
|
||||
fn test_composition_and_growth() {
|
||||
var b = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&b); }
|
||||
|
||||
expect_ok(ByteBuffer.append(&b, "x="));
|
||||
expect_ok(append_i64(&b, -42, 10, false));
|
||||
@@ -229,13 +414,98 @@ fn test_composition_and_growth() {
|
||||
var s = ByteBuffer.as_slice(b);
|
||||
@assert(s.ptr[0] == '0', "starts with 0");
|
||||
@assert(s.ptr[1] == ',', "0 then comma");
|
||||
}
|
||||
|
||||
ByteBuffer.drop(&b);
|
||||
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();
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// 哈希工具:FNV-1a 字节哈希 + 整型混合;并给出可直接交给 map 的回调
|
||||
// (map 的 hash/eq 回调接收指向键字节的指针,这里按 usize / ByteView 解释)。
|
||||
// 仅提供确定性哈希与等值,不承诺密码学性质。
|
||||
@@ -7,10 +8,11 @@ type .{ .ByteView } = @import("byte_view.spl");
|
||||
|
||||
// FNV-1a 64 位,逐字节;空输入返回 offset basis。
|
||||
fn hash_bytes(data: *u8, len: usize): usize {
|
||||
var h: usize = 14695981039346656037;
|
||||
var h: usize = 14695981039346656037; // FNV-1a 64 位 offset basis
|
||||
var prime: usize = 1099511628211; // FNV 质数
|
||||
var i: usize = 0;
|
||||
while i < len {
|
||||
h = (h ^ (data[i] as usize)) * 1099511628211;
|
||||
h = (h ^ (data[i] as usize)) * prime;
|
||||
i = i + 1;
|
||||
}
|
||||
ret h;
|
||||
@@ -22,12 +24,13 @@ fn hash_byte_view(view: ByteView): usize {
|
||||
|
||||
// 整型混合(splitmix 风格的移位-乘法),保证非零与良好扩散。
|
||||
fn hash_usize_value(value: usize): usize {
|
||||
var prime: usize = 1099511628211; // splitmix 风格的混合乘子
|
||||
var h = value;
|
||||
h = (h ^ (h >> 30)) * 1099511628211;
|
||||
h = (h ^ (h >> 30)) * prime;
|
||||
h = h ^ (h >> 27);
|
||||
h = h * 1099511628211;
|
||||
h = h * prime;
|
||||
h = h ^ (h >> 31);
|
||||
if h == 0 { ret 1099511628211; } // 0 映射到非零
|
||||
if h == 0 { ret prime; } // 0 映射到非零
|
||||
ret h;
|
||||
}
|
||||
|
||||
|
||||
+130
-38
@@ -1,14 +1,17 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// I/O 便利层(构建在 sys/file 之上):
|
||||
// - C 字符串路径/模式的 open 包装
|
||||
// - 整块读写(write_all / read_all)
|
||||
// - 精确读取、完整写入和普通文件整读
|
||||
// - stdout/stderr 打印
|
||||
// 说明:read_all 会按文件大小一次性读入 ByteBuffer;不处理增长中的文件。
|
||||
// 短读、短写后的零进展及关闭失败返回 Err(.Io);文件写入不承诺回滚。
|
||||
|
||||
type .{ .Error, .Status, .make } = @import("error.spl");
|
||||
type .{ .ByteView } = @import("byte_view.spl");
|
||||
type .{ .ByteBuffer } = @import("byte_buffer.spl");
|
||||
type File = @import("../sys/file.spl");
|
||||
type Mem = @import("../sys/mem.spl");
|
||||
|
||||
// 把 view 复制到以 NUL 结尾的临时缓冲,返回其 data(归属 tmp,调用后勿再用)。
|
||||
fn cstr(tmp: *ByteBuffer, view: ByteView): *u8 {
|
||||
@@ -31,59 +34,99 @@ fn open_cstr(path: ByteView, mode: ByteView): File {
|
||||
var mbuf = ByteBuffer.new();
|
||||
var p = cstr(&pbuf, path);
|
||||
var m = cstr(&mbuf, mode);
|
||||
var f = File.open(p, m);
|
||||
var f = File { .handle = null };
|
||||
if p != null && m != null { f = File.open(p, m); }
|
||||
ByteBuffer.drop(&pbuf);
|
||||
ByteBuffer.drop(&mbuf);
|
||||
ret f;
|
||||
}
|
||||
|
||||
// 反复写入直到写完;返回实际写入字节数(!= bytes.len 表示中途失败)。
|
||||
fn write_all(f: *File, bytes: []u8): u64 {
|
||||
var total: u64 = 0;
|
||||
var want = bytes.len as u64;
|
||||
while total < want {
|
||||
var offset = total as usize;
|
||||
var w = File.write(f, bytes.ptr + offset, want - total);
|
||||
if w == 0 { break; }
|
||||
total = total + w;
|
||||
// 反复写入直到完成。发生错误时文件可能已经包含前缀,不承诺回滚。
|
||||
fn write_all(f: *File, bytes: []u8): Status {
|
||||
if f == null || !File.valid(f) { ret .Err(make(.Io)); }
|
||||
if bytes.len != 0 && bytes.ptr == null { ret .Err(make(.InvalidArgument)); }
|
||||
var total: usize = 0;
|
||||
while total < bytes.len {
|
||||
var written = File.write(f, bytes.ptr + total, (bytes.len - total) as u64);
|
||||
if written == 0 || written > (bytes.len - total) as u64 {
|
||||
ret .Err(make(.Io));
|
||||
}
|
||||
ret total;
|
||||
total = total + written as usize;
|
||||
}
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
fn print(bytes: []u8) {
|
||||
fn print(bytes: []u8): Status {
|
||||
var out = File.stdout();
|
||||
write_all(&out, bytes);
|
||||
ret write_all(&out, bytes);
|
||||
}
|
||||
|
||||
fn println(bytes: []u8) {
|
||||
print(bytes);
|
||||
print("\n");
|
||||
fn println(bytes: []u8): Status {
|
||||
match print(bytes) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
ret print("\n");
|
||||
}
|
||||
|
||||
fn eprint(bytes: []u8) {
|
||||
fn eprint(bytes: []u8): Status {
|
||||
var err = File.stderr();
|
||||
write_all(&err, bytes);
|
||||
ret write_all(&err, bytes);
|
||||
}
|
||||
|
||||
// 读取整个文件到 out(先清空)。打开失败或不完整返回 Err(.Io)。
|
||||
// 仅供已经隔离的临时存储使用;失败时可能写入目标前缀。
|
||||
fn read_fully(f: *File, data: *u8, len: usize): Status {
|
||||
var total: usize = 0;
|
||||
while total < len {
|
||||
var got = File.read(f, data + total, len - total);
|
||||
if got == 0 || got > len - total { ret .Err(make(.Io)); }
|
||||
total = total + got;
|
||||
}
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
// 精确读取 bytes.len 字节。短读返回 Err(.Io),失败保持 bytes 原内容不变。
|
||||
fn read_exact(f: *File, bytes: []u8): Status {
|
||||
if f == null || !File.valid(f) { ret .Err(make(.Io)); }
|
||||
if bytes.len == 0 { ret .Ok; }
|
||||
if bytes.ptr == null { ret .Err(make(.InvalidArgument)); }
|
||||
|
||||
var temporary = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&temporary); }
|
||||
match ByteBuffer.reserve(&temporary, bytes.len) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
match read_fully(f, temporary.storage.data, bytes.len) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
Mem.copy(bytes.ptr as *any, temporary.storage.data as *any, bytes.len);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
// 读取整个普通文件到 out。打开、短读或关闭失败时保持 out 不变。
|
||||
fn read_all(path: ByteView, out: *ByteBuffer): Status {
|
||||
if out == null { ret .Err(make(.InvalidArgument)); }
|
||||
var f = open_cstr(path, ByteView.from_slice("rb"));
|
||||
if !File.valid(&f) { ret .Err(make(.Io)); }
|
||||
|
||||
var sz = File.size(&f);
|
||||
ByteBuffer.clear(out);
|
||||
var fresh = ByteBuffer.new();
|
||||
if sz != 0 {
|
||||
match ByteBuffer.reserve(out, sz as usize) {
|
||||
match ByteBuffer.reserve(&fresh, sz) {
|
||||
.Ok => {},
|
||||
.Err(error) => { File.close(&f); ret .Err(error); },
|
||||
.Err(error) => { File.close(&f); ByteBuffer.drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
var got = File.read(&f, out.storage.data, sz);
|
||||
out.storage.len = got as usize;
|
||||
File.close(&f);
|
||||
if got != sz { ret .Err(make(.Io)); }
|
||||
ret .Ok;
|
||||
match read_fully(&f, fresh.storage.data, sz) {
|
||||
.Ok => {},
|
||||
.Err(error) => { File.close(&f); ByteBuffer.drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
File.close(&f);
|
||||
fresh.storage.len = sz;
|
||||
}
|
||||
if File.close(&f) != 0 { ByteBuffer.drop(&fresh); ret .Err(make(.Io)); }
|
||||
ByteBuffer.drop(out);
|
||||
out.* = ByteBuffer.take(&fresh);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
@@ -93,14 +136,32 @@ fn view(text: []u8): ByteView {
|
||||
ret ByteView.from_slice(text);
|
||||
}
|
||||
|
||||
fn expect_ok(result: Status) {
|
||||
match result {
|
||||
.Ok => {},
|
||||
.Err(error) => { @assert(false, "unexpected io error"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_io(result: Status) {
|
||||
match result {
|
||||
.Ok => { @assert(false, "expected io error"); },
|
||||
.Err(error) => {
|
||||
match error.kind {
|
||||
.Io => {},
|
||||
_ => { @assert(false, "expected Io kind"); },
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn test_roundtrip() {
|
||||
var path = view("build/_io_roundtrip.tmp");
|
||||
|
||||
var f = open_cstr(path, view("wb"));
|
||||
@assert(File.valid(&f), "open wb");
|
||||
var w = write_all(&f, "hello\nworld");
|
||||
@assert(w == 11, "write_all length");
|
||||
File.close(&f);
|
||||
expect_ok(write_all(&f, "hello\nworld"));
|
||||
@assert(File.close(&f) == 0, "close writer");
|
||||
|
||||
var out = ByteBuffer.new();
|
||||
match read_all(path, &out) {
|
||||
@@ -118,14 +179,13 @@ fn test_append_and_missing() {
|
||||
|
||||
var f = open_cstr(path, view("wb"));
|
||||
@assert(File.valid(&f), "open wb");
|
||||
write_all(&f, "ab");
|
||||
File.close(&f);
|
||||
expect_ok(write_all(&f, "ab"));
|
||||
@assert(File.close(&f) == 0, "close initial writer");
|
||||
|
||||
f = open_cstr(path, view("ab"));
|
||||
@assert(File.valid(&f), "open ab");
|
||||
var w2 = write_all(&f, "cd");
|
||||
File.close(&f);
|
||||
@assert(w2 == 2, "append length");
|
||||
expect_ok(write_all(&f, "cd"));
|
||||
@assert(File.close(&f) == 0, "close append writer");
|
||||
|
||||
var out = ByteBuffer.new();
|
||||
match read_all(path, &out) {
|
||||
@@ -142,11 +202,43 @@ fn test_append_and_missing() {
|
||||
match error.kind { .Io => {}, _ => { @assert(false, "error kind"); } }
|
||||
},
|
||||
}
|
||||
got = ByteBuffer.as_slice(out);
|
||||
@assert(got.len == 4 && got.ptr[0] == 'a' && got.ptr[3] == 'd',
|
||||
"failed read_all preserves output");
|
||||
ByteBuffer.drop(&out);
|
||||
}
|
||||
|
||||
fn test_read_exact_and_errors() {
|
||||
var path = view("build/_io_exact.tmp");
|
||||
var f = open_cstr(path, view("wb"));
|
||||
@assert(File.valid(&f));
|
||||
expect_ok(write_all(&f, "xyz"));
|
||||
@assert(File.close(&f) == 0);
|
||||
|
||||
f = open_cstr(path, view("rb"));
|
||||
@assert(File.valid(&f));
|
||||
var too_long: [4]u8 = .[9, 8, 7, 6];
|
||||
expect_io(read_exact(&f, too_long[0..4]));
|
||||
@assert(too_long[0] == 9 && too_long[1] == 8 &&
|
||||
too_long[2] == 7 && too_long[3] == 6,
|
||||
"short read preserves destination");
|
||||
@assert(File.close(&f) == 0);
|
||||
|
||||
f = open_cstr(path, view("rb"));
|
||||
@assert(File.valid(&f));
|
||||
var exact: [3]u8 = .[0, 0, 0];
|
||||
expect_ok(read_exact(&f, exact[0..3]));
|
||||
@assert(exact[0] == 'x' && exact[1] == 'y' && exact[2] == 'z');
|
||||
@assert(File.close(&f) == 0);
|
||||
|
||||
var invalid = File { .handle = null };
|
||||
expect_io(write_all(&invalid, "x"));
|
||||
expect_io(read_exact(&invalid, exact[0..3]));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
test_roundtrip();
|
||||
test_append_and_missing();
|
||||
println("io: ok");
|
||||
test_read_exact_and_errors();
|
||||
expect_ok(println("io: ok"));
|
||||
}
|
||||
+75
-23
@@ -4,13 +4,14 @@
|
||||
// - 输入 ByteView,字符串/数字以“源切片”借用,不做解码,round-trip 保真;
|
||||
// - 节点池 Vec[JsonNode];数组/对象子项存 Vec[JsonChild],保留插入顺序;
|
||||
// - 错误带字节位置(error.spl);输出写 ByteBuffer;缩进/数字用 ByteBuffer/fmt。
|
||||
// 限制:不做字符串转义解码、无递归深度保护、数字不做数值化(原样保存)。
|
||||
// 限制:不做字符串转义解码、数字不做数值化(原样保存);递归/输出深度上限见 max_depth()。
|
||||
|
||||
type .{ .Error, .Status, .make, .at } = @import("error.spl");
|
||||
type .{ .ByteView, .ViewResult, .FindResult } = @import("byte_view.spl");
|
||||
type .{ .Vec } = @import("vec.spl");
|
||||
type .{ .ByteBuffer } = @import("byte_buffer.spl");
|
||||
type .{ .is_digit, .is_hex_digit, .is_space } = @import("ascii.spl");
|
||||
type .{ .checked_mul } = @import("memory.spl");
|
||||
type .{ .append_i64, .append_u64 } = @import("fmt.spl");
|
||||
|
||||
type JsonKind = enum {
|
||||
@@ -29,6 +30,10 @@ type JsonChild = struct {
|
||||
val: usize,
|
||||
}
|
||||
|
||||
// 节点:kind 决定有效载荷字段,其余字段保持空。访问方必须按 kind 读取:
|
||||
// Null/True/False -> text 空、children 空;
|
||||
// Number/String -> text 为源切片、children 空;
|
||||
// Array/Object -> children 保存子项、text 空。
|
||||
type JsonNode = struct {
|
||||
kind: JsonKind,
|
||||
text: ByteView,
|
||||
@@ -57,6 +62,11 @@ type Parser = struct {
|
||||
nodes: Vec,
|
||||
}
|
||||
|
||||
// 递归/输出深度上限:防止病态输入耗尽调用栈。256 层足够覆盖正常 JSON。
|
||||
fn max_depth(): usize {
|
||||
ret 256;
|
||||
}
|
||||
|
||||
fn blank_node(kind: JsonKind): JsonNode {
|
||||
ret JsonNode {
|
||||
.kind = kind,
|
||||
@@ -78,7 +88,7 @@ fn intern_node(p: *Parser, node: JsonNode): NodeResult {
|
||||
fn node_at(doc: *Json, index: usize): *JsonNode {
|
||||
match Vec.get(&doc.nodes, index) {
|
||||
.Ok(ptr) => { ret ptr as *JsonNode; },
|
||||
.Err(error) => { ret null; },
|
||||
.Err(error) => { ret null; }, // 越界/无效池统一返回 null,调用方判空
|
||||
}
|
||||
ret null;
|
||||
}
|
||||
@@ -122,6 +132,7 @@ fn parse_string_raw(p: *Parser): ViewResult {
|
||||
}
|
||||
}
|
||||
} else if c < 32 {
|
||||
// JSON 只禁止未转义的 U+0000..U+001F;0x7F 合法,不能用 ascii.is_control。
|
||||
ret .Err(at(.InvalidFormat, p.pos));
|
||||
}
|
||||
p.pos = p.pos + 1;
|
||||
@@ -192,7 +203,8 @@ fn parse_number(p: *Parser): NodeResult {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_array(p: *Parser): NodeResult {
|
||||
fn parse_array(p: *Parser, depth: usize): NodeResult {
|
||||
if depth >= max_depth() { ret .Err(at(.OutOfRange, p.pos)); }
|
||||
p.pos = p.pos + 1; // '['
|
||||
var kids = Vec.blank(@sizeof(JsonChild), 8);
|
||||
|
||||
@@ -201,11 +213,14 @@ fn parse_array(p: *Parser): NodeResult {
|
||||
p.pos = p.pos + 1;
|
||||
var empty = blank_node(.Array);
|
||||
empty.children = kids;
|
||||
ret intern_node(p, empty);
|
||||
match intern_node(p, empty) {
|
||||
.Ok(index) => { ret .Ok(index); },
|
||||
.Err(error) => { Vec.drop(&empty.children); ret .Err(error); },
|
||||
}
|
||||
}
|
||||
|
||||
while true {
|
||||
var element = parse_value(p);
|
||||
var element = parse_value(p, depth + 1);
|
||||
var index: usize = 0;
|
||||
match element {
|
||||
.Ok(v) => { index = v; },
|
||||
@@ -232,10 +247,14 @@ fn parse_array(p: *Parser): NodeResult {
|
||||
|
||||
var node = blank_node(.Array);
|
||||
node.children = kids;
|
||||
ret intern_node(p, node);
|
||||
match intern_node(p, node) {
|
||||
.Ok(index) => { ret .Ok(index); },
|
||||
.Err(error) => { Vec.drop(&node.children); ret .Err(error); },
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_object(p: *Parser): NodeResult {
|
||||
fn parse_object(p: *Parser, depth: usize): NodeResult {
|
||||
if depth >= max_depth() { ret .Err(at(.OutOfRange, p.pos)); }
|
||||
p.pos = p.pos + 1; // '{'
|
||||
var kids = Vec.blank(@sizeof(JsonChild), 8);
|
||||
|
||||
@@ -244,7 +263,10 @@ fn parse_object(p: *Parser): NodeResult {
|
||||
p.pos = p.pos + 1;
|
||||
var empty = blank_node(.Object);
|
||||
empty.children = kids;
|
||||
ret intern_node(p, empty);
|
||||
match intern_node(p, empty) {
|
||||
.Ok(index) => { ret .Ok(index); },
|
||||
.Err(error) => { Vec.drop(&empty.children); ret .Err(error); },
|
||||
}
|
||||
}
|
||||
|
||||
while true {
|
||||
@@ -262,7 +284,7 @@ fn parse_object(p: *Parser): NodeResult {
|
||||
}
|
||||
p.pos = p.pos + 1;
|
||||
|
||||
var value = parse_value(p);
|
||||
var value = parse_value(p, depth + 1);
|
||||
var index: usize = 0;
|
||||
match value {
|
||||
.Ok(v) => { index = v; },
|
||||
@@ -290,10 +312,13 @@ fn parse_object(p: *Parser): NodeResult {
|
||||
|
||||
var node = blank_node(.Object);
|
||||
node.children = kids;
|
||||
ret intern_node(p, node);
|
||||
match intern_node(p, node) {
|
||||
.Ok(index) => { ret .Ok(index); },
|
||||
.Err(error) => { Vec.drop(&node.children); ret .Err(error); },
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_value(p: *Parser): NodeResult {
|
||||
fn parse_value(p: *Parser, depth: usize): NodeResult {
|
||||
skip_ws(p);
|
||||
var c = peek(p);
|
||||
if c < 0 { ret .Err(at(.UnexpectedEnd, p.pos)); }
|
||||
@@ -309,8 +334,8 @@ fn parse_value(p: *Parser): NodeResult {
|
||||
node.text = view;
|
||||
ret intern_node(p, node);
|
||||
}
|
||||
if ch == '{' { ret parse_object(p); } // '{'
|
||||
if ch == '[' { ret parse_array(p); } // '['
|
||||
if ch == '{' { ret parse_object(p, depth); } // '{'
|
||||
if ch == '[' { ret parse_array(p, depth); } // '['
|
||||
if ch == 't' { ret parse_literal(p, "true", .True); }
|
||||
if ch == 'f' { ret parse_literal(p, "false", .False); }
|
||||
if ch == 'n' { ret parse_literal(p, "null", .Null); }
|
||||
@@ -325,6 +350,7 @@ fn drop_node_pool(pool: *Vec) {
|
||||
var n = ptr as *JsonNode;
|
||||
Vec.drop(&n.children);
|
||||
},
|
||||
// i < pool.len 且池有效,get 不会失败;失败时无额外资源可释放。
|
||||
.Err(error) => {},
|
||||
}
|
||||
i = i + 1;
|
||||
@@ -341,7 +367,7 @@ fn parse(src: ByteView): JsonResult {
|
||||
|
||||
skip_ws(&p);
|
||||
var root: usize = 0;
|
||||
var result = parse_value(&p);
|
||||
var result = parse_value(&p, 0);
|
||||
match result {
|
||||
.Ok(index) => { root = index; },
|
||||
.Err(error) => {
|
||||
@@ -367,12 +393,6 @@ fn root_index(doc: *Json): usize {
|
||||
ret doc.root;
|
||||
}
|
||||
|
||||
fn kind_of(doc: *Json, index: usize): JsonKind {
|
||||
var n = node_at(doc, index);
|
||||
if n == null { ret .Null; }
|
||||
ret n.kind;
|
||||
}
|
||||
|
||||
// 枚举 tag 取整(stage1 枚举 == 有缺陷,用 @enum2int 比较更稳妥)。
|
||||
fn kind_code(kind: JsonKind): usize {
|
||||
ret @enum2int(kind) as usize;
|
||||
@@ -424,6 +444,7 @@ fn object_find(doc: *Json, index: usize, key: ByteView): FindResult {
|
||||
var child = ptr as *JsonChild;
|
||||
if ByteView.eq(child.key, key) { ret .Found(child.val); }
|
||||
},
|
||||
// i < count == children.len,get 不会失败。
|
||||
.Err(error) => {},
|
||||
}
|
||||
i = i + 1;
|
||||
@@ -452,10 +473,16 @@ fn emit_indent(out: *ByteBuffer, indent: usize, depth: usize): Status {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
ret ByteBuffer.append_repeat(out, 32, depth * indent);
|
||||
var spaces: usize = 0;
|
||||
match checked_mul(depth, indent) {
|
||||
.Ok(value) => { spaces = value; },
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
ret ByteBuffer.append_repeat(out, 32, spaces);
|
||||
}
|
||||
|
||||
fn emit(out: *ByteBuffer, doc: *Json, index: usize, indent: usize, depth: usize): Status {
|
||||
if depth >= max_depth() { ret .Err(make(.OutOfRange)); }
|
||||
var n = node_at(doc, index);
|
||||
if n == null { ret .Err(make(.OutOfRange)); }
|
||||
|
||||
@@ -738,19 +765,44 @@ fn test_errors() {
|
||||
expect_parse_error(parse(ByteView.from_slice("01")), 1);
|
||||
}
|
||||
|
||||
fn test_depth_limit() {
|
||||
var out = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&out); }
|
||||
var i: usize = 0;
|
||||
while i < max_depth() + 8 {
|
||||
expect_ok(ByteBuffer.append_byte(&out, '['));
|
||||
i = i + 1;
|
||||
}
|
||||
i = 0;
|
||||
while i < max_depth() + 8 {
|
||||
expect_ok(ByteBuffer.append_byte(&out, ']'));
|
||||
i = i + 1;
|
||||
}
|
||||
match parse(ByteView.from_slice(ByteBuffer.as_slice(out))) {
|
||||
.Ok(doc) => { drop(&doc); @assert(false, "deep input should be rejected"); },
|
||||
.Err(error) => {
|
||||
match error.kind {
|
||||
.OutOfRange => {},
|
||||
_ => { @assert(false, "depth limit error kind"); },
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn test_construction_helpers() {
|
||||
var out = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&out); }
|
||||
expect_ok(ByteBuffer.append(&out, "{\"id\":"));
|
||||
expect_ok(write_u64(&out, 18446744073709551615));
|
||||
expect_ok(ByteBuffer.append(&out, ",\"n\":"));
|
||||
expect_ok(write_int(&out, -7));
|
||||
expect_ok(ByteBuffer.append(&out, "}"));
|
||||
expect_bytes(&out, "{\"id\":18446744073709551615,\"n\":-7}");
|
||||
ByteBuffer.drop(&out);
|
||||
}
|
||||
|
||||
fn test_large_array() {
|
||||
var out = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&out); }
|
||||
expect_ok(ByteBuffer.append_byte(&out, '[')); // '['
|
||||
var i: usize = 0;
|
||||
while i < 1000 {
|
||||
@@ -771,7 +823,6 @@ fn test_large_array() {
|
||||
},
|
||||
.Err(error) => { @assert(false, "parse large array"); },
|
||||
}
|
||||
ByteBuffer.drop(&out);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -780,6 +831,7 @@ fn main() {
|
||||
test_roundtrip_compact();
|
||||
test_pretty();
|
||||
test_errors();
|
||||
test_depth_limit();
|
||||
test_construction_helpers();
|
||||
test_large_array();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// LSP 传输帧:Content-Length 头 + 空行 + 正文。
|
||||
// 这里只做纯逻辑(写入/解析 ByteBuffer/ByteView),真正的 stdin/stdout I/O 由上层负责。
|
||||
|
||||
@@ -6,6 +7,8 @@ type .{ .Error, .Status, .make } = @import("error.spl");
|
||||
type .{ .ByteView } = @import("byte_view.spl");
|
||||
type .{ .ByteBuffer } = @import("byte_buffer.spl");
|
||||
type .{ .append_usize } = @import("fmt.spl");
|
||||
type .{ .checked_add, .checked_mul } = @import("memory.spl");
|
||||
type .{ .to_lower } = @import("ascii.spl");
|
||||
|
||||
type FrameResult = enum {
|
||||
Ok: usize, // 消耗的字节数
|
||||
@@ -13,17 +16,12 @@ type FrameResult = enum {
|
||||
Err: Error,
|
||||
}
|
||||
|
||||
fn ascii_lower(c: u8): u8 {
|
||||
if c >= 'A' && c <= 'Z' { ret c + ('a' - 'A'); }
|
||||
ret c;
|
||||
}
|
||||
|
||||
// data[j..] 是否以 word 开头(ASCII 大小写不敏感)。
|
||||
fn ci_prefix(data: *u8, len: usize, j: usize, word: []u8): bool {
|
||||
if j + word.len > len { ret false; }
|
||||
var i: usize = 0;
|
||||
while i < word.len {
|
||||
if ascii_lower(data[j + i]) != ascii_lower(word.ptr[i]) { ret false; }
|
||||
if to_lower(data[j + i]) != to_lower(word.ptr[i]) { ret false; }
|
||||
i = i + 1;
|
||||
}
|
||||
ret true;
|
||||
@@ -64,7 +62,16 @@ fn parse_frame(input: ByteView, body_out: *ByteBuffer): FrameResult {
|
||||
var n: usize = 0;
|
||||
var digits = 0;
|
||||
while k < term && data[k] >= '0' && data[k] <= '9' {
|
||||
n = n * 10 + (data[k] - '0') as usize;
|
||||
var digit = (data[k] - '0') as usize;
|
||||
var scaled: usize = 0;
|
||||
match checked_mul(n, 10) {
|
||||
.Ok(value) => { scaled = value; },
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
match checked_add(scaled, digit) {
|
||||
.Ok(value) => { n = value; },
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
digits = digits + 1;
|
||||
k = k + 1;
|
||||
}
|
||||
@@ -80,9 +87,15 @@ fn parse_frame(input: ByteView, body_out: *ByteBuffer): FrameResult {
|
||||
var body_start = term + 4;
|
||||
if len < body_start || content_len > len - body_start { ret .Incomplete; }
|
||||
|
||||
ByteBuffer.clear(body_out);
|
||||
// 先写入临时缓冲区,成功后再替换 body_out,失败不产生部分输出。
|
||||
var fresh = ByteBuffer.new();
|
||||
var body = data[body_start..body_start + content_len];
|
||||
match ByteBuffer.append(body_out, body) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
match ByteBuffer.append(&fresh, body) {
|
||||
.Ok => {},
|
||||
.Err(e) => { ByteBuffer.drop(&fresh); ret .Err(e); },
|
||||
}
|
||||
ByteBuffer.drop(body_out);
|
||||
body_out.* = ByteBuffer.take(&fresh);
|
||||
ret .Ok(body_start + content_len);
|
||||
}
|
||||
|
||||
@@ -107,11 +120,13 @@ fn expect_body(out: *ByteBuffer, expected: []u8) {
|
||||
|
||||
fn test_roundtrip() {
|
||||
var wire = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&wire); }
|
||||
expect_ok(write_frame(&wire, "{\"jsonrpc\":\"2.0\"}"));
|
||||
var bytes = ByteBuffer.as_slice(wire);
|
||||
@assert(bytes.len == 16 + 2 + 4 + 17, "wire length"); // "Content-Length: 17\r\n\r\n" + body
|
||||
|
||||
var body = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&body); }
|
||||
match parse_frame(ByteView.from_slice(bytes), &body) {
|
||||
.Ok(used) => {
|
||||
@assert(used == bytes.len, "consumed all");
|
||||
@@ -120,17 +135,17 @@ fn test_roundtrip() {
|
||||
.Incomplete => { @assert(false, "unexpected incomplete"); },
|
||||
.Err(error) => { @assert(false, "unexpected err"); },
|
||||
}
|
||||
ByteBuffer.drop(&body);
|
||||
ByteBuffer.drop(&wire);
|
||||
}
|
||||
|
||||
fn test_multiple_and_leftover() {
|
||||
var wire = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&wire); }
|
||||
expect_ok(write_frame(&wire, "abcd"));
|
||||
expect_ok(write_frame(&wire, "ef"));
|
||||
var bytes = ByteBuffer.as_slice(wire);
|
||||
|
||||
var body = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&body); }
|
||||
match parse_frame(ByteView.from_slice(bytes), &body) {
|
||||
.Ok(used) => {
|
||||
expect_body(&body, "abcd");
|
||||
@@ -142,12 +157,11 @@ fn test_multiple_and_leftover() {
|
||||
},
|
||||
_ => { @assert(false, "first frame"); },
|
||||
}
|
||||
ByteBuffer.drop(&body);
|
||||
ByteBuffer.drop(&wire);
|
||||
}
|
||||
|
||||
fn test_incomplete_and_errors() {
|
||||
var body = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&body); }
|
||||
|
||||
// 头未结束
|
||||
var h1 = "Content-Length: 3\r\n";
|
||||
@@ -174,7 +188,6 @@ fn test_incomplete_and_errors() {
|
||||
.Ok(used) => { expect_body(&body, "hi"); },
|
||||
_ => { @assert(false, "h4"); },
|
||||
}
|
||||
ByteBuffer.drop(&body);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
+136
-59
@@ -1,4 +1,5 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// 开放寻址 + tombstone 哈希表。键和值都按字节复制到表自己的存储。
|
||||
// 键类型由 hash_fn / eq_fn 解释:回调收到指向键字节的指针(如 *usize / *ByteView)。
|
||||
// 普通赋值只复制句柄(与 Vec 一样),所有权转移必须使用 take。
|
||||
@@ -7,11 +8,29 @@ type .{ .Error, .Status, .make } = @import("error.spl");
|
||||
type .{ .Vec } = @import("vec.spl");
|
||||
type .{ .ByteView } = @import("byte_view.spl");
|
||||
type .{ .hash_usize, .eq_usize, .hash_view, .eq_view } = @import("hash.spl");
|
||||
type .{ .checked_add, .checked_mul, .grow_capacity } = @import("memory.spl");
|
||||
type .{ .copy } = @import("../sys/mem.spl");
|
||||
|
||||
// 槽位状态:0=EMPTY,1=OCCUPIED,2=DELETED(墓碑,仅在扩容重哈希时清除)。
|
||||
// 槽位状态:Empty/Occupied/Deleted(墓碑,仅在扩容重哈希时清除)。
|
||||
// 负载因子 70/128。
|
||||
|
||||
type SlotState = enum {
|
||||
Empty,
|
||||
Occupied,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
fn state_code(state: SlotState): u8 {
|
||||
var s: SlotState = state;
|
||||
ret @enum2int(s) as u8;
|
||||
}
|
||||
|
||||
// 查找/删除结果:显式区分命中与缺失,替代 bool 哨兵。
|
||||
type LookupResult = enum {
|
||||
Found,
|
||||
NotFound,
|
||||
}
|
||||
|
||||
type Map = @self;
|
||||
|
||||
keys: Vec,
|
||||
@@ -64,12 +83,22 @@ fn slot_count(self: Map): usize {
|
||||
ret self.states.len;
|
||||
}
|
||||
|
||||
fn slot_state(self: Map, index: usize): u8 {
|
||||
ret self.states.data[index];
|
||||
fn slot_state(self: Map, index: usize): SlotState {
|
||||
var code = self.states.data[index];
|
||||
var occupied: SlotState = .Occupied;
|
||||
var deleted_state: SlotState = .Deleted;
|
||||
if code == state_code(occupied) { ret .Occupied; }
|
||||
if code == state_code(deleted_state) { ret .Deleted; }
|
||||
ret .Empty;
|
||||
}
|
||||
|
||||
fn slot_occupied(state: u8): bool {
|
||||
ret state == 1;
|
||||
fn slot_occupied(state: SlotState): bool {
|
||||
match state {
|
||||
.Occupied => { ret true; },
|
||||
.Empty => { ret false; },
|
||||
.Deleted => { ret false; },
|
||||
}
|
||||
ret false;
|
||||
}
|
||||
|
||||
fn key_at(self: *Map, index: usize): *u8 {
|
||||
@@ -85,25 +114,42 @@ fn find_slot(self: *Map, key: *u8): usize {
|
||||
if self.states.cap == 0 { ret ~(0 as usize); }
|
||||
var mask = self.states.cap - 1;
|
||||
var i = self.hash_fn(key) & mask;
|
||||
while self.states.data[i] != 0 {
|
||||
if self.states.data[i] == 1 && self.eq_fn(self.keys.data + i * self.key_size, key) {
|
||||
var empty: SlotState = .Empty;
|
||||
var occupied: SlotState = .Occupied;
|
||||
var probes: usize = 0;
|
||||
while probes < self.states.cap && self.states.data[i] != state_code(empty) {
|
||||
if self.states.data[i] == state_code(occupied) &&
|
||||
self.eq_fn(self.keys.data + i * self.key_size, key) {
|
||||
ret i;
|
||||
}
|
||||
i = (i + 1) & mask;
|
||||
probes = probes + 1;
|
||||
}
|
||||
ret ~(0 as usize);
|
||||
}
|
||||
|
||||
// 要求容量非零且键不存在,直接写入第一个空槽(重哈希专用)。
|
||||
// 要求容量非零且键不存在。优先复用探测链中的第一个墓碑。
|
||||
fn place(self: *Map, key: *u8, val: *u8) {
|
||||
var mask = self.states.cap - 1;
|
||||
var i = self.hash_fn(key) & mask;
|
||||
while self.states.data[i] != 0 {
|
||||
var empty: SlotState = .Empty;
|
||||
var deleted: SlotState = .Deleted;
|
||||
var occupied: SlotState = .Occupied;
|
||||
var first_deleted = ~(0 as usize);
|
||||
var probes: usize = 0;
|
||||
while probes < self.states.cap && self.states.data[i] != state_code(empty) {
|
||||
if self.states.data[i] == state_code(deleted) && first_deleted == ~(0 as usize) {
|
||||
first_deleted = i;
|
||||
}
|
||||
i = (i + 1) & mask;
|
||||
probes = probes + 1;
|
||||
}
|
||||
if first_deleted != ~(0 as usize) {
|
||||
i = first_deleted;
|
||||
}
|
||||
copy((self.keys.data + i * self.key_size) as *any, key as *any, self.key_size);
|
||||
copy((self.vals.data + i * self.val_size) as *any, val as *any, self.val_size);
|
||||
self.states.data[i] = 1;
|
||||
self.states.data[i] = state_code(occupied);
|
||||
self.size = self.size + 1;
|
||||
}
|
||||
|
||||
@@ -112,15 +158,7 @@ fn reserve(self: *Map, need: usize): Status {
|
||||
if !valid(self.*) { ret .Err(make(.InvalidArgument)); }
|
||||
if need <= self.states.cap { ret .Ok; }
|
||||
|
||||
var new_cap = self.states.cap;
|
||||
if new_cap < 8 { new_cap = 8; }
|
||||
while new_cap < need {
|
||||
if new_cap > (~(0 as usize)) / 2 {
|
||||
new_cap = need;
|
||||
break;
|
||||
}
|
||||
new_cap = new_cap * 2;
|
||||
}
|
||||
var new_cap = grow_capacity(self.states.cap, 8, need);
|
||||
|
||||
var fresh = self.*;
|
||||
fresh.keys = Vec.blank(self.key_size, self.keys.elem_align);
|
||||
@@ -145,14 +183,16 @@ fn reserve(self: *Map, need: usize): Status {
|
||||
fresh.states.len = new_cap;
|
||||
|
||||
var i: usize = 0;
|
||||
var empty: SlotState = .Empty;
|
||||
var occupied: SlotState = .Occupied;
|
||||
while i < new_cap {
|
||||
fresh.states.data[i] = 0;
|
||||
fresh.states.data[i] = state_code(empty);
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
i = 0;
|
||||
while i < self.states.len {
|
||||
if self.states.data[i] == 1 {
|
||||
if self.states.data[i] == state_code(occupied) {
|
||||
place(&fresh, self.keys.data + i * self.key_size,
|
||||
self.vals.data + i * self.val_size);
|
||||
}
|
||||
@@ -181,42 +221,64 @@ fn put(self: *Map, key: *u8, val: *u8): Status {
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
if self.states.cap == 0 || (self.size + 1) * 128 / self.states.cap >= 70 {
|
||||
var need = self.states.cap * 2;
|
||||
if self.states.cap == 0 { need = 8; }
|
||||
if self.states.cap == 0 {
|
||||
match reserve(self, 8) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
} else {
|
||||
var next_size: usize = 0;
|
||||
match checked_add(self.size, 1) {
|
||||
.Ok(value) => { next_size = value; },
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
var scaled: usize = 0;
|
||||
match checked_mul(next_size, 128) {
|
||||
.Ok(value) => { scaled = value; },
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
if scaled / self.states.cap >= 70 {
|
||||
var need: usize = 0;
|
||||
match checked_mul(self.states.cap, 2) {
|
||||
.Ok(value) => { need = value; },
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
match reserve(self, need) {
|
||||
.Ok => {},
|
||||
.Err(error) => { ret .Err(error); },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
place(self, key, val);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
// 找到键则把值复制到 out 并返回 true;否则 out 不变并返回 false。
|
||||
fn get(self: *Map, key: *u8, out: *u8): bool {
|
||||
if !valid(self.*) || key == null || out == null { ret false; }
|
||||
// 找到键则把值复制到 out 并返回 Found;否则 out 不变并返回 NotFound。
|
||||
fn get(self: *Map, key: *u8, out: *u8): LookupResult {
|
||||
if !valid(self.*) || key == null || out == null { ret .NotFound; }
|
||||
var i = find_slot(self, key);
|
||||
if i == ~(0 as usize) { ret false; }
|
||||
if i == ~(0 as usize) { ret .NotFound; }
|
||||
copy(out as *any, (self.vals.data + i * self.val_size) as *any, self.val_size);
|
||||
ret true;
|
||||
ret .Found;
|
||||
}
|
||||
|
||||
fn del(self: *Map, key: *u8): bool {
|
||||
if !valid(self.*) || key == null { ret false; }
|
||||
fn del(self: *Map, key: *u8): LookupResult {
|
||||
if !valid(self.*) || key == null { ret .NotFound; }
|
||||
var i = find_slot(self, key);
|
||||
if i == ~(0 as usize) { ret false; }
|
||||
self.states.data[i] = 2;
|
||||
if i == ~(0 as usize) { ret .NotFound; }
|
||||
var deleted_state: SlotState = .Deleted;
|
||||
self.states.data[i] = state_code(deleted_state);
|
||||
self.size = self.size - 1;
|
||||
ret true;
|
||||
ret .Found;
|
||||
}
|
||||
|
||||
// 清空内容但保留容量。墓碑与负载计数一并复位。
|
||||
fn clear(self: *Map) {
|
||||
var i: usize = 0;
|
||||
var empty: SlotState = .Empty;
|
||||
while i < self.states.len {
|
||||
self.states.data[i] = 0;
|
||||
self.states.data[i] = state_code(empty);
|
||||
i = i + 1;
|
||||
}
|
||||
self.size = 0;
|
||||
@@ -269,6 +331,22 @@ fn count_occupied(m: *Map): usize {
|
||||
ret n;
|
||||
}
|
||||
|
||||
fn found(m: *Map, key: *u8, out: *u8): bool {
|
||||
match get(m, key, out) {
|
||||
.Found => { ret true; },
|
||||
.NotFound => { ret false; },
|
||||
}
|
||||
ret false;
|
||||
}
|
||||
|
||||
fn deleted(m: *Map, key: *u8): bool {
|
||||
match del(m, key) {
|
||||
.Found => { ret true; },
|
||||
.NotFound => { ret false; },
|
||||
}
|
||||
ret false;
|
||||
}
|
||||
|
||||
fn make_int_map(): Map {
|
||||
ret new(@sizeof(usize), @alignof(usize), @sizeof(i32), @alignof(i32), hash_usize, eq_usize);
|
||||
}
|
||||
@@ -292,24 +370,26 @@ fn test_put_get_update() {
|
||||
@assert(size(m) == 1 && valid(m));
|
||||
|
||||
out = -1;
|
||||
@assert(get(&m, &key as *u8, &out as *u8) && out == 70, "insert then get");
|
||||
@assert(found(&m, &key as *u8, &out as *u8) && out == 70, "insert then get");
|
||||
|
||||
value = 71;
|
||||
expect_ok(put(&m, &key as *u8, &value as *u8));
|
||||
@assert(size(m) == 1, "update does not grow size");
|
||||
out = -1;
|
||||
@assert(get(&m, &key as *u8, &out as *u8) && out == 71, "update overwrites");
|
||||
@assert(found(&m, &key as *u8, &out as *u8) && out == 71, "update overwrites");
|
||||
|
||||
key = 8;
|
||||
out = 123;
|
||||
@assert(!get(&m, &key as *u8, &out as *u8) && out == 123, "missing key preserves out");
|
||||
@assert(!found(&m, &key as *u8, &out as *u8) && out == 123, "missing key preserves out");
|
||||
|
||||
// drop 后还有断言,故保持手动释放,不改为 defer。
|
||||
drop(&m);
|
||||
@assert(size(m) == 0 && capacity(m) == 0, "drop resets map");
|
||||
}
|
||||
|
||||
fn test_growth_and_delete() {
|
||||
var m = make_int_map();
|
||||
defer { drop(&m); }
|
||||
var i: i32 = 0;
|
||||
while i < 1000 {
|
||||
var key: usize = i as usize;
|
||||
@@ -324,7 +404,7 @@ fn test_growth_and_delete() {
|
||||
while i < 1000 {
|
||||
var key: usize = i as usize;
|
||||
var out: i32 = 0;
|
||||
@assert(get(&m, &key as *u8, &out as *u8), "get after growth");
|
||||
@assert(found(&m, &key as *u8, &out as *u8), "get after growth");
|
||||
@assert(out == i * 3 - 7, "value after growth");
|
||||
i = i + 1;
|
||||
}
|
||||
@@ -332,7 +412,7 @@ fn test_growth_and_delete() {
|
||||
i = 0;
|
||||
while i < 500 {
|
||||
var key: usize = i as usize;
|
||||
@assert(del(&m, &key as *u8));
|
||||
@assert(deleted(&m, &key as *u8));
|
||||
i = i + 1;
|
||||
}
|
||||
@assert(size(m) == 500 && count_occupied(&m) == 500, "deleted slots not occupied");
|
||||
@@ -341,7 +421,7 @@ fn test_growth_and_delete() {
|
||||
while i < 500 {
|
||||
var key: usize = i as usize;
|
||||
var out: i32 = 0;
|
||||
@assert(!get(&m, &key as *u8, &out as *u8), "deleted key missing");
|
||||
@assert(!found(&m, &key as *u8, &out as *u8), "deleted key missing");
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
@@ -351,41 +431,40 @@ fn test_growth_and_delete() {
|
||||
expect_ok(put(&m, &key as *u8, &value as *u8));
|
||||
@assert(size(m) == 501);
|
||||
var out: i32 = 0;
|
||||
@assert(get(&m, &key as *u8, &out as *u8) && out == 99, "reuse tombstone");
|
||||
@assert(found(&m, &key as *u8, &out as *u8) && out == 99, "reuse tombstone");
|
||||
|
||||
@assert(del(&m, &key as *u8), "delete key");
|
||||
@assert(deleted(&m, &key as *u8), "delete key");
|
||||
@assert(size(m) == 500);
|
||||
@assert(!get(&m, &key as *u8, &out as *u8));
|
||||
@assert(!found(&m, &key as *u8, &out as *u8));
|
||||
|
||||
clear(&m);
|
||||
@assert(size(m) == 0 && count_occupied(&m) == 0, "clear empties");
|
||||
|
||||
drop(&m);
|
||||
}
|
||||
|
||||
fn test_take_and_self_put() {
|
||||
var m = make_int_map();
|
||||
defer { drop(&m); }
|
||||
var key: usize = 5;
|
||||
var value: i32 = 55;
|
||||
expect_ok(put(&m, &key as *u8, &value as *u8));
|
||||
|
||||
var moved = take(&m);
|
||||
defer { drop(&moved); }
|
||||
@assert(size(m) == 0 && capacity(m) == 0, "take leaves source empty");
|
||||
var out: i32 = 0;
|
||||
@assert(get(&moved, &key as *u8, &out as *u8) && out == 55, "moved keeps content");
|
||||
@assert(found(&moved, &key as *u8, &out as *u8) && out == 55, "moved keeps content");
|
||||
|
||||
// 值指针可以指向表内已有的值(按字节移动,允许重叠)。
|
||||
var alias: i32 = 0;
|
||||
@assert(get(&moved, &key as *u8, &alias as *u8));
|
||||
@assert(found(&moved, &key as *u8, &alias as *u8));
|
||||
expect_ok(put(&moved, &key as *u8, &alias as *u8));
|
||||
@assert(get(&moved, &key as *u8, &out as *u8) && out == 55, "self value put");
|
||||
|
||||
drop(&moved);
|
||||
@assert(found(&moved, &key as *u8, &out as *u8) && out == 55, "self value put");
|
||||
}
|
||||
|
||||
fn test_byte_view_keys() {
|
||||
var m = new(@sizeof(ByteView), @alignof(ByteView), @sizeof(i32), @alignof(i32),
|
||||
hash_view, eq_view);
|
||||
defer { drop(&m); }
|
||||
|
||||
var alpha = ByteView.from_slice("alpha");
|
||||
var beta = ByteView.from_slice("beta");
|
||||
@@ -400,21 +479,19 @@ fn test_byte_view_keys() {
|
||||
@assert(size(m) == 3);
|
||||
|
||||
var out: i32 = 0;
|
||||
@assert(get(&m, &alpha as *u8, &out as *u8) && out == 1, "alpha");
|
||||
@assert(get(&m, &beta as *u8, &out as *u8) && out == 2, "beta");
|
||||
@assert(get(&m, &gamma as *u8, &out as *u8) && out == 3, "gamma");
|
||||
@assert(found(&m, &alpha as *u8, &out as *u8) && out == 1, "alpha");
|
||||
@assert(found(&m, &beta as *u8, &out as *u8) && out == 2, "beta");
|
||||
@assert(found(&m, &gamma as *u8, &out as *u8) && out == 3, "gamma");
|
||||
|
||||
// 内容相同的不同 ByteView 视为同一个键。
|
||||
var alpha2 = ByteView.from_slice("alpha");
|
||||
out = 0;
|
||||
@assert(get(&m, &alpha2 as *u8, &out as *u8) && out == 1, "equal content key");
|
||||
@assert(found(&m, &alpha2 as *u8, &out as *u8) && out == 1, "equal content key");
|
||||
@assert(size(m) == 3, "equal content does not insert");
|
||||
|
||||
@assert(del(&m, &beta as *u8), "delete beta");
|
||||
@assert(deleted(&m, &beta as *u8), "delete beta");
|
||||
@assert(size(m) == 2);
|
||||
@assert(!get(&m, &beta as *u8, &out as *u8));
|
||||
|
||||
drop(&m);
|
||||
@assert(!found(&m, &beta as *u8, &out as *u8));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// 尺寸计算和字节移动;不分配、不释放内存,不改变任何资源的所有权。
|
||||
|
||||
type .{ .Error, .Status, .make } = @import("error.spl");
|
||||
@@ -31,6 +32,22 @@ fn align_up(size: usize, alignment: usize): SizeResult {
|
||||
ret .Ok((size + mask) & ~mask);
|
||||
}
|
||||
|
||||
// 计算至少 need 的容量:从 current 起(不低于 min_cap)按倍增增长;
|
||||
// 无法再倍增(会溢出)时直接取 need。need 可表示时不溢出。
|
||||
fn grow_capacity(current: usize, min_cap: usize, need: usize): usize {
|
||||
var cap = current;
|
||||
if cap < min_cap { cap = min_cap; }
|
||||
var limit: usize = ~(0 as usize);
|
||||
while cap < need {
|
||||
if cap > limit / 2 {
|
||||
cap = need;
|
||||
break;
|
||||
}
|
||||
cap = cap * 2;
|
||||
}
|
||||
ret cap;
|
||||
}
|
||||
|
||||
// 将 src.len 个字节复制到 dst 的开头,允许两个切片任意重叠。
|
||||
// 零长度源不访问内存,允许空指针。所有可恢复错误均在首次写入前返回。
|
||||
// 非空源要求可读,目标前 src.len 字节要求可写,生命周期由调用方保证。
|
||||
@@ -164,6 +181,13 @@ fn test_sizes() {
|
||||
expect_size(align_up(1, highest_power), highest_power);
|
||||
expect_size(align_up(highest_power, highest_power), highest_power);
|
||||
expect_overflow(align_up(highest_power + 1, highest_power));
|
||||
|
||||
@assert(grow_capacity(0, 8, 0) == 8, "grow respects minimum");
|
||||
@assert(grow_capacity(0, 8, 5) == 8, "grow minimum covers need");
|
||||
@assert(grow_capacity(8, 8, 9) == 16, "grow doubles");
|
||||
@assert(grow_capacity(8, 8, 100) == 128, "grow repeats");
|
||||
@assert(grow_capacity(limit / 2, 8, limit) == limit, "grow exact to limit");
|
||||
@assert(grow_capacity(limit / 2 + 1, 8, limit) == limit, "grow clamps to need");
|
||||
}
|
||||
|
||||
// 穷举同一 8 字节数组内的所有合法源、目标和长度组合。
|
||||
|
||||
+1643
File diff suppressed because it is too large.
Load diff
+321
-152
@@ -1,231 +1,400 @@
|
||||
// Expect retcode = 0
|
||||
// 路径工具(不访问文件系统):同时接受 '/' 与 '\\';支持 Windows 盘符 "C:"。
|
||||
// 输入为借用 ByteView,输出写入调用方的 ByteBuffer。
|
||||
// 注意:normalize 对相对路径的前导 ".." 采取丢弃(不保留),适用于导入路径场景。
|
||||
|
||||
type .{ .Error, .Status, .make } = @import("error.spl");
|
||||
// 纯词法路径工具,不访问文件系统。调用方必须显式选择 POSIX 或 Windows 规则;
|
||||
// 输入视图均为借用,拥有型结果写入 ByteBuffer,失败保持原输出不变。
|
||||
type .{ .Status, .make } = @import("error.spl");
|
||||
type .{ .ByteView } = @import("byte_view.spl");
|
||||
type .{ .ByteBuffer } = @import("byte_buffer.spl");
|
||||
type .{ .Vec } = @import("vec.spl");
|
||||
type .{ .is_alpha } = @import("ascii.spl");
|
||||
|
||||
fn is_sep(c: u8): bool {
|
||||
ret c == '/' || c == '\\'; // '/' '\\'
|
||||
type PathStyle = enum { Posix, Windows }
|
||||
|
||||
fn is_windows(style: PathStyle): bool {
|
||||
match style { .Posix => { ret false; }, .Windows => { ret true; }, }
|
||||
ret false;
|
||||
}
|
||||
|
||||
fn is_drive(path: ByteView): bool {
|
||||
ret path.len >= 2 && path.ptr[1] == ':'; // ':'
|
||||
fn separator(style: PathStyle): u8 {
|
||||
if is_windows(style) { ret '\\'; }
|
||||
ret '/';
|
||||
}
|
||||
|
||||
fn is_absolute(path: ByteView): bool {
|
||||
fn is_sep(style: PathStyle, byte: u8): bool {
|
||||
if is_windows(style) { ret byte == '/' || byte == '\\'; }
|
||||
ret byte == '/';
|
||||
}
|
||||
|
||||
fn is_drive_prefix(path: ByteView): bool {
|
||||
ret path.len >= 2 && is_alpha(path.ptr[0]) && path.ptr[1] == ':';
|
||||
}
|
||||
|
||||
fn is_unc(path: ByteView, style: PathStyle): bool {
|
||||
ret is_windows(style) && path.len >= 2 &&
|
||||
is_sep(style, path.ptr[0]) && is_sep(style, path.ptr[1]);
|
||||
}
|
||||
|
||||
fn is_absolute(path: ByteView, style: PathStyle): bool {
|
||||
if path.len == 0 { ret false; }
|
||||
if is_sep(path.ptr[0]) { ret true; }
|
||||
ret is_drive(path);
|
||||
if !is_windows(style) { ret path.ptr[0] == '/'; }
|
||||
if is_unc(path, style) { ret true; }
|
||||
if is_drive_prefix(path) {
|
||||
ret path.len >= 3 && is_sep(style, path.ptr[2]);
|
||||
}
|
||||
// 根相对路径以当前盘符为根,本模块将其视为绝对。
|
||||
ret is_sep(style, path.ptr[0]);
|
||||
}
|
||||
|
||||
// 最后一个分隔符下标;无返回 ~0。
|
||||
fn last_sep(path: ByteView): usize {
|
||||
// UNC 根长度不含共享名后的分隔符;不完整时返回 0。
|
||||
fn unc_root_len(path: ByteView, style: PathStyle): usize {
|
||||
if !is_unc(path, style) { ret 0; }
|
||||
var i: usize = 2;
|
||||
while i < path.len && is_sep(style, path.ptr[i]) { i = i + 1; }
|
||||
var server = i;
|
||||
while i < path.len && !is_sep(style, path.ptr[i]) { i = i + 1; }
|
||||
if i == server { ret 0; }
|
||||
while i < path.len && is_sep(style, path.ptr[i]) { i = i + 1; }
|
||||
var share = i;
|
||||
while i < path.len && !is_sep(style, path.ptr[i]) { i = i + 1; }
|
||||
if i == share { ret 0; }
|
||||
ret i;
|
||||
}
|
||||
|
||||
fn root_len(path: ByteView, style: PathStyle): usize {
|
||||
if path.len == 0 { ret 0; }
|
||||
if !is_windows(style) {
|
||||
if path.ptr[0] == '/' { ret 1; }
|
||||
ret 0;
|
||||
}
|
||||
var unc = unc_root_len(path, style);
|
||||
if unc != 0 { ret unc; }
|
||||
if is_drive_prefix(path) {
|
||||
if path.len >= 3 && is_sep(style, path.ptr[2]) { ret 3; }
|
||||
ret 2;
|
||||
}
|
||||
if is_sep(style, path.ptr[0]) { ret 1; }
|
||||
ret 0;
|
||||
}
|
||||
|
||||
fn last_sep(path: ByteView, style: PathStyle): usize {
|
||||
var i = path.len;
|
||||
while i > 0 {
|
||||
i = i - 1;
|
||||
if is_sep(path.ptr[i]) { ret i; }
|
||||
if is_sep(style, path.ptr[i]) { ret i; }
|
||||
}
|
||||
ret ~(0 as usize);
|
||||
}
|
||||
|
||||
// 目录部分(不含末尾分隔符);根 "/" 返回 "/";无分隔符返回空。
|
||||
fn dirname(path: ByteView): ByteView {
|
||||
var idx = last_sep(path);
|
||||
if idx == ~(0 as usize) { ret ByteView.empty(); }
|
||||
if idx == 0 {
|
||||
var r = ByteView.slice(path, 0, 1);
|
||||
match r {
|
||||
.Ok(v) => { ret v; },
|
||||
.Err(error) => { ret ByteView.empty(); },
|
||||
}
|
||||
}
|
||||
var r = ByteView.slice(path, 0, idx);
|
||||
match r {
|
||||
.Ok(v) => { ret v; },
|
||||
fn slice_or_empty(path: ByteView, start: usize, end: usize): ByteView {
|
||||
match ByteView.slice(path, start, end) {
|
||||
.Ok(value) => { ret value; },
|
||||
.Err(error) => { ret ByteView.empty(); },
|
||||
}
|
||||
ret ByteView.empty();
|
||||
}
|
||||
|
||||
fn basename(path: ByteView): ByteView {
|
||||
var idx = last_sep(path);
|
||||
if idx == ~(0 as usize) { ret path; }
|
||||
var r = ByteView.slice(path, idx + 1, path.len);
|
||||
match r {
|
||||
.Ok(v) => { ret v; },
|
||||
.Err(error) => { ret ByteView.empty(); },
|
||||
fn dirname(path: ByteView, style: PathStyle): ByteView {
|
||||
var root = root_len(path, style);
|
||||
if root != 0 && path.len == root { ret path; }
|
||||
var index = last_sep(path, style);
|
||||
if index == ~(0 as usize) {
|
||||
if is_windows(style) && is_drive_prefix(path) {
|
||||
ret slice_or_empty(path, 0, 2);
|
||||
}
|
||||
ret ByteView.empty();
|
||||
}
|
||||
if root != 0 && index <= root { ret slice_or_empty(path, 0, root); }
|
||||
if index == 0 { ret slice_or_empty(path, 0, 1); }
|
||||
ret slice_or_empty(path, 0, index);
|
||||
}
|
||||
|
||||
// 扩展名(不含 '.');无扩展名返回空。
|
||||
fn extension(path: ByteView): ByteView {
|
||||
var base = basename(path);
|
||||
fn basename(path: ByteView, style: PathStyle): ByteView {
|
||||
var root = root_len(path, style);
|
||||
if root != 0 && path.len == root { ret ByteView.empty(); }
|
||||
var index = last_sep(path, style);
|
||||
if index == ~(0 as usize) {
|
||||
if is_windows(style) && is_drive_prefix(path) {
|
||||
ret slice_or_empty(path, 2, path.len);
|
||||
}
|
||||
ret path;
|
||||
}
|
||||
ret slice_or_empty(path, index + 1, path.len);
|
||||
}
|
||||
|
||||
fn extension(path: ByteView, style: PathStyle): ByteView {
|
||||
var base = basename(path, style);
|
||||
var i = base.len;
|
||||
while i > 0 {
|
||||
i = i - 1;
|
||||
if base.ptr[i] == '.' { // '.'
|
||||
if base.ptr[i] == '.' {
|
||||
if i == 0 { ret ByteView.empty(); }
|
||||
var r = ByteView.slice(base, i + 1, base.len);
|
||||
match r {
|
||||
.Ok(v) => { ret v; },
|
||||
.Err(error) => { ret ByteView.empty(); },
|
||||
}
|
||||
ret slice_or_empty(base, i + 1, base.len);
|
||||
}
|
||||
}
|
||||
ret ByteView.empty();
|
||||
}
|
||||
|
||||
// out = dir + sep + rel(rel 绝对时直接使用 rel);先清空 out。
|
||||
fn join_into(out: *ByteBuffer, dir: ByteView, rel: ByteView): Status {
|
||||
ByteBuffer.clear(out);
|
||||
if is_absolute(rel) {
|
||||
ret ByteBuffer.append(out, ByteView.as_slice(rel));
|
||||
// Windows 盘符相对路径依赖进程的盘符工作目录,join 明确拒绝该歧义。
|
||||
fn join_into(out: *ByteBuffer, dir: ByteView,
|
||||
rel: ByteView, style: PathStyle): Status {
|
||||
if out == null { ret .Err(make(.InvalidArgument)); }
|
||||
var fresh = ByteBuffer.new();
|
||||
if is_absolute(rel, style) {
|
||||
match ByteBuffer.append(&fresh, ByteView.as_slice(rel)) {
|
||||
.Ok => {}, .Err(error) => { ByteBuffer.drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
match ByteBuffer.append(out, ByteView.as_slice(dir)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
if dir.len != 0 && !is_sep(dir.ptr[dir.len - 1]) {
|
||||
match ByteBuffer.append_byte(out, '/') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||||
ByteBuffer.drop(out);
|
||||
out.* = ByteBuffer.take(&fresh);
|
||||
ret .Ok;
|
||||
}
|
||||
ret ByteBuffer.append(out, ByteView.as_slice(rel));
|
||||
if is_windows(style) && is_drive_prefix(rel) {
|
||||
ByteBuffer.drop(&fresh);
|
||||
ret .Err(make(.InvalidArgument));
|
||||
}
|
||||
|
||||
// 折叠重复分隔符、'.',及'..'(回退上一段;相对路径前导 '..' 丢弃)。
|
||||
fn normalize_into(out: *ByteBuffer, path: ByteView): Status {
|
||||
ByteBuffer.clear(out);
|
||||
if path.len == 0 { ret .Ok; }
|
||||
|
||||
var starts = Vec.blank(@sizeof(usize), 8);
|
||||
var i: usize = 0;
|
||||
|
||||
if is_sep(path.ptr[0]) {
|
||||
match ByteBuffer.append_byte(out, '/') { .Ok => {}, .Err(e) => { Vec.drop(&starts); ret .Err(e); } }
|
||||
} else if is_drive(path) {
|
||||
match ByteBuffer.append_byte(out, path.ptr[0]) { .Ok => {}, .Err(e) => { Vec.drop(&starts); ret .Err(e); } }
|
||||
match ByteBuffer.append_byte(out, ':') { .Ok => {}, .Err(e) => { Vec.drop(&starts); ret .Err(e); } }
|
||||
i = 2; // 跳过盘符
|
||||
match ByteBuffer.append(&fresh, ByteView.as_slice(dir)) {
|
||||
.Ok => {}, .Err(error) => { ByteBuffer.drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
|
||||
while i < path.len {
|
||||
while i < path.len && is_sep(path.ptr[i]) { i = i + 1; }
|
||||
if i >= path.len { break; }
|
||||
var start = i;
|
||||
while i < path.len && !is_sep(path.ptr[i]) { i = i + 1; }
|
||||
|
||||
var seg_result = ByteView.slice(path, start, i);
|
||||
var seg = ByteView.empty();
|
||||
match seg_result {
|
||||
.Ok(v) => { seg = v; },
|
||||
.Err(error) => { Vec.drop(&starts); ret .Err(error); },
|
||||
}
|
||||
|
||||
if ByteView.eq(seg, ByteView.from_slice(".")) { continue; }
|
||||
if ByteView.eq(seg, ByteView.from_slice("..")) {
|
||||
if starts.len > 0 {
|
||||
var idx = starts.len - 1;
|
||||
match Vec.get(&starts, idx) {
|
||||
.Ok(p) => { out.storage.len = (p as *usize).*; },
|
||||
.Err(error) => {},
|
||||
}
|
||||
starts.len = starts.len - 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var comp_start = out.storage.len;
|
||||
if comp_start > 0 && !is_sep(out.storage.data[comp_start - 1]) {
|
||||
match ByteBuffer.append_byte(out, '/') { .Ok => {}, .Err(e) => { Vec.drop(&starts); ret .Err(e); } }
|
||||
}
|
||||
var recorded = comp_start;
|
||||
match Vec.push(&starts, &recorded as *u8) {
|
||||
.Ok => {},
|
||||
.Err(error) => { Vec.drop(&starts); ret .Err(error); },
|
||||
}
|
||||
match ByteBuffer.append(out, ByteView.as_slice(seg)) {
|
||||
.Ok => {},
|
||||
.Err(error) => { Vec.drop(&starts); ret .Err(error); },
|
||||
var drive_relative_base = is_windows(style) && dir.len == 2 && is_drive_prefix(dir);
|
||||
if dir.len != 0 && !is_sep(style, dir.ptr[dir.len - 1]) && rel.len != 0 &&
|
||||
!drive_relative_base {
|
||||
match ByteBuffer.append_byte(&fresh, separator(style)) {
|
||||
.Ok => {}, .Err(error) => { ByteBuffer.drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
}
|
||||
|
||||
Vec.drop(&starts);
|
||||
match ByteBuffer.append(&fresh, ByteView.as_slice(rel)) {
|
||||
.Ok => {}, .Err(error) => { ByteBuffer.drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
ByteBuffer.drop(out);
|
||||
out.* = ByteBuffer.take(&fresh);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
// ---- 测试辅助 ----
|
||||
fn segment_is_parent(buffer: ByteBuffer, rollback: usize, style: PathStyle): bool {
|
||||
var start = rollback;
|
||||
if start < buffer.storage.len && is_sep(style, buffer.storage.data[start]) {
|
||||
start = start + 1;
|
||||
}
|
||||
ret buffer.storage.len - start == 2 &&
|
||||
buffer.storage.data[start] == '.' && buffer.storage.data[start + 1] == '.';
|
||||
}
|
||||
|
||||
fn expect_ok(result: Status) {
|
||||
match result {
|
||||
fn append_segment(out: *ByteBuffer, starts: *Vec,
|
||||
segment: ByteView, style: PathStyle): Status {
|
||||
var rollback = out.storage.len;
|
||||
if rollback != 0 && !is_sep(style, out.storage.data[rollback - 1]) &&
|
||||
!(is_windows(style) && out.storage.data[rollback - 1] == ':') {
|
||||
match ByteBuffer.append_byte(out, separator(style)) {
|
||||
.Ok => {}, .Err(error) => { ret .Err(error); },
|
||||
}
|
||||
}
|
||||
match Vec.push(starts, &rollback as *u8) {
|
||||
.Ok => {},
|
||||
.Err(error) => { @assert(false, "unexpected path error"); },
|
||||
.Err(error) => { out.storage.len = rollback; ret .Err(error); },
|
||||
}
|
||||
match ByteBuffer.append(out, ByteView.as_slice(segment)) {
|
||||
.Ok => {},
|
||||
.Err(error) => {
|
||||
starts.len = starts.len - 1;
|
||||
out.storage.len = rollback;
|
||||
ret .Err(error);
|
||||
},
|
||||
}
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
// 规范化分隔符并折叠 '.'/'..'。相对路径无法消去的前导 '..' 会被保留;
|
||||
// 绝对路径的 '..' 不会越过根,UNC 的 server/share 也不可被消去。
|
||||
fn normalize_into(out: *ByteBuffer, path: ByteView, style: PathStyle): Status {
|
||||
if out == null { ret .Err(make(.InvalidArgument)); }
|
||||
var fresh = ByteBuffer.new();
|
||||
var starts = Vec.blank(@sizeof(usize), @alignof(usize));
|
||||
var i: usize = 0;
|
||||
var absolute = false;
|
||||
var protected: usize = 0;
|
||||
|
||||
if path.len != 0 {
|
||||
if !is_windows(style) && path.ptr[0] == '/' {
|
||||
absolute = true;
|
||||
match ByteBuffer.append_byte(&fresh, '/') {
|
||||
.Ok => {}, .Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
while i < path.len && is_sep(style, path.ptr[i]) { i = i + 1; }
|
||||
} else if is_unc(path, style) {
|
||||
absolute = true;
|
||||
protected = 2;
|
||||
match ByteBuffer.append(&fresh, "\\\\") {
|
||||
.Ok => {}, .Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
i = 2;
|
||||
while i < path.len && is_sep(style, path.ptr[i]) { i = i + 1; }
|
||||
} else if is_windows(style) && is_drive_prefix(path) {
|
||||
match ByteBuffer.append(&fresh, path.ptr[0..2]) {
|
||||
.Ok => {}, .Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
i = 2;
|
||||
if i < path.len && is_sep(style, path.ptr[i]) {
|
||||
absolute = true;
|
||||
match ByteBuffer.append_byte(&fresh, '\\') {
|
||||
.Ok => {}, .Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
while i < path.len && is_sep(style, path.ptr[i]) { i = i + 1; }
|
||||
}
|
||||
} else if is_windows(style) && is_sep(style, path.ptr[0]) {
|
||||
absolute = true;
|
||||
match ByteBuffer.append_byte(&fresh, '\\') {
|
||||
.Ok => {}, .Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
while i < path.len && is_sep(style, path.ptr[i]) { i = i + 1; }
|
||||
}
|
||||
}
|
||||
|
||||
fn view(text: []u8): ByteView {
|
||||
ret ByteView.from_slice(text);
|
||||
while i < path.len {
|
||||
while i < path.len && is_sep(style, path.ptr[i]) { i = i + 1; }
|
||||
if i >= path.len { break; }
|
||||
var begin = i;
|
||||
while i < path.len && !is_sep(style, path.ptr[i]) { i = i + 1; }
|
||||
var segment = slice_or_empty(path, begin, i);
|
||||
|
||||
if ByteView.eq(segment, ByteView.from_slice(".")) {
|
||||
if protected != 0 && starts.len < protected {
|
||||
Vec.drop(&starts); ByteBuffer.drop(&fresh);
|
||||
ret .Err(make(.InvalidArgument));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ByteView.eq(segment, ByteView.from_slice("..")) {
|
||||
if protected != 0 && starts.len < protected {
|
||||
Vec.drop(&starts); ByteBuffer.drop(&fresh);
|
||||
ret .Err(make(.InvalidArgument));
|
||||
}
|
||||
if starts.len > protected {
|
||||
var last: usize = 0;
|
||||
match Vec.get(&starts, starts.len - 1) {
|
||||
.Ok(ptr) => { last = (ptr as *usize).*; },
|
||||
.Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
if !segment_is_parent(fresh, last, style) {
|
||||
fresh.storage.len = last;
|
||||
starts.len = starts.len - 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if absolute { continue; }
|
||||
}
|
||||
|
||||
match append_segment(&fresh, &starts, segment, style) {
|
||||
.Ok => {}, .Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
|
||||
}
|
||||
}
|
||||
|
||||
if protected != 0 && starts.len < protected {
|
||||
Vec.drop(&starts); ByteBuffer.drop(&fresh);
|
||||
ret .Err(make(.InvalidArgument));
|
||||
}
|
||||
Vec.drop(&starts);
|
||||
ByteBuffer.drop(out);
|
||||
out.* = ByteBuffer.take(&fresh);
|
||||
ret .Ok;
|
||||
}
|
||||
|
||||
// ---- 测试 ----
|
||||
fn expect_ok(result: Status) {
|
||||
match result { .Ok => {}, .Err(error) => { @assert(false, "unexpected path error"); }, }
|
||||
}
|
||||
|
||||
fn expect_invalid(result: Status) {
|
||||
match result {
|
||||
.Ok => { @assert(false, "expected invalid path"); },
|
||||
.Err(error) => {
|
||||
match error.kind { .InvalidArgument => {}, _ => { @assert(false); }, }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn view(text: []u8): ByteView { ret ByteView.from_slice(text); }
|
||||
|
||||
fn expect_view(got: ByteView, expected: []u8) {
|
||||
@assert(ByteView.eq(got, view(expected)), "view value");
|
||||
@assert(ByteView.eq(got, view(expected)), "path view");
|
||||
}
|
||||
|
||||
fn expect_out(out: *ByteBuffer, expected: []u8) {
|
||||
var actual = ByteBuffer.as_slice(out.*);
|
||||
@assert(actual.len == expected.len, "out length");
|
||||
@assert(actual.len == expected.len, "path output length");
|
||||
var i: usize = 0;
|
||||
while i < expected.len {
|
||||
@assert(actual.ptr[i] == expected.ptr[i], "out byte");
|
||||
@assert(actual.ptr[i] == expected.ptr[i], "path output byte");
|
||||
i = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn test_parts() {
|
||||
@assert(is_sep('/') && is_sep('\\'), "seps");
|
||||
@assert(is_absolute(view("/a/b")), "posix abs");
|
||||
@assert(is_absolute(view("\\a")), "win abs");
|
||||
@assert(is_absolute(view("C:/a")), "drive abs");
|
||||
@assert(!is_absolute(view("a/b")), "relative");
|
||||
|
||||
expect_view(dirname(view("/a/b/c")), "/a/b");
|
||||
expect_view(dirname(view("/a")), "/");
|
||||
expect_view(dirname(view("abc")), "");
|
||||
expect_view(basename(view("/a/b/c")), "c");
|
||||
expect_view(basename(view("abc")), "abc");
|
||||
expect_view(extension(view("a/b.spl")), "spl");
|
||||
expect_view(extension(view("a/b")), "");
|
||||
expect_view(extension(view("a/.hidden")), "");
|
||||
fn test_style_and_parts() {
|
||||
@assert(is_absolute(view("/a"), .Posix));
|
||||
@assert(!is_absolute(view("\\a"), .Posix));
|
||||
@assert(is_absolute(view("C:\\a"), .Windows));
|
||||
@assert(!is_absolute(view("C:a"), .Windows));
|
||||
@assert(is_absolute(view("\\\\server\\share\\a"), .Windows));
|
||||
@assert(!is_absolute(view("C:/a"), .Posix));
|
||||
expect_view(dirname(view("/a/b"), .Posix), "/a");
|
||||
expect_view(dirname(view("/a"), .Posix), "/");
|
||||
expect_view(basename(view("/a/b"), .Posix), "b");
|
||||
expect_view(extension(view("a/b.spl"), .Posix), "spl");
|
||||
expect_view(extension(view("a/.hidden"), .Posix), "");
|
||||
expect_view(dirname(view("C:\\a"), .Windows), "C:\\");
|
||||
expect_view(basename(view("C:\\a"), .Windows), "a");
|
||||
expect_view(dirname(view("C:relative"), .Windows), "C:");
|
||||
expect_view(basename(view("C:relative"), .Windows), "relative");
|
||||
expect_view(dirname(view("\\\\server\\share\\file"), .Windows),
|
||||
"\\\\server\\share");
|
||||
}
|
||||
|
||||
fn test_join() {
|
||||
var out = ByteBuffer.new();
|
||||
expect_ok(join_into(&out, view("stage2/utils"), view("vec.spl")));
|
||||
defer { ByteBuffer.drop(&out); }
|
||||
expect_ok(join_into(&out, view("stage2/utils"), view("vec.spl"), .Posix));
|
||||
expect_out(&out, "stage2/utils/vec.spl");
|
||||
expect_ok(join_into(&out, view("a/"), view("b.spl")));
|
||||
expect_out(&out, "a/b.spl");
|
||||
expect_ok(join_into(&out, view("ignored"), view("/abs/x")));
|
||||
expect_out(&out, "/abs/x");
|
||||
ByteBuffer.drop(&out);
|
||||
expect_ok(join_into(&out, view("C:\\base"), view("child\\x"), .Windows));
|
||||
expect_out(&out, "C:\\base\\child\\x");
|
||||
expect_ok(join_into(&out, view("C:"), view("child"), .Windows));
|
||||
expect_out(&out, "C:child");
|
||||
expect_ok(join_into(&out, view("ignored"), view("D:/abs"), .Windows));
|
||||
expect_out(&out, "D:/abs");
|
||||
expect_invalid(join_into(&out, view("C:\\base"), view("D:relative"), .Windows));
|
||||
expect_out(&out, "D:/abs");
|
||||
}
|
||||
|
||||
fn test_normalize() {
|
||||
fn test_normalize_posix() {
|
||||
var out = ByteBuffer.new();
|
||||
expect_ok(normalize_into(&out, view("a/b/../c")));
|
||||
defer { ByteBuffer.drop(&out); }
|
||||
expect_ok(normalize_into(&out, view("a/b/../c"), .Posix));
|
||||
expect_out(&out, "a/c");
|
||||
expect_ok(normalize_into(&out, view("./a//b")));
|
||||
expect_out(&out, "a/b");
|
||||
expect_ok(normalize_into(&out, view("/a/./b/../c")));
|
||||
expect_out(&out, "/a/c");
|
||||
expect_ok(normalize_into(&out, view("C:/a/../b")));
|
||||
expect_out(&out, "C:/b");
|
||||
expect_ok(normalize_into(&out, view("a/..")));
|
||||
expect_out(&out, "");
|
||||
ByteBuffer.drop(&out);
|
||||
expect_ok(normalize_into(&out, view("../../a/./b"), .Posix));
|
||||
expect_out(&out, "../../a/b");
|
||||
expect_ok(normalize_into(&out, view("/../../a//b"), .Posix));
|
||||
expect_out(&out, "/a/b");
|
||||
expect_ok(normalize_into(&out, view("a\\b/../c"), .Posix));
|
||||
expect_out(&out, "c");
|
||||
}
|
||||
|
||||
fn test_normalize_windows() {
|
||||
var out = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&out); }
|
||||
expect_ok(normalize_into(&out, view("C:/a\\b/../c"), .Windows));
|
||||
expect_out(&out, "C:\\a\\c");
|
||||
expect_ok(normalize_into(&out, view("C:foo\\..\\bar"), .Windows));
|
||||
expect_out(&out, "C:bar");
|
||||
expect_ok(normalize_into(&out, view("\\root\\..\\file"), .Windows));
|
||||
expect_out(&out, "\\file");
|
||||
expect_ok(normalize_into(&out,
|
||||
view("\\\\server\\share\\a\\..\\file"), .Windows));
|
||||
expect_out(&out, "\\\\server\\share\\file");
|
||||
expect_ok(ByteBuffer.assign(&out, "preserved"));
|
||||
expect_invalid(normalize_into(&out, view("\\\\server\\..\\file"), .Windows));
|
||||
expect_out(&out, "preserved");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
test_parts();
|
||||
test_style_and_parts();
|
||||
test_join();
|
||||
test_normalize();
|
||||
test_normalize_posix();
|
||||
test_normalize_windows();
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// 开放寻址集合。键按字节复制到集合自己的存储中;集合只释放自身存储,
|
||||
// 不释放键值内部引用的资源。哈希与相等回调的约定和 Map 一致。
|
||||
type .{ .Status } = @import("error.spl");
|
||||
type .{ .Map, .LookupResult } = @import("map.spl");
|
||||
type .{ .ByteView } = @import("byte_view.spl");
|
||||
type .{ .hash_usize, .eq_usize, .hash_view, .eq_view } = @import("hash.spl");
|
||||
|
||||
type Set = @self;
|
||||
|
||||
map: Map,
|
||||
|
||||
fn new(key_size: usize, key_align: usize,
|
||||
hash_fn: fn(*u8): usize, eq_fn: fn(*u8, *u8): bool): Set {
|
||||
ret Set {
|
||||
.map = Map.new(key_size, key_align, 1, 1, hash_fn, eq_fn),
|
||||
};
|
||||
}
|
||||
|
||||
fn valid(self: Set): bool {
|
||||
ret Map.valid(self.map) && self.map.val_size == 1;
|
||||
}
|
||||
|
||||
fn size(self: Set): usize {
|
||||
ret self.map.size;
|
||||
}
|
||||
|
||||
fn capacity(self: Set): usize {
|
||||
ret self.map.states.cap;
|
||||
}
|
||||
|
||||
fn reserve(self: *Set, need: usize): Status {
|
||||
ret Map.reserve(&self.map, need);
|
||||
}
|
||||
|
||||
// 重复插入保持集合大小不变。
|
||||
fn insert(self: *Set, key: *u8): Status {
|
||||
var marker: u8 = 1;
|
||||
ret Map.put(&self.map, key, &marker);
|
||||
}
|
||||
|
||||
fn find(self: *Set, key: *u8): LookupResult {
|
||||
var marker: u8 = 0;
|
||||
ret Map.get(&self.map, key, &marker);
|
||||
}
|
||||
|
||||
fn remove(self: *Set, key: *u8): LookupResult {
|
||||
ret Map.del(&self.map, key);
|
||||
}
|
||||
|
||||
// 遍历 [0, slot_count);仅 slot_occupied 为真时 key_at 有效。
|
||||
fn slot_count(self: Set): usize {
|
||||
ret Map.slot_count(self.map);
|
||||
}
|
||||
|
||||
fn slot_occupied(self: Set, index: usize): bool {
|
||||
ret Map.slot_occupied(Map.slot_state(self.map, index));
|
||||
}
|
||||
|
||||
fn key_at(self: *Set, index: usize): *u8 {
|
||||
ret Map.key_at(&self.map, index);
|
||||
}
|
||||
|
||||
fn clear(self: *Set) {
|
||||
Map.clear(&self.map);
|
||||
}
|
||||
|
||||
fn take(source: *Set): Set {
|
||||
ret Set { .map = Map.take(&source.map) };
|
||||
}
|
||||
|
||||
fn drop(self: *Set) {
|
||||
Map.drop(&self.map);
|
||||
}
|
||||
|
||||
// ---- 测试 ----
|
||||
|
||||
fn expect_ok(result: Status) {
|
||||
match result {
|
||||
.Ok => {},
|
||||
.Err(error) => { @assert(false, "unexpected Set error"); },
|
||||
}
|
||||
}
|
||||
|
||||
fn found(set: *Set, key: *u8): bool {
|
||||
match find(set, key) {
|
||||
.Found => { ret true; },
|
||||
.NotFound => { ret false; },
|
||||
}
|
||||
ret false;
|
||||
}
|
||||
|
||||
fn removed(set: *Set, key: *u8): bool {
|
||||
match remove(set, key) {
|
||||
.Found => { ret true; },
|
||||
.NotFound => { ret false; },
|
||||
}
|
||||
ret false;
|
||||
}
|
||||
|
||||
fn constant_hash(key: *u8): usize {
|
||||
ret 3;
|
||||
}
|
||||
|
||||
fn test_insert_find_remove() {
|
||||
var set = new(@sizeof(usize), @alignof(usize), hash_usize, eq_usize);
|
||||
defer { drop(&set); }
|
||||
@assert(valid(set));
|
||||
@assert(size(set) == 0 && capacity(set) == 0);
|
||||
|
||||
var i: usize = 0;
|
||||
while i < 1000 {
|
||||
expect_ok(insert(&set, &i as *u8));
|
||||
i = i + 1;
|
||||
}
|
||||
@assert(size(set) == 1000, "all unique keys inserted");
|
||||
|
||||
i = 0;
|
||||
while i < 1000 {
|
||||
@assert(found(&set, &i as *u8), "key survives growth");
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
// 重复键不增加 size。
|
||||
i = 0;
|
||||
while i < 1000 {
|
||||
expect_ok(insert(&set, &i as *u8));
|
||||
i = i + 1;
|
||||
}
|
||||
@assert(size(set) == 1000, "duplicate insertion is idempotent");
|
||||
|
||||
i = 0;
|
||||
while i < 1000 {
|
||||
if i % 2 == 0 { @assert(removed(&set, &i as *u8)); }
|
||||
i = i + 1;
|
||||
}
|
||||
@assert(size(set) == 500);
|
||||
|
||||
i = 0;
|
||||
while i < 1000 {
|
||||
@assert(found(&set, &i as *u8) == (i % 2 != 0), "removed membership");
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
i = 0;
|
||||
while i < 1000 {
|
||||
if i % 2 == 0 { expect_ok(insert(&set, &i as *u8)); }
|
||||
i = i + 1;
|
||||
}
|
||||
@assert(size(set) == 1000, "removed keys can be reinserted");
|
||||
|
||||
var occupied: usize = 0;
|
||||
i = 0;
|
||||
while i < slot_count(set) {
|
||||
if slot_occupied(set, i) {
|
||||
var key = (key_at(&set, i) as *usize).*;
|
||||
@assert(key < 1000, "iteration yields stored key");
|
||||
occupied = occupied + 1;
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
@assert(occupied == size(set), "iteration visits each member once");
|
||||
|
||||
var missing: usize = 2000;
|
||||
@assert(!removed(&set, &missing as *u8));
|
||||
clear(&set);
|
||||
@assert(size(set) == 0 && capacity(set) != 0, "clear retains allocation");
|
||||
}
|
||||
|
||||
fn test_collisions_and_take() {
|
||||
var set = new(@sizeof(usize), @alignof(usize), constant_hash, eq_usize);
|
||||
defer { drop(&set); }
|
||||
|
||||
var i: usize = 0;
|
||||
while i < 64 {
|
||||
expect_ok(insert(&set, &i as *u8));
|
||||
i = i + 1;
|
||||
}
|
||||
i = 0;
|
||||
while i < 64 {
|
||||
@assert(found(&set, &i as *u8), "collision chain lookup");
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
// 删除整条碰撞链后重新插入,验证墓碑会被复用且探测会终止。
|
||||
i = 0;
|
||||
while i < 64 {
|
||||
@assert(removed(&set, &i as *u8));
|
||||
i = i + 1;
|
||||
}
|
||||
@assert(size(set) == 0);
|
||||
i = 64;
|
||||
while i < 128 {
|
||||
expect_ok(insert(&set, &i as *u8));
|
||||
i = i + 1;
|
||||
}
|
||||
i = 64;
|
||||
while i < 128 {
|
||||
@assert(found(&set, &i as *u8), "tombstone collision chain reuse");
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
var moved = take(&set);
|
||||
defer { drop(&moved); }
|
||||
@assert(size(set) == 0 && capacity(set) == 0, "take clears source");
|
||||
@assert(size(moved) == 64, "take preserves members");
|
||||
i = 64;
|
||||
while i < 128 {
|
||||
@assert(found(&moved, &i as *u8));
|
||||
i = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn test_byte_view_keys() {
|
||||
var set = new(@sizeof(ByteView), @alignof(ByteView), hash_view, eq_view);
|
||||
defer { drop(&set); }
|
||||
|
||||
var alpha = ByteView.from_slice("alpha");
|
||||
var alpha_copy = ByteView.from_slice("alpha");
|
||||
var beta = ByteView.from_slice("beta");
|
||||
expect_ok(insert(&set, &alpha as *u8));
|
||||
expect_ok(insert(&set, &alpha_copy as *u8));
|
||||
expect_ok(insert(&set, &beta as *u8));
|
||||
@assert(size(set) == 2, "ByteView keys compare by content");
|
||||
@assert(found(&set, &alpha_copy as *u8));
|
||||
@assert(removed(&set, &alpha_copy as *u8));
|
||||
@assert(!found(&set, &alpha as *u8));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
test_insert_find_remove();
|
||||
test_collisions_and_take();
|
||||
test_byte_view_keys();
|
||||
}
|
||||
@@ -173,7 +173,10 @@ fn set_at(set: *Sources, index: usize): *Source {
|
||||
|
||||
fn set_find(set: *Sources, fname: ByteView): FindResult {
|
||||
var out: usize = 0;
|
||||
if Map.get(&set.by_name, &fname as *u8, &out as *u8) { ret .Found(out); }
|
||||
match Map.get(&set.by_name, &fname as *u8, &out as *u8) {
|
||||
.Found => { ret .Found(out); },
|
||||
.NotFound => {},
|
||||
}
|
||||
ret .NotFound;
|
||||
}
|
||||
|
||||
|
||||
+8
-10
@@ -138,6 +138,7 @@ fn expect_text(s: *Str, expected: []u8) {
|
||||
|
||||
fn test_construction() {
|
||||
var s = new();
|
||||
defer { drop(&s); }
|
||||
@assert(is_empty(s) && size(s) == 0, "empty new");
|
||||
expect_ok(append(&s, "hello"));
|
||||
expect_ok(append_byte(&s, ' '));
|
||||
@@ -154,18 +155,19 @@ fn test_construction() {
|
||||
},
|
||||
.Err(error) => { @assert(false, "from_slice"); },
|
||||
}
|
||||
drop(&s);
|
||||
}
|
||||
|
||||
fn test_compare_and_search() {
|
||||
var hello_r = from_slice("hello");
|
||||
var hello: Str = new();
|
||||
defer { drop(&hello); }
|
||||
match hello_r {
|
||||
.Ok(v) => { hello = v; },
|
||||
.Err(error) => { @assert(false, "hello"); },
|
||||
}
|
||||
var help_r = from_slice("help");
|
||||
var help: Str = new();
|
||||
defer { drop(&help); }
|
||||
match help_r {
|
||||
.Ok(v) => { help = v; },
|
||||
.Err(error) => { @assert(false, "help"); },
|
||||
@@ -181,12 +183,14 @@ fn test_compare_and_search() {
|
||||
@assert(ends_with(hello, hello), "ends_with");
|
||||
var pre_r = from_slice("he");
|
||||
var pre: Str = new();
|
||||
defer { drop(&pre); }
|
||||
match pre_r { .Ok(v) => { pre = v; }, .Err(e) => { @assert(false, "pre"); } }
|
||||
@assert(starts_with(hello, pre), "starts he");
|
||||
@assert(!ends_with(hello, pre), "!ends he");
|
||||
|
||||
var needle_r = from_slice("ll");
|
||||
var needle: Str = new();
|
||||
defer { drop(&needle); }
|
||||
match needle_r { .Ok(v) => { needle = v; }, .Err(e) => { @assert(false, "needle"); } }
|
||||
match find(hello, needle) {
|
||||
.Found(index) => { @assert(index == 2, "find ll"); },
|
||||
@@ -196,18 +200,15 @@ fn test_compare_and_search() {
|
||||
.Found(index) => { @assert(index == 0, "find he"); },
|
||||
.NotFound => { @assert(false, "he not found"); },
|
||||
}
|
||||
|
||||
drop(&needle);
|
||||
drop(&pre);
|
||||
drop(&help);
|
||||
drop(&hello);
|
||||
}
|
||||
|
||||
fn test_clone_take_and_failures() {
|
||||
var source = new();
|
||||
defer { drop(&source); }
|
||||
expect_ok(append(&source, "owned"));
|
||||
|
||||
var copy = new();
|
||||
defer { drop(©); }
|
||||
expect_ok(clone(©, source));
|
||||
expect_text(©, "owned");
|
||||
expect_ok(append_byte(&source, '!')); // '!'
|
||||
@@ -219,16 +220,13 @@ fn test_clone_take_and_failures() {
|
||||
expect_text(©, "owned");
|
||||
|
||||
var moved = take(©);
|
||||
defer { drop(&moved); }
|
||||
@assert(ByteBuffer.size(copy.storage) == 0 && ByteBuffer.capacity(copy.storage) == 0,
|
||||
"take empties source");
|
||||
expect_text(&moved, "owned");
|
||||
|
||||
clear(&moved);
|
||||
@assert(is_empty(moved) && ByteBuffer.capacity(moved.storage) != 0, "clear keeps capacity");
|
||||
|
||||
drop(&moved);
|
||||
drop(©);
|
||||
drop(&source);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
+84
-45
@@ -1,60 +1,70 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// UTF-8 工具:解码/编码、校验、码点数、以及 UTF-8 字节偏移 <-> UTF-16 code unit 偏移
|
||||
// (LSP 位置基于 UTF-16)。严格校验过长编码与代理区。
|
||||
|
||||
type .{ .Error, .Status, .make } = @import("error.spl");
|
||||
type .{ .ByteBuffer } = @import("byte_buffer.spl");
|
||||
|
||||
// 由首字节推断序列长度;非法/续字节返回 0。
|
||||
fn seq_len(b: u8): usize {
|
||||
if b < 0x80 { ret 1; }
|
||||
if b < 0xC0 { ret 0; } // 续字节
|
||||
if b < 0xE0 { ret 2; }
|
||||
if b < 0xF0 { ret 3; }
|
||||
if b < 0xF8 { ret 4; }
|
||||
ret 0;
|
||||
// 解码结果:Ok 携带消耗字节数;明确区分越界/截断/非法,避免 0 哨兵歧义。
|
||||
type DecodeResult = enum {
|
||||
Ok: usize,
|
||||
Eof, // index 已越过输入
|
||||
Truncated, // 序列尾部字节不足
|
||||
Invalid, // 续字节/过长编码/代理区/超出码点范围
|
||||
}
|
||||
|
||||
// 从 index 解码一个码点到 out_cp,返回消耗字节数;非法返回 0。
|
||||
fn decode(data: *u8, len: usize, index: usize, out_cp: *u32): usize {
|
||||
if index >= len { ret 0; }
|
||||
// 从 index 解码一个码点到 out_cp;返回 DecodeResult。
|
||||
fn decode(data: *u8, len: usize, index: usize, out_cp: *u32): DecodeResult {
|
||||
if index >= len { ret .Eof; }
|
||||
var b0 = data[index];
|
||||
|
||||
if b0 < 0x80 {
|
||||
out_cp.* = b0 as u32;
|
||||
ret 1;
|
||||
ret .Ok(1);
|
||||
}
|
||||
if b0 < 0xC2 { ret 0; } // 续字节或过长编码的前导
|
||||
if b0 < 0xC2 { ret .Invalid; } // 续字节或过长编码的前导
|
||||
|
||||
if b0 < 0xE0 {
|
||||
if index + 1 >= len { ret 0; }
|
||||
if index + 1 >= len { ret .Truncated; }
|
||||
var b1 = data[index + 1];
|
||||
if (b1 & 0xC0) != 0x80 { ret 0; }
|
||||
if (b1 & 0xC0) != 0x80 { ret .Invalid; }
|
||||
out_cp.* = (((b0 & 0x1F) as u32) << 6) | ((b1 & 0x3F) as u32);
|
||||
ret 2;
|
||||
ret .Ok(2);
|
||||
}
|
||||
if b0 < 0xF0 {
|
||||
if index + 2 >= len { ret 0; }
|
||||
if index + 2 >= len { ret .Truncated; }
|
||||
var b1 = data[index + 1];
|
||||
var b2 = data[index + 2];
|
||||
if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 { ret 0; }
|
||||
if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 { ret .Invalid; }
|
||||
var cp = (((b0 & 0x0F) as u32) << 12) | (((b1 & 0x3F) as u32) << 6) | ((b2 & 0x3F) as u32);
|
||||
if cp < 0x800 { ret 0; } // 过长
|
||||
if cp >= 0xD800 && cp <= 0xDFFF { ret 0; } // 代理区
|
||||
if cp < 0x800 { ret .Invalid; } // 过长
|
||||
if cp >= 0xD800 && cp <= 0xDFFF { ret .Invalid; } // 代理区
|
||||
out_cp.* = cp;
|
||||
ret 3;
|
||||
ret .Ok(3);
|
||||
}
|
||||
if b0 < 0xF5 {
|
||||
if index + 3 >= len { ret 0; }
|
||||
if index + 3 >= len { ret .Truncated; }
|
||||
var b1 = data[index + 1];
|
||||
var b2 = data[index + 2];
|
||||
var b3 = data[index + 3];
|
||||
if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80 { ret 0; }
|
||||
if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80 { ret .Invalid; }
|
||||
var cp = (((b0 & 0x07) as u32) << 18) | (((b1 & 0x3F) as u32) << 12) |
|
||||
(((b2 & 0x3F) as u32) << 6) | ((b3 & 0x3F) as u32);
|
||||
if cp < 0x10000 || cp > 0x10FFFF { ret 0; }
|
||||
if cp < 0x10000 || cp > 0x10FFFF { ret .Invalid; }
|
||||
out_cp.* = cp;
|
||||
ret 4;
|
||||
ret .Ok(4);
|
||||
}
|
||||
ret .Invalid;
|
||||
}
|
||||
|
||||
// 测试辅助:把 DecodeResult 归约为“消耗字节数”,非 Ok 视为 0。
|
||||
fn decode_used(data: *u8, len: usize, index: usize, out_cp: *u32): usize {
|
||||
match decode(data, len, index, out_cp) {
|
||||
.Ok(used) => { ret used; },
|
||||
.Eof => { ret 0; },
|
||||
.Truncated => { ret 0; },
|
||||
.Invalid => { ret 0; },
|
||||
}
|
||||
ret 0;
|
||||
}
|
||||
@@ -63,9 +73,10 @@ fn valid(data: *u8, len: usize): bool {
|
||||
var i: usize = 0;
|
||||
var cp: u32 = 0;
|
||||
while i < len {
|
||||
var n = decode(data, len, i, &cp);
|
||||
if n == 0 { ret false; }
|
||||
i = i + n;
|
||||
match decode(data, len, i, &cp) {
|
||||
.Ok(used) => { i = i + used; },
|
||||
_ => { ret false; },
|
||||
}
|
||||
}
|
||||
ret true;
|
||||
}
|
||||
@@ -107,10 +118,10 @@ fn count_codepoints(data: *u8, len: usize): usize {
|
||||
var n: usize = 0;
|
||||
var cp: u32 = 0;
|
||||
while i < len {
|
||||
var used = decode(data, len, i, &cp);
|
||||
if used == 0 { ret n; }
|
||||
i = i + used;
|
||||
n = n + 1;
|
||||
match decode(data, len, i, &cp) {
|
||||
.Ok(used) => { i = i + used; n = n + 1; },
|
||||
_ => { ret n; },
|
||||
}
|
||||
}
|
||||
ret n;
|
||||
}
|
||||
@@ -121,10 +132,13 @@ fn utf16_units(data: *u8, len: usize): usize {
|
||||
var n: usize = 0;
|
||||
var cp: u32 = 0;
|
||||
while i < len {
|
||||
var used = decode(data, len, i, &cp);
|
||||
if used == 0 { ret n; }
|
||||
match decode(data, len, i, &cp) {
|
||||
.Ok(used) => {
|
||||
if cp >= 0x10000 { n = n + 2; } else { n = n + 1; }
|
||||
i = i + used;
|
||||
},
|
||||
_ => { ret n; },
|
||||
}
|
||||
}
|
||||
ret n;
|
||||
}
|
||||
@@ -135,10 +149,13 @@ fn byte_to_utf16(data: *u8, len: usize, byte_off: usize): usize {
|
||||
var n: usize = 0;
|
||||
var cp: u32 = 0;
|
||||
while i < len && i < byte_off {
|
||||
var used = decode(data, len, i, &cp);
|
||||
if used == 0 { ret n; }
|
||||
match decode(data, len, i, &cp) {
|
||||
.Ok(used) => {
|
||||
if cp >= 0x10000 { n = n + 2; } else { n = n + 1; }
|
||||
i = i + used;
|
||||
},
|
||||
_ => { ret n; },
|
||||
}
|
||||
}
|
||||
ret n;
|
||||
}
|
||||
@@ -150,8 +167,11 @@ fn utf16_to_byte(data: *u8, len: usize, utf16_off: usize): usize {
|
||||
var cp: u32 = 0;
|
||||
while i < len {
|
||||
if n >= utf16_off { ret i; }
|
||||
var used = decode(data, len, i, &cp);
|
||||
if used == 0 { ret i; }
|
||||
var used: usize = 0;
|
||||
match decode(data, len, i, &cp) {
|
||||
.Ok(value) => { used = value; },
|
||||
_ => { ret i; },
|
||||
}
|
||||
var units: usize = 1;
|
||||
if cp >= 0x10000 { units = 2; }
|
||||
if n + units > utf16_off { ret i; }
|
||||
@@ -172,6 +192,7 @@ fn expect_ok(result: Status) {
|
||||
|
||||
fn test_decode_encode_roundtrip() {
|
||||
var out = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&out); }
|
||||
expect_ok(encode_into(&out, 0x41)); // 'A'
|
||||
expect_ok(encode_into(&out, 0xE9)); // é
|
||||
expect_ok(encode_into(&out, 0x20AC)); // €
|
||||
@@ -188,11 +209,10 @@ fn test_decode_encode_roundtrip() {
|
||||
@assert(utf16_units(bytes.ptr, bytes.len) == 1 + 1 + 1 + 2, "utf16 units");
|
||||
|
||||
var cp: u32 = 0;
|
||||
@assert(decode(bytes.ptr, bytes.len, 0, &cp) == 1 && cp == 0x41, "dec A");
|
||||
@assert(decode(bytes.ptr, bytes.len, 1, &cp) == 2 && cp == 0xE9, "dec e");
|
||||
@assert(decode(bytes.ptr, bytes.len, 3, &cp) == 3 && cp == 0x20AC, "dec euro");
|
||||
@assert(decode(bytes.ptr, bytes.len, 6, &cp) == 4 && cp == 0x1F600, "dec emoji");
|
||||
ByteBuffer.drop(&out);
|
||||
@assert(decode_used(bytes.ptr, bytes.len, 0, &cp) == 1 && cp == 0x41, "dec A");
|
||||
@assert(decode_used(bytes.ptr, bytes.len, 1, &cp) == 2 && cp == 0xE9, "dec e");
|
||||
@assert(decode_used(bytes.ptr, bytes.len, 3, &cp) == 3 && cp == 0x20AC, "dec euro");
|
||||
@assert(decode_used(bytes.ptr, bytes.len, 6, &cp) == 4 && cp == 0x1F600, "dec emoji");
|
||||
}
|
||||
|
||||
fn test_invalid() {
|
||||
@@ -212,7 +232,27 @@ fn test_invalid() {
|
||||
@assert(!valid(s4.ptr, s4.len), "lone cont");
|
||||
@assert(!valid(s5.ptr, s5.len), "truncated");
|
||||
|
||||
// 新 ADT:越界/截断/非法可区分。
|
||||
var cp: u32 = 0;
|
||||
match decode(s5.ptr, s5.len, 0, &cp) {
|
||||
.Truncated => {},
|
||||
_ => { @assert(false, "truncated sequence should be Truncated"); },
|
||||
}
|
||||
match decode(s5.ptr, s5.len, s5.len, &cp) {
|
||||
.Eof => {},
|
||||
_ => { @assert(false, "past end should be Eof"); },
|
||||
}
|
||||
match decode(s1.ptr, s1.len, 0, &cp) {
|
||||
.Invalid => {},
|
||||
_ => { @assert(false, "overlong should be Invalid"); },
|
||||
}
|
||||
match decode(s4.ptr, s4.len, 0, &cp) {
|
||||
.Invalid => {},
|
||||
_ => { @assert(false, "lone continuation should be Invalid"); },
|
||||
}
|
||||
|
||||
var out = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&out); }
|
||||
match encode_into(&out, 0xD800) {
|
||||
.Ok => { @assert(false, "surrogate encode"); },
|
||||
.Err(error) => {},
|
||||
@@ -221,11 +261,11 @@ fn test_invalid() {
|
||||
.Ok => { @assert(false, "out of range encode"); },
|
||||
.Err(error) => {},
|
||||
}
|
||||
ByteBuffer.drop(&out);
|
||||
}
|
||||
|
||||
fn test_offsets() {
|
||||
var out = ByteBuffer.new();
|
||||
defer { ByteBuffer.drop(&out); }
|
||||
expect_ok(encode_into(&out, 0x41)); // byte 0, utf16 0
|
||||
expect_ok(encode_into(&out, 0x1F600)); // bytes 1..4, utf16 1..2
|
||||
expect_ok(encode_into(&out, 0x42)); // byte 5, utf16 3
|
||||
@@ -241,7 +281,6 @@ fn test_offsets() {
|
||||
@assert(utf16_to_byte(b.ptr, b.len, 2) == 1, "u2 stays in emoji");
|
||||
@assert(utf16_to_byte(b.ptr, b.len, 3) == 5, "u3");
|
||||
@assert(utf16_to_byte(b.ptr, b.len, 99) == 6, "clamp");
|
||||
ByteBuffer.drop(&out);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
+11
-16
@@ -1,9 +1,10 @@
|
||||
// Expect retcode = 0
|
||||
|
||||
// 类型擦除的动态数组。元素按值复制,数组只释放自身存储,不释放元素内部资源。
|
||||
// Vec 拥有 raw 指向的分配;普通赋值会复制所有权,所有权转移必须使用 take。
|
||||
|
||||
type .{ .Error, .Kind, .Status } = @import("error.spl");
|
||||
type .{ .SizeResult, .checked_add, .checked_mul, .move } = @import("memory.spl");
|
||||
type .{ .SizeResult, .checked_add, .checked_mul, .grow_capacity, .move } = @import("memory.spl");
|
||||
type .{ .alloc, .copy, .free } = @import("../sys/mem.spl");
|
||||
|
||||
type Vec = @self;
|
||||
@@ -87,6 +88,9 @@ fn allocate(capacity: usize, elem_size: usize, elem_align: usize): VecResult {
|
||||
}
|
||||
|
||||
var raw = alloc(total) as *u8;
|
||||
if raw == null {
|
||||
ret .Err(Error.make(.Overflow));
|
||||
}
|
||||
var address = raw as usize;
|
||||
var mask = elem_align - 1;
|
||||
if address > (~(0 as usize)) - mask {
|
||||
@@ -109,15 +113,7 @@ fn reserve(self: *Vec, need: usize): Status {
|
||||
if !valid(self.*) { ret status_error(.InvalidArgument); }
|
||||
if need <= self.cap { ret .Ok; }
|
||||
|
||||
var new_cap = self.cap;
|
||||
if new_cap < 8 { new_cap = 8; }
|
||||
while new_cap < need {
|
||||
if new_cap > (~(0 as usize)) / 2 {
|
||||
new_cap = need;
|
||||
break;
|
||||
}
|
||||
new_cap = new_cap * 2;
|
||||
}
|
||||
var new_cap = grow_capacity(self.cap, 8, need);
|
||||
|
||||
var fresh: Vec = undefined;
|
||||
match allocate(new_cap, self.elem_size, self.elem_align) {
|
||||
@@ -337,6 +333,7 @@ fn test_construction_and_alignment() {
|
||||
|
||||
fn test_growth_and_access() {
|
||||
var v = make_vec(@sizeof(i32), @alignof(i32));
|
||||
defer { drop(&v); }
|
||||
var i: i32 = 0;
|
||||
while i < 1000 {
|
||||
var value = i * 3 - 7;
|
||||
@@ -358,11 +355,11 @@ fn test_growth_and_access() {
|
||||
var popped: i32 = 0;
|
||||
expect_ok(pop(&v, &popped as *u8));
|
||||
@assert(popped == 2990 && v.len == 999, "pop copies then removes");
|
||||
drop(&v);
|
||||
}
|
||||
|
||||
fn test_self_reference() {
|
||||
var v = make_vec(@sizeof(i32), @alignof(i32));
|
||||
defer { drop(&v); }
|
||||
var i: i32 = 0;
|
||||
while i < 8 {
|
||||
expect_ok(push(&v, &i as *u8));
|
||||
@@ -391,17 +388,18 @@ fn test_self_reference() {
|
||||
}
|
||||
expect_ok(set(&v, 0, last_ptr));
|
||||
expect_i32(&v, 0, 7);
|
||||
drop(&v);
|
||||
}
|
||||
|
||||
fn test_clone_take_and_failures() {
|
||||
var source = make_vec(@sizeof(i32), @alignof(i32));
|
||||
defer { drop(&source); }
|
||||
var one: i32 = 1;
|
||||
var two: i32 = 2;
|
||||
expect_ok(push(&source, &one as *u8));
|
||||
expect_ok(push(&source, &two as *u8));
|
||||
|
||||
var copy = make_vec(@sizeof(i32), @alignof(i32));
|
||||
defer { drop(©); }
|
||||
expect_ok(clone(©, source));
|
||||
expect_i32(©, 0, 1);
|
||||
one = 99;
|
||||
@@ -415,6 +413,7 @@ fn test_clone_take_and_failures() {
|
||||
expect_i32(©, 1, 2);
|
||||
|
||||
var moved = take(©);
|
||||
defer { drop(&moved); }
|
||||
@assert(copy.raw == null && copy.data == null && copy.len == 0 && copy.cap == 0);
|
||||
expect_i32(&moved, 0, 1);
|
||||
|
||||
@@ -430,10 +429,6 @@ fn test_clone_take_and_failures() {
|
||||
var output: i32 = 55;
|
||||
expect_error(pop(&moved, &output as *u8), false);
|
||||
@assert(output == 55, "failed pop preserves output");
|
||||
|
||||
drop(&moved);
|
||||
drop(©);
|
||||
drop(&source);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
Reference in new issue
Block a user