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

@@ -28,9 +28,10 @@
// the same reason (task #5), and because task #6 (arr[i].field
// = v) blocks the obvious `h[i].kind = ...` callsite spelling.
// * [[option]] is a `struct { flag: rune, value: str }` rather
// than Hare's `(rune, str)` 2-tuple: ww's cgen mis-sizes a
// tuple of (i32, str) as 20B (no 4B pad before the str field).
// The struct shape lays out as 24B as expected.
// than Hare's `(rune, str)` 2-tuple: ww's cgen mis-pads a
// tuple of (i32, str) (no 4B pad before the str field). The
// struct shape lays out as rune (4) + pad (4) + str (24) = 32B
// as expected.
// * [[error]] is a flat 24B struct (kind + flag + name). Hare's
// `!(str, []help, (requiresarg | unknownopt | unknownsubcmd))`
// 3-tuple-with-nested-union doesn't map onto ww's single-payload
@@ -161,8 +162,9 @@ export type errorkind = enum i32 {
UNKNOWNOPT = 1, // -X isn't in the help list
};
// error — parse failure. 24B layout (i32 kind + rune flag + str
// name) so `(void | error)` rides the tagged-union return ABI.
// error — parse failure. 32B (i32 kind + rune flag + str name);
// the 40B `(void | error)` slot exceeds the 24B register cap, so
// it returns via sret/MEMORY, not the register ABI (see #38).
export type error = struct {
kind: errorkind,
flag: rune, // the offending letter
@@ -170,9 +172,9 @@ export type error = struct {
};
// option — one element of the parsed option list. Hare's getopts
// uses a `(rune, str)` 2-tuple; ww's cgen mis-sizes that as 20B
// (no 4B pad before the str field), so we use a named struct — the
// struct layout pads correctly.
// uses a `(rune, str)` 2-tuple; ww's cgen mis-pads that (no 4B pad
// before the str field), so we use a named struct — the struct
// layout pads correctly to rune (4) + pad (4) + str (24) = 32B.
export type option = struct {
flag: rune,
value: str,
@@ -283,7 +285,7 @@ export fn tryparse(out: *command, argv: []str, help: []help) (void | error) = {
case void => {
if (opts.cap > 0) {
os.free(opts.ptr: *void,
(opts.cap: u64) * 24u64);
(opts.cap: u64) * size(option): u64);
};
let e: error;
e.kind = errorkind.UNKNOWNOPT;
@@ -317,7 +319,7 @@ export fn tryparse(out: *command, argv: []str, help: []help) (void | error) = {
if (i + 1 >= argv.len) {
if (opts.cap > 0) {
os.free(opts.ptr: *void,
(opts.cap: u64) * 24u64);
(opts.cap: u64) * size(option): u64);
};
let e: error;
e.kind = errorkind.REQUIRESARG;
@@ -352,8 +354,8 @@ export fn tryparse(out: *command, argv: []str, help: []help) (void | error) = {
// tryparse; args is a borrowed view into argv.
export fn finish(cmd: *command) void = {
if (cmd.optscap > 0) {
// option layout: rune (4) + pad (4) + str (16) = 24B.
os.free(cmd.optsptr: *void, (cmd.optscap: u64) * 24u64);
// option layout: rune (4) + pad (4) + str (24) = 32B.
os.free(cmd.optsptr: *void, (cmd.optscap: u64) * size(option): u64);
};
cmd.optsptr = nil: *option;
cmd.optslen = 0;

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.