8382 -> 8004 B of flash, 725 -> 511 B of RAM on a part that has 2048.
The command names were a std::to_array of string_view, and on a Harvard
machine that is the worst of both: the characters land in .data and so does
the table's own pointer-and-length pair for each of them, so the firmware
carried 216 B of RAM for thirteen words that never change - and paid for them
in flash too, since .data is copied out of an initialiser image at startup.
They are one NUL-separated blob in flash now, walked with lpm. Separators
rather than an offset table, because an offset table is the RAM this exists to
give back; the names sit in it in match order, so the walk that finds a name
is the same walk that compares it and measures it. Flash falls further than
RAM does: the initialiser image and the two tables were 216 B of it, and the
blob is 87.
The header said the names "cannot" be in flash because they are matched at run
time. Being matched at run time is not a reason to be in RAM on a machine with
two address spaces - only being *written* is, and nothing writes these.
Two smaller things came with it. `reset`'s exact-match rule was a bool on
every entry to protect one; it is an index found by searching the list, so
reordering the commands cannot move the protection onto a different one. And
`version` was the last string_view left, holding its own characters and a
pointer to them.
The matching is now pinned rather than assumed: lookup() is constexpr and the
battery asserts the load-bearing order the README documents - `s` is show and
not statistics, `st` is statistics, no abbreviation of `reset` resolves, and
`helpful` is not `help`. Red-checked by claiming `s` is statistics.
Both modes byte-identical, ten tests green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>