stage2 更多的工具库

This commit is contained in:
zzy committed 2026-09-17 22:32:36 +08:00
1 parent 4141e7d574
commit 16a1f030d4
15 files changed
+1914 -109

No files matched your search

+177
View File
@@ -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();
}
+37 -36
View File
@@ -17,23 +17,23 @@ fn is_control(c: u8): bool {
} }
fn is_space(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 { fn is_blank(c: u8): bool {
ret c == 32 || c == 9; ret c == ' ' || c == '\t';
} }
fn is_digit(c: u8): bool { fn is_digit(c: u8): bool {
ret c >= 48 && c <= 57; ret c >= '0' && c <= '9';
} }
fn is_lower(c: u8): bool { fn is_lower(c: u8): bool {
ret c >= 97 && c <= 122; ret c >= 'a' && c <= 'z';
} }
fn is_upper(c: u8): bool { fn is_upper(c: u8): bool {
ret c >= 65 && c <= 90; ret c >= 'A' && c <= 'Z';
} }
fn is_alpha(c: u8): bool { fn is_alpha(c: u8): bool {
@@ -45,15 +45,15 @@ fn is_alnum(c: u8): bool {
} }
fn is_hex_digit(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 { fn is_print(c: u8): bool {
ret c >= 32 && c <= 126; ret c >= ' ' && c <= '~';
} }
fn is_graph(c: u8): bool { fn is_graph(c: u8): bool {
ret c >= 33 && c <= 126; ret c >= '!' && c <= '~';
} }
fn is_punct(c: u8): bool { fn is_punct(c: u8): bool {
@@ -61,12 +61,12 @@ fn is_punct(c: u8): bool {
} }
fn to_lower(c: u8): u8 { fn to_lower(c: u8): u8 {
if is_upper(c) { ret c + 32; } if is_upper(c) { ret c + ('a' - 'A'); }
ret c; ret c;
} }
fn to_upper(c: u8): u8 { fn to_upper(c: u8): u8 {
if is_lower(c) { ret c - 32; } if is_lower(c) { ret c - ('a' - 'A'); }
ret c; ret c;
} }
@@ -78,11 +78,11 @@ fn digit_value(c: u8, radix: u8): DigitResult {
var value: u8 = 0; var value: u8 = 0;
if is_digit(c) { if is_digit(c) {
value = c - 48; value = c - '0';
} else if is_upper(c) { } else if is_upper(c) {
value = c - 65 + 10; value = c - 'A' + 10;
} else if is_lower(c) { } else if is_lower(c) {
value = c - 97 + 10; value = c - 'a' + 10;
} else { } else {
ret .Err(make(.InvalidDigit)); ret .Err(make(.InvalidDigit));
} }
@@ -94,9 +94,9 @@ fn digit_value(c: u8, radix: u8): DigitResult {
// value 必须位于 0..35。uppercase 只影响 10..35 的字母形式。 // value 必须位于 0..35。uppercase 只影响 10..35 的字母形式。
fn digit_char(value: u8, uppercase: bool): DigitResult { fn digit_char(value: u8, uppercase: bool): DigitResult {
if value >= 36 { ret .Err(make(.OutOfRange)); } if value >= 36 { ret .Err(make(.OutOfRange)); }
if value < 10 { ret .Ok(value + 48); } if value < 10 { ret .Ok(value + '0'); }
if uppercase { ret .Ok(value - 10 + 65); } if uppercase { ret .Ok(value - 10 + 'A'); }
ret .Ok(value - 10 + 97); ret .Ok(value - 10 + 'a');
} }
fn expect_digit(result: DigitResult, expected: u8) { fn expect_digit(result: DigitResult, expected: u8) {
@@ -141,27 +141,28 @@ fn test_all_bytes() {
var c = i as u8; var c = i as u8;
@assert(is_ascii(c) == (i <= 127), "ASCII boundary"); @assert(is_ascii(c) == (i <= 127), "ASCII boundary");
@assert(is_control(c) == (i <= 31 || i == 127), "control boundary"); @assert(is_control(c) == (i <= 31 || i == 127), "control boundary");
@assert(is_print(c) == (i >= 32 && i <= 126), "print boundary"); @assert(is_print(c) == (c >= ' ' && c <= '~'), "print boundary");
@assert(is_graph(c) == (i >= 33 && i <= 126), "graph boundary"); @assert(is_graph(c) == (c >= '!' && c <= '~'), "graph boundary");
@assert(is_blank(c) == (i == 9 || i == 32), "blank set"); @assert(is_blank(c) == (c == '\t' || c == ' '), "blank set");
@assert(is_space(c) == (i == 9 || i == 10 || i == 11 || i == 12 || i == 13 || i == 32), @assert(is_space(c) == (c == '\t' || c == '\n' || c == '\x0B' || c == '\x0C' ||
c == '\r' || c == ' '),
"space set"); "space set");
@assert(is_digit(c) == (i >= 48 && i <= 57), "digit set"); @assert(is_digit(c) == (c >= '0' && c <= '9'), "digit set");
@assert(is_upper(c) == (i >= 65 && i <= 90), "upper set"); @assert(is_upper(c) == (c >= 'A' && c <= 'Z'), "upper set");
@assert(is_lower(c) == (i >= 97 && i <= 122), "lower set"); @assert(is_lower(c) == (c >= 'a' && c <= 'z'), "lower set");
@assert(is_hex_digit(c) == ((i >= 48 && i <= 57) || @assert(is_hex_digit(c) == ((c >= '0' && c <= '9') ||
(i >= 65 && i <= 70) || (c >= 'A' && c <= 'F') ||
(i >= 97 && i <= 102)), (c >= 'a' && c <= 'f')),
"hex digit set"); "hex digit set");
@assert(is_alpha(c) == (is_upper(c) || is_lower(c)), "alpha relation"); @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_alnum(c) == (is_alpha(c) || is_digit(c)), "alnum relation");
@assert(is_punct(c) == (is_graph(c) && !is_alnum(c)), "punct relation"); @assert(is_punct(c) == (is_graph(c) && !is_alnum(c)), "punct relation");
if is_upper(c) { 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"); @assert(to_upper(to_lower(c)) == c, "case round trip");
} else if is_lower(c) { } 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"); @assert(to_lower(to_upper(c)) == c, "case round trip");
} else { } else {
@assert(to_lower(c) == c, "lower preserves non-letter"); @assert(to_lower(c) == c, "lower preserves non-letter");
@@ -192,20 +193,20 @@ fn test_digits() {
if value < 10 { if value < 10 {
@assert(lower == upper, "decimal digits ignore case"); @assert(lower == upper, "decimal digits ignore case");
} else { } else {
@assert(lower == upper + 32, "letter digit case distance"); @assert(lower == upper + ('a' - 'A'), "letter digit case distance");
} }
value = value + 1; value = value + 1;
} }
radix = radix + 1; radix = radix + 1;
} }
expect_error(digit_value(48, 0), true); expect_error(digit_value('0', 0), true);
expect_error(digit_value(48, 1), true); expect_error(digit_value('0', 1), true);
expect_error(digit_value(48, 37), true); expect_error(digit_value('0', 37), true);
expect_error(digit_value(47, 10), false); expect_error(digit_value('/', 10), false);
expect_error(digit_value(58, 10), false); expect_error(digit_value(':', 10), false);
expect_error(digit_value(50, 2), false); expect_error(digit_value('2', 2), false);
expect_error(digit_value(103, 16), false); expect_error(digit_value('g', 16), false);
expect_range_error(digit_char(36, false)); expect_range_error(digit_char(36, false));
expect_range_error(digit_char(255, true)); expect_range_error(digit_char(255, true));
} }
+5 -5
View File
@@ -528,8 +528,8 @@ fn parse(out: *BigInt, bytes: []u8, radix: u8): Status {
var i: usize = 0; var i: usize = 0;
var negative = false; var negative = false;
if bytes.ptr[0] == 45 || bytes.ptr[0] == 43 { if bytes.ptr[0] == '-' || bytes.ptr[0] == '+' {
negative = bytes.ptr[0] == 45; negative = bytes.ptr[0] == '-';
i = 1; i = 1;
} }
if i == bytes.len { ret .Err(Error.at(.UnexpectedEnd, i)); } 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; magnitude.negative = false;
if magnitude.len == 0 { if magnitude.len == 0 {
match ByteBuffer.append_byte(&reverse, 48) { match ByteBuffer.append_byte(&reverse, '0') {
.Ok => {}, .Err(error) => { drop(&magnitude); ret .Err(error); }, .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 { if source.negative {
match ByteBuffer.append_byte(&fresh, 45) { match ByteBuffer.append_byte(&fresh, '-') {
.Ok => {}, .Ok => {},
.Err(error) => { .Err(error) => {
drop(&magnitude); ByteBuffer.drop(&reverse); ret .Err(error); drop(&magnitude); ByteBuffer.drop(&reverse); ret .Err(error);
@@ -773,7 +773,7 @@ fn test_parse_format_and_conversion() {
var output = ByteBuffer.new(); var output = ByteBuffer.new();
expect_ok(format(&output, value, 16, false)); expect_ok(format(&output, value, 16, false));
var text = ByteBuffer.as_slice(output); 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"); expect_text_radix(value, 16, true, "-DEADBEEF");
var base36 = zero(); var base36 = zero();
+1 -1
View File
@@ -211,7 +211,7 @@ fn test_append_and_resize() {
@assert(capacity(buffer) >= 1); @assert(capacity(buffer) >= 1);
expect_ok(append(&buffer, "abc")); expect_ok(append(&buffer, "abc"));
expect_ok(append_byte(&buffer, 0)); expect_ok(append_byte(&buffer, '\0'));
expect_ok(append(&buffer, "de")); expect_ok(append(&buffer, "de"));
@assert(buffer.storage.len == 6, "append logical length"); @assert(buffer.storage.len == 6, "append logical length");
var embedded: [6]u8 = .[97, 98, 99, 0, 100, 101]; var embedded: [6]u8 = .[97, 98, 99, 0, 100, 101];
+243
View File
@@ -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();
}
+5 -5
View File
@@ -19,7 +19,7 @@ fn append_u64(out: *ByteBuffer, value: u64, radix: u8, uppercase: bool): Status
var count: usize = 0; var count: usize = 0;
if value == 0 { if value == 0 {
digits[0] = 48; digits[0] = '0';
count = 1; count = 1;
} else { } else {
var v = value; 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 { fn append_i64(out: *ByteBuffer, value: i64, radix: u8, uppercase: bool): Status {
if !valid_radix(radix) { ret .Err(make(.InvalidArgument)); } if !valid_radix(radix) { ret .Err(make(.InvalidArgument)); }
if value < 0 { if value < 0 {
match ByteBuffer.append_byte(out, 45) { // '-' match ByteBuffer.append_byte(out, '-') {
.Ok => {}, .Ok => {},
.Err(error) => { ret .Err(error); }, .Err(error) => { ret .Err(error); },
} }
@@ -221,14 +221,14 @@ fn test_composition_and_growth() {
var i: i32 = 0; var i: i32 = 0;
while i < 200 { while i < 200 {
expect_ok(append_i64(&b, i as i64, 10, false)); 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; i = i + 1;
} }
expect_ok(append_i64(&b, 200, 10, false)); expect_ok(append_i64(&b, 200, 10, false));
@assert(ByteBuffer.size(b) > 0, "growth content"); @assert(ByteBuffer.size(b) > 0, "growth content");
var s = ByteBuffer.as_slice(b); var s = ByteBuffer.as_slice(b);
@assert(s.ptr[0] == 48, "starts with 0"); @assert(s.ptr[0] == '0', "starts with 0");
@assert(s.ptr[1] == 44, "0 then comma"); @assert(s.ptr[1] == ',', "0 then comma");
ByteBuffer.drop(&b); ByteBuffer.drop(&b);
} }
+184
View File
@@ -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();
}
+88
View File
@@ -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();
}
+152
View File
@@ -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");
}
+43 -38
View File
@@ -95,17 +95,24 @@ fn peek(p: *Parser): i32 {
ret p.src.ptr[p.pos] as 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 { 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; p.pos = p.pos + 1;
var start = p.pos; var start = p.pos;
while p.pos < p.src.len { while p.pos < p.src.len {
var c = p.src.ptr[p.pos]; var c = p.src.ptr[p.pos];
if c == 34 { break; } if c == '"' { break; }
if c == 92 { // '\\' if c == '\\' { // '\\'
p.pos = p.pos + 1; p.pos = p.pos + 1;
if p.pos >= p.src.len { ret .Err(at(.UnexpectedEnd, p.pos)); } 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; var k: usize = 0;
while k < 4 { while k < 4 {
p.pos = p.pos + 1; 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 { fn parse_number(p: *Parser): NodeResult {
var start = p.pos; 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]) { if p.pos >= p.src.len || !is_digit(p.src.ptr[p.pos]) {
ret .Err(at(.InvalidDigit, 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; p.pos = p.pos + 1;
} else { } else {
while p.pos < p.src.len && is_digit(p.src.ptr[p.pos]) { while p.pos < p.src.len && is_digit(p.src.ptr[p.pos]) {
p.pos = p.pos + 1; 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; p.pos = p.pos + 1;
if p.pos >= p.src.len || !is_digit(p.src.ptr[p.pos]) { if p.pos >= p.src.len || !is_digit(p.src.ptr[p.pos]) {
ret .Err(at(.InvalidDigit, p.pos)); ret .Err(at(.InvalidDigit, p.pos));
@@ -161,9 +168,9 @@ fn parse_number(p: *Parser): NodeResult {
p.pos = p.pos + 1; 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; 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; p.pos = p.pos + 1;
} }
if p.pos >= p.src.len || !is_digit(p.src.ptr[p.pos]) { 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); var kids = Vec.blank(@sizeof(JsonChild), 8);
skip_ws(p); skip_ws(p);
if peek(p) == 93 { // ']' if at_char(p, ']') { // ']'
p.pos = p.pos + 1; p.pos = p.pos + 1;
var empty = blank_node(.Array); var empty = blank_node(.Array);
empty.children = kids; empty.children = kids;
@@ -211,12 +218,11 @@ fn parse_array(p: *Parser): NodeResult {
} }
skip_ws(p); skip_ws(p);
var c = peek(p); if at_char(p, ',') { // ','
if c == 44 { // ','
p.pos = p.pos + 1; p.pos = p.pos + 1;
continue; continue;
} }
if c == 93 { // ']' if at_char(p, ']') { // ']'
p.pos = p.pos + 1; p.pos = p.pos + 1;
break; break;
} }
@@ -234,7 +240,7 @@ fn parse_object(p: *Parser): NodeResult {
var kids = Vec.blank(@sizeof(JsonChild), 8); var kids = Vec.blank(@sizeof(JsonChild), 8);
skip_ws(p); skip_ws(p);
if peek(p) == 125 { // '}' if at_char(p, '}') { // '}'
p.pos = p.pos + 1; p.pos = p.pos + 1;
var empty = blank_node(.Object); var empty = blank_node(.Object);
empty.children = kids; empty.children = kids;
@@ -250,7 +256,7 @@ fn parse_object(p: *Parser): NodeResult {
} }
skip_ws(p); skip_ws(p);
if peek(p) != 58 { // ':' if !at_char(p, ':') { // ':'
Vec.drop(&kids); Vec.drop(&kids);
ret .Err(at(.InvalidFormat, p.pos)); ret .Err(at(.InvalidFormat, p.pos));
} }
@@ -270,12 +276,11 @@ fn parse_object(p: *Parser): NodeResult {
} }
skip_ws(p); skip_ws(p);
var c = peek(p); if at_char(p, ',') { // ','
if c == 44 { // ','
p.pos = p.pos + 1; p.pos = p.pos + 1;
continue; continue;
} }
if c == 125 { // '}' if at_char(p, '}') { // '}'
p.pos = p.pos + 1; p.pos = p.pos + 1;
break; break;
} }
@@ -294,7 +299,7 @@ fn parse_value(p: *Parser): NodeResult {
if c < 0 { ret .Err(at(.UnexpectedEnd, p.pos)); } if c < 0 { ret .Err(at(.UnexpectedEnd, p.pos)); }
var ch = c as u8; var ch = c as u8;
if ch == 34 { // '"' if ch == '"' { // '"'
var view = ByteView.empty(); var view = ByteView.empty();
match parse_string_raw(p) { match parse_string_raw(p) {
.Ok(v) => { view = v; }, .Ok(v) => { view = v; },
@@ -304,11 +309,11 @@ fn parse_value(p: *Parser): NodeResult {
node.text = view; node.text = view;
ret intern_node(p, node); ret intern_node(p, node);
} }
if ch == 123 { ret parse_object(p); } // '{' if ch == '{' { ret parse_object(p); } // '{'
if ch == 91 { ret parse_array(p); } // '[' if ch == '[' { ret parse_array(p); } // '['
if ch == 116 { ret parse_literal(p, "true", .True); } if ch == 't' { ret parse_literal(p, "true", .True); }
if ch == 102 { ret parse_literal(p, "false", .False); } if ch == 'f' { ret parse_literal(p, "false", .False); }
if ch == 110 { ret parse_literal(p, "null", .Null); } if ch == 'n' { ret parse_literal(p, "null", .Null); }
ret parse_number(p); 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 { fn emit_quoted(out: *ByteBuffer, text: ByteView): Status {
match ByteBuffer.append_byte(out, 34) { // '"' match ByteBuffer.append_byte(out, '"') { // '"'
.Ok => {}, .Ok => {},
.Err(error) => { ret .Err(error); }, .Err(error) => { ret .Err(error); },
} }
@@ -439,11 +444,11 @@ fn emit_quoted(out: *ByteBuffer, text: ByteView): Status {
.Ok => {}, .Ok => {},
.Err(error) => { ret .Err(error); }, .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 { 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 => {}, .Ok => {},
.Err(error) => { ret .Err(error); }, .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, fn emit_children(out: *ByteBuffer, doc: *Json, n: *JsonNode, is_object: bool, indent: usize,
depth: usize): Status { depth: usize): Status {
var open_byte: u8 = 91; // '[' var open_byte: u8 = '['; // '['
var close_byte: u8 = 93; // ']' var close_byte: u8 = ']'; // ']'
if is_object { if is_object {
open_byte = 123; // '{' open_byte = '{'; // '{'
close_byte = 125; // '}' close_byte = '}'; // '}'
} }
match ByteBuffer.append_byte(out, open_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; var i: usize = 0;
while i < count { while i < count {
if i != 0 { if i != 0 {
match ByteBuffer.append_byte(out, 44) { // ',' match ByteBuffer.append_byte(out, ',') { // ','
.Ok => {}, .Ok => {},
.Err(error) => { ret .Err(error); }, .Err(error) => { ret .Err(error); },
} }
@@ -508,12 +513,12 @@ fn emit_children(out: *ByteBuffer, doc: *Json, n: *JsonNode, is_object: bool, in
.Ok => {}, .Ok => {},
.Err(error) => { ret .Err(error); }, .Err(error) => { ret .Err(error); },
} }
match ByteBuffer.append_byte(out, 58) { // ':' match ByteBuffer.append_byte(out, ':') { // ':'
.Ok => {}, .Ok => {},
.Err(error) => { ret .Err(error); }, .Err(error) => { ret .Err(error); },
} }
if indent != 0 { if indent != 0 {
match ByteBuffer.append_byte(out, 32) { // ' ' match ByteBuffer.append_byte(out, ' ') { // ' '
.Ok => {}, .Ok => {},
.Err(error) => { ret .Err(error); }, .Err(error) => { ret .Err(error); },
} }
@@ -746,14 +751,14 @@ fn test_construction_helpers() {
fn test_large_array() { fn test_large_array() {
var out = ByteBuffer.new(); var out = ByteBuffer.new();
expect_ok(ByteBuffer.append_byte(&out, 91)); // '[' expect_ok(ByteBuffer.append_byte(&out, '[')); // '['
var i: usize = 0; var i: usize = 0;
while i < 1000 { while i < 1000 {
if i != 0 { expect_ok(ByteBuffer.append_byte(&out, 44)); } if i != 0 { expect_ok(ByteBuffer.append_byte(&out, ',')); }
expect_ok(ByteBuffer.append_byte(&out, 49)); // '1' expect_ok(ByteBuffer.append_byte(&out, '1')); // '1'
i = i + 1; i = i + 1;
} }
expect_ok(ByteBuffer.append_byte(&out, 93)); // ']' expect_ok(ByteBuffer.append_byte(&out, ']')); // ']'
var text = ByteBuffer.as_slice(out); var text = ByteBuffer.as_slice(out);
var r = parse(ByteView.from_slice(text)); var r = parse(ByteView.from_slice(text));
+1 -24
View File
@@ -6,6 +6,7 @@
type .{ .Error, .Status, .make } = @import("error.spl"); type .{ .Error, .Status, .make } = @import("error.spl");
type .{ .Vec } = @import("vec.spl"); type .{ .Vec } = @import("vec.spl");
type .{ .ByteView } = @import("byte_view.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"); type .{ .copy } = @import("../sys/mem.spl");
// 槽位状态:0=EMPTY,1=OCCUPIED,2=DELETED(墓碑,仅在扩容重哈希时清除)。 // 槽位状态: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) { fn expect_ok(result: Status) {
match result { match result {
.Ok => {}, .Ok => {},
@@ -391,21 +383,6 @@ fn test_take_and_self_put() {
drop(&moved); 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() { fn test_byte_view_keys() {
var m = new(@sizeof(ByteView), @alignof(ByteView), @sizeof(i32), @alignof(i32), var m = new(@sizeof(ByteView), @alignof(ByteView), @sizeof(i32), @alignof(i32),
hash_view, eq_view); hash_view, eq_view);
+231
View File
@@ -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();
}
+258
View File
@@ -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();
}
+238
View File
@@ -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, "copy");
@assert(eq(copy, copy), "self eq");
drop(&copy);
},
.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(&copy, source));
expect_text(&copy, "owned");
expect_ok(append_byte(&source, '!')); // '!'
expect_text(&copy, "owned"); // 独立
var before = ByteBuffer.as_slice(copy.storage).ptr;
expect_ok(clone(&copy, copy));
@assert(ByteBuffer.as_slice(copy.storage).ptr != before, "self clone replaces allocation");
expect_text(&copy, "owned");
var moved = take(&copy);
@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(&copy);
drop(&source);
}
fn main() {
test_construction();
test_compare_and_search();
test_clone_take_and_failures();
}
+251
View File
@@ -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();
}