getopt: free opts at size(option) (32B), not a stale 24B literal (F-E)

option = struct{flag:rune, value:str} is 32B now that str is 24B (Phase 3: cstage type.c:68, wwstage typ.ww:239 -- rune 4 + pad 4 + str 24). The three free sites (tryparse's two error paths + finish) freed (cap)*24u64 -- an 8-byte-per-element under-free of a buffer that append() grew at the real 32B stride, plus a rule-13 hardcoded-size-literal violation. Route all three through size(option).

Also corrects the stale 16B-era layout comments, including the error struct: error is 32B, so its (void|error) return rides sret/MEMORY (the 40B slot exceeds the 24B register cap), not the register ABI -- verified sound at runtime (ken; see #38).

Test (getopttest.ww, test 982_getopt_run): optionsizepin pins size(option)==32 as the regression guard; freeroundtrip is a table over {0,1,3} options exercising tryparse-then-finish. Full make test: 450 green incl. 990-997 byte-id.
This commit is contained in:
2026-06-19 14:24:07 +09:00
parent 801d105c0f
commit e9f64a0727
2 changed files with 73 additions and 12 deletions

View File

@@ -434,6 +434,65 @@ fn streq(a: str, b: str) bool = {
};
};
// ---- optionsizepin: option is 32B, not 24B -----------------------------
//
// tryparse/finish free the parsed-option array at `cap * size(option)`.
// option = rune (4) + pad (4) + str (24, #1/Phase 3) = 32B. A hardcoded
// `24u64` here was an 8-byte-per-element under-free; pinning the size
// keeps the literal from silently drifting back and catches any future
// cgen struct mis-sizing before it corrupts the heap.
@test fn optionsizepin() void = {
assert(!(size(getopt.option) != 32));
};
// ---- freeroundtrip: parse → finish over {0, 1, several} options --------
//
// Exercises the free path at three distinct cap values so the
// `cap * size(option)` free in [[finish]] runs for an empty, single,
// and multi-element option array. The under-free is not directly
// observable through the allocator here, so this guards the parse →
// finish roundtrip doesn't crash/corrupt across the three sizes; the
// exact-size guarantee rides on optionsizepin above.
@test fn freeroundtrip() void = {
let helps: [3]getopt.help;
getopt.cmdhelp(&helps[0], "prog");
getopt.flaghelp(&helps[1], 'v': rune, "verbose");
getopt.flaghelp(&helps[2], 'a': rune, "all");
// row 0: ["p"] → 0 opts (nil opts, finish no-ops)
// row 1: ["p","-v"] → 1 opt (finish frees 1*32)
// row 2: ["p","-vav"] → 3 opts (finish frees 3*32)
let srcs: [5]str;
srcs[0]="p";
srcs[1]="p"; srcs[2]="-v";
srcs[3]="p"; srcs[4]="-vav";
let argo: [3]i32;
let argn: [3]i32;
let wantopts: [3]i32;
argo[0]=0; argn[0]=1; wantopts[0]=0;
argo[1]=1; argn[1]=2; wantopts[1]=1;
argo[2]=3; argn[2]=2; wantopts[2]=3;
let i: i32 = 0;
for (i < 3) {
let argv: []str;
argv.ptr = &srcs[argo[i]];
argv.len = argn[i];
argv.cap = argn[i];
let cmd: getopt.command;
let r: (void | getopt.error) = getopt.tryparse(&cmd, argv, helps[0:3]);
match (r) {
case void => {};
case let e: getopt.error => abort();
};
assert(!(cmd.optslen != wantopts[i]));
getopt.finish(&cmd);
i += 1;
};
};
// ---- printhelp_cases: table of help-output paths ----------------------
//
// ref/hare/getopt/getopts.ha:281-314.