diff --git a/lib/getopt/getopt.ww b/lib/getopt/getopt.ww index 45406ed0..24d61f5d 100644 --- a/lib/getopt/getopt.ww +++ b/lib/getopt/getopt.ww @@ -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; diff --git a/lib/getopt/getopttest.ww b/lib/getopt/getopttest.ww index f9250185..49a9f0e9 100644 --- a/lib/getopt/getopttest.ww +++ b/lib/getopt/getopttest.ww @@ -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.