tinymux/mux/modules/engine/funmath.cpp

3637 lines
82 KiB
C++
Raw Permalink Normal View History

/*! \file funmath.cpp
* \brief MUX math function handlers.
*
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
#include "sha1.h"
#include <algorithm>
#include <climits>
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
#include <memory>
#include <vector>
static const long nMaximums[10] =
{
0, 9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999
};
static double g_aDoubles[MAX_WORDS];
FUNCTION(fun_add)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
int nArgs = nfargs;
if (MAX_WORDS < nArgs)
{
nArgs = MAX_WORDS;
}
int i;
for (i = 0; i < nArgs; i++)
{
int nDigits;
long nMaxValue = 0;
if ( !is_integer(fargs[i], &nDigits)
|| nDigits > 9
|| (nMaxValue += nMaximums[nDigits]) > 999999999L)
{
// Do it the slow way.
//
for (int j = 0; j < nArgs; j++)
{
g_aDoubles[j] = mux_atof(fargs[j]);
}
fval(buff, bufc, AddDoubles(nArgs, g_aDoubles));
return;
}
}
// We can do it the fast way.
//
int64_t sum = 0;
for (i = 0; i < nArgs; i++)
{
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:03:45 -06:00
sum += mux_atoi64(fargs[i]);
}
safe_i64toa(sum, buff, bufc);
}
FUNCTION(fun_ladd)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
int n = 0;
if (0 < nfargs)
{
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
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
LBuf scList = LBuf_Src("fun_ladd.list");
UTF8 *cp = trim_space_sep(list_copy_for_split(scList, fargs[0]), sep);
while ( cp
&& n < MAX_WORDS)
{
UTF8 *curr = split_token(&cp, sep);
g_aDoubles[n++] = mux_atof(curr);
}
}
fval(buff, bufc, AddDoubles(n, g_aDoubles));
}
/////////////////////////////////////////////////////////////////
// Function : iadd(Arg[0], Arg[1],..,Arg[n])
//
// Written by : Chris Rouse (Seraphim) 04/04/2000
/////////////////////////////////////////////////////////////////
FUNCTION(fun_iadd)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// #1861: signed overflow is UB; wrap via i64Add (see #1472 / timeutil.h).
//
int64_t sum = 0;
for (int i = 0; i < nfargs; i++)
{
sum = i64Add(sum, mux_atoi64(fargs[i]));
}
safe_i64toa(sum, buff, bufc);
}
FUNCTION(fun_sub)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
int nDigits;
if ( is_integer(fargs[0], &nDigits)
&& nDigits <= 9
&& is_integer(fargs[1], &nDigits)
&& nDigits <= 9)
{
int64_t a = mux_atoi64(fargs[0]);
int64_t b = mux_atoi64(fargs[1]);
safe_i64toa(a - b, buff, bufc);
}
else
{
g_aDoubles[0] = mux_atof(fargs[0]);
g_aDoubles[1] = -mux_atof(fargs[1]);
fval(buff, bufc, AddDoubles(2, g_aDoubles));
}
}
/////////////////////////////////////////////////////////////////
// Function : isub(Arg[0], Arg[1])
//
// Written by : Chris Rouse (Seraphim) 04/04/2000
/////////////////////////////////////////////////////////////////
FUNCTION(fun_isub)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// #1861: wrap via i64Sub rather than signed a - b.
//
int64_t a = mux_atoi64(fargs[0]);
int64_t b = mux_atoi64(fargs[1]);
safe_i64toa(i64Sub(a, b), buff, bufc);
}
FUNCTION(fun_mul)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double prod = 1.0;
for (int i = 0; i < nfargs; i++)
{
prod *= mux_atof(fargs[i]);
}
fval(buff, bufc, NearestPretty(prod));
}
/////////////////////////////////////////////////////////////////
// Function : imul(Arg[0], Arg[1], ... , Arg[n])
//
// Written by : Chris Rouse (Seraphim) 04/04/2000
/////////////////////////////////////////////////////////////////
FUNCTION(fun_imul)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// #1861: wrap via i64Mul rather than signed *=.
//
int64_t prod = 1;
for (int i = 0; i < nfargs; i++)
{
prod = i64Mul(prod, mux_atoi64(fargs[i]));
}
safe_i64toa(prod, buff, bufc);
}
FUNCTION(fun_gt)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
bool bResult = false;
int nDigits;
if ( is_integer(fargs[0], &nDigits)
&& nDigits <= 9
&& is_integer(fargs[1], &nDigits)
&& nDigits <= 9)
{
int64_t a = mux_atoi64(fargs[0]);
int64_t b = mux_atoi64(fargs[1]);
bResult = (a > b);
}
else
{
double a = mux_atof(fargs[0]);
double b = mux_atof(fargs[1]);
bResult = (a > b);
}
safe_bool(bResult, buff, bufc);
}
FUNCTION(fun_gte)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
bool bResult = false;
int nDigits;
if ( is_integer(fargs[0], &nDigits)
&& nDigits <= 9
&& is_integer(fargs[1], &nDigits)
&& nDigits <= 9)
{
int64_t a = mux_atoi64(fargs[0]);
int64_t b = mux_atoi64(fargs[1]);
bResult = (a >= b);
}
else
{
double a = mux_atof(fargs[0]);
double b = mux_atof(fargs[1]);
bResult = (a >= b);
}
safe_bool(bResult, buff, bufc);
}
FUNCTION(fun_lt)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
bool bResult = false;
int nDigits;
if ( is_integer(fargs[0], &nDigits)
&& nDigits <= 9
&& is_integer(fargs[1], &nDigits)
&& nDigits <= 9)
{
int64_t a = mux_atoi64(fargs[0]);
int64_t b = mux_atoi64(fargs[1]);
bResult = (a < b);
}
else
{
double a = mux_atof(fargs[0]);
double b = mux_atof(fargs[1]);
bResult = (a < b);
}
safe_bool(bResult, buff, bufc);
}
FUNCTION(fun_lte)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
bool bResult = false;
int nDigits;
if ( is_integer(fargs[0], &nDigits)
&& nDigits <= 9
&& is_integer(fargs[1], &nDigits)
&& nDigits <= 9)
{
int64_t a = mux_atoi64(fargs[0]);
int64_t b = mux_atoi64(fargs[1]);
bResult = (a <= b);
}
else
{
double a = mux_atof(fargs[0]);
double b = mux_atof(fargs[1]);
bResult = (a <= b);
}
safe_bool(bResult, buff, bufc);
}
FUNCTION(fun_eq)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
bool bResult = true;
int nDigits;
if ( is_integer(fargs[0], &nDigits)
&& nDigits <= 9
&& is_integer(fargs[1], &nDigits)
&& nDigits <= 9)
{
int64_t a = mux_atoi64(fargs[0]);
int64_t b = mux_atoi64(fargs[1]);
bResult = (a == b);
}
else
{
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
if (strcmp(reinterpret_cast<const char *>(fargs[0]), reinterpret_cast<const char *>(fargs[1])) != 0)
{
double a = mux_atof(fargs[0]);
double b = mux_atof(fargs[1]);
bResult = (a == b);
}
}
safe_bool(bResult, buff, bufc);
}
FUNCTION(fun_neq)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
bool bResult = false;
int nDigits;
if ( is_integer(fargs[0], &nDigits)
&& nDigits <= 9
&& is_integer(fargs[1], &nDigits)
&& nDigits <= 9)
{
int64_t a = mux_atoi64(fargs[0]);
int64_t b = mux_atoi64(fargs[1]);
bResult = (a != b);
}
else
{
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
if (strcmp(reinterpret_cast<const char *>(fargs[0]), reinterpret_cast<const char *>(fargs[1])) != 0)
{
double a = mux_atof(fargs[0]);
double b = mux_atof(fargs[1]);
bResult = (a != b);
}
}
safe_bool(bResult, buff, bufc);
}
/*
* ---------------------------------------------------------------------------
* * fun_max, fun_min: Return maximum (minimum) value.
*/
FUNCTION(fun_max)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double maximum = 0.0;
for (int i = 0; i < nfargs; i++)
{
double tval = mux_atof(fargs[i]);
if ( i == 0
|| tval > maximum)
{
maximum = tval;
}
}
fval(buff, bufc, maximum);
}
FUNCTION(fun_lmax)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double maximum = 0.0;
if (0 < nfargs)
{
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
2015-01-05 15:41:48 -07:00
int n = 0;
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
LBuf scList = LBuf_Src("fun_lmax.list");
UTF8 *cp = trim_space_sep(list_copy_for_split(scList, fargs[0]), sep);
2018-10-03 17:54:51 +00:00
while (nullptr != cp)
{
UTF8 *curr = split_token(&cp, sep);
2015-01-05 15:41:48 -07:00
double tval = mux_atof(curr);
if ( n++ == 0
|| tval > maximum)
{
maximum = tval;
}
}
}
fval(buff, bufc, maximum);
}
FUNCTION(fun_min)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double minimum = 0.0;
for (int i = 0; i < nfargs; i++)
{
double tval = mux_atof(fargs[i]);
if ( i == 0
|| tval < minimum)
{
minimum = tval;
}
}
fval(buff, bufc, minimum);
}
2015-01-05 15:41:48 -07:00
FUNCTION(fun_lmin)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double minimum = 0.0;
if (0 < nfargs)
{
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
int n = 0;
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
LBuf scList = LBuf_Src("fun_lmin.list");
UTF8 *cp = trim_space_sep(list_copy_for_split(scList, fargs[0]), sep);
2018-10-03 17:54:51 +00:00
while (nullptr != cp)
2015-01-05 15:41:48 -07:00
{
UTF8 *curr = split_token(&cp, sep);
double tval = mux_atof(curr);
if ( n++ == 0
|| tval < minimum)
{
minimum = tval;
}
}
}
fval(buff, bufc, minimum);
}
FUNCTION(fun_lmath)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// lmath(<operation>, <list>[, <delim>])
//
SEP sep;
if (!OPTIONAL_DELIM(3, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
int n = 0;
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
LBuf scList = LBuf_Src("fun_lmath.list");
UTF8 *cp = trim_space_sep(list_copy_for_split(scList, fargs[1]), sep);
while ( cp
&& n < MAX_WORDS)
{
UTF8 *curr = split_token(&cp, sep);
g_aDoubles[n++] = mux_atof(curr);
}
if (n == 0)
{
safe_chr('0', buff, bufc);
return;
}
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
const UTF8 *op = fargs[0];
if ( mux_stricmp(op, T("add")) == 0
|| mux_stricmp(op, T("sum")) == 0)
{
fval(buff, bufc, AddDoubles(n, g_aDoubles));
}
else if (mux_stricmp(op, T("mul")) == 0)
{
double prod = 1.0;
for (int i = 0; i < n; i++)
{
prod *= g_aDoubles[i];
}
fval(buff, bufc, NearestPretty(prod));
}
else if (mux_stricmp(op, T("sub")) == 0)
{
double result = g_aDoubles[0];
for (int i = 1; i < n; i++)
{
result -= g_aDoubles[i];
}
fval(buff, bufc, NearestPretty(result));
}
else if (mux_stricmp(op, T("div")) == 0)
{
double result = g_aDoubles[0];
for (int i = 1; i < n; i++)
{
if (g_aDoubles[i] == 0.0)
{
safe_str(S_("#-1 DIVIDE BY ZERO"), buff, bufc);
return;
}
result /= g_aDoubles[i];
}
fval(buff, bufc, NearestPretty(result));
}
else if (mux_stricmp(op, T("mod")) == 0)
{
int64_t result = static_cast<int64_t>(g_aDoubles[0]);
for (int i = 1; i < n; i++)
{
int64_t divisor = static_cast<int64_t>(g_aDoubles[i]);
if (divisor == 0)
{
safe_str(S_("#-1 DIVIDE BY ZERO"), buff, bufc);
return;
}
result = i64Mod(result, divisor);
}
safe_i64toa(result, buff, bufc);
}
else if (mux_stricmp(op, T("min")) == 0)
{
double minimum = g_aDoubles[0];
for (int i = 1; i < n; i++)
{
if (g_aDoubles[i] < minimum)
{
minimum = g_aDoubles[i];
}
}
fval(buff, bufc, minimum);
}
else if (mux_stricmp(op, T("max")) == 0)
{
double maximum = g_aDoubles[0];
for (int i = 1; i < n; i++)
{
if (g_aDoubles[i] > maximum)
{
maximum = g_aDoubles[i];
}
}
fval(buff, bufc, maximum);
}
else if ( mux_stricmp(op, T("mean")) == 0
|| mux_stricmp(op, T("avg")) == 0)
{
fval(buff, bufc, AddDoubles(n, g_aDoubles) / n);
}
else if (mux_stricmp(op, T("median")) == 0)
{
// #1119: O(n log n) sort — insertion sort was O(n²) up to MAX_WORDS.
//
if (alarm_clock.alarmed)
{
safe_str(S_("#-1 CPU LIMITED"), buff, bufc);
return;
}
std::sort(g_aDoubles, g_aDoubles + n);
if (n % 2 == 1)
{
fval(buff, bufc, g_aDoubles[n / 2]);
}
else
{
g_aDoubles[0] = g_aDoubles[n / 2 - 1];
g_aDoubles[1] = g_aDoubles[n / 2];
fval(buff, bufc, AddDoubles(2, g_aDoubles) / 2.0);
}
}
else if (mux_stricmp(op, T("stddev")) == 0)
{
double mean = AddDoubles(n, g_aDoubles) / n;
double sumSqDiff = 0.0;
for (int i = 0; i < n; i++)
{
double diff = g_aDoubles[i] - mean;
sumSqDiff += diff * diff;
}
fval(buff, bufc, sqrt(sumSqDiff / n));
}
else
{
safe_str(S_("#-1 UNKNOWN OPERATION"), buff, bufc);
}
}
// ---------------------------------------------------------------------------
// limath: Integer-only list reduction, parallel to lmath().
//
// limath(<operation>, <list>[, <delim>])
//
// Supported operations: add/sum, sub, mul, div, mod, min, max, median.
// All arithmetic is 64-bit integer.
//
FUNCTION(fun_limath)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
SEP sep;
if (!OPTIONAL_DELIM(3, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
// #1110: heap-allocate — vals[MAX_WORDS] was ~256 KiB on the stack and
// ate recursion margin under deep mux_exec (leaf, but still one big frame).
//
std::vector<int64_t> vals;
vals.reserve(64);
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
LBuf scList = LBuf_Src("fun_limath.list");
UTF8 *cp = trim_space_sep(list_copy_for_split(scList, fargs[1]), sep);
while (cp)
{
if (static_cast<int>(vals.size()) >= MAX_WORDS)
{
safe_str(S_("#-1 LIST TOO LONG"), buff, bufc);
return;
}
UTF8 *curr = split_token(&cp, sep);
if (!is_integer(curr, nullptr))
{
safe_str(S_("#-1 ARGUMENTS MUST BE INTEGERS"), buff, bufc);
return;
}
vals.push_back(mux_atoi64(curr));
}
const int n = static_cast<int>(vals.size());
if (n == 0)
{
safe_chr('0', buff, bufc);
return;
}
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
const UTF8 *op = fargs[0];
if ( mux_stricmp(op, T("add")) == 0
|| mux_stricmp(op, T("sum")) == 0)
{
// #1861: wrap via i64Add (see timeutil.h / #1472).
//
int64_t sum = 0;
for (int i = 0; i < n; i++)
{
sum = i64Add(sum, vals[i]);
}
safe_i64toa(sum, buff, bufc);
}
else if (mux_stricmp(op, T("mul")) == 0)
{
int64_t prod = 1;
for (int i = 0; i < n; i++)
{
prod = i64Mul(prod, vals[i]);
}
safe_i64toa(prod, buff, bufc);
}
else if (mux_stricmp(op, T("sub")) == 0)
{
int64_t result = vals[0];
for (int i = 1; i < n; i++)
{
result = i64Sub(result, vals[i]);
}
safe_i64toa(result, buff, bufc);
}
else if (mux_stricmp(op, T("div")) == 0)
{
int64_t result = vals[0];
for (int i = 1; i < n; i++)
{
if (vals[i] == 0)
{
safe_str(S_("#-1 DIVIDE BY ZERO"), buff, bufc);
return;
}
result = i64Division(result, vals[i]);
}
safe_i64toa(result, buff, bufc);
}
else if (mux_stricmp(op, T("mod")) == 0)
{
int64_t result = vals[0];
for (int i = 1; i < n; i++)
{
if (vals[i] == 0)
{
safe_str(S_("#-1 DIVIDE BY ZERO"), buff, bufc);
return;
}
result = i64Mod(result, vals[i]);
}
safe_i64toa(result, buff, bufc);
}
else if (mux_stricmp(op, T("min")) == 0)
{
int64_t minimum = vals[0];
for (int i = 1; i < n; i++)
{
if (vals[i] < minimum)
{
minimum = vals[i];
}
}
safe_i64toa(minimum, buff, bufc);
}
else if (mux_stricmp(op, T("max")) == 0)
{
int64_t maximum = vals[0];
for (int i = 1; i < n; i++)
{
if (vals[i] > maximum)
{
maximum = vals[i];
}
}
safe_i64toa(maximum, buff, bufc);
}
else if (mux_stricmp(op, T("median")) == 0)
{
// #1119: O(n log n) sort — insertion sort was O(n²) up to MAX_WORDS.
//
if (alarm_clock.alarmed)
{
safe_str(S_("#-1 CPU LIMITED"), buff, bufc);
return;
}
std::sort(vals.begin(), vals.end());
if (n % 2 == 1)
{
safe_i64toa(vals[n / 2], buff, bufc);
}
else
{
// Integer median of even-length list: floor of average.
// a + (b - a) / 2 with defined wrap (#1861) so INT64_MIN/MAX
// pairs cannot invoke signed overflow.
//
int64_t a = vals[n / 2 - 1];
int64_t b = vals[n / 2];
safe_i64toa(i64Add(a, i64Division(i64Sub(b, a), 2)), buff, bufc);
}
}
else
{
safe_str(S_("#-1 UNKNOWN OPERATION"), buff, bufc);
}
}
/* ---------------------------------------------------------------------------
* fun_sign: Returns -1, 0, or 1 based on the the sign of its argument.
*/
FUNCTION(fun_sign)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double num = mux_atof(fargs[0]);
if (num < 0)
{
safe_str(T("-1"), buff, bufc);
}
else
{
safe_bool(num > 0, buff, bufc);
}
}
// fun_isign: Returns -1, 0, or 1 based on the the sign of its argument.
//
FUNCTION(fun_isign)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
int64_t num = mux_atoi64(fargs[0]);
if (num < 0)
{
safe_str(T("-1"), buff, bufc);
}
else
{
safe_bool(num > 0, buff, bufc);
}
}
// shl() and shr() borrowed from PennMUSH 1.50
//
FUNCTION(fun_shl)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
2018-10-03 17:54:51 +00:00
if ( is_integer(fargs[0], nullptr)
&& is_integer(fargs[1], nullptr))
{
// #1109: shift count must be in [0, 63] for int64_t — larger is UB.
//
int64_t b = mux_atoi64(fargs[1]);
if (0 <= b && b < 64)
2010-06-19 11:37:23 -07:00
{
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
// #1472: bounding the count is necessary but not sufficient.
// Left-shifting a negative value is undefined however small the
// count, and so is shifting a bit into or past the sign bit --
// shl(-2,1), shl(1,63) and shl(3,63) all reach it. Shift in
// uint64_t, where the operation is defined as the modular one
// this function already documents, and convert back. Every
// result is bit-for-bit what the wrap produced before.
//
int64_t a = mux_atoi64(fargs[0]);
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
uint64_t ua = static_cast<uint64_t>(a) << b;
safe_i64toa(static_cast<int64_t>(ua), buff, bufc);
2010-06-19 11:37:23 -07:00
}
else if (b < 0)
2010-06-19 11:37:23 -07:00
{
// Keep historical wording for smoke tests (0 is allowed).
safe_str(S_("#-1 SECOND ARGUMENT MUST BE A POSITIVE NUMBER"), buff, bufc);
2010-06-19 11:37:23 -07:00
}
else
{
safe_str(S_("#-1 SECOND ARGUMENT MUST BE LESS THAN 64"), buff, bufc);
}
}
else
{
safe_str(S_("#-1 ARGUMENTS MUST BE INTEGERS"), buff, bufc);
}
}
FUNCTION(fun_shr)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
2018-10-03 17:54:51 +00:00
if ( is_integer(fargs[0], nullptr)
&& is_integer(fargs[1], nullptr))
{
// #1109: shift count must be in [0, 63] for int64_t — larger is UB.
//
int64_t b = mux_atoi64(fargs[1]);
if (0 <= b && b < 64)
2010-06-19 11:37:23 -07:00
{
int64_t a = mux_atoi64(fargs[0]);
2010-06-19 11:37:23 -07:00
safe_i64toa(a >> b, buff, bufc);
}
else if (b < 0)
2010-06-19 11:37:23 -07:00
{
safe_str(S_("#-1 SECOND ARGUMENT MUST BE A POSITIVE NUMBER"), buff, bufc);
2010-06-19 11:37:23 -07:00
}
else
{
safe_str(S_("#-1 SECOND ARGUMENT MUST BE LESS THAN 64"), buff, bufc);
}
}
else
{
safe_str(S_("#-1 ARGUMENTS MUST BE INTEGERS"), buff, bufc);
}
}
FUNCTION(fun_inc)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (nfargs == 1)
{
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
// #1472: wraps at INT64_MAX, which is signed overflow. Add in
// uint64_t for the same wrap without the undefined behaviour.
//
uint64_t v = static_cast<uint64_t>(mux_atoi64(fargs[0]));
safe_i64toa(static_cast<int64_t>(v + 1), buff, bufc);
}
else
{
safe_chr('1', buff, bufc);
}
}
FUNCTION(fun_dec)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (nfargs == 1)
{
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
// #1472: wraps at INT64_MIN, which is signed overflow. Subtract in
// uint64_t for the same wrap without the undefined behaviour.
//
uint64_t v = static_cast<uint64_t>(mux_atoi64(fargs[0]));
safe_i64toa(static_cast<int64_t>(v - 1), buff, bufc);
}
else
{
safe_str(T("-1"), buff, bufc);
}
}
FUNCTION(fun_trunc)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double rArg = mux_atof(fargs[0]);
double rIntegerPart;
mux_FPRestore();
(void)modf(rArg, &rIntegerPart);
mux_FPSet();
#ifdef HAVE_IEEE_FP_FORMAT
int fpc = mux_fpclass(rIntegerPart);
if (MUX_FPGROUP(fpc) == MUX_FPGROUP_PASS)
{
#endif // HAVE_IEEE_FP_FORMAT
fval(buff, bufc, rIntegerPart);
#ifdef HAVE_IEEE_FP_FORMAT
}
else
{
safe_str(mux_FPStrings[MUX_FPCLASS(fpc)], buff, bufc);
}
#endif // HAVE_IEEE_FP_FORMAT
}
FUNCTION(fun_fdiv)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double bot = mux_atof(fargs[1]);
double top = mux_atof(fargs[0]);
#ifndef HAVE_IEEE_FP_SNAN
if (bot == 0.0)
{
if (top > 0.0)
{
safe_str(T("+Inf"), buff, bufc);
}
else if (top < 0.0)
{
safe_str(T("-Inf"), buff, bufc);
}
else
{
safe_str(T("Ind"), buff, bufc);
}
}
else
{
fval(buff, bufc, top/bot);
}
#else
fval(buff, bufc, top/bot);
#endif
}
FUNCTION(fun_idiv)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
int64_t bot, top;
bot = mux_atoi64(fargs[1]);
if (bot == 0)
{
safe_str(S_("#-1 DIVIDE BY ZERO"), buff, bufc);
}
else
{
top = mux_atoi64(fargs[0]);
top = i64Division(top, bot);
safe_i64toa(top, buff, bufc);
}
}
FUNCTION(fun_floordiv)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
int64_t bot, top;
bot = mux_atoi64(fargs[1]);
if (bot == 0)
{
safe_str(S_("#-1 DIVIDE BY ZERO"), buff, bufc);
}
else
{
top = mux_atoi64(fargs[0]);
top = i64FloorDivision(top, bot);
safe_i64toa(top, buff, bufc);
}
}
FUNCTION(fun_mod)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
int64_t bot, top;
bot = mux_atoi64(fargs[1]);
if (bot == 0)
{
bot = 1;
}
top = mux_atoi64(fargs[0]);
top = i64Mod(top, bot);
safe_i64toa(top, buff, bufc);
}
FUNCTION(fun_remainder)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
int64_t bot, top;
bot = mux_atoi64(fargs[1]);
if (bot == 0)
{
bot = 1;
}
top = mux_atoi64(fargs[0]);
top = i64Remainder(top, bot);
safe_i64toa(top, buff, bufc);
}
/* ---------------------------------------------------------------------------
* fun_abs: Returns the absolute value of its argument.
*/
FUNCTION(fun_abs)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// #1255: |INT64_MIN| is 2**63. mux_atof can take the literal, but
// fval formats whole numbers near that magnitude as int64 and wraps
// back to a negative string — abs() returning a negative. Reject
// the exact integer domain like iabs() (#1114): error beats silent
// nonsense. Non-integer strings still take the float path.
//
int nDigits = 0;
if ( is_integer(fargs[0], &nDigits)
&& 0 < nDigits
&& mux_atoi64(fargs[0]) == INT64_MIN)
{
safe_range(buff, bufc);
return;
}
double num = mux_atof(fargs[0]);
if (0.0 == num)
{
safe_chr('0', buff, bufc);
}
else if (num < 0.0)
{
fval(buff, bufc, -num);
}
else
{
fval(buff, bufc, num);
}
}
// fun_iabs: Returns the absolute value of its argument.
//
FUNCTION(fun_iabs)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
int64_t num = mux_atoi64(fargs[0]);
if (num == 0)
{
safe_chr('0', buff, bufc);
}
else if (num == INT64_MIN)
{
// #1114: |INT64_MIN| is 2**63, which int64_t cannot represent —
// negating it is UB. Reject rather than hand back the magnitude
// as a string: "9223372036854775808" is not a valid int64, and
// every consumer corrupts it. Measured on this tree, feeding it
// to an integer-path function re-parses through mux_atoi64 (which
// wraps rather than saturates) straight back to INT64_MIN —
// idiv(iabs(-9223372036854775808),1) and shl(...,0) both yield
// -9223372036854775808 — while the float path loses precision
// instead (add(...,0) -> 9223372036854769664). Either way iabs()'s
// one invariant is silently broken. Failing loudly matches how the
// integer family handles an out-of-domain argument (fun_table,
// fun_columns).
//
safe_range(buff, bufc);
}
else if (num < 0)
{
safe_i64toa(-num, buff, bufc);
}
else
{
safe_i64toa(num, buff, bufc);
}
}
FUNCTION(fun_dist2d)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double a, b, d;
double sum;
a = mux_atof(fargs[0]);
b = mux_atof(fargs[2]);
d = a - b;
sum = d * d;
a = mux_atof(fargs[1]);
b = mux_atof(fargs[3]);
d = a - b;
sum += d * d;
mux_FPRestore();
double result = sqrt(sum);
mux_FPSet();
fval(buff, bufc, result);
}
FUNCTION(fun_dist3d)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double a, b, d;
double sum;
a = mux_atof(fargs[0]);
b = mux_atof(fargs[3]);
d = a - b;
sum = d * d;
a = mux_atof(fargs[1]);
b = mux_atof(fargs[4]);
d = a - b;
sum += d * d;
a = mux_atof(fargs[2]);
b = mux_atof(fargs[5]);
d = a - b;
sum += d * d;
mux_FPRestore();
double result = sqrt(sum);
mux_FPSet();
fval(buff, bufc, result);
}
//------------------------------------------------------------------------
// Vector functions: VADD, VSUB, VMUL, VCROSS, VMAG, VUNIT, VDIM
// Vectors are space-separated numbers.
//
#define VADD_F 0
#define VSUB_F 1
#define VMUL_F 2
#define VDOT_F 3
#define VCROSS_F 4
static void handle_vectors
(
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
const UTF8 *vecarg1, const UTF8 *vecarg2, UTF8 *buff, UTF8 **bufc,
2025-03-24 14:53:29 -06:00
const SEP &sep, const SEP &osep, int flag
)
{
// Return if the list is empty.
//
if (!vecarg1 || !*vecarg1 || !vecarg2 || !*vecarg2)
{
return;
}
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
// Uninitialized on purpose (#2145): list2arr writes arr[i] only for
// i < its return value and nothing reads past it, so value-initializing
// 2x 128 KB of pointers per call bought nothing — and was ~170x the
// cost of actually splitting a small vector.
//
std::unique_ptr<UTF8*[]> v1(new UTF8*[(LBUF_SIZE+1)/2]);
std::unique_ptr<UTF8*[]> v2(new UTF8*[(LBUF_SIZE+1)/2]);
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
// Split the lists up, or return if a list is empty. Non-destructive
// (#2136): vecarg1/vecarg2 are the caller's fargs, borrowed memory.
//
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
LBuf sc1 = LBuf_Src("handle_vectors.1");
LBuf sc2 = LBuf_Src("handle_vectors.2");
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
int n = list2arr_nd(v1.get(), (LBUF_SIZE+1)/2, vecarg1, sep, sc1);
int m = list2arr_nd(v2.get(), (LBUF_SIZE+1)/2, vecarg2, sep, sc2);
// vmul() and vadd() accepts a scalar in the first or second arg,
// but everything else has to be same-dimensional.
//
if ( n != m
&& !( ( flag == VMUL_F
|| flag == VADD_F
|| flag == VSUB_F)
&& ( n == 1
|| m == 1)))
{
safe_str(S_("#-1 VECTORS MUST BE SAME DIMENSIONS"), buff, bufc);
return;
}
double scalar;
int i;
switch (flag)
{
case VADD_F:
// If n or m is 1, this is scalar addition.
// otherwise, add element-wise.
//
if (n == 1)
{
scalar = mux_atof(v1[0]);
for (i = 0; i < m; i++)
{
if (i != 0)
{
print_sep(osep, buff, bufc);
}
fval(buff, bufc, mux_atof(v2[i]) + scalar);
}
n = m;
}
else if (m == 1)
{
scalar = mux_atof(v2[0]);
for (i = 0; i < n; i++)
{
if (i != 0)
{
print_sep(osep, buff, bufc);
}
fval(buff, bufc, mux_atof(v1[i]) + scalar);
}
}
else
{
for (i = 0; i < n; i++)
{
if (i != 0)
{
print_sep(osep, buff, bufc);
}
double a = mux_atof(v1[i]);
double b = mux_atof(v2[i]);
fval(buff, bufc, a + b);
}
}
break;
case VSUB_F:
if (n == 1)
{
// This is a scalar minus a vector.
//
scalar = mux_atof(v1[0]);
for (i = 0; i < m; i++)
{
if (i != 0)
{
print_sep(osep, buff, bufc);
}
fval(buff, bufc, scalar - mux_atof(v2[i]));
}
}
else if (m == 1)
{
// This is a vector minus a scalar.
//
scalar = mux_atof(v2[0]);
for (i = 0; i < n; i++)
{
if (i != 0)
{
print_sep(osep, buff, bufc);
}
fval(buff, bufc, mux_atof(v1[i]) - scalar);
}
}
else
{
// This is a vector minus a vector.
//
for (i = 0; i < n; i++)
{
if (i != 0)
{
print_sep(osep, buff, bufc);
}
double a = mux_atof(v1[i]);
double b = mux_atof(v2[i]);
fval(buff, bufc, a - b);
}
}
break;
case VMUL_F:
// If n or m is 1, this is scalar multiplication.
// otherwise, multiply elementwise.
//
if (n == 1)
{
scalar = mux_atof(v1[0]);
for (i = 0; i < m; i++)
{
if (i != 0)
{
print_sep(osep, buff, bufc);
}
fval(buff, bufc, mux_atof(v2[i]) * scalar);
}
}
else if (m == 1)
{
scalar = mux_atof(v2[0]);
for (i = 0; i < n; i++)
{
if (i != 0)
{
print_sep(osep, buff, bufc);
}
fval(buff, bufc, mux_atof(v1[i]) * scalar);
}
}
else
{
// Vector element-wise product.
//
for (i = 0; i < n; i++)
{
if (i != 0)
{
print_sep(osep, buff, bufc);
}
double a = mux_atof(v1[i]);
double b = mux_atof(v2[i]);
fval(buff, bufc, a * b);
}
}
break;
case VDOT_F:
scalar = 0.0;
for (i = 0; i < n; i++)
{
double a = mux_atof(v1[i]);
double b = mux_atof(v2[i]);
scalar += a * b;
}
fval(buff, bufc, scalar);
break;
case VCROSS_F:
// cross product: (a,b,c) x (d,e,f) = (bf - ce, cd - af, ae - bd)
//
// Or in other words:
//
// | a b c |
// det | d e f | = i(bf-ce) + j(cd-af) + k(ae-bd)
// | i j k |
//
// where i, j, and k are unit vectors in the x, y, and z
// cartisian coordinate space and are understood when expressed
// in vector form.
//
if (n != 3)
{
safe_str(S_("#-1 VECTORS MUST BE DIMENSION OF 3"), buff, bufc);
}
else
{
double a[2][3];
for (i = 0; i < 3; i++)
{
a[0][i] = mux_atof(v1[i]);
a[1][i] = mux_atof(v2[i]);
}
fval(buff, bufc, (a[0][1] * a[1][2]) - (a[0][2] * a[1][1]));
print_sep(osep, buff, bufc);
fval(buff, bufc, (a[0][2] * a[1][0]) - (a[0][0] * a[1][2]));
print_sep(osep, buff, bufc);
fval(buff, bufc, (a[0][0] * a[1][1]) - (a[0][1] * a[1][0]));
}
break;
default:
// If we reached this, we're in trouble.
//
safe_str(S_("#-1 UNIMPLEMENTED"), buff, bufc);
}
}
FUNCTION(fun_vadd)
{
SEP sep;
if (!OPTIONAL_DELIM(3, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
SEP osep = sep;
if (!OPTIONAL_DELIM(4, osep, DELIM_NULL|DELIM_CRLF|DELIM_STRING|DELIM_INIT))
{
return;
}
handle_vectors(fargs[0], fargs[1], buff, bufc, sep, osep, VADD_F);
}
FUNCTION(fun_vsub)
{
SEP sep;
if (!OPTIONAL_DELIM(3, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
SEP osep = sep;
if (!OPTIONAL_DELIM(4, osep, DELIM_NULL|DELIM_CRLF|DELIM_STRING|DELIM_INIT))
{
return;
}
handle_vectors(fargs[0], fargs[1], buff, bufc, sep, osep, VSUB_F);
}
FUNCTION(fun_vmul)
{
SEP sep;
if (!OPTIONAL_DELIM(3, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
SEP osep = sep;
if (!OPTIONAL_DELIM(4, osep, DELIM_NULL|DELIM_CRLF|DELIM_STRING|DELIM_INIT))
{
return;
}
handle_vectors(fargs[0], fargs[1], buff, bufc, sep, osep, VMUL_F);
}
FUNCTION(fun_vdot)
{
// dot product: (a,b,c) . (d,e,f) = ad + be + cf
//
SEP sep;
if (!OPTIONAL_DELIM(3, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
SEP osep = sep;
if (!OPTIONAL_DELIM(4, osep, DELIM_NULL|DELIM_CRLF|DELIM_STRING|DELIM_INIT))
{
return;
}
handle_vectors(fargs[0], fargs[1], buff, bufc, sep, osep, VDOT_F);
}
FUNCTION(fun_vcross)
{
// cross product: (a,b,c) x (d,e,f) = (bf - ce, cd - af, ae - bd)
//
SEP sep;
if (!OPTIONAL_DELIM(3, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
SEP osep = sep;
if (!OPTIONAL_DELIM(4, osep, DELIM_NULL|DELIM_CRLF|DELIM_STRING|DELIM_INIT))
{
return;
}
handle_vectors(fargs[0], fargs[1], buff, bufc, sep, osep, VCROSS_F);
}
FUNCTION(fun_vmag)
{
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
// Split the list up, or return if the list is empty.
//
if (!fargs[0] || !*fargs[0])
{
return;
}
std::unique_ptr<UTF8*[]> v1(new UTF8*[LBUF_SIZE/2]); // uninit (#2145): past-count reads are UB now, not nullptr
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
LBuf sc = LBuf_Src("fun_vmag.nd");
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
int n = list2arr_nd(v1.get(), LBUF_SIZE/2, fargs[0], sep, sc);
// Calculate the magnitude.
//
double res = 0.0;
for (int i = 0; i < n; i++)
{
double tmp = mux_atof(v1[i]);
res += tmp * tmp;
}
if (res > 0)
{
mux_FPRestore();
double result = sqrt(res);
mux_FPSet();
fval(buff, bufc, result);
}
else
{
safe_chr('0', buff, bufc);
}
}
FUNCTION(fun_vunit)
{
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
// Split the list up, or return if the list is empty.
//
if (!fargs[0] || !*fargs[0])
{
return;
}
std::unique_ptr<UTF8*[]> v1(new UTF8*[LBUF_SIZE/2]); // uninit (#2145): past-count reads are UB now, not nullptr
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
LBuf sc = LBuf_Src("fun_vunit.nd");
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
int n = list2arr_nd(v1.get(), LBUF_SIZE/2, fargs[0], sep, sc);
// Calculate the magnitude.
//
int i;
double res = 0.0;
for (i = 0; i < n; i++)
{
double tmp = mux_atof(v1[i]);
res += tmp * tmp;
}
if (res <= 0)
{
safe_str(S_("#-1 CANNOT MAKE UNIT VECTOR FROM ZERO-LENGTH VECTOR"),
buff, bufc);
return;
}
for (i = 0; i < n; i++)
{
if (0 != i)
{
print_sep(sep, buff, bufc);
}
mux_FPRestore();
double result = sqrt(res);
mux_FPSet();
fval(buff, bufc, mux_atof(v1[i]) / result);
}
}
FUNCTION(fun_floor)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
mux_FPRestore();
double r = floor(mux_atof(fargs[0]));
mux_FPSet();
#ifdef HAVE_IEEE_FP_FORMAT
int fpc = mux_fpclass(r);
if (MUX_FPGROUP(fpc) == MUX_FPGROUP_PASS)
{
#endif // HAVE_IEEE_FP_FORMAT
fval(buff, bufc, r);
#ifdef HAVE_IEEE_FP_FORMAT
}
else
{
safe_str(mux_FPStrings[MUX_FPCLASS(fpc)], buff, bufc);
}
#endif // HAVE_IEEE_FP_FORMAT
}
FUNCTION(fun_ceil)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
mux_FPRestore();
double r = ceil(mux_atof(fargs[0]));
mux_FPSet();
#ifdef HAVE_IEEE_FP_FORMAT
int fpc = mux_fpclass(r);
if (MUX_FPGROUP(fpc) == MUX_FPGROUP_PASS)
{
#endif // HAVE_IEEE_FP_FORMAT
fval(buff, bufc, r);
#ifdef HAVE_IEEE_FP_FORMAT
}
else
{
safe_str(mux_FPStrings[MUX_FPCLASS(fpc)], buff, bufc);
}
#endif // HAVE_IEEE_FP_FORMAT
}
FUNCTION(fun_round)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double r = mux_atof(fargs[0]);
#ifdef HAVE_IEEE_FP_FORMAT
int fpc = mux_fpclass(r);
if ( MUX_FPGROUP(fpc) == MUX_FPGROUP_PASS
|| MUX_FPGROUP(fpc) == MUX_FPGROUP_ZERO)
{
if (MUX_FPGROUP(fpc) == MUX_FPGROUP_ZERO)
{
r = 0.0;
}
#endif // HAVE_IEEE_FP_FORMAT
int64_t frac = mux_atoi64(fargs[1]);
safe_str(mux_ftoa(r, true, frac), buff, bufc);
#ifdef HAVE_IEEE_FP_FORMAT
}
else
{
safe_str(mux_FPStrings[MUX_FPCLASS(fpc)], buff, bufc);
}
#endif // HAVE_IEEE_FP_FORMAT
}
FUNCTION(fun_pi)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(fargs);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
safe_str(T("3.141592653589793"), buff, bufc);
}
FUNCTION(fun_e)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(fargs);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
safe_str(T("2.718281828459045"), buff, bufc);
}
static double ConvertRDG2R(double d, const UTF8 *szUnits)
{
switch (szUnits[0])
{
case 'd':
case 'D':
// Degrees to Radians.
//
d *= 0.017453292519943295;
break;
case 'g':
case 'G':
// Gradians to Radians.
//
d *= 0.015707963267948967;
break;
}
return d;
}
static double ConvertR2RDG(double d, const UTF8 *szUnits)
{
switch (szUnits[0])
{
case 'd':
case 'D':
// Radians to Degrees.
//
d *= 57.29577951308232;
break;
case 'g':
case 'G':
// Radians to Gradians.
//
d *= 63.66197723675813;
break;
}
return d;
}
FUNCTION(fun_ctu)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double val = mux_atof(fargs[0]);
val = ConvertRDG2R(val, fargs[1]);
val = ConvertR2RDG(val, fargs[2]);
fval(buff, bufc, val);
}
FUNCTION(fun_sin)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double d = mux_atof(fargs[0]);
if (nfargs == 2)
{
d = ConvertRDG2R(d, fargs[1]);
}
mux_FPRestore();
d = sin(d);
mux_FPSet();
fval(buff, bufc, d);
}
FUNCTION(fun_cos)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double d = mux_atof(fargs[0]);
if (nfargs == 2)
{
d = ConvertRDG2R(d, fargs[1]);
}
mux_FPRestore();
d = cos(d);
mux_FPSet();
fval(buff, bufc, d);
}
FUNCTION(fun_tan)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double d = mux_atof(fargs[0]);
if (nfargs == 2)
{
d = ConvertRDG2R(d, fargs[1]);
}
mux_FPRestore();
d = tan(d);
mux_FPSet();
fval(buff, bufc, d);
}
FUNCTION(fun_asin)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double val = mux_atof(fargs[0]);
#ifndef HAVE_IEEE_FP_SNAN
if ((val < -1.0) || (val > 1.0))
{
safe_str(T("Ind"), buff, bufc);
return;
}
#endif
mux_FPRestore();
val = asin(val);
mux_FPSet();
if (nfargs == 2)
{
val = ConvertR2RDG(val, fargs[1]);
}
fval(buff, bufc, val);
}
FUNCTION(fun_acos)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double val = mux_atof(fargs[0]);
#ifndef HAVE_IEEE_FP_SNAN
if ((val < -1.0) || (val > 1.0))
{
safe_str(T("Ind"), buff, bufc);
return;
}
#endif
mux_FPRestore();
val = acos(val);
mux_FPSet();
if (nfargs == 2)
{
val = ConvertR2RDG(val, fargs[1]);
}
fval(buff, bufc, val);
}
FUNCTION(fun_atan)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double val = mux_atof(fargs[0]);
mux_FPRestore();
val = atan(val);
mux_FPSet();
if (nfargs == 2)
{
val = ConvertR2RDG(val, fargs[1]);
}
fval(buff, bufc, val);
}
2010-06-17 09:18:56 -07:00
FUNCTION(fun_atan2)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double val1 = mux_atof(fargs[0]);
double val2 = mux_atof(fargs[1]);
mux_FPRestore();
val1 = atan2(val1, val2);
mux_FPSet();
if (3 == nfargs)
{
val1 = ConvertR2RDG(val1, fargs[2]);
}
fval(buff, bufc, val1);
}
FUNCTION(fun_exp)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double val = mux_atof(fargs[0]);
mux_FPRestore();
val = exp(val);
mux_FPSet();
fval(buff, bufc, val);
}
FUNCTION(fun_power)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double val, val1, val2;
val1 = mux_atof(fargs[0]);
val2 = mux_atof(fargs[1]);
#ifndef HAVE_IEEE_FP_SNAN
if (val1 < 0.0)
{
safe_str(T("Ind"), buff, bufc);
}
else
{
mux_FPRestore();
val = pow(val1, val2);
mux_FPSet();
fval(buff, bufc, val);
}
#else
mux_FPRestore();
val = pow(val1, val2);
mux_FPSet();
fval(buff, bufc, val);
#endif
}
FUNCTION(fun_fmod)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double val, val1, val2;
val1 = mux_atof(fargs[0]);
val2 = mux_atof(fargs[1]);
#ifndef HAVE_IEEE_FP_SNAN
// The indeterminate case is a zero *divisor* (fmod(x,0) is NaN);
// fmod(0,y) is a valid 0. The guard checked val1 (the dividend),
// copy-pasted from fun_power's val1 sign check — so on a
// NO_IEEE_FP_SNAN lane fmod(0,3) wrongly returned Ind and fmod(x,0)
// fell through unguarded. Reported by ThresholdOps.
if (val2 == 0.0)
{
safe_str(T("Ind"), buff, bufc);
}
else
{
mux_FPRestore();
val = fmod(val1, val2);
mux_FPSet();
fval(buff, bufc, val);
}
#else
mux_FPRestore();
val = fmod(val1, val2);
mux_FPSet();
fval(buff, bufc, val);
#endif
}
FUNCTION(fun_ln)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double val;
val = mux_atof(fargs[0]);
#ifndef HAVE_IEEE_FP_SNAN
if (val < 0.0)
{
safe_str(T("Ind"), buff, bufc);
}
else if (val == 0.0)
{
safe_str(T("-Inf"), buff, bufc);
}
else
{
mux_FPRestore();
val = log(val);
mux_FPSet();
}
#else
mux_FPRestore();
val = log(val);
mux_FPSet();
#endif
fval(buff, bufc, val);
}
FUNCTION(fun_log)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
typedef enum
{
2011-11-04 17:30:43 -06:00
#ifdef HAVE_LOG2
kBinary,
#endif
kNatural,
kCommon,
kOther
} logarithm_base;
logarithm_base kBase;
double val = mux_atof(fargs[0]);
double base;
if (2 == nfargs)
{
int nDigits;
if ( is_integer(fargs[1], &nDigits)
2011-11-04 17:30:43 -06:00
&& nDigits <= 2)
{
int64_t iBase = mux_atoi64(fargs[1]);
if (10 == iBase)
{
kBase = kCommon;
}
2011-11-04 17:30:43 -06:00
#ifdef HAVE_LOG2
else if (2 == iBase)
{
kBase = kBinary;
}
#endif
else
{
kBase = kOther;
base = mux_atof(fargs[1]);
}
}
else if ( 'e' == fargs[1][0]
&& '\0' == fargs[1][1])
{
kBase = kNatural;
}
else
{
kBase = kOther;
base = mux_atof(fargs[1]);
}
}
else
{
kBase = kCommon;
}
if ( kOther == kBase
&& base <= 1)
{
safe_str(S_("#-1 BASE OUT OF RANGE"), buff, bufc);
return;
}
#ifndef HAVE_IEEE_FP_SNAN
if (val < 0.0)
{
safe_str(T("Ind"), buff, bufc);
return;
}
else if (0.0 == val)
{
safe_str(T("-Inf"), buff, bufc);
return;
}
else
#endif
{
mux_FPRestore();
if (kCommon == kBase)
{
val = log10(val);
}
else if (kNatural == kBase)
{
val = log(val);
}
2011-11-04 17:30:43 -06:00
#ifdef HAVE_LOG2
else if (kBinary == kBase)
{
val = log2(val);
}
#endif
else
{
val = log(val)/log(base);
}
mux_FPSet();
}
fval(buff, bufc, val);
}
FUNCTION(fun_sqrt)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
double val;
val = mux_atof(fargs[0]);
#ifndef HAVE_IEEE_FP_SNAN
if (val < 0.0)
{
safe_str(T("Ind"), buff, bufc);
}
else if (val == 0.0)
{
safe_chr('0', buff, bufc);
}
else
{
mux_FPRestore();
val = sqrt(val);
mux_FPSet();
}
#else
mux_FPRestore();
val = sqrt(val);
mux_FPSet();
#endif
fval(buff, bufc, val);
}
/* ---------------------------------------------------------------------------
* isnum: is the argument a number?
*/
FUNCTION(fun_isnum)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
safe_bool(is_real(fargs[0]), buff, bufc);
}
/* ---------------------------------------------------------------------------
* israt: is the argument an rational?
*/
FUNCTION(fun_israt)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
safe_bool(is_rational(fargs[0]), buff, bufc);
}
/* ---------------------------------------------------------------------------
* isint: is the argument an integer?
*/
FUNCTION(fun_isint)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
2018-10-03 17:54:51 +00:00
safe_bool(is_integer(fargs[0], nullptr), buff, bufc);
}
FUNCTION(fun_and)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
bool val = true;
for (int i = 0; i < nfargs && val; i++)
{
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
val = isTRUE(mux_atoi64(fargs[i]));
}
safe_bool(val, buff, bufc);
}
FUNCTION(fun_or)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
bool val = false;
for (int i = 0; i < nfargs && !val; i++)
{
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
val = isTRUE(mux_atoi64(fargs[i]));
}
safe_bool(val, buff, bufc);
}
FUNCTION(fun_andbool)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
bool val = true;
for (int i = 0; i < nfargs && val; i++)
{
val = xlate(fargs[i]);
}
safe_bool(val, buff, bufc);
}
FUNCTION(fun_orbool)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
bool val = false;
for (int i = 0; i < nfargs && !val; i++)
{
val = xlate(fargs[i]);
}
safe_bool(val, buff, bufc);
}
FUNCTION(fun_cand)
{
bool val = true;
LBuf temp = LBuf_Src("fun_cand");
for (int i = 0; i < nfargs && val && !alarm_clock.alarmed; i++)
{
UTF8 *bp = temp.get();
mux_exec(fargs[i], LBUF_SIZE-1, temp, &bp, executor, caller, enactor,
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
*bp = '\0';
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
val = isTRUE(mux_atoi64(temp));
}
safe_bool(val, buff, bufc);
}
FUNCTION(fun_cor)
{
bool val = false;
LBuf temp = LBuf_Src("fun_cor");
for (int i = 0; i < nfargs && !val && !alarm_clock.alarmed; i++)
{
UTF8 *bp = temp.get();
mux_exec(fargs[i], LBUF_SIZE-1, temp, &bp, executor, caller, enactor,
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
*bp = '\0';
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
val = isTRUE(mux_atoi64(temp));
}
safe_bool(val, buff, bufc);
}
// firstof(arg1, arg2, ...) — return the first true (non-zero) argument.
// Evaluates left-to-right, stops at first true. If none are true,
// returns the last evaluated value.
//
FUNCTION(fun_firstof)
{
if (0 == nfargs)
{
return;
}
LBuf temp = LBuf_Src("fun_firstof");
for (int i = 0; i < nfargs && !alarm_clock.alarmed; i++)
{
UTF8 *bp = temp.get();
mux_exec(fargs[i], LBUF_SIZE-1, temp, &bp, executor, caller, enactor,
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
*bp = '\0';
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
if (isTRUE(mux_atoi64(temp)) || i == nfargs - 1)
{
safe_str(temp, buff, bufc);
return;
}
}
}
// strfirstof(arg1, arg2, ...) — return the first non-empty string argument.
// Evaluates left-to-right, stops at first non-empty. If all are empty,
// returns empty string.
//
FUNCTION(fun_strfirstof)
{
if (0 == nfargs)
{
return;
}
LBuf temp = LBuf_Src("fun_strfirstof");
for (int i = 0; i < nfargs && !alarm_clock.alarmed; i++)
{
UTF8 *bp = temp.get();
mux_exec(fargs[i], LBUF_SIZE-1, temp, &bp, executor, caller, enactor,
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
*bp = '\0';
if ('\0' != temp[0] || i == nfargs - 1)
{
safe_str(temp, buff, bufc);
return;
}
}
}
// allof(arg1, arg2, ...[, osep]) — return all true (non-zero) arguments,
// separated by osep. Last arg is the output separator if nfargs >= 2.
//
FUNCTION(fun_allof)
{
if (0 == nfargs)
{
return;
}
// Last argument is the output separator.
//
int nArgs = nfargs;
LBuf osep = LBuf_Src("fun_allof");
osep[0] = ' ';
osep[1] = '\0';
size_t nOsep = 1;
if (nfargs >= 2)
{
UTF8 *sp = osep;
mux_exec(fargs[nfargs - 1], LBUF_SIZE-1, osep, &sp, executor, caller,
enactor, eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
*sp = '\0';
nOsep = sp - osep;
nArgs = nfargs - 1;
}
bool bFirst = true;
LBuf temp = LBuf_Src("fun_allof");
for (int i = 0; i < nArgs && !alarm_clock.alarmed; i++)
{
UTF8 *bp = temp.get();
mux_exec(fargs[i], LBUF_SIZE-1, temp, &bp, executor, caller, enactor,
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
*bp = '\0';
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
if (isTRUE(mux_atoi64(temp)))
{
if (!bFirst)
{
safe_copy_buf(osep, nOsep, buff, bufc);
}
safe_str(temp, buff, bufc);
bFirst = false;
}
}
}
// strallof(arg1, arg2, ...[, osep]) — return all non-empty string arguments,
// separated by osep. Last arg is the output separator if nfargs >= 2.
//
FUNCTION(fun_strallof)
{
if (0 == nfargs)
{
return;
}
int nArgs = nfargs;
LBuf osep = LBuf_Src("fun_strallof");
osep[0] = ' ';
osep[1] = '\0';
size_t nOsep = 1;
if (nfargs >= 2)
{
UTF8 *sp = osep;
mux_exec(fargs[nfargs - 1], LBUF_SIZE-1, osep, &sp, executor, caller,
enactor, eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
*sp = '\0';
nOsep = sp - osep;
nArgs = nfargs - 1;
}
bool bFirst = true;
LBuf temp = LBuf_Src("fun_strallof");
for (int i = 0; i < nArgs && !alarm_clock.alarmed; i++)
{
UTF8 *bp = temp.get();
mux_exec(fargs[i], LBUF_SIZE-1, temp, &bp, executor, caller, enactor,
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
*bp = '\0';
if ('\0' != temp[0])
{
if (!bFirst)
{
safe_copy_buf(osep, nOsep, buff, bufc);
}
safe_str(temp, buff, bufc);
bFirst = false;
}
}
}
FUNCTION(fun_candbool)
{
bool val = true;
LBuf temp = LBuf_Src("fun_candbool");
for (int i = 0; i < nfargs && val && !alarm_clock.alarmed; i++)
{
UTF8 *bp = temp.get();
mux_exec(fargs[i], LBUF_SIZE-1, temp, &bp, executor, caller, enactor,
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
*bp = '\0';
val = xlate(temp);
}
safe_bool(val, buff, bufc);
}
FUNCTION(fun_corbool)
{
bool val = false;
LBuf temp = LBuf_Src("fun_corbool");
for (int i = 0; i < nfargs && !val && !alarm_clock.alarmed; i++)
{
UTF8 *bp = temp.get();
mux_exec(fargs[i], LBUF_SIZE-1, temp, &bp, executor, caller, enactor,
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
*bp = '\0';
val = xlate(temp);
}
safe_bool(val, buff, bufc);
}
FUNCTION(fun_xor)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
bool val = false;
for (int i = 0; i < nfargs; i++)
{
// Test truthiness on the full 64-bit value, matching and()/or()/
// lxor(); narrowing to int drops the high bits of large operands.
//
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
bool tval = isTRUE(mux_atoi64(fargs[i]));
val = (val && !tval) || (!val && tval);
}
safe_bool(val, buff, bufc);
}
FUNCTION(fun_not)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
safe_bool(!xlate(fargs[0]), buff, bufc);
}
FUNCTION(fun_t)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if ( nfargs <= 0
|| fargs[0][0] == '\0')
{
safe_chr('0', buff, bufc);
}
else
{
safe_bool(xlate(fargs[0]), buff, bufc);
}
}
static const UTF8 *bigones[] =
{
T(""),
T("thousand"),
T("million"),
T("billion"),
T("trillion")
};
static const UTF8 *singles[] =
{
T(""),
T("one"),
T("two"),
T("three"),
T("four"),
T("five"),
T("six"),
T("seven"),
T("eight"),
T("nine")
};
static const UTF8 *teens[] =
{
T("ten"),
T("eleven"),
T("twelve"),
T("thirteen"),
T("fourteen"),
T("fifteen"),
T("sixteen"),
T("seventeen"),
T("eighteen"),
T("nineteen")
};
static const UTF8 *tens[] =
{
T(""),
T(""),
T("twenty"),
T("thirty"),
T("forty"),
T("fifty"),
T("sixty"),
T("seventy"),
T("eighty"),
T("ninety")
};
static const UTF8 *th_prefix[] =
{
T(""),
T("ten"),
T("hundred")
};
class CSpellNum
{
public:
void SpellNum(const UTF8 *p, UTF8 *buff_arg, UTF8 **bufc_arg);
private:
void TwoDigits(const UTF8 *p);
void ThreeDigits(const UTF8 *p, size_t iBigOne);
void ManyDigits(size_t n, const UTF8 *p, bool bHundreds);
void FractionalDigits(size_t n, const UTF8 *p);
void StartWord(void);
void AddWord(const UTF8 *p);
UTF8 *buff;
UTF8 **bufc;
bool bNeedSpace;
};
void CSpellNum::StartWord(void)
{
if (bNeedSpace)
{
safe_chr(' ', buff, bufc);
}
bNeedSpace = true;
}
void CSpellNum::AddWord(const UTF8 *p)
{
safe_str(p, buff, bufc);
}
// Handle two-character sequences.
//
void CSpellNum::TwoDigits(const UTF8 *p)
{
int n0 = p[0] - '0';
int n1 = p[1] - '0';
if (n0 == 0)
{
if (n1 != 0)
{
StartWord();
AddWord(singles[n1]);
}
return;
}
else if (n0 == 1)
{
StartWord();
AddWord(teens[n1]);
return;
}
if (n1 == 0)
{
StartWord();
AddWord(tens[n0]);
}
else
{
StartWord();
AddWord(tens[n0]);
AddWord(T("-"));
AddWord(singles[n1]);
}
}
// Handle three-character sequences.
//
void CSpellNum::ThreeDigits(const UTF8 *p, size_t iBigOne)
{
if ( p[0] == '0'
&& p[1] == '0'
&& p[2] == '0')
{
return;
}
// Handle hundreds.
//
if (p[0] != '0')
{
StartWord();
AddWord(singles[p[0]-'0']);
StartWord();
AddWord(T("hundred"));
}
TwoDigits(p+1);
if (iBigOne > 0)
{
StartWord();
AddWord(bigones[iBigOne]);
}
}
// Handle a series of patterns of three.
//
void CSpellNum::ManyDigits(size_t n, const UTF8 *p, bool bHundreds)
{
// Handle special Hundreds cases.
//
if ( bHundreds
&& n == 4
&& p[1] != '0')
{
TwoDigits(p);
StartWord();
AddWord(T("hundred"));
TwoDigits(p+2);
return;
}
// Handle normal cases.
//
size_t ndiv = ((n + 2) / 3) - 1;
size_t nrem = n % 3;
UTF8 buf[3];
if (nrem == 0)
{
nrem = 3;
}
size_t j = nrem;
for (int i = 2; 0 <= i; i--)
{
if (j)
{
j--;
buf[i] = p[j];
}
else
{
buf[i] = '0';
}
}
ThreeDigits(buf, ndiv);
p += nrem;
while (ndiv-- > 0)
{
ThreeDigits(p, ndiv);
p += 3;
}
}
// Handle precision ending for part to the right of the decimal place.
//
void CSpellNum::FractionalDigits(size_t n, const UTF8 *p)
{
ManyDigits(n, p, false);
if ( 0 < n
&& n < 15)
{
size_t d = n / 3;
size_t r = n % 3;
StartWord();
if (r != 0)
{
AddWord(th_prefix[r]);
if (d != 0)
{
AddWord(T("-"));
}
}
AddWord(bigones[d]);
AddWord(T("th"));
int64_t i64 = mux_atoi64(p);
if (i64 != 1)
{
AddWord(T("s"));
}
}
}
void CSpellNum::SpellNum(const UTF8 *number, UTF8 *buff_arg, UTF8 **bufc_arg)
{
buff = buff_arg;
bufc = bufc_arg;
bNeedSpace = false;
// Trim Spaces from beginning.
//
while (mux_isspace(*number))
{
number++;
}
if (*number == '-')
{
StartWord();
AddWord(T("negative"));
number++;
}
// Trim Zeroes from Beginning.
//
while (*number == '0')
{
number++;
}
const UTF8 *pA = number;
while (mux_isdigit(*number))
{
number++;
}
size_t nA = number - pA;
2018-10-03 17:54:51 +00:00
const UTF8 *pB = nullptr;
size_t nB = 0;
if (*number == '.')
{
number++;
pB = number;
while (mux_isdigit(*number))
{
number++;
}
nB = number - pB;
}
// Skip trailing spaces.
//
while (mux_isspace(*number))
{
number++;
}
if ( *number
|| nA >= 16
|| nB >= 15)
{
safe_str(S_("#-1 ARGUMENT MUST BE A NUMBER"), buff, bufc);
return;
}
if (nA == 0)
{
if (nB == 0)
{
StartWord();
AddWord(T("zero"));
}
}
else
{
ManyDigits(nA, pA, true);
if (nB)
{
StartWord();
AddWord(T("and"));
}
}
if (nB)
{
FractionalDigits(nB, pB);
}
}
FUNCTION(fun_spellnum)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
CSpellNum sn;
sn.SpellNum(fargs[0], buff, bufc);
}
FUNCTION(fun_roman)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
const UTF8 *number = fargs[0];
// Trim Spaces from beginning.
//
while (mux_isspace(*number))
{
number++;
}
// Trim Zeroes from Beginning.
//
while (*number == '0')
{
number++;
}
const UTF8 *pA = number;
while (mux_isdigit(*number))
{
number++;
}
size_t nA = number - pA;
// Skip trailing spaces.
//
while (mux_isspace(*number))
{
number++;
}
// Validate that argument is numeric with a value between 1 and 3999.
//
if (*number || nA < 1)
{
safe_str(S_("#-1 ARGUMENT MUST BE A POSITIVE NUMBER"), buff, bufc);
return;
}
else if ( nA > 4
|| ( nA == 1
&& pA[0] == '0')
|| ( nA == 4
&& '3' < pA[0]))
{
safe_range(buff, bufc);
return;
}
// I:1, V:5, X:10, L:50, C:100, D:500, M:1000
//
// Ones: _ I II III IV V VI VII VIII IX
// Tens: _ X XX XXX XL L LX LXX LXXX XC
// Hundreds: _ C CC CCC CD D DC DCC DCCC CM
// Thousands: _ M MM MMM
//
static const UTF8 aLetters[4][3] =
{
{ 'I', 'V', 'X' },
{ 'X', 'L', 'C' },
{ 'C', 'D', 'M' },
{ 'M', ' ', ' ' }
};
static const UTF8 *aCode[10] =
{
T(""),
T("1"),
T("11"),
T("111"),
T("12"),
T("2"),
T("21"),
T("211"),
T("2111"),
T("13")
};
while (nA--)
{
const UTF8 *pCode = aCode[*pA - '0'];
const UTF8 *pLetters = aLetters[nA];
while (*pCode)
{
safe_chr(pLetters[*pCode - '1'], buff, bufc);
pCode++;
}
pA++;
}
}
/*-------------------------------------------------------------------------
* List-based numeric functions.
*/
FUNCTION(fun_land)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
bool bValue = true;
if (0 < nfargs)
{
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
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
LBuf scList = LBuf_Src("fun_lbool.list");
UTF8 *cp = trim_space_sep(list_copy_for_split(scList, fargs[0]), sep);
while (cp && bValue)
{
UTF8 *curr = split_token(&cp, sep);
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
bValue = isTRUE(mux_atoi64(curr));
}
}
safe_bool(bValue, buff, bufc);
}
FUNCTION(fun_lor)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
bool bValue = false;
if (0 < nfargs)
{
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
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
LBuf scList = LBuf_Src("fun_lbool.list");
UTF8 *cp = trim_space_sep(list_copy_for_split(scList, fargs[0]), sep);
while (cp && !bValue)
{
UTF8 *curr = split_token(&cp, sep);
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
bValue = isTRUE(mux_atoi64(curr));
}
}
safe_bool(bValue, buff, bufc);
}
FUNCTION(fun_lxor)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
bool bValue = false;
if (0 < nfargs)
{
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
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
LBuf scList = LBuf_Src("fun_lbool.list");
UTF8 *cp = trim_space_sep(list_copy_for_split(scList, fargs[0]), sep);
while (cp)
{
UTF8 *curr = split_token(&cp, sep);
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
bool bCurr = isTRUE(mux_atoi64(curr));
bValue = (bValue && !bCurr) || (!bValue && bCurr);
}
}
safe_bool(bValue, buff, bufc);
}
FUNCTION(fun_lband)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
uint64_t val = UINT64_MAX;
if (0 < nfargs)
{
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
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
LBuf scList = LBuf_Src("fun_lbool.list");
UTF8 *cp = trim_space_sep(list_copy_for_split(scList, fargs[0]), sep);
while (cp)
{
UTF8 *curr = split_token(&cp, sep);
if (!is_integer(curr, nullptr))
{
safe_str(S_("#-1 ARGUMENTS MUST BE INTEGERS"), buff, bufc);
return;
}
val &= mux_atoi64(curr);
}
}
safe_i64toa(val, buff, bufc);
}
FUNCTION(fun_lbor)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
uint64_t val = 0;
if (0 < nfargs)
{
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
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
LBuf scList = LBuf_Src("fun_lbool.list");
UTF8 *cp = trim_space_sep(list_copy_for_split(scList, fargs[0]), sep);
while (cp)
{
UTF8 *curr = split_token(&cp, sep);
if (!is_integer(curr, nullptr))
{
safe_str(S_("#-1 ARGUMENTS MUST BE INTEGERS"), buff, bufc);
return;
}
val |= mux_atoi64(curr);
}
}
safe_i64toa(val, buff, bufc);
}
FUNCTION(fun_lbxor)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
uint64_t val = 0;
if (0 < nfargs)
{
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
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
LBuf scList = LBuf_Src("fun_lbool.list");
UTF8 *cp = trim_space_sep(list_copy_for_split(scList, fargs[0]), sep);
while (cp)
{
UTF8 *curr = split_token(&cp, sep);
if (!is_integer(curr, nullptr))
{
safe_str(S_("#-1 ARGUMENTS MUST BE INTEGERS"), buff, bufc);
return;
}
val ^= mux_atoi64(curr);
}
}
safe_i64toa(val, buff, bufc);
}
FUNCTION(fun_band)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
uint64_t val = UINT64_MAX;
for (int i = 0; i < nfargs; i++)
{
2018-10-03 17:54:51 +00:00
if (is_integer(fargs[i], nullptr))
{
val &= mux_atoi64(fargs[i]);
}
else
{
safe_str(S_("#-1 ARGUMENTS MUST BE INTEGERS"), buff, bufc);
return;
}
}
safe_i64toa(val, buff, bufc);
}
FUNCTION(fun_bor)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
uint64_t val = 0;
for (int i = 0; i < nfargs; i++)
{
2018-10-03 17:54:51 +00:00
if (is_integer(fargs[i], nullptr))
{
val |= mux_atoi64(fargs[i]);
}
else
{
safe_str(S_("#-1 ARGUMENTS MUST BE INTEGERS"), buff, bufc);
return;
}
}
safe_i64toa(val, buff, bufc);
}
FUNCTION(fun_bnand)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
2018-10-03 17:54:51 +00:00
if ( is_integer(fargs[0], nullptr)
&& is_integer(fargs[1], nullptr))
{
int64_t a = mux_atoi64(fargs[0]);
int64_t b = mux_atoi64(fargs[1]);
safe_i64toa(a & ~(b), buff, bufc);
}
else
{
safe_str(S_("#-1 ARGUMENTS MUST BE INTEGERS"), buff, bufc);
}
}
FUNCTION(fun_bxor)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
uint64_t val = 0;
for (int i = 0; i < nfargs; i++)
{
2018-10-03 17:54:51 +00:00
if (is_integer(fargs[i], nullptr))
{
val ^= mux_atoi64(fargs[i]);
}
else
{
safe_str(S_("#-1 ARGUMENTS MUST BE INTEGERS"), buff, bufc);
return;
}
}
safe_i64toa(val, buff, bufc);
}
FUNCTION(fun_crc32)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
uint32_t ulCRC32 = 0;
for (int i = 0; i < nfargs; i++)
{
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
size_t n = strlen(reinterpret_cast<const char *>(fargs[i]));
ulCRC32 = CRC32_ProcessBuffer(ulCRC32, fargs[i], n);
}
safe_i64toa(ulCRC32, buff, bufc);
}
void safe_hex(uint8_t md[], size_t len, bool bUpper, UTF8 *buff, UTF8 **bufc)
2010-01-14 22:32:26 -08:00
{
std::vector<UTF8> buf((len * 2) + 1);
2010-01-14 22:32:26 -08:00
int bufoffset = 0;
2012-02-13 22:19:24 -08:00
const UTF8 *Digits16 = bUpper ? Digits16U : Digits16L;
2010-01-14 22:32:26 -08:00
for (size_t i = 0; i < len; i++)
{
uint8_t c = md[i];
2012-02-13 22:19:24 -08:00
buf[bufoffset++] = Digits16[(c >> 4) & 0x0F];
buf[bufoffset++] = Digits16[(c ) & 0x0F];
2010-01-14 22:32:26 -08:00
}
buf[bufoffset] = '\0';
safe_str(buf.data(), buff, bufc);
2010-01-14 22:32:26 -08:00
}
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
void sha1_helper(int nfargs, const UTF8 * const fargs[], UTF8 *buff, UTF8 **bufc)
{
#ifdef UNIX_DIGEST
uint8_t md[EVP_MAX_MD_SIZE];
#else
uint8_t md[MUX_SHA1_DIGEST_LENGTH];
#endif
unsigned int len = 0;
std::vector<size_t> lens(nfargs);
for (int i = 0; i < nfargs; i++)
2010-01-14 22:32:26 -08:00
{
lens[i] = strlen(reinterpret_cast<const char *>(fargs[i]));
}
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
if (!mux_sha1_digest(fargs, lens.data(), nfargs, md, &len))
{
safe_str(S_("#-1 UNSUPPORTED"), buff, bufc);
return;
2010-01-14 22:32:26 -08:00
}
safe_hex(md, len, true, buff, bufc);
2010-01-14 22:32:26 -08:00
}
FUNCTION(fun_sha1)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
2010-01-14 22:32:26 -08:00
sha1_helper(nfargs, fargs, buff, bufc);
}
FUNCTION(fun_digest)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
#ifdef UNIX_DIGEST
EVP_MD_CTX *ctx;
#if HAVE_EVP_MD_CTX_NEW
ctx = EVP_MD_CTX_new();
#elif HAVE_EVP_MD_CTX_CREATE
ctx = EVP_MD_CTX_create();
#else
#error Need EVP_MD_CTX_new() or EVP_MD_CTX_create().
#endif
2010-01-14 22:32:26 -08:00
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
// Resolve the (user-supplied) digest name. On OpenSSL 3.0+ use the
// provider-native EVP_MD_fetch(): unlike the legacy EVP_get_digestbyname(),
// it resolves algorithm aliases such as "sha-1" deterministically from a
// cold process, with no dependence on an earlier digest call having lazily
// loaded the default provider (#1961). A fetched EVP_MD is a reference that
// must be freed; the legacy pointer is a static const that must not be.
#if OPENSSL_VERSION_NUMBER >= 0x30000000L && !defined(LIBRESSL_VERSION_NUMBER)
EVP_MD *mp = EVP_MD_fetch(nullptr, reinterpret_cast<const char *>(fargs[0]), nullptr);
#else
const EVP_MD *mp = EVP_get_digestbyname(reinterpret_cast<const char *>(fargs[0]));
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
#endif
2018-10-03 17:54:51 +00:00
if (nullptr == mp)
{
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
// Release the context allocated above before bailing (#1961 leak fix).
#if HAVE_EVP_MD_CTX_NEW
EVP_MD_CTX_free(ctx);
#elif HAVE_EVP_MD_CTX_CREATE
EVP_MD_CTX_destroy(ctx);
#endif
safe_str(S_("#-1 UNSUPPORTED DIGEST TYPE"), buff, bufc);
2010-01-14 22:32:26 -08:00
return;
}
EVP_DigestInit(ctx, mp);
2010-01-14 22:32:26 -08:00
int i;
for (i = 1; i < nfargs; i++)
{
EVP_DigestUpdate(ctx, fargs[i], strlen(reinterpret_cast<const char *>(fargs[i])));
2010-01-14 22:32:26 -08:00
}
2010-01-14 22:32:26 -08:00
unsigned int len = 0;
uint8_t md[EVP_MAX_MD_SIZE];
EVP_DigestFinal(ctx, md, &len);
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
#if OPENSSL_VERSION_NUMBER >= 0x30000000L && !defined(LIBRESSL_VERSION_NUMBER)
EVP_MD_free(mp);
#endif
#if HAVE_EVP_MD_CTX_NEW
EVP_MD_CTX_free(ctx);
#elif HAVE_EVP_MD_CTX_CREATE
EVP_MD_CTX_destroy(ctx);
#else
#error Need EVP_MD_CTX_new() or EVP_MD_CTX_create().
#endif
2012-02-13 22:19:24 -08:00
safe_hex(md, len, true, buff, bufc);
2010-01-14 22:32:26 -08:00
#else
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
// CNG backend (#1963): sha1/sha256/sha384/sha512/md5, resolved by
// mux_digest. Same #-1 on unknown names as the OpenSSL side.
uint8_t md[MUX_MAX_DIGEST_LENGTH];
unsigned int len = 0;
std::vector<size_t> lens(nfargs > 1 ? nfargs - 1 : 0);
for (int i = 1; i < nfargs; i++)
{
lens[i-1] = strlen(reinterpret_cast<const char *>(fargs[i]));
}
if (mux_digest(fargs[0], const_cast<const UTF8 **>(fargs + 1), lens.data(),
nfargs - 1, md, &len))
{
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
safe_hex(md, len, true, buff, bufc);
}
2010-01-14 22:32:26 -08:00
else
{
safe_str(S_("#-1 UNSUPPORTED DIGEST TYPE"), buff, bufc);
2010-01-14 22:32:26 -08:00
}
#endif // UNIX_DIGEST
}