stage2 更多的工具库
This commit is contained in:
1 parent
4141e7d574
commit
16a1f030d4
15 files changed
+1914
-109
No files matched your search
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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));
|
||||
|
||||
+1
-24
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user