lib/strings+test: port tokenize family (Hare cross-module re-export)

This commit is contained in:
2026-05-19 15:22:45 +09:00
parent a1d9f36d11
commit b0da6167b8
5 changed files with 429 additions and 0 deletions

View File

@@ -542,3 +542,67 @@ export fn slice(begin: *iterator, end: *iterator) str = {
export fn position(it: *iterator) i32 = {
return it.offs;
};
// tokenizer — re-export of bytes.tokenizer. ref/hare/strings/tokenize.ha:7.
// First cross-module type alias in tree; needs #22's transitive
// alias-chain unwrap (cstage type_chase_named + wwstage
// structlookupchain) to walk struct fields through the chain.
export type tokenizer = bytes.tokenizer;
// tokenize — yield substrings of `s` split on any byte in `delim`.
// Leading / trailing / adjacent delims yield empty tokens. `s` and
// `delim` are borrowed; caller keeps them live for the tokenizer's
// lifetime. ref/hare/strings/tokenize.ha:32. ASCII-only delim
// asserted per Hare lines 35-37: a multibyte rune in delim would
// split on a single continuation byte and yield invalid UTF-8.
export fn tokenize(s: str, delim: str) tokenizer = {
let d: []u8 = toutf8(delim);
let i: i32 = 0;
for (i < d.len) {
os.assert((d[i] & 0x80u8) == 0u8,
"strings.tokenize cannot tokenize on non-ASCII delimiters");
i += 1;
};
return bytes.tokenize(toutf8(s), d...);
};
// rtokenize — reverse-direction counterpart to [[tokenize]]. First
// next_token yields the last token, last yields the first.
// ref/hare/strings/tokenize.ha:44.
export fn rtokenize(s: str, delim: str) tokenizer = {
let d: []u8 = toutf8(delim);
let i: i32 = 0;
for (i < d.len) {
os.assert((d[i] & 0x80u8) == 0u8,
"strings.rtokenize cannot tokenize on non-ASCII delimiters");
i += 1;
};
return bytes.rtokenize(toutf8(s), d...);
};
// next_token — current token, advancing the cursor.
// ref/hare/strings/tokenize.ha:62.
export fn next_token(s: *tokenizer) (str | bytes.done) = {
let b: *bytes.tokenizer = s: *bytes.tokenizer;
match (bytes.next_token(b)) {
case let v: []u8 => return fromutf8_unsafe(v);
case bytes.done => { let d: bytes.done; return d; };
};
};
// peek_token — current token without advancing.
// ref/hare/strings/tokenize.ha:71.
export fn peek_token(s: *tokenizer) (str | bytes.done) = {
let b: *bytes.tokenizer = s: *bytes.tokenizer;
match (bytes.peek_token(b)) {
case let v: []u8 => return fromutf8_unsafe(v);
case bytes.done => { let d: bytes.done; return d; };
};
};
// remaining_tokens — unconsumed portion of the input ahead of the
// cursor. ref/hare/strings/tokenize.ha:79.
export fn remaining_tokens(s: *tokenizer) str = {
let b: *bytes.tokenizer = s: *bytes.tokenizer;
return fromutf8_unsafe(bytes.remaining_tokens(b));
};

View File

@@ -895,6 +895,175 @@ fn streq(a: str, b: str) bool = {
if (!streq(strings.iterstr(&rit), "he")) { fail(); };
};
// ---- tokenize / rtokenize / peek_token / remaining_tokens -----------
// ref/hare/strings/tokenize.ha:110 @test fn tokenize. Row drivers
// mirror lib/bytes/bytestest expect_token / expect_done but compare
// str via streq.
fn expect_str_token(t: *strings.tokenizer, want: str) void = {
match (strings.peek_token(t)) {
case let p: str => { if (!streq(p, want)) { fail(); }; };
case bytes.done => { fail(); };
};
match (strings.next_token(t)) {
case let n: str => { if (!streq(n, want)) { fail(); }; };
case bytes.done => { fail(); };
};
};
fn expect_str_done(t: *strings.tokenizer) void = {
match (strings.peek_token(t)) {
case let p: str => { fail(); };
case bytes.done => void;
};
match (strings.next_token(t)) {
case let n: str => { fail(); };
case bytes.done => void;
};
};
@test fn tokenize_cases() void = {
// Hare vector — single-space delim. ref/hare/strings/tokenize.ha:112.
signalled = 1700;
let t: strings.tokenizer = strings.tokenize(
"Hello world! My name is Harriet.", " ");
expect_str_token(&t, "Hello");
expect_str_token(&t, "world!");
expect_str_token(&t, "My");
expect_str_token(&t, "name");
expect_str_token(&t, "is");
expect_str_token(&t, "Harriet.");
expect_str_done(&t);
// Multi-byte delim set — space + tab.
// ref/hare/strings/tokenize.ha:124.
signalled = 1701;
let t2: strings.tokenizer = strings.tokenize(
"/dev/sda1\t/ ext4 rw,relatime\t0 0", " \t");
expect_str_token(&t2, "/dev/sda1");
expect_str_token(&t2, "/");
expect_str_token(&t2, "ext4");
expect_str_token(&t2, "rw,relatime");
expect_str_token(&t2, "0");
expect_str_token(&t2, "0");
expect_str_done(&t2);
// Consecutive delimiters — empty interior tokens.
// ref/hare/strings/tokenize.ha:136.
signalled = 1702;
let t3: strings.tokenizer = strings.tokenize("hello world", " ");
expect_str_token(&t3, "hello");
expect_str_token(&t3, "");
expect_str_token(&t3, "");
expect_str_token(&t3, "");
expect_str_token(&t3, "world");
expect_str_done(&t3);
// Leading + trailing delimiters yield empty tokens.
// ref/hare/strings/tokenize.ha:147.
signalled = 1703;
let t4: strings.tokenizer = strings.tokenize(" hello world ", " ");
expect_str_token(&t4, "");
expect_str_token(&t4, "hello");
expect_str_token(&t4, "world");
expect_str_token(&t4, "");
expect_str_done(&t4);
// No delim hit — single full token.
signalled = 1704;
let t5: strings.tokenizer = strings.tokenize("abc", " ");
expect_str_token(&t5, "abc");
expect_str_done(&t5);
// Empty input — done immediately.
signalled = 1705;
let t6: strings.tokenizer = strings.tokenize("", " ");
expect_str_done(&t6);
};
@test fn rtokenize_cases() void = {
// Reverse direction — first next_token is the last token.
signalled = 1710;
let t: strings.tokenizer = strings.rtokenize(
"Hello world! My name is Harriet.", " ");
expect_str_token(&t, "Harriet.");
expect_str_token(&t, "is");
expect_str_token(&t, "name");
expect_str_token(&t, "My");
expect_str_token(&t, "world!");
expect_str_token(&t, "Hello");
expect_str_done(&t);
// Multi-byte delim set, reverse direction.
signalled = 1711;
let t2: strings.tokenizer = strings.rtokenize("a b\tc", " \t");
expect_str_token(&t2, "c");
expect_str_token(&t2, "b");
expect_str_token(&t2, "a");
expect_str_done(&t2);
// Empty input — done immediately.
signalled = 1712;
let t3: strings.tokenizer = strings.rtokenize("", " ");
expect_str_done(&t3);
};
@test fn peek_token_cases() void = {
// Two peeks without advancing return the same token.
signalled = 1720;
let t: strings.tokenizer = strings.tokenize("a b c", " ");
match (strings.peek_token(&t)) {
case let p: str => { if (!streq(p, "a")) { fail(); }; };
case bytes.done => { fail(); };
};
match (strings.peek_token(&t)) {
case let p: str => { if (!streq(p, "a")) { fail(); }; };
case bytes.done => { fail(); };
};
// Advance once — peek then returns "b".
signalled = 1721;
match (strings.next_token(&t)) {
case let n: str => { if (!streq(n, "a")) { fail(); }; };
case bytes.done => { fail(); };
};
match (strings.peek_token(&t)) {
case let p: str => { if (!streq(p, "b")) { fail(); }; };
case bytes.done => { fail(); };
};
// Empty input — peek is done.
signalled = 1722;
let t2: strings.tokenizer = strings.tokenize("", " ");
match (strings.peek_token(&t2)) {
case let p: str => { fail(); };
case bytes.done => void;
};
};
@test fn remaining_tokens_cases() void = {
// ref/hare/strings/tokenize.ha:157. After 2 next_tokens, remaining
// is "My name is Harriet.".
signalled = 1730;
let t: strings.tokenizer = strings.tokenize(
"Hello world! My name is Harriet.", " ");
match (strings.next_token(&t)) {
case let n: str => { if (!streq(n, "Hello")) { fail(); }; };
case bytes.done => { fail(); };
};
match (strings.next_token(&t)) {
case let n: str => { if (!streq(n, "world!")) { fail(); }; };
case bytes.done => { fail(); };
};
if (!streq(strings.remaining_tokens(&t), "My name is Harriet.")) {
fail();
};
// Fresh tokenizer — remaining_tokens is the whole input.
signalled = 1731;
let t2: strings.tokenizer = strings.tokenize("a b c", " ");
if (!streq(strings.remaining_tokens(&t2), "a b c")) { fail(); };
};
export fn main() i32 = {
signalled = 1; dup_cases();
signalled = 2; concat_cases();
@@ -926,5 +1095,9 @@ export fn main() i32 = {
signalled = 25; iter_position_cases();
signalled = 26; iter_iterstr_reverse_cases();
signalled = 27; iter_slice_cases();
signalled = 31; tokenize_cases();
signalled = 32; rtokenize_cases();
signalled = 33; peek_token_cases();
signalled = 34; remaining_tokens_cases();
return 0;
};