Commit graph

28 commits

Author SHA1 Message Date
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
114495b4d8 docs(functions): pay the #2146 review debts — invariant at every site, honest CHANGES
Three items from Kagura's review, none behavioural:

- Every `uninit (#2145)` marker now states the invariant the safety
  rests on ("past-count reads are UB now, not nullptr") instead of only
  the what; the canonical version lives with the splitter contracts in
  functions.h, which also picks up the #2144 nit that list2arr_nd
  truncates beyond LBUF_SIZE-1 where list2arr does not.

- CHANGES no longer implies a measured win for the set family.  Their
  identical memsets measured near-free on BOTH architectures before the
  change (x86-64: setunion 0.95us vs vadd 7.70us with the same two
  memset instructions confirmed in disassembly; arm64 corroborates),
  which neither box can explain.  Tracked separately rather than
  guessed at.

- CHANGES gains the measured x86-64 numbers, where the conditional
  word-index sites turned out to be the largest wins (last 23x,
  lrest 18x) — the "lower priority" class from the issue was the
  opposite.

Smoke: 1660 passed, 1 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:28:47 -06:00
Stephen Dennis
c0155c51a0 perf(functions): stop zeroing 128-256 KB of pointer table per list call (#2145)
std::vector<T>(n) value-initializes, so every call to a list builtin
memset a quarter-megabyte of pointer table before looking at a single
token — ~6 of vadd()'s ~7 us on the issue's x86-64 box, 85% of the
function, and 4x worse since #1990 grew LBUF_SIZE 8000 -> 32768 with
nothing measuring the constant overhead (it is identical at every N,
the shape test-growth is blind to by design).

list2arr writes arr[i] only for i < its return value and every caller
reads only that far, so the tables never needed initializing.  They are
now uninitialized unique_ptr<T[]> allocations under the same RAII
lifetimes.

Converted:
  handle_vectors     2x  (vadd/vsub/vmul/vdot/vcross)
  fun_vmag/vunit     1x each
  fun_choose         2x
  fun_ledit          2x
  fun_sortby         1x
  handle_sets        2x  (setunion/setinter/setdiff — the issue's table
                          attributed this pair to fun_sortkey, whose own
                          allocation is already sized to strlen+1)
  shuffle/pickrand/last/lrest — the conditional multi-char-delimiter
                     co_split_words index tables (2x 256 KB each), same
                     shape, filled to nWords and read no further

Left alone, deliberately: sites already sized to the real token bound
(fun_sortkey's strlen+1 vector, cluster-offset tables) — proportional
zeroing is not the defect.

Measured (macOS arm64, benchmark() 10k iterations, us/call):
  vadd    1.89 -> 0.74     choose  1.70 -> 0.58
  ledit   2.00 -> 0.63     vmag    1.39 -> 1.03
Apple Silicon's memset made the before milder than the issue's x86-64
numbers (75x on the microbench there); the gradient-by-vector-count is
gone on both.

Full make test EXPECT_CONFIG="jit=yes": 35 passed, 1 skipped
(stubslave, not configured), 0 failed.  Spot checks exact: vadd,
setunion, shuffle, sortby with a live comparator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 08:42:38 -06:00
Stephen Dennis
be4bd0c7fa feat(functions): list2arr_nd — non-destructive list split, first conversions (#2136)
Phase 1 of the non-destructive argument contract.  Builtins have always
been allowed to tokenize fargs in place — split_token NULs every
separator in the CALLER's buffer — which is invisible while the
interpreter hands out fresh evaluation buffers and silent corruption the
moment the compiled route hands out cached memory: #2128 was a cached
program's own constant edited permanently by words(map(...)), and #2135
now copies every ordinary ECALL argument defensively to contain it.
That copy has two hand-found special cases and every new builtin is a
potential repeat; the way out is builtins that do not scribble, after
which the copy can be deleted rather than maintained.

list2arr_nd() tokenizes a PRIVATE copy: the caller's buffer is never
written, arr[] points into a caller-supplied LBUF-sized scratch whose
lifetime brackets the tokens, and tokens stay writable so consumers that
edit them in place remain legal.  One memcpy per split, pool-allocated.

Converted in this pass — every list2arr call site whose input is
borrowed memory (fargs or a parameter aliasing fargs):

  handle_vectors   both lists  (vadd/vsub/vmul/vdot/vcross family)
  fun_vmag         fargs[0]
  fun_vunit        fargs[0]
  fun_choose       fargs[0], fargs[1]
  fun_ledit        fargs[1], fargs[2], and its inline trim/split walk
                   over fargs[0]
  real_regmatch    the register list (fargs[2] of regmatch/regmatchi)

Sites already tokenizing their own copies (fun_sort, fun_sortby,
handle_sets, do_asort_finish, fun_shuffle) are correct as-is and were
left untouched — the conversion targets the contract violation, not the
idiom.

Remaining phases, tracked in #2136: the ~13 FUNCTION bodies that
trim/split fargs directly without list2arr; then a const-qualified
argument contract so the compiler enforces what this establishes by
convention; then the #2135 ECALL copy is deleted, not defended.

No behavioural change intended: full make test EXPECT_CONFIG="jit=yes"
is 35 passed / 1 skipped (stubslave, not configured) / 0 failed, and the
smoke suite's golden outputs cover every converted builtin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 07:35:25 -06:00
Stephen Dennis
957caeb1b0 harden(digest): back mux_sha1_digest with Windows CNG, retire homegrown SHA-1 (#1963)
The non-OpenSSL digest backend is now CNG (BCrypt) with cached
algorithm-provider handles; the FIPS-180 MUX_SHA1_* implementation is
deleted and the tree ships no cryptographic source, matching the
Schannel-for-TLS precedent.  A new generalized mux_digest(name, ...)
entry point serves sha1/sha256/sha384/sha512/md5 (case-insensitive,
hyphenated aliases), and fun_digest's non-OpenSSL branch dispatches
through it, so digest(sha256,...) et al. now work on Windows --
digest_fn.mux TC004/TC005 flip from Skipped to Succeeded there via
their existing behavior-probing guards.

Output is byte-identical across the swap: tests/digest (new, wired as
make test-digest and into test-asan) pins the surfaces whose bytes may
never change -- RFC 6455 Sec-WebSocket-Accept (single-part and the
two-part gather websocket.cpp performs), the $SHA1$ salt||password
gather and bare-password $P6H$ shapes from player.cpp, and the sha1()
softcode FIPS vectors -- with every golden value generated by the
openssl(1) CLI as an external oracle.  Verified on Windows: homegrown
== oracle == CNG on all six SHA-1 vectors, 17/17 KATs against the CNG
build, full solution build with zero new warnings, smoke ALL 1601
PASSED / 0 failed.

A non-Windows non-OpenSSL platform now hits #error by design:
configure.ac hard-errors without OpenSSL, so no shipped config lands
there, and the homegrown fallback must not silently return.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 14:01:56 -06:00
Stephen Dennis
af97bce20c fix(digest): resolve digest()/hmac() names via EVP_MD_fetch on OpenSSL 3.0+ (#1961)
digest(<name>) and hmac(...,<name>) resolved algorithm names with the legacy
EVP_get_digestbyname(). On OpenSSL 3.0 that does not resolve hyphenated aliases
(e.g. "sha-1") until the default provider has been lazily loaded by an earlier
successful digest. In threaded netmux that warm-up straddles TC005's two cand()
branches, so smoke went red 4/4 on OpenSSL 3.0.13 (Debian 12 / Ubuntu 22.04 /
RHEL 9), while newer OpenSSL (3.6.2) resolves the alias cold and passes.

Use the provider-native EVP_MD_fetch(NULL, name, NULL) on OpenSSL 3.0+
(non-LibreSSL), which resolves aliases deterministically from a cold process,
and free the fetched EVP_MD. Keep EVP_get_digestbyname() on pre-3.0 / LibreSSL,
where it returns a static const and the lazy-provider behavior does not occur.
Gated on OPENSSL_VERSION_NUMBER rather than AC_CHECK_FUNCS to avoid regenerating
configure with autoconf 2.71 vs the tree's required 2.73 (#1477); EVP_MD_fetch
is inherently a 3.0 API so the version guard is semantically exact.

Also fix an EVP_MD_CTX leak in fun_digest on the unsupported-name path (it
returned after EVP_MD_CTX_new() without freeing the context).

Verified on OpenSSL 3.0.13 (Kagura): reverting just these two files gives
TC005 FAIL 3/3; with the fix TC005 PASS 4/4 and full smoke ALL 1588 PASSED.
digest(sha-1)/hmac(...,sha-1) resolve from a cold process; sha_1/bogus still
rejected. Needs 3.6.2 no-regression confirmation on the box that already passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 13:22:07 -06:00
Stephen Dennis
2b93a62a42 fix(funmath): defined wrap for iadd/isub/imul/limath overflow (#1861)
Signed int64 add/sub/mul is undefined behavior and softcode-reachable.
Match #1472: perform the operations in uint64_t modular arithmetic via
i64Add/i64Sub/i64Mul, preserving historical two's-complement wrap for
iadd/isub/imul and limath add/mul/sub (and the even-median mid formula).

Smoke pins INT64_MAX+1, INT64_MIN-1, and INT64_MAX*2 wrap results.
2026-07-31 08:49:56 -06:00
Stephen Dennis
ddff5b45a5 chore(int64): softcode mux_atoi64 sinks use int64_t (#1402)
Convert remaining engine `int x = mux_atoi64(...)` (and explicit
static_cast<int> forms) to int64_t so parse width is not discarded on
any platform. Includes functions/funceval/funmath, mail folder numbers,
comsys charge parse, JIT register indices, and HIR lower mid/left/right.

dbref parse keeps full-width parse then rejects values outside int
range. Channel charge still stores int after an in-range check.
2026-07-28 19:40:41 -06:00
Stephen Dennis
cd76d59b10 fix(funmath): remove softcode-reachable signed overflow in shl/inc/dec (#1472)
Three arithmetic UB sites, each reachable from a one-line softcode expression.
The arithmetic is moved into uint64_t, which is defined as exactly the modular
operation these functions already produce, so no answer changes.

  shl(-2,1)                      left shift of a negative value
  shl(1,63), shl(3,63)           a bit shifted into the sign bit
  inc(9223372036854775807)       signed overflow at INT64_MAX
  dec(-9223372036854775808)      signed overflow at INT64_MIN

shl() is the interesting one. #1109 bounded the shift *count* to [0,63] and
that check is correct, but a negative left operand is undefined at any count,
and so is shifting a bit into or past the sign bit -- so #1109 closed one half
of the UB at that line while its comment reads as though the line were done.

Verified under -fsanitize=undefined, driving each expression through
muxscript.  Without the fix, on the interpreter route:

  funmath.cpp:901:27  left shift of negative value -2
  funmath.cpp:966:20  signed integer overflow: 9223372036854775807 + 1 ...
  funmath.cpp:985:20  signed integer overflow: -9223372036854775808 - 1 ...

With the fix, zero reports, and all 20 sampled results are bit-identical to
the pre-fix values.

Worth recording how the inc/dec sites hide: with the JIT on, only the shl
report appears, because inc/dec are serviced natively and fun_inc/fun_dec
never run.  Reaching them needs jit_eval_brackets 0.  A sanitizer run over
the default configuration alone will not see these.

Tests: shl TC008, inc TC003 and dec TC003 pin the wrap at the 64-bit
boundaries.  Since the fix deliberately preserves every value, these cannot
fail by reverting it -- they guard against a future change to saturating or
erroring semantics, which #1402 may yet settle.  Each was confirmed capable
of failing by corrupting its expected value (3 failures, one per case).

Also corrected these three files to signal tr.done exactly once, from both
branches of the final case; inc_fn and dec_fn fired it from a non-final
case's success branch as well.  41 files share that defect -- see #1495.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 20:25:43 -06:00
Stephen Dennis
e7eb6ec76d nls: route literal #-1 softcode tokens through S_ (#1475)
Mechanical hygiene under the opt-in M_() design: replace T("#-1…") and
T("#-2…") with S_() so softcode ABI tokens are obvious in source and
cannot enter a player catalog. ~400 call sites across engine, exp3,
mail, and driver. Assembled/library-spliced diagnostics (plan §4.2)
are unchanged where they are not a single literal.
2026-07-27 01:00:55 +00:00
Stephen Dennis
f1dba51386
fix(win32): stop narrowing mux_atoi64 results into long (#1402) (#1404)
After #1374/#1384 the parser is 64-bit, but several sites still stored
the result in long. On LLP64 that truncates again: shl(1,4294967296)
became a no-op shift, and printf(%d,4294967296) printed 0.

Widen the residual sinks to int64_t (funmath fast paths, shl/shr count,
printf %d/%i, width check, netaddr threshold, timeutil, mathutil digits)
and add smoke coverage for the two softcode-visible failure modes.
2026-07-26 13:29:06 -06:00
Stephen Dennis
2f106f200f fix(win32): migrate the remaining mux_atol callers to mux_atoi64 (#1373)
Completes the sweep the issue called for.  mux_atol returns long, which
is 32-bit on LLP64, so every caller silently truncated on Windows.  Two
of those were real defects (the truthiness family and cf_size, fixed in
the preceding commits); the rest were latent, waiting for a value large
enough to matter.

Rather than audit 290 sites for whether each can reach 2^31 today, use
the 64-bit parser everywhere and remove the class.  A dbref cannot
overflow now, but nothing stops a later caller passing that same site a
timestamp or a byte count.

Pure 1:1 substitution: 285 lines changed, and every removed line
contained mux_atol while every added line contains mux_atoi64.  No
control flow, no types, no behaviour beyond the wider parse.

This is a NO-OP on LP64 -- long is already 64-bit on Linux and macOS, so
the generated code there is unchanged.  It only widens the parse on
Windows.  Narrowing destinations are unaffected either way: `int x =
mux_atoi64(s)` truncates exactly as `int x = mux_atol(s)` did, on both
models.

Left alone: mux_atol itself in mathutil, its declaration, and three
comments that name it.  Callers that genuinely want 32-bit semantics can
still ask for them; none appear to.

Verified on Windows: full solution builds clean with no new warnings,
smoke is 1418 passed / 16 failed / 0 crashes / 306 of 306 dispatched --
identical to before the sweep, with the same 16 build-configuration
failures (exp3 module not loaded, hmac/digest behind UNIX_DIGEST).
Spot checks after the change: the boolean family returns 1 for multiples
of 2^32, cf_size round-trips 3000000000 and still reads -1 as unlimited,
and arithmetic, string and list functions are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:12:35 -06:00
Stephen Dennis
6c54d5972e fix(win32): use mux_atoi64 for softcode truthiness (#1373)
mux_atol() returns long: 64-bit on LP64, 32-bit on Windows.  Thirteen
sites evaluate truthiness as isTRUE(mux_atol(...)), so on Windows any
value whose low 32 bits are zero read as false.

    xor(4294967296)   0   should be 1
    lxor(4294967296)  0   should be 1
    and(4294967296)   0   should be 1
    or(4294967296)    0   should be 1
    t(4294967296)     1   correct -- goes through xlate()

The last line is the tell: on Windows the server contradicted itself,
with t() calling a value true while and() called the same value false.

correctness_fn.mux TC001 and the comment in fun_xor both already say the
intent is "the full 64-bit value, not a 32-bit-truncated copy".  The fix
simply never worked on LLP64, because long is the wrong type to say it
in.  Two of the thirteen are in ast.cpp, the evaluator's own boolean
handling, so this was not confined to a few list functions.

mux_atoi64() already exists beside mux_atol() and returns int64_t.  This
is a no-op on LP64 -- long is already 64-bit there -- and corrects LLP64.
isTRUE(x) is ((x) != 0), so nothing else changes.

Deliberately NOT switched to xlate(), even though that is the single
definition of a softcode boolean: doing so would change behaviour on
Linux too, since xlate treats #- errors and non-numeric text differently
from isTRUE(atol).  This is a portability defect, not a semantics
decision.

Found by the first smoke run ever performed on Windows (#1347).  Full
smoke there went from 1415 passed / 19 failed to 1418 / 16.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 09:43:38 -06:00
Stephen Dennis
b32f3ab4aa fix(math/jit): drop broken HIR_ABS; abs(INT64_MIN) is OUT OF RANGE
#1256: Remove integer HIR_ABS (enum, codegen, fold, purity). Softcode
abs() is float after #1150; the branchless ABS sequence corrupted
INT64_MIN into non-numeric text. Leaving the dead op would reintroduce
that when a future emitter returned.

#1255: fun_abs rejects the exact integer INT64_MIN like iabs (#1114);
fval could not format |INT64_MIN| as a non-negative value and returned
a negative string. Const-fold of abs() matches. Smoke covers both.
2026-07-25 19:06:28 -06:00
Stephen Dennis
a17bfea581 fix(softcode): iabs(INT64_MIN) returns #-1 OUT OF RANGE (#1114)
The #1114 fix removed the -INT64_MIN UB by emitting the magnitude as a
string, "9223372036854775808".  That value is not a representable int64,
and every consumer corrupts it — measured on this tree:

  idiv(iabs(-9223372036854775808),1) -> -9223372036854775808
  shl(iabs(-9223372036854775808),0)  -> -9223372036854775808
  mod(iabs(-9223372036854775808),10) -> 2
  add(iabs(-9223372036854775808),0)  -> 9223372036854769664

Integer-path callers re-parse through mux_atoi64, which wraps rather than
saturates, landing back on INT64_MIN — so iabs() output comes out
NEGATIVE, silently breaking the function's one invariant.  The float path
merely loses precision.  Either way the caller gets a wrong number with
no indication.

Reject instead, via the existing safe_range() (#-1 OUT OF RANGE), which is
how the integer family already handles an out-of-domain argument
(fun_table, fun_columns).  The UB stays fixed; the failure is now loud.

Adds iabs TC003 pinning the policy plus the INT64_MIN+1 and INT64_MAX
edges that must still work.  Verified it catches a regression: restoring
the magnitude string fails TC003 with
iabs(-9223372036854775808)=9223372036854775808.  smoke 1325/1325.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 20:08:20 -06:00
Stephen Dennis
591c624afe fix(softcode): use the standard #-1 CPU LIMITED string in lmath/limath
The #1119 alarm pre-check introduced "#-1 CPU LIMIT EXCEEDED", a string
that appears nowhere else in the tree.  The engine-wide convention for
this condition is "#-1 CPU LIMITED" (ast.cpp:2046, jit_compiler.cpp:974,
and the AST/JIT alarm paths generally), so softcode testing for the
standard wording would silently miss the lmath/limath case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 19:59:10 -06:00
Stephen Dennis
fe10b1211e fix: resolve Pass 5 Medium softcode defects #1111–#1115, #1117–#1119 + #1122
- #1111: wrapcolumns caps colWidth/nCols to LBUF-scale bounds
- #1112: regmatch pcre2_substring_copy capacity = LBUF_SIZE-1
- #1113: regrep_util null-checks match_data (fail closed)
- #1114: iabs(INT64_MIN) returns magnitude string (no negation UB)
- #1115: sql/mapsql drop second mux_exec; hard Wizard(); sql() invk/alarm
- #1117: sandbox reverse uses dynamic entry table (no 512 fail-open)
- #1118: trim_space_sep_LEN early-out when nStr==0
- #1119: lmath/limath median uses std::sort + alarm check
- #1122: mailreview body rejects nObjEvalNest (sent-mail twin of #1106)

engine.so builds clean. #1116 remains not-a-bug (closed).
2026-07-24 19:42:21 -06:00
Stephen Dennis
b1a9a196bd fix: resolve Pass 5 High softcode defects #1106–#1110
- #1106: mail() body access rejects self under nObjEvalNest (objeval nest)
- #1107: pose() only runs SPEECHMOD/SAYSTRING when Controls(target)
- #1108: lrooms BFS requires Good_obj+isRoom before Examinable/enqueue
- #1109: shl/shr require shift count in [0, 63] (UB for >= 64)
- #1110: heap-allocate limath vals and LBUF-scale list scratch (stack margin)

Claude confirmed all five; #1110 framed as recursion-margin erosion (leaf
handlers), not nest-to-multiply. engine.so builds clean.
2026-07-24 19:19:40 -06:00
Stephen Dennis
cd90317993 fix: fmod() guards the divisor, not the dividend, on NO_IEEE_FP_SNAN lanes
fun_fmod's #ifndef HAVE_IEEE_FP_SNAN branch returned Ind when val1
(the dividend) was 0 — but fmod(0,y) is a valid 0; the indeterminate
case is a zero divisor, fmod(x,0).  The check was copy-pasted from
fun_power, where guarding val1 (the base, for the negative-base
fractional-power domain error) is correct.  So on a NO_IEEE_FP_SNAN
build fmod(0,3) wrongly returned Ind and fmod(x,0) fell through
unguarded — exactly inverted.

Guard val2 == 0.0 instead.  Verified by force-compiling the branch on
this HAVE_IEEE_FP_SNAN build: before, fmod(0,3)=Ind; after,
fmod(0,3)=0, fmod(10,0)=Ind, fmod(10,3)=1, fmod(-7,3)=-1.  Dead code on
the primary build (full smoke 1278/1278 unchanged); the fix matters
only on portability lanes where the configure probe picks
NO_IEEE_FP_SNAN.

Reported by ThresholdOps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 14:39:57 -06:00
Stephen Dennis
c34df4be01 Fix correctness bugs in xor(), wrapcolumns(), step(), tr()
Correctness sweep of the softcode function library, evaluator, and lock/
wildcard semantics. Four confirmed bugs, each empirically validated on a
live server before fixing:

 - xor(): tested truthiness on a 32-bit-truncated copy of each argument,
   so values whose low 32 bits are zero (e.g. xor(4294967296)) were wrongly
   false and disagreed with lxor(). Now uses isTRUE(mux_atol()). (#850)
 - wrapcolumns(): dropped the character at the break column on every hard
   (no-space) line break. Segments are now copied out with their own
   terminator instead of NUL-overwriting input in place. (#851)
 - step(): evaluated the attribute as the calling executor rather than the
   attribute-owning object, unlike map()/mix()/foreach(). Behavior change
   for cross-object step(). (#852)
 - tr(): registered with a 1-arg minimum but unconditionally reads
   fargs[1]/fargs[2], so tr(abc) dereferenced NULL. Now min == 3. (#853)

Adds regression tests TC001-TC005 (correctness_fn.mux), including a guard
for switch()/#$ pattern scoping (a flagged finding that the live engine
already handles correctly). Build clean, all 1263 smoke tests pass.
unique()'s unimplemented <sorttype> is documented as an open item in
docs/survey-correctness-pass-2026-06.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 05:56:27 -06:00
Stephen Dennis
3b62628772 Migrate 47 more alloc_lbuf/free_lbuf sites to LBuf RAII
Second wave: convert manual alloc/free pairs in cque (1), db_rw (4),
conf (5), db (2), engine_com (6), help (7), levels (2), predicates (5),
player (7), funmath (8).  Eliminates ~80 explicit free_lbuf calls
including multi-exit error paths in getboolexp1 (5 frees → 0),
get_list, AnnounceConnect/Disconnect, and the eight NOEVAL function
variants (cand/cor/firstof/allof and bool variants).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:55:40 -06:00
Stephen Dennis
614d2049be Add limath() for 64-bit integer list reductions
Integer-only counterpart to lmath() supporting add, sub, mul, div, mod,
min, max, and median. Validates integer input, fails with #-1 LIST TOO
LONG on overflow rather than silently truncating. Even-length median
uses overflow-safe a + (b - a) / 2 form. 14 smoke tests, Tier 2
complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:28:11 -06:00
Stephen Dennis
4e4808489d Add lband(), lbor(), lbxor() bitwise list reducers
Completes the horizontal bitwise family: lband() returns AND of all
list elements (identity: -1), lbor() returns OR (identity: 0), lbxor()
returns XOR (identity: 0). Validates integer input, supports custom
delimiters. 12 smoke tests, survey updated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:59:30 -06:00
Stephen Dennis
1deed77aed Add lxor() boolean parity reduction for lists
Completes the land()/lor()/lxor() family. Returns 1 when an odd number
of list elements are true, 0 otherwise. Empty list returns 0 (XOR
identity). 9 smoke tests, survey updated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:09:25 -06:00
Stephen Dennis
fa18bdae47 Replace stack-allocated LBUF arrays with pool-backed LBuf RAII wrapper
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>
2026-03-23 21:52:30 -06:00
Stephen Dennis
2ccfb99298 Add firstof(), strfirstof(), allof(), strallof(), prompt()
Short-circuit evaluation functions (FN_NOEVAL):
- firstof(a,b,...) — return first true (non-zero) argument
- strfirstof(a,b,...) — return first non-empty string argument
- allof(a,b,...,osep) — return all true arguments, separated by osep
- strallof(a,b,...,osep) — return all non-empty arguments, separated by osep

Notification function:
- prompt(target, text) — send text with telnet GA, no trailing newline

Completes the PennMUSH quick-wins sprint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:03:15 -06:00
Stephen Dennis
d9ab0546f7 JIT: harden compile cache keys and extract SHA1 wrapper 2026-03-15 02:18:13 -06:00
Stephen Dennis
4ff1398de1 Restructure mux/ directory: component-based layout with proper build root
Move from flat mux/src/ layout to clean component hierarchy:
- mux/ is now the autoconf/automake build root (configure.ac lives here)
- mux/include/ — shared headers used by multiple components
- mux/lib/ — libmux.so (core utilities, no game state)
- mux/src/ — netmux driver only (thin networking shell)
- mux/modules/engine/ — engine.so (game logic)
- mux/modules/{comsys,mail,exp3,sqlproxy,sqlslave}/ — external modules
- mux/ganl/ — GANL networking library
- mux/sqlite/ — SQLite amalgamation (builds libsqlite3.a)
- mux/announce/ — announce tool (was mux/src/tools/)

Build changes:
- SUBDIRS ordering: ganl sqlite lib src modules announce
- libmux.so gets -Wl,-soname,libmux.so; netmux links via -L -lmux
- engine.so links libsqlite3.a and libmux.so with -Wl,--no-undefined
- RPATH uses $ORIGIN for portable .so resolution
- Install hooks use absolute paths for game/bin symlinks

Bug fixes:
- engine.so mux_Register() now passes nullptr to mux_RegisterClassObjects
  (matches all other modules; libmux already has the factory via dlsym)
- DbConvert() now calls pcache_init() before db_write, fixing a latent
  crash (free(): invalid pointer) when exporting from SQLite databases

411/411 smoke tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:38:37 -06:00
Renamed from mux/src/funmath.cpp (Browse further)