Files
ww/lib/getopt/getopt.ww
Hojun-Cho fda8c2f636 lib: add getopt (Hare's tryparse + error helpers)
Mirrors Hare's getopt subject to current cgen gaps: flat `command`
fields instead of slices, struct-not-tuple `option`, error/help
constructors take out-pointers. The `parse` wrapper plus
printusage/printhelp/printsubcmds are deferred (need fmt.fprintf
with {}-interpolation, which lib/fmt doesn't expose yet). SUBCMD
machinery dropped entirely per Drew — accept-but-inert would have
been a silent-misuse hazard.

Graduates in one go when cgen tasks #4 #5 #6 #7 #9 #10 #11 #14
land. File header names the three surface sweeps that will follow.

Surfaces task #15 (w6c: && doesn't short-circuit, nil-arg deref
in test exposed it).
2026-05-13 16:58:19 +09:00

405 lines
14 KiB
Plaintext

// getopt — POSIX-style short-flag argument parser.
//
// Mirrors Hare's getopts:: at ref/hare/getopt/getopts.ha; drops
// underscores per plan 9 style (parameter_help → paramhelp,
// unknown_option → unknownopt).
//
// Surface divergence from Hare, all forced by current cgen + lib/fmt
// limits:
//
// * [[command]] is a flat struct: instead of `opts: []option` /
// `args: []str` / `help: []help`, it carries three (ptr, len,
// cap) triples — `optsptr/optslen/optscap`, `argsptr/argslen/
// argscap`, `helpptr/helplen/helpcap`. The cstage cgen drops
// chained writes through a slice subfield (`out.opts.len = v`
// stores nothing) and similarly mis-reads chained `cmd.opts.len`,
// so a struct that *contains* slice fields can't round-trip.
// Build a `[]option` locally from the triple when iteration is
// wanted (or index `cmd.optsptr[i]` directly).
// * [[command]] is filled through an out-pointer; Hare's
// `let c = parse(args, help...)` is blocked by the 32B+ return-
// by-value gap (task #5). Same shape as lib/memio.
// * [[help]] is a flat 40B struct with a `kind: helpkind`
// discriminator rather than Hare's
// `(cmdhelp | flaghelp | paramhelp)` tagged union. The Hare
// paramhelp variant alone is 40B, which doesn't fit ww's 32B
// tagged-union return slot. Constructors [[cmdhelp]] /
// [[flaghelp]] / [[paramhelp]] write through an out-pointer for
// 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.
// * [[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
// `!` tag. UNKNOWNSUBCMD is omitted because we don't parse
// subcommands; the remaining 24B fits the tagged-union return
// ABI (tag + 3 words).
// * Hare's subcmd_help variant + recursive subcommand parsing is
// not shipped — no SUBCMD helpkind, no subcmdhelp constructor.
// The recursion needs heap-allocated self-referential types
// (`(str, *command)` payload + `alloc(command)?`) and a real
// consumer to drive the test surface. Add when the first
// consumer is queued.
// * [[parse]], [[printusage]], [[printhelp]], [[printsubcmds]]
// aren't shipped. The print family needs fmt.fprintf-with-{}-
// interpolation that lib/fmt doesn't expose (lib/fmt is print-
// string-only); [[parse]] is a thin wrapper that bundles
// printusage + os.exit on top of [[tryparse]], so it goes too.
// Callers handle errors themselves; [[strerror]] formats a one-
// line summary into a module-level buffer (same shape as
// strconv.*tos).
//
// GRADUATE-IN-ONE-GO WARNING (lib/CLAUDE.md policy). Callers MUST
// NOT bake the current cgen-forced field names into themselves —
// they will collapse to the Hare shape in a single sweep when the
// underlying cgen gaps close, and the old spellings will disappear
// in the same commit (no transition period).
//
// * `cmd.optsptr / cmd.optslen / cmd.optscap` (and `args*` /
// `help*`) graduate to `cmd.opts: []option`, `cmd.args: []str`,
// `cmd.help: []help` once tasks #9 + #10 land. Iterate via the
// slice header you build locally; don't read the triple by name
// from many call sites.
// * `option.flag` / `option.value` graduate to tuple positions
// `cmd.opts[i].0` (rune) / `cmd.opts[i].1` (str) once task #11
// lands. Treat them as opaque positional fields where possible.
// * `strerror(err: *error)` graduates to `strerror(err: error)`
// (by value, no &) once task #14 lands.
//
// Caller layout for `ls -Fa files.txt`:
//
// let helps: [4]getopt.help;
// getopt.cmdhelp(&helps[0], "list files");
// getopt.flaghelp(&helps[1], 'F': rune, "...");
// getopt.flaghelp(&helps[2], 'a': rune, "...");
// getopt.cmdhelp(&helps[3], "files...");
//
// let cmd: getopt.command;
// match (getopt.tryparse(&cmd, args, helps[0:4])) {
// case void => {
// let j: i32 = 0;
// for (j < cmd.optslen) {
// let o: *getopt.option = &cmd.optsptr[j];
// /* o.flag, o.value */
// j += 1;
// };
// };
// case let e: getopt.error => { /* render via strerror */ };
// };
// defer getopt.finish(&cmd);
use strings;
// Direct rt_free / rt_ensure bindings rather than `use os;` — os
// exports read/write/close, which collide with io.read/write/close
// in callers that mix both (task #7). Same pattern as lib/memio.
//
// rt_ensure is the runtime slice-growth helper invoked by the
// `append(s, v)` builtin. We bind it directly because the builtin's
// expansion stores only 8 bytes of the new element (cgen emits a
// single MOVQ), losing the `value: str` half of an [[option]].
// [[appendoption]] grows manually and stores both fields via *option.
@symbol("rt_free") fn rtfree(p: *void, n: u64) void;
@symbol("rt_ensure") fn rtensure(s: *void, membsz: u64) void;
// helpkind — which slot of [[help]] is meaningful. Hare's getopt
// also has a subcmd_help variant; not shipped here (see file header).
export type helpkind = enum i32 {
CMD = 0, // label / arg-name / one-line summary
FLAG = 1, // -X (no argument)
PARAM = 2, // -X <value>
};
// help — one entry in the program's option/help list.
//
// CMD: `text` is the label.
// FLAG: `flag` is the rune; `text` is the help text.
// PARAM: `flag` is the rune; `name` is the value's display name;
// `text` is the help text.
//
// Use [[cmdhelp]] / [[flaghelp]] / [[paramhelp]] to fill help
// entries — direct struct-field assignment via `h[i].field = v`
// is blocked by task #6.
export type help = struct {
kind: helpkind,
flag: rune,
name: str,
text: str,
};
// cmdhelp — fill `*out` as a CMD-kind [[help]] entry (label / arg-
// name). Mirrors Hare's cmd_help.
export fn cmdhelp(out: *help, text: str) void = {
out.kind = helpkind.CMD;
out.flag = 0: rune;
out.name = "";
out.text = text;
};
// flaghelp — fill `*out` as a FLAG-kind [[help]] entry. Mirrors
// Hare's flag_help = (rune, str).
export fn flaghelp(out: *help, r: rune, text: str) void = {
out.kind = helpkind.FLAG;
out.flag = r;
out.name = "";
out.text = text;
};
// paramhelp — fill `*out` as a PARAM-kind [[help]] entry. Mirrors
// Hare's parameter_help = (rune, str, str).
export fn paramhelp(out: *help, r: rune, name: str, text: str) void = {
out.kind = helpkind.PARAM;
out.flag = r;
out.name = name;
out.text = text;
};
// errorkind — which payload of [[error]] carries the offender.
export type errorkind = enum i32 {
REQUIRESARG = 0, // -X needs an argument we didn't get
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.
export type error = struct {
kind: errorkind,
flag: rune, // the offending letter
name: str, // argv[0] (program name)
};
// 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.
export type option = struct {
flag: rune,
value: str,
};
// command — parse result. Caller owns the slot; [[finish]] releases
// the storage tryparse allocated.
//
// Flat (ptr, len, cap) triples replace the natural slice fields
// because the cstage cgen miscompiles both writes and reads of a
// slice subfield through a struct (see file header). Reconstruct a
// `[]option` view locally if iteration is more convenient than
// pointer-indexing `optsptr[i]`.
//
// opts*: every option encountered, in argv order.
// args*: positional arguments after option processing — a borrowed
// view into the caller's argv.
// help*: a borrowed view of the help slice tryparse was called with.
export type command = struct {
optsptr: *option,
optslen: i32,
optscap: i32,
argsptr: *str,
argslen: i32,
argscap: i32,
helpptr: *help,
helplen: i32,
helpcap: i32,
};
// appendoption — grow `*opts` by one slot and store `(flag, value)`.
// Avoids the `append(opts, pair)` builtin: cgen lowers that to a
// MOVQ-of-the-first-8-bytes, which drops the `value: str` half.
fn appendoption(opts: *[]option, flag: rune, value: str) void = {
let newlen: i32 = opts.len + 1;
opts.len = newlen;
rtensure(opts: *void, 24u64);
let dst: *option = &opts.ptr[newlen - 1];
dst.flag = flag;
dst.value = value;
};
// findflag — lookup the kind of `r` in `hs`. Returns void if absent.
//
// Reads go through `&hs[i]` rather than `hs[i].field` directly: the
// cstage cgen emits a bare MOVQ for a slice-indexed struct-field
// read, so `hs[i].kind == K` and `hs[i].flag == r` would compare 8B
// loads (kind packed with flag) against the 4B constants. The
// chained-N_DOT-through-*struct path (cmd/w6c/cgen.c, 3801) uses
// MOVL for i32 fields and avoids the read-too-much. Same pattern
// callers should use when iterating `cmd.optsptr[i]`.
fn findflag(hs: []help, r: rune) (helpkind | void) = {
let i: i32 = 0;
for (i < hs.len) {
let p: *help = &hs[i];
if (p.kind == helpkind.FLAG && p.flag == r) {
return helpkind.FLAG;
};
if (p.kind == helpkind.PARAM && p.flag == r) {
return helpkind.PARAM;
};
i += 1;
};
return;
};
// tryparse — parse `argv` against `help`, filling `*out` on success.
// `argv` must include the program name in argv[0]; matches Hare and
// matches the slice [[os.args]] would hand back.
//
// Recognised forms:
// -X FLAG-kind option
// -X value PARAM-kind option, value from next argv slot
// -Xvalue PARAM-kind option, value glued to flag
// -XYZ FLAG-cluster (each rune looked up independently)
// -- stop processing; everything after is positional
// - positional argument (commonly stdin)
//
// Returns void on success; an [[error]] on the first failure.
//
// `opts` is accumulated in a *local* slice and the (ptr, len, cap)
// triple is copied into `*out.opts*` at the end: `append` on a
// slice that lives inside `*out` would need `&out.foo`, which hits
// the `&x.field` gap (task #4).
export fn tryparse(out: *command, argv: []str, help: []help) (void | error) = {
out.optsptr = nil: *option;
out.optslen = 0;
out.optscap = 0;
out.argsptr = nil: *str;
out.argslen = 0;
out.argscap = 0;
out.helpptr = help.ptr;
out.helplen = help.len;
out.helpcap = help.cap;
let opts: []option;
opts.ptr = nil: *option;
opts.len = 0;
opts.cap = 0;
let i: i32 = 1;
for (i < argv.len) {
let arg: str = argv[i];
if (arg.len == 0) { break; };
if (arg.len == 1) { break; }; // bare "-" → positional
if (arg[0] != 45: u8) { break; }; // not '-' → positional
if (arg.len == 2 && arg[1] == 45: u8) {
// "--" separator: skip and stop.
i += 1;
break;
};
let bi: i32 = 1;
let advanced: bool = false;
for (bi < arg.len) {
if (advanced) { break; };
let r: rune = arg[bi]: rune;
let look: (helpkind | void) = findflag(help, r);
match (look) {
case void => {
if (opts.cap > 0) {
rtfree(opts.ptr: *void,
(opts.cap: u64) * 24u64);
};
let e: error;
e.kind = errorkind.UNKNOWNOPT;
e.flag = r;
e.name = argv[0];
return e;
};
case let k: helpkind => {
if (k == helpkind.FLAG) {
appendoption(&opts, r, "");
bi += 1;
} else {
// PARAM: glued value, or next argv slot.
if (bi + 1 < arg.len) {
let v: str = strings.sub(arg, bi + 1, arg.len);
appendoption(&opts, r, v);
advanced = true;
} else {
if (i + 1 >= argv.len) {
if (opts.cap > 0) {
rtfree(opts.ptr: *void,
(opts.cap: u64) * 24u64);
};
let e: error;
e.kind = errorkind.REQUIRESARG;
e.flag = r;
e.name = argv[0];
return e;
};
i += 1;
appendoption(&opts, r, argv[i]);
advanced = true;
};
};
};
};
};
i += 1;
};
out.optsptr = opts.ptr;
out.optslen = opts.len;
out.optscap = opts.cap;
if (i < argv.len) {
out.argsptr = &argv[i];
out.argslen = argv.len - i;
out.argscap = argv.len - i;
};
return;
};
// finish — release storage owned by `cmd`. Only opts was grown by
// 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.
rtfree(cmd.optsptr: *void, (cmd.optscap: u64) * 24u64);
};
cmd.optsptr = nil: *option;
cmd.optslen = 0;
cmd.optscap = 0;
};
// strerror — render `err` as a one-line message into a module-level
// buffer. The buffer is reused; callers needing the bytes to outlive
// the next strerror call duplicate via strings.dup. Mirrors Hare's
// strerror(error) str shape (and matches strconv's static-buffer
// idiom).
let strerrorbuf: [1024]u8;
fn writebuf(s: str, off: i32) i32 = {
let n: i32 = s.len;
if (off + n > 1024) { n = 1024 - off; };
let i: i32 = 0;
for (i < n) {
strerrorbuf[off + i] = s[i];
i += 1;
};
return off + n;
};
// strerror takes `*error` rather than Hare's value `error`: the cstage
// cgen only passes the first 8 bytes of a >8B struct argument, which
// would truncate the `name` field. Returning a `case let e: error`
// scrutinee through `&e` keeps the call shape uniform.
export fn strerror(err: *error) str = {
let off: i32 = 0;
off = writebuf(err.name, off);
off = writebuf(": ", off);
if (err.kind == errorkind.REQUIRESARG) {
off = writebuf("option -", off);
if (off < 1024) { strerrorbuf[off] = err.flag: u8; off += 1; };
off = writebuf(" requires an argument", off);
};
if (err.kind == errorkind.UNKNOWNOPT) {
off = writebuf("unrecognized option: -", off);
if (off < 1024) { strerrorbuf[off] = err.flag: u8; off += 1; };
};
let r: str;
r.ptr = &strerrorbuf[0];
r.len = off;
return r;
};