diff --git a/stage2/utils/arena.spl b/stage2/utils/arena.spl new file mode 100644 index 0000000..816b7a7 --- /dev/null +++ b/stage2/utils/arena.spl @@ -0,0 +1,177 @@ +// Expect retcode = 0 +// 分块 bump 分配器:按块分配、块内顺序推进,只在 drop 时整体释放。 +// 适合 AST/IR 这类生命周期一致的批量小对象;不支持单独 free。 +// 失败(内存不足/参数非法)返回 null,不改变已有分配。 + +type .{ .Vec } = @import("vec.spl"); +type Mem = @import("../sys/mem.spl"); + +type Chunk = struct { + base: *u8, + size: usize, + used: usize, +} + +type Arena = @self; + +chunk_size: usize, +chunks: Vec, // 元素类型 Chunk +current: usize, + +fn new(chunk_size: usize): Arena { + var cs = chunk_size; + if cs == 0 { cs = 4096; } + ret Arena { + .chunk_size = cs, + .chunks = Vec.blank(@sizeof(Chunk), 8), + .current = 0, + }; +} + +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; }, + .Err(error) => { ret null; }, + } + ret null; +} + +fn push_chunk(self: *Arena, size: usize): bool { + var base = Mem.alloc(size) as *u8; + if base == null { ret false; } + 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; }, + } + self.current = self.chunks.len - 1; + ret true; +} + +// 分配 size 字节,地址按 align(非零二次幂)对齐。失败返回 null。 +fn alloc(self: *Arena, size: usize, align: usize): *u8 { + if size == 0 { ret null; } + 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; + if need > want { want = need; } + if !push_chunk(self, want) { ret null; } + } + + var c = chunk_at(self, self.current); + if c == null { ret null; } + var addr = c.base as usize; + var aligned = align_up(addr + c.used, align); + var offset = aligned - addr; + if offset <= c.size && size <= c.size - offset { + c.used = offset + size; + ret aligned as *u8; + } + self.current = self.current + 1; + } + ret null; +} + +fn chunk_count(self: Arena): usize { + ret self.chunks.len; +} + +// 复用已分配的块,不释放内存。 +fn reset(self: *Arena) { + var i: usize = 0; + while i < self.chunks.len { + var c = chunk_at(self, i); + if c != null { c.used = 0; } + i = i + 1; + } + self.current = 0; +} + +fn drop(self: *Arena) { + var i: usize = 0; + while i < self.chunks.len { + var c = chunk_at(self, i); + if c != null { Mem.free(c.base as *any); } + i = i + 1; + } + Vec.drop(&self.chunks); + self.current = 0; +} + +// ---- 测试辅助 ---- + +fn is_aligned(p: *u8, align: usize): bool { + ret ((p as usize) & (align - 1)) == 0; +} + +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); + @assert(p1 != null && p8 != null && p16 != null, "alloc"); + @assert(p1 != p8 && p8 != p16 && p1 != p16, "distinct"); + @assert(is_aligned(p8, 8), "align 8"); + @assert(is_aligned(p16, 16), "align 16"); + + p1[0] = 7; + @assert(p1[0] == 7, "writable"); + @assert(p8[0] == p8[0], "no crash"); + + @assert(chunk_count(a) == 1, "one chunk"); + reset(&a); + @assert(chunk_count(a) == 1, "reset keeps chunks"); + var q = alloc(&a, 1, 1); + @assert(q == p1, "reset reuses first chunk"); + + drop(&a); + @assert(chunk_count(a) == 0, "drop empties"); +} + +fn test_growth() { + var a = new(64); + var i: i32 = 0; + while i < 100 { + var p = alloc(&a, 16, 8); + @assert(p != null, "grow alloc"); + @assert(is_aligned(p, 8), "grow align"); + p[0] = i as u8; + i = i + 1; + } + @assert(chunk_count(a) > 1, "grew multiple chunks"); + + // 超过块大小的单次分配也能成功(新块按需放大)。 + var big = alloc(&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); + @assert(d != null && is_aligned(d, 1), "align 0 defaults to 1"); + @assert(alloc(&a, 8, 3) == null, "non-pow2 align"); + drop(&a); +} + +fn main() { + test_alloc_and_alignment(); + test_growth(); + test_failures(); +} diff --git a/stage2/utils/ascii.spl b/stage2/utils/ascii.spl index d538465..a4cd82f 100644 --- a/stage2/utils/ascii.spl +++ b/stage2/utils/ascii.spl @@ -17,23 +17,23 @@ fn is_control(c: u8): bool { } fn is_space(c: u8): bool { - ret c == 32 || (c >= 9 && c <= 13); + ret c == ' ' || (c >= '\t' && c <= '\r'); } fn is_blank(c: u8): bool { - ret c == 32 || c == 9; + ret c == ' ' || c == '\t'; } fn is_digit(c: u8): bool { - ret c >= 48 && c <= 57; + ret c >= '0' && c <= '9'; } fn is_lower(c: u8): bool { - ret c >= 97 && c <= 122; + ret c >= 'a' && c <= 'z'; } fn is_upper(c: u8): bool { - ret c >= 65 && c <= 90; + ret c >= 'A' && c <= 'Z'; } fn is_alpha(c: u8): bool { @@ -45,15 +45,15 @@ fn is_alnum(c: u8): bool { } fn is_hex_digit(c: u8): bool { - ret is_digit(c) || (c >= 65 && c <= 70) || (c >= 97 && c <= 102); + ret is_digit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); } fn is_print(c: u8): bool { - ret c >= 32 && c <= 126; + ret c >= ' ' && c <= '~'; } fn is_graph(c: u8): bool { - ret c >= 33 && c <= 126; + ret c >= '!' && c <= '~'; } fn is_punct(c: u8): bool { @@ -61,12 +61,12 @@ fn is_punct(c: u8): bool { } fn to_lower(c: u8): u8 { - if is_upper(c) { ret c + 32; } + if is_upper(c) { ret c + ('a' - 'A'); } ret c; } fn to_upper(c: u8): u8 { - if is_lower(c) { ret c - 32; } + if is_lower(c) { ret c - ('a' - 'A'); } ret c; } @@ -78,11 +78,11 @@ fn digit_value(c: u8, radix: u8): DigitResult { var value: u8 = 0; if is_digit(c) { - value = c - 48; + value = c - '0'; } else if is_upper(c) { - value = c - 65 + 10; + value = c - 'A' + 10; } else if is_lower(c) { - value = c - 97 + 10; + value = c - 'a' + 10; } else { ret .Err(make(.InvalidDigit)); } @@ -94,9 +94,9 @@ fn digit_value(c: u8, radix: u8): DigitResult { // value 必须位于 0..35。uppercase 只影响 10..35 的字母形式。 fn digit_char(value: u8, uppercase: bool): DigitResult { if value >= 36 { ret .Err(make(.OutOfRange)); } - if value < 10 { ret .Ok(value + 48); } - if uppercase { ret .Ok(value - 10 + 65); } - ret .Ok(value - 10 + 97); + if value < 10 { ret .Ok(value + '0'); } + if uppercase { ret .Ok(value - 10 + 'A'); } + ret .Ok(value - 10 + 'a'); } fn expect_digit(result: DigitResult, expected: u8) { @@ -141,27 +141,28 @@ fn test_all_bytes() { var c = i as u8; @assert(is_ascii(c) == (i <= 127), "ASCII boundary"); @assert(is_control(c) == (i <= 31 || i == 127), "control boundary"); - @assert(is_print(c) == (i >= 32 && i <= 126), "print boundary"); - @assert(is_graph(c) == (i >= 33 && i <= 126), "graph boundary"); - @assert(is_blank(c) == (i == 9 || i == 32), "blank set"); - @assert(is_space(c) == (i == 9 || i == 10 || i == 11 || i == 12 || i == 13 || i == 32), + @assert(is_print(c) == (c >= ' ' && c <= '~'), "print boundary"); + @assert(is_graph(c) == (c >= '!' && c <= '~'), "graph boundary"); + @assert(is_blank(c) == (c == '\t' || c == ' '), "blank set"); + @assert(is_space(c) == (c == '\t' || c == '\n' || c == '\x0B' || c == '\x0C' || + c == '\r' || c == ' '), "space set"); - @assert(is_digit(c) == (i >= 48 && i <= 57), "digit set"); - @assert(is_upper(c) == (i >= 65 && i <= 90), "upper set"); - @assert(is_lower(c) == (i >= 97 && i <= 122), "lower set"); - @assert(is_hex_digit(c) == ((i >= 48 && i <= 57) || - (i >= 65 && i <= 70) || - (i >= 97 && i <= 102)), + @assert(is_digit(c) == (c >= '0' && c <= '9'), "digit set"); + @assert(is_upper(c) == (c >= 'A' && c <= 'Z'), "upper set"); + @assert(is_lower(c) == (c >= 'a' && c <= 'z'), "lower set"); + @assert(is_hex_digit(c) == ((c >= '0' && c <= '9') || + (c >= 'A' && c <= 'F') || + (c >= 'a' && c <= 'f')), "hex digit set"); @assert(is_alpha(c) == (is_upper(c) || is_lower(c)), "alpha relation"); @assert(is_alnum(c) == (is_alpha(c) || is_digit(c)), "alnum relation"); @assert(is_punct(c) == (is_graph(c) && !is_alnum(c)), "punct relation"); if is_upper(c) { - @assert(to_lower(c) == c + 32, "upper to lower"); + @assert(to_lower(c) == c + ('a' - 'A'), "upper to lower"); @assert(to_upper(to_lower(c)) == c, "case round trip"); } else if is_lower(c) { - @assert(to_upper(c) == c - 32, "lower to upper"); + @assert(to_upper(c) == c - ('a' - 'A'), "lower to upper"); @assert(to_lower(to_upper(c)) == c, "case round trip"); } else { @assert(to_lower(c) == c, "lower preserves non-letter"); @@ -192,20 +193,20 @@ fn test_digits() { if value < 10 { @assert(lower == upper, "decimal digits ignore case"); } else { - @assert(lower == upper + 32, "letter digit case distance"); + @assert(lower == upper + ('a' - 'A'), "letter digit case distance"); } value = value + 1; } radix = radix + 1; } - expect_error(digit_value(48, 0), true); - expect_error(digit_value(48, 1), true); - expect_error(digit_value(48, 37), true); - expect_error(digit_value(47, 10), false); - expect_error(digit_value(58, 10), false); - expect_error(digit_value(50, 2), false); - expect_error(digit_value(103, 16), false); + expect_error(digit_value('0', 0), true); + expect_error(digit_value('0', 1), true); + expect_error(digit_value('0', 37), true); + expect_error(digit_value('/', 10), false); + expect_error(digit_value(':', 10), false); + expect_error(digit_value('2', 2), false); + expect_error(digit_value('g', 16), false); expect_range_error(digit_char(36, false)); expect_range_error(digit_char(255, true)); } diff --git a/stage2/utils/big_int.spl b/stage2/utils/big_int.spl index a3a78cc..18a491c 100644 --- a/stage2/utils/big_int.spl +++ b/stage2/utils/big_int.spl @@ -528,8 +528,8 @@ fn parse(out: *BigInt, bytes: []u8, radix: u8): Status { var i: usize = 0; var negative = false; - if bytes.ptr[0] == 45 || bytes.ptr[0] == 43 { - negative = bytes.ptr[0] == 45; + if bytes.ptr[0] == '-' || bytes.ptr[0] == '+' { + negative = bytes.ptr[0] == '-'; i = 1; } if i == bytes.len { ret .Err(Error.at(.UnexpectedEnd, i)); } @@ -578,7 +578,7 @@ fn format(out: *ByteBuffer, source: BigInt, radix: u8, uppercase: bool): Status } magnitude.negative = false; if magnitude.len == 0 { - match ByteBuffer.append_byte(&reverse, 48) { + match ByteBuffer.append_byte(&reverse, '0') { .Ok => {}, .Err(error) => { drop(&magnitude); ret .Err(error); }, } } @@ -597,7 +597,7 @@ fn format(out: *ByteBuffer, source: BigInt, radix: u8, uppercase: bool): Status } } if source.negative { - match ByteBuffer.append_byte(&fresh, 45) { + match ByteBuffer.append_byte(&fresh, '-') { .Ok => {}, .Err(error) => { drop(&magnitude); ByteBuffer.drop(&reverse); ret .Err(error); @@ -773,7 +773,7 @@ fn test_parse_format_and_conversion() { var output = ByteBuffer.new(); expect_ok(format(&output, value, 16, false)); var text = ByteBuffer.as_slice(output); - @assert(text.len == 9 && text.ptr[0] == 45 && text.ptr[1] == 100 && text.ptr[8] == 102); + @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(); diff --git a/stage2/utils/byte_buffer.spl b/stage2/utils/byte_buffer.spl index c374a9d..e825876 100644 --- a/stage2/utils/byte_buffer.spl +++ b/stage2/utils/byte_buffer.spl @@ -211,7 +211,7 @@ fn test_append_and_resize() { @assert(capacity(buffer) >= 1); expect_ok(append(&buffer, "abc")); - expect_ok(append_byte(&buffer, 0)); + expect_ok(append_byte(&buffer, '\0')); expect_ok(append(&buffer, "de")); @assert(buffer.storage.len == 6, "append logical length"); var embedded: [6]u8 = .[97, 98, 99, 0, 100, 101]; diff --git a/stage2/utils/diag.spl b/stage2/utils/diag.spl new file mode 100644 index 0000000..74c4a04 --- /dev/null +++ b/stage2/utils/diag.spl @@ -0,0 +1,243 @@ +// 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(); +} diff --git a/stage2/utils/fmt.spl b/stage2/utils/fmt.spl index 79c7292..dc93fe9 100644 --- a/stage2/utils/fmt.spl +++ b/stage2/utils/fmt.spl @@ -19,7 +19,7 @@ fn append_u64(out: *ByteBuffer, value: u64, radix: u8, uppercase: bool): Status var count: usize = 0; if value == 0 { - digits[0] = 48; + digits[0] = '0'; count = 1; } else { var v = value; @@ -50,7 +50,7 @@ fn append_u64(out: *ByteBuffer, value: u64, radix: u8, uppercase: bool): Status 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, 45) { // '-' + match ByteBuffer.append_byte(out, '-') { .Ok => {}, .Err(error) => { ret .Err(error); }, } @@ -221,14 +221,14 @@ fn test_composition_and_growth() { var i: i32 = 0; while i < 200 { expect_ok(append_i64(&b, i as i64, 10, false)); - expect_ok(ByteBuffer.append_byte(&b, 44)); // ',' + expect_ok(ByteBuffer.append_byte(&b, ',')); // ',' i = i + 1; } expect_ok(append_i64(&b, 200, 10, false)); @assert(ByteBuffer.size(b) > 0, "growth content"); var s = ByteBuffer.as_slice(b); - @assert(s.ptr[0] == 48, "starts with 0"); - @assert(s.ptr[1] == 44, "0 then comma"); + @assert(s.ptr[0] == '0', "starts with 0"); + @assert(s.ptr[1] == ',', "0 then comma"); ByteBuffer.drop(&b); } diff --git a/stage2/utils/frame.spl b/stage2/utils/frame.spl new file mode 100644 index 0000000..e0cc6b5 --- /dev/null +++ b/stage2/utils/frame.spl @@ -0,0 +1,184 @@ +// Expect retcode = 0 +// LSP 传输帧:Content-Length 头 + 空行 + 正文。 +// 这里只做纯逻辑(写入/解析 ByteBuffer/ByteView),真正的 stdin/stdout I/O 由上层负责。 + +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 FrameResult = enum { + Ok: usize, // 消耗的字节数 + Incomplete, + 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; } + i = i + 1; + } + ret true; +} + +fn write_frame(out: *ByteBuffer, body: []u8): Status { + match ByteBuffer.append(out, "Content-Length: ") { .Ok => {}, .Err(e) => { ret .Err(e); } } + match append_usize(out, body.len, 10, false) { .Ok => {}, .Err(e) => { ret .Err(e); } } + match ByteBuffer.append(out, "\r\n\r\n") { .Ok => {}, .Err(e) => { ret .Err(e); } } + ret ByteBuffer.append(out, body); +} + +// 从 input 解析一个帧;正文复制到 body_out。缺数据返回 Incomplete。 +fn parse_frame(input: ByteView, body_out: *ByteBuffer): FrameResult { + var data = ByteView.as_slice(input); + var len = data.len; + + // 找头结束标记 \r\n\r\n + var term = ~(0 as usize); + var i: usize = 0; + while i + 3 < len { + if data[i] == '\r' && data[i + 1] == '\n' && data[i + 2] == '\r' && data[i + 3] == '\n' { + term = i; + break; + } + i = i + 1; + } + if term == ~(0 as usize) { ret .Incomplete; } + + // 在头内找 Content-Length(大小写不敏感) + var content_len: usize = 0; + var found = false; + var j: usize = 0; + while j + 15 <= term { + if ci_prefix(data.ptr, term, j, "content-length:") { + var k = j + 15; + while k < term && (data[k] == ' ' || data[k] == '\t') { k = k + 1; } + var n: usize = 0; + var digits = 0; + while k < term && data[k] >= '0' && data[k] <= '9' { + n = n * 10 + (data[k] - '0') as usize; + digits = digits + 1; + k = k + 1; + } + if digits == 0 { ret .Err(make(.InvalidDigit)); } + content_len = n; + found = true; + break; + } + j = j + 1; + } + if !found { ret .Err(make(.InvalidFormat)); } + + var body_start = term + 4; + if len < body_start || content_len > len - body_start { ret .Incomplete; } + + ByteBuffer.clear(body_out); + var body = data[body_start..body_start + content_len]; + match ByteBuffer.append(body_out, body) { .Ok => {}, .Err(e) => { ret .Err(e); } } + ret .Ok(body_start + content_len); +} + +// ---- 测试辅助 ---- + +fn expect_ok(result: Status) { + match result { + .Ok => {}, + .Err(error) => { @assert(false, "unexpected frame error"); }, + } +} + +fn expect_body(out: *ByteBuffer, expected: []u8) { + var actual = ByteBuffer.as_slice(out.*); + @assert(actual.len == expected.len, "body length"); + var i: usize = 0; + while i < expected.len { + @assert(actual.ptr[i] == expected.ptr[i], "body byte"); + i = i + 1; + } +} + +fn test_roundtrip() { + var wire = ByteBuffer.new(); + 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(); + match parse_frame(ByteView.from_slice(bytes), &body) { + .Ok(used) => { + @assert(used == bytes.len, "consumed all"); + expect_body(&body, "{\"jsonrpc\":\"2.0\"}"); + }, + .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(); + expect_ok(write_frame(&wire, "abcd")); + expect_ok(write_frame(&wire, "ef")); + var bytes = ByteBuffer.as_slice(wire); + + var body = ByteBuffer.new(); + match parse_frame(ByteView.from_slice(bytes), &body) { + .Ok(used) => { + expect_body(&body, "abcd"); + var rest = bytes[used..bytes.len]; + match parse_frame(ByteView.from_slice(rest), &body) { + .Ok(used2) => { expect_body(&body, "ef"); @assert(used2 == rest.len, "second"); }, + _ => { @assert(false, "second frame"); }, + } + }, + _ => { @assert(false, "first frame"); }, + } + ByteBuffer.drop(&body); + ByteBuffer.drop(&wire); +} + +fn test_incomplete_and_errors() { + var body = ByteBuffer.new(); + + // 头未结束 + var h1 = "Content-Length: 3\r\n"; + match parse_frame(ByteView.from_slice(h1), &body) { + .Incomplete => {}, + _ => { @assert(false, "h1"); }, + } + // 正文不足 + var h2 = "Content-Length: 3\r\n\r\nab"; + match parse_frame(ByteView.from_slice(h2), &body) { + .Incomplete => {}, + _ => { @assert(false, "h2"); }, + } + // 缺 Content-Length + var h3 = "X: 1\r\n\r\n"; + match parse_frame(ByteView.from_slice(h3), &body) { + .Ok(used) => { @assert(false, "h3 ok"); }, + .Incomplete => { @assert(false, "h3 incomplete"); }, + .Err(error) => {}, + } + // 大小写不敏感 + 额外头 + var h4 = "X-Test: yes\r\ncontent-length: 2\r\n\r\nhi"; + match parse_frame(ByteView.from_slice(h4), &body) { + .Ok(used) => { expect_body(&body, "hi"); }, + _ => { @assert(false, "h4"); }, + } + ByteBuffer.drop(&body); +} + +fn main() { + test_roundtrip(); + test_multiple_and_leftover(); + test_incomplete_and_errors(); +} diff --git a/stage2/utils/hash.spl b/stage2/utils/hash.spl new file mode 100644 index 0000000..6b31505 --- /dev/null +++ b/stage2/utils/hash.spl @@ -0,0 +1,88 @@ +// Expect retcode = 0 +// 哈希工具:FNV-1a 字节哈希 + 整型混合;并给出可直接交给 map 的回调 +// (map 的 hash/eq 回调接收指向键字节的指针,这里按 usize / ByteView 解释)。 +// 仅提供确定性哈希与等值,不承诺密码学性质。 + +type .{ .ByteView } = @import("byte_view.spl"); + +// FNV-1a 64 位,逐字节;空输入返回 offset basis。 +fn hash_bytes(data: *u8, len: usize): usize { + var h: usize = 14695981039346656037; + var i: usize = 0; + while i < len { + h = (h ^ (data[i] as usize)) * 1099511628211; + i = i + 1; + } + ret h; +} + +fn hash_byte_view(view: ByteView): usize { + ret hash_bytes(view.ptr, view.len); +} + +// 整型混合(splitmix 风格的移位-乘法),保证非零与良好扩散。 +fn hash_usize_value(value: usize): usize { + var h = value; + h = (h ^ (h >> 30)) * 1099511628211; + h = h ^ (h >> 27); + h = h * 1099511628211; + h = h ^ (h >> 31); + if h == 0 { ret 1099511628211; } // 0 映射到非零 + ret h; +} + +// ---- map 回调 ---- + +// 键按 usize 解释。 +fn hash_usize(key: *u8): usize { + ret hash_usize_value((key as *usize).*); +} + +fn eq_usize(a: *u8, b: *u8): bool { + ret (a as *usize).* == (b as *usize).*; +} + +// 键按 ByteView 解释,哈希其指向的字节。 +fn hash_view(key: *u8): usize { + var view = (key as *ByteView).*; + ret hash_byte_view(view); +} + +fn eq_view(a: *u8, b: *u8): bool { + ret ByteView.eq((a as *ByteView).*, (b as *ByteView).*); +} + +// ---- 测试辅助 ---- + +fn expect_hash(view: []u8, expected: usize) { + var got = hash_bytes(view.ptr, view.len); + @assert(got == expected, "hash value"); +} + +fn test_fnv_vectors() { + expect_hash("", 14695981039346656037); + expect_hash("a", 12638187200555641996); + expect_hash("foobar", 9625390261332436968); + expect_hash("hello", 11831194018420276491); + + // 同一输入稳定;不同输入不同。 + var s1: []u8 = "abc"; + var s2: []u8 = "abd"; + @assert(hash_bytes(s1.ptr, 3) == hash_bytes(s1.ptr, 3), "deterministic"); + @assert(hash_bytes(s1.ptr, 3) != hash_bytes(s2.ptr, 3), "distinct"); + + // ByteView 与 bytes 一致。 + var v = ByteView.from_slice("foobar"); + @assert(hash_byte_view(v) == hash_bytes(v.ptr, v.len), "view == bytes"); + + // 整型混合:0 不保持 0,且稳定/区分。 + @assert(hash_usize_value(0) != 0, "zero mixed"); + @assert(hash_usize_value(7) == hash_usize_value(7), "int stable"); + @assert(hash_usize_value(7) != hash_usize_value(8), "int distinct"); +} + +// map 回调(hash_usize/eq_usize/hash_view/eq_view)由 map.spl 在自己的测试里验证。 + +fn main() { + test_fnv_vectors(); +} diff --git a/stage2/utils/io.spl b/stage2/utils/io.spl new file mode 100644 index 0000000..90058b3 --- /dev/null +++ b/stage2/utils/io.spl @@ -0,0 +1,152 @@ +// Expect retcode = 0 +// I/O 便利层(构建在 sys/file 之上): +// - C 字符串路径/模式的 open 包装 +// - 整块读写(write_all / read_all) +// - stdout/stderr 打印 +// 说明:read_all 会按文件大小一次性读入 ByteBuffer;不处理增长中的文件。 + +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"); + +// 把 view 复制到以 NUL 结尾的临时缓冲,返回其 data(归属 tmp,调用后勿再用)。 +fn cstr(tmp: *ByteBuffer, view: ByteView): *u8 { + ByteBuffer.clear(tmp); + if view.len != 0 { + match ByteBuffer.append(tmp, ByteView.as_slice(view)) { + .Ok => {}, + .Err(error) => { ret null; }, + } + } + match ByteBuffer.append_byte(tmp, '\0') { + .Ok => {}, + .Err(error) => { ret null; }, + } + ret tmp.storage.data; +} + +fn open_cstr(path: ByteView, mode: ByteView): File { + var pbuf = ByteBuffer.new(); + var mbuf = ByteBuffer.new(); + var p = cstr(&pbuf, path); + var m = cstr(&mbuf, mode); + var 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; + } + ret total; +} + +fn print(bytes: []u8) { + var out = File.stdout(); + write_all(&out, bytes); +} + +fn println(bytes: []u8) { + print(bytes); + print("\n"); +} + +fn eprint(bytes: []u8) { + var err = File.stderr(); + write_all(&err, bytes); +} + +// 读取整个文件到 out(先清空)。打开失败或不完整返回 Err(.Io)。 +fn read_all(path: ByteView, out: *ByteBuffer): Status { + 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); + if sz != 0 { + match ByteBuffer.reserve(out, sz as usize) { + .Ok => {}, + .Err(error) => { File.close(&f); 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; + } + File.close(&f); + ret .Ok; +} + +// ---- 测试辅助 ---- + +fn view(text: []u8): ByteView { + ret ByteView.from_slice(text); +} + +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); + + var out = ByteBuffer.new(); + match read_all(path, &out) { + .Ok => {}, + .Err(error) => { @assert(false, "read_all"); }, + } + var got = ByteBuffer.as_slice(out); + @assert(got.len == 11, "roundtrip length"); + @assert(got.ptr[0] == 'h' && got.ptr[5] == '\n' && got.ptr[10] == 'd', "roundtrip bytes"); + ByteBuffer.drop(&out); +} + +fn test_append_and_missing() { + var path = view("build/_io_append.tmp"); + + var f = open_cstr(path, view("wb")); + @assert(File.valid(&f), "open wb"); + write_all(&f, "ab"); + File.close(&f); + + 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"); + + var out = ByteBuffer.new(); + match read_all(path, &out) { + .Ok => {}, + .Err(error) => { @assert(false, "read append"); }, + } + var got = ByteBuffer.as_slice(out); + @assert(got.len == 4 && got.ptr[2] == 'c' && got.ptr[3] == 'd', "append content"); + + // 不存在的文件 + match read_all(view("build/_io_no_such_file.tmp"), &out) { + .Ok => { @assert(false, "missing should fail"); }, + .Err(error) => { + match error.kind { .Io => {}, _ => { @assert(false, "error kind"); } } + }, + } + ByteBuffer.drop(&out); +} + +fn main() { + test_roundtrip(); + test_append_and_missing(); + println("io: ok"); +} diff --git a/stage2/utils/json.spl b/stage2/utils/json.spl index 6c873f0..bfaf081 100644 --- a/stage2/utils/json.spl +++ b/stage2/utils/json.spl @@ -95,17 +95,24 @@ fn peek(p: *Parser): i32 { ret p.src.ptr[p.pos] as i32; } +// 当前字符是否等于 ch(EOF 时 false)。 +fn at_char(p: *Parser, ch: u8): bool { + var c = peek(p); + if c < 0 { ret false; } + ret (c as u8) == ch; +} + fn parse_string_raw(p: *Parser): ViewResult { - if peek(p) != 34 { ret .Err(at(.InvalidFormat, p.pos)); } + if !at_char(p, '"') { ret .Err(at(.InvalidFormat, p.pos)); } p.pos = p.pos + 1; var start = p.pos; while p.pos < p.src.len { var c = p.src.ptr[p.pos]; - if c == 34 { break; } - if c == 92 { // '\\' + if c == '"' { break; } + if c == '\\' { // '\\' p.pos = p.pos + 1; if p.pos >= p.src.len { ret .Err(at(.UnexpectedEnd, p.pos)); } - if p.src.ptr[p.pos] == 117 { // 'u' + if p.src.ptr[p.pos] == 'u' { // 'u' var k: usize = 0; while k < 4 { p.pos = p.pos + 1; @@ -141,18 +148,18 @@ fn parse_literal(p: *Parser, word: []u8, kind: JsonKind): NodeResult { fn parse_number(p: *Parser): NodeResult { var start = p.pos; - if peek(p) == 45 { p.pos = p.pos + 1; } // '-' + if at_char(p, '-') { p.pos = p.pos + 1; } // '-' if p.pos >= p.src.len || !is_digit(p.src.ptr[p.pos]) { ret .Err(at(.InvalidDigit, p.pos)); } - if p.src.ptr[p.pos] == 48 { // 前导 0 后面不能接数字 + if p.src.ptr[p.pos] == '0' { // 前导 0 后面不能接数字 p.pos = p.pos + 1; } else { while p.pos < p.src.len && is_digit(p.src.ptr[p.pos]) { p.pos = p.pos + 1; } } - if p.pos < p.src.len && p.src.ptr[p.pos] == 46 { // '.' + if p.pos < p.src.len && p.src.ptr[p.pos] == '.' { // '.' p.pos = p.pos + 1; if p.pos >= p.src.len || !is_digit(p.src.ptr[p.pos]) { ret .Err(at(.InvalidDigit, p.pos)); @@ -161,9 +168,9 @@ fn parse_number(p: *Parser): NodeResult { p.pos = p.pos + 1; } } - if p.pos < p.src.len && (p.src.ptr[p.pos] == 101 || p.src.ptr[p.pos] == 69) { // e/E + if p.pos < p.src.len && (p.src.ptr[p.pos] == 'e' || p.src.ptr[p.pos] == 'E') { // e/E p.pos = p.pos + 1; - if p.pos < p.src.len && (p.src.ptr[p.pos] == 43 || p.src.ptr[p.pos] == 45) { + if p.pos < p.src.len && (p.src.ptr[p.pos] == '+' || p.src.ptr[p.pos] == '-') { p.pos = p.pos + 1; } if p.pos >= p.src.len || !is_digit(p.src.ptr[p.pos]) { @@ -190,7 +197,7 @@ fn parse_array(p: *Parser): NodeResult { var kids = Vec.blank(@sizeof(JsonChild), 8); skip_ws(p); - if peek(p) == 93 { // ']' + if at_char(p, ']') { // ']' p.pos = p.pos + 1; var empty = blank_node(.Array); empty.children = kids; @@ -211,12 +218,11 @@ fn parse_array(p: *Parser): NodeResult { } skip_ws(p); - var c = peek(p); - if c == 44 { // ',' + if at_char(p, ',') { // ',' p.pos = p.pos + 1; continue; } - if c == 93 { // ']' + if at_char(p, ']') { // ']' p.pos = p.pos + 1; break; } @@ -234,7 +240,7 @@ fn parse_object(p: *Parser): NodeResult { var kids = Vec.blank(@sizeof(JsonChild), 8); skip_ws(p); - if peek(p) == 125 { // '}' + if at_char(p, '}') { // '}' p.pos = p.pos + 1; var empty = blank_node(.Object); empty.children = kids; @@ -250,7 +256,7 @@ fn parse_object(p: *Parser): NodeResult { } skip_ws(p); - if peek(p) != 58 { // ':' + if !at_char(p, ':') { // ':' Vec.drop(&kids); ret .Err(at(.InvalidFormat, p.pos)); } @@ -270,12 +276,11 @@ fn parse_object(p: *Parser): NodeResult { } skip_ws(p); - var c = peek(p); - if c == 44 { // ',' + if at_char(p, ',') { // ',' p.pos = p.pos + 1; continue; } - if c == 125 { // '}' + if at_char(p, '}') { // '}' p.pos = p.pos + 1; break; } @@ -294,7 +299,7 @@ fn parse_value(p: *Parser): NodeResult { if c < 0 { ret .Err(at(.UnexpectedEnd, p.pos)); } var ch = c as u8; - if ch == 34 { // '"' + if ch == '"' { // '"' var view = ByteView.empty(); match parse_string_raw(p) { .Ok(v) => { view = v; }, @@ -304,11 +309,11 @@ fn parse_value(p: *Parser): NodeResult { node.text = view; ret intern_node(p, node); } - if ch == 123 { ret parse_object(p); } // '{' - if ch == 91 { ret parse_array(p); } // '[' - if ch == 116 { ret parse_literal(p, "true", .True); } - if ch == 102 { ret parse_literal(p, "false", .False); } - if ch == 110 { ret parse_literal(p, "null", .Null); } + if ch == '{' { ret parse_object(p); } // '{' + if ch == '[' { ret parse_array(p); } // '[' + 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); } ret parse_number(p); } @@ -431,7 +436,7 @@ fn object_find(doc: *Json, index: usize, key: ByteView): FindResult { } fn emit_quoted(out: *ByteBuffer, text: ByteView): Status { - match ByteBuffer.append_byte(out, 34) { // '"' + match ByteBuffer.append_byte(out, '"') { // '"' .Ok => {}, .Err(error) => { ret .Err(error); }, } @@ -439,11 +444,11 @@ fn emit_quoted(out: *ByteBuffer, text: ByteView): Status { .Ok => {}, .Err(error) => { ret .Err(error); }, } - ret ByteBuffer.append_byte(out, 34); + ret ByteBuffer.append_byte(out, '"'); } fn emit_indent(out: *ByteBuffer, indent: usize, depth: usize): Status { - match ByteBuffer.append_byte(out, 10) { // '\n' + match ByteBuffer.append_byte(out, '\n') { // '\n' .Ok => {}, .Err(error) => { ret .Err(error); }, } @@ -468,11 +473,11 @@ fn emit(out: *ByteBuffer, doc: *Json, index: usize, indent: usize, depth: usize) fn emit_children(out: *ByteBuffer, doc: *Json, n: *JsonNode, is_object: bool, indent: usize, depth: usize): Status { - var open_byte: u8 = 91; // '[' - var close_byte: u8 = 93; // ']' + var open_byte: u8 = '['; // '[' + var close_byte: u8 = ']'; // ']' if is_object { - open_byte = 123; // '{' - close_byte = 125; // '}' + open_byte = '{'; // '{' + close_byte = '}'; // '}' } match ByteBuffer.append_byte(out, open_byte) { @@ -488,7 +493,7 @@ fn emit_children(out: *ByteBuffer, doc: *Json, n: *JsonNode, is_object: bool, in var i: usize = 0; while i < count { if i != 0 { - match ByteBuffer.append_byte(out, 44) { // ',' + match ByteBuffer.append_byte(out, ',') { // ',' .Ok => {}, .Err(error) => { ret .Err(error); }, } @@ -508,12 +513,12 @@ fn emit_children(out: *ByteBuffer, doc: *Json, n: *JsonNode, is_object: bool, in .Ok => {}, .Err(error) => { ret .Err(error); }, } - match ByteBuffer.append_byte(out, 58) { // ':' + match ByteBuffer.append_byte(out, ':') { // ':' .Ok => {}, .Err(error) => { ret .Err(error); }, } if indent != 0 { - match ByteBuffer.append_byte(out, 32) { // ' ' + match ByteBuffer.append_byte(out, ' ') { // ' ' .Ok => {}, .Err(error) => { ret .Err(error); }, } @@ -746,14 +751,14 @@ fn test_construction_helpers() { fn test_large_array() { var out = ByteBuffer.new(); - expect_ok(ByteBuffer.append_byte(&out, 91)); // '[' + expect_ok(ByteBuffer.append_byte(&out, '[')); // '[' var i: usize = 0; while i < 1000 { - if i != 0 { expect_ok(ByteBuffer.append_byte(&out, 44)); } - expect_ok(ByteBuffer.append_byte(&out, 49)); // '1' + if i != 0 { expect_ok(ByteBuffer.append_byte(&out, ',')); } + expect_ok(ByteBuffer.append_byte(&out, '1')); // '1' i = i + 1; } - expect_ok(ByteBuffer.append_byte(&out, 93)); // ']' + expect_ok(ByteBuffer.append_byte(&out, ']')); // ']' var text = ByteBuffer.as_slice(out); var r = parse(ByteView.from_slice(text)); diff --git a/stage2/utils/map.spl b/stage2/utils/map.spl index bc98544..ed9670e 100644 --- a/stage2/utils/map.spl +++ b/stage2/utils/map.spl @@ -6,6 +6,7 @@ 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 .{ .copy } = @import("../sys/mem.spl"); // 槽位状态:0=EMPTY,1=OCCUPIED,2=DELETED(墓碑,仅在扩容重哈希时清除)。 @@ -239,15 +240,6 @@ fn take(source: *Map): Map { // ---- 测试辅助 ---- -fn hash_usize(key: *u8): usize { - var value = (key as *usize).*; - ret value * 2654435761; -} - -fn eq_usize(a: *u8, b: *u8): bool { - ret (a as *usize).* == (b as *usize).*; -} - fn expect_ok(result: Status) { match result { .Ok => {}, @@ -391,21 +383,6 @@ fn test_take_and_self_put() { drop(&moved); } -fn hash_view(key: *u8): usize { - var view = (key as *ByteView).*; - var h: usize = 14695981039346656037; - var i: usize = 0; - while i < view.len { - h = (h ^ (view.ptr[i] as usize)) * 1099511628211; - i = i + 1; - } - ret h; -} - -fn eq_view(a: *u8, b: *u8): bool { - ret ByteView.eq((a as *ByteView).*, (b as *ByteView).*); -} - fn test_byte_view_keys() { var m = new(@sizeof(ByteView), @alignof(ByteView), @sizeof(i32), @alignof(i32), hash_view, eq_view); diff --git a/stage2/utils/path.spl b/stage2/utils/path.spl new file mode 100644 index 0000000..3ba5cbe --- /dev/null +++ b/stage2/utils/path.spl @@ -0,0 +1,231 @@ +// Expect retcode = 0 +// 路径工具(不访问文件系统):同时接受 '/' 与 '\\';支持 Windows 盘符 "C:"。 +// 输入为借用 ByteView,输出写入调用方的 ByteBuffer。 +// 注意:normalize 对相对路径的前导 ".." 采取丢弃(不保留),适用于导入路径场景。 + +type .{ .Error, .Status, .make } = @import("error.spl"); +type .{ .ByteView } = @import("byte_view.spl"); +type .{ .ByteBuffer } = @import("byte_buffer.spl"); +type .{ .Vec } = @import("vec.spl"); + +fn is_sep(c: u8): bool { + ret c == '/' || c == '\\'; // '/' '\\' +} + +fn is_drive(path: ByteView): bool { + ret path.len >= 2 && path.ptr[1] == ':'; // ':' +} + +fn is_absolute(path: ByteView): bool { + if path.len == 0 { ret false; } + if is_sep(path.ptr[0]) { ret true; } + ret is_drive(path); +} + +// 最后一个分隔符下标;无返回 ~0。 +fn last_sep(path: ByteView): usize { + var i = path.len; + while i > 0 { + i = i - 1; + if is_sep(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; }, + .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(); }, + } + ret ByteView.empty(); +} + +// 扩展名(不含 '.');无扩展名返回空。 +fn extension(path: ByteView): ByteView { + var base = basename(path); + var i = base.len; + while i > 0 { + i = i - 1; + 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 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)); + } + 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); } } + } + ret ByteBuffer.append(out, ByteView.as_slice(rel)); +} + +// 折叠重复分隔符、'.',及'..'(回退上一段;相对路径前导 '..' 丢弃)。 +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; // 跳过盘符 + } + + 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); }, + } + } + + Vec.drop(&starts); + ret .Ok; +} + +// ---- 测试辅助 ---- + +fn expect_ok(result: Status) { + match result { + .Ok => {}, + .Err(error) => { @assert(false, "unexpected path error"); }, + } +} + +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"); +} + +fn expect_out(out: *ByteBuffer, expected: []u8) { + var actual = ByteBuffer.as_slice(out.*); + @assert(actual.len == expected.len, "out length"); + var i: usize = 0; + while i < expected.len { + @assert(actual.ptr[i] == expected.ptr[i], "out 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_join() { + var out = ByteBuffer.new(); + expect_ok(join_into(&out, view("stage2/utils"), view("vec.spl"))); + 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); +} + +fn test_normalize() { + var out = ByteBuffer.new(); + expect_ok(normalize_into(&out, view("a/b/../c"))); + 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); +} + +fn main() { + test_parts(); + test_join(); + test_normalize(); +} diff --git a/stage2/utils/source.spl b/stage2/utils/source.spl new file mode 100644 index 0000000..7a7c388 --- /dev/null +++ b/stage2/utils/source.spl @@ -0,0 +1,258 @@ +// Expect retcode = 0 +// 源文件与位置映射:fname + text 借用调用方存储;行起始表由 Source 拥有。 +// 提供字节偏移 <-> (行,列)(行列均为 1-based、按字节计),以及取整行文本。 +// Sources 是按 fname 索引的多文件集合。 + +type .{ .Error, .Status, .make } = @import("error.spl"); +type .{ .ByteView, .FindResult } = @import("byte_view.spl"); +type .{ .Vec } = @import("vec.spl"); +type .{ .Map } = @import("map.spl"); +type .{ .hash_view, .eq_view } = @import("hash.spl"); + +type Position = struct { + line: usize, // 1-based + col: usize, // 1-based,按字节 +} + +type Source = @self; + +fname: ByteView, // 借用 +text: ByteView, // 借用 +line_starts: Vec, // usize:每行起始字节偏移(含首行 0) + +type SourceResult = enum { + Ok: Source, + Err: Error, +} + +type AddResult = enum { + Ok: usize, + Err: Error, +} + +fn blank(): Source { + ret Source { + .fname = ByteView.empty(), + .text = ByteView.empty(), + .line_starts = Vec.blank(@sizeof(usize), 8), + }; +} + +fn new(fname: ByteView, text: ByteView): SourceResult { + var starts = Vec.blank(@sizeof(usize), 8); + var zero: usize = 0; + match Vec.push(&starts, &zero as *u8) { + .Ok => {}, + .Err(error) => { ret .Err(error); }, + } + + var i: usize = 0; + while i < text.len { + if text.ptr[i] == '\n' { + var next = i + 1; + match Vec.push(&starts, &next as *u8) { + .Ok => {}, + .Err(error) => { Vec.drop(&starts); ret .Err(error); }, + } + } + i = i + 1; + } + + ret .Ok(Source { .fname = fname, .text = text, .line_starts = starts }); +} + +fn line_count(self: Source): usize { + ret self.line_starts.len; +} + +fn line_start_offset(self: Source, line: usize): usize { + if line == 0 || line > self.line_starts.len { ret ~(0 as usize); } + match Vec.get(&self.line_starts, line - 1) { + .Ok(ptr) => { ret (ptr as *usize).*; }, + .Err(error) => { ret ~(0 as usize); }, + } + ret ~(0 as usize); +} + +// offset 所在行(0-based);超出范围夹到末行。 +fn line_index_at(self: Source, offset: usize): usize { + var off = offset; + if off > self.text.len { off = self.text.len; } + var lo: usize = 0; + var hi: usize = self.line_starts.len; + while lo + 1 < hi { + var mid = (lo + hi) / 2; + var start = line_start_offset(self, mid + 1); // 1-based -> 内部 + if start != ~(0 as usize) && start <= off { lo = mid; } else { hi = mid; } + } + ret lo; +} + +fn line_col(self: Source, offset: usize): Position { + var off = offset; + if off > self.text.len { off = self.text.len; } + var line0 = line_index_at(self, off); + var start = line_start_offset(self, line0 + 1); // 1-based + if start == ~(0 as usize) { start = 0; } + ret Position { .line = line0 + 1, .col = off - start + 1 }; +} + +// line 为 1-based;返回整行(去掉行尾 '\n' 与 '\r')。 +fn line_text(self: Source, line: usize): ByteView { + var n = self.line_starts.len; + if line == 0 || line > n { ret ByteView.empty(); } + var start = line_start_offset(self, line); + if start == ~(0 as usize) { ret ByteView.empty(); } + var end = self.text.len; + if line < n { + var next = line_start_offset(self, line + 1); + if next != ~(0 as usize) { end = next; } + } + while end > start && (self.text.ptr[end - 1] == '\n' || self.text.ptr[end - 1] == '\r') { + end = end - 1; + } + var r = ByteView.slice(self.text, start, end); + match r { + .Ok(v) => { ret v; }, + .Err(error) => { ret ByteView.empty(); }, + } + ret ByteView.empty(); +} + +fn drop(self: *Source) { + Vec.drop(&self.line_starts); +} + +// ---- 多文件集合 ---- + +type Sources = struct { + items: Vec, // Source + by_name: Map, // ByteView(fname) -> usize +} + +fn set_new(): Sources { + ret Sources { + .items = Vec.blank(@sizeof(Source), 8), + .by_name = Map.blank(@sizeof(ByteView), @alignof(ByteView), @sizeof(usize), + @alignof(usize), hash_view, eq_view), + }; +} + +fn set_add(set: *Sources, fname: ByteView, text: ByteView): AddResult { + var src = new(fname, text); + var s = blank(); + match src { + .Ok(v) => { s = v; }, + .Err(error) => { ret .Err(error); }, + } + + var index = set.items.len; + match Vec.push(&set.items, &s as *u8) { + .Ok => {}, + .Err(error) => { drop(&s); ret .Err(error); }, + } + match Map.put(&set.by_name, &fname as *u8, &index as *u8) { + .Ok => {}, + .Err(error) => { + // 回滚最后一项 + set.items.len = index; + drop(&s); + ret .Err(error); + }, + } + ret .Ok(index); +} + +fn set_at(set: *Sources, index: usize): *Source { + match Vec.get(&set.items, index) { + .Ok(ptr) => { ret ptr as *Source; }, + .Err(error) => { ret null; }, + } + ret null; +} + +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); } + ret .NotFound; +} + +fn set_drop(set: *Sources) { + var i: usize = 0; + while i < set.items.len { + var s = set_at(set, i); + if s != null { drop(s); } + i = i + 1; + } + Vec.drop(&set.items); + Map.drop(&set.by_name); +} + +// ---- 测试辅助 ---- + +fn view(text: []u8): ByteView { + ret ByteView.from_slice(text); +} + +fn test_single() { + var text = "line1\nline2\n\nline4"; + var r = new(view("a.spl"), view(text)); + var src = blank(); + match r { + .Ok(v) => { src = v; }, + .Err(error) => { @assert(false, "source new"); }, + } + + @assert(line_count(src) == 4, "line count"); + + var p0 = line_col(src, 0); + @assert(p0.line == 1 && p0.col == 1, "start"); + var p6 = line_col(src, 6); + @assert(p6.line == 2 && p6.col == 1, "line2 start"); + var p8 = line_col(src, 8); + @assert(p8.line == 2 && p8.col == 3, "line2 col3"); + var p12 = line_col(src, 12); + @assert(p12.line == 3 && p12.col == 1, "empty line3"); + var p13 = line_col(src, 13); + @assert(p13.line == 4 && p13.col == 1, "line4 start"); + var p18 = line_col(src, 18); + @assert(p18.line == 4 && p18.col == 6, "past end clamps"); + + var l1 = line_text(src, 1); + var l3 = line_text(src, 3); + var l4 = line_text(src, 4); + @assert(ByteView.eq(l1, view("line1")), "line1"); + @assert(ByteView.eq(l3, view("")), "line3 empty"); + @assert(ByteView.eq(l4, view("line4")), "line4"); + + drop(&src); +} + +fn test_sources() { + var set = set_new(); + var r1 = set_add(&set, view("a.spl"), view("x\ny")); + var r2 = set_add(&set, view("b.spl"), view("z")); + var a: usize = 0; + var b: usize = 0; + match r1 { .Ok(v) => { a = v; }, .Err(e) => { @assert(false, "add a"); } } + match r2 { .Ok(v) => { b = v; }, .Err(e) => { @assert(false, "add b"); } } + @assert(a == 0 && b == 1, "indices"); + + match set_find(&set, view("b.spl")) { + .Found(i) => { @assert(i == 1, "find b"); }, + .NotFound => { @assert(false, "b missing"); }, + } + match set_find(&set, view("c.spl")) { + .Found(i) => { @assert(false, "c unexpected"); }, + .NotFound => {}, + } + + var sb = set_at(&set, b); + @assert(sb != null && line_count(sb.*) == 1, "at b"); + set_drop(&set); +} + +fn main() { + test_single(); + test_sources(); +} diff --git a/stage2/utils/str.spl b/stage2/utils/str.spl new file mode 100644 index 0000000..889126b --- /dev/null +++ b/stage2/utils/str.spl @@ -0,0 +1,238 @@ +// Expect retcode = 0 +// 拥有型字节字符串:在 ByteBuffer 之上提供字符串常用操作。 +// 普通赋值复制句柄(共享存储),所有权转移用 take;失败不产生部分状态。 + +type .{ .Error, .Status, .make } = @import("error.spl"); +type .{ .ByteView, .FindResult } = @import("byte_view.spl"); +type .{ .ByteBuffer } = @import("byte_buffer.spl"); + +type Str = @self; + +storage: ByteBuffer, + +type StrResult = enum { + Ok: Str, + Err: Error, +} + +fn new(): Str { + ret Str { .storage = ByteBuffer.new() }; +} + +// 复制 view 指向的字节。 +fn from_view(view: ByteView): StrResult { + var s = new(); + match append_view(&s, view) { + .Ok => {}, + .Err(error) => { drop(&s); ret .Err(error); }, + } + ret .Ok(s); +} + +fn from_slice(bytes: []u8): StrResult { + ret from_view(ByteView.from_slice(bytes)); +} + +fn size(self: Str): usize { + ret ByteBuffer.size(self.storage); +} + +fn is_empty(self: Str): bool { + ret ByteBuffer.is_empty(self.storage); +} + +fn as_view(self: *Str): ByteView { + ret ByteView.from_slice(ByteBuffer.as_slice(self.storage)); +} + +fn as_slice(self: *Str): []u8 { + ret ByteBuffer.as_slice(self.storage); +} + +fn reserve(self: *Str, capacity: usize): Status { + ret ByteBuffer.reserve(&self.storage, capacity); +} + +fn append_view(self: *Str, view: ByteView): Status { + ret ByteBuffer.append(&self.storage, ByteView.as_slice(view)); +} + +fn append(self: *Str, bytes: []u8): Status { + ret ByteBuffer.append(&self.storage, bytes); +} + +fn append_str(self: *Str, other: Str): Status { + var o = other; + ret append_view(self, as_view(&o)); +} + +fn append_byte(self: *Str, byte: u8): Status { + ret ByteBuffer.append_byte(&self.storage, byte); +} + +fn clear(self: *Str) { + ByteBuffer.clear(&self.storage); +} + +fn eq(a: Str, b: Str): bool { + var x = a; + var y = b; + ret ByteView.eq(as_view(&x), as_view(&y)); +} + +fn eq_slice(a: Str, bytes: []u8): bool { + var x = a; + ret ByteView.eq(as_view(&x), ByteView.from_slice(bytes)); +} + +fn cmp(a: Str, b: Str): i32 { + var x = a; + var y = b; + ret ByteView.cmp(as_view(&x), as_view(&y)); +} + +fn starts_with(self: Str, prefix: Str): bool { + var x = self; + var p = prefix; + ret ByteView.starts_with(as_view(&x), as_view(&p)); +} + +fn ends_with(self: Str, suffix: Str): bool { + var x = self; + var s = suffix; + ret ByteView.ends_with(as_view(&x), as_view(&s)); +} + +fn find(self: Str, needle: Str): FindResult { + var x = self; + var n = needle; + ret ByteView.find(as_view(&x), as_view(&n)); +} + +// 深复制;允许 out 与 source 为同一对象。 +fn clone(out: *Str, source: Str): Status { + ret ByteBuffer.clone(&out.storage, source.storage); +} + +fn take(source: *Str): Str { + ret Str { .storage = ByteBuffer.take(&source.storage) }; +} + +fn drop(self: *Str) { + ByteBuffer.drop(&self.storage); +} + +// ---- 测试辅助 ---- + +fn expect_ok(result: Status) { + match result { + .Ok => {}, + .Err(error) => { @assert(false, "unexpected str error"); }, + } +} + +fn expect_text(s: *Str, expected: []u8) { + var view = as_view(s); + @assert(ByteView.eq(view, ByteView.from_slice(expected)), "str text"); +} + +fn test_construction() { + var s = new(); + @assert(is_empty(s) && size(s) == 0, "empty new"); + expect_ok(append(&s, "hello")); + expect_ok(append_byte(&s, ' ')); + expect_ok(append(&s, "world")); + expect_text(&s, "hello world"); + @assert(size(s) == 11, "size"); + + var r = from_slice("copy"); + match r { + .Ok(copy) => { + expect_text(©, "copy"); + @assert(eq(copy, copy), "self eq"); + drop(©); + }, + .Err(error) => { @assert(false, "from_slice"); }, + } + drop(&s); +} + +fn test_compare_and_search() { + var hello_r = from_slice("hello"); + var hello: Str = new(); + match hello_r { + .Ok(v) => { hello = v; }, + .Err(error) => { @assert(false, "hello"); }, + } + var help_r = from_slice("help"); + var help: Str = new(); + match help_r { + .Ok(v) => { help = v; }, + .Err(error) => { @assert(false, "help"); }, + } + + @assert(eq(hello, hello), "eq"); + @assert(!eq(hello, help), "ne"); + @assert(eq_slice(hello, "hello"), "eq_slice"); + @assert(cmp(hello, help) < 0, "cmp lt"); + @assert(cmp(help, hello) > 0, "cmp gt"); + + @assert(starts_with(hello, hello), "starts_with"); + @assert(ends_with(hello, hello), "ends_with"); + var pre_r = from_slice("he"); + var pre: Str = new(); + 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(); + match needle_r { .Ok(v) => { needle = v; }, .Err(e) => { @assert(false, "needle"); } } + match find(hello, needle) { + .Found(index) => { @assert(index == 2, "find ll"); }, + .NotFound => { @assert(false, "ll not found"); }, + } + match find(hello, pre) { + .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(); + expect_ok(append(&source, "owned")); + + var copy = new(); + expect_ok(clone(©, source)); + expect_text(©, "owned"); + expect_ok(append_byte(&source, '!')); // '!' + expect_text(©, "owned"); // 独立 + + var before = ByteBuffer.as_slice(copy.storage).ptr; + expect_ok(clone(©, copy)); + @assert(ByteBuffer.as_slice(copy.storage).ptr != before, "self clone replaces allocation"); + expect_text(©, "owned"); + + var moved = take(©); + @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() { + test_construction(); + test_compare_and_search(); + test_clone_take_and_failures(); +} diff --git a/stage2/utils/utf8.spl b/stage2/utils/utf8.spl new file mode 100644 index 0000000..e4ba7ec --- /dev/null +++ b/stage2/utils/utf8.spl @@ -0,0 +1,251 @@ +// 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; +} + +// 从 index 解码一个码点到 out_cp,返回消耗字节数;非法返回 0。 +fn decode(data: *u8, len: usize, index: usize, out_cp: *u32): usize { + if index >= len { ret 0; } + var b0 = data[index]; + + if b0 < 0x80 { + out_cp.* = b0 as u32; + ret 1; + } + if b0 < 0xC2 { ret 0; } // 续字节或过长编码的前导 + + if b0 < 0xE0 { + if index + 1 >= len { ret 0; } + var b1 = data[index + 1]; + if (b1 & 0xC0) != 0x80 { ret 0; } + out_cp.* = (((b0 & 0x1F) as u32) << 6) | ((b1 & 0x3F) as u32); + ret 2; + } + if b0 < 0xF0 { + if index + 2 >= len { ret 0; } + var b1 = data[index + 1]; + var b2 = data[index + 2]; + if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 { ret 0; } + 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; } // 代理区 + out_cp.* = cp; + ret 3; + } + if b0 < 0xF5 { + if index + 3 >= len { ret 0; } + 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; } + 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; } + out_cp.* = cp; + ret 4; + } + ret 0; +} + +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; + } + ret true; +} + +fn encode_into(out: *ByteBuffer, cp: u32): Status { + if cp < 0x80 { + ret ByteBuffer.append_byte(out, cp as u8); + } + if cp < 0x800 { + var b0 = (0xC0 | ((cp >> 6) as u8)); + var b1 = (0x80 | ((cp & 0x3F) as u8)); + match ByteBuffer.append_byte(out, b0) { .Ok => {}, .Err(e) => { ret .Err(e); } } + ret ByteBuffer.append_byte(out, b1); + } + if cp >= 0xD800 && cp <= 0xDFFF { ret .Err(make(.InvalidArgument)); } + if cp < 0x10000 { + var b0 = (0xE0 | ((cp >> 12) as u8)); + var b1 = (0x80 | (((cp >> 6) & 0x3F) as u8)); + var b2 = (0x80 | ((cp & 0x3F) as u8)); + match ByteBuffer.append_byte(out, b0) { .Ok => {}, .Err(e) => { ret .Err(e); } } + match ByteBuffer.append_byte(out, b1) { .Ok => {}, .Err(e) => { ret .Err(e); } } + ret ByteBuffer.append_byte(out, b2); + } + if cp <= 0x10FFFF { + var b0 = (0xF0 | ((cp >> 18) as u8)); + var b1 = (0x80 | (((cp >> 12) & 0x3F) as u8)); + var b2 = (0x80 | (((cp >> 6) & 0x3F) as u8)); + var b3 = (0x80 | ((cp & 0x3F) as u8)); + match ByteBuffer.append_byte(out, b0) { .Ok => {}, .Err(e) => { ret .Err(e); } } + match ByteBuffer.append_byte(out, b1) { .Ok => {}, .Err(e) => { ret .Err(e); } } + match ByteBuffer.append_byte(out, b2) { .Ok => {}, .Err(e) => { ret .Err(e); } } + ret ByteBuffer.append_byte(out, b3); + } + ret .Err(make(.InvalidArgument)); +} + +fn count_codepoints(data: *u8, len: usize): usize { + var i: usize = 0; + 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; + } + ret n; +} + +// 码点占用的 UTF-16 code unit 数:BMP 为 1,补充平面为 2。 +fn utf16_units(data: *u8, len: usize): usize { + var i: usize = 0; + var n: usize = 0; + var cp: u32 = 0; + while i < len { + var used = decode(data, len, i, &cp); + if used == 0 { ret n; } + if cp >= 0x10000 { n = n + 2; } else { n = n + 1; } + i = i + used; + } + ret n; +} + +// 字节偏移 -> UTF-16 偏移(夹取到 [0, utf16_units])。 +fn byte_to_utf16(data: *u8, len: usize, byte_off: usize): usize { + var i: usize = 0; + 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; } + if cp >= 0x10000 { n = n + 2; } else { n = n + 1; } + i = i + used; + } + ret n; +} + +// UTF-16 偏移 -> 字节偏移(夹取;落在码点内部时返回该码点起始字节)。 +fn utf16_to_byte(data: *u8, len: usize, utf16_off: usize): usize { + var i: usize = 0; + var n: usize = 0; + 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 units: usize = 1; + if cp >= 0x10000 { units = 2; } + if n + units > utf16_off { ret i; } + n = n + units; + i = i + used; + } + ret i; +} + +// ---- 测试辅助 ---- + +fn expect_ok(result: Status) { + match result { + .Ok => {}, + .Err(error) => { @assert(false, "unexpected utf8 error"); }, + } +} + +fn test_decode_encode_roundtrip() { + var out = ByteBuffer.new(); + expect_ok(encode_into(&out, 0x41)); // 'A' + expect_ok(encode_into(&out, 0xE9)); // é + expect_ok(encode_into(&out, 0x20AC)); // € + expect_ok(encode_into(&out, 0x1F600)); // 😀 + var bytes = ByteBuffer.as_slice(out); + @assert(bytes.len == 1 + 2 + 3 + 4, "encoded length"); + @assert(bytes.ptr[0] == 0x41, "A"); + @assert(bytes.ptr[1] == 0xC3 && bytes.ptr[2] == 0xA9, "e-acute"); + @assert(bytes.ptr[3] == 0xE2 && bytes.ptr[4] == 0x82 && bytes.ptr[5] == 0xAC, "euro"); + @assert(bytes.ptr[6] == 0xF0 && bytes.ptr[9] == 0x80, "emoji head/tail"); + + @assert(valid(bytes.ptr, bytes.len), "valid"); + @assert(count_codepoints(bytes.ptr, bytes.len) == 4, "codepoints"); + @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); +} + +fn test_invalid() { + var bad1: [2]u8 = .[0xC0, 0x80]; // 过长 + var bad2: [3]u8 = .[0xED, 0xA0, 0x80]; // 代理区 + var bad3: [2]u8 = .[0xC3, 0x28]; // 坏续字节 + var bad4: [1]u8 = .[0x80]; // 孤立续字节 + var bad5: [3]u8 = .[0xE2, 0x82]; // 截断 + var s1 = bad1[0..2]; + var s2 = bad2[0..3]; + var s3 = bad3[0..2]; + var s4 = bad4[0..1]; + var s5 = bad5[0..2]; + @assert(!valid(s1.ptr, s1.len), "overlong"); + @assert(!valid(s2.ptr, s2.len), "surrogate"); + @assert(!valid(s3.ptr, s3.len), "bad cont"); + @assert(!valid(s4.ptr, s4.len), "lone cont"); + @assert(!valid(s5.ptr, s5.len), "truncated"); + + var out = ByteBuffer.new(); + match encode_into(&out, 0xD800) { + .Ok => { @assert(false, "surrogate encode"); }, + .Err(error) => {}, + } + match encode_into(&out, 0x110000) { + .Ok => { @assert(false, "out of range encode"); }, + .Err(error) => {}, + } + ByteBuffer.drop(&out); +} + +fn test_offsets() { + var out = ByteBuffer.new(); + 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 + var b = ByteBuffer.as_slice(out); + + @assert(byte_to_utf16(b.ptr, b.len, 0) == 0, "b0"); + @assert(byte_to_utf16(b.ptr, b.len, 1) == 1, "b1"); + @assert(byte_to_utf16(b.ptr, b.len, 5) == 3, "b5"); + @assert(byte_to_utf16(b.ptr, b.len, 6) == 4, "clamp"); + + @assert(utf16_to_byte(b.ptr, b.len, 0) == 0, "u0"); + @assert(utf16_to_byte(b.ptr, b.len, 1) == 1, "u1"); + @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() { + test_decode_encode_roundtrip(); + test_invalid(); + test_offsets(); +}