The Windows build took PCRE2 from a hand-built out-of-tree directory
reached through $(Pcre2Dir). engine.vcxproj honoured that property for
its headers but hardcoded ..\..\src\pcre2\{Release,Debug} for its
libraries, so -p:Pcre2Dir= applied to one half and not the other and the
link failed with LNK1104 in any tree where PCRE2 was not at the default.
Declare pcre2 in mux/vcpkg.json instead, alongside grpc and
nlohmann-json, and point netmux, libmux and engine at $(VcpkgDir). All
three now read include and library paths from the same place. Note
vcpkg's layout is lib/ for release and debug/lib/ for debug, not
Release/ and Debug/.
PCRE2's JIT is required -- funceval2.cpp calls pcre2_jit_compile -- and
comes automatically: the port's platform-default-features pulls in jit
everywhere except emscripten and iOS.
Ship mux/vcpkg.json in both source TOCs. It was in neither, so the
source distribution carried no statement of its own dependencies; with
PCRE2 moving to vcpkg, the file that declares it has to travel with the
source or an unpacking reader is worse off than before.
Unix is unaffected -- configure finds system PCRE2 via pkg-config.
Verified on hatsuhara: Release and Debug both build clean with no
hand-built PCRE2 present anywhere on the box.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- engine_com: drop the two const_casts into now-const APIs
(lookup_player, select_channel) — harmless re-const the campaign made
unnecessary; the remaining casts there target still-mutable APIs
(load/save_comsys, connect_player) and stay.
- campaign brief: the merge-gate paragraph was written as an instruction
and had become history — rewritten to record the closed gate and keep
the pointer to landing Kagura's 35-shape probe in-tree.
Smoke: ALL 1661 PASSED on jit=yes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both ECALL paths (builtins and ufuns) now hand guest memory to callees
directly: ecall_arg_or_copy, guest_addr_outlives_ecall's ptr < CARGS_BASE
predicate, and jit_internal_abi_fn are gone, and the fargs arrays are
const UTF8 * (which also removed the mux_exec cast on the ufun path).
The compiled route regains its advantage of not materializing arguments,
per-ECALL LBuf allocation included.
Safe because FUNCTION bodies take const UTF8 * const fargs[] with zero
const_casts, and the mutating command handlers receive FargCopy/FargVec
copies from their wrappers. run_cached_program's fragile-predicate
caveat (#2140/#2139 review) now cites the contract instead of the copy.
Acceptance: smoke TC013 — the #2128 reproduction, asserted 3/3/3 — plus
the full suite (35 passed / 0 failed, jit=yes) and the sidefx_fargs.py
live probes, all green on macOS arm64. Merge remains gated on
make test-poison on a Linux box (see the campaign brief).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The flip: FUNCTION/XFUNCTION/FUN::fun/delim_check and the module
interfaces take `const UTF8 * const fargs[]`. Double-const is
load-bearing: C++ qualification conversion needs const at both pointer
levels, so builder-side `UTF8 *[]` arrays convert implicitly — the
evaluator, the JIT marshaller, and every owner site need zero casts,
and slot reassignment inside bodies becomes a compile error for free.
The conversions: the flip landed first so the compiler enumerated every
violation; this commit is that inventory worked to zero — ~250 sites
across funceval, funceval2, functions, funmath, help, mail, session,
powers, levels, predicates, conf, walkdb, stringutil, timeutil/
date_scan (regenerated, one-line diff), exp3, and mux_main, each
classified per docs/campaign-2136-const-fargs.md's four recipes.
New idioms (functions.h): trim_space_sep_n() — non-destructive trim for
(pointer, length) consumers, so trim-then-scan sites need no copy at
all; FargVec — the argv counterpart of FargCopy for CS_ARGV handlers.
countwords() and DecodeListOfIntegers() rewritten non-destructive.
The flip deleted more than it added: #2157's fun_munge list1 copy, the
engine_com help-topic copy, fun_index's in-place NUL write, and five
const_casts (process_sex x4, sha1_helper). const_cast budget: zero
added.
Trap recorded in the brief: an old-signature definition doesn't fail
the build — it becomes a C++ overload, and the new-signature symbol
stays undefined until dlopen(RTLD_NOW). delim_check, the conn_bridge
bridges, the dbt_spike stub, and exp3::Call were all silently shadowed;
muxscript was the only host that noticed, because netmux's own net.cpp
resolved the flat-namespace lookup. After any signature flip, grep the
old spelling.
Verified: make test EXPECT_CONFIG="jit=yes" (35 passed / 0 failed) and
make test-scenario, including the new tests/scenario/sidefx_fargs.py
that live-probes the class-3 wrappers smoke never touches (pemit/
trigger/link/tel/wipe/destroy).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Header tracking only works when every .eo/.lo has a -MMD side-car.
An orphan object is invisible to make, so a header edit under-rebuilds
and the binary lies coherently (#2107 was exactly that). Self-heal at
parse time in engine and libmux: delete orphans, warn once, recompile.
No new check target, stamp, or CI gate.
The release-justifying number was listed as unmeasured since this survey was
written. Measured.
tests/profile/ works cross-version because it measures from OUTSIDE the server
-- commands over a socket, CPU from /proc/<pid>/stat -- so it needs none of
rvbench(), benchmark() or jitstats(), which do not exist in 2.13. run.sh gains
--bin/--db/--label to point it at any build; verified against 2.14 itself
before being trusted against 2.13.
Same box, same harness, 375 objects, two runs each, both lines logging-gated:
phase 2.13 2.14 2.14 is
create 507/507us 373/427us 1.27x
attrwrite 227/211us 71/69us 3.13x
attrread 142/138us 80/84us 1.71x
dispatch 100/92us 34/42us 2.53x
softcode_arith 30/26us 28/28us 1.00x
softcode_list 90/95us 65/55us 1.54x
2.14 wins every phase, and the gains are in storage and the command path.
THREE things the document now says about how to read that:
* Equalising the logging was not optional. 2.13 has the same ungated
#ifndef NDEBUG gate, 375 sites (#2089, PR #2090): 158,660 debug lines
against 2.14's 14. Comparing as-shipped would have credited 2.14 with 2-3x
for a reason unrelated to any 2.14 work.
* softcode_arith being level is NOT a verdict on the JIT. That phase
evaluates one expression per command against a 34-42us per-command floor,
so a 10x win on the expression moves it by single-digit percent -- inside
the spread. The phase cannot resolve the JIT either way.
* The JIT's target workload is not represented here at all. It is not aimed
at the database, the network or dispatch -- it is aimed at large softcode
and Lua bodies, which do not exist in the smoke corpus, in tests/profile,
in the starter database, or largely in existing games, which were written
against an interpreter and shaped by what was affordable under one.
Measuring it against workloads written for the old cost model measures the
old cost model.
The headline gains the same qualifier, since it is the line most likely to be
quoted alone.
Two PRs each appended an item '9' to the gotchas list -- #2069's 'to attribute
a cost, remove it' and the benchmark-field mislabelling note. Renumber the
second to 10; content untouched.
The live-server numbers in this document were taken before #2073, #2076,
#2077/#2084, #2078 and #2079 landed, and people are choosing work from it.
Re-measured on current master.
What moved, and what did not:
* softcode_list 82us -> 61us, the only phase to change (the iter() work).
* attrread and attrwrite unchanged -- and #2084 not helping attrread is
CORRECT, since get() fetches one attribute directly and never enumerates.
That cache serves $-matching and lattr(), not value fetches.
* $-dispatch at 200 objects: 3430us -> 1580us (#2073) -> 290us (#2084).
Per object 16.5us -> 1.1us.
The ranking has inverted as a result. With storage out of the attribute path,
the two attribute phases are now the most expensive in the harness by 5x, and
both are dominated by name resolution -- 66.8% of attrread, 74.8% of attrwrite.
Records two things the pass got wrong and corrected:
* The DFA. Twice the "obvious" fix for $-dispatch was named before it was
profiled, and twice the profile pointed elsewhere -- first a whole-cache
scan, then a per-object SQLite query. wild() + wild_lit_eq() have never
risen above 0.48% of that path. Retired explicitly so it is not
re-proposed.
* "Storage is not the big number" was narrower than written. It held for
cache_get and not for atr_head: the attribute LIST went to SQLite on every
enumeration, which is what made $-dispatch 40.6% sqlite3.c until #2084.
Also records the match_list finding: it returns early only on a dbref, never
on a name hit, because ambiguity cannot be detected by stopping at the first
match. So lookup cost barely depends on position (1.5x head to tail over 1000
objects) and there is no cheap early-exit to add. A hypothesis that the
original 20us figure was best-case was tested and did not hold.
The survey named cache_collect_pending_attrnums as the root cause on the
strength of a profile (58.8% of user time) and a code read. That is
correlation. This records the causation experiment.
The scan was bypassed behind a throwaway env knob -- deliberately incorrect,
since it drops dirty/tombstoned entries SQLite has not seen -- so its cost
could be attributed. Same build, same probe, one variable:
objects scan active bypassed us/object active us/object bypassed
25 380 us 290 us 10.0 6.8
50 680 us 470 us 11.0 7.0
100 1380 us 810 us 12.5 6.9
200 3430 us 1510 us 16.5 6.9
The finding is the last column, not the 56% saving. With the scan gone,
per-object cost is FLAT. $-dispatch is inherently linear in objects-in-scope,
and this function is the ENTIRE superlinear term rather than a large constant
on top of it -- which a profile alone could not have distinguished.
Also sharpens the two consequences. Enlarging the attribute cache still makes
things worse. And a pattern automaton addresses the residual ~6.9us/object,
which is worth attacking on its own (1.4ms at 200 objects) but is linear and is
not what made the curve bend.
Adds the general lesson to the methodology section: to attribute a cost,
remove it -- and revert before measuring anything else, because an
experimental artifact left installed poisons every later number.
* docs: record the 2026-08 performance pass (#2046)
Measurements, not proposals. The pass produced a lot of numbers across two
workloads that disagree with each other, and the disagreement is the point --
a single blended percentage answers a question nobody asked.
The finding worth writing down: for a live game none of the top costs are in
the JIT. $-command dispatch is the largest, and its root cause is that
cache_collect_pending_attrnums (attrcache.cpp:307) iterates the ENTIRE
attribute LRU cache and filters for one object, once per object in scope, per
typed command. That makes the per-command cost (objects in scope) x O(total
cached attributes), which is the observed O(n^1.4), and it means enlarging the
attribute cache makes $-dispatch SLOWER rather than faster. It also means a
combined pattern automaton would optimise a term that is not the bottleneck.
Also records: name resolution at ~66% of the attribute path with a measured
slope; storage no longer being the big number and why; the suite being ~57%
compilation against 5.5% execution, and why tier2_install's 13.4% is worth
about nothing to a live game.
The methodology section is deliberately long. Every entry in it cost real time
this pass, and two of them (attribute writes needing a name-resolvable object,
and a positive control that never read its socket) produced confident WRONG
results that looked like findings rather than like errors.
Closes nothing. #2052, #2057, #2058 and #2061 carry the individual items.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: correct the iter() finding — only the JIT is quadratic (#2052)
The survey recorded iter() as quadratic on BOTH the interpreter and the JIT,
and drew a differential-testing lesson from it. Measured with the growth
harness (#2059), the interpreter is linear:
interpreter astbench ast= b=0.99 2.06x 1.96x 1.96x
JIT rvbench cached= b=1.99 3.96x 3.95x 4.01x
fun_iter advances a real cursor via split_token. The JIT's lowering calls
EXTRACT(list, i, 1, delim) per element because the cursor-based SPLIT_TOKEN
fast path hir_lower.cpp:2310 tests for was never registered in s_tier2_map
and has never executed.
The cause of the error is worth keeping, because it is reusable: rvbench's
`native=` calls mux_exec, which dispatches to the JIT for anything
JIT-eligible. So `native=` and `cached=` were two measurements of the JIT
differing only by dispatch overhead -- which is also what the "flat 26-35us
saving" was, rather than a decaying JIT benefit. The tell is that `native=`
BEAT the interpreter on iter(lnum(50),mul(##,2)): 18.9us against 31.9us.
So the differential-testing note inverts too. The routes do not agree -- they
differ by a complexity class, which a differential SCALING test would find at
once. What jit_diff cannot see is cost at all: it compares outputs, and both
routes produce the same list. The #1319/#1320 parallel does not hold; that was
a fault genuinely shared by two implementations.
Same instrument underlies the headline's "2.4-9.5x faster than the
interpreter", so that is re-derived against a real interpreter measurement
(6.1x / 1.26x / 1.42x) and, more importantly, labelled as still not answering
the live-game question -- mux_exec with the JIT against mux_exec without it,
which needs a non-JIT build. Added to "what this pass did not measure".
Added as gotcha 9, next to the others that cost real time.
The document's central finding is untouched: for a live game the top costs are
$-command dispatch and name resolution, not the JIT.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Import the retired FTP contrib ansify as a Ragel -G2 scanner under
tools/ansify, matching muxescape/color_ops. Check in generated ansify.c
so a normal `make` / `make test` only needs a C compiler; use `make regen`
when ansify.rl changes. Binary stays gitignored.
Document under AGENTS.md and docs/generated-files.md.
#1800: Keep DS_NEED_PROTO while bytes are a proper prefix of "GET ";
buffer and replay into WS or telnet once classified; grace timeout
replays any partial preface.
#1802: Cap SMTP read reassembly (64 KiB); 120s overall deadline; surface
queue/flush failures so the singleton channel cannot wedge forever.
#1803: If any listen port was configured but none started, fail
initialize() (clean teardown) instead of running a dead network service.
#1801 (Win32 adoptConnection for @email) remains open — needs engine
initiateConnect on wselect/IOCP.
Required driver→engine CreateInstance results (Log, Platform, GameEngine,
Notify, ObjectInfo, PlayerSession) now abort before signals/listeners with
a stderr diagnostic and release of any partial acquisition. Stops null
deref on Log failure, login-path crash on PlayerSession failure, and
silent Notify/ObjectInfo drop after a broken start.
Beef residual coverage at the softcode JIT / Lua JIT boundary: production
nest shapes, host setq/attr survival, multi-entry lua(), mux.eval effects,
and float/nil/bool marshal consumers. Expand tests/luajit NESTED under
brackets-on and add STATE e4 so mux.eval cannot re-run silently.
Record #1791 softcode post-entry on D1, note #1788/#1790 client portfolio
done, and close the Pass 14 rotation. Parallel docs-only PRs (#1777,
#1781–#1786) are obsolete against master map state.
Same unbounded line/SB/Hydra reassembly as console/iOS. Cap text at 64 KiB
and telnet SB at 4 KiB; cap web grpc-web frame reassembly. Stamp J1–J6 on
the audit map.
Softcode CALL_FUNC is already total (guest error strings, never
ECALL_DECLINE). The residual poison was mid-run DBT failure (code buffer
full, harvest miss) returning false → full AST re-run after effects.
Track eval_ctx::host_ecalls on softcode host ECALLs; after entry, commit
#-1 JIT POST-ENTRY FAIL instead of re-running. Document the audit in
plan-softcode-post-entry-contract.md and cross-link the Lua plan.
Hostile servers could grow Titan iOS and console heap without bound via
streams without newlines or oversized IAC SB. Cap text lines at 64 KiB
and telnet SB at 4 KiB (server-aligned); discard overflowed SB on SE.
Stamp J1/J5 Pass 14 on the audit map.
#1764 shape 2: default STRING-claim calls no longer marshal to TY_STRING
text immediately. ECALL_LUA_CALL_VAL leaves the first pcall result on the
Lua stack and returns a TY_LUA_HANDLE. Softcode boundary marshals via
ECALL_LUA_MARSHAL (fun_lua rules). TEST/NOT use ECALL_LUA_TOBOOL so
tostring(0)/"" / string.find stay truthy under Lua semantics.
CALL_INT / CALL_VOID unchanged. Equality of a value handle to a string
constant still declines (needs VM compare).
AGREE_DECLINE 16→12: the four #1764 if/not pins now execute.
Range-check the prefix as int64 before narrowing to int so strings like
/4294967296 no longer wrap to /0 (whole IPv4). Free base/mask on the
out-of-range path. Expand tests/netaddr; stamp A7 Pass 14 on the audit map.
The EQK nil fix is right, and its lesson generalizes further than it was
applied. Lua's == is false across TYPES, and HIR erases every
distinction that decides it: false and 0 are the same ICONST, nil and ""
the same empty SCONST, and the numeric path coerces "5" to 5. Measured
on the PR as submitted -- all executing, all silently wrong:
local a=0 if a == false jit "y" interp "n"
local a=1 if a == true jit "y" interp "n"
local a=false if a == 0 jit "y" interp "n"
local a="5" if a == 5 jit "y" interp "n"
local a=5 if a == "5" jit "y" interp "n"
Generalizes the author's nil branch into a type-class test
(lua_type_class_of_value / _of_const, from the truth tag plus the HIR
type): mismatched classes are constant false whatever the
representations say; nil == nil stays true.
EQI needed the same treatment and is now its own case rather than
sharing CMP_RI: `a == 0` lowers to EQI and `a == ""` to EQK, so fixing
either alone leaves the other wrong -- the opcode-twin trap this PR's
own doc note names, one level deeper. Order comparisons keep declining
instead: Lua RAISES on mismatched types there, so answering false would
be its own wrong answer.
Five AGREE pins across both opcode forms; all EXECUTE and answer like
the interpreter. Plan gains the rule as item 5.
Also records the softcode no-post-entry-decline assumption that #1767's
exactly-once argument rests on -- derived today from the #1002 pre-entry
watermarks plus Phase 4 leaving one defensive ECALL_DECLINE that nothing
emits into, and therefore worth writing where it can be checked.
make test exit 0; smoke 1561/0; luajit PASSED.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#1766's consumer audit covered CMP_RR (return t[2]=="") but not EQK
(if t[2]=="" then). nil is the empty SCONST in HIR; EQK's STRCMP path
made nil == "" true compiled while the interpreter answers false.
Resolve EQK against the pool constant type and the lhs NIL tag before
STRCMP: nil==nil is true, nil vs anything else (including "") is false.
Tag pool TNIL/TFALSE/TTRUE on LOADK/LOADKX and on EQK's constant side.
Also refuse GTI/GEI on nil (order of nil).
Three EXEC-friendly AGREE pins for the if-EQK shapes. Document the
"new value class ⇒ consumer audit" lesson (including opcode twins) in
the post-entry plan.
Phase 4 deleted silent re-run, so #1750's effect-free corridor is obsolete:
bridge notify/pemit/set/eval run on both routes under the same permissions.
Remove EFFECT_REFUSED and compile-time effectful ineligibility.
STATE e2 delivers PING once on both legs. POST_ENTRY_LOUD 1→0;
AGREE_DECLINE 10→8 (mux.eval executes).
Docs: rewrite plan-lua-post-entry-contract as campaign-complete + residual
design goals; product plan points residual quality at §3.
File plan-lua-post-entry-contract.md as the multi-agent source of truth
for moving Lua from silent decline-then-rerun to softcode's eligibility
decides / run commits model. Point plan-lua-jit-product.md at it so
default-on is not read as architecture done.
Default-on (#1745) exposed that the compiled path baked the back-edge
budget into each program as a constant:
int budget_init = h.emit(HIR_ICONST, TY_INT, -1, -1,
static_cast<int64_t>(mudconf.lua_instruction_limit));
A compiled program is cached in memory and persisted in code_cache, so the
limit in force was whatever was configured when the chunk happened to
compile -- @admin lua_instruction_limit reported Set. and changed nothing,
which is #1613's bug arriving on the compiled path. test-config's
runtime-bounds case caught it the moment the flip put the compiled path in
its way; on default-configure trees --enable-jit is off and everything
stayed green, which is why the flip validated cleanly elsewhere.
## Fix
A dedicated no-arg ECALL, following the LUA_LEN pattern end to end:
HIR_LUA_INSN_BUDGET -> ECALL_LUA_INSN_BUDGET (0x314)
-> a0 = mudconf.lua_instruction_limit, read per run
The entry seed becomes ECALL + STORE_Q, so the program carries no config
value at all. That fixes the in-memory cache and makes the persisted
code_cache safe by construction rather than by flush discipline; blobs from
before this change carry the old entry-store, and JIT_BUILD_STAMP
(__DATE__ __TIME__) already invalidates them on rebuild.
The handler clamps the limit to >= 1: a zero or negative limit must abort
loops, not arm an effectively unbounded unsigned countdown.
The op is listed in needs_int_reg() -- whose own comment documents that
omitting an int-producing ECALL fails silently (codegen emits nothing and
the consumer reads garbage), which is the trap this listing avoids.
## Verified
FAIL: lowering lua_instruction_limit at runtime had no effect before
ok: lua limits apply at runtime, both directions, and read back after
## Second layer found under this one: #1748
Full smoke under default-on now completes and fails exactly two cases
(TC013 mux.name, TC014 mux.eval -- compiled path answers wrongly instead of
declining). On the UNFIXED merge commit those cases are unreachable: the
smoke chain stalls at 164/320 dispatched with 800+ lost verdicts. So this
fix converts a catastrophic stall into two known failures, filed as #1748
with the pre/post evidence. make test on JIT trees stays red at those two
until #1748 resolves; lua_jit 0 remains 1561/1561.
Also amends plan-lua-jit-product.md, replacing "residual optional polish
only" with the post-flip regression record -- as promised in the #1747
review.
Refs #1325, #1613, #1732, #1747, #1748.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Update the living product plan and audit D4 for #1745/#1325 default-on
and #1326 nesting; leave bring-up as a historical checklist with a
status stamp so agents stop treating default-off as current truth.
Translator README still said mux_vsnprintf had no positional args; the
runtime and run_ko.sh case 4 already require %N$. Refresh plan §4/§6.5/§6.6
for completed formatting and tabular-grid work (#1667/#1648 closed).
Convert engine @mail list/review summary lines from tprintf to
mux_table display-column layout (From width 16, Sub trunc 25), matching
the module helpers. Extract format_mail_list_line_at on both paths.
Re-bless comsys_conformance for From field padding. B4 multi-line
read/detail forms remain out of grid scope.
Regenerate pot/xx/ko after Phase 4 C* (drop blob headers; add per-label
msgids). Row right-just must use RightJustifyNumber — mux_vsnprintf does
not implement %* width (#1429), so the filed %*d/%*lld path would echo
literally and drop the rest of each stats line.
Finish the ownership phase: record an explicit exit checklist and the
libmux placement constraints Phase 3 must honor (module-safe header,
co_copy_field base, dual callers, no gettext inside layout). Still no
table conversion and no layout implementation.
Record Phase 2 for NLS-safe multi-column tables without converting any table:
- Product ownership is already decided on #1614 (modules become 2.14 default;
built-ins remain fallback until directives land last).
- Layout ownership is libmux (option A): one emit API on co_copy_field for
modules, engine fallback, and engine-only staff tables (inventory C*).
- Reject module-only conversion (partial while dual life continues) and
engine-only layout (modules cannot link engine.so).
Unblocks Phase 3 (layout API design/impl in libmux only). Phase 4 still
requires dual families to convert both sides in one change. No code migration.
Add inventory-tabular-notifies.md: dual comsys/mail grids (A/B), staff
engine-only tables (C), pot blob-header list, cell-primitive map, and a
suggested conversion order. No code migration.
Point design-tabular-notifies.md and plan-server-i18n.md §6.6 at the
inventory; mark Phase 1 exit criteria complete. Next gate remains Phase 2
ownership (#1614 or layout in libmux).
Record the binary design for pre-spaced table headers (#1648) and the
composition half of the column-layout work (#1649):
- docs/design-tabular-notifies.md — full design, phases, inventory starter,
dual-impl gate (#1614), translator policy
- docs/plan-server-i18n.md §6.6 + risk row — pointer under the NLS plan
No code migration. Implementation waits on inventory and ownership (epic #1667).
#1650 closed @cwho via UnparseObject; #1655/#1657 aligned mail stats and
the fstats phantom +1. The open-divergences table and the harness PASS
line still claimed those rows. Baseline is 19 lines (#1637 From/To plus
debug/lifecycle labels).
Closes the command-output gaps #1640 listed for items 1, 3 and 4, plus
padding and speech comtitle that the same comparison found.
1. @clist/full — honour CLIST_FULL in the module (do_chanlist hands the
key through before checking the switch). Columns match do_listchannels.
3. comtitle — channel_speaker_name() for join/leave and ordinary speech,
matching BuildChannelMessage (SPOOF replaces; otherwise title + name).
4. delcom — disconnect before the leave broadcast (bConnected gates
delivery), personal "You have left channel" confirmation, suppress
Hidden; mechanism-matched so busy-channel broadcast still reaches others.
Also pad @clist description to 45/column 79.
Item 2 (@cwho dbref+flags) needs Examinable/UnparseObject on a module
interface and an IID bump — deferred.
Verification is tests/comsys_conformance, not a separate harness: the
baseline re-blessed 39 → 29 divergent lines as the three fixes landed.
#1643 fixed the engine fstats phantom byte (27 → 26 characters). The
whole-output harness fails when a known divergence vanishes, so the
baseline is regenerated and the #1639 row is dropped from the docs.
Also correct the conn_bridge comment: under muxscript g_pConnMgr is the
script stub, not null — TrimmedName still leaves cbuff unwritten.
#1614 step 4 asked for the conformance list -- "the artefact none of the
eight issues produced". This is that list, plus the harness that keeps it
honest, plus one fix for a bug found writing it.
## Why a diff and not assertions
Two harnesses already existed and neither compares what a command PRINTS:
comsys_handoff state written under one implementation, read under the
other. Catches disagreement about STORED state.
comsys_mogrify each side joins and speaks in one process. Catches
disagreement during DELIVERY.
Assertions can only catch a divergence somebody already thought to write a
case for, and that is exactly how the ones below survived. So the third
harness runs one command stream under both implementations against identical
fresh databases and diffs the entire output against a recorded baseline.
Both directions fail the run. A NEW divergence means the two just drifted
apart somewhere nobody was looking. A KNOWN divergence VANISHING means
something was fixed and the baseline is now lying, so it must be regenerated
as part of that fix -- otherwise it hides the next regression. That property
paid for itself twice while this branch was being written.
The stream is organised by the ABI rather than by feature, because
mux_IComsysControl (20 methods) and mux_IMailControl (12) are the definitive
list of places the two can answer differently.
## What one 47-line command stream found
Six divergences, none previously reported, in commands nobody had compared:
@clist/full module ignores the switch; Header, Access, Users and Msgs
columns all lost #1640
@cwho engine "Wizard(#1PcW)", module "Wizard" #1640
comtitle dropped by the module from speech AND join/leave #1640
delcom engine gives a personal confirmation, module emits
the channel broadcast instead #1640
@mail/dstats identical to /stats under the module -- three
@mail/fstats commands collapsed into one, widening #1631#1631
@mail/fstats engine counts +1 byte per message, disagreeing with
its own @mail/list about the same message #1639
Two of those are engine-side, and on @mail/stats and the From: field the
module is the MORE correct implementation. That cuts against #1614's
"the module is strictly behind" reading and is worth having before the
decision is made.
## The fix: trimmed_name left the caller's buffer uninitialised (#1637)
Found because the harness was flaky before it was committed, which is the
only reason anyone would have looked.
conn_bridge.cpp's trimmed_name does nothing when g_pConnMgr is null. Every
other bridge in that file returns void or a number, so that is a complete
answer for them; this one has an out parameter, and doing nothing leaves the
CALLER'S stack buffer untouched. mail.cpp has eight sites passing a bare
UTF8 szFromName[MBUF_SIZE] straight to tprintf("%s").
muxscript has no connection manager, so what got printed was whatever was on
the stack. Measured across three runs of the same command: "From: p", then
"From: ", then -- when the garbage terminated the format early -- the entire
message header vanished, taking At:, Fldr:, Status:, To: and Subject: with
it and leaving only the body.
cbuff[0] = '\0';
Unconditional, so it also covers a TrimmedName that fails partway. netmux
always has a connection manager, so no live game was affected; muxscript is
what the whole suite runs on.
Restoring the header immediately exposed a module gap the truncation had been
hiding: the module's @mail <n> omits the To: line. Nobody could have seen
that while the side it is compared against printed nothing at all.
## -Wformat on the module build
#1634 added __attribute__((format(printf,...))) to the new
log_storage_failure helpers and reported the format strings had been
type-checked. They had not been: -Wformat is off by default and these
modules compile with neither -Wall nor -W, so a clean build says nothing.
The strings are in fact correct -- I checked them with the flag on -- but the
guard was inert.
Adding -Wformat, and only -Wformat, to the modules' AM_CPPFLAGS. Measured
first: all five modules built there are already clean, so it lands no new
warnings. Not -Wall, which has not been audited here. Negative control:
one "%s" changed to "%d" now produces, through the real build,
comsys_mod.cpp:752: warning: format '%d' expects argument of type 'int',
but argument 5 has type 'const char*' [-Wformat=]
Regenerating mux/modules/Makefile.in used automake 1.16.5, the version
docs/building.md pins; it also picks up one unrelated line, PCRE2_CFLAGS,
which configure.ac already substitutes and the checked-in Makefile.in had
drifted from. configure is NOT regenerated -- that wants autoconf 2.73 and
this box has 2.71.
## Interaction with what landed today
#1644 turned muxscript logging on, so diagnostics now reach the output and
are part of the comparison -- the module announces its shutdown and startup
and the engine does not, which the baseline records rather than discards.
Only the leading timestamp is normalised.
#1643 is open against #1639. When it merges the fstats row disappears and
this baseline must be re-blessed; that is the harness working, not breaking.
Verified: make test green -- 1561 smoke on the module implementation, 1561 on
the built-in, 1561 on the interpreted route, handoff 13/13, mogrify 5/5,
conformance passing against the recorded baseline, and the rest of the suite
unchanged.
Refs #1564, #1572, #1585, #1587, #1589, #1594, #1620, #1631, #1633, #1634,
#1637, #1639, #1640, #1643, #1644.