Commit graph

408 commits

Author SHA1 Message Date
Stephen Dennis
b05539f86a fix(#2212): PCRE2 comes from vcpkg, like grpc
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>
2026-08-10 20:39:31 -06:00
Stephen Dennis
a906a66af6 chore(#2136): post-merge nits from the PR review
- 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>
2026-08-06 15:24:55 -06:00
Stephen Dennis
3289e8b935 feat(#2136): phase 4 — delete #2135's ECALL argument copy; the contract replaced it
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>
2026-08-06 14:34:23 -06:00
Stephen Dennis
8808b4c375 feat(#2136): flip fargs to const UTF8 * const — and convert every site the compiler surfaced
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>
2026-08-06 14:34:23 -06:00
Stephen Dennis
18d906640e build: drop objects that lack sibling .d files (#2118)
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.
2026-08-05 23:14:14 +00:00
Stephen Dennis
6d7600b5ee docs: measure 2.13 vs 2.14 end to end, and say what it does not show (#2046)
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.
2026-08-04 22:35:16 -06:00
Stephen Dennis
a872b4b28b docs: renumber the duplicated methodology item
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.
2026-08-04 22:09:51 -06:00
Stephen Dennis
c21e8b2883 docs: re-baseline the performance survey after five merges (#2046)
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.
2026-08-04 22:08:52 -06:00
Stephen Dennis
2fd3b5ab94 docs: verify the $-dispatch root cause by removing it (#2046)
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.
2026-08-04 18:26:35 -06:00
Stephen Dennis
92d212280c
docs: record the 2026-08 performance pass (#2046) (#2064)
* 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>
2026-08-04 17:50:26 -06:00
Stephen Dennis
709198c353 Add tools/ansify: MUX percent-color to ANSI (Ragel → C)
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.
2026-08-01 17:12:16 -06:00
Stephen Dennis
1fca23e3e5 fix(net): residual full-width SOCKET logs; Pass 15 audit map closeout 2026-07-31 06:15:11 -06:00
Stephen Dennis
3113fb15f6 fix(net): Pass 15 A2 — WS preface split, SMTP bounds, zero-listener fail
#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.
2026-07-31 05:49:27 -06:00
Stephen Dennis
9db087c0f3 fix(driver): fail closed when required COM interfaces are missing (#1798)
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.
2026-07-31 05:44:03 -06:00
Stephen Dennis
ddb6e75f5e
Merge pull request #1796 from brazilofmux/test/lua-seam-corpus-079
test(lua): grow softcode↔Lua seam corpus (TC079–090)
2026-07-29 15:31:39 -06:00
Stephen Dennis
caaf252cff test(lua): grow softcode↔Lua seam corpus (TC079–090, NESTED, STATE e4)
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.
2026-07-29 19:44:30 +00:00
Stephen Dennis
10db61ee11 docs(audit): Pass 14 closeout — map complete, supersede stack
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.
2026-07-29 13:24:19 -06:00
Stephen Dennis
d0cdcce123 fix(client): extend #1788 buffer caps to web, tf, android, win32gui
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.
2026-07-29 13:21:35 -06:00
Stephen Dennis
03ae705717 fix(softcode/jit): no AST re-run after host ECALL mid-run fail (#1791)
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.
2026-07-29 13:17:18 -06:00
Stephen Dennis
f9d0495f94 docs(audit): finish J1/J5/H2/K4 Pass 14 stamps 2026-07-29 13:10:28 -06:00
Stephen Dennis
703a0a2f33 fix(client): cap line and telnet SB reassembly buffers (#1788)
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.
2026-07-29 13:10:28 -06:00
Stephen Dennis
414ea0ecca feat(lua/jit): CALL_VAL keeps call results as typed stack values
#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.
2026-07-29 13:02:01 -06:00
Stephen Dennis
cbc40c7f86 fix(netaddr): reject oversized CIDR prefix before int truncation (#1774)
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.
2026-07-29 13:00:55 -06:00
Stephen Dennis
6f38d62b7b fix(lua/jit): equality compares Lua TYPES, not representations (#1770 review)
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>
2026-07-29 12:40:33 -06:00
Stephen Dennis
0efe4956ca fix(lua/jit): EQK must not treat nil as empty string
#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.
2026-07-29 12:22:12 -06:00
Stephen Dennis
20fe115a32 fix(lua/jit): allow effects on compiled path; close post-entry loud
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.
2026-07-29 11:08:22 -06:00
Stephen Dennis
adb64b6fdb docs(lua): transition plan for post-entry decline/rerun contract (#1751)
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.
2026-07-29 01:10:35 -06:00
Stephen Dennis
2a85210ad5 fix(lua/jit): read the instruction budget at run time, not compile time (#1745)
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>
2026-07-28 21:45:42 -06:00
Stephen Dennis
dbb48020c4 docs(lua/jit): stamp product path complete after default-on
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.
2026-07-29 03:28:12 +00:00
Stephen Dennis
2e7807821e docs(nls): record %N$ support and table close-out (#1419)
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).
2026-07-28 11:59:41 -06:00
Stephen Dennis
81b4a4b409 docs: note Phase 4 B3 in tabular design changelog 2026-07-28 17:57:02 +00:00
Stephen Dennis
92039ec241 feat(mail): folder/review list lines via mux_table (#1667 Phase 4 B3)
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.
2026-07-28 11:54:46 -06:00
Stephen Dennis
431380e3ea test(nls): ban pre-spaced multi-column table headers (#1667 Phase 5) 2026-07-28 11:41:45 -06:00
Stephen Dennis
e1df4cab56 feat(tables): remaining staff lists via mux_table (#1667 Phase 4 C5-C9) 2026-07-28 17:38:11 +00:00
Stephen Dennis
5f43951f46 fix(tables): pot for C* labels; no %* width on mux_sprintf
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.
2026-07-28 17:34:14 +00:00
Stephen Dennis
5cee6cb721 feat(tables): staff list layouts via mux_table (#1667 Phase 4 C*) 2026-07-28 11:31:23 -06:00
Stephen Dennis
1be8bf830c feat(mail): alias lists via mux_table schema (#1667 Phase 4 B1/B2) 2026-07-28 11:22:02 -06:00
Stephen Dennis
d4d14066e2 feat(comsys): @cwho via mux_table display columns (#1667 Phase 4 A4) 2026-07-28 09:22:57 -06:00
Stephen Dennis
3d1bd97a65 feat(comsys): comlist via mux_table schema (#1667 Phase 4 A3) 2026-07-28 15:05:01 +00:00
Stephen Dennis
fadc7648e3 feat(comsys): @clist/full via mux_table schema (#1667 Phase 4 A1) 2026-07-28 08:31:41 -06:00
Stephen Dennis
40017efc41 feat(comsys): @clist headers via mux_table schema (#1667 Phase 4 A2) 2026-07-28 08:22:41 -06:00
Stephen Dennis
db2aa4e09e feat(libmux): mux_table layout helpers for multi-column notifies (#1667)
Phase 3 of the NLS-safe table design: put pad/truncate/emit helpers on
co_copy_field in libmux so modules and engine share one layout path.

- mux/include/mux_table.h + mux/lib/mux_table.c (module-safe, freestanding C)
- comsys_mod / mail_mod: drop local append_* copies; call mux_table_*
- tests/table: ljust, trunc, CJK width, emit_fields header/row parity
- docs: mark Phase 3 done; Phase 4 still owns blob-header conversion

Verified: MSVC Release libmux + modules; tests/build-msvc.sh table PASSED.
2026-07-28 14:10:05 +00:00
Stephen Dennis
22531555c3 docs: Phase 2 exit checklist and Phase 3 handoff constraints (#1667)
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.
2026-07-28 07:43:44 -06:00
Stephen Dennis
17939e3493 docs: Phase 2 ownership — libmux layout + #1614 product decision (#1667)
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.
2026-07-28 07:06:44 -06:00
Stephen Dennis
ffda8ba72f docs: Phase 1 inventory of multi-column player tables (#1667)
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).
2026-07-28 07:03:29 -06:00
Stephen Dennis
90e56712d7 docs: NLS-safe multi-column notifies — leave alone or do it right
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).
2026-07-28 05:19:05 +00:00
Stephen Dennis
210df0c226 docs(comsys): drop stale @cwho / mail-stats rows from conformance notes
#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).
2026-07-28 05:11:10 +00:00
Stephen Dennis
7d5ea6458b fix(comsys): three of four module command-surface divergences (#1640)
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.
2026-07-28 02:20:23 +00:00
Stephen Dennis
85abc7c235 test(comsys): re-bless conformance baseline after #1639
#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.
2026-07-28 02:01:43 +00:00
Stephen Dennis
24fd0b7249 test(comsys,mail): whole-output conformance diff, and the six divergences it found (#1614)
#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.
2026-07-28 01:47:47 +00:00