The numextra path in wild1() (a '?' immediately after a '*') advanced the data
pointer by one byte per '?' and copied one byte per '?' in both capture-fill
sites, so on multibyte data it split a UTF-8 character across captures, e.g.
wild('*?x','éx') gave %0='\xC3' %1='\xA9' instead of %0='' %1='é'. (The
standalone '?' was already character-aware; only this post-'*' path was not.)
Make the three numextra sites character-oriented: the do-while '?'-skip advances
a whole character (wild_char_len); both capture-fill loops copy a whole character
(new wild_capture_char helper); the trailing '*' span is computed by walking back
numextra whole characters from the anchor (new wild_step_back helper) rather than
dstr - numextra bytes. datapos becomes const (read-only cursor).
wild_test.cpp extended with 8 '*?'-adjacency cases (ASCII + UTF-8); 28/28 pass,
ASCII captures unchanged. Smoke 1254/1254.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
wild1() and wild()'s literal-prefix fast-match folded only ASCII in the data
side (byte-wise EQUAL/NOTEQUAL), so $-command / ^-listen wildcard captures did
not match non-ASCII letters case-insensitively (e.g. a $café * command would
not match CAFÉ ...), even though quick_wild/strmatch already did (#836).
Make the literal comparisons character-oriented via the same wild_lit_eq()
helper quick_wild_impl uses (fold each data char with mux_tolower, XOR-mask
aware, advancing the pattern by the folded length and the data by the original
length). Captures stay original-case: only the literal comparisons fold; the
'*'/'?' capture spans copy from the unmodified data. The scan-forward and retry
advances are now whole-character. EQUAL/NOTEQUAL are removed (no byte-wise
comparisons remain).
Verified with a new standalone unit harness, wild_test.cpp (links the real
wild.eo against libmux, supplies the mudstate/mudconf globals + pool_init, calls
wild()/quick_wild() directly and asserts %0..%9 captures) — muxscript's REPL
drives neither $-commands nor ^-listens and the smoke suite has no capture
coverage, so this is the only test of the capture path. 20 cases (ASCII + UTF-8,
captures + spans); first run against byte-wise wild1 to confirm it caught the
bug, then green after the fix. Smoke 1254/1254.
A pre-existing separate bug remains: wild1's post-'*' trailing-'?' (numextra)
handling is byte-wise and can split a multibyte character into a capture.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The matcher pre-lowercases the pattern with Unicode-aware mux_strlwr, but the
data side folded only with mux_tolower_ascii (identity for non-ASCII), so
case-insensitive matching worked when the uppercase was in the pattern but not
the data: strmatch(café,CAFÉ)=1 yet strmatch(CAFÉ,café)=0.
quick_wild_impl() — behind strmatch/quick_wild/wild_match and thus name/attr/
channel/help/lock matching — now folds each data character on the fly with
mux_tolower() (the same mapping mux_strlwr applies to the pattern) via a new
wild_lit_eq() helper, comparing folded bytes and advancing the pattern by the
folded length and the data by the original length. Handles Latin-1 XOR folds
(apply the mask mux_tolower returns), byte-count-changing table folds, and
1:many folds; '*' is unaffected (UTF-8 self-synchronizes).
The capturing matcher wild1() (hmtBccommand/^-listen captures) still folds only
ASCII in its literals — a documented, separately-tracked follow-up (it needs a
unit harness; muxscript can't drive hmtBccommands/listens to verify captures).
strmatch_fn.mux TC004 added; muxscript -e matrix (21 cases) + smoke 1254/1254.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
quick_wild_impl() — the non-capturing matcher behind strmatch(), quick_wild()
and wild_match() (name/attr/channel/help matching) — advanced the '?' wildcard
by a single byte, while the capturing wild1() advances by a full UTF-8 character
(reading utf8_FirstByte + validating continuation bytes). The two are the same
match differing only in capture, so they must agree; on multibyte data they
didn't, and quick_wild also disagreed with strlen():
strlen(café)=4 but strmatch(café,????)=0 and strmatch(café,?????)=1.
Add a wild_char_len() helper (mirrors wild1's validation) and use it at both
quick_wild_impl '?' sites (the pre-'*' literal loop and the post-'*' wildcard-
skip loop) so '?' consumes a whole character. '*' matching is unaffected (UTF-8
is self-synchronizing and '*' absorbs any byte/char count difference).
strmatch_fn.mux TC003 added; smoke 1253/1253. Verified before/after with
muxscript -e.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add LBuf class to alloc.h: an RAII wrapper around alloc_lbuf/free_lbuf
that moves LBUF_SIZE buffers from the stack to the heap pool. Convert
all 108 non-static UTF8 xxx[LBUF_SIZE] stack arrays across 25 source
files. Static BSS buffers (24) are unchanged.
This eliminates LBUF_SIZE from recursive stack frames, making it safe
to increase LBUF_SIZE without risking stack overflow in the evaluation
pipeline.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>