lib/strings+test: graduate concat to Hare str... variadic

concat(a, b: str) -> concat(strs: str...) per ref/hare/strings/concat.ha:5.
Drop nomem return per project no-alloc-error idiom (os.alloc aborts).
Unblocked by #16 variadic-pack store fix (3bd9b1d).

concat_cases rewritten to table-driven: flat pool + argo/argn parallel
arrays + slice-spread call. 9 rows cover Hare concat.ha:18 vectors
(0/1/2/3-arg, multibyte) plus empty-mid/first/last/2-empty edges.
Bisect via signalled = 200 + i.

trim/contains variadic held on task #36 — surfaced by worker-variadic
pre-flight: iter + match prev composition in non-leaf callees still
hits scanlocals offset divergence. Resolves via #15 size-strategy.

make test 121/121; ww2==ww3==ww4 byte-id holds.
This commit is contained in:
2026-05-19 00:40:33 +09:00
parent d985622cb1
commit 5466ea048d
2 changed files with 78 additions and 35 deletions

View File

@@ -3,17 +3,18 @@
// //
// Documented divergences from Hare: // Documented divergences from Hare:
// //
// - `concat(a, b)` is 2-arg. Hare ships `concat(strs: str...)`
// (ref/hare/strings/concat.ha:5). Blocks on task #16 (cstage
// variadic-pack drops .len of multi-field element type). Cite
// reverts on fix.
// - `trim` / `ltrim` / `rtrim` take a single rune. Hare's are // - `trim` / `ltrim` / `rtrim` take a single rune. Hare's are
// `(exclude: rune...)` (ref/hare/strings/trim.ha:54). Same // `(trim: rune...)` (ref/hare/strings/trim.ha:11,32,54). The
// blocker as concat. Hare's no-rune branch (strip whitespace) // port body uses `iter`/`next` + inner match against the pack +
// is also dropped — depends on a rune set. // `prev` step-back; that shape triggers #36 (wwstage scanlocals
// misses match-arm `case let` bindings, slot offsets diverge —
// `.ai/probe_trim_36extra.{ww,diff}`). Hare's no-rune
// strip-whitespace branch additionally needs `lib/bytes`
// variadic graduation.
// - `contains` is non-variadic. Hare's is // - `contains` is non-variadic. Hare's is
// `contains(haystack, needles: (str | rune)...)` // `contains(haystack, needles: (str | rune)...)`
// (ref/hare/strings/contains.ha:9). Same blocker. // (ref/hare/strings/contains.ha:9). Gated on `(str|rune)...`
// tagged-variadic gather + runtime — task #5.
// - `byteindex` / `rbyteindex` rune arms encode via // - `byteindex` / `rbyteindex` rune arms encode via
// `utf8.encoderune`; the legacy impls scanned for `r: u8` (an // `utf8.encoderune`; the legacy impls scanned for `r: u8` (an
// undocumented ASCII-only restriction that silently dropped // undocumented ASCII-only restriction that silently dropped
@@ -107,17 +108,30 @@ export fn freeall(s: []str) void = {
}; };
}; };
// concat — fresh allocation containing `a` then `b`. Caller releases // concat — fresh allocation containing each element of `strs` in
// with `os.free(r.ptr, r.len: u64)`. ref/hare/strings/concat.ha:5 // order. Caller releases with `os.free(r.ptr, r.len: u64)`.
// (subset: Hare's `(strs: str...)` blocks on task #16). // ref/hare/strings/concat.ha:5. Hare's `nomem` return is dropped:
export fn concat(a: str, b: str) str = { // `os.alloc` aborts on OOM.
let total: i32 = a.len + b.len; export fn concat(strs: str...) str = {
let buf: *u8 = os.alloc(total: u64): *u8; let total: i32 = 0;
let i: i32 = 0; let i: i32 = 0;
for (i < a.len) { buf[i] = a[i]; i += 1; }; for (i < strs.len) { total += strs[i].len; i += 1; };
let j: i32 = 0;
for (j < b.len) { buf[a.len + j] = b[j]; j += 1; };
let r: str; let r: str;
r.ptr = nil;
r.len = 0;
if (total == 0) { return r; };
let buf: *u8 = os.alloc(total: u64): *u8;
let off: i32 = 0;
i = 0;
for (i < strs.len) {
let j: i32 = 0;
for (j < strs[i].len) {
buf[off + j] = strs[i][j];
j += 1;
};
off += strs[i].len;
i += 1;
};
r.ptr = buf; r.ptr = buf;
r.len = total; r.len = total;
return r; return r;

View File

@@ -46,28 +46,57 @@ fn streq(a: str, b: str) bool = {
}; };
// ---- concat ----------------------------------------------------------- // ---- concat -----------------------------------------------------------
// ref/hare/strings/concat.ha:18 (2-arg subset). // ref/hare/strings/concat.ha:18. Rows mirror Hare's vectors (0/1/2/3-arg,
// empty-mid, 2-empty) plus empty-first / empty-last / multibyte. The
// per-row `signalled` bump narrows a failure exit code to the offending
// row.
@test fn concat_cases() void = { @test fn concat_cases() void = {
let a: str = strings.concat("hello ", "world"); let pool: [17]str;
if (!streq(a, "hello world")) { fail(); }; pool[0] = "hello";
defer os.free(a.ptr: *void, a.len: u64); pool[1] = "hello ";
pool[2] = "world";
pool[3] = "hello";
pool[4] = " ";
pool[5] = "world";
pool[6] = "hello";
pool[7] = "";
pool[8] = "world";
pool[9] = "";
pool[10] = "";
pool[11] = "";
pool[12] = "world";
pool[13] = "hello";
pool[14] = "";
pool[15] = "こん";
pool[16] = "にちは";
let e: str = strings.concat("", ""); let argo: [9]i32;
if (!streq(e, "")) { fail(); }; let argn: [9]i32;
// e.len == 0 — os.free guarded, skip. let want: [9]str;
let labels: [9]str;
argo[0]=0; argn[0]=0; want[0]=""; labels[0]="0-arg";
argo[1]=0; argn[1]=1; want[1]="hello"; labels[1]="1-arg";
argo[2]=1; argn[2]=2; want[2]="hello world"; labels[2]="2-arg";
argo[3]=3; argn[3]=3; want[3]="hello world"; labels[3]="3-arg";
argo[4]=6; argn[4]=3; want[4]="helloworld"; labels[4]="empty-mid";
argo[5]=9; argn[5]=2; want[5]=""; labels[5]="2-empty";
argo[6]=11; argn[6]=2; want[6]="world"; labels[6]="empty-first";
argo[7]=13; argn[7]=2; want[7]="hello"; labels[7]="empty-last";
argo[8]=15; argn[8]=2; want[8]="こんにちは"; labels[8]="multibyte";
let l: str = strings.concat("", "world"); let i: i32 = 0;
if (!streq(l, "world")) { fail(); }; for (i < 9) {
defer os.free(l.ptr: *void, l.len: u64); signalled = 200 + i;
let argv: []str;
let r: str = strings.concat("hello", ""); argv.ptr = &pool[argo[i]];
if (!streq(r, "hello")) { fail(); }; argv.len = argn[i];
defer os.free(r.ptr: *void, r.len: u64); argv.cap = argn[i];
let got: str = strings.concat(argv...);
let m: str = strings.concat("こん", "にちは"); if (!streq(got, want[i])) { fail(); };
if (!streq(m, "こんにちは")) { fail(); }; if (got.len > 0) { os.free(got.ptr: *void, got.len: u64); };
defer os.free(m.ptr: *void, m.len: u64); i += 1;
};
}; };
// ---- hasprefix -------------------------------------------------------- // ---- hasprefix --------------------------------------------------------