File Coverage

LikeR.xs
Criterion Covered Total %
statement 5622 6264 89.7
branch 4350 6266 69.4
condition n/a
subroutine n/a
pod n/a
total 9972 12530 79.5


line stmt bran cond sub pod time code
1             #ifndef _GNU_SOURCE
2             #define _GNU_SOURCE // glibc / Linux
3             #endif
4             #ifndef __EXTENSIONS__
5             #define __EXTENSIONS__ 1 // Solaris/illumos: expose off64_t, sigjmp_buf under -std=c99
6             #endif
7             #define PERL_NO_GET_CONTEXT
8             #include "EXTERN.h"
9             #include "perl.h"
10             #include "XSUB.h"
11             #include "ppport.h"
12             #include
13             #include
14             #include
15             #include
16             #include
17             #include
18             #include // uint64_t — harmless if perl.h already pulled it in
19             /*
20             XS words:
21             SvROK = scalar value reference is OK
22             */
23             /* sample(): private splitmix64 PRNG
24              
25             sample() gets its own PRNG state, completely separate from Drand01.
26             That means generate_binomial(), ruif(), rbinom(), and every other caller
27             of Drand01() are unaffected — their streams are never advanced or reseeded
28             by anything sample() does.
29              
30             Seeding is lazy (first call) and reads from /dev/urandom; falls back to
31             time()^PID on systems without it. No aTHX needed: all calls are plain C.
32             PERL_NO_GET_CONTEXT is therefore not a concern here. */
33             static uint64_t sample__state = 0;
34              
35             PERL_STATIC_INLINE uint64_t
36             sample__mix64(void)
37             {
38             uint64_t z = (sample__state += UINT64_C(0x9e3779b97f4a7c15));
39             z = (z ^ (z >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
40             z = (z ^ (z >> 27)) * UINT64_C(0x94d049bb133111eb);
41             return z ^ (z >> 31);
42             }
43              
44             /* * Helper function to increment the count for a given SV.
45             * Skips NULL or Undefined values as requested. */
46 26           static void increment_count(pTHX_ HV* counts_hv, SV* val) {
47             /* Skip null pointers or undef (non-OK) values */
48 26 50         if (!val || !SvOK(val)) return;
    50          
49             STRLEN len;
50             // SvPV forces stringification (so numbers become string keys)
51 26           char*restrict str = SvPV(val, len);
52             // hv_fetch with lval=1 creates the key if it doesn't exist
53 26           SV**restrict svp = hv_fetch(counts_hv, str, len, 1);
54 26 50         if (svp) {
55 26 100         if (!SvOK(*svp)) {
56 17           sv_setuv(*svp, 1);// Initialize count to 1 as an Unsigned Value (UV)
57             } else {
58 9           sv_setuv(*svp, SvUV(*svp) + 1);// Increment existing Unsigned Value
59             }
60             }
61             }
62              
63             // Uniform integer in [0, upper) — rejection loop, no modulo bias
64             PERL_STATIC_INLINE size_t
65             sample__rand(size_t upper) {
66             const uint64_t u = (uint64_t)upper;
67             const uint64_t t = (uint64_t)(-(uint64_t)u) % u;
68             uint64_t r;
69             do { r = sample__mix64(); } while (r < t);
70             return (size_t)(r % u);
71             }
72             // end sample() private PRNG
73              
74             // Ensure Perl's PRNG is seeded, matching the lazy-evaluation of Perl's rand()
75             #define AUTO_SEED_PRNG() \
76             do { \
77             if (!PL_srand_called) { \
78             (void)seedDrand01((Rand_seed_t)Perl_seed(aTHX)); \
79             PL_srand_called = TRUE; \
80             } \
81             } while (0)
82              
83             // Helpers for Random Number Generation
84             #ifndef M_PI
85             #define M_PI 3.14159265358979323846
86             #endif
87             // C helper for EXACT Non-central T-distribution CDF via Numerical Integration.
88             // This perfectly replicates R's pt(..., ncp) exactness without requiring complex Beta functions.
89 229           static NV exact_pnt(NV t, NV df, NV ncp) {
90 229 50         if (df <= 0.0) return 0.0;
91 229           unsigned short int n_steps = 30000;
92 229           NV step = 1.0 / n_steps;
93 229           NV integral = 0.0, half_df = df / 2.0;
94 229           NV log_coef = log(2.0) + half_df * log(half_df) - lgamma(half_df);
95 229           NV root_half = 0.70710678118654752440; // 1 / sqrt(2)
96 6870000 100         for (unsigned short i = 1; i < n_steps; i++) {
97 6869771           NV u = i * step;
98 6869771           NV w = u / (1.0 - u);
99             // Scaled Chi-distribution log-density
100 6869771           NV log_M = log_coef + (df - 1.0) * log(w) - half_df * w * w;
101 6869771           NV M = exp(log_M);
102             // Exact Normal CDF using the C standard library's erfc function
103 6869771           NV z = t * w - ncp;
104 6869771           NV pnorm_val = 0.5 * erfc(-z * root_half);
105 6869771 100         NV weight = (i % 2 != 0) ? 4.0 : 2.0;
106 6869771           integral += weight * (pnorm_val * M / ((1.0 - u) * (1.0 - u)));
107             }
108 229           return integral * (step / 3.0);
109             }
110             // --- Math Helpers for P-values and Confidence Intervals ---
111              
112             // Ranking helper with tie adjustment (matches R's tie handling)
113             typedef struct { NV val; size_t idx; NV rank; } RankInfo;
114 75           static int compare_rank(const void *restrict a, const void *restrict b) {
115 75           NV diff = ((RankInfo*)a)->val - ((RankInfo*)b)->val;
116 75           return (diff > 0) - (diff < 0);
117             }
118              
119 75           static int compare_index(const void *restrict a, const void *restrict b) {
120 75           return ((RankInfo*)a)->idx - ((RankInfo*)b)->idx;
121             }
122              
123 6           static void compute_ranks(NV *restrict data, NV *restrict ranks, size_t n) {
124 6           RankInfo *restrict items = safemalloc(n * sizeof(RankInfo));
125 56 100         for (size_t i = 0; i < n; i++) {
126 50           items[i].val = data[i];
127 50           items[i].idx = i;
128             }
129 6           qsort(items, n, sizeof(RankInfo), compare_rank);
130             // Handle ties by averaging ranks
131 56 100         for (size_t i = 0; i < n; ) {
132 50           size_t j = i + 1;
133 50 100         while (j < n && items[j].val == items[i].val) j++;
    50          
134 50           NV avg_rank = (i + 1 + j) / 2.0;
135 100 100         for (size_t k = i; k < j; k++) items[k].rank = avg_rank;
136 50           i = j;
137             }
138 6           qsort(items, n, sizeof(RankInfo), compare_index);
139 56 100         for (size_t i = 0; i < n; i++) ranks[i] = items[i].rank;
140 6           Safefree(items);
141 6           }
142             // Generates a single binomial random variate.
143             //Uses the standard Bernoulli trial loop. Drand01() taps into Perl's PRNG.
144 20499           static size_t generate_binomial(pTHX_ const size_t size, const NV prob) {
145 20499 100         if (prob <= 0.0) return 0;
146 20399 100         if (prob >= 1.0) return size;
147              
148 20299           size_t successes = 0;
149 312290 100         for (size_t i = 0; i < size; i++) {
150 291991 100         if (Drand01() <= prob) successes++;
151             }
152 20299           return successes;
153             }
154              
155             #define FT_EPS 2.220446049250313e-16
156             #define FT_TOL 0.0001220703125 // .Machine$double.eps^0.25, R uniroot default
157              
158 546           static NV ft_lchoose(long n, long k) {
159 546 50         if (k < 0 || k > n || n < 0) return -INFINITY;
    50          
    50          
160 546           return lgamma((NV)n + 1) - lgamma((NV)k + 1) - lgamma((NV)(n - k) + 1);
161             }
162              
163             typedef struct {
164             long lo, hi, ns, m, n, k, x;
165             NV *restrict logdc; // central log hypergeometric density over the support
166             } ft_support;
167              
168 26           static int ft_init(ft_support *S, long a, long b, long c, long d) {
169 26           S->m = a + c; S->n = b + d; S->k = a + b; S->x = a;
170 26           S->lo = (S->k - S->n > 0) ? (S->k - S->n) : 0;
171 26           S->hi = (S->k < S->m) ? S->k : S->m;
172 26           S->ns = S->hi - S->lo + 1;
173 26 50         if (S->ns <= 0) { S->logdc = NULL; return 0; }
174 26 50         Newx(S->logdc, S->ns, NV);
175 208 100         for (long i = 0; i < S->ns; i++) {
176 182           long j = S->lo + i;
177 182           S->logdc[i] = ft_lchoose(S->m, j) + ft_lchoose(S->n, S->k - j)
178 182           - ft_lchoose(S->m + S->n, S->k);
179             }
180 26           return 1;
181             }
182 26           static void ft_free(ft_support *S) { Safefree(S->logdc); S->logdc = NULL; }
183              
184 233           static void ft_dnhyper(const ft_support *S, NV ncp, NV *out) {
185 233           NV lncp = log(ncp), mx = -INFINITY;
186 2015 100         for (long i = 0; i < S->ns; i++) {
187 1782           out[i] = S->logdc[i] + lncp * (NV)(S->lo + i);
188 1782 100         if (out[i] > mx) mx = out[i];
189             }
190 233           NV s = 0;
191 2015 100         for (long i = 0; i < S->ns; i++) { out[i] = exp(out[i] - mx); s += out[i]; }
192 2015 100         for (long i = 0; i < S->ns; i++) out[i] /= s;
193 233           }
194              
195 93           static NV ft_mnhyper(const ft_support *restrict S, NV ncp, NV *scratch) {
196 93 100         if (ncp == 0) return (NV)S->lo;
197 91 50         if (isinf(ncp)) return (NV)S->hi;
198 91           ft_dnhyper(S, ncp, scratch);
199 91           NV mu = 0;
200 770 100         for (long i = 0; i < S->ns; i++) mu += (NV)(S->lo + i) * scratch[i];
201 91           return mu;
202             }
203              
204             // upper != 0 => P(X >= q), upper == 0 => P(X <= q)
205 185           static NV ft_pnhyper(const ft_support *S, long q, NV ncp, int upper, NV *scratch) {
206 185 100         if (ncp == 1.0) {
207 42           NV s = 0;
208 344 100         for (long i = 0; i < S->ns; i++) {
209 302           long j = S->lo + i;
210 302 100         if (upper ? (j >= q) : (j <= q)) s += exp(S->logdc[i]);
    100          
211             }
212 42           return s;
213             }
214 143 100         if (ncp == 0.0) return upper ? (NV)(q <= S->lo) : (NV)(q >= S->lo);
    100          
    50          
    50          
215 133 50         if (isinf(ncp)) return upper ? (NV)(q <= S->hi) : (NV)(q >= S->hi);
    0          
    0          
    0          
216 133           ft_dnhyper(S, ncp, scratch);
217 133           NV s = 0;
218 1173 100         for (long i = 0; i < S->ns; i++) {
219 1040           long j = S->lo + i;
220 1040 100         if (upper ? (j >= q) : (j <= q)) s += scratch[i];
    100          
221             }
222 133           return s;
223             }
224              
225             /* R's src/library/stats/src/zeroin.c (Brent-Dekker) */
226             typedef NV (*ft_fn)(NV t, void *ctx);
227 29           static NV ft_zeroin(NV ax, NV bx, ft_fn f, void *ctx, NV tol, int maxit) {
228 29           NV a = ax, b = bx, fa = f(a, ctx), fb = f(b, ctx), c = a, fc = fa;
229 216 50         while (maxit-- > 0) {
230 216           NV prev = b - a;
231 216 100         if (fabs(fc) < fabs(fb)) { a = b; b = c; c = a; fa = fb; fb = fc; fc = fa; }
232 216           NV tol_act = 2 * FT_EPS * fabs(b) + tol / 2;
233 216           NV step = (c - b) / 2;
234 216 100         if (fabs(step) <= tol_act || fb == 0.0) return b;
    50          
235 187 50         if (fabs(prev) >= tol_act && fabs(fa) > fabs(fb)) {
    50          
236 187           NV cb = c - b, p, q;
237 187 100         if (a == c) { NV t1 = fb / fa; p = cb * t1; q = 1.0 - t1; }
238             else {
239 68           NV q0 = fa / fc, t1 = fb / fc, t2 = fb / fa;
240 68           p = t2 * (cb * q0 * (q0 - t1) - (b - a) * (t1 - 1.0));
241 68           q = (q0 - 1.0) * (t1 - 1.0) * (t2 - 1.0);
242             }
243 187 100         if (p > 0) q = -q; else p = -p;
244 187 100         if (p < 0.75 * cb * q - fabs(tol_act * q) / 2 && p < fabs(prev * q / 2)) step = p / q;
    100          
245             }
246 187 100         if (fabs(step) < tol_act) step = step > 0 ? tol_act : -tol_act;
    100          
247 187           a = b; fa = fb; b += step; fb = f(b, ctx);
248 187 100         if ((fb > 0) == (fc > 0)) { c = a; fc = fa; }
249             }
250 0           return b;
251             }
252              
253             typedef struct { const ft_support *S; NV target; NV *scratch; int mode; } ft_rc;
254             /* mode 0: mnhyper(t)-target 1: mnhyper(1/t)-target
255             mode 2: pnhyper(x,t,low)-tgt 3: pnhyper(x,1/t,low)-tgt
256             mode 4: pnhyper(x,t,up)-tgt 5: pnhyper(x,1/t,up)-tgt */
257 245           static NV ft_rootf(NV t, void *ctx) {
258 245           ft_rc *r = (ft_rc *)ctx; const ft_support *S = r->S;
259 245           switch (r->mode) {
260 18           case 0: return ft_mnhyper(S, t, r->scratch) - r->target;
261 65           case 1: return ft_mnhyper(S, 1.0 / t, r->scratch) - r->target;
262 31           case 2: return ft_pnhyper(S, S->x, t, 0, r->scratch) - r->target;
263 40           case 3: return ft_pnhyper(S, S->x, 1.0 / t, 0, r->scratch) - r->target;
264 62           case 4: return ft_pnhyper(S, S->x, t, 1, r->scratch) - r->target;
265 29           default:return ft_pnhyper(S, S->x, 1.0 / t, 1, r->scratch) - r->target;
266             }
267             }
268              
269 13           static NV exact_p_value(long a, long b, long c, long d, const char *alt) {
270             ft_support S;
271 13 50         if (!ft_init(&S, a, b, c, d)) return 1.0;
272 13 50         NV *restrict sc; Newx(sc, S.ns, NV);
273             NV p;
274 13 100         if (!strcmp(alt, "less")) p = ft_pnhyper(&S, S.x, 1.0, 0, sc);
275 11 100         else if (!strcmp(alt, "greater")) p = ft_pnhyper(&S, S.x, 1.0, 1, sc);
276             else {
277 9           ft_dnhyper(&S, 1.0, sc);
278 9           NV dx = sc[S.x - S.lo], relErr = 1 + 1e-7, s = 0;
279 72 100         for (long i = 0; i < S.ns; i++) if (sc[i] <= dx * relErr) s += sc[i];
    100          
280 9           p = s;
281             }
282 13 50         if (p < 0) p = 0; if (p > 1) p = 1;
    50          
283 13           Safefree(sc); ft_free(&S);
284 13           return p;
285             }
286              
287 13           static void calculate_exact_stats(long a, long b, long c, long d, NV conf,
288             const char *alt, NV *orp, NV *lop, NV *hip) {
289             ft_support S;
290 13 50         if (!ft_init(&S, a, b, c, d)) { *orp = NAN; *lop = NAN; *hip = NAN; return; }
291 13 50         NV *restrict sc; Newx(sc, S.ns, NV);
292 13           long x = S.x, lo = S.lo, hi = S.hi;
293              
294             // conditional MLE of the odds ratio
295             NV est;
296 13 100         if (x == lo) est = 0.0;
297 12 100         else if (x == hi) est = INFINITY;
298             else {
299 10           NV mu = ft_mnhyper(&S, 1.0, sc);
300 10           ft_rc r = { &S, (NV)x, sc, 0 };
301 10 100         if (mu > x) { r.mode = 0; est = ft_zeroin(0, 1, ft_rootf, &r, FT_TOL, 1000); }
302 8 50         else if (mu < x) { r.mode = 1; est = 1.0 / ft_zeroin(FT_EPS, 1, ft_rootf, &r, FT_TOL, 1000); }
303 0           else est = 1.0;
304             }
305 13           *orp = est;
306             // confidence interval via inversion of the noncentral hypergeometric
307             NV clo, chi;
308 13           ft_rc r = { &S, 0, sc, 0 };
309             #define FT_NCP_L(alpha, dst) do { \
310             if (x == lo) { dst = 0.0; } else { \
311             NV p = ft_pnhyper(&S, x, 1.0, 1, sc); \
312             if (p > (alpha)) { r.mode = 4; r.target = (alpha); dst = ft_zeroin(0, 1, ft_rootf, &r, FT_TOL, 1000); } \
313             else if (p < (alpha)) { r.mode = 5; r.target = (alpha); dst = 1.0 / ft_zeroin(FT_EPS, 1, ft_rootf, &r, FT_TOL, 1000); } \
314             else dst = 1.0; } } while (0)
315             #define FT_NCP_U(alpha, dst) do { \
316             if (x == hi) { dst = INFINITY; } else { \
317             NV p = ft_pnhyper(&S, x, 1.0, 0, sc); \
318             if (p < (alpha)) { r.mode = 2; r.target = (alpha); dst = ft_zeroin(0, 1, ft_rootf, &r, FT_TOL, 1000); } \
319             else if (p > (alpha)) { r.mode = 3; r.target = (alpha); dst = 1.0 / ft_zeroin(FT_EPS, 1, ft_rootf, &r, FT_TOL, 1000); } \
320             else dst = 1.0; } } while (0)
321              
322 13 100         if (!strcmp(alt, "less")) { clo = 0.0; FT_NCP_U(1 - conf, chi); }
    50          
    100          
    50          
323 11 100         else if (!strcmp(alt, "greater")) { FT_NCP_L(1 - conf, clo); chi = INFINITY; }
    50          
    50          
    0          
324 9 100         else { NV al = (1 - conf) / 2; FT_NCP_L(al, clo); FT_NCP_U(al, chi); }
    100          
    50          
    100          
    100          
    50          
325              
326 13           *lop = clo; *hip = chi;
327 13           Safefree(sc); ft_free(&S);
328             }
329              
330             // small helper: fetch a nonnegative integer cell from an SV, with validation
331 56           static long ft_cell(pTHX_ SV *sv, const char *what) {
332 56 50         if (!sv || !SvOK(sv)) croak("fisher_test: %s is undef", what);
    50          
333 56 50         if (!looks_like_number(sv)) croak("fisher_test: %s is not a number", what);
334 56           IV v = SvIV(sv);
335 56 100         if (v < 0) croak("fisher_test: %s must be nonnegative (got %" IVdf ")", what, v);
336 55           return (long)v;
337             }
338              
339             /*Helpers for lm Linear Regression: OLS Matrix Math & Formula Parsing
340             * -----------------------------------------------------------------------
341             Sweep operator for symmetric positive-definite matrices (e.g., XtX).
342             This gracefully handles collinearity by bypassing aliased columns.
343             Utilizes a relative tolerance check to prevent dropping micro-variance features.*/
344 70           static int sweep_matrix_ols(NV *restrict A, size_t n, bool *restrict aliased) {
345 70           int rank = 0;
346 70           NV *restrict orig_diag = (NV*)safemalloc(n * sizeof(NV));
347             // Save the original diagonal values to use as a baseline for relative variance
348 246 100         for (size_t k = 0; k < n; k++) {
349 176           aliased[k] = FALSE;
350 176           orig_diag[k] = A[k * n + k];
351             }
352 246 100         for (size_t k = 0; k < n; k++) {
353             // Check pivot for collinearity using a RELATIVE tolerance
354             // (Fallback to a tiny absolute tolerance of 1e-24 to catch literal zero vectors)
355 176 100         if (fabs(A[k * n + k]) <= 1e-10 * orig_diag[k] || fabs(A[k * n + k]) < 1e-24) {
    50          
356 1           aliased[k] = TRUE;
357             // Isolate this column so it doesn't affect the rest of the matrix
358 4 100         for (size_t i = 0; i < n; i++) {
359 3           A[k * n + i] = 0.0;
360 3           A[i * n + k] = 0.0;
361             }
362 1           continue;
363             }
364 175           rank++;
365 175           NV pivot = 1.0 / A[k * n + k];
366 175           A[k * n + k] = 1.0;
367 640 100         for (size_t j = 0; j < n; j++) A[k * n + j] *= pivot;
368 640 100         for (size_t i = 0; i < n; i++) {
369 465 100         if (i != k && A[i * n + k] != 0.0) {
    100          
370 284           NV factor = A[i * n + k];
371 284           A[i * n + k] = 0.0;
372 1090 100         for (size_t j = 0; j < n; j++) {
373 806           A[i * n + j] -= factor * A[k * n + j];
374             }
375             }
376             }
377             }
378 70           Safefree(orig_diag);
379 70           return rank;
380             }
381              
382             // Internal extractor resolving single data values. Returns NAN on missing or non-numeric.
383 1805           static NV get_data_value(pTHX_ HV *restrict data_hoa, HV **restrict row_hashes, unsigned int i, const char *restrict var) {
384 1805           SV **restrict val = NULL;
385 1805 100         if (row_hashes) {
386 1184           val = hv_fetch(row_hashes[i], var, strlen(var), 0);
387 1184 50         if (val && SvROK(*val) && SvTYPE(SvRV(*val)) == SVt_PVAV) {
    50          
    50          
388 1184           AV*restrict av = (AV*)SvRV(*val);
389 1184           val = av_fetch(av, 0, 0);
390             }
391 621 50         } else if (data_hoa) {
392 621           SV**restrict col = hv_fetch(data_hoa, var, strlen(var), 0);
393 621 50         if (col && SvROK(*col) && SvTYPE(SvRV(*col)) == SVt_PVAV) {
    50          
    50          
394 621           AV*restrict av = (AV*)SvRV(*col);
395 621           val = av_fetch(av, i, 0);
396             }
397             }
398 1805 50         if (val && SvOK(*val)) {
    100          
399 1802 100         if (looks_like_number(*val)) return SvNV(*val);
400 49           return NAN; // Catch strings like "blue"
401             }
402 3           return NAN; // Catch undef/missing keys
403             }
404              
405             // Helper: Get all available columns for the '.' operator expansion
406 9           static AV* get_all_columns(pTHX_ HV *restrict data_hoa, HV **restrict row_hashes, size_t n) {
407 9           AV *restrict cols = newAV();
408 9 50         if (data_hoa) {
409 9           hv_iterinit(data_hoa);
410             HE *restrict entry;
411 33 100         while ((entry = hv_iternext(data_hoa))) {
412 24           av_push(cols, newSVsv(hv_iterkeysv(entry)));
413             }
414 0 0         } else if (row_hashes && n > 0 && row_hashes[0]) {
    0          
    0          
415 0           hv_iterinit(row_hashes[0]);
416             HE *restrict entry;
417 0 0         while ((entry = hv_iternext(row_hashes[0]))) {
418 0           av_push(cols, newSVsv(hv_iterkeysv(entry)));
419             }
420             }
421 9           return cols;
422             }
423              
424             // Recursive formula resolver with tightened NaN and Null handling
425 1837           static NV evaluate_term(pTHX_ HV *restrict data_hoa, HV **restrict row_hashes, unsigned int i, const char *restrict term) {
426 1837 50         if (!term || term[0] == '\0') return NAN;
    50          
427              
428 1837           char *restrict term_cpy = savepv(term);
429 1837           char *restrict colon = strchr(term_cpy, ':');
430 1837 100         if (colon) {
431 32           *colon = '\0';
432 32           NV left = evaluate_term(aTHX_ data_hoa, row_hashes, i, term_cpy);
433 32           NV right = evaluate_term(aTHX_ data_hoa, row_hashes, i, colon + 1);
434 32           Safefree(term_cpy);
435 32 50         if (isnan(left) || isnan(right)) return NAN;
    50          
436 32           return left * right;
437             }
438 1805 50         if (strncmp(term_cpy, "I(", 2) == 0) {
439 0           char *restrict end = strrchr(term_cpy, ')');
440 0 0         if (end) *end = '\0';
441 0           char *restrict inner = term_cpy + 2;
442 0           char *restrict caret = strchr(inner, '^');
443 0           int power = 1;
444 0 0         if (caret) {
445 0           *caret = '\0';
446 0           power = atoi(caret + 1);
447             }
448 0           NV v = get_data_value(aTHX_ data_hoa, row_hashes, i, inner);
449 0           Safefree(term_cpy);
450              
451 0 0         if (isnan(v)) return NAN;
452 0 0         return power == 1 ? v : pow(v, power);
453             }
454 1805           NV result = get_data_value(aTHX_ data_hoa, row_hashes, i, term_cpy);
455 1805           Safefree(term_cpy);
456 1805           return result;
457             }
458              
459             // Helper to infer column type from its first valid element
460 58           static bool is_column_categorical(pTHX_ HV *restrict data_hoa, HV **restrict row_hashes, size_t n, const char *restrict var) {
461 90 100         for (size_t i = 0; i < n; i++) {
462 89           SV **restrict val = NULL;
463 89 100         if (row_hashes) {
464 55           val = hv_fetch(row_hashes[i], var, strlen(var), 0);
465 55 100         if (val && SvROK(*val) && SvTYPE(SvRV(*val)) == SVt_PVAV) {
    50          
    50          
466 23           AV*restrict av = (AV*)SvRV(*val);
467 23           val = av_fetch(av, 0, 0);
468             }
469 34 50         } else if (data_hoa) {
470 34           SV **restrict col = hv_fetch(data_hoa, var, strlen(var), 0);
471 34 50         if (col && SvROK(*col) && SvTYPE(SvRV(*col)) == SVt_PVAV) {
    50          
    50          
472 34           AV*restrict av = (AV*)SvRV(*col);
473 34           val = av_fetch(av, i, 0);
474             }
475             }
476 89 100         if (val && SvOK(*val)) {
    50          
477 57 100         if (looks_like_number(*val)) return FALSE; // First valid is number -> Numeric Column
478 10           return TRUE; // First valid is string -> Categorical Column
479             }
480             }
481 1           return FALSE;
482             }
483              
484             /* Internal extractor resolving single data string values using dynamic allocation. */
485 371           static char* get_data_string_alloc(pTHX_ HV *restrict data_hoa, HV **restrict row_hashes, size_t i, const char *restrict var) {
486 371           SV **restrict val = NULL;
487 371 50         if (row_hashes) {
488 0           val = hv_fetch(row_hashes[i], var, strlen(var), 0);
489 0 0         if (val && SvROK(*val) && SvTYPE(SvRV(*val)) == SVt_PVAV) {
    0          
    0          
490 0           AV*restrict av = (AV*)SvRV(*val);
491 0           val = av_fetch(av, 0, 0);
492             }
493 371 50         } else if (data_hoa) {
494 371           SV **restrict col = hv_fetch(data_hoa, var, strlen(var), 0);
495 371 50         if (col && SvROK(*col) && SvTYPE(SvRV(*col)) == SVt_PVAV) {
    50          
    50          
496 371           AV*restrict av = (AV*)SvRV(*col);
497 371           val = av_fetch(av, i, 0);
498             }
499             }
500 371 50         if (val && SvOK(*val)) {
    50          
501 371           return savepv(SvPV_nolen(*val)); /* Allocates and returns string */
502             }
503 0           return NULL;
504             }
505              
506             // Struct for sorting p-values while remembering their original index
507             typedef struct {
508             NV p;
509             size_t orig_idx;
510             } PVal;
511              
512             // Comparator for qsort
513 1519           static int cmp_pval(const void *restrict a, const void *restrict b) {
514 1519           NV diff = ((PVal*)a)->p - ((PVal*)b)->p;
515 1519 100         if (diff < 0) return -1;
516 812 50         if (diff > 0) return 1;
517             /* Stabilize sort by falling back to original index */
518 0           return ((PVal*)a)->orig_idx - ((PVal*)b)->orig_idx;
519             }
520             /* -----------------------------------------------------------------------
521             * Helpers for cor(): ranking (Spearman), Pearson r, Kendall tau-b
522             * ----------------------------------------------------------------------- */
523             /* Item used to sort values while remembering their original index,
524             * needed for average-rank tie-breaking in Spearman correlation. */
525             typedef struct {
526             NV val;
527             size_t idx;
528             } RankItem;
529              
530 57           static int cmp_rank_item(const void *restrict a, const void *restrict b) {
531 57           NV diff = ((RankItem*)a)->val - ((RankItem*)b)->val;
532 57 100         if (diff < 0) return -1;
533 4 100         if (diff > 0) return 1;
534 1           return 0;
535             }
536              
537             /* Compute 1-based average ranks with tie-breaking into out[].
538             * in[] is not modified. */
539 8           static void rank_data(const NV *restrict in, NV *restrict out, size_t n) {
540             RankItem *restrict ri;
541 8 50         Newx(ri, n, RankItem);
542 56 100         for (size_t i = 0; i < n; i++) { ri[i].val = in[i]; ri[i].idx = i; }
543 8           qsort(ri, n, sizeof(RankItem), cmp_rank_item);
544              
545 8           size_t i = 0;
546 55 100         while (i < n) {
547 47           size_t j = i;
548             /* Find the full extent of this tie group */
549 48 100         while (j + 1 < n && ri[j + 1].val == ri[j].val) j++;
    100          
550             /* All members get the average of ranks i+1 … j+1 (1-based) */
551 47           NV avg = (NV)(i + j) / 2.0 + 1.0;
552 95 100         for (size_t k = i; k <= j; k++) out[ri[k].idx] = avg;
553 47           i = j + 1;
554             }
555 8           Safefree(ri);
556 8           }
557              
558             /* Pearson product-moment r between two n-element arrays.
559             * Returns NAN when either variable has zero variance (matches R). */
560 61           static NV pearson_corr(const NV *restrict x, const NV *restrict y, size_t n) {
561 61           NV sx = 0, sy = 0, sxy = 0, sx2 = 0, sy2 = 0;
562 364 100         for (size_t i = 0; i < n; i++) {
563 303           sx += x[i]; sy += y[i];
564 303           sxy += x[i]*y[i]; sx2 += x[i]*x[i]; sy2 += y[i]*y[i];
565             }
566 61           NV num = (NV)n * sxy - sx * sy;
567 61           NV den = sqrt(((NV)n * sx2 - sx*sx) * ((NV)n * sy2 - sy*sy));
568 61 50         if (den == 0.0) return NAN;
569 61           return num / den;
570             }
571              
572             /* Kendall's tau-b between two n-element arrays.
573              
574             * tau-b = (C − D) / sqrt((C + D + T_x)(C + D + T_y))
575             *
576             * where C = concordant pairs, D = discordant, T_x = pairs tied only on
577             * x, T_y = pairs tied only on y. Joint ties (both zero) are excluded
578             * from numerator and denominator, matching R's cor(method="kendall").
579             * Returns NAN when the denominator is zero. */
580 1           static NV kendall_tau_b(const NV *restrict x, const NV *restrict y, unsigned int n) {
581 1           size_t C = 0, D = 0, tie_x = 0, tie_y = 0;
582 9 100         for (size_t i = 0; i < n - 1; i++) {
583 44 100         for (size_t j = i + 1; j < n; j++) {
584 36           int sx = (x[i] > x[j]) - (x[i] < x[j]); /* sign of x[i]-x[j] */
585 36           int sy = (y[i] > y[j]) - (y[i] < y[j]);
586 36 100         if (sx == 0 && sy == 0) { /* joint tie — not counted */ }
    50          
587 36 100         else if (sx == 0) tie_x++;
588 35 50         else if (sy == 0) tie_y++;
589 35 50         else if (sx == sy) C++;
590 0           else D++;
591             }
592             }
593 1           NV denom = sqrt((NV)(C + D + tie_x) * (NV)(C + D + tie_y));
594 1 50         if (denom == 0.0) return NAN;
595 1           return (NV)(C - D) / denom;
596             }
597              
598             /* Single dispatch: compute correlation according to method string.
599             * Allocates and frees temporary rank arrays internally for Spearman. */
600 62           static NV compute_cor(const NV *restrict x, const NV *restrict y,
601             size_t n, const char *restrict method) {
602 62 100         if (strcmp(method, "spearman") == 0) {
603             NV *restrict rx, *restrict ry;
604 3 50         Newx(rx, n, NV); Newx(ry, n, NV);
    50          
605 3           rank_data(x, rx, n);
606 3           rank_data(y, ry, n);
607 3           NV r = pearson_corr(rx, ry, n);
608 3           Safefree(rx); Safefree(ry);
609 3           return r;
610             }
611 59 100         if (strcmp(method, "kendall") == 0)
612 1           return kendall_tau_b(x, y, n);
613             /* default: pearson */
614 58           return pearson_corr(x, y, n);
615             }
616              
617             // Math macros
618             #define MAX_ITER 500
619             #define EPS 3.0e-15
620             #define FPMIN 1.0e-30
621              
622 8623           static NV _incbeta_cf(NV a, NV b, NV x) {
623             int m;
624             NV aa, c, d, del, h, qab, qam, qap;
625 8623           qab = a + b; qap = a + 1.0; qam = a - 1.0;
626 8623           c = 1.0; d = 1.0 - qab * x / qap;
627 8623 50         if (fabs(d) < FPMIN) d = FPMIN;
628 8623           d = 1.0 / d; h = d;
629 183197 50         for (m = 1; m <= MAX_ITER; m++) {
630 183197           int m2 = 2 * m;
631 183197           aa = m * (b - m) * x / ((qam + m2) * (a + m2));
632 183197           d = 1.0 + aa * d;
633 183197 50         if (fabs(d) < FPMIN) d = FPMIN;
634 183197           c = 1.0 + aa / c;
635 183197 50         if (fabs(c) < FPMIN) c = FPMIN;
636 183197           d = 1.0 / d; h *= d * c;
637 183197           aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
638 183197           d = 1.0 + aa * d;
639 183197 50         if (fabs(d) < FPMIN) d = FPMIN;
640 183197           c = 1.0 + aa / c;
641 183197 50         if (fabs(c) < FPMIN) c = FPMIN;
642 183197           d = 1.0 / d; del = d * c; h *= del;
643 183197 100         if (fabs(del - 1.0) < EPS) break;
644             }
645 8623           return h;
646             }
647              
648 8669           static NV incbeta(NV a, NV b, NV x) {
649 8669 100         if (x <= 0.0) return 0.0;
650 8664 100         if (x >= 1.0) return 1.0;
651 8623           NV bt = exp(lgamma(a + b) - lgamma(a) - lgamma(b) + a * log(x) + b * log(1.0 - x));
652 8623 100         if (x < (a + 1.0) / (a + b + 2.0)) return bt * _incbeta_cf(a, b, x) / a;
653 1590           return 1.0 - bt * _incbeta_cf(b, a, 1.0 - x) / b;
654             }
655              
656 8365           static NV get_t_pvalue(NV t, NV df, const char*restrict alt) {
657 8365           NV x = df / (df + t * t);
658 8365           NV prob_2tail = incbeta(df / 2.0, 0.5, x);
659 8365 100         if (strcmp(alt, "less") == 0) return (t < 0) ? 0.5 * prob_2tail : 1.0 - 0.5 * prob_2tail;
    100          
660 8363 100         if (strcmp(alt, "greater") == 0) return (t > 0) ? 0.5 * prob_2tail : 1.0 - 0.5 * prob_2tail;
    50          
661 115           return prob_2tail;
662             }
663              
664             // Bisection algorithm to find the inverse t-distribution (Critical t-value)
665 277           static NV qt_tail(NV df, NV p_tail) {
666 277           NV low = 0.0, high = 1.0;
667             // Find upper bound
668 661 100         while (get_t_pvalue(high, df, "greater") > p_tail) {
669 384           low = high;
670 384           high *= 2.0;
671 384 50         if (high > 1000000.0) break; /* Fallback limit */
672             }
673             // Bisect to find the root
674 7586 50         for (unsigned short int i = 0; i < 100; i++) {
675 7586           NV mid = (low + high) / 2.0;
676 7586           NV p_mid = get_t_pvalue(mid, df, "greater");
677 7586 100         if (p_mid > p_tail) {
678 3711           low = mid;
679             } else {
680 3875           high = mid;
681             }
682 7586 100         if (high - low < 1e-8) break;
683             }
684 277           return (low + high) / 2.0;
685             }
686              
687 2336           int compare_doubles(const void *restrict a, const void *restrict b) {
688 2336           NV da = *(const NV*restrict)a;
689 2336           NV db = *(const NV*restrict)b;
690 2336           return (da > db) - (da < db);
691             }
692             /* Helper to calculate the number of bins using Sturges' formula: log2(n) + 1 */
693 0           static size_t calculate_sturges_bins(size_t n) {
694 0 0         if (n == 0) return 1;
695 0           return (size_t)(log((NV)n) / log(2.0) + 1.0);
696             }
697              
698             // Logic for distributing data into bins (Optimized to O(N))
699 5           static void compute_hist_logic(NV *restrict x, size_t n, NV *restrict breaks, size_t n_bins,
700             size_t *restrict counts, NV *restrict mids, NV *restrict density) {
701 5           NV total_n = (NV)n;
702 5           NV min_val = breaks[0];
703 5 50         NV step = (n_bins > 0) ? (breaks[1] - breaks[0]) : 0.0;
704             // Initialize counts and compute midpoints
705 23 100         for (size_t i = 0; i < n_bins; i++) {
706 18           counts[i] = 0;
707 18           mids[i] = (breaks[i] + breaks[i+1]) / 2.0;
708             }
709             // Single O(N) pass to assign elements to bins
710 5 100         if (step > 0.0) {
711 2017 100         for (size_t j = 0; j < n; j++) {
712 2014           NV val = x[j];
713             // Ignore out-of-bounds or invalid values
714 2014 50         if (isnan(val) || isinf(val) || val < min_val) continue;
    50          
    50          
715             // Calculate initial bin index mathematically
716 2014           size_t idx = (size_t)((val - min_val) / step);
717             // Clamp to valid array bounds first to prevent overflow */
718 2014 100         if (idx >= n_bins) {
719 3           idx = n_bins - 1;
720             }
721             /* Adjust for exact boundaries (R's right-inclusive default: (a, b]) */
722             /* If value is exactly on or slightly below the lower boundary of the assigned bin,
723             it belongs in the previous bin. (First bin [a, b] is inclusive on both ends) */
724 2023 100         while (idx > 0 && val <= breaks[idx]) {
    100          
725 9           idx--;
726             }
727             // Conversely, if floating-point truncation placed it too low, push it up
728 2014 100         while (idx < n_bins - 1 && val > breaks[idx + 1]) {
    50          
729 0           idx++;
730             }
731 2014           counts[idx]++;
732             }
733 2 50         } else if (n_bins > 0) {
734             // Edge case: All data points have the exact same value (step == 0)
735 2           counts[0] = n;
736             }
737             // Compute densities
738 23 100         for (size_t i = 0; i < n_bins; i++) {
739 18           NV bin_width = breaks[i+1] - breaks[i];
740 18 100         if (bin_width > 0) {
741 16           density[i] = (NV)counts[i] / (total_n * bin_width);
742             } else {
743 2 50         density[i] = (n_bins == 1) ? 1.0 : 0.0;
744             }
745             }
746 5           }
747              
748             // Standard Normal CDF approximation
749 59           NV approx_pnorm(NV x) {
750 59           return 0.5 * erfc(-x * 0.70710678118654752440); // 0.707... = 1/sqrt(2)
751             }
752             #ifndef M_SQRT1_2
753             #define M_SQRT1_2 0.70710678118654752440
754             #endif
755              
756             /* Macro for exact Wilcoxon 3D array indexing */
757             #define DP_INDEX(i, j, k, n2, max_u) ((i) * ((n2) + 1) * ((max_u) + 1) + (j) * ((max_u) + 1) + (k))
758 30           static NV inverse_normal_cdf(NV p) {
759 30           NV a[4] = {2.50662823884, -18.61500062529, 41.39119773534, -25.44106049637};
760 30           NV b[4] = {-8.47351093090, 23.08336743743, -21.06224101826, 3.13082909833};
761 30           NV c[9] = {0.3374754822726147, 0.9761690190917186, 0.1607979714918209,
762             0.0276438810333863, 0.0038405729373609, 0.0003951896511919,
763             0.0000321767881768, 0.0000002888167364, 0.0000003960315187};
764             NV x, r, y;
765 30           y = p - 0.5;
766 30 100         if (fabs(y) < 0.42) {
767 22           r = y * y;
768 22           x = y * (((a[3]*r + a[2])*r + a[1])*r + a[0]) /
769 22           ((((b[3]*r + b[2])*r + b[1])*r + b[0])*r + 1.0);
770             } else {
771 8           r = p;
772 8 100         if (y > 0) r = 1.0 - p;
773 8           r = log(-log(r));
774 8           x = c[0] + r * (c[1] + r * (c[2] + r * (c[3] + r * (c[4] +
775 8           r * (c[5] + r * (c[6] + r * (c[7] + r * c[8])))))));
776 8 100         if (y < 0) x = -x;
777             }
778 30           return x;
779             }
780             /* -----------------------------------------------------------------------
781             * Exact Spearman p-value via exhaustive permutation enumeration.
782             *
783             * Under H0, all n! orderings of ranks are equally probable. We visit
784             * every permutation of {1..n} with Heap's algorithm (O(n!), no allocs
785             * inside the loop) and count how many yield S ≤ s_obs ("lower tail",
786             * i.e. rho ≥ rho_obs) and how many yield S ≥ s_obs ("upper tail").
787             *
788             * Mirrors R's default: exact = (n < 10) with no ties.
789             * Valid up to n = 9 (362 880 iterations — negligible cost).
790             * ----------------------------------------------------------------------- */
791 1           static NV spearman_exact_pvalue(NV s_obs, size_t n, const char *restrict alt) {
792 1           int *restrict perm = (int*)safemalloc(n * sizeof(int));
793 1           int *restrict c = (int*)safemalloc(n * sizeof(int));
794 6 100         for (size_t i = 0; i < n; i++) { perm[i] = i + 1; c[i] = 0; }
795              
796 1           long count_le = 0, count_ge = 0, total = 0;
797              
798             #define TALLY_PERM() do { \
799             NV s_ = 0.0; \
800             for (int ii = 0; ii < n; ii++) { \
801             NV d_ = (NV)(ii + 1) - (NV)perm[ii];\
802             s_ += d_ * d_; \
803             } \
804             if (s_ <= s_obs + 1e-9) count_le++; \
805             if (s_ >= s_obs - 1e-9) count_ge++; \
806             total++; \
807             } while (0)
808              
809 6 100         TALLY_PERM(); /* initial permutation [1, 2, ..., n] */
    50          
    50          
810              
811 1           unsigned int k = 1;
812 206 100         while (k < n) {
813 205 100         if (c[k] < k) {
814             int tmp;
815 119 100         if (k % 2 == 0) {
816 44           tmp = perm[0]; perm[0] = perm[k]; perm[k] = tmp;
817             } else {
818 75           tmp = perm[c[k]]; perm[c[k]] = perm[k]; perm[k] = tmp;
819             }
820 714 100         TALLY_PERM();
    100          
    100          
821 119           c[k]++;
822 119           k = 1;
823             } else {
824 86           c[k] = 0;
825 86           k++;
826             }
827             }
828             #undef TALLY_PERM
829 1           Safefree(perm); Safefree(c);
830             /* p_le = P(S ≤ s_obs) ≡ P(rho ≥ rho_obs) — upper rho tail
831             * p_ge = P(S ≥ s_obs) ≡ P(rho ≤ rho_obs) — lower rho tail */
832 1           NV p_le = (NV)count_le / (NV)total;
833 1           NV p_ge = (NV)count_ge / (NV)total;
834              
835 1 50         if (strcmp(alt, "greater") == 0) return p_le;
836 1 50         if (strcmp(alt, "less") == 0) return p_ge;
837             /* two.sided: 2 × the smaller tail, clamped to 1 */
838 1 50         NV p = 2.0 * (p_le < p_ge ? p_le : p_ge);
839 1 50         return (p > 1.0) ? 1.0 : p;
840             }
841             /* -----------------------------------------------------------------------
842             * Exact Kendall p-value via Mahonian Numbers (Inversions distribution)
843             * Matches R's behavior for N < 50 without ties.
844             * ----------------------------------------------------------------------- */
845 2           static NV kendall_exact_pvalue(size_t n, NV s_obs, const char *restrict alt) {
846 2           long max_inv = (long)n * (n - 1) / 2;
847 2           NV *restrict dp = (NV*)safemalloc((max_inv + 1) * sizeof(NV));
848 24 100         for (long i = 0; i <= max_inv; i++) dp[i] = 0.0;
849 2           dp[0] = 1.0;
850             /* Build the distribution of inversions via DP */
851 10 100         for (size_t i = 2; i <= n; i++) {
852 8           NV *restrict next_dp = (NV*)safemalloc((max_inv + 1) * sizeof(NV));
853 96 100         for (long k = 0; k <= max_inv; k++) next_dp[k] = 0.0;
854 8           int current_max_inv = i * (i - 1) / 2;
855 56 100         for (int k = 0; k <= current_max_inv; k++) {
856 48           NV sum = 0;
857 206 100         for (int j = 0; j <= i - 1 && k - j >= 0; j++) {
    100          
858 158           sum += dp[k - j];
859             }
860             // Divide by 'i' directly to keep array as pure probabilities and prevent overflow
861 48           next_dp[k] = sum / (NV)i;
862             }
863 8           Safefree(dp);
864 8           dp = next_dp;
865             }
866             // Convert S statistic to target number of inversions
867 2           long i_obs = (long)round((max_inv - s_obs) / 2.0);
868 2 50         if (i_obs < 0) i_obs = 0;
869 2 50         if (i_obs > max_inv) i_obs = max_inv;
870 2           NV p_le = 0.0; /* P(S <= S_obs) */
871 20 100         for (long k = i_obs; k <= max_inv; k++) p_le += dp[k];
872 2           NV p_ge = 0.0; /* P(S >= S_obs) */
873 8 100         for (long k = 0; k <= i_obs; k++) p_ge += dp[k];
874 2           Safefree(dp);
875 2 50         if (strcmp(alt, "greater") == 0) return p_ge;
876 2 100         if (strcmp(alt, "less") == 0) return p_le;
877             // two.sided
878 1 50         NV p = 2.0 * (p_ge < p_le ? p_ge : p_le);
879 1 50         return p > 1.0 ? 1.0 : p;
880             }
881             // F-distribution Cumulative Distribution Function P(F <= f)
882 304           static NV pf(NV f, NV df1, NV df2) {
883 304 50         if (f <= 0.0) return 0.0;
884 304           NV x = (df1 * f) / (df1 * f + df2);
885 304           return incbeta(df1 / 2.0, df2 / 2.0, x);
886             }
887              
888             /* Householder QR Decomposition for Sequential Sums of Squares */
889             /* Householder QR Decomposition for Sequential Sums of Squares */
890 7           static void apply_householder_aov(NV** restrict X, NV* restrict y, size_t n, size_t p, bool* restrict aliased, size_t* restrict rank_map) {
891 7           size_t r = 0; // Rank/Row tracker
892 27 100         for (size_t k = 0; k < p; k++) {
893 20           aliased[k] = FALSE;
894 20 50         if (r >= n) {
895 0           aliased[k] = TRUE;
896 0           continue;
897             }
898              
899 20           NV max_val = 0;
900 188 100         for (size_t i = r; i < n; i++) {
901 168 100         if (fabs(X[i][k]) > max_val) max_val = fabs(X[i][k]);
902             }
903 20 100         if (max_val < 1e-10) {
904 1           aliased[k] = TRUE;
905 1           continue;
906             } // Collinear or zero column
907              
908 19           NV norm = 0;
909 184 100         for (size_t i = r; i < n; i++) {
910 165           X[i][k] /= max_val;
911 165           norm += X[i][k] * X[i][k];
912             }
913 19           norm = sqrt(norm);
914 19 100         NV s = (X[r][k] > 0) ? -norm : norm;
915 19           NV u1 = X[r][k] - s;
916 19           X[r][k] = s * max_val;
917              
918 39 100         for (size_t j = k + 1; j < p; j++) {
919 20           NV dot = u1 * X[r][j];
920 202 100         for (size_t i = r + 1; i < n; i++) dot += X[i][j] * X[i][k];
921 20           NV tau = dot / (s * u1);
922 20           X[r][j] += tau * u1;
923 202 100         for (size_t i = r + 1; i < n; i++) X[i][j] += tau * X[i][k];
924             }
925              
926             // Transform the response vector y
927 19           NV dot_y = u1 * y[r];
928 165 100         for (size_t i = r + 1; i < n; i++) dot_y += y[i] * X[i][k];
929 19           NV tau_y = dot_y / (s * u1);
930 19           y[r] += tau_y * u1;
931 165 100         for (size_t i = r + 1; i < n; i++) y[i] += tau_y * X[i][k];
932              
933 19           rank_map[k] = r; // Map original column index to orthogonal row index
934 19           r++;
935             }
936 7           }
937              
938             // --- write_table Helpers ---
939              
940             // Sorts string arrays alphabetically
941 2           static int cmp_string_wt(const void *a, const void *b) {
942 2           return strcmp(*(const char**)a, *(const char**)b);
943             }
944              
945             // Emulates Perl's /\D/ check
946 21           static bool contains_nondigit(pTHX_ SV *restrict sv) {
947 21 50         if (!sv || !SvOK(sv)) return 0;
    50          
948             STRLEN len;
949 21           char *restrict s = SvPVbyte(sv, len);
950 40 100         for (size_t i = 0; i < len; i++) {
951 21 100         if (!isdigit(s[i])) return 1;
952             }
953 19           return 0;
954             }
955              
956             /* ---------------------------------------------------------------------------
957             * print_string_row: emit one record.
958             *
959             * Quoting contract (matches the behaviors your tests pin down):
960             * - A field is quoted IFF it contains the separator string, a double
961             * quote, or a newline / carriage return. Quoting is per-field, so in a
962             * TSV "hello,world" stays bare while "tab\tin" becomes "tab in".
963             * - Inside a quoted field, embedded double quotes are doubled:
964             * p"q -> "p""q" (RFC 4180 style)
965             * - A NULL or zero-length field prints as NOTHING between separators:
966             * a,,c -- never '' or "". A zero-length field cannot contain a
967             * separator, quote, or newline, so it never needs quoting.
968             * - The separator is treated as a string (strstr), so multi-character
969             * separators work; an empty separator never triggers quoting.
970             *
971             * Returns nothing; I/O errors surface on PerlIO_close at the call site.
972             */
973 163           static void print_string_row(pTHX_ PerlIO *restrict fh,
974             const char **restrict fields, size_t n, const char *restrict sep)
975             {
976 163 50         const size_t sep_len = sep ? strlen(sep) : 0;
977 140634 100         for (size_t i = 0; i < n; i++) {
978 140471 100         if (i && sep_len) PerlIO_write(fh, sep, sep_len);
    50          
979 140471           const char *restrict f = fields[i];
980 140471 50         if (!f || !*f) continue; /* undef/empty -> print nothing */
    100          
981             /* Does this field need quoting? */
982 140420           bool need_quotes = 0;
983 140420 100         if (strchr(f, '"') || strchr(f, '\n') || strchr(f, '\r')) {
    100          
    100          
984 10           need_quotes = 1;
985 140410 50         } else if (sep_len && strstr(f, sep)) {
    100          
986 9           need_quotes = 1;
987             }
988 140420 100         if (!need_quotes) {
989 140401           PerlIO_write(fh, f, strlen(f));
990             } else {
991 19           PerlIO_putc(fh, '"');
992 169 100         for (const char *restrict p = f; *p; p++) {
993 150 100         if (*p == '"') PerlIO_putc(fh, '"'); /* double it */
994 150           PerlIO_putc(fh, *p);
995             }
996 19           PerlIO_putc(fh, '"');
997             }
998             }
999 163           PerlIO_putc(fh, '\n');
1000 163           }
1001              
1002             // Calculates the Regularized Upper Incomplete Gamma Function Q(a, x)
1003             // This perfectly replicates R's pchisq(..., lower.tail=FALSE)
1004 11           NV igamc(NV a, NV x) {
1005 11 50         if (x < 0.0 || a <= 0.0) return 1.0;
    50          
1006 11 50         if (x == 0.0) return 1.0;
1007              
1008             // Series expansion for x < a + 1
1009 11 100         if (x < a + 1.0) {
1010 4           NV sum = 1.0 / a;
1011 4           NV term = 1.0 / a;
1012 4           NV n = 1.0;
1013 62 100         while (fabs(term) > 1e-15) {
1014 58           term *= x / (a + n);
1015 58           sum += term;
1016 58           n += 1.0;
1017             }
1018 4           return 1.0 - (sum * exp(-x + a * log(x) - lgamma(a)));
1019             }
1020              
1021             // Continued fraction for x >= a + 1
1022 7           NV b = x + 1.0 - a;
1023 7           NV c = 1.0 / 1e-30;
1024 7           NV d = 1.0 / b;
1025 7           NV h = d, i = 1.0;
1026 105 50         while (i < 10000) { // Safety bound
1027 105           NV an = -i * (i - a);
1028 105           b += 2.0;
1029 105           d = an * d + b;
1030 105 50         if (fabs(d) < 1e-30) d = 1e-30;
1031 105           c = b + an / c;
1032 105 50         if (fabs(c) < 1e-30) c = 1e-30;
1033 105           d = 1.0 / d;
1034 105           NV del = d * c;
1035 105           h *= del;
1036 105 100         if (fabs(del - 1.0) < 1e-15) break;
1037 98           i += 1.0;
1038             }
1039 7           return h * exp(-x + a * log(x) - lgamma(a));
1040             }
1041              
1042             // Chi-Squared p-value is simply the Incomplete Gamma of (df/2, stat/2)
1043 11           NV get_p_value(NV stat, int df) {
1044 11 50         if (df <= 0) return 1.0;
1045 11 50         if (stat <= 0.0) return 1.0;
1046 11           return igamc((NV)df / 2.0, stat / 2.0);
1047             }
1048              
1049             #ifndef M_SQRT1_2
1050             #define M_SQRT1_2 0.70710678118654752440
1051             #endif
1052              
1053             // Robust Binomial Coefficient using long double
1054 2           static long double choose_comb(int n, int k) {
1055 2 50         if (k < 0 || k > n) return 0.0L;
    50          
1056 2 50         if (k > n / 2) k = n - k;
1057 2           long double res = 1.0L;
1058 8 100         for (int i = 1; i <= k; i++) {
1059 6           res = res * (long double)(n - i + 1) / (long double)i;
1060             }
1061 2           return res;
1062             }
1063              
1064             /* Exact CDF for Mann-Whitney U: P(U <= q)
1065             Mathematically identical to R's cwilcox generating function */
1066 4           static NV exact_pwilcox(NV q, int m, int n) {
1067 4           int k = (int)floor(q + 1e-7); // R uses 1e-7 fuzz
1068 4           int max_u = m * n;
1069 4 100         if (k < 0) return 0.0;
1070 2 50         if (k >= max_u) return 1.0;
1071              
1072 2           long double *restrict w = (long double *)safecalloc(max_u + 1, sizeof(long double));
1073 2           w[0] = 1.0L;
1074              
1075 8 100         for (int j = 1; j <= n; j++) {
1076 54 100         for (int i = j; i <= max_u; i++) w[i] += w[i - j];
1077 36 100         for (int i = max_u; i >= j + m; i--) w[i] -= w[i - j - m];
1078             }
1079              
1080 2           long double cum_p = 0.0L;
1081 4 100         for (int i = 0; i <= k; i++) cum_p += w[i];
1082              
1083 2           long double total = choose_comb(m + n, n);
1084 2           NV result = (NV)(cum_p / total);
1085              
1086 2           Safefree(w);
1087 2           return result;
1088             }
1089              
1090             /* Exact CDF for Wilcoxon Signed Rank: P(V <= q)
1091             Mathematically identical to R's csignrank subset-sum DP */
1092 6           static NV exact_psignrank(NV q, int n) {
1093 6           int k = (int)floor(q + 1e-7);
1094 6           int max_v = n * (n + 1) / 2;
1095 6 50         if (k < 0) return 0.0;
1096 6 100         if (k >= max_v) return 1.0;
1097              
1098 5           long double *restrict w = (long double *)safecalloc(max_v + 1, sizeof(long double));
1099 5           w[0] = 1.0L;
1100              
1101 46 100         for (int i = 1; i <= n; i++) {
1102 1582 100         for (int j = max_v; j >= i; j--) w[j] += w[j - i];
1103             }
1104              
1105 5           long double cum_p = 0.0L;
1106 182 100         for (int i = 0; i <= k; i++) cum_p += w[i];
1107              
1108 5           long double total = powl(2.0L, (long double)n);
1109 5           NV result = (NV)(cum_p / total);
1110              
1111 5           Safefree(w);
1112 5           return result;
1113             }
1114              
1115 297           static int cmp_rank_info(const void *a, const void *b) {
1116 297           NV da = ((const RankInfo*)a)->val;
1117 297           NV db = ((const RankInfo*)b)->val;
1118 297           return (da > db) - (da < db);
1119             }
1120              
1121 11           static NV rank_and_count_ties(RankInfo *restrict ri, size_t n, bool *restrict has_ties) {
1122 11 50         if (n == 0) return 0.0;
1123 11           qsort(ri, n, sizeof(RankInfo), cmp_rank_info);
1124 11           size_t i = 0;
1125 11           NV tie_adj = 0.0;
1126 11           *has_ties = 0;
1127 124 100         while (i < n) {
1128 113           size_t j = i + 1;
1129 121 100         while (j < n && ri[j].val == ri[i].val) j++;
    100          
1130 113           NV r = (NV)(i + 1 + j) / 2.0;
1131 234 100         for (size_t k = i; k < j; k++) ri[k].rank = r;
1132 113           size_t t = j - i;
1133 113 100         if (t > 1) { *has_ties = 1; tie_adj += ((NV)t * t * t - t); }
1134 113           i = j;
1135             }
1136 11           return tie_adj;
1137             }
1138             /* --- KS-TEST C HELPER SECTION --- */
1139             #ifndef M_PI_2
1140             #define M_PI_2 1.57079632679489661923
1141             #endif
1142             #ifndef M_PI_4
1143             #define M_PI_4 0.78539816339744830962
1144             #endif
1145             #ifndef M_1_SQRT_2PI
1146             #define M_1_SQRT_2PI 0.39894228040143267794
1147             #endif
1148              
1149             // Scalar integer power used by K2x
1150 39           static NV r_pow_di(NV x, int n) {
1151 39 50         if (n == 0) return 1.0;
1152 39 50         if (n < 0) return 1.0 / r_pow_di(x, -n);
1153 39           NV val = 1.0;
1154 438 100         for (int i = 0; i < n; i++) val *= x;
1155 39           return val;
1156             }
1157              
1158             // Two-sample two-sided asymptotic distribution
1159 0           static NV K2l(NV x, int lower, NV tol) {
1160             NV s, z, p;
1161             int k;
1162 0 0         if(x <= 0.) {
1163 0 0         if(lower) p = 0.;
1164 0           else p = 1.;
1165 0 0         } else if(x < 1.) {
1166 0           int k_max = (int) sqrt(2.0 - log(tol));
1167 0           NV w = log(x);
1168 0           z = - (M_PI_2 * M_PI_4) / (x * x);
1169 0           s = 0;
1170 0 0         for(k = 1; k < k_max; k += 2) {
1171 0           s += exp(k * k * z - w);
1172             }
1173 0           p = s / M_1_SQRT_2PI;
1174 0 0         if(!lower) p = 1.0 - p;
1175             } else {
1176             NV new_val, old_val;
1177 0           z = -2.0 * x * x;
1178 0           s = -1.0;
1179 0 0         if(lower) {
1180 0           k = 1; old_val = 0.0; new_val = 1.0;
1181             } else {
1182 0           k = 2; old_val = 0.0; new_val = 2.0 * exp(z);
1183             }
1184 0 0         while(fabs(old_val - new_val) > tol) {
1185 0           old_val = new_val;
1186 0           new_val += 2.0 * s * exp(z * k * k);
1187 0           s *= -1.0;
1188 0           k++;
1189             }
1190 0           p = new_val;
1191             }
1192 0           return p;
1193             }
1194              
1195             // Auxiliary routines used by K2x() for matrix operations
1196 7           static void m_multiply(NV *A, NV *B, NV *C, unsigned int m) {
1197 140 100         for(unsigned int i = 0; i < m; i++) {
1198 2660 100         for(unsigned int j = 0; j < m; j++) {
1199 2527           NV s = 0.;
1200 50540 100         for(unsigned int k = 0; k < m; k++) s += A[i * m + k] * B[k * m + j];
1201 2527           C[i * m + j] = s;
1202             }
1203             }
1204 7           }
1205              
1206 6           static void m_power(NV *A, int eA, NV *V, int *eV, int m, int n) {
1207 6 100         if(n == 1) {
1208 362 100         for(int i = 0; i < m * m; i++) V[i] = A[i];
1209 1           *eV = eA;
1210 1           return;
1211             }
1212 5           m_power(A, eA, V, eV, m, n / 2);
1213 5           NV *restrict B = (NV*) safecalloc(m * m, sizeof(NV));
1214 5           m_multiply(V, V, B, m);
1215 5           int eB = 2 * (*eV);
1216 5 100         if((n % 2) == 0) {
1217 1086 100         for(int i = 0; i < m * m; i++) V[i] = B[i];
1218 3           *eV = eB;
1219             } else {
1220 2           m_multiply(A, B, V, m);
1221 2           *eV = eA + eB;
1222             }
1223 5 50         if(V[(m / 2) * m + (m / 2)] > 1e140) {
1224 0 0         for(int i = 0; i < m * m; i++) V[i] = V[i] * 1e-140;
1225 0           *eV += 140;
1226             }
1227 5           Safefree(B);
1228             }
1229              
1230             // One-sample two-sided exact distribution
1231 1           static NV K2x(int n, NV d) {
1232 1           int k = (int) (n * d) + 1;
1233 1           int m = 2 * k - 1;
1234 1           NV h = k - n * d;
1235 1           NV *restrict H = (NV*) safecalloc(m * m, sizeof(NV));
1236 1           NV *restrict Q = (NV*) safecalloc(m * m, sizeof(NV));
1237              
1238 20 100         for(int i = 0; i < m; i++) {
1239 380 100         for(int j = 0; j < m; j++) {
1240 361 100         if(i - j + 1 < 0) H[i * m + j] = 0;
1241 208           else H[i * m + j] = 1;
1242             }
1243             }
1244 20 100         for(int i = 0; i < m; i++) {
1245 19           H[i * m] -= r_pow_di(h, i + 1);
1246 19           H[(m - 1) * m + i] -= r_pow_di(h, (m - i));
1247             }
1248 1 50         H[(m - 1) * m] += ((2 * h - 1 > 0) ? r_pow_di(2 * h - 1, m) : 0);
1249              
1250 20 100         for(int i = 0; i < m; i++) {
1251 380 100         for(int j = 0; j < m; j++) {
1252 361 100         if(i - j + 1 > 0) {
1253 1520 100         for(int g = 1; g <= i - j + 1; g++) H[i * m + j] /= g;
1254             }
1255             }
1256             }
1257              
1258 1           int eH = 0, eQ;
1259 1           m_power(H, eH, Q, &eQ, m, n);
1260 1           NV s = Q[(k - 1) * m + k - 1];
1261              
1262 51 100         for(int i = 1; i <= n; i++) {
1263 50           s = s * (NV)i / (NV)n;
1264 50 50         if(s < 1e-140) {
1265 0           s *= 1e140;
1266 0           eQ -= 140;
1267             }
1268             }
1269 1           s *= pow(10.0, eQ);
1270 1           Safefree(H);
1271 1           Safefree(Q);
1272 1           return s;
1273             }
1274             // Calculate D (two-sided), D+ (greater), and D- (less) simultaneously
1275 9           static void calc_2sample_stats(NV *x, size_t nx, NV *y, size_t ny,
1276             NV *d, NV *d_plus, NV *d_minus) {
1277 9           qsort(x, nx, sizeof(NV), compare_doubles);
1278 9           qsort(y, ny, sizeof(NV), compare_doubles);
1279 9           NV max_d = 0.0, max_d_plus = 0.0, max_d_minus = 0.0;
1280 9           size_t i = 0, j = 0;
1281 309 100         while(i < nx || j < ny) {
    100          
1282             NV val;
1283 300 100         if (i < nx && j < ny) val = (x[i] < y[j]) ? x[i] : y[j];
    100          
    100          
1284 69 100         else if (i < nx) val = x[i];
1285 15           else val = y[j];
1286 480 100         while(i < nx && x[i] <= val) i++;
    100          
1287 420 100         while(j < ny && y[j] <= val) j++;
    100          
1288 300           NV cdf1 = (NV)i / nx;
1289 300           NV cdf2 = (NV)j / ny;
1290 300           NV diff = cdf1 - cdf2;
1291 300 100         if (diff > max_d_plus) max_d_plus = diff;
1292 300 100         if (-diff > max_d_minus) max_d_minus = -diff;
1293 300 100         if (fabs(diff) > max_d) max_d = fabs(diff);
1294             }
1295 9           *d = max_d;
1296 9           *d_plus = max_d_plus;
1297 9           *d_minus = max_d_minus;
1298 9           }
1299              
1300             // Branch the DP boundary check based on the 'alternative'
1301 4950           static int psmirnov_exact_test(NV q, NV r, NV s, int two_sided) {
1302 4950 100         if (two_sided) return (fabs(r - s) >= q);
1303 3160           return ((r - s) >= q); // Used for both D+ and D- via symmetry
1304             }
1305              
1306             // Evaluate the exact 2-sample probability
1307 9           static NV psmirnov_exact_uniq_upper(NV q, int m, int n, int two_sided) {
1308 9           NV md = (NV) m, nd = (NV) n;
1309 9           NV *restrict u = (NV *) safecalloc(n + 1, sizeof(NV));
1310 9           u[0] = 0.;
1311              
1312 129 100         for(unsigned int j = 1; j <= n; j++) {
1313 120 100         if(psmirnov_exact_test(q, 0., j / nd, two_sided)) u[j] = 1.;
1314 96           else u[j] = u[j - 1];
1315             }
1316 189 100         for(unsigned int i = 1; i <= m; i++) {
1317 180 100         if(psmirnov_exact_test(q, i / md, 0., two_sided)) u[0] = 1.;
1318 4830 100         for(int j = 1; j <= n; j++) {
1319 4650 100         if(psmirnov_exact_test(q, i / md, j / nd, two_sided)) u[j] = 1.;
1320             else {
1321 3484           NV v = (NV)(i) / (NV)(i + j);
1322 3484           NV w = (NV)(j) / (NV)(i + j);
1323 3484           u[j] = v * u[j] + w * u[j - 1];
1324             }
1325             }
1326             }
1327 9           NV res = u[n];
1328 9           Safefree(u);
1329 9           return res;
1330             }
1331              
1332 229           static NV p_body(NV n, NV delta, NV sd, NV sig_level, int tsample, int tside, bool strict) {
1333 229           NV nu = (n - 1.0) * (NV)tsample;
1334 229 50         if (nu < 1e-7) nu = 1e-7;
1335              
1336             // Ensure sig_level/tside is not truncated
1337 229           NV p_tail = sig_level / (NV)tside;
1338 229           NV qu = qt_tail(nu, p_tail); // qt(p, df, lower.tail=FALSE)
1339              
1340 229           NV ncp = sqrt(n / (NV)tsample) * (delta / sd);
1341              
1342 229 50         if (strict && tside == 2) {
    0          
1343             // Use R-style tail calls: 1 - P(T < qu) + P(T < -qu)
1344 0           return (1.0 - exact_pnt(qu, nu, ncp)) + exact_pnt(-qu, nu, ncp);
1345             } else {
1346             // Default: 1 - P(T < qu)
1347             // Ensure exact_pnt is using a convergence tolerance of at least 1e-15
1348 229           return 1.0 - exact_pnt(qu, nu, ncp);
1349             }
1350             }
1351              
1352             // Bisection algorithm to find the inverse F-distribution (Quantile function)
1353             // Equivalent to R's qf(p, df1, df2)
1354 6           static NV qf_bisection(NV p, NV df1, NV df2) {
1355 6 50         if (p <= 0.0) return 0.0;
1356 6 50         if (p >= 1.0) return INFINITY;
1357 6           NV low = 0.0, high = 1.0;
1358             // Find upper bound
1359 20 100         while (pf(high, df1, df2) < p) {
1360 14           low = high;
1361 14           high *= 2.0;
1362 14 50         if (high > 1e100) break; /* Fallback limit */
1363             }
1364              
1365             // Bisect to find the root
1366 251 50         for (unsigned short int i = 0; i < 150; i++) {
1367 251           NV mid = low + (high - low) / 2.0;
1368 251           NV p_mid = pf(mid, df1, df2);
1369              
1370 251 100         if (p_mid < p) {
1371 122           low = mid;
1372             } else {
1373 129           high = mid;
1374             }
1375 251 100         if (high - low < 1e-12) break;
1376             }
1377 6           return (low + high) / 2.0;
1378             }
1379              
1380             typedef struct {
1381             NV statistic;
1382             NV num_df;
1383             NV denom_df;
1384             NV p_value;
1385             NV ss_between; /* between-group sum of squares */
1386             NV ss_within; /* within-group sum of squares */
1387             NV ms_between; /* ss_between / num_df */
1388             NV ms_within; /* ss_within / denom_df */
1389             int k; /* number of groups */
1390             IV n; /* total observations */
1391             bool var_equal; /* 0 = Welch, 1 = classic */
1392             } OneWayResult;
1393              
1394             static OneWayResult
1395 3           c_oneway_test(const NV *restrict data, const size_t *restrict sizes,
1396             size_t k, bool var_equal)
1397             {
1398             OneWayResult res;
1399 3           res.var_equal = var_equal;
1400 3           res.k = (int)k;
1401              
1402 3           NV *restrict n_i = (NV *)safemalloc(k * sizeof(NV));
1403 3           NV *restrict m_i = (NV *)safemalloc(k * sizeof(NV));
1404 3           NV *restrict v_i = (NV *)safemalloc(k * sizeof(NV));
1405 3           size_t offset = 0;
1406 3           IV total_n = 0;
1407 9 100         for (size_t g = 0; g < k; g++) {
1408 6           size_t ng = sizes[g];
1409 6           n_i[g] = (NV)ng;
1410 6           total_n += (IV)ng;
1411 6           NV sum = 0.0;
1412 36 100         for (size_t i = 0; i < ng; i++) sum += data[offset + i];
1413 6           NV mean = sum / (NV)ng;
1414 6           m_i[g] = mean;
1415              
1416 6           NV ss = 0.0;
1417 36 100         for (size_t i = 0; i < ng; i++) {
1418 30           NV d = data[offset + i] - mean;
1419 30           ss += d * d;
1420             }
1421 6           v_i[g] = ss / (NV)(ng - 1); /* ng >= 2 guaranteed by caller */
1422 6           offset += ng;
1423             }
1424 3           res.n = total_n;
1425             // grand mean (simple average over all obs; used only by classic branch)/
1426 3           NV grand_mean = 0.0;
1427 33 100         for (IV i = 0; i < (IV)total_n; i++) grand_mean += data[i];
1428 3           grand_mean /= (NV)total_n;
1429              
1430 3           NV df1 = (NV)(k - 1);
1431              
1432 3 50         if (var_equal) {/* ── Classic one-way ANOVA
1433             * F = [Σ n_i·(m_i − ȳ)² / (k−1)] / [Σ (n_i−1)·v_i / (n−k)] */
1434 0           NV ssbg = 0.0, sswg = 0.0;
1435 0 0         for (size_t g = 0; g < k; g++) {
1436 0           NV dm = m_i[g] - grand_mean;
1437 0           ssbg += n_i[g] * dm * dm;
1438 0           sswg += (n_i[g] - 1.0) * v_i[g];
1439             }
1440 0           NV df2 = (NV)(total_n - (IV)k);
1441 0           res.statistic = (ssbg / df1) / (sswg / df2);
1442 0           res.num_df = df1;
1443 0           res.denom_df = df2;
1444 0           res.ss_between = ssbg;
1445 0           res.ss_within = sswg;
1446 0           res.ms_between = ssbg / df1;
1447 0           res.ms_within = sswg / df2;
1448             } else {// ── Welch one-way (heteroscedastic)
1449 3           NV *restrict w_i = (NV *)safemalloc(k * sizeof(NV));
1450 3           NV sum_w = 0.0;
1451 9 100         for (size_t g = 0; g < k; g++) { w_i[g] = n_i[g] / v_i[g]; sum_w += w_i[g]; }
1452 3           NV wgrand = 0.0;
1453 9 100         for (size_t g = 0; g < k; g++) wgrand += w_i[g] * m_i[g];
1454 3           wgrand /= sum_w;
1455 3           NV tmp = 0.0;
1456 9 100         for (size_t g = 0; g < k; g++) {
1457 6           NV t = 1.0 - w_i[g] / sum_w;
1458 6           tmp += (t * t) / (n_i[g] - 1.0);
1459             }
1460 3           tmp /= ((NV)k * (NV)k - 1.0); /* k² − 1 */
1461 3           NV num = 0.0;
1462 9 100         for (size_t g = 0; g < k; g++) {
1463 6           NV dm = m_i[g] - wgrand;
1464 6           num += w_i[g] * dm * dm;
1465             }
1466 3           res.statistic = num / (df1 * (1.0 + 2.0 * (NV)(k - 2) * tmp));
1467 3           res.num_df = df1;
1468 3 50         res.denom_df = (tmp > 0.0) ? (1.0 / (3.0 * tmp)) : 1e300;
1469             /* unweighted SS for the output table */
1470 3           NV ssbg = 0.0, sswg = 0.0;
1471 9 100         for (size_t g = 0; g < k; g++) {
1472 6           NV dm = m_i[g] - grand_mean;
1473 6           ssbg += n_i[g] * dm * dm;
1474 6           sswg += (n_i[g] - 1.0) * v_i[g];
1475             }
1476 3           res.ss_between = ssbg;
1477 3           res.ss_within = sswg;
1478 3 50         res.ms_between = (df1 > 0.0) ? ssbg / df1 : 0.0;
1479 3 50         res.ms_within = (res.denom_df > 0.0) ? sswg / res.denom_df : 0.0;
1480 3           Safefree(w_i);
1481             }
1482             // upper-tail p-value P(F ≥ statistic)
1483 3           res.p_value = 1 - pf(res.statistic, res.num_df, res.denom_df);
1484 3           Safefree(n_i); Safefree(m_i); Safefree(v_i);
1485 3           return res;
1486             }
1487              
1488             /* ── parse_formula
1489             *
1490             * Splits "response ~ factor" into two NUL-terminated, heap-allocated
1491             * strings. Leading/trailing whitespace is stripped from each side.
1492             * Returns 1 on success, 0 on failure (malformed / missing '~').
1493             * Caller must Safefree() both *lhs and *rhs on success. */
1494             static int
1495 4           parse_formula(const char *formula, char **lhs, char **rhs)
1496             {
1497 4           const char *restrict tilde = strchr(formula, '~');
1498 4 100         if (!tilde) return 0;
1499              
1500             // left-hand side: trim trailing whitespace
1501 3           const char *l_start = formula;
1502 3           const char *l_end = tilde - 1;
1503 6 50         while (l_end >= l_start && isspace((unsigned char)*l_end)) l_end--;
    100          
1504 3 50         if (l_end < l_start) return 0; /* empty LHS */
1505              
1506             // right-hand side: trim leading whitespace */
1507 3           const char *restrict r_start = tilde + 1;
1508 6 50         while (*r_start && isspace((unsigned char)*r_start)) r_start++;
    100          
1509 3           const char *restrict r_end = r_start + strlen(r_start) - 1;
1510 3 50         while (r_end >= r_start && isspace((unsigned char)*r_end)) r_end--;
    50          
1511 3 50         if (r_end < r_start) return 0; /* empty RHS */
1512              
1513 3           size_t llen = (size_t)(l_end - l_start + 1);
1514 3           size_t rlen = (size_t)(r_end - r_start + 1);
1515              
1516 3           *lhs = (char *)safemalloc(llen + 1);
1517 3           *rhs = (char *)safemalloc(rlen + 1);
1518 3           memcpy(*lhs, l_start, llen); (*lhs)[llen] = '\0';
1519 3           memcpy(*rhs, r_start, rlen); (*rhs)[rlen] = '\0';
1520 3           return 1;
1521             }
1522              
1523             /* ── build_groups_from_formula ───────────────
1524             *
1525             * Takes parallel response[] and label[] arrays (each length n) and
1526             * partitions them into groups, filling:
1527             * out_flat[] – observations sorted into contiguous group blocks
1528             * out_sizes[] – number of observations per group (caller allocates n
1529             * slots for both; actual group count returned via *out_k)
1530             * out_names – if non-NULL, receives a heap-allocated char** of k
1531             * group-name strings (caller must free each and the array)
1532             *
1533             * Group identity is the string representation of each label element
1534             * (SvPV_nolen), so integer 0 and string "0" are the same group.
1535             * Groups are ordered by first appearance in label[], matching R's
1536             * factor level ordering from stack().
1537             *
1538             * Returns 1 on success; 0 if any validation error (sets errbuf).
1539             */
1540             #define OWT_MAX_GROUPS 1024 /* sane ceiling; ANOVA with >1024 groups is absurd */
1541              
1542 2           static int build_groups_from_formula(pTHX_
1543             AV *restrict response_av,
1544             AV *restrict label_av,
1545             NV *restrict out_flat,
1546             size_t *restrict out_sizes,
1547             size_t *restrict out_k,
1548             char ***restrict out_names,
1549             char *restrict errbuf,
1550             size_t errbuf_len)
1551             {
1552 2           IV n = av_len(response_av) + 1;
1553 2           IV nl = av_len(label_av) + 1;
1554              
1555 2 100         if (n != nl) {
1556 1           snprintf(errbuf, errbuf_len,
1557             "formula: response length (%"IVdf") != factor length (%"IVdf")",
1558             n, nl);
1559 1           return 0;
1560             }
1561 1 50         if (n < 2) {
1562 0           snprintf(errbuf, errbuf_len, "formula: need at least 2 observations");
1563 0           return 0;
1564             }
1565              
1566             /* ── discover unique group labels in order of first appearance ─── */
1567             /* We store pointers into a heap-allocated label string table. */
1568 1           char **restrict group_names = (char **)safemalloc(OWT_MAX_GROUPS * sizeof(char *));
1569 1           size_t ngroups = 0;
1570 1           IV *restrict obs_group = (IV *)safemalloc((size_t)n * sizeof(IV));
1571             /* maps obs index → group index */
1572              
1573 7 100         for (IV i = 0; i < n; i++) {
1574 6           SV **restrict lsv = av_fetch(label_av, i, 0);
1575 6 50         const char *restrict label = (lsv && *lsv) ? SvPV_nolen(*lsv) : "";
    50          
1576             /* linear scan for existing group (k is small, O(n·k) is fine) */
1577 6           IV gidx = -1;
1578 9 100         for (size_t g = 0; g < ngroups; g++) {
1579 7 100         if (strEQ(group_names[g], label)) { gidx = (IV)g; break; }
1580             }
1581 6 100         if (gidx < 0) {
1582 2 50         if (ngroups >= OWT_MAX_GROUPS) {
1583 0           snprintf(errbuf, errbuf_len,
1584             "formula: too many distinct groups (max %d)", OWT_MAX_GROUPS);
1585 0           Safefree(group_names);
1586 0           Safefree(obs_group);
1587 0           return 0;
1588             }
1589             /* new group: copy the label string */
1590 2           size_t lablen = strlen(label);
1591 2           group_names[ngroups] = (char *)safemalloc(lablen + 1);
1592 2           memcpy(group_names[ngroups], label, lablen + 1);
1593 2           gidx = (IV)ngroups++;
1594             }
1595 6           obs_group[i] = gidx;
1596             }
1597              
1598 1 50         if (ngroups < 2) {
1599 0           snprintf(errbuf, errbuf_len,
1600             "formula: need at least 2 distinct groups, found %zu", ngroups);
1601 0 0         for (size_t g = 0; g < ngroups; g++) Safefree(group_names[g]);
1602 0           Safefree(group_names); Safefree(obs_group);
1603 0           return 0;
1604             }
1605              
1606             /* count per-group sizes */
1607 1           memset(out_sizes, 0, ngroups * sizeof(size_t));
1608 7 100         for (unsigned i = 0; i < n; i++) out_sizes[obs_group[i]]++;
1609              
1610             /* validate: every group needs >= 2 observations */
1611 3 100         for (size_t g = 0; g < ngroups; g++) {
1612 2 50         if (out_sizes[g] < 2) {
1613 0           snprintf(errbuf, errbuf_len,
1614             "formula: group '%s' has only %zu observation(s); need >= 2",
1615 0           group_names[g], out_sizes[g]);
1616 0 0         for (size_t gg = 0; gg < ngroups; gg++) Safefree(group_names[gg]);
1617 0           Safefree(group_names); Safefree(obs_group);
1618 0           return 0;
1619             }
1620             }
1621             /* ── fill flat output array in group order ─────────────────────── *
1622             * We compute a running write-offset per group, then scatter*/
1623 1           size_t *restrict write_pos = (size_t *)safemalloc(ngroups * sizeof(size_t));
1624 1           write_pos[0] = 0;
1625 2 100         for (size_t g = 1; g < ngroups; g++)
1626 1           write_pos[g] = write_pos[g - 1] + out_sizes[g - 1];
1627 7 100         for (IV i = 0; i < n; i++) {
1628 6           SV **restrict rsv = av_fetch(response_av, i, 0);
1629 6 50         NV val = (rsv && *rsv) ? SvNV(*rsv) : 0.0;
    50          
1630 6           size_t g = (size_t)obs_group[i];
1631 6           out_flat[write_pos[g]++] = val;
1632             }
1633              
1634 1           *out_k = ngroups;
1635              
1636             /* ── clean up or hand off group names */
1637 1           Safefree(write_pos); Safefree(obs_group);
1638 1 50         if (out_names) {
1639 1           *out_names = group_names; /* caller takes ownership */
1640             } else {
1641 0 0         for (size_t g = 0; g < ngroups; g++) Safefree(group_names[g]);
1642 0           Safefree(group_names);
1643             }
1644 1           return 1;
1645             }
1646             #undef OWT_MAX_GROUPS
1647             // --- Math Macros ---
1648             #ifndef M_LN_SQRT_2PI
1649             #define M_LN_SQRT_2PI 0.91893853320467274178
1650             #endif
1651             #ifndef M_LN2
1652             #define M_LN2 0.69314718055994530941
1653             #endif
1654             #ifndef M_1_SQRT_2PI
1655             #define M_1_SQRT_2PI 0.39894228040143267794
1656             #endif
1657              
1658             /* c_dnorm: Normal distribution PDF
1659             *
1660             * Mathematically identical to R's dnorm4.
1661             * Includes Morten Welinder's precision improvements for extreme tails.
1662             * ----------------------------------------------------------------------- */
1663 25           static NV c_dnorm(NV x, NV mu, NV sigma, int give_log) {
1664             // Propagate NaNs
1665 25 50         if (isnan(x) || isnan(mu) || isnan(sigma)) return x + mu + sigma;
    50          
    50          
1666 25 50         if (sigma < 0.0) {
1667 0           warn("dnorm: standard deviation must be non-negative");
1668 0           return NAN;
1669             }
1670 25 50         if (isinf(sigma)) return 0.0;
1671 25 50         if ((isnan(x) || isinf(x)) && mu == x) return NAN; // x-mu is NaN
    50          
    0          
1672             // Dirac delta behavior for zero variance
1673 25 50         if (sigma == 0.0) return (x == mu) ? INFINITY : 0.0;
    0          
1674              
1675             // Standardize x
1676 25           x = (x - mu) / sigma;
1677 25 50         if (isnan(x) || isinf(x)) return 0.0;
    50          
1678 25           x = fabs(x);
1679             // Catch massive limits early to prevent math overflow
1680 25 50         if (x >= 2.0 * sqrt(DBL_MAX)) return 0.0;
1681 25 100         if (give_log) {
1682 1           return -(M_LN_SQRT_2PI + 0.5 * x * x + log(sigma));
1683             }
1684             // Naive formula for standard bodies
1685 24 100         if (x < 5.0) {
1686 22           return M_1_SQRT_2PI * exp(-0.5 * x * x) / sigma;
1687             }
1688             // Underflow boundary check using IEEE float characteristics
1689 2 50         if (x > sqrt(-2.0 * M_LN2 * (DBL_MIN_EXP + 1.0 - DBL_MANT_DIG))) {
1690 0           return 0.0;
1691             }
1692             /* Splitting x to dodge floating point inaccuracies in x^2 for large x.
1693             * x = x1 + x2, where |x2| <= 2^-16
1694             * trunc() safely substitutes R_forceint() */
1695 2           NV x1 = ldexp(trunc(ldexp(x, 16)), -16);
1696 2           NV x2 = x - x1;
1697 2           return (M_1_SQRT_2PI / sigma) * (exp(-0.5 * x1 * x1) * exp((-0.5 * x2 - x1) * x2));
1698             }
1699             /*Helper for prcomp: Jacobi Eigenvalue Algorithm for Symmetric Matrices
1700             * Used to compute the eigendecomposition of the X^T X covariance matrix.*/
1701 7           static void jacobi_eigen(NV *restrict A, size_t n, NV *restrict d, NV *restrict v) {
1702 21 100         for (size_t i = 0; i < n; i++) {
1703 42 100         for (size_t j = 0; j < n; j++) v[i * n + j] = (i == j) ? 1.0 : 0.0;
    100          
1704 14           d[i] = A[i * n + i];
1705             }
1706 7           NV *restrict b = (NV*)safemalloc(n * sizeof(NV));
1707 7           NV *restrict z = (NV*)safemalloc(n * sizeof(NV));
1708 21 100         for (size_t i = 0; i < n; i++) { b[i] = d[i]; z[i] = 0.0; }
1709 14 50         for (int iter = 1; iter <= 50; iter++) {
1710 14           NV sm = 0.0;
1711 28 100         for (size_t i = 0; i < n - 1; i++) {
1712 28 100         for (size_t j = i + 1; j < n; j++) sm += fabs(A[i * n + j]);
1713             }
1714 14 100         if (sm == 0.0) break;
1715 7 50         NV tresh = (iter < 4) ? 0.2 * sm / (n * n) : 0.0;
1716 14 100         for (size_t i = 0; i < n - 1; i++) {
1717 14 100         for (size_t j = i + 1; j < n; j++) {
1718 7           NV g = 100.0 * fabs(A[i * n + j]);
1719 7 50         if (iter > 4 && fabs(d[i]) + g == fabs(d[i]) && fabs(d[j]) + g == fabs(d[j])) {
    0          
    0          
1720 0           A[i * n + j] = 0.0;
1721 7 50         } else if (fabs(A[i * n + j]) > tresh) {
1722 7           NV h = d[j] - d[i];
1723             NV t;
1724 7 50         if (fabs(h) + g == fabs(h)) {
1725 0           t = A[i * n + j] / h;
1726             } else {
1727 7           NV theta = 0.5 * h / A[i * n + j];
1728 7           t = 1.0 / (fabs(theta) + sqrt(1.0 + theta * theta));
1729 7 100         if (theta < 0.0) t = -t;
1730             }
1731 7           NV c = 1.0 / sqrt(1.0 + t * t);
1732 7           NV s = t * c;
1733 7           NV tau = s / (1.0 + c);
1734 7           NV h_t = t * A[i * n + j];
1735 7           z[i] -= h_t;
1736 7           z[j] += h_t;
1737 7           d[i] -= h_t;
1738 7           d[j] += h_t;
1739 7           A[i * n + j] = 0.0;
1740 7 50         for (size_t k = 0; k < i; k++) {
1741 0           g = A[k * n + i]; NV h_val = A[k * n + j];
1742 0           A[k * n + i] = g - s * (h_val + g * tau);
1743 0           A[k * n + j] = h_val + s * (g - h_val * tau);
1744             }
1745 7 50         for (size_t k = i + 1; k < j; k++) {
1746 0           g = A[i * n + k]; NV h_val = A[k * n + j];
1747 0           A[i * n + k] = g - s * (h_val + g * tau);
1748 0           A[k * n + j] = h_val + s * (g - h_val * tau);
1749             }
1750 7 50         for (size_t k = j + 1; k < n; k++) {
1751 0           g = A[i * n + k]; NV h_val = A[j * n + k];
1752 0           A[i * n + k] = g - s * (h_val + g * tau);
1753 0           A[j * n + k] = h_val + s * (g - h_val * tau);
1754             }
1755 21 100         for (size_t k = 0; k < n; k++) {
1756 14           g = v[k * n + i]; NV h_val = v[k * n + j];
1757 14           v[k * n + i] = g - s * (h_val + g * tau);
1758 14           v[k * n + j] = h_val + s * (g - h_val * tau);
1759             }
1760             }
1761             }
1762             }
1763 21 100         for (size_t i = 0; i < n; i++) {
1764 14           b[i] += z[i];
1765 14           d[i] = b[i];
1766 14           z[i] = 0.0;
1767             }
1768             }
1769 7           Safefree(b); Safefree(z);
1770             // Sort eigenvalues and corresponding eigenvectors in descending order
1771 14 100         for (size_t i = 0; i < n - 1; i++) {
1772 7           size_t max_k = i;
1773 7           NV max_val = d[i];
1774 14 100         for (size_t j = i + 1; j < n; j++) {
1775 7 100         if (d[j] > max_val) {
1776 6           max_val = d[j];
1777 6           max_k = j;
1778             }
1779             }
1780 7 100         if (max_k != i) {
1781 6           d[max_k] = d[i];
1782 6           d[i] = max_val;
1783 18 100         for (size_t k = 0; k < n; k++) {
1784 12           NV tmp = v[k * n + i];
1785 12           v[k * n + i] = v[k * n + max_k];
1786 12           v[k * n + max_k] = tmp;
1787             }
1788             }
1789             }
1790 7           }
1791              
1792             // --- pull a numeric value out of an SV* slot
1793 456           static int c2c_num(pTHX_ SV **restrict ep, NV *restrict out) {
1794 456 50         if (ep && *ep && SvOK(*ep) && looks_like_number(*ep)) {
    50          
    100          
    50          
1795 427           *out = SvNV(*ep);
1796 427           return 1;
1797             }
1798 29           return 0;
1799             }
1800              
1801 5           static SV* c2c_call(pTHX_ SV *restrict cv, SV *restrict rv1, SV *restrict rv2) {
1802 5           dSP;
1803 5           ENTER;
1804 5           SAVETMPS;
1805 5 50         PUSHMARK(SP);
1806 5 50         EXTEND(SP, 2);
1807 5           PUSHs(rv1);
1808 5           PUSHs(rv2);
1809 5           PUTBACK;
1810 5           unsigned int count = call_sv(cv, G_SCALAR);
1811 4           SPAGAIN;
1812 4 50         SV *restrict ret = (count > 0) ? newSVsv(POPs) : newSV(0);
1813 4           PUTBACK;
1814 4 50         FREETMPS;
1815 4           LEAVE;
1816 4           return ret;
1817             }
1818             // Mark col_names[idx] whose name equals (wname,wl) as an outer column; returns
1819             // 1 if a matching column was found, 0 otherwise.
1820 7           static int c2c_mark(SV **col_names, STRLEN *name_len, size_t ncols, const char *wname, STRLEN wl, char *is_outer) {
1821 15 100         for (size_t cc = 0; cc < ncols; cc++) {
1822 13 100         if (name_len[cc] == wl && memEQ(SvPVX(col_names[cc]), wname, wl)) { is_outer[cc] = 1; return 1; }
    100          
1823             }
1824 2           return 0;
1825             }
1826             //
1827             // filter() helpers — place this block in the C section, ABOVE the MODULE line
1828             //
1829             // Resolve the cell SV for a column in the "current row".
1830             // AoH: current row is row_hv -> hv_fetch(row_hv, col)
1831             // HoA: current row is index idx -> hv_fetch(data_hv,col) -> AV -> av_fetch(idx)
1832             typedef struct {
1833             int is_aoh;
1834             HV *restrict row_hv;
1835             HV *restrict data_hv;
1836             SSize_t idx;
1837             } filt_ctx;
1838 85           static SV* filt_cell(pTHX_ filt_ctx *restrict ctx, const char *restrict col, STRLEN clen) {
1839 85 100         if (ctx->is_aoh) {
1840 70           SV **restrict p = hv_fetch(ctx->row_hv, col, clen, 0);
1841 70 100         return (p && *p) ? *p : NULL;
    50          
1842             }
1843 15           SV **restrict cp = hv_fetch(ctx->data_hv, col, clen, 0);
1844 15 50         if (!cp || !*cp || !SvROK(*cp) || SvTYPE(SvRV(*cp)) != SVt_PVAV) return NULL;
    50          
    50          
    50          
1845 15           SV **restrict vp = av_fetch((AV*)SvRV(*cp), ctx->idx, 0);
1846 15 50         return (vp && *vp) ? *vp : NULL;
    50          
1847             }
1848             // Recursively interpret a Stats::LikeR::Pred tree against the current row.
1849 101           static bool filt_eval(pTHX_ SV *restrict pred, filt_ctx *restrict ctx) {
1850 101 50         if (!pred || !SvROK(pred) || SvTYPE(SvRV(pred)) != SVt_PVHV)
    50          
    50          
1851 0           croak("filter: malformed predicate (expected an object built with col())");
1852 101           HV *restrict h = (HV*)SvRV(pred);
1853 101           SV **restrict opp = hv_fetchs(h, "op", 0);
1854 101 50         if (!opp || !*opp) croak("filter: predicate node missing 'op'");
    50          
1855 101           const char *restrict op = SvPV_nolen(*opp);
1856 101 100         if (strEQ(op, "and") || strEQ(op, "or")) {
    100          
1857 12           SV **restrict lp = hv_fetchs(h, "l", 0);
1858 12           SV **restrict rp = hv_fetchs(h, "r", 0);
1859 12 50         bool L = filt_eval(aTHX_ (lp ? *lp : NULL), ctx);
1860 12 100         if (op[0] == 'a') return L ? filt_eval(aTHX_ (rp ? *rp : NULL), ctx) : 0; // and
    100          
    50          
    100          
1861 4 100         return L ? 1 : filt_eval(aTHX_ (rp ? *rp : NULL), ctx); // or
    50          
    100          
1862             }
1863 89 100         if (strEQ(op, "not")) {
1864 4           SV **restrict lp = hv_fetchs(h, "l", 0);
1865 4 50         return !filt_eval(aTHX_ (lp ? *lp : NULL), ctx);
1866             }
1867 85           SV **restrict cp = hv_fetchs(h, "col", 0);
1868 85           SV **restrict vp = hv_fetchs(h, "val", 0);
1869 85 50         if (!cp || !*cp) croak("filter: comparison node missing 'col'");
    50          
1870             STRLEN clen;
1871 85           const char *restrict col = SvPV(*cp, clen);
1872 85           SV *restrict cell = filt_cell(aTHX_ ctx, col, clen);
1873 85 100         if (!cell || !SvOK(cell)) return 0; // missing / undef cell never matches
    100          
1874 83 50         SV *restrict val = (vp && *vp) ? *vp : &PL_sv_undef;
    50          
1875 83 100         if (strEQ(op, ">")) return SvNV(cell) > SvNV(val);
1876 45 100         if (strEQ(op, "<")) return SvNV(cell) < SvNV(val);
1877 38 100         if (strEQ(op, ">=")) return SvNV(cell) >= SvNV(val);
1878 34 100         if (strEQ(op, "<=")) return SvNV(cell) <= SvNV(val);
1879 30 100         if (strEQ(op, "==")) return SvNV(cell) == SvNV(val);
1880 19 100         if (strEQ(op, "!=")) return SvNV(cell) != SvNV(val);
1881             {
1882             STRLEN al, bl;
1883 15           const char *restrict a = SvPV(cell, al);
1884 15           const char *restrict b = SvPV(val, bl);
1885 15           STRLEN m = al < bl ? al : bl;
1886 15 50         int c = m ? memcmp(a, b, m) : 0;
1887 15 100         if (c == 0) c = (al > bl) - (al < bl);
1888 23 100         if (strEQ(op, "eq")) return c == 0;
1889 8 100         if (strEQ(op, "ne")) return c != 0;
1890 4 50         if (strEQ(op, "lt")) return c < 0;
1891 4 50         if (strEQ(op, "gt")) return c > 0;
1892 0 0         if (strEQ(op, "le")) return c <= 0;
1893 0 0         if (strEQ(op, "ge")) return c >= 0;
1894             }
1895 0           croak("filter: unknown operator '%s' in predicate", op);
1896             return 0; // not reached
1897             }
1898             // Call a coderef predicate with $_ (and $_[0]) set to the row hashref.
1899 12           static bool filt_call(pTHX_ SV *restrict cv, SV *restrict row) {
1900 12           dSP;
1901             bool keep;
1902             int n;
1903 12           ENTER; SAVETMPS;
1904 12           SAVE_DEFSV;
1905 12           DEFSV_set(row);
1906 12 50         PUSHMARK(SP);
1907 12 50         EXTEND(SP, 1);
1908 12           PUSHs(row);
1909 12           PUTBACK;
1910 12           n = call_sv(cv, G_SCALAR);
1911 12           SPAGAIN;
1912 12 50         keep = (n > 0) ? (bool)SvTRUE(TOPs) : 0;
    100          
1913 12 50         if (n > 0) (void)POPs;
1914 12           PUTBACK;
1915 12 50         FREETMPS; LEAVE;
1916 12           return keep;
1917             }
1918              
1919 13           static int h2h_keycmp(const void *pa, const void *pb) {
1920             dTHX;
1921 13           SV *restrict const *a = (SV * const *)pa;
1922 13           SV *restrict const *b = (SV * const *)pb;
1923 13           return sv_cmp(*a, *b);
1924             }
1925 2919           int compare_NVs(const void *restrict a, const void *restrict b) {
1926 2919           NV arg1 = *(const NV *)a;
1927 2919           NV arg2 = *(const NV *)b;
1928 2919 100         if (arg1 < arg2) return -1;
1929 890 50         if (arg1 > arg2) return 1;
1930 0           return 0;
1931             }
1932             // Call a column predicate as $cv->($col_values, $col_name) and return its truth.
1933             // $col_values is an array ref of the column's DEFINED cells; $col_name is the
1934             // column key. Used so a block like sub { sd($_[0]) == 0 } can pick columns out.
1935 39           static bool cf_pred(pTHX_ SV *cv_sv, AV *a_av, AV *b_av, SV *name_sv) {
1936 39           dSP;
1937 39           bool truth = FALSE;
1938             int count;
1939 39           ENTER;
1940 39           SAVETMPS;
1941 39 50         PUSHMARK(SP);
1942 39 50         XPUSHs(sv_2mortal(newRV_inc((SV*)a_av)));
1943 39 100         if (b_av) XPUSHs(sv_2mortal(newRV_inc((SV*)b_av)));
    50          
1944 39 50         XPUSHs(sv_2mortal(newSVsv(name_sv)));
1945 39           PUTBACK;
1946 39           count = call_sv(cv_sv, G_SCALAR);
1947 39           SPAGAIN;
1948 39 50         if (count > 0) {
1949 39           SV *restrict ret = POPs; // POPs has a side effect: pop exactly once,
1950 39           truth = cBOOL(SvTRUE(ret)); // because SvTRUE() may evaluate its arg twice.
1951             }
1952 39           PUTBACK;
1953 39 50         FREETMPS;
1954 39           LEAVE;
1955 39           return truth;
1956             }
1957             /* ---------------------------------------------------------------------------
1958             * Helpers for _parse_csv_file. Place in the C section of the .xs file
1959             * (above the first MODULE line).
1960             * ------------------------------------------------------------------------- */
1961              
1962             /* save-stack destructor: closes the input handle on ANY exit, including a
1963             * croak thrown inside the row callback */
1964 522           static void S_pclose(pTHX_ void *p)
1965             {
1966 522           PerlIO_close((PerlIO*)p);
1967 522           }
1968              
1969             /* Finish the current record: push the pending field, hand the row to the
1970             * callback (streaming) or to @$data (slurp), and start a fresh row.
1971             *
1972             * Ownership: the row AV's single reference is transferred to a MORTAL RV
1973             * (newRV_noinc + sv_2mortal). On the normal path the inner FREETMPS releases
1974             * it; if the callback dies, the unwind's FREETMPS releases it just the same.
1975             * If the callback kept a copy of the ref, that copy bumped the refcount and
1976             * the row survives for the caller -- exactly the old semantics, minus the
1977             * leak and minus one SvREFCNT_dec per row. */
1978 6718           static void S_emit_row(pTHX_ AV **rowp, SV *field, bool use_cb, SV *callback, AV *data)
1979             {
1980 6718           av_push(*rowp, newSVsv(field));
1981 6718           sv_setpvs(field, "");
1982 6718 50         if (use_cb) {
1983 6718           AV *restrict row = *rowp;
1984 6718           *rowp = NULL; /* ownership leaves this function NOW */
1985 6718           dSP;
1986 6718           ENTER;
1987 6718           SAVETMPS;
1988 6718 50         PUSHMARK(SP);
1989 6718 50         XPUSHs(sv_2mortal(newRV_noinc((SV*)row)));
1990 6718           PUTBACK;
1991 6718           call_sv(callback, G_DISCARD); /* may die: nothing left to leak */
1992 6717 50         FREETMPS;
1993 6717           LEAVE;
1994             } else {
1995 0           av_push(data, newRV_noinc((SV*)*rowp));
1996 0           *rowp = NULL;
1997             }
1998 6717           *rowp = newAV();
1999 6717           }
2000              
2001             // --- XS SECTION ---
2002             MODULE = Stats::LikeR PACKAGE = Stats::LikeR
2003              
2004             SV *cfilter(data, ...)
2005             SV *data
2006             CODE:
2007             {
2008             /* 0. options. Exactly one of keep/remove is required; it is either an
2009             array ref of column names or a value predicate (CODE ref / function
2010             name). For a predicate, undef handling is:
2011             na => 'keep' (default) - the predicate sees every cell, incl undef
2012             na => 'omit' - single-column funcs (sd) get defined cells
2013             against => 'col' - two-column funcs (cor): the predicate gets
2014             ($col, $ref) over rows defined in BOTH.*/
2015 32           SV *restrict keep_sv = NULL, *restrict remove_sv = NULL;
2016 32           SV *restrict na_sv = NULL, *restrict against_sv = NULL;
2017 32 50         if ((items - 1) & 1) croak("cfilter: trailing options must be name => value pairs");
2018 78 100         for (int oi = 1; oi < items; oi += 2) {
2019             STRLEN ol;
2020 47           const char *restrict oname = SvPV(ST(oi), ol);
2021 47           SV *restrict oval = ST(oi + 1);
2022 47 100         if (ol == 4 && memEQ(oname, "keep", 4)) keep_sv = oval;
    50          
2023 18 100         else if (ol == 6 && memEQ(oname, "remove", 6)) remove_sv = oval;
    50          
2024 16 100         else if (ol == 2 && memEQ(oname, "na", 2)) na_sv = oval;
    50          
2025 7 100         else if (ol == 7 && memEQ(oname, "against", 7)) against_sv = oval;
    50          
2026 1           else croak("cfilter: unknown option '%s'", oname);
2027             }
2028 31 100         if (keep_sv && remove_sv) croak("cfilter: give either keep or remove, not both");
    100          
2029 30 100         if (!keep_sv && !remove_sv) croak("cfilter: need a keep or remove argument");
    100          
2030 29           bool removing = (remove_sv != NULL);
2031 29 100         SV *restrict sel = removing ? remove_sv : keep_sv;
2032             // classify the selector: array ref of names, or a value predicate.
2033             bool by_name;
2034 29           SV *restrict cv_sv = NULL;
2035 29 100         if (SvROK(sel) && SvTYPE(SvRV(sel)) == SVt_PVAV) by_name = TRUE;
    100          
2036 18 100         else if ((SvROK(sel) && SvTYPE(SvRV(sel)) == SVt_PVCV) || (SvOK(sel) && !SvROK(sel))) {
    100          
    50          
    100          
2037 17           by_name = FALSE;
2038 17 100         if (SvROK(sel)) cv_sv = SvRV(sel);
2039             else {
2040             STRLEN nl;
2041 1           const char *restrict name = SvPV(sel, nl);
2042 1 50         SV *restrict fq = strstr(name, "::") ? newSVpvn(name, nl) : newSVpvf("Stats::LikeR::%s", name);
2043 1           CV *restrict cv = get_cv(SvPV_nolen(fq), 0);
2044 1           SvREFCNT_dec(fq);
2045 1 50         if (!cv) croak("cfilter: unknown function '%s'", name);
2046 0           cv_sv = (SV*)cv;
2047             }
2048             }
2049 1           else croak("cfilter: keep/remove must be an array ref of column names or a code ref / function name");
2050             // decode the undef policy (predicate only).
2051 27           bool na_omit = FALSE;
2052 27 100         if (na_sv && SvOK(na_sv)) {
    50          
2053             STRLEN nl;
2054 9           const char *restrict nv = SvPV(na_sv, nl);
2055 9 100         if (nl == 4 && memEQ(nv, "omit", 4)) na_omit = TRUE;
    50          
2056 1 50         else if (nl == 4 && memEQ(nv, "keep", 4)) na_omit = FALSE;
    0          
2057 1           else croak("cfilter: na must be 'keep' or 'omit'");
2058             }
2059 26 100         if (by_name && (na_sv || against_sv)) croak("cfilter: na/against only apply to a predicate selector");
    100          
    50          
2060 25 100         if (against_sv && na_sv) croak("cfilter: give na or against, not both");
    100          
2061             // 1. detect the data shape.
2062 24 100         if (!SvROK(data)) croak("cfilter: data must be a reference");
2063 23           SV *restrict rv = SvRV(data);
2064             short int kind; // 0 = array-of-hashes, 1 = hash-of-arrays, 2 = hash-of-hashes
2065 23 100         if (SvTYPE(rv) == SVt_PVAV) kind = 0;
2066 20 50         else if (SvTYPE(rv) == SVt_PVHV) {
2067 20           HV *restrict h = (HV*)rv;
2068 20           hv_iterinit(h);
2069 20           HE *restrict fe = hv_iternext(h);
2070 20 50         if (!fe) kind = 2;
2071             else {
2072 20           SV *restrict fv = hv_iterval(h, fe);
2073 20 50         if (SvROK(fv) && SvTYPE(SvRV(fv)) == SVt_PVAV) kind = 1;
    100          
2074 2 50         else if (SvROK(fv) && SvTYPE(SvRV(fv)) == SVt_PVHV) kind = 2;
    50          
2075 0           else croak("cfilter: hash values must be array refs (HoA) or hash refs (HoH)");
2076             }
2077             }
2078 0           else croak("cfilter: data must be an array ref or hash ref");
2079             // 2. the column universe, and (predicate only) a row-aligned cell table
2080             // `cellmap`: colname -> AV of length nrows, undef in the gaps. The
2081             // alignment lets `against` pair two columns by row.
2082 23           HV *restrict universe = newHV();
2083 23           AV *restrict colnames = newAV();
2084 23 100         HV *restrict cellmap = by_name ? NULL : newHV();
2085 23           SSize_t nrows = 0;
2086 23 100         if (kind == 1) {
2087 18           HV *restrict h = (HV*)rv;
2088             HE *restrict e;
2089 18           hv_iterinit(h);
2090 72 100         while ((e = hv_iternext(h))) {
2091 54           SV *restrict val = hv_iterval(h, e);
2092 54 50         if (!SvROK(val) || SvTYPE(SvRV(val)) != SVt_PVAV) croak("cfilter: every value must be an array ref (hash of arrays)");
    50          
2093 54           SSize_t len = av_len((AV*)SvRV(val)) + 1;
2094 54 100         if (len > nrows) nrows = len;
2095             }
2096 18           hv_iterinit(h);
2097 90 100         while ((e = hv_iternext(h))) {
2098 54           SV *restrict ck = hv_iterkeysv(e);
2099 54           (void)hv_store_ent(universe, ck, newSViv(1), 0);
2100 54           av_push(colnames, newSVsv(ck));
2101 54 100         if (!by_name) {
2102 36           AV *restrict src = (AV*)SvRV(hv_iterval(h, e)), *restrict col = newAV();
2103 36 50         if (nrows > 0) av_extend(col, nrows - 1);
2104 216 100         for (SSize_t r = 0; r < nrows; r++) {
2105 180 50         SV **restrict ep = (r <= av_len(src)) ? av_fetch(src, r, 0) : NULL;
2106 180 50         av_push(col, (ep && *ep && SvOK(*ep)) ? newSVsv(*ep) : newSV(0));
    50          
    100          
2107             }
2108 36           (void)hv_store_ent(cellmap, ck, newRV_noinc((SV*)col), 0);
2109             }
2110             }
2111             } else {
2112             // row-major: collect the rows in a stable order, then build per column.
2113 5           AV *restrict rows = newAV();
2114 5 100         if (kind == 0) {
2115 3           AV *restrict a = (AV*)rv;
2116 3           SSize_t n = av_len(a) + 1;
2117 12 100         for (SSize_t r = 0; r < n; r++) {
2118 9           SV **restrict ep = av_fetch(a, r, 0);
2119 9 50         if (!ep || !*ep || !SvROK(*ep) || SvTYPE(SvRV(*ep)) != SVt_PVHV) croak("cfilter: array elements must be hash refs (array of hashes)");
    50          
    50          
    50          
2120 9           av_push(rows, newRV_inc(SvRV(*ep)));
2121             }
2122             } else {
2123 2           HV *restrict h = (HV*)rv;
2124             HE *restrict e;
2125 2           hv_iterinit(h);
2126 9 100         while ((e = hv_iternext(h))) {
2127 7           SV *restrict val = hv_iterval(h, e);
2128 7 50         if (!SvROK(val) || SvTYPE(SvRV(val)) != SVt_PVHV) croak("cfilter: every value must be a hash ref (hash of hashes)");
    50          
2129 7           av_push(rows, newRV_inc(SvRV(val)));
2130             }
2131             }
2132 5           nrows = av_len(rows) + 1;
2133             // union of columns, in first-seen order.
2134             {
2135 5           HV *restrict seen = newHV();
2136 21 100         for (SSize_t r = 0; r < nrows; r++) {
2137 16           HV *restrict row = (HV*)SvRV(*av_fetch(rows, r, 0));
2138             HE *restrict ie;
2139 16           hv_iterinit(row);
2140 72 100         while ((ie = hv_iternext(row))) {
2141 40           SV *restrict ck = hv_iterkeysv(ie);
2142 40 100         if (!hv_exists_ent(seen, ck, 0)) {
2143 14           (void)hv_store_ent(seen, ck, newSViv(1), 0);
2144 14           (void)hv_store_ent(universe, ck, newSViv(1), 0);
2145 14           av_push(colnames, newSVsv(ck));
2146             }
2147             }
2148             }
2149 5           SvREFCNT_dec((SV*)seen);
2150             }
2151 5 100         if (!by_name) {
2152 2           SSize_t nc = av_len(colnames) + 1;
2153 8 100         for (SSize_t c = 0; c < nc; c++) {
2154 6           SV *restrict ck = *av_fetch(colnames, c, 0);
2155 6           AV *restrict col = newAV();
2156 6 50         if (nrows > 0) av_extend(col, nrows - 1);
2157 36 100         for (SSize_t r = 0; r < nrows; r++) {
2158 30           HV *restrict row = (HV*)SvRV(*av_fetch(rows, r, 0));
2159 30           HE *restrict che = hv_fetch_ent(row, ck, 0, 0);
2160 30 100         SV *restrict cell = che ? HeVAL(che) : NULL;
2161 30 100         av_push(col, (cell && SvOK(cell)) ? newSVsv(cell) : newSV(0));
    50          
2162             }
2163 6           (void)hv_store_ent(cellmap, ck, newRV_noinc((SV*)col), 0);
2164             }
2165             }
2166 5           SvREFCNT_dec((SV*)rows);
2167             }
2168             // 2b. resolve the `against` reference column into its cell array.
2169 23           AV *restrict against_av = NULL;
2170 23 100         if (against_sv) {
2171 5 50         if (!SvOK(against_sv) || SvROK(against_sv)) croak("cfilter: against must be a column name (string)");
    50          
2172 5 100         if (!hv_exists_ent(universe, against_sv, 0)) croak("cfilter: against column '%s' not found in data", SvPV_nolen(against_sv));
2173 4           against_av = (AV*)SvRV(HeVAL(hv_fetch_ent(cellmap, against_sv, 0, 0)));
2174             }
2175             // 3. decide which columns to keep.
2176 22           HV *restrict keepset = newHV();
2177 22 100         if (by_name) {
2178 9           AV *restrict names = (AV*)SvRV(sel);
2179 9           HV *restrict listed = newHV();
2180 9           SSize_t n = av_len(names) + 1;
2181 21 100         for (SSize_t i = 0; i < n; i++) {
2182 13           SV **restrict ep = av_fetch(names, i, 0);
2183 13 50         if (!ep || !*ep || !SvOK(*ep)) croak("cfilter: column list contains an undefined entry");
    50          
    50          
2184 13 100         if (!hv_exists_ent(universe, *ep, 0)) croak("cfilter: column '%s' not found in data", SvPV_nolen(*ep));
2185 12           (void)hv_store_ent(listed, *ep, newSViv(1), 0);
2186             }
2187 8           SSize_t nc = av_len(colnames) + 1;
2188 31 100         for (SSize_t c = 0; c < nc; c++) {
2189 23           SV *restrict ck = *av_fetch(colnames, c, 0);
2190 23           bool in_list = cBOOL(hv_exists_ent(listed, ck, 0));
2191 23 100         if (removing ? !in_list : in_list) (void)hv_store_ent(keepset, ck, newSViv(1), 0);
    100          
2192             }
2193 8           SvREFCNT_dec((SV*)listed);
2194             } else {
2195             // predicate over the flat colnames list (never a live hash iterator
2196             // across call_sv). Apply the undef policy per column.
2197 13           SSize_t nc = av_len(colnames) + 1;
2198 52 100         for (SSize_t c = 0; c < nc; c++) {
2199 39           SV *restrict ck = *av_fetch(colnames, c, 0);
2200 39           AV *restrict cells = (AV*)SvRV(HeVAL(hv_fetch_ent(cellmap, ck, 0, 0)));
2201             bool pass;
2202 39 100         if (against_av) {
2203             // two columns, pairwise complete: rows defined in BOTH.
2204 12           AV *restrict a1 = newAV(), *restrict a2 = newAV();
2205 72 100         for (SSize_t r = 0; r < nrows; r++) {
2206 60           SV **restrict p1 = av_fetch(cells, r, 0);
2207 60           SV **restrict p2 = av_fetch(against_av, r, 0);
2208 60 50         if (p1 && *p1 && SvOK(*p1) && p2 && *p2 && SvOK(*p2)) {
    50          
    100          
    50          
    50          
    50          
2209 57           av_push(a1, newSVsv(*p1));
2210 57           av_push(a2, newSVsv(*p2));
2211             }
2212             }
2213 12           pass = cf_pred(aTHX_ cv_sv, a1, a2, ck);
2214 12           SvREFCNT_dec((SV*)a1);
2215 12           SvREFCNT_dec((SV*)a2);
2216 27 100         } else if (na_omit) {
2217             // one column, defined cells only.
2218 18           AV *restrict a1 = newAV();
2219 108 100         for (SSize_t r = 0; r < nrows; r++) {
2220 90           SV **restrict p = av_fetch(cells, r, 0);
2221 90 50         if (p && *p && SvOK(*p)) av_push(a1, newSVsv(*p));
    50          
    100          
2222             }
2223 18           pass = cf_pred(aTHX_ cv_sv, a1, NULL, ck);
2224 18           SvREFCNT_dec((SV*)a1);
2225             } else {
2226             // one column, every cell including undef.
2227 9           pass = cf_pred(aTHX_ cv_sv, cells, NULL, ck);
2228             }
2229 39 50         if (removing ? !pass : pass) (void)hv_store_ent(keepset, ck, newSViv(1), 0);
    100          
2230             }
2231             }
2232             // 4. rebuild the data in its original shape with only the kept columns.
2233             SV *restrict out;
2234 21 100         if (kind == 1) {
2235 16           HV *restrict outh = newHV(), *restrict h = (HV*)rv;
2236             HE *restrict e;
2237 16           hv_iterinit(h);
2238 64 100         while ((e = hv_iternext(h))) {
2239 48           SV *restrict ck = hv_iterkeysv(e);
2240 48 100         if (!hv_exists_ent(keepset, ck, 0)) continue;
2241 33           AV *restrict src = (AV*)SvRV(hv_iterval(h, e)), *restrict dst = newAV();
2242 33           SSize_t n = av_len(src) + 1;
2243 33 50         if (n > 0) av_extend(dst, n - 1);
2244 190 100         for (SSize_t i = 0; i < n; i++) {
2245 157           SV **restrict ep = av_fetch(src, i, 0);
2246 157 50         av_push(dst, (ep && *ep) ? newSVsv(*ep) : newSV(0));
    50          
2247             }
2248 33           (void)hv_store_ent(outh, ck, newRV_noinc((SV*)dst), 0);
2249             }
2250 16           out = (SV*)outh;
2251 5 100         } else if (kind == 2) {
2252 2           HV *restrict outh = newHV(), *restrict h = (HV*)rv;
2253             HE *restrict e;
2254 2           hv_iterinit(h);
2255 9 100         while ((e = hv_iternext(h))) {
2256 7           SV *restrict rk = hv_iterkeysv(e);
2257 7           HV *restrict row = (HV*)SvRV(hv_iterval(h, e)), *restrict nr = newHV();
2258             HE *restrict ie;
2259 7           hv_iterinit(row);
2260 23 100         while ((ie = hv_iternext(row))) {
2261 16           SV *restrict ck = hv_iterkeysv(ie);
2262 16 100         if (!hv_exists_ent(keepset, ck, 0)) continue;
2263 5           (void)hv_store_ent(nr, ck, newSVsv(HeVAL(ie)), 0);
2264             }
2265 7           (void)hv_store_ent(outh, rk, newRV_noinc((SV*)nr), 0);
2266             }
2267 2           out = (SV*)outh;
2268             } else {
2269 3           AV *restrict outa = newAV(), *restrict a = (AV*)rv;
2270 3           SSize_t n = av_len(a) + 1;
2271 12 100         for (SSize_t r = 0; r < n; r++) {
2272 9           HV *restrict row = (HV*)SvRV(*av_fetch(a, r, 0)), *restrict nr = newHV();
2273             HE *restrict ie;
2274 9           hv_iterinit(row);
2275 33 100         while ((ie = hv_iternext(row))) {
2276 24           SV *restrict ck = hv_iterkeysv(ie);
2277 24 100         if (!hv_exists_ent(keepset, ck, 0)) continue;
2278 9           (void)hv_store_ent(nr, ck, newSVsv(HeVAL(ie)), 0);
2279             }
2280 9           av_push(outa, newRV_noinc((SV*)nr));
2281             }
2282 3           out = (SV*)outa;
2283             }
2284             // 5. tidy up the scratch tables (the result keeps its own copies).
2285 21           SvREFCNT_dec((SV*)universe);
2286 21           SvREFCNT_dec((SV*)colnames);
2287 21           SvREFCNT_dec((SV*)keepset);
2288 21 100         if (cellmap) SvREFCNT_dec((SV*)cellmap);
2289 21           RETVAL = newRV_noinc(out);
2290             }
2291             OUTPUT:
2292             RETVAL
2293              
2294             SV *hoh2hoa(data, ...)
2295             SV *data
2296             CODE:
2297             {
2298             // 0. parse trailing name => value options (done before any allocation so
2299             // option/usage errors can't leak). undef.val sets the fill for a
2300             // missing key or an undef cell (default: undef). row.names, if given,
2301             // adds a column of that name holding the sorted row labels.
2302 20           SV *restrict fill = NULL; // NULL => fill gaps with undef
2303 20           SV *restrict rn_sv = NULL; // NULL => do not emit a row-names column
2304 20 100         if ((items - 1) & 1) croak("hoh2hoa: trailing options must be name => value pairs");
2305 27 100         for (int oi = 1; oi < items; oi += 2) {
2306             STRLEN ol;
2307 10           const char *restrict oname = SvPV(ST(oi), ol);
2308 10           SV *restrict oval = ST(oi + 1);
2309 10 100         if (ol == 9 && memEQ(oname, "undef.val", 9)) fill = SvOK(oval) ? oval : NULL;
    100          
    100          
2310 5 100         else if (ol == 9 && memEQ(oname, "row.names", 9)) {
    50          
2311 4 50         if (SvOK(oval) && !SvROK(oval)) rn_sv = oval;
    100          
2312 1           else croak("hoh2hoa: row.names must be a column name (string)");
2313             }
2314 1           else croak("hoh2hoa: unknown option '%s'", oname);
2315             }
2316             // 1. the input must be a hash ref (a hash of hashes).
2317 17 100         if (!SvROK(data) || SvTYPE(SvRV(data)) != SVt_PVHV) croak("hoh2hoa: data must be a hash ref (hash of hashes)");
    100          
2318 15           HV *restrict in_hv = (HV*)SvRV(data);
2319             // 2. these cross the section boundaries (gather -> build -> cleanup).
2320 15           HV *restrict out_hv = newHV(); // the result: column name -> array ref
2321 15           AV *restrict rows_av = newAV(); // outer keys, sorted into the row order
2322 15           AV *restrict cols_av = newAV(); // union of inner keys (column names)
2323 15           HV *restrict seen = newHV(); // membership test while taking the union
2324             // 3. collect the outer keys (row labels) and sort for a stable row order.
2325             {
2326             HE *restrict e;
2327 15           hv_iterinit(in_hv);
2328 39 100         while ((e = hv_iternext(in_hv))) {
2329 25           SV *restrict rv = hv_iterval(in_hv, e);
2330 25 50         if (!SvROK(rv) || SvTYPE(SvRV(rv)) != SVt_PVHV) croak("hoh2hoa: every value must be a hash ref (hash of hashes)");
    100          
2331 24           av_push(rows_av, newSVsv(hv_iterkeysv(e)));
2332             }
2333             }
2334 14           SSize_t nrows = av_len(rows_av) + 1;
2335 14 100         if (nrows > 1) qsort(AvARRAY(rows_av), (size_t)nrows, sizeof(SV*), h2h_keycmp);
2336             // 4. discover the union of inner keys. Each new column gets an empty array
2337             // in the result straight away so step 5 can just push into it.
2338             {
2339             HE *restrict e;
2340 14           hv_iterinit(in_hv);
2341 38 100         while ((e = hv_iternext(in_hv))) {
2342 24           HV *restrict row = (HV*)SvRV(hv_iterval(in_hv, e));
2343             HE *restrict ie;
2344 24           hv_iterinit(row);
2345 88 100         while ((ie = hv_iternext(row))) {
2346 40           SV *restrict ck = hv_iterkeysv(ie);
2347 40 100         if (!hv_exists_ent(seen, ck, 0)) {
2348 26           (void)hv_store_ent(seen, ck, &PL_sv_yes, 0);
2349 26           av_push(cols_av, newSVsv(ck));
2350 26           (void)hv_store_ent(out_hv, ck, newRV_noinc((SV*)newAV()), 0);
2351             }
2352             }
2353             }
2354             }
2355 14           SSize_t ncols = av_len(cols_av) + 1;
2356             // 5. walk the rows in sorted order; for every column push the cell (a copy)
2357             // or the fill value, so each column ends up exactly nrows long.
2358 38 100         for (SSize_t r = 0; r < nrows; r++) {
2359 24           SV *restrict rk = *av_fetch(rows_av, r, 0);
2360 24           HE *restrict rhe = hv_fetch_ent(in_hv, rk, 0, 0);
2361 24           HV *restrict row = (HV*)SvRV(HeVAL(rhe));
2362 75 100         for (SSize_t c = 0; c < ncols; c++) {
2363 51           SV *restrict ck = *av_fetch(cols_av, c, 0);
2364 51           HE *restrict che = hv_fetch_ent(row, ck, 0, 0);
2365 51 100         SV *restrict src = che ? HeVAL(che) : NULL;
2366 51 100         SV *restrict cell = (src && SvOK(src)) ? newSVsv(src) : (fill ? newSVsv(fill) : newSV(0));
    100          
    100          
2367 51           HE *restrict colhe = hv_fetch_ent(out_hv, ck, 0, 0);
2368 51           av_push((AV*)SvRV(HeVAL(colhe)), cell);
2369             }
2370             }
2371             // 6. optional row-names column: the sorted labels under the requested name.
2372 14 100         if (rn_sv) {
2373 3 100         if (hv_exists_ent(out_hv, rn_sv, 0)) croak("hoh2hoa: row.names column '%s' collides with an existing column", SvPV_nolen(rn_sv));
2374 2           AV *restrict rn_av = newAV();
2375 4 100         for (SSize_t r = 0; r < nrows; r++) av_push(rn_av, newSVsv(*av_fetch(rows_av, r, 0)));
2376 2           (void)hv_store_ent(out_hv, rn_sv, newRV_noinc((SV*)rn_av), 0);
2377             }
2378             // 7. tidy up the scratch structures (the result keeps its own copies).
2379 13           SvREFCNT_dec((SV*)rows_av);
2380 13           SvREFCNT_dec((SV*)cols_av);
2381 13           SvREFCNT_dec((SV*)seen);
2382 13           RETVAL = newRV_noinc((SV*)out_hv);
2383             }
2384             OUTPUT:
2385             RETVAL
2386              
2387             void filter(df, pred)
2388             SV *df
2389             SV *pred
2390             PPCODE:
2391             {
2392 27 50         if (!df || !SvROK(df))
    100          
2393 1           croak("filter: first argument must be a HASH or ARRAY reference (a data frame)");
2394 26 50         bool is_code = (pred && SvROK(pred) && SvTYPE(SvRV(pred)) == SVt_PVCV);
    100          
    100          
2395 26 100         if (!is_code && (!pred || !SvROK(pred) || SvTYPE(SvRV(pred)) != SVt_PVHV))
    50          
    100          
    50          
2396 1           croak("filter: second argument must be a CODE ref or a predicate built with col()");
2397 25           SV *restrict ref = SvRV(df);
2398             SV *restrict result;
2399 25 100         if (SvTYPE(ref) == SVt_PVAV) {
2400             // ----- Array of Hashes: keep matching row hashrefs (shared, not copied) -----
2401 20           AV *restrict in = (AV*)ref;
2402 20           AV *restrict out = newAV();
2403 20           SSize_t n = av_len(in) + 1, i;
2404 20           filt_ctx ctx; ctx.is_aoh = 1; ctx.data_hv = NULL; ctx.idx = 0;
2405 92 100         for (i = 0; i < n; i++) {
2406 73           SV **restrict rp = av_fetch(in, i, 0);
2407 73 50         if (!rp || !*rp || !SvROK(*rp) || SvTYPE(SvRV(*rp)) != SVt_PVHV) {
    50          
    100          
    50          
2408 1           SvREFCNT_dec((SV*)out);
2409 1           croak("filter: array data frame must hold HASH references; element %ld is not one", (long)i);
2410             }
2411             bool keep;
2412 72 100         if (is_code) keep = filt_call(aTHX_ pred, *rp);
2413 64           else { ctx.row_hv = (HV*)SvRV(*rp); keep = filt_eval(aTHX_ pred, &ctx); }
2414 72 100         if (keep) av_push(out, SvREFCNT_inc_simple_NN(*rp));
2415             }
2416 19           result = newRV_noinc((SV*)out);
2417 5 50         } else if (SvTYPE(ref) == SVt_PVHV) {
2418             // ----- Hash of Arrays: keep matching row indices across every column -----
2419 5           HV *restrict in = (HV*)ref;
2420 5           I32 ncols = hv_iterinit(in);
2421 5 50         if (ncols <= 0) {
2422 0           result = newRV_noinc((SV*)newHV());
2423             } else {
2424 5           char **restrict names = (char**)safemalloc(ncols * sizeof(char*));
2425 5           STRLEN *restrict nlens = (STRLEN*)safemalloc(ncols * sizeof(STRLEN));
2426 5           AV **restrict inav = (AV**)safemalloc(ncols * sizeof(AV*));
2427 5           AV **restrict outav = (AV**)safemalloc(ncols * sizeof(AV*));
2428 5           HV *restrict out = newHV();
2429 5           SSize_t maxrows = 0, i;
2430 5           I32 c = 0, cc;
2431             HE *restrict e;
2432 17 100         while ((e = hv_iternext(in)) && c < ncols) {
    50          
2433             STRLEN klen;
2434 13 50         char *restrict k = HePV(e, klen);
2435 13           SV *restrict v = HeVAL(e);
2436 13 50         if (!v || !SvROK(v) || SvTYPE(SvRV(v)) != SVt_PVAV) {
    100          
    50          
2437 1           safefree(names); safefree(nlens); safefree(inav); safefree(outav);
2438 1           SvREFCNT_dec((SV*)out);
2439 1           croak("filter: hash data frame must hold ARRAY references (a hash of arrays); column '%s' is not one", k);
2440             }
2441 12           AV *restrict a = (AV*)SvRV(v);
2442 12           SSize_t len = av_len(a) + 1;
2443 12 100         if (len > maxrows) maxrows = len;
2444 12           names[c] = k; nlens[c] = klen; inav[c] = a;
2445 12           outav[c] = newAV();
2446 12           hv_store(out, k, klen, newRV_noinc((SV*)outav[c]), 0);
2447 12           c++;
2448             }
2449 4           filt_ctx ctx; ctx.is_aoh = 0; ctx.row_hv = NULL; ctx.data_hv = in;
2450 20 100         for (i = 0; i < maxrows; i++) {
2451             bool keep;
2452 16 100         if (is_code) {
2453 4           HV *restrict rowh = newHV();
2454 16 100         for (cc = 0; cc < ncols; cc++) {
2455 12           SV **restrict vp = av_fetch(inav[cc], i, 0);
2456 12 50         hv_store(rowh, names[cc], nlens[cc], newSVsv((vp && *vp) ? *vp : &PL_sv_undef), 0);
    50          
2457             }
2458 4           SV *restrict rowrv = newRV_noinc((SV*)rowh);
2459 4           keep = filt_call(aTHX_ pred, rowrv);
2460 4           SvREFCNT_dec(rowrv);
2461             } else {
2462 12           ctx.idx = i;
2463 12           keep = filt_eval(aTHX_ pred, &ctx);
2464             }
2465 16 100         if (keep) {
2466 28 100         for (cc = 0; cc < ncols; cc++) {
2467 21           SV **restrict vp = av_fetch(inav[cc], i, 0);
2468 21 50         av_push(outav[cc], newSVsv((vp && *vp) ? *vp : &PL_sv_undef));
    50          
2469             }
2470             }
2471             }
2472 4           safefree(names); safefree(nlens); safefree(inav); safefree(outav);
2473 4           result = newRV_noinc((SV*)out);
2474             }
2475             } else {
2476 0           croak("filter: unsupported data frame; expected an array of hashes (AoH) or a hash of arrays (HoA)");
2477             }
2478 23           ST(0) = sv_2mortal(result);
2479 23           XSRETURN(1);
2480             }
2481              
2482             SV *col2col(data, cmd, cols = &PL_sv_undef, ...)
2483             SV *data
2484             SV *cmd
2485             SV *cols
2486             CODE:
2487             {
2488             // Only these cross the section boundaries (build -> loop -> cleanup);
2489             // everything else is declared at its point of use just below.
2490 51           SV *restrict cv_sv = NULL;
2491 51           size_t ncols = 0, nrows = 0;
2492 51           AV *restrict names_av = newAV();
2493 51           NV **restrict col_val = NULL;
2494 51           char **restrict col_def = NULL;
2495 51           short int na_mode = 0; // 0 = pairwise, 1 = omit, 2 = keep; see section 0
2496 51           bool skip_errors = TRUE; // skip.errors (default true): trap a croaking block, store its message
2497             // 0. options. They may be given either as trailing name => value pairs
2498             // (after the positional cols), or - so no placeholder is needed when
2499             // there is no column restriction - as a single hash ref in cols's
2500             // place, e.g. col2col($data, 'cor', { 'skip.errors' => 1 }).
2501             // `na` controls how undef is handled when one column is paired with
2502             // another:
2503             // 'pairwise' (default) - a row counts for the (a,b) pair only if
2504             // BOTH columns are defined there, so the block gets two equal
2505             // length, aligned columns. This is what paired stats (cor) want.
2506             // 'omit' - each column independently drops its own undef values,
2507             // so the two columns may differ in length. This is what unpaired
2508             // tests (t_test, kruskal_test) want: a gap in one column must not
2509             // throw away a good value in the other.
2510             // 'keep' - every row passes through and undef reaches the block.
2511             // rm.undef / rm.na (bool) remain as aliases: true => 'pairwise' (the
2512             // old default), false => 'keep'.
2513             // skip.errors (bool, default true): a block that croaks for a pair
2514             // does not abort col2col; instead the first line of its error message
2515             // is stored as that cell's value, so the result shows which
2516             // (outer => inner) pair failed and why. Set it false to make a croak
2517             // propagate and abort the whole call instead.
2518 51           SV *restrict cols_eff = cols;
2519 51           bool na_set = FALSE, rm_set = FALSE;
2520             #define C2C_DECODE_OPT(ONAME, OL, OVAL) do { \
2521             if ((OL) == 2 && memEQ((ONAME), "na", 2)) { \
2522             STRLEN vl_; const char *restrict nv_ = SvPV((OVAL), vl_); \
2523             if (vl_ == 8 && memEQ(nv_, "pairwise", 8)) na_mode = 0; \
2524             else if (vl_ == 4 && memEQ(nv_, "omit", 4)) na_mode = 1; \
2525             else if (vl_ == 4 && memEQ(nv_, "keep", 4)) na_mode = 2; \
2526             else croak("col2col: na must be 'pairwise', 'omit' or 'keep'"); \
2527             na_set = TRUE; \
2528             } else if (((OL) == 8 && memEQ((ONAME), "rm.undef", 8)) || ((OL) == 5 && memEQ((ONAME), "rm.na", 5))) { \
2529             na_mode = cBOOL(SvTRUE((OVAL))) ? 0 : 2; rm_set = TRUE; \
2530             } else if ((OL) == 11 && memEQ((ONAME), "skip.errors", 11)) { \
2531             skip_errors = cBOOL(SvTRUE((OVAL))); \
2532             } else croak("col2col: unknown option '%s'", (ONAME)); \
2533             } while (0)
2534 51 100         if (SvROK(cols) && SvTYPE(SvRV(cols)) == SVt_PVHV) {
    100          
2535             // options supplied as a hash ref instead of cols: no column restriction
2536 6           HV *restrict oh = (HV*)SvRV(cols);
2537             HE *restrict he;
2538 6 100         if (items > 3) croak("col2col: an options hash ref must be the last argument");
2539 5           hv_iterinit(oh);
2540 8 100         while ((he = hv_iternext(oh))) {
2541             STRLEN ol;
2542 5 50         const char *restrict oname = HePV(he, ol);
2543 5           SV *restrict oval = HeVAL(he);
2544 5 100         C2C_DECODE_OPT(oname, ol, oval);
    50          
    50          
    0          
    50          
    50          
    0          
    0          
    50          
    0          
    100          
    50          
    0          
    100          
    50          
2545             }
2546 3           cols_eff = &PL_sv_undef;
2547 45 100         } else if (items > 3) {
2548 18 100         if ((items - 3) & 1) croak("col2col: trailing options must be name => value pairs");
2549 33 100         for (int oi = 3; oi < items; oi += 2) {
2550             STRLEN ol;
2551 18           const char *restrict oname = SvPV(ST(oi), ol);
2552 18           SV *restrict oval = ST(oi + 1);
2553 18 100         C2C_DECODE_OPT(oname, ol, oval);
    50          
    100          
    50          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    50          
    100          
    100          
    50          
2554             }
2555             }
2556 45 100         if (na_set && rm_set) croak("col2col: give na or rm.undef, not both");
    100          
2557             #undef C2C_DECODE_OPT
2558             // 1. resolve the command: a CODE block or a function name. Either way
2559             // we end up with the CV to call as $cv->($col_a, $col_b).
2560 44 100         if (SvROK(cmd) && SvTYPE(SvRV(cmd)) == SVt_PVCV) cv_sv = SvRV(cmd);
    100          
2561 4 100         else if (SvOK(cmd) && !SvROK(cmd)) {
    100          
2562             STRLEN nl;
2563 2           const char *restrict name = SvPV(cmd, nl);
2564 2 50         SV *restrict fq = strstr(name, "::") ? newSVpvn(name, nl) : newSVpvf("Stats::LikeR::%s", name);
2565 2           CV *restrict cv = get_cv(SvPV_nolen(fq), 0);
2566 2           SvREFCNT_dec(fq);
2567 2 100         if (!cv) croak("col2col: unknown function '%s'", name);
2568 1           cv_sv = (SV*)cv;
2569 2           } else croak("col2col: command must be a CODE ref or a function name");
2570             // 2. detect the data shape and build per-column value/defined tables.
2571 41 100         if (!SvROK(data)) croak("col2col: data must be a reference");
2572             {
2573 40           SV *restrict rv = SvRV(data);
2574             short int kind;
2575 40 100         if (SvTYPE(rv) == SVt_PVAV) kind = 1;
2576 38 50         else if (SvTYPE(rv) == SVt_PVHV) {
2577 38           HV *restrict h = (HV*)rv;
2578 38           hv_iterinit(h);
2579 38           HE *restrict e = hv_iternext(h);
2580 38 50         if (!e) croak("col2col: empty data hash");
2581 38           SV *restrict first = hv_iterval(h, e);
2582 38 50         if (SvROK(first) && SvTYPE(SvRV(first)) == SVt_PVAV) kind = 0;
    100          
2583 1 50         else if (SvROK(first) && SvTYPE(SvRV(first)) == SVt_PVHV) kind = 2;
    50          
2584 0           else croak("col2col: hash values must be array refs (HoA) or hash refs (HoH)");
2585             }
2586 0           else croak("col2col: data must be an array ref or hash ref");
2587 40 100         if (kind == 0) {
2588             // hash of arrays: names = keys, rows = longest column.
2589 37           HV *restrict h = (HV*)rv;
2590 37           AV **restrict src = NULL;
2591             HE *restrict e;
2592 37           hv_iterinit(h);
2593 129 100         while ((e = hv_iternext(h))) {
2594 92           SV *restrict val = hv_iterval(h, e);
2595 92 50         if (!SvROK(val) || SvTYPE(SvRV(val)) != SVt_PVAV) continue;
    50          
2596 92           av_push(names_av, newSVsv(hv_iterkeysv(e)));
2597 92           AV *restrict a = (AV*)SvRV(val);
2598 92           size_t len = (size_t)(av_len(a) + 1);
2599 92 100         if (len > nrows) nrows = len;
2600 92 50         Renew(src, av_len(names_av) + 1, AV*);
2601 92           src[av_len(names_av)] = a;
2602             }
2603 37           ncols = (size_t)(av_len(names_av) + 1);
2604 37 50         Newxz(col_val, ncols ? ncols : 1, NV*);
    50          
    50          
2605 37 50         Newxz(col_def, ncols ? ncols : 1, char*);
    50          
    50          
2606 129 100         for (size_t cc = 0; cc < ncols; cc++) {
2607 92 50         Newxz(col_val[cc], nrows ? nrows : 1, NV);
    50          
    50          
2608 92 50         Newxz(col_def[cc], nrows ? nrows : 1, char);
2609 92           AV *restrict a = src[cc];
2610 518 100         for (size_t r = 0; r < nrows; r++) {
2611             NV v;
2612 426 100         if (c2c_num(aTHX_ av_fetch(a, (SSize_t)r, 0), &v)) { col_val[cc][r] = v; col_def[cc][r] = 1; }
2613             }
2614             }
2615 37           Safefree(src);
2616             } else {
2617             // row-major (array of hashes / hash of hashes): union of keys.
2618 3           HV **restrict row_hv = NULL;
2619 3 100         if (kind == 1) {
2620 2           AV *restrict a = (AV*)rv;
2621 2           nrows = (size_t)(av_len(a) + 1);
2622 2 50         Newxz(row_hv, nrows ? nrows : 1, HV*);
    50          
    50          
2623 10 100         for (size_t r = 0; r < nrows; r++) {
2624 8           SV **restrict ep = av_fetch(a, (SSize_t)r, 0);
2625 8 50         if (ep && *ep && SvROK(*ep) && SvTYPE(SvRV(*ep)) == SVt_PVHV) row_hv[r] = (HV*)SvRV(*ep);
    50          
    100          
    50          
2626             }
2627             } else {
2628 1           HV *restrict h = (HV*)rv;
2629             HE *restrict e;
2630 1           size_t r = 0;
2631 1 50         nrows = (size_t)HvKEYS(h);
2632 1 50         Newxz(row_hv, nrows ? nrows : 1, HV*);
    50          
    50          
2633 1           hv_iterinit(h);
2634 6 100         while ((e = hv_iternext(h)) && r < nrows) {
    50          
2635 5           SV *restrict val = hv_iterval(h, e);
2636 5 50         if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVHV) row_hv[r] = (HV*)SvRV(val);
    50          
2637 5           r++;
2638             }
2639             }
2640             {
2641 3           HV *restrict seen = newHV();
2642 16 100         for (size_t r = 0; r < nrows; r++) {
2643 13 100         if (!row_hv[r]) continue;
2644             HE *restrict e;
2645 10           hv_iterinit(row_hv[r]);
2646 40 100         while ((e = hv_iternext(row_hv[r]))) {
2647             STRLEN kl;
2648 30 50         char *restrict k = HePV(e, kl);
2649 30 100         if (!hv_exists(seen, k, kl)) { (void)hv_store(seen, k, kl, &PL_sv_yes, 0); av_push(names_av, newSVsv(hv_iterkeysv(e))); }
2650             }
2651             }
2652 3           SvREFCNT_dec((SV*)seen);
2653             }
2654 3           ncols = (size_t)(av_len(names_av) + 1);
2655 3 100         Newxz(col_val, ncols ? ncols : 1, NV*);
    50          
    100          
2656 3 100         Newxz(col_def, ncols ? ncols : 1, char*);
    50          
    100          
2657 9 100         for (size_t cc = 0; cc < ncols; cc++) {
2658             STRLEN kl;
2659 6           char *restrict k = SvPV(*av_fetch(names_av, (SSize_t)cc, 0), kl);
2660 6 50         Newxz(col_val[cc], nrows ? nrows : 1, NV);
    50          
    50          
2661 6 50         Newxz(col_def[cc], nrows ? nrows : 1, char);
2662 36 100         for (size_t r = 0; r < nrows; r++) {
2663             NV v;
2664 30 50         if (!row_hv[r]) continue;
2665 30 50         if (c2c_num(aTHX_ hv_fetch(row_hv[r], k, kl, 0), &v)) { col_val[cc][r] = v; col_def[cc][r] = 1; }
2666             }
2667             }
2668 3           Safefree(row_hv);
2669             }
2670             }
2671 40 100         if (ncols == 0) croak("col2col: no usable columns found");
2672             // 3. flatten the column names for fast hv_store keys in the loop.
2673             SV **restrict col_names;
2674             STRLEN *restrict name_len;
2675 39 50         Newx(col_names, ncols, SV*);
2676 39 50         Newx(name_len, ncols, STRLEN);
2677 137 100         for (size_t cc = 0; cc < ncols; cc++) {
2678 98           col_names[cc] = *av_fetch(names_av, (SSize_t)cc, 0);
2679 98           (void)SvPV(col_names[cc], name_len[cc]);
2680             }
2681             // 3b. decide which columns may be col_a (the outer/"from" side). With no
2682             // restriction every column qualifies; a name or list narrows it.
2683             char *restrict is_outer;
2684 39           Newxz(is_outer, ncols, char);
2685 39 100         if (!SvOK(cols_eff)) {
2686 118 100         for (size_t cc = 0; cc < ncols; cc++) is_outer[cc] = 1;
2687             }
2688 6 100         else if (SvROK(cols_eff) && SvTYPE(SvRV(cols_eff)) == SVt_PVAV) {
    50          
2689 2           AV *restrict want = (AV*)SvRV(cols_eff);
2690 2           SSize_t n = av_len(want) + 1;
2691 5 100         for (SSize_t i = 0; i < n; i++) {
2692 4           SV **restrict ep = av_fetch(want, i, 0);
2693             STRLEN wl;
2694             const char *restrict wname;
2695 4 50         if (!ep || !*ep || !SvOK(*ep)) croak("col2col: column list contains an undefined entry");
    50          
    50          
2696 4           wname = SvPV(*ep, wl);
2697 4 100         if (!c2c_mark(col_names, name_len, ncols, wname, wl, is_outer)) croak("col2col: column '%s' not found in data", wname);
2698             }
2699 3 50         } else if (!SvROK(cols_eff)) {
2700             STRLEN wl;
2701 3           const char *restrict wname = SvPV(cols_eff, wl);
2702 3 100         if (!c2c_mark(col_names, name_len, ncols, wname, wl, is_outer)) croak("col2col: column '%s' not found in data", wname);
2703 0           } else croak("col2col: cols must be a column name or an array ref of names");
2704             // 4. each selected column vs every other column. The two columns reach
2705             // the block as @_ = ($col_a, $col_b); how undef is handled depends on
2706             // na (section 0): 'pairwise' drops a row missing in either side (equal
2707             // aligned lengths, for cor); 'omit' drops each column's own undef
2708             // independently (lengths may differ, for t_test / kruskal_test);
2709             // 'keep' passes every row through with undef in the gaps.
2710 37           HV *restrict out_hv = newHV();
2711 127 100         for (size_t a = 0; a < ncols; a++) {
2712             HV *restrict inner;
2713 91 100         if (!is_outer[a]) continue;
2714 87           inner = newHV();
2715 308 100         for (size_t b = 0; b < ncols; b++) {
2716             AV *restrict ca, *restrict cb;
2717             SV *restrict rv1, *restrict rv2, *restrict res;
2718 222 100         if (a == b) continue;
2719 136           ca = newAV();
2720 136           cb = newAV();
2721 136 100         if (na_mode == 0) { // pairwise complete: keep rows defined in both
2722 648 100         for (size_t r = 0; r < nrows; r++)
2723 531 100         if (col_def[a][r] && col_def[b][r]) { av_push(ca, newSVnv(col_val[a][r])); av_push(cb, newSVnv(col_val[b][r])); }
    100          
2724 19 100         } else if (na_mode == 1) { // omit: each column drops its own undef (lengths may differ)
2725 44 100         for (size_t r = 0; r < nrows; r++) if (col_def[a][r]) av_push(ca, newSVnv(col_val[a][r]));
    100          
2726 44 100         for (size_t r = 0; r < nrows; r++) if (col_def[b][r]) av_push(cb, newSVnv(col_val[b][r]));
    100          
2727             } else { // keep: every row, undef passed through
2728 66 100         for (size_t r = 0; r < nrows; r++) {
2729 55 100         av_push(ca, col_def[a][r] ? newSVnv(col_val[a][r]) : newSV(0));
2730 55 100         av_push(cb, col_def[b][r] ? newSVnv(col_val[b][r]) : newSV(0));
2731             }
2732             }
2733 136           rv1 = newRV_noinc((SV*)ca);
2734 136           rv2 = newRV_noinc((SV*)cb);
2735 136 100         if (av_len(ca) < 0 || av_len(cb) < 0) {
    100          
2736 2           res = newSV(0); // a column had no usable values for this pair
2737 134 100         } else if (!skip_errors) {
2738 5           res = c2c_call(aTHX_ cv_sv, rv1, rv2); // a croak here propagates
2739             } else {
2740             // skip.errors: run the block under eval; on a croak keep the
2741             // first line of its message as this cell so the caller sees
2742             // which pair failed and why instead of the whole call dying.
2743 129           dSP;
2744             int n;
2745 129           ENTER; SAVETMPS;
2746 129 50         PUSHMARK(SP);
2747 129 50         XPUSHs(rv1); XPUSHs(rv2);
    50          
2748 129           PUTBACK;
2749 129           n = call_sv(cv_sv, G_SCALAR | G_EVAL);
2750 129           SPAGAIN;
2751 129 50         if (SvTRUE(ERRSV)) {
    100          
2752             STRLEN el;
2753 8 50         const char *restrict ep = SvPV(ERRSV, el);
2754 8           STRLEN ll = 0; // length of the first line only
2755 132 50         while (ll < el && ep[ll] != '\n' && ep[ll] != '\r') ll++;
    100          
    50          
2756 8           res = newSVpvn(ep, ll);
2757 8 50         if (n > 0) (void)POPs; // discard the undef G_SCALAR leaves
2758             } else {
2759 121 50         res = (n > 0) ? newSVsv(POPs) : newSV(0);
2760             }
2761 129           PUTBACK;
2762 129 50         FREETMPS; LEAVE;
2763             }
2764 135           (void)hv_store(inner, SvPVX(col_names[b]), (I32)name_len[b], res, 0);
2765 135           SvREFCNT_dec(rv1);
2766 135           SvREFCNT_dec(rv2);
2767             }
2768 86           (void)hv_store(out_hv, SvPVX(col_names[a]), (I32)name_len[a], newRV_noinc((SV*)inner), 0);
2769             }
2770             // 5. tidy up.
2771 125 100         for (size_t cc = 0; cc < ncols; cc++) { Safefree(col_val[cc]); Safefree(col_def[cc]); }
2772 36           Safefree(col_val); Safefree(col_def); Safefree(col_names);
2773 36           Safefree(name_len); Safefree(is_outer); SvREFCNT_dec((SV*)names_av);
2774 36           RETVAL = newRV_noinc((SV*)out_hv);
2775             }
2776             OUTPUT:
2777             RETVAL
2778              
2779             SV *oneway_test(data_ref, ...)
2780             SV *data_ref
2781             PREINIT:
2782 6           HV *restrict in_hv = NULL;
2783 6           AV *restrict in_av = NULL;
2784             HE *restrict he;
2785 6           bool var_equal = 0;
2786 6           const char *restrict formula_str = NULL;
2787 6           const char *restrict factor_name = "Group";
2788 6           char *lhs = NULL, *rhs = NULL;
2789 6           NV *restrict flat = NULL;
2790 6           size_t *restrict sizes = NULL;
2791 6           char ** gnames = NULL;
2792 6           NV *restrict gmeans = NULL;
2793 6           size_t k = 0;
2794 6           IV total_n = 0;
2795             OneWayResult res;
2796             HV *restrict ret_hv;
2797             char errbuf[512];
2798             CODE:
2799             // parse named arguments
2800 10 100         for (I32 ai = 1; ai + 1 < items; ai += 2) {
2801 4           const char *restrict key = SvPV_nolen(ST(ai));
2802 4           SV *restrict val = ST(ai + 1);
2803 4 50         if (strEQ(key, "var_equal"))
2804 0           var_equal = SvTRUE(val) ? 1 : 0;
2805 4 50         else if (strEQ(key, "formula"))
2806 4           formula_str = SvPV_nolen(val);
2807             }
2808             // validate data_ref and determine if it's an Array or Hash
2809 6 50         if (!SvROK(data_ref))
2810 0           croak("oneway_test: first argument must be a hash or array reference");
2811 6           SV *restrict rv = SvRV(data_ref);
2812 6 100         if (SvTYPE(rv) == SVt_PVHV) {
2813 5           in_hv = (HV *)rv;
2814 1 50         } else if (SvTYPE(rv) == SVt_PVAV) {
2815 1           in_av = (AV *)rv;
2816             } else {
2817 0           croak("oneway_test: first argument must be a hash or array reference");
2818             }
2819 6 100         if (in_av) {
2820             // MODE 3 – Array of Arrays (AoA)
2821 1 50         if (formula_str != NULL)
2822 0           croak("oneway_test: formula mode is not supported with an array of arrays");
2823              
2824 1           k = (size_t)av_len(in_av) + 1;
2825 1 50         if (k < 2)
2826 0           croak("oneway_test: need at least 2 groups, got %zu", k);
2827 1           sizes = (size_t *)safemalloc(k * sizeof(size_t));
2828 1           gnames = (char **)safemalloc(k * sizeof(char *));
2829             // first pass: sizes, total_n, and generate index names
2830 3 100         for (size_t g = 0; g < k; g++) {
2831 2           SV **restrict val = av_fetch(in_av, (I32)g, 0);
2832 2 50         if (!val || !*val || !SvROK(*val) || SvTYPE(SvRV(*val)) != SVt_PVAV)
    50          
    50          
    50          
2833 0           croak("oneway_test: index %zu is not an array reference", g);
2834 2           IV len = av_len((AV *)SvRV(*val)) + 1;
2835 2 50         if (len < 2)
2836 0           croak("oneway_test: index %zu has fewer than 2 observations", g);
2837 2           sizes[g] = (size_t)len;
2838 2           total_n += (IV)len;
2839             /* synthesize group names: "Index 0", "Index 1", ... to match 0-based index */
2840             char buf[64];
2841 2           snprintf(buf, sizeof(buf), "Index %zu", g);
2842 2           size_t klen = strlen(buf);
2843 2           gnames[g] = (char *)safemalloc(klen + 1);
2844 2           memcpy(gnames[g], buf, klen + 1);
2845             }
2846             // second pass: fill flat array
2847 1           flat = (NV *)safemalloc((size_t)total_n * sizeof(NV));
2848 1           size_t offset = 0;
2849 3 100         for (size_t g = 0; g < k; g++) {
2850 2           SV **restrict val = av_fetch(in_av, (I32)g, 0);
2851 2           AV *restrict av = (AV *)SvRV(*val);
2852 2           IV len = av_len(av) + 1;
2853 14 100         for (IV i = 0; i < len; i++) {
2854 12           SV **restrict svp = av_fetch(av, i, 0);
2855 12 50         flat[offset++] = (svp && *svp) ? SvNV(*svp) : 0.0;
    50          
2856             }
2857             }
2858 5 100         } else if (formula_str != NULL) {// MODE 2 – formula "response ~ factor"
2859 4 100         if (!parse_formula(formula_str, &lhs, &rhs))
2860 1           croak("oneway_test: cannot parse formula '%s' — "
2861             "expected 'response ~ factor'", formula_str);
2862 3           factor_name = rhs; /* use the actual factor variable name */
2863 3           SV **restrict resp_svp = hv_fetch(in_hv, lhs, (I32)strlen(lhs), 0);
2864 3 100         if (!resp_svp || !*resp_svp || !SvROK(*resp_svp)
    50          
    50          
2865 2 50         || SvTYPE(SvRV(*resp_svp)) != SVt_PVAV)
2866 1           croak("oneway_test: formula LHS '%s' not found as an array ref "
2867             "in the hash", lhs);
2868 2           SV **restrict fact_svp = hv_fetch(in_hv, rhs, (I32)strlen(rhs), 0);
2869 2 50         if (!fact_svp || !*fact_svp || !SvROK(*fact_svp)
    50          
    50          
2870 2 50         || SvTYPE(SvRV(*fact_svp)) != SVt_PVAV)
2871 0           croak("oneway_test: formula RHS '%s' not found as an array ref "
2872             "in the hash", rhs);
2873 2           AV *restrict resp_av = (AV *)SvRV(*resp_svp);
2874 2           AV *restrict label_av = (AV *)SvRV(*fact_svp);
2875 2           IV n = av_len(resp_av) + 1;
2876 2           flat = (NV *)safemalloc((size_t)n * sizeof(NV));
2877 2           sizes = (size_t *)safemalloc((size_t)n * sizeof(size_t));
2878 2 100         if (!build_groups_from_formula(aTHX_ resp_av, label_av,
2879             flat, sizes, &k, &gnames,
2880             errbuf, sizeof errbuf)) {
2881 1           Safefree(flat); Safefree(sizes); Safefree(lhs); Safefree(rhs);
2882 1           croak("oneway_test: %s", errbuf);
2883             }
2884 3 100         for (size_t g = 0; g < k; g++) total_n += (IV)sizes[g];
2885             } else {
2886             /* MODE 1 – hash of groups { label => \@observations, … } */
2887 1           k = (size_t)hv_iterinit(in_hv);
2888 1 50         if (k < 2)
2889 0           croak("oneway_test: need at least 2 groups, got %zu", k);
2890 1           sizes = (size_t *)safemalloc(k * sizeof(size_t));
2891 1           gnames = (char **)safemalloc(k * sizeof(char *));
2892             /* first pass: sizes, total_n, and group name strings */
2893             {
2894 1           size_t g = 0;
2895 3 100         while ((he = hv_iternext(in_hv)) != NULL) {
2896 2           SV *restrict val = HeVAL(he);
2897 2 50         if (!SvROK(val) || SvTYPE(SvRV(val)) != SVt_PVAV)
    50          
2898 0 0         croak("oneway_test: value for group '%s' is not an array ref",
2899             HePV(he, PL_na));
2900 2           IV len = av_len((AV *)SvRV(val)) + 1;
2901 2 50         if (len < 2)
2902 0 0         croak("oneway_test: group '%s' has fewer than 2 observations",
2903             HePV(he, PL_na));
2904 2           sizes[g] = (size_t)len;
2905 2           total_n += (IV)len;
2906             /* save a copy of the key string */
2907             STRLEN klen;
2908 2 50         const char *kstr = HePV(he, klen);
2909 2           gnames[g] = (char *)safemalloc(klen + 1);
2910 2           memcpy(gnames[g], kstr, klen + 1);
2911 2           g++;
2912             }
2913             }
2914             // second pass: fill flat in the same iteration order
2915 1           flat = (NV *)safemalloc((size_t)total_n * sizeof(NV));
2916             {
2917 1           size_t offset = 0;
2918 1           hv_iterinit(in_hv);
2919 3 100         while ((he = hv_iternext(in_hv)) != NULL) {
2920 2           AV *restrict av = (AV *)SvRV(HeVAL(he));
2921 2           IV len = av_len(av) + 1;
2922 14 100         for (IV i = 0; i < len; i++) {
2923 12           SV **restrict svp = av_fetch(av, i, 0);
2924 12 50         flat[offset++] = (svp && *svp) ? SvNV(*svp) : 0.0;
    50          
2925             }
2926             }
2927             }
2928             }
2929             // per-group means from flat (before c_oneway_test frees nothing)
2930 3           gmeans = (NV *)safemalloc(k * sizeof(NV));
2931             {
2932 3           size_t offset = 0;
2933 9 100         for (size_t g = 0; g < k; g++) {
2934 6           NV sum = 0.0;
2935 36 100         for (size_t i = 0; i < sizes[g]; i++) sum += flat[offset + i];
2936 6           gmeans[g] = sum / (NV)sizes[g];
2937 6           offset += sizes[g];
2938             }
2939             }
2940             // run the arithmetic
2941 3           res = c_oneway_test(flat, sizes, k, var_equal);
2942 3           Safefree(flat);
2943 3 100         if (lhs) Safefree(lhs);
2944             /* rhs kept alive as factor_name until after output */
2945             /* ── build return hash ref
2946             * { *
2947             * => { Df, "Sum Sq", "Mean Sq", "F value", "Pr(>F)" } *
2948             * Residuals => { Df, "Sum Sq", "Mean Sq" } *
2949             * group_stats => { mean => { g => v, … }, size => { g => n, … } } *
2950             * }*/
2951 3           ret_hv = (HV *)sv_2mortal((SV *)newHV());
2952             /* Group (factor) sub-hash */
2953             {
2954 3           HV *restrict g_hv = newHV();
2955 3           hv_stores(g_hv, "Df", newSVnv(res.num_df));
2956 3           hv_stores(g_hv, "Sum Sq", newSVnv(res.ss_between));
2957 3           hv_stores(g_hv, "Mean Sq", newSVnv(res.ms_between));
2958 3           hv_stores(g_hv, "F value", newSVnv(res.statistic));
2959 3           hv_stores(g_hv, "Pr(>F)", newSVnv(res.p_value));
2960 3           hv_store(ret_hv, factor_name, (I32)strlen(factor_name),
2961             newRV_noinc((SV *)g_hv), 0);
2962             }
2963             /* Residuals sub-hash */
2964             {
2965 3           HV *restrict r_hv = newHV();
2966 3           hv_stores(r_hv, "Df", newSVnv(res.denom_df));
2967 3           hv_stores(r_hv, "Sum Sq", newSVnv(res.ss_within));
2968 3           hv_stores(r_hv, "Mean Sq", newSVnv(res.ms_within));
2969 3           hv_stores(ret_hv, "Residuals", newRV_noinc((SV *)r_hv));
2970             }
2971             /* group_stats sub-hash */
2972             {
2973 3           HV *restrict gs_hv = newHV();
2974 3           HV *restrict mean_hv = newHV();
2975 3           HV *restrict size_hv = newHV();
2976 9 100         for (size_t g = 0; g < k; g++) {
2977 6           const char *restrict gn = gnames[g];
2978 6           I32 gnl = (I32)strlen(gn);
2979 6           hv_store(mean_hv, gn, gnl, newSVnv(gmeans[g]), 0);
2980 6           hv_store(size_hv, gn, gnl, newSViv((IV)sizes[g]), 0);
2981             }
2982 3           hv_stores(gs_hv, "mean", newRV_noinc((SV *)mean_hv));
2983 3           hv_stores(gs_hv, "size", newRV_noinc((SV *)size_hv));
2984 3           hv_stores(ret_hv, "group_stats", newRV_noinc((SV *)gs_hv));
2985             }
2986             // clean up
2987 3           Safefree(gmeans); Safefree(sizes);
2988 9 100         for (size_t g = 0; g < k; g++) Safefree(gnames[g]);
2989 3           Safefree(gnames);
2990 3 100         if (rhs) Safefree(rhs);
2991             // freed here, after factor_name is no longer needed
2992 3           RETVAL = newRV((SV *)ret_hv);
2993             OUTPUT:
2994             RETVAL
2995              
2996             SV* ks_test(...)
2997             CODE:
2998             {
2999 10           SV *restrict x_sv = NULL, *restrict y_sv = NULL;
3000 10           short int exact = -1;
3001 10           const char *restrict alternative = "two.sided";
3002 10           int arg_idx = 0;
3003              
3004             // Shift arrays if provided positionally
3005 10 50         if (arg_idx < items && SvROK(ST(arg_idx)) && SvTYPE(SvRV(ST(arg_idx))) == SVt_PVAV) {
    50          
    50          
3006 10           x_sv = ST(arg_idx);
3007 10           arg_idx++;
3008             }
3009             // Check if second argument is an array (2-sample) or a string representing a CDF (1-sample)
3010 10 50         if (arg_idx < items) {
3011 10 100         if (SvROK(ST(arg_idx)) && SvTYPE(SvRV(ST(arg_idx))) == SVt_PVAV) {
    50          
3012 9           y_sv = ST(arg_idx);
3013 9           arg_idx++;
3014 1 50         } else if (SvPOK(ST(arg_idx))) {
3015 1           y_sv = ST(arg_idx); // Save string (e.g., "pnorm") for 1-sample test logic
3016 1           arg_idx++;
3017             }
3018             }
3019             // Parse named arguments
3020 12 100         for (; arg_idx < items; arg_idx += 2) {
3021 2           const char *restrict key = SvPV_nolen(ST(arg_idx));
3022 2           SV *restrict val = ST(arg_idx + 1);
3023 2 50         if (strEQ(key, "x")) x_sv = val;
3024 2 50         else if (strEQ(key, "y")) y_sv = val;
3025 2 50         else if (strEQ(key, "exact")) {
3026 0 0         if (!SvOK(val)) exact = -1;
3027 0           else exact = SvTRUE(val) ? 1 : 0;
3028             }
3029 2 50         else if (strEQ(key, "alternative")) alternative = SvPV_nolen(val);
3030 0           else croak("ks_test: unknown argument '%s'", key);
3031             }
3032              
3033 10 50         if (!x_sv || !SvROK(x_sv) || SvTYPE(SvRV(x_sv)) != SVt_PVAV) {
    50          
    50          
3034 0           croak("ks_test: 'x' is a required argument and must be an ARRAY reference");
3035             }
3036              
3037 10           bool is_two_sided = strEQ(alternative, "two.sided") ? 1 : 0;
3038 10           bool is_greater = strEQ(alternative, "greater") ? 1 : 0;
3039 10           bool is_less = strEQ(alternative, "less") ? 1 : 0;
3040              
3041 10 100         if (!is_two_sided && !is_greater && !is_less) {
    100          
    50          
3042 0           croak("ks_test: alternative must be 'two.sided', 'less', or 'greater'");
3043             }
3044              
3045 10           AV *restrict x_av = (AV*)SvRV(x_sv);
3046 10           size_t nx = av_len(x_av) + 1;
3047 10 50         if (nx == 0) croak("Not enough 'x' observations");
3048              
3049             // Extract 'x' array to C-array
3050 10           NV *restrict x_data = (NV *)safemalloc(nx * sizeof(NV));
3051 10           size_t valid_nx = 0;
3052 240 100         for (size_t i = 0; i < nx; i++) {
3053 230           SV**restrict el = av_fetch(x_av, i, 0);
3054 230 50         if (el && SvOK(*el) && looks_like_number(*el)) {
    50          
    50          
3055 230           x_data[valid_nx++] = SvNV(*el);
3056             }
3057             }
3058 10           NV statistic = 0.0, p_value = 0.0;
3059 10           const char *restrict method_desc = "";
3060             // --- TWO SAMPLE ---
3061 19 50         if (y_sv && SvROK(y_sv) && SvTYPE(SvRV(y_sv)) == SVt_PVAV) {
    100          
    50          
3062 9           AV *restrict y_av = (AV*)SvRV(y_sv);
3063 9           size_t ny = av_len(y_av) + 1;
3064 9           NV *restrict y_data = (NV *)safemalloc(ny * sizeof(NV));
3065 9           size_t valid_ny = 0;
3066 129 100         for (size_t i = 0; i < ny; i++) {
3067 120           SV**restrict el = av_fetch(y_av, i, 0);
3068 120 50         if (el && SvOK(*el) && looks_like_number(*el)) {
    50          
    50          
3069 120           y_data[valid_ny++] = SvNV(*el);
3070             }
3071             }
3072 9 50         if (valid_nx < 1 || valid_ny < 1) {
    50          
3073 0           Safefree(x_data); Safefree(y_data);
3074 0           croak("Not enough non-missing observations for KS test");
3075             }
3076             NV d, d_plus, d_minus;
3077 9           calc_2sample_stats(x_data, valid_nx, y_data, valid_ny, &d, &d_plus, &d_minus);
3078             // Map alternative to the correct statistic
3079 9 100         if (is_greater) statistic = d_plus;
3080 8 100         else if (is_less) statistic = d_minus;
3081 7           else statistic = d;
3082             // Determine if exact or asymptotic
3083 9           bool use_exact = FALSE;
3084 9 50         if (exact == 1) use_exact = TRUE;
3085 9 50         else if (exact == 0) use_exact = FALSE;
3086 9           else use_exact = (valid_nx * valid_ny < 10000);
3087             // Check for ties in combined set
3088 9           size_t total_n = valid_nx + valid_ny;
3089 9           NV *restrict comb = (NV *)safemalloc(total_n * sizeof(NV));
3090 189 100         for(size_t i=0; i
3091 129 100         for(size_t i=0; i
3092 9           qsort(comb, total_n, sizeof(NV), compare_NVs);
3093 9           bool has_ties = FALSE;
3094 300 100         for(size_t i = 1; i < total_n; i++) {
3095 291 50         if(comb[i] == comb[i-1]) { has_ties = TRUE; break; }
3096             }
3097 9           Safefree(comb);
3098 9 50         if (use_exact && has_ties) {
    50          
3099 0           warn("ks_test: cannot compute exact p-value with ties; falling back to asymptotic");
3100 0           use_exact = FALSE;
3101             }
3102 9 50         if (use_exact) {
3103 9           method_desc = "Two-sample Kolmogorov-Smirnov exact test";
3104 9           NV q = (0.5 + floor(statistic * valid_nx * valid_ny - 1e-7)) / ((NV)valid_nx * valid_ny);
3105 9           p_value = psmirnov_exact_uniq_upper(q, valid_nx, valid_ny, is_two_sided);
3106             } else {
3107 0           method_desc = "Two-sample Kolmogorov-Smirnov test (asymptotic)";
3108 0           NV z = statistic * sqrt((NV)(valid_nx * valid_ny) / (valid_nx + valid_ny));
3109 0 0         if (is_two_sided) {
3110 0           p_value = K2l(z, 0, 1e-9);
3111             } else {
3112 0           p_value = exp(-2.0 * z * z); // One-sided limit distribution
3113             }
3114             }
3115 9           Safefree(y_data);
3116 2 50         } else if (y_sv && SvPOK(y_sv)) {// --- ONE SAMPLE (e.g. against pnorm) ---
    50          
3117 1           const char *restrict dist = SvPV_nolen(y_sv);
3118 1 50         if (strEQ(dist, "pnorm")) {
3119 1           qsort(x_data, valid_nx, sizeof(NV), compare_NVs);
3120 1           NV max_d = 0.0, max_d_plus = 0.0, max_d_minus = 0.0;
3121 51 100         for(size_t i = 0; i < valid_nx; i++) {
3122 50           NV cdf_obs_low = (NV)i / valid_nx;
3123 50           NV cdf_obs_high = (NV)(i + 1) / valid_nx;
3124 50           NV cdf_theor = approx_pnorm(x_data[i]);
3125 50           NV diff1 = cdf_obs_low - cdf_theor;
3126 50           NV diff2 = cdf_obs_high - cdf_theor;
3127 50 50         if (diff1 > max_d_plus) max_d_plus = diff1;
3128 50 100         if (diff2 > max_d_plus) max_d_plus = diff2;
3129 50 100         if (-diff1 > max_d_minus) max_d_minus = -diff1;
3130 50 50         if (-diff2 > max_d_minus) max_d_minus = -diff2;
3131 50 100         if (fabs(diff1) > max_d) max_d = fabs(diff1);
3132 50 50         if (fabs(diff2) > max_d) max_d = fabs(diff2);
3133             }
3134 1 50         if (is_greater) statistic = max_d_plus;
3135 1 50         else if (is_less) statistic = max_d_minus;
3136 1           else statistic = max_d;
3137 1 50         bool use_exact = (exact == -1) ? (valid_nx < 100) : (exact == 1);
3138 1 50         if (use_exact) {
3139 1           method_desc = "One-sample Kolmogorov-Smirnov exact test";
3140 1 50         if (is_two_sided) {
3141 1           p_value = 1.0 - K2x(valid_nx, statistic);
3142             } else {
3143 0           warn("exact 1-sample 1-sided KS test not implemented; using asymptotic");
3144 0           NV z = statistic * sqrt((NV)valid_nx);
3145 0           p_value = exp(-2.0 * z * z);
3146             }
3147             } else {
3148 0           method_desc = "One-sample Kolmogorov-Smirnov test (asymptotic)";
3149 0           NV z = statistic * sqrt((NV)valid_nx);
3150 0 0         if (is_two_sided) p_value = K2l(z, 0, 1e-6);
3151 0           else p_value = exp(-2.0 * z * z);
3152             }
3153             } else {
3154 0           Safefree(x_data);
3155 0           croak("ks_test: Unsupported 1-sample distribution '%s'. Use arrays for 2-sample.", dist);
3156             }
3157             } else {
3158 0           Safefree(x_data);
3159 0           croak("ks_test: Invalid arguments for 'y'.");
3160             }
3161 10           Safefree(x_data);
3162 10 50         if (p_value > 1.0) p_value = 1.0;
3163 10 50         if (p_value < 0.0) p_value = 0.0;
3164 10           HV *restrict res = newHV();
3165 10           hv_stores(res, "statistic", newSVnv(statistic));
3166 10           hv_stores(res, "p_value", newSVnv(p_value));
3167 10           hv_stores(res, "method", newSVpv(method_desc, 0));
3168 10           hv_stores(res, "alternative", newSVpv(alternative, 0));
3169 10           RETVAL = newRV_noinc((SV*)res);
3170             }
3171             OUTPUT:
3172             RETVAL
3173              
3174             SV* wilcox_test(...)
3175             CODE:
3176             {
3177 10           SV *restrict x_sv = NULL, *restrict y_sv = NULL;
3178 10           bool paired = FALSE, correct = TRUE;
3179 10           NV mu = 0.0;
3180 10           short int exact = -1;
3181 10           const char *restrict alternative = "two.sided";
3182 10           int arg_idx = 0;
3183             // 1. Shift first positional argument as 'x' if it's an array reference
3184 10 50         if (arg_idx < items && SvROK(ST(arg_idx)) && SvTYPE(SvRV(ST(arg_idx))) == SVt_PVAV) {
    100          
    50          
3185 2           x_sv = ST(arg_idx);
3186 2           arg_idx++;
3187             }
3188             // 2. Shift second positional argument as 'y' if it's an array reference
3189 10 50         if (arg_idx < items && SvROK(ST(arg_idx)) && SvTYPE(SvRV(ST(arg_idx))) == SVt_PVAV) {
    100          
    50          
3190 2           y_sv = ST(arg_idx);
3191 2           arg_idx++;
3192             }
3193             // Ensure the remaining arguments form complete key-value pairs
3194 10 50         if ((items - arg_idx) % 2 != 0) {
3195 0           croak("Usage: wilcox_test(\\@x, [\\@y], key => value, ...)");
3196             }
3197             // --- Parse named arguments from the remaining flat stack ---
3198 30 100         for (; arg_idx < items; arg_idx += 2) {
3199 20           const char *restrict key = SvPV_nolen(ST(arg_idx));
3200 20           SV *restrict val = ST(arg_idx + 1);
3201 20 100         if (strEQ(key, "x")) x_sv = val;
3202 13 100         else if (strEQ(key, "y")) y_sv = val;
3203 6 100         else if (strEQ(key, "paired")) paired = SvTRUE(val);
3204 3 50         else if (strEQ(key, "correct")) correct = SvTRUE(val);
3205 3 100         else if (strEQ(key, "mu")) mu = SvNV(val);
3206 2 50         else if (strEQ(key, "exact")) {
3207 0 0         if (!SvOK(val)) exact = -1;
3208 0           else exact = SvTRUE(val) ? 1 : 0;
3209             }
3210 2 50         else if (strEQ(key, "alternative")) alternative = SvPV_nolen(val);
3211 0           else croak("wilcox_test: unknown argument '%s'", key);
3212             }
3213             // --- Validate required / types ---
3214 10 100         if (!x_sv || !SvROK(x_sv) || SvTYPE(SvRV(x_sv)) != SVt_PVAV)
    50          
    50          
3215 1           croak("wilcox_test: 'x' is a required argument and must be an ARRAY reference");
3216 9           AV *restrict x_av = (AV*)SvRV(x_sv);
3217 9           size_t nx = av_len(x_av) + 1;
3218 9 50         if (nx == 0) croak("Not enough 'x' observations");
3219              
3220 9           AV *restrict y_av = NULL;
3221 9           size_t ny = 0;
3222 9 100         if (y_sv && SvROK(y_sv) && SvTYPE(SvRV(y_sv)) == SVt_PVAV) {
    50          
    50          
3223 8           y_av = (AV*)SvRV(y_sv);
3224 8           ny = av_len(y_av) + 1;
3225             }
3226 9           NV p_value = 0.0, statistic = 0.0;
3227 9           const char *restrict method_desc = "";
3228 9           bool use_exact = FALSE;
3229             // --- TWO SAMPLE (Mann-Whitney) ---
3230 14 100         if (ny > 0 && !paired) {
    100          
3231 5           RankInfo *restrict ri = (RankInfo *)safemalloc((nx + ny) * sizeof(RankInfo));
3232 5           size_t valid_nx = 0, valid_ny = 0;
3233 33 100         for (size_t i = 0; i < nx; i++) {
3234 28           SV**restrict el = av_fetch(x_av, i, 0);
3235 28 50         if (el && SvOK(*el) && looks_like_number(*el)) {
    50          
    50          
3236 28           ri[valid_nx].val = SvNV(*el) - mu; // R subtracts mu from x
3237 28           ri[valid_nx].idx = 1;
3238 28           valid_nx++;
3239             }
3240             }
3241 33 100         for (size_t i = 0; i < ny; i++) {
3242 28           SV**restrict el = av_fetch(y_av, i, 0);
3243 28 50         if (el && SvOK(*el) && looks_like_number(*el)) {
    50          
    50          
3244 28           ri[valid_nx + valid_ny].val = SvNV(*el);
3245 28           ri[valid_nx + valid_ny].idx = 2;
3246 28           valid_ny++;
3247             }
3248             }
3249 5 50         if (valid_nx == 0) { Safefree(ri); croak("not enough (non-missing) 'x' observations"); }
3250 5 50         if (valid_ny == 0) { Safefree(ri); croak("not enough 'y' observations"); }
3251 5           size_t total_n = valid_nx + valid_ny;
3252 5           bool has_ties = 0;
3253 5           NV tie_adj = rank_and_count_ties(ri, total_n, &has_ties);
3254 5           NV w_rank_sum = 0.0;
3255 61 100         for (size_t i = 0; i < total_n; i++) if (ri[i].idx == 1) w_rank_sum += ri[i].rank;
    100          
3256 5           statistic = w_rank_sum - (NV)valid_nx * (valid_nx + 1.0) / 2.0;
3257 5 50         if (exact == 1) use_exact = TRUE;
3258 5 50         else if (exact == 0) use_exact = FALSE;
3259 5 50         else use_exact = (valid_nx < 50 && valid_ny < 50 && !has_ties);
    50          
    100          
3260 5 100         if (use_exact && has_ties) {
    50          
3261 0           warn("wilcox_test: cannot compute exact p-value with ties; falling back to approximation");
3262 0           use_exact = FALSE;
3263             }
3264 5 100         if (use_exact) {
3265 2           method_desc = "Wilcoxon rank sum exact test";
3266 2           NV p_less = exact_pwilcox(statistic, valid_nx, valid_ny);
3267 2           NV p_greater = 1.0 - exact_pwilcox(statistic - 1.0, valid_nx, valid_ny);
3268              
3269 2 100         if (strcmp(alternative, "less") == 0) p_value = p_less;
3270 1 50         else if (strcmp(alternative, "greater") == 0) p_value = p_greater;
3271             else {
3272 0 0         NV p = (p_less < p_greater) ? p_less : p_greater;
3273 0           p_value = 2.0 * p;
3274             }
3275             } else {
3276 3 50         method_desc = correct ? "Wilcoxon rank sum test with continuity correction" : "Wilcoxon rank sum test";
3277 3           NV exp = (NV)valid_nx * valid_ny / 2.0;
3278 3           NV var = ((NV)valid_nx * valid_ny / 12.0) * ((total_n + 1.0) - tie_adj / (total_n * (total_n - 1.0)));
3279 3           NV z = statistic - exp;
3280            
3281 3           NV CORRECTION = 0.0;
3282 3 50         if (correct) {
3283 3 50         if (strcmp(alternative, "two.sided") == 0) CORRECTION = (z > 0 ? 0.5 : -0.5);
    100          
3284 0 0         else if (strcmp(alternative, "greater") == 0) CORRECTION = 0.5;
3285 0 0         else if (strcmp(alternative, "less") == 0) CORRECTION = -0.5;
3286             }
3287 3           z = (z - CORRECTION) / sqrt(var);
3288              
3289 3 50         if (strcmp(alternative, "less") == 0) p_value = approx_pnorm(z);
3290 3 50         else if (strcmp(alternative, "greater") == 0) p_value = 1.0 - approx_pnorm(z);
3291 3           else p_value = 2.0 * approx_pnorm(-fabs(z));
3292             }
3293 5           Safefree(ri);
3294             } else { // --- ONE SAMPLE / PAIRED ---
3295 4 100         if (paired && (!y_av || nx != ny)) croak("'x' and 'y' must have the same length for paired test");
    50          
    100          
3296 3           NV *restrict diffs = (NV *)safemalloc(nx * sizeof(NV));
3297 3           size_t n_nz = 0;
3298 3           bool has_zeroes = FALSE;
3299 26 100         for (size_t i = 0; i < nx; i++) {
3300 23           SV**restrict x_el = av_fetch(x_av, i, 0);
3301 23 50         if (!x_el || !SvOK(*x_el) || !looks_like_number(*x_el)) continue;
    50          
    50          
3302 23           NV dx = SvNV(*x_el);
3303              
3304 23 100         if (paired) {
3305 18           SV**restrict y_el = av_fetch(y_av, i, 0);
3306 18 50         if (!y_el || !SvOK(*y_el) || !looks_like_number(*y_el)) continue;
    50          
    50          
3307 18           NV dy = SvNV(*y_el);
3308 18           NV d = dx - dy - mu;
3309 18 50         if (d == 0.0) has_zeroes = TRUE; // Drop exact zeroes
3310 18           else diffs[n_nz++] = d;
3311             } else {
3312 5           NV d = dx - mu;
3313 5 50         if (d == 0.0) has_zeroes = TRUE;
3314 5           else diffs[n_nz++] = d;
3315             }
3316             }
3317 3 50         if (n_nz == 0) {
3318 0           Safefree(diffs);
3319 0           croak("not enough (non-missing) observations");
3320             }
3321 3           RankInfo *restrict ri = (RankInfo *)safemalloc(n_nz * sizeof(RankInfo));
3322 26 100         for (size_t i = 0; i < n_nz; i++) {
3323 23           ri[i].val = fabs(diffs[i]);
3324 23           ri[i].idx = (diffs[i] > 0);
3325             }
3326 3           bool has_ties = 0;
3327 3           NV tie_adj = rank_and_count_ties(ri, n_nz, &has_ties);
3328 3           statistic = 0.0;
3329 26 100         for (size_t i = 0; i < n_nz; i++) {
3330 23 100         if (ri[i].idx) statistic += ri[i].rank;
3331             }
3332 3 50         if (exact == 1) use_exact = TRUE;
3333 3 50         else if (exact == 0) use_exact = FALSE;
3334 3 50         else use_exact = (n_nz < 50 && !has_ties);
    50          
3335 3 50         if (use_exact && has_ties) {
    50          
3336 0           warn("cannot compute exact p-value with ties; falling back to approximation");
3337 0           use_exact = FALSE;
3338             }
3339 3 50         if (use_exact && has_zeroes) {
    50          
3340 0           warn("cannot compute exact p-value with zeroes; falling back to approximation");
3341 0           use_exact = FALSE;
3342             }
3343 3 50         if (use_exact) {
3344 3           method_desc = paired ? "Wilcoxon exact signed rank test" : "Wilcoxon exact signed rank test";
3345 3           NV p_less = exact_psignrank(statistic, n_nz);
3346 3           NV p_greater = 1.0 - exact_psignrank(statistic - 1.0, n_nz);
3347              
3348 3 50         if (strcmp(alternative, "less") == 0) p_value = p_less;
3349 3 50         else if (strcmp(alternative, "greater") == 0) p_value = p_greater;
3350             else {
3351 3 50         NV p = (p_less < p_greater) ? p_less : p_greater;
3352 3           p_value = 2.0 * p;
3353             }
3354             } else {
3355 0 0         method_desc = correct ? "Wilcoxon signed rank test with continuity correction" : "Wilcoxon signed rank test";
3356 0           NV exp = (NV)n_nz * (n_nz + 1.0) / 4.0;
3357 0           NV var = (n_nz * (n_nz + 1.0) * (2.0 * n_nz + 1.0) / 24.0) - (tie_adj / 48.0);
3358 0           NV z = statistic - exp;
3359 0           NV CORRECTION = 0.0;
3360 0 0         if (correct) {
3361 0 0         if (strcmp(alternative, "two.sided") == 0) CORRECTION = (z > 0 ? 0.5 : -0.5);
    0          
3362 0 0         else if (strcmp(alternative, "greater") == 0) CORRECTION = 0.5;
3363 0 0         else if (strcmp(alternative, "less") == 0) CORRECTION = -0.5;
3364             }
3365 0           z = (z - CORRECTION) / sqrt(var);
3366              
3367 0 0         if (strcmp(alternative, "less") == 0) p_value = approx_pnorm(z);
3368 0 0         else if (strcmp(alternative, "greater") == 0) p_value = 1.0 - approx_pnorm(z);
3369 0           else p_value = 2.0 * approx_pnorm(-fabs(z));
3370             }
3371 3           Safefree(ri); Safefree(diffs);
3372             }
3373 8 50         if (p_value > 1.0) p_value = 1.0;
3374 8           HV *restrict res = newHV();
3375 8           hv_stores(res, "statistic", newSVnv(statistic));
3376 8           hv_stores(res, "p_value", newSVnv(p_value));
3377 8           hv_stores(res, "method", newSVpv(method_desc, 0));
3378 8           hv_stores(res, "alternative", newSVpv(alternative, 0));
3379 8           RETVAL = newRV_noinc((SV*)res);
3380             }
3381             OUTPUT:
3382             RETVAL
3383              
3384             SV* chisq_test(data_ref)
3385             SV* data_ref;
3386             CODE:
3387             {
3388             // 1. Input Validation & Data Matrix Construction
3389 16 100         if (!SvROK(data_ref)) {
3390 3           croak("Input must be a reference");
3391             }
3392              
3393 13           svtype input_type = SvTYPE(SvRV(data_ref));
3394 13 100         if (input_type != SVt_PVAV && input_type != SVt_PVHV) {
    100          
3395 1           croak("Input must be an array reference or a hash reference");
3396             }
3397              
3398 12           NV **restrict obs_matrix = NULL;
3399 12           NV *restrict obs_array = NULL;
3400 12           AV*restrict row_keys = NULL;
3401 12           AV*restrict col_keys = NULL;
3402 12           unsigned int r = 0, c = 0;
3403 12           bool is_2d = 0;
3404              
3405 12 100         if (input_type == SVt_PVAV) {
3406 8           AV*restrict obs_av = (AV*)SvRV(data_ref);
3407 8 50         r = av_top_index(obs_av) + 1;
3408 8 100         if (r > 0) {
3409 7           SV**restrict first_elem = av_fetch(obs_av, 0, 0);
3410 7 50         if (first_elem && SvROK(*first_elem) && SvTYPE(SvRV(*first_elem)) == SVt_PVAV) {
    100          
    50          
3411 4           is_2d = 1;
3412 4 50         c = av_top_index((AV*)SvRV(*first_elem)) + 1;
3413 4           obs_matrix = (NV**)safemalloc(r * sizeof(NV*));
3414 12 100         for (unsigned int i = 0; i < r; i++) {
3415 8           obs_matrix[i] = (NV*)safecalloc(c, sizeof(NV));
3416 8           SV**restrict row_sv = av_fetch(obs_av, i, 0);
3417 8 50         if (row_sv && SvROK(*row_sv)) {
    50          
3418 8           AV*restrict row_av = (AV*)SvRV(*row_sv);
3419 28 100         for (unsigned int j = 0; j < c; j++) {
3420 20           SV**restrict val_sv = av_fetch(row_av, j, 0);
3421 20 50         if (val_sv) obs_matrix[i][j] = SvNV(*val_sv);
3422             }
3423             }
3424             }
3425             } else {
3426 3           c = r;
3427 3           r = 1;
3428 3           obs_array = (NV*)safemalloc(c * sizeof(NV));
3429 9 100         for (unsigned int j = 0; j < c; j++) {
3430 7           SV**restrict val_sv = av_fetch(obs_av, j, 0);
3431 7 50         if (val_sv) obs_array[j] = SvNV(*val_sv);
3432             }
3433             }
3434             }
3435 4 50         } else if (input_type == SVt_PVHV) {
3436 4           HV*restrict obs_hv = (HV*)SvRV(data_ref);
3437 4           row_keys = newAV();
3438 4           col_keys = newAV();
3439              
3440             HE*restrict first_entry;
3441 4           hv_iterinit(obs_hv);
3442 4           first_entry = hv_iternext(obs_hv);
3443              
3444 4 100         if (first_entry) {
3445 3           SV*restrict first_val = hv_iterval(obs_hv, first_entry);
3446 4 100         if (SvROK(first_val) && SvTYPE(SvRV(first_val)) == SVt_PVHV) {
    50          
3447 1           is_2d = 1;
3448 1           HV*restrict col_idx_map = newHV();
3449 1           hv_iterinit(obs_hv);
3450             HE*restrict row_entry;
3451 3 100         while ((row_entry = hv_iternext(obs_hv))) {
3452 2           av_push(row_keys, newSVsv(hv_iterkeysv(row_entry)));
3453 2           r++;
3454 2           SV*restrict inner_sv = hv_iterval(obs_hv, row_entry);
3455 2 50         if (SvROK(inner_sv) && SvTYPE(SvRV(inner_sv)) == SVt_PVHV) {
    50          
3456 2           HV*restrict inner_hv = (HV*)SvRV(inner_sv);
3457             HE*restrict col_entry;
3458 2           hv_iterinit(inner_hv);
3459 8 100         while ((col_entry = hv_iternext(inner_hv))) {
3460 4           SV*restrict col_key = hv_iterkeysv(col_entry);
3461 4 100         if (!hv_exists_ent(col_idx_map, col_key, 0)) {
3462 2           hv_store_ent(col_idx_map, col_key, newSViv(c), 0);
3463 2           av_push(col_keys, newSVsv(col_key));
3464 2           c++;
3465             }
3466             }
3467             }
3468             }
3469              
3470 1           obs_matrix = (NV**)safemalloc(r * sizeof(NV*));
3471 3 100         for (unsigned int i = 0; i < r; i++) {
3472 2           obs_matrix[i] = (NV*)safecalloc(c, sizeof(NV));
3473 2           SV**restrict row_key_sv = av_fetch(row_keys, i, 0);
3474            
3475             // FIX 1: Extract HE* instead of SV**
3476 2           HE* inner_he = hv_fetch_ent(obs_hv, *row_key_sv, 0, 0);
3477 2 50         if (inner_he) {
3478 2           SV*restrict inner_sv = HeVAL(inner_he);
3479 2 50         if (SvROK(inner_sv)) {
3480 2           HV*restrict inner_hv = (HV*)SvRV(inner_sv);
3481 6 100         for (unsigned int j = 0; j < c; j++) {
3482 4           SV**restrict col_key_sv = av_fetch(col_keys, j, 0);
3483            
3484             // FIX 2: Extract HE* instead of SV**
3485 4           HE*restrict val_he = hv_fetch_ent(inner_hv, *col_key_sv, 0, 0);
3486 4 50         if (val_he) {
3487 4           obs_matrix[i][j] = SvNV(HeVAL(val_he));
3488             }
3489             }
3490             }
3491             }
3492             }
3493 1           SvREFCNT_dec(col_idx_map);
3494             } else {
3495             // 1D Hash Handling
3496 2           hv_iterinit(obs_hv);
3497             HE*restrict row_entry;
3498 6 100         while ((row_entry = hv_iternext(obs_hv))) {
3499 4           av_push(col_keys, newSVsv(hv_iterkeysv(row_entry)));
3500 4           c++;
3501             }
3502 2           obs_array = (NV*)safemalloc(c * sizeof(NV));
3503 5 100         for (unsigned int j = 0; j < c; j++) {
3504 4           SV**restrict col_key_sv = av_fetch(col_keys, j, 0);
3505             // FIX 3: Extract HE* instead of SV**
3506 4           HE*restrict val_he = hv_fetch_ent(obs_hv, *col_key_sv, 0, 0);
3507 4 50         if (val_he) {
3508 4           obs_array[j] = SvNV(HeVAL(val_he));
3509             }
3510             }
3511             }
3512             }
3513             }
3514              
3515 10 100         if ((is_2d && (r == 0 || c == 0)) || (!is_2d && c == 0)) {
    50          
    50          
    100          
    100          
3516 2           croak("Empty data structure");
3517             }
3518              
3519             // 2. Perform Math Algorithm
3520 8           NV stat = 0.0, grand_total = 0.0;
3521 8           unsigned int df = 0;
3522 8 100         bool yates = (is_2d && r == 2 && c == 2) ? 1 : 0;
    50          
    100          
3523 8           SV*restrict expected_ref = NULL;
3524              
3525 8 100         if (is_2d) {
3526 5           NV *restrict row_sum = (NV*)safemalloc(r * sizeof(NV));
3527 5           NV *restrict col_sum = (NV*)safemalloc(c * sizeof(NV));
3528 15 100         for(unsigned int i=0; i
3529 17 100         for(unsigned int j=0; j
3530              
3531 15 100         for (unsigned int i = 0; i < r; i++) {
3532 34 100         for (unsigned int j = 0; j < c; j++) {
3533 24           NV val = obs_matrix[i][j];
3534 24           row_sum[i] += val;
3535 24           col_sum[j] += val;
3536 24           grand_total += val;
3537             }
3538             }
3539              
3540 5 100         if (input_type == SVt_PVAV) {
3541 4           AV*restrict expected_av = newAV();
3542 12 100         for (unsigned int i = 0; i < r; i++) {
3543 8           AV*restrict exp_row = newAV();
3544 28 100         for (unsigned int j = 0; j < c; j++) {
3545 20           NV E = (row_sum[i] * col_sum[j]) / grand_total;
3546 20           NV O = obs_matrix[i][j];
3547 20           av_push(exp_row, newSVnv(E));
3548 20 100         if (yates) {
3549 8           NV abs_diff = fabs(O - E);
3550 8 50         NV y_corr = (abs_diff > 0.5) ? 0.5 : abs_diff;
3551 8           NV diff = abs_diff - y_corr;
3552 8           stat += (diff * diff) / E;
3553             } else {
3554 12           stat += ((O - E) * (O - E)) / E;
3555             }
3556             }
3557 8           av_push(expected_av, newRV_noinc((SV*)exp_row));
3558             }
3559 4           expected_ref = newRV_noinc((SV*)expected_av);
3560             } else { // SVt_PVHV
3561 1           HV*restrict expected_hv = newHV();
3562 3 100         for (unsigned int i = 0; i < r; i++) {
3563 2           HV*restrict exp_row = newHV();
3564 6 100         for (unsigned int j = 0; j < c; j++) {
3565 4           NV E = (row_sum[i] * col_sum[j]) / grand_total;
3566 4           NV O = obs_matrix[i][j];
3567 4           SV**restrict col_key_sv = av_fetch(col_keys, j, 0);
3568 4           hv_store_ent(exp_row, *col_key_sv, newSVnv(E), 0);
3569              
3570 4 50         if (yates) {
3571 4           NV abs_diff = fabs(O - E);
3572 4 50         NV y_corr = (abs_diff > 0.5) ? 0.5 : abs_diff;
3573 4           NV diff = abs_diff - y_corr;
3574 4           stat += (diff * diff) / E;
3575             } else {
3576 0           stat += ((O - E) * (O - E)) / E;
3577             }
3578             }
3579 2           SV**restrict row_key_sv = av_fetch(row_keys, i, 0);
3580 2           hv_store_ent(expected_hv, *row_key_sv, newRV_noinc((SV*)exp_row), 0);
3581             }
3582 1           expected_ref = newRV_noinc((SV*)expected_hv);
3583             }
3584 5           safefree(row_sum); safefree(col_sum);
3585 5           df = (r - 1) * (c - 1);
3586             } else {
3587 12 100         for (unsigned int j = 0; j < c; j++) {
3588 9           grand_total += obs_array[j];
3589             }
3590 3           NV E = grand_total / (NV)c;
3591              
3592 3 100         if (input_type == SVt_PVAV) {
3593 2           AV*restrict expected_av = newAV();
3594 8 100         for (unsigned int j = 0; j < c; j++) {
3595 6           NV O = obs_array[j];
3596 6           av_push(expected_av, newSVnv(E));
3597 6           stat += ((O - E) * (O - E)) / E;
3598             }
3599 2           expected_ref = newRV_noinc((SV*)expected_av);
3600             } else { // SVt_PVHV
3601 1           HV*restrict expected_hv = newHV();
3602 4 100         for (unsigned int j = 0; j < c; j++) {
3603 3           NV O = obs_array[j];
3604 3           SV**restrict col_key_sv = av_fetch(col_keys, j, 0);
3605 3           hv_store_ent(expected_hv, *col_key_sv, newSVnv(E), 0);
3606 3           stat += ((O - E) * (O - E)) / E;
3607             }
3608 1           expected_ref = newRV_noinc((SV*)expected_hv);
3609             }
3610 3           df = c - 1;
3611             }
3612              
3613             // Memory Cleanup for Matrices/Arrays
3614 8 100         if (obs_matrix) {
3615 15 100         for (unsigned int i = 0; i < r; i++) {
3616 10           safefree(obs_matrix[i]);
3617             }
3618 5           safefree(obs_matrix);
3619             }
3620 8 100         if (obs_array) safefree(obs_array);
3621 8 100         if (row_keys) SvREFCNT_dec(row_keys);
3622 8 100         if (col_keys) SvREFCNT_dec(col_keys);
3623              
3624 8           NV p_val = get_p_value(stat, df);
3625              
3626             // 3. Build the top-level results Hash (mimicking R's htest structure)
3627 8           HV*restrict results = newHV();
3628              
3629 8           HV*restrict statistic_hv = newHV();
3630 8           hv_store(statistic_hv, "X-squared", 9, newSVnv(stat), 0);
3631 8           hv_store(results, "statistic", 9, newRV_noinc((SV*)statistic_hv), 0);
3632              
3633 8           HV*restrict parameter_hv = newHV();
3634 8           hv_store(parameter_hv, "df", 2, newSViv(df), 0);
3635 8           hv_store(results, "parameter", 9, newRV_noinc((SV*)parameter_hv), 0);
3636              
3637 8           hv_store(results, "p.value", 7, newSVnv(p_val), 0);
3638 8           hv_store(results, "expected", 8, expected_ref, 0);
3639 8           hv_store(results, "observed", 8, SvREFCNT_inc(data_ref), 0);
3640              
3641 8 100         if (input_type == SVt_PVAV) {
3642 6           hv_store(results, "data.name", 9, newSVpv("Perl ArrayRef", 0), 0);
3643             } else {
3644 2           hv_store(results, "data.name", 9, newSVpv("Perl HashRef", 0), 0);
3645             }
3646              
3647 8 100         if (is_2d) {
3648 5 100         if (yates) {
3649 3           hv_store(results, "method", 6, newSVpv("Pearson's Chi-squared test with Yates' continuity correction", 0), 0);
3650             } else {
3651 2           hv_store(results, "method", 6, newSVpv("Pearson's Chi-squared test", 0), 0);
3652             }
3653             } else {
3654 3           hv_store(results, "method", 6, newSVpv("Chi-squared test for given probabilities", 0), 0);
3655             }
3656              
3657 8           RETVAL = newRV_noinc((SV*)results);
3658             }
3659             OUTPUT:
3660             RETVAL
3661              
3662             PROTOTYPES: ENABLE
3663              
3664             void write_table(...)
3665             PPCODE:
3666             {
3667 64           SV *restrict data_sv = NULL;
3668 64           SV *restrict file_sv = NULL;
3669 64           unsigned int arg_idx = 0;
3670             // Mimic the Perl shift logic
3671 64 100         if (arg_idx < items && SvROK(ST(arg_idx))) {
    100          
3672 62           int type = SvTYPE(SvRV(ST(arg_idx)));
3673 62 100         if (type == SVt_PVHV || type == SVt_PVAV) {
    50          
3674 62           data_sv = ST(arg_idx);
3675 62           arg_idx++;
3676             }
3677             }
3678             // Only consume a positional file argument if it is a plain string that is
3679             // NOT one of the named option keys. Otherwise write_table(data=>..., file=>...)
3680             // would grab the literal string "data" as the filename.
3681 64 100         if (arg_idx < items) {
3682 62           SV *restrict cand = ST(arg_idx);
3683 62 50         if (SvOK(cand) && !SvROK(cand)) {
    50          
3684 62           const char *restrict k = SvPV_nolen(cand);
3685 62 100         if (!(strEQ(k, "data") || strEQ(k, "file") || strEQ(k, "col.names") ||
    100          
    50          
3686 60 50         strEQ(k, "row.names") || strEQ(k, "sep") || strEQ(k, "delim") ||
    100          
    50          
3687 59 50         strEQ(k, "undef.val"))) {
3688 59           file_sv = cand;
3689 59           arg_idx++;
3690             }
3691             }
3692             }
3693 64           const char *restrict sep = ",";
3694 64           bool explicit_sep = 0; // Track if delimiter was manually specified
3695             // CHANGED: default undef cells to a true empty value ("") instead of NULL.
3696             // With print_string_row emitting zero-length fields bare (no quotes), an
3697             // undef cell now prints as nothing at all: a,,c -- not a,'',c or a,"",c.
3698             // 'undef.val' => 'NA' (etc.) still overrides this.
3699 64           const char *restrict undef_val = "";
3700 64           SV *restrict row_names_sv = sv_2mortal(newSViv(1));
3701 64           SV *restrict col_names_sv = NULL;
3702             // Read the remaining Hash-style arguments
3703 143 100         for (; arg_idx < items; arg_idx += 2) {
3704 82 100         if (arg_idx + 1 >= items) croak("write_table: Odd number of arguments passed");
3705 80           const char *restrict key = SvPV_nolen(ST(arg_idx));
3706 80           SV *restrict val = ST(arg_idx + 1);
3707 80 100         if (strEQ(key, "data")) data_sv = val;
3708 79 100         else if (strEQ(key, "col.names")) col_names_sv = val;
3709 63 100         else if (strEQ(key, "file")) file_sv = val;
3710 61 100         else if (strEQ(key, "row.names")) row_names_sv = val;
3711             // Check for either "sep" or "delim" and mark as explicitly provided
3712 43 100         else if (strEQ(key, "sep") || strEQ(key, "delim")) {
    100          
3713 18           sep = SvPV_nolen(val);
3714 18           explicit_sep = 1;
3715             }
3716             // FIX: 'undef.val' => undef used to call SvPV_nolen(&PL_sv_undef)
3717             // (warning + empty string by accident); make it explicit.
3718 25 100         else if (strEQ(key, "undef.val")) undef_val = SvOK(val) ? SvPV_nolen(val) : "";
    100          
3719 1           else croak("write_table: Unknown arguments passed: %s", key);
3720             }
3721 61 100         if (!data_sv || !SvROK(data_sv)) {
    50          
3722 1           croak("write_table: 'data' must be a HASH or ARRAY reference\n");
3723             }
3724 60           SV *restrict data_ref = SvRV(data_sv);
3725 60 100         if (SvTYPE(data_ref) != SVt_PVHV && SvTYPE(data_ref) != SVt_PVAV) {
    50          
3726 0           croak("write_table: 'data' must be a HASH or ARRAY reference\n");
3727             }
3728 60 100         if (!file_sv || !SvOK(file_sv)) croak("write_table: file name missing\n");
    50          
3729 59           const char *restrict file = SvPV_nolen(file_sv);
3730             // Auto-detect separator from file extension if not overridden
3731 59 100         if (!explicit_sep) {
3732 41           size_t file_len = strlen(file);
3733 41 50         if (file_len >= 4) {
3734 41           const char *restrict ext = file + file_len - 4;
3735 41 100         if (strEQ(ext, ".tsv") || strEQ(ext, ".TSV")) {
    50          
3736 3           sep = "\t";
3737 38 50         } else if (strEQ(ext, ".csv") || strEQ(ext, ".CSV")) {
    0          
3738 38           sep = ",";
3739             }
3740             }
3741             }
3742 59 100         if (col_names_sv && SvOK(col_names_sv)) {
    50          
3743 16 100         if (!SvROK(col_names_sv) || SvTYPE(SvRV(col_names_sv)) != SVt_PVAV) {
    50          
3744 2           croak("write_table: 'col.names' must be an ARRAY reference\n");
3745             }
3746             }
3747 57           bool is_hoh = 0, is_hoa = 0, is_aoh = 0, is_flat_hash = 0;
3748 57           AV *restrict rows_av = NULL;
3749             // Validate Input Structures & Homogeneity
3750 57 100         if (SvTYPE(data_ref) == SVt_PVHV) {
3751 48           HV *restrict hv = (HV*)data_ref;
3752 48 100         if (hv_iterinit(hv) == 0) XSRETURN_EMPTY;
3753 47           HE *restrict entry = hv_iternext(hv);
3754 47           SV *restrict first_val = hv_iterval(hv, entry);
3755              
3756 47 50         if (!first_val) {
3757 0           croak("write_table: Invalid hash entry\n");
3758             }
3759             // Check if top level values are scalars (Flat Hash)
3760 47 100         if (!SvROK(first_val)) {
3761 11           is_flat_hash = 1;
3762             } else {
3763 36           int first_type = SvTYPE(SvRV(first_val));
3764 36 100         if (first_type != SVt_PVHV && first_type != SVt_PVAV) {
    50          
3765 0           croak("write_table: Data values must be either all HASHes, all ARRAYs, or all scalars\n");
3766             }
3767 36           is_hoh = (first_type == SVt_PVHV);
3768 36           is_hoa = (first_type == SVt_PVAV);
3769             }
3770 47           hv_iterinit(hv);
3771 145 100         while ((entry = hv_iternext(hv))) {
3772 100           SV *restrict val = hv_iterval(hv, entry);
3773 100 100         if (is_flat_hash) {
3774 30 50         if (val && SvROK(val)) {
    100          
3775 1           croak("write_table: Mixed data types detected. Ensure all values are scalars for a flat hash.\n");
3776             }
3777             } else {
3778 70 50         if (!val || !SvROK(val) || SvTYPE(SvRV(val)) != (is_hoh ? SVt_PVHV : SVt_PVAV)) {
    50          
    100          
    100          
3779 1 50         croak("write_table: Mixed data types detected. Ensure all values are %s references.\n", is_hoh ? "HASH" : "ARRAY");
3780             }
3781             }
3782             }
3783 45 100         if (is_hoh) { // Rows are only explicitly pre-gathered for HOH
3784 11           rows_av = newAV();
3785 11           hv_iterinit(hv);
3786 28 100         while ((entry = hv_iternext(hv))) {
3787 17           av_push(rows_av, newSVsv(hv_iterkeysv(entry)));
3788             }
3789             }
3790             } else {
3791 9           AV *restrict av = (AV*)data_ref;
3792 9 100         if (av_len(av) < 0) XSRETURN_EMPTY;
3793 8           SV **restrict first_ptr = av_fetch(av, 0, 0);
3794 8 50         if (!first_ptr || !*first_ptr || !SvROK(*first_ptr) || SvTYPE(SvRV(*first_ptr)) != SVt_PVHV) {
    50          
    100          
    50          
3795 1 50         if (first_ptr && *first_ptr && SvROK(*first_ptr))
    50          
    50          
3796 0           croak("write_table: For ARRAY data, every element must be a HASH reference "
3797             "(Array of Hashes); element 0 is a reference of type '%s'\n",
3798             sv_reftype(SvRV(*first_ptr), 0));
3799 1 50         else if (first_ptr && *first_ptr && SvOK(*first_ptr))
    50          
    50          
3800 1           croak("write_table: For ARRAY data, every element must be a HASH reference "
3801             "(Array of Hashes); element 0 is a non-reference scalar (value: '%s')\n",
3802             SvPV_nolen(*first_ptr));
3803             else
3804 0           croak("write_table: For ARRAY data, every element must be a HASH reference "
3805             "(Array of Hashes); element 0 is undef\n");
3806             }
3807             // FIX: i was size_t while av_len() returns SSize_t; keep both signed.
3808 32 100         for (SSize_t i = 0; i <= av_len(av); i++) {
3809 25           SV **restrict ptr = av_fetch(av, i, 0);
3810 25 50         if (!ptr || !*ptr || !SvROK(*ptr) || SvTYPE(SvRV(*ptr)) != SVt_PVHV) {
    50          
    50          
    50          
3811 0           croak("write_table: Mixed data types detected in Array of Hashes. All elements must be HASH references.\n");
3812             }
3813             }
3814 7           is_aoh = 1;
3815             }
3816 52           PerlIO *restrict fh = PerlIO_open(file, "w");
3817 52 100         if (!fh) {
3818             // FIX: rows_av was leaked here when the open failed on HoH input.
3819 1 50         if (rows_av) SvREFCNT_dec(rows_av);
3820 1           croak("write_table: Could not open '%s' for writing", file);
3821             }
3822 51           AV *restrict headers_av = newAV();
3823 51 50         bool inc_rownames = (row_names_sv && SvTRUE(row_names_sv)) ? 1 : 0;
    100          
3824 51           const char *restrict rownames_col = NULL;
3825             // ----- Hash of Hashes -----
3826 51 100         if (is_hoh) {
3827 12 100         if (col_names_sv && SvOK(col_names_sv)) {
    50          
3828 2           AV *restrict c_av = (AV*)SvRV(col_names_sv);
3829             // FIX: i was size_t; av_len() == -1 on an empty col.names array
3830             // converted to SIZE_MAX and looped (effectively) forever.
3831 5 100         for (SSize_t i = 0; i <= av_len(c_av); i++) {
3832 3           SV **restrict c = av_fetch(c_av, i, 0);
3833 3 50         if (c && SvOK(*c)) av_push(headers_av, newSVsv(*c));
    50          
3834             }
3835             } else {
3836 8           HV *restrict col_map = newHV();
3837 8           hv_iterinit((HV*)data_ref);
3838             HE *restrict entry;
3839 20 100         while ((entry = hv_iternext((HV*)data_ref))) {
3840 12           HV *restrict inner = (HV*)SvRV(hv_iterval((HV*)data_ref, entry));
3841 12           hv_iterinit(inner);
3842             HE *restrict inner_entry;
3843 70031 100         while ((inner_entry = hv_iternext(inner))) {
3844 70019           hv_store_ent(col_map, hv_iterkeysv(inner_entry), newSViv(1), 0);
3845             }
3846             }
3847 8           unsigned num_cols = hv_iterinit(col_map);
3848             // FIX (UTF-8 safety): keep the key SVs (flags intact) and sort
3849             // them with sv_cmp instead of round-tripping through char*.
3850 70022 100         for (unsigned i = 0; i < num_cols; i++) {
3851 70014           HE *restrict ce = hv_iternext(col_map);
3852 70014           av_push(headers_av, newSVsv(hv_iterkeysv(ce)));
3853             }
3854 8 100         if (num_cols > 1)
3855 5           sortsv(AvARRAY(headers_av), num_cols, Perl_sv_cmp);
3856 8           SvREFCNT_dec(col_map);
3857             }
3858 10           size_t num_headers = (size_t)(av_len(headers_av) + 1);
3859 10           const char **restrict header_row = safemalloc((num_headers + 1) * sizeof(char*));
3860 10           size_t h_idx = 0;
3861 10 50         if (inc_rownames) header_row[h_idx++] = "";
3862             // FIX: loop index was 'unsigned short int' -- silently wraps (and
3863             // loops forever) past 65535 columns. Use size_t like everywhere else.
3864 70027 100         for (size_t i = 0; i < num_headers; i++) {
3865 70017           SV **restrict h_ptr = av_fetch(headers_av, (SSize_t)i, 0);
3866 70017 50         header_row[h_idx++] = (h_ptr && SvOK(*h_ptr)) ? SvPV_nolen(*h_ptr) : "";
    50          
3867             }
3868 10           print_string_row(aTHX_ fh, header_row, h_idx, sep);
3869 10           safefree(header_row);
3870 10           size_t num_rows = (size_t)(av_len(rows_av) + 1);
3871             // FIX (UTF-8/NUL safety): sort the key SVs themselves and look rows
3872             // up by SV (hv_fetch_ent) so UTF-8-flagged or NUL-containing outer
3873             // keys still match. sortsv+sv_cmp is plain string order, as before.
3874 10           sortsv(AvARRAY(rows_av), num_rows, Perl_sv_cmp);
3875 10           HV *restrict data_hv = (HV*)data_ref;
3876 10           const char **restrict row_data = safemalloc((num_headers + 1) * sizeof(char*));
3877 24 100         for (size_t i = 0; i < num_rows; i++) {
3878 16           size_t d_idx = 0;
3879 16           SV *restrict row_key_sv = *av_fetch(rows_av, (SSize_t)i, 0);
3880 16 50         if (inc_rownames) row_data[d_idx++] = SvPV_nolen(row_key_sv);
3881 16           HE *restrict inner_he = hv_fetch_ent(data_hv, row_key_sv, 0, 0);
3882 16 50         SV *restrict inner_sv = inner_he ? HeVAL(inner_he) : NULL;
3883 16 50         HV *restrict inner_hv = (inner_sv && SvROK(inner_sv)) ? (HV*)SvRV(inner_sv) : NULL;
    50          
3884 70047 100         for (size_t j = 0; j < num_headers; j++) {
3885 70033           SV **restrict h_ptr = av_fetch(headers_av, (SSize_t)j, 0);
3886 70033 50         SV *restrict h_sv = (h_ptr && SvOK(*h_ptr)) ? *h_ptr : NULL;
    50          
3887             // FIX (UTF-8/NUL safety): fetch by SV, not by raw bytes
3888 70033 50         HE *restrict cell_he = (inner_hv && h_sv) ? hv_fetch_ent(inner_hv, h_sv, 0, 0) : NULL;
    50          
3889 70033 100         SV *restrict cell_sv = cell_he ? HeVAL(cell_he) : NULL;
3890 70033 100         if (cell_sv && SvOK(cell_sv)) {
    100          
3891 70022 100         if (SvROK(cell_sv)) {
3892 2           PerlIO_close(fh);
3893 2           safefree(row_data);
3894 2 50         if (headers_av) SvREFCNT_dec(headers_av);
3895 2 50         if (rows_av) SvREFCNT_dec(rows_av);
3896 2           croak("write_table: Cannot write nested reference types to table\n");
3897             }
3898 70020           row_data[d_idx++] = SvPV_nolen(cell_sv);
3899             } else {
3900 11           row_data[d_idx++] = undef_val;
3901             }
3902             }
3903 14           print_string_row(aTHX_ fh, row_data, d_idx, sep);
3904             }
3905 8           safefree(row_data);
3906             // ----- Flat Hash -----
3907 41 100         } else if (is_flat_hash) {
3908 10           HV *restrict data_hv = (HV*)data_ref;
3909 11 100         if (col_names_sv && SvOK(col_names_sv)) {
    50          
3910 1           AV *restrict c_av = (AV*)SvRV(col_names_sv);
3911 1 50         for (SSize_t i = 0; i <= av_len(c_av); i++) {
3912 0           SV **restrict c = av_fetch(c_av, i, 0);
3913 0 0         if (c && SvOK(*c)) av_push(headers_av, newSVsv(*c));
    0          
3914             }
3915             } else {
3916             // FIX (UTF-8 safety): keep the key SVs (flags intact) and sort
3917             // them with sv_cmp instead of round-tripping through char*.
3918 9           unsigned int num_cols = hv_iterinit(data_hv);
3919 34 100         for (unsigned int i = 0; i < num_cols; i++) {
3920 25           HE *restrict ce = hv_iternext(data_hv);
3921 25           av_push(headers_av, newSVsv(hv_iterkeysv(ce)));
3922             }
3923 9 50         if (num_cols > 1)
3924 9           sortsv(AvARRAY(headers_av), num_cols, Perl_sv_cmp);
3925             }
3926 10           size_t num_headers = (size_t)(av_len(headers_av) + 1);
3927 10           const char **restrict header_row = safemalloc((num_headers + 1) * sizeof(char*));
3928 10           size_t h_idx = 0;
3929 10 100         if (inc_rownames) header_row[h_idx++] = "";
3930 35 100         for (size_t i = 0; i < num_headers; i++) {
3931 25           SV **restrict h_ptr = av_fetch(headers_av, (SSize_t)i, 0);
3932 25 50         header_row[h_idx++] = (h_ptr && SvOK(*h_ptr)) ? SvPV_nolen(*h_ptr) : "";
    50          
3933             }
3934 10           print_string_row(aTHX_ fh, header_row, h_idx, sep);
3935 10           safefree(header_row);
3936 10           const char **restrict row_data = safemalloc((num_headers + 1) * sizeof(char*));
3937 10           size_t d_idx = 0;
3938             // Give the single row a default numeric identifier if row names are on
3939 10 100         if (inc_rownames) row_data[d_idx++] = "1";
3940 35 100         for (size_t j = 0; j < num_headers; j++) {
3941 25           SV **restrict h_ptr = av_fetch(headers_av, (SSize_t)j, 0);
3942 25 50         SV *restrict h_sv = (h_ptr && SvOK(*h_ptr)) ? *h_ptr : NULL;
    50          
3943             // FIX (UTF-8/NUL safety): fetch by SV, not by raw bytes
3944 25 50         HE *restrict val_he = h_sv ? hv_fetch_ent(data_hv, h_sv, 0, 0) : NULL;
3945 25 50         SV *restrict val_sv = val_he ? HeVAL(val_he) : NULL;
3946             // FIX: a flat-hash cell holding a reference was stringified
3947             // (e.g. ARRAY(0x...)) instead of croaking like every other shape.
3948 25 50         if (val_sv && SvOK(val_sv)) {
    50          
3949 25 50         if (SvROK(val_sv)) {
3950 0           PerlIO_close(fh);
3951 0           safefree(row_data);
3952 0 0         if (headers_av) SvREFCNT_dec(headers_av);
3953 0           croak("write_table: Cannot write nested reference types to table\n");
3954             }
3955 25           row_data[d_idx++] = SvPV_nolen(val_sv);
3956             } else {
3957 0           row_data[d_idx++] = undef_val;
3958             }
3959             }
3960 10           print_string_row(aTHX_ fh, row_data, d_idx, sep);
3961 10           safefree(row_data);
3962             // ----- Hash of Arrays -----
3963 31 100         } else if (is_hoa) {
3964 24           HV *restrict data_hv = (HV*)data_ref;
3965 24           size_t max_rows = 0;
3966 24           hv_iterinit(data_hv);
3967             HE *restrict entry;
3968 75 100         while ((entry = hv_iternext(data_hv))) {
3969 51           AV *restrict arr = (AV*)SvRV(hv_iterval(data_hv, entry));
3970 51           size_t len = (size_t)(av_len(arr) + 1);
3971 51 100         if (len > max_rows) max_rows = len;
3972             }
3973 32 100         if (col_names_sv && SvOK(col_names_sv)) {
    50          
3974 8           AV *restrict c_av = (AV*)SvRV(col_names_sv);
3975             // FIX: size_t vs av_len() == -1 (empty col.names looped forever)
3976 24 100         for (SSize_t i = 0; i <= av_len(c_av); i++) {
3977 16           SV **restrict c = av_fetch(c_av, i, 0);
3978 16 50         if (c && SvOK(*c)) av_push(headers_av, newSVsv(*c));
    100          
3979             }
3980             } else {
3981             // FIX (UTF-8 safety): keep the key SVs (flags intact) and sort
3982             // them with sv_cmp instead of round-tripping through char*.
3983 16           unsigned int num_cols = hv_iterinit(data_hv);
3984 51 100         for (unsigned int i = 0; i < num_cols; i++) {
3985 35           HE *restrict ce = hv_iternext(data_hv);
3986 35           av_push(headers_av, newSVsv(hv_iterkeysv(ce)));
3987             }
3988 16 100         if (num_cols > 1)
3989 14           sortsv(AvARRAY(headers_av), num_cols, Perl_sv_cmp);
3990             }
3991 24 100         if (av_len(headers_av) < 0) {
3992             // FIX: this croak leaked the open filehandle and headers_av.
3993 1           PerlIO_close(fh);
3994 1           SvREFCNT_dec(headers_av);
3995 1           croak("Could not get headers in write_table");
3996             }
3997 23 100         if (inc_rownames && contains_nondigit(aTHX_ row_names_sv)) {
    100          
3998 1           rownames_col = SvPV_nolen(row_names_sv);
3999 1           AV *restrict filtered_headers = newAV();
4000             // FIX: size_t vs av_len() (same wrap as above if headers empty)
4001 3 100         for (SSize_t i = 0; i <= av_len(headers_av); i++) {
4002 2           SV **restrict h_ptr = av_fetch(headers_av, i, 0);
4003 2 50         if (!h_ptr || !*h_ptr) continue;
    50          
4004 2           SV *restrict h_sv = *h_ptr;
4005             // FIX (UTF-8 safety): sv_eq, not strcmp on raw bytes
4006 2 100         if (!sv_eq(h_sv, row_names_sv)) {
4007 1           av_push(filtered_headers, newSVsv(h_sv));
4008             }
4009             }
4010 1           SvREFCNT_dec(headers_av);
4011 1           headers_av = filtered_headers;
4012             }
4013 23           size_t num_headers = (size_t)(av_len(headers_av) + 1);
4014 23           const char **restrict header_row = safemalloc((num_headers + 1) * sizeof(char*));
4015 23           size_t h_idx = 0;
4016 23 100         if (inc_rownames) header_row[h_idx++] = "";
4017 72 100         for (size_t i = 0; i < num_headers; i++) {
4018 49           SV **restrict h_ptr = av_fetch(headers_av, (SSize_t)i, 0);
4019 49 50         header_row[h_idx++] = (h_ptr && SvOK(*h_ptr)) ? SvPV_nolen(*h_ptr) : "";
    50          
4020             }
4021 23           print_string_row(aTHX_ fh, header_row, h_idx, sep);
4022 23           safefree(header_row);
4023 23           const char **restrict row_data = safemalloc((num_headers + 1) * sizeof(char*));
4024             // FIX: numeric row labels used savepv() + safefree() every row; a
4025             // stack buffer reused per row does the same job with no allocation
4026             // (and removes the const-away cast in the old safefree call).
4027             char rn_buf[32];
4028 87 100         for (size_t i = 0; i < max_rows; i++) {
4029 64           size_t d_idx = 0;
4030 64 100         if (inc_rownames) {
4031 46 100         if (rownames_col) {
4032             // FIX (UTF-8 safety): fetch the row-name column by SV
4033 2           HE *restrict rn_arr_he = hv_fetch_ent(data_hv, row_names_sv, 0, 0);
4034 2 50         SV *restrict rn_arr_sv = rn_arr_he ? HeVAL(rn_arr_he) : NULL;
4035 4 50         if (rn_arr_sv && SvROK(rn_arr_sv)) {
    50          
4036 2           AV *restrict rn_arr = (AV*)SvRV(rn_arr_sv);
4037 2           SV **restrict rn_val_ptr = av_fetch(rn_arr, (SSize_t)i, 0);
4038 2 50         if (rn_val_ptr && SvOK(*rn_val_ptr)) {
    50          
4039 2 50         if (SvROK(*rn_val_ptr)) {
4040 0           PerlIO_close(fh);
4041 0           safefree(row_data);
4042 0 0         if (headers_av) SvREFCNT_dec(headers_av);
4043 0           croak("write_table: Cannot write nested reference types to table\n");
4044             }
4045 2           row_data[d_idx++] = SvPV_nolen(*rn_val_ptr);
4046             } else {
4047 0           row_data[d_idx++] = undef_val;
4048             }
4049             } else {
4050 0           row_data[d_idx++] = undef_val;
4051             }
4052             } else {
4053 44           snprintf(rn_buf, sizeof(rn_buf), "%lu", (unsigned long)(i + 1));
4054 44           row_data[d_idx++] = rn_buf;
4055             }
4056             }
4057 218 100         for (size_t j = 0; j < num_headers; j++) {
4058 154           SV **restrict h_ptr = av_fetch(headers_av, (SSize_t)j, 0);
4059 154 50         SV *restrict h_sv = (h_ptr && SvOK(*h_ptr)) ? *h_ptr : NULL;
    50          
4060             // FIX (UTF-8/NUL safety): fetch by SV, not by raw bytes
4061 154 50         HE *restrict arr_he = h_sv ? hv_fetch_ent(data_hv, h_sv, 0, 0) : NULL;
4062 154 100         SV *restrict arr_sv = arr_he ? HeVAL(arr_he) : NULL;
4063 304 100         if (arr_sv && SvROK(arr_sv)) {
    50          
4064 150           AV *restrict arr = (AV*)SvRV(arr_sv);
4065 150           SV **restrict cell_ptr = av_fetch(arr, (SSize_t)i, 0);
4066 150 100         if (cell_ptr && SvOK(*cell_ptr)) {
    100          
4067 100 50         if (SvROK(*cell_ptr)) {
4068 0           PerlIO_close(fh);
4069 0           safefree(row_data);
4070 0 0         if (headers_av) SvREFCNT_dec(headers_av);
4071 0           croak("write_table: Cannot write nested reference types to table\n");
4072             }
4073 100           row_data[d_idx++] = SvPV_nolen(*cell_ptr);
4074             } else {
4075 50           row_data[d_idx++] = undef_val;
4076             }
4077             } else {
4078 4           row_data[d_idx++] = undef_val;
4079             }
4080             }
4081 64           print_string_row(aTHX_ fh, row_data, d_idx, sep);
4082             }
4083 23           safefree(row_data);
4084 7 50         } else if (is_aoh) { // ----- Array of Hashes
4085 7           AV *restrict data_av = (AV*)data_ref;
4086 7           size_t num_rows = (size_t)(av_len(data_av) + 1);
4087 10 100         if (col_names_sv && SvOK(col_names_sv)) {
    50          
4088 3           AV *restrict c_av = (AV*)SvRV(col_names_sv);
4089             // FIX: size_t vs av_len() == -1 (empty col.names looped forever)
4090 5 100         for (SSize_t i = 0; i <= av_len(c_av); i++) {
4091 2           SV **restrict c = av_fetch(c_av, i, 0);
4092 2 50         if (c && SvOK(*c)) av_push(headers_av, newSVsv(*c));
    50          
4093             }
4094             } else {
4095 4           HV *restrict col_map = newHV();
4096 23 100         for (size_t i = 0; i < num_rows; i++) {
4097 19           SV **restrict row_ptr = av_fetch(data_av, (SSize_t)i, 0);
4098 19 50         if (row_ptr && SvROK(*row_ptr)) {
    50          
4099 19           HV *restrict row_hv = (HV*)SvRV(*row_ptr);
4100 19           hv_iterinit(row_hv);
4101             HE *restrict entry;
4102 44 100         while ((entry = hv_iternext(row_hv))) {
4103 25           hv_store_ent(col_map, hv_iterkeysv(entry), newSViv(1), 0);
4104             }
4105             }
4106             }
4107 4           unsigned num_cols = hv_iterinit(col_map);
4108             // FIX (UTF-8 safety): keep the key SVs (flags intact) and sort
4109             // them with sv_cmp instead of round-tripping through char*.
4110 12 100         for (unsigned int i = 0; i < num_cols; i++) {
4111 8           HE *restrict ce = hv_iternext(col_map);
4112 8           av_push(headers_av, newSVsv(hv_iterkeysv(ce)));
4113             }
4114 4 100         if (num_cols > 1)
4115 3           sortsv(AvARRAY(headers_av), num_cols, Perl_sv_cmp);
4116 4           SvREFCNT_dec(col_map);
4117             }
4118 7 100         if (inc_rownames && contains_nondigit(aTHX_ row_names_sv)) {
    100          
4119 1           rownames_col = SvPV_nolen(row_names_sv);
4120 1           AV *restrict filtered_headers = newAV();
4121             // FIX: size_t vs av_len() (same wrap as above if headers empty)
4122 1 50         for (SSize_t i = 0; i <= av_len(headers_av); i++) {
4123 0           SV **restrict h_ptr = av_fetch(headers_av, i, 0);
4124 0 0         if (!h_ptr || !*h_ptr) continue;
    0          
4125 0           SV *restrict h_sv = *h_ptr;
4126             // FIX (UTF-8 safety): sv_eq, not strcmp on raw bytes
4127 0 0         if (!sv_eq(h_sv, row_names_sv)) {
4128 0           av_push(filtered_headers, newSVsv(h_sv));
4129             }
4130             }
4131 1           SvREFCNT_dec(headers_av);
4132 1           headers_av = filtered_headers;
4133             }
4134 7           size_t num_headers = (size_t)(av_len(headers_av) + 1);
4135 7           const char **restrict header_row = safemalloc((num_headers + 1) * sizeof(char*));
4136 7           size_t h_idx = 0;
4137 7 100         if (inc_rownames) header_row[h_idx++] = "";
4138 17 100         for (size_t i = 0; i < num_headers; i++) {
4139 10           SV **restrict h_ptr = av_fetch(headers_av, (SSize_t)i, 0);
4140 10 50         header_row[h_idx++] = (h_ptr && SvOK(*h_ptr)) ? SvPV_nolen(*h_ptr) : "";
    50          
4141             }
4142 7           print_string_row(aTHX_ fh, header_row, h_idx, sep);
4143 7           safefree(header_row);
4144 7           const char **restrict row_data = safemalloc((num_headers + 1) * sizeof(char*));
4145             char rn_buf[32]; // FIX: replaces per-row savepv/safefree (see HoA)
4146 32 100         for (size_t i = 0; i < num_rows; i++) {
4147 25           size_t d_idx = 0;
4148 25           SV **restrict row_ptr = av_fetch(data_av, (SSize_t)i, 0);
4149 25 50         HV *restrict row_hv = (row_ptr && SvROK(*row_ptr)) ? (HV*)SvRV(*row_ptr) : NULL;
    50          
4150 25 100         if (inc_rownames) {
4151 21 100         if (rownames_col) {
4152             // FIX (UTF-8 safety): fetch the row-name cell by SV
4153 2 50         HE *restrict rn_he = row_hv ? hv_fetch_ent(row_hv, row_names_sv, 0, 0) : NULL;
4154 2 50         SV *restrict rn_sv = rn_he ? HeVAL(rn_he) : NULL;
4155 2 50         if (rn_sv && SvOK(rn_sv)) {
    50          
4156 2 50         if (SvROK(rn_sv)) {
4157 0           PerlIO_close(fh);
4158 0           safefree(row_data);
4159 0 0         if (headers_av) SvREFCNT_dec(headers_av);
4160 0           croak("write_table: Cannot write nested reference types to table\n");
4161             }
4162 2           row_data[d_idx++] = SvPV_nolen(rn_sv);
4163             } else {
4164 0           row_data[d_idx++] = undef_val;
4165             }
4166             } else {
4167 19           snprintf(rn_buf, sizeof(rn_buf), "%lu", (unsigned long)(i + 1));
4168 19           row_data[d_idx++] = rn_buf;
4169             }
4170             }
4171 58 100         for (size_t j = 0; j < num_headers; j++) {
4172 33           SV **restrict h_ptr = av_fetch(headers_av, (SSize_t)j, 0);
4173 33 50         SV *restrict h_sv = (h_ptr && SvOK(*h_ptr)) ? *h_ptr : NULL;
    50          
4174             // FIX (UTF-8/NUL safety): fetch by SV, not by raw bytes
4175 33 50         HE *restrict cell_he = (row_hv && h_sv) ? hv_fetch_ent(row_hv, h_sv, 0, 0) : NULL;
    50          
4176 33 100         SV *restrict cell_sv = cell_he ? HeVAL(cell_he) : NULL;
4177 33 100         if (cell_sv && SvOK(cell_sv)) {
    50          
4178 29 50         if (SvROK(cell_sv)) {
4179 0           PerlIO_close(fh);
4180 0           safefree(row_data);
4181 0 0         if (headers_av) SvREFCNT_dec(headers_av);
4182 0           croak("write_table: Cannot write nested reference types to table\n");
4183             }
4184 29           row_data[d_idx++] = SvPV_nolen(cell_sv);
4185             } else {
4186 4           row_data[d_idx++] = undef_val;
4187             }
4188             }
4189 25           print_string_row(aTHX_ fh, row_data, d_idx, sep);
4190             }
4191 7           safefree(row_data);
4192             }
4193 48 50         if (headers_av) SvREFCNT_dec(headers_av);
4194 48 100         if (rows_av) SvREFCNT_dec(rows_av);
4195 48           PerlIO_close(fh);
4196 48           XSRETURN_EMPTY;
4197             }
4198              
4199             SV* _parse_csv_file(char* file, const char* sep_str, const char* comment_str, SV* callback = &PL_sv_undef)
4200             PREINIT:
4201             /* Declarations only -- C declarations cost nothing. ALLOCATIONS are
4202             * deferred into CODE, after every croak-able validation, so that no
4203             * error path can leak. (The old version allocated current_row, field,
4204             * and data in INIT: the open-failure croak leaked all three, and a die
4205             * inside the callback leaked those plus line_sv plus the open handle.) */
4206             PerlIO *restrict fp;
4207 522           AV *restrict data = NULL;
4208 522           AV *current_row = NULL;
4209 522           SV *restrict field = NULL;
4210 522           SV *restrict line_sv = NULL;
4211 522           bool in_quotes = 0, post_quote = 0, use_cb = 0;
4212             size_t sep_len, comment_len;
4213 522           char sep0 = 0;
4214             CODE:
4215             /* ---- validation: nothing is allocated yet, so croaks are leak-free */
4216 522 50         if (SvOK(callback)) {
4217 522 50         if (SvROK(callback) && SvTYPE(SvRV(callback)) == SVt_PVCV)
    50          
4218 522           use_cb = 1;
4219             else
4220             /* FIX: a defined non-CODE callback used to be silently ignored
4221             * (falling back to slurp mode); now it is an error. */
4222 0           croak("_parse_csv_file: callback must be a CODE reference");
4223             }
4224 522 50         sep_len = sep_str ? strlen(sep_str) : 0;
4225 522 50         comment_len = comment_str ? strlen(comment_str) : 0;
4226 522 50         sep0 = sep_len ? sep_str[0] : 0;
4227 522           fp = PerlIO_open(file, "r");
4228 522 50         if (!fp)
4229 0           croak("Could not open file '%s'", file);
4230             /* ---- from here on, a die inside the callback must not leak anything:
4231             * tie every long-lived resource to the save stack, which croak unwinds */
4232 522           ENTER;
4233 522           SAVEDESTRUCTOR_X(S_pclose, fp); /* fp closes on normal LEAVE or die */
4234 522           line_sv = newSV(128);
4235 522           SAVEFREESV(line_sv);
4236 522           field = newSVpvs("");
4237 522           SAVEFREESV(field);
4238 522 50         if (!use_cb)
4239 0           data = newAV(); /* slurp mode runs no perl code: no die can reach it */
4240 522           current_row = newAV(); /* covered by the ownership dance in S_emit_row */
4241             /* The wrapper strips a leading comment marker from the HEADER itself, so
4242             * the first content line must reach the callback even when it begins with
4243             * the comment string. Comment-skipping therefore starts only after the
4244             * first row has been emitted. (In the old code the header-strip logic in
4245             * read_table was dead: the parser ate any '#'-prefixed header first.) */
4246 522           bool seen_first = 0;
4247 7239 100         while (sv_gets(line_sv, fp, 0) != NULL) {
4248 6718           char *restrict line = SvPVX(line_sv);
4249 6718           size_t len = SvCUR(line_sv);
4250             // chomp \n and a preceding \r (CRLF)
4251 6718 50         if (len && line[len-1] == '\n') {
    100          
4252 6717           len--;
4253 6717 50         if (len && line[len-1] == '\r')
    100          
4254 4928           len--;
4255             }
4256 6718 50         if (!in_quotes) {
4257             // skip blank / whitespace-only lines
4258 6718           size_t k = 0;
4259 6720 50         while (k < len && (line[k] == ' ' || line[k] == '\t'))
    50          
    100          
4260 2           k++;
4261 6718 50         if (k == len)
4262 0           continue;
4263             // skip comment lines -- but never the first content line
4264 6718 100         if (seen_first && comment_len && len >= comment_len
    50          
    50          
4265 6196 50         && memcmp(line, comment_str, comment_len) == 0)
4266 0           continue;
4267             }
4268             // ---- core parser: chunked copies instead of per-char appends
4269             {
4270 6718           size_t i = 0;
4271 105660 100         while (i < len) {
4272 105638 100         if (in_quotes) {
4273             /* Everything up to the next quote is literal -- including
4274             * \r, which the old parser wrongly stripped inside quotes
4275             * (breaking round-trips of values like "x\ry"). */
4276 14881           const char *q = (const char *)memchr(line + i, '"', len - i);
4277 14881 50         if (!q) {
4278 0           sv_catpvn(field, line + i, len - i);
4279 0           i = len;
4280 0           break;
4281             }
4282             {
4283 14881           size_t run = (size_t)(q - (line + i));
4284 14881 100         if (run)
4285 14873           sv_catpvn(field, line + i, run);
4286 14881           i += run; /* i is now at the quote */
4287             }
4288 14881 100         if (i + 1 < len && line[i+1] == '"') {
    100          
4289 4           sv_catpvn(field, "\"", 1); /* "" -> literal " */
4290 4           i += 2;
4291             } else {
4292 14877           in_quotes = 0;
4293 14877           post_quote = 1;
4294 14877           i += 1;
4295             }
4296             } else {
4297             /* copy a run of ordinary bytes in one shot */
4298 90757           size_t start = i;
4299 293619 100         while (i < len) {
4300 286923           const char c = line[i];
4301 286923 100         if (c == '"' || c == '\r')
    50          
4302             break;
4303 272046 100         if (c == sep0 && sep_len && (len - i) >= sep_len
    50          
    50          
4304 69184 50         && (sep_len == 1
4305 0 0         || memcmp(line + i, sep_str, sep_len) == 0))
4306             break;
4307 202862           i++;
4308             }
4309 90757 100         if (i > start)
4310 61001           sv_catpvn(field, line + start, i - start);
4311 90757 100         if (i >= len)
4312 6696           break;
4313             {
4314 84061           const char c = line[i];
4315 84061 100         if (c == '"') {
4316             /* lenient: a quote after a closed quote is dropped,
4317             * matching the old parser */
4318 14877 50         if (!post_quote)
4319 14877           in_quotes = 1;
4320 14877           i++;
4321 69184 50         } else if (c == '\r') {
4322 0           i++; /* stray CR outside quotes: ignored, as before */
4323             } else {
4324             /* separator */
4325 69184           av_push(current_row, newSVsv(field));
4326 69184           sv_setpvs(field, "");
4327 69184           post_quote = 0;
4328 69184           i += sep_len;
4329             }
4330             }
4331             }
4332             }
4333             }
4334 6718 50         if (in_quotes) {
4335             /* open quote at EOL: logical record continues on the next line */
4336 0           sv_catpvn(field, "\n", 1);
4337             } else {
4338 6718           post_quote = 0;
4339 6718           S_emit_row(aTHX_ ¤t_row, field, use_cb, callback, data);
4340 6717           seen_first = 1;
4341             }
4342             }
4343 521 50         if (in_quotes) {/* EOF with an unterminated quote: flush the trailing record */
4344 0           S_emit_row(aTHX_ ¤t_row, field, use_cb, callback, data);
4345             }
4346 521           SvREFCNT_dec((SV*)current_row);// the spare row S_emit_row left behind
4347 521           LEAVE;// closes fp, frees line_sv and field
4348 521 50         if (use_cb) {
4349 521           RETVAL = newSV(0); // fresh undef; mortalizing immortal &PL_sv_undef underflows it on perl<5.18
4350             } else {
4351 0           RETVAL = newRV_noinc((SV*)data);
4352             }
4353             OUTPUT:
4354             RETVAL
4355              
4356             SV* cov(SV* x_sv, SV* y_sv, const char* method = "pearson")
4357             CODE:
4358             {
4359             // 1. Validate inputs are Array References
4360 4 50         if (!SvROK(x_sv) || SvTYPE(SvRV(x_sv)) != SVt_PVAV) {
    50          
4361 0           croak("cov: first argument 'x' must be an ARRAY reference");
4362             }
4363 4 50         if (!SvROK(y_sv) || SvTYPE(SvRV(y_sv)) != SVt_PVAV) {
    50          
4364 0           croak("cov: second argument 'y' must be an ARRAY reference");
4365             }
4366              
4367             // 2. Validate method argument
4368 4 100         if (strcmp(method, "pearson") != 0 &&
4369 2 100         strcmp(method, "spearman") != 0 &&
4370 1 50         strcmp(method, "kendall") != 0) {
4371 0           croak("cov: unknown method '%s' (use 'pearson', 'spearman', or 'kendall')", method);
4372             }
4373              
4374 4           AV *restrict x_av = (AV*)SvRV(x_sv);
4375 4           AV *restrict y_av = (AV*)SvRV(y_sv);
4376 4           size_t nx = av_len(x_av) + 1;
4377 4           size_t ny = av_len(y_av) + 1;
4378              
4379 4 50         if (nx != ny) {
4380 0           croak("cov: incompatible dimensions (x has %lu, y has %lu)",
4381             (unsigned long)nx, (unsigned long)ny);
4382             }
4383              
4384             // 3. Extract Valid Pairwise Data
4385             // Allocate temporary C arrays for numeric processing
4386 4           NV *restrict x_val = (NV*)safemalloc(nx * sizeof(NV));
4387 4           NV *restrict y_val = (NV*)safemalloc(nx * sizeof(NV));
4388 4           size_t n = 0;
4389              
4390 24 100         for (size_t i = 0; i < nx; i++) {
4391 20           SV **restrict x_tv = av_fetch(x_av, i, 0);
4392 20           SV **restrict y_tv = av_fetch(y_av, i, 0);
4393              
4394             // Extract numeric values, defaulting to NAN for missing/invalid data
4395 20 50         NV xv = (x_tv && SvOK(*x_tv) && looks_like_number(*x_tv)) ? SvNV(*x_tv) : NAN;
    50          
    50          
4396 20 50         NV yv = (y_tv && SvOK(*y_tv) && looks_like_number(*y_tv)) ? SvNV(*y_tv) : NAN;
    50          
    50          
4397              
4398             // Pairwise complete observations (skips NAs seamlessly like R)
4399 20 50         if (!isnan(xv) && !isnan(yv)) {
    50          
4400 20           x_val[n] = xv;
4401 20           y_val[n] = yv;
4402 20           n++;
4403             }
4404             }
4405              
4406             // 4. Handle edge cases where data is too sparse
4407 4 50         if (n < 2) {
4408 0           Safefree(x_val); Safefree(y_val);
4409 0           RETVAL = newSVnv(NAN);
4410             } else {
4411 4           NV ans = 0.0;
4412             // 5. Algorithm routing
4413 4 100         if (strcmp(method, "kendall") == 0) {
4414             // R's default cov(..., method="kendall") iterates the full n x n space
4415 6 100         for (size_t i = 0; i < n; i++) {
4416 30 100         for (size_t j = 0; j < n; j++) {
4417 25           int sx = (x_val[i] > x_val[j]) - (x_val[i] < x_val[j]);
4418 25           int sy = (y_val[i] > y_val[j]) - (y_val[i] < y_val[j]);
4419 25           ans += (NV)(sx * sy);
4420             }
4421             }
4422             } else {
4423 3           NV mean_x = 0.0, mean_y = 0.0, cov_sum = 0.0;
4424 3 100         if (strcmp(method, "spearman") == 0) {
4425             // Spearman: Rank the data first, then run standard covariance
4426 1           NV *restrict rx = (NV*)safemalloc(n * sizeof(NV));
4427 1           NV *restrict ry = (NV*)safemalloc(n * sizeof(NV));
4428             // Uses your existing rank_data() helper from LikeR.xs
4429 1           rank_data(x_val, rx, n);
4430 1           rank_data(y_val, ry, n);
4431 6 100         for (size_t i = 0; i < n; i++) {
4432 5           NV dx = rx[i] - mean_x;
4433 5           mean_x += dx / (i + 1);
4434 5           NV dy = ry[i] - mean_y;
4435 5           mean_y += dy / (i + 1);
4436 5           cov_sum += dx * (ry[i] - mean_y);
4437             }
4438 1           Safefree(rx); Safefree(ry);
4439             } else {
4440             // Pearson: Welford's Single-Pass Covariance Algorithm
4441 12 100         for (size_t i = 0; i < n; i++) {
4442 10           NV dx = x_val[i] - mean_x;
4443 10           mean_x += dx / (i + 1);
4444 10           NV dy = y_val[i] - mean_y;
4445 10           mean_y += dy / (i + 1);
4446 10           cov_sum += dx * (y_val[i] - mean_y);
4447             }
4448             }
4449              
4450             // Unbiased Sample Covariance (N - 1) for Pearson & Spearman
4451 3           ans = cov_sum / (n - 1);
4452             }
4453 4           Safefree(x_val); Safefree(y_val);
4454 4           RETVAL = newSVnv(ans);
4455             }
4456             }
4457             OUTPUT:
4458             RETVAL
4459              
4460             SV* glm(...)
4461             CODE:
4462             {
4463 10           const char *restrict formula = NULL;
4464 10           SV *restrict data_sv = NULL;
4465 10           const char *restrict family_str = "gaussian";
4466             char f_cpy[512];
4467             char *restrict src, *restrict dst, *restrict tilde, *restrict lhs, *restrict rhs, *restrict chunk;
4468              
4469             // Dynamic Term Arrays
4470 10           char **restrict terms = NULL, **restrict uniq_terms = NULL, **restrict exp_terms = NULL;
4471 10           bool *restrict is_dummy = NULL;
4472 10           char **restrict dummy_base = NULL, **restrict dummy_level = NULL;
4473 10           unsigned int term_cap = 64, exp_cap = 64, num_terms = 0, num_uniq = 0, p = 0, p_exp = 0;
4474 10           size_t n = 0, valid_n = 0, i;
4475 10           bool has_intercept = TRUE, converged = FALSE, boundary = FALSE;
4476 10           unsigned int iter = 0, max_iter = 25, final_rank = 0, df_res = 0;
4477 10           NV deviance_old = 0.0, deviance_new = 0.0, null_dev = 0.0, aic = 0.0;
4478 10           NV dispersion = 0.0, epsilon = 1e-8;
4479              
4480 10           char **restrict row_names = NULL;
4481 10           char **restrict valid_row_names = NULL;
4482 10           HV **restrict row_hashes = NULL;
4483 10           HV *restrict data_hoa = NULL;
4484 10           SV *restrict ref = NULL;
4485              
4486 10           NV *restrict X = NULL, *restrict Y = NULL, *restrict mu = NULL, *restrict eta = NULL;
4487 10           NV *restrict W = NULL, *restrict Z = NULL, *restrict beta = NULL, *restrict beta_old = NULL;
4488 10           bool *restrict aliased = NULL;
4489 10           NV *restrict XtWX = NULL, *restrict XtWZ = NULL;
4490              
4491             HV *restrict res_hv, *restrict coef_hv, *restrict fitted_hv, *restrict resid_hv, *restrict summary_hv;
4492             AV *restrict terms_av;
4493             HE *restrict entry;
4494              
4495 10 50         if (items % 2 != 0) croak("Usage: glm(formula => 'am ~ wt + hp', data => \\%mtcars)");
4496              
4497 38 100         for (unsigned short i_arg = 0; i_arg < items; i_arg += 2) {
4498 28           const char *restrict key = SvPV_nolen(ST(i_arg));
4499 28           SV *restrict val = ST(i_arg + 1);
4500 28 100         if (strEQ(key, "formula")) formula = SvPV_nolen(val);
4501 18 100         else if (strEQ(key, "data")) data_sv = val;
4502 8 50         else if (strEQ(key, "family")) family_str = SvPV_nolen(val);
4503 0           else croak("glm: unknown argument '%s'", key);
4504             }
4505 10 50         if (!formula) croak("glm: formula is required");
4506 10 50         if (!data_sv || !SvROK(data_sv)) croak("glm: data is required and must be a reference");
    50          
4507              
4508 10           bool is_binomial = (strcmp(family_str, "binomial") == 0);
4509 10           bool is_gaussian = (strcmp(family_str, "gaussian") == 0);
4510 10 100         if (!is_binomial && !is_gaussian) croak("glm: unsupported family '%s'", family_str);
    50          
4511              
4512             // --- Formula Parsing & Expansion ---
4513 10           Newx(terms, term_cap, char*); Newx(uniq_terms, term_cap, char*);
4514 10           Newx(exp_terms, exp_cap, char*); Newx(is_dummy, exp_cap, bool);
4515 10           Newx(dummy_base, exp_cap, char*); Newx(dummy_level, exp_cap, char*);
4516              
4517 10           src = (char*restrict)formula; dst = f_cpy;
4518 148 100         while (*src && (dst - f_cpy < 511)) { if (!isspace(*src)) { *dst++ = *src; } src++; }
    100          
    50          
4519 10           *dst = '\0';
4520              
4521 10           tilde = strchr(f_cpy, '~');
4522 10 50         if (!tilde) croak("glm: invalid formula, missing '~'");
4523 10           *tilde = '\0';
4524 10           lhs = f_cpy;
4525 10           rhs = tilde + 1;
4526             char *restrict minus_one;
4527 10 100         if ((minus_one = strstr(rhs, "-1")) != NULL) {
4528 1           has_intercept = FALSE;
4529 1           memmove(
4530 1           minus_one, minus_one + 2, strlen(minus_one + 2) + 1
4531             );
4532             }
4533 10           char *restrict minus1 = strstr(rhs, "-1");
4534 10 50         if (minus1) {
4535 0           has_intercept = FALSE;
4536 0           memmove(/* remove the "-1" token from the RHS */
4537 0           minus1, minus1 + 2, strlen(minus1 + 2) + 1
4538             );
4539             }
4540 10 100         if (has_intercept) terms[num_terms++] = savepv("Intercept");
4541              
4542 10           chunk = strtok(rhs, "+");
4543 26 100         while (chunk != NULL) {
4544 16 50         if (num_terms >= term_cap - 3) {
4545 0           term_cap *= 2;
4546 0           Renew(terms, term_cap, char*); Renew(uniq_terms, term_cap, char*);
4547             }
4548 16 50         if (strcmp(chunk, "1") == 0 || strcmp(chunk, "-1") == 0) {
    50          
4549 0           chunk = strtok(NULL, "+");
4550 0           continue;
4551             }
4552 16           char *restrict star = strchr(chunk, '*');
4553 16 50         if (star) {
4554 0           *star = '\0';
4555 0           char *restrict left = chunk; char *restrict right = star + 1;
4556 0 0         char *restrict c_l = strchr(left, '^'); if (c_l && strncmp(left, "I(", 2) != 0) *c_l = '\0';
    0          
4557 0 0         char *restrict c_r = strchr(right, '^'); if (c_r && strncmp(right, "I(", 2) != 0) *c_r = '\0';
    0          
4558 0           terms[num_terms++] = savepv(left);
4559 0           terms[num_terms++] = savepv(right);
4560 0           size_t inter_len = strlen(left) + strlen(right) + 2;
4561 0           terms[num_terms] = (char*)safemalloc(inter_len);
4562 0           snprintf(terms[num_terms++], inter_len, "%s:%s", left, right);
4563             } else {
4564 16           char *restrict c_chunk = strchr(chunk, '^');
4565 16 50         if (c_chunk && strncmp(chunk, "I(", 2) != 0) *c_chunk = '\0';
    0          
4566 16           terms[num_terms++] = savepv(chunk);
4567             }
4568 16           chunk = strtok(NULL, "+");
4569             }
4570              
4571 35 100         for (i = 0; i < num_terms; i++) {
4572 25           bool found = FALSE;
4573 46 100         for (size_t j = 0; j < num_uniq; j++) {
4574 21 50         if (strcmp(terms[i], uniq_terms[j]) == 0) { found = TRUE; break; }
4575             }
4576 25 50         if (!found) uniq_terms[num_uniq++] = savepv(terms[i]);
4577             }
4578 10           p = num_uniq;
4579             // --- Data Extraction ---
4580 10           ref = SvRV(data_sv);
4581 10 50         if (SvTYPE(ref) == SVt_PVHV) {
4582 10           HV*restrict hv = (HV*)ref;
4583 10 50         if (hv_iterinit(hv) == 0) croak("glm: Data hash is empty");
4584 10           entry = hv_iternext(hv);
4585 10 50         if (entry) {
4586 10           SV*restrict val = hv_iterval(hv, entry);
4587 10 50         if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVAV) {
    100          
4588 5           data_hoa = hv;
4589 5           n = av_len((AV*)SvRV(val)) + 1;
4590 5 50         Newx(row_names, n, char*);
4591 136 100         for(i = 0; i < n; i++) {
4592 131           char buf[32]; snprintf(buf, sizeof(buf), "%lu", i+1);
4593 131           row_names[i] = savepv(buf);
4594             }
4595 5 50         } else if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVHV) {
    50          
4596 5           n = hv_iterinit(hv);
4597 5 50         Newx(row_names, n, char*); Newx(row_hashes, n, HV*);
    50          
4598 5           i = 0;
4599 165 100         while ((entry = hv_iternext(hv))) {
4600             I32 len;
4601 160           row_names[i] = savepv(hv_iterkey(entry, &len));
4602 160           row_hashes[i] = (HV*)SvRV(hv_iterval(hv, entry));
4603 160           i++;
4604             }
4605 0           } else croak("glm: Hash values must be ArrayRefs (HoA) or HashRefs (HoH)");
4606             }
4607 0 0         } else if (SvTYPE(ref) == SVt_PVAV) {
4608 0           AV*restrict av = (AV*)ref;
4609 0           n = av_len(av) + 1;
4610 0 0         Newx(row_names, n, char*); Newx(row_hashes, n, HV*);
    0          
4611 0 0         for (i = 0; i < n; i++) {
4612 0           SV**restrict val = av_fetch(av, i, 0);
4613 0 0         if (val && SvROK(*val) && SvTYPE(SvRV(*val)) == SVt_PVHV) {
    0          
    0          
4614 0           row_hashes[i] = (HV*)SvRV(*val);
4615 0           char buf[32]; snprintf(buf, sizeof(buf), "%lu", i + 1);
4616 0           row_names[i] = savepv(buf);
4617             } else {
4618 0 0         for (size_t k = 0; k < i; k++) Safefree(row_names[k]);
4619 0           Safefree(row_names); Safefree(row_hashes);
4620 0           croak("glm: Array values must be HashRefs (AoH)");
4621             }
4622             }
4623 0           } else croak("glm: Data must be an Array or Hash reference");
4624             // --- Categorical Expansion ---
4625 35 100         for (size_t j = 0; j < p; j++) {
4626 25 50         if (p_exp + 32 >= exp_cap) {
4627 0           exp_cap *= 2;
4628 0           Renew(exp_terms, exp_cap, char*); Renew(is_dummy, exp_cap, bool);
4629 0           Renew(dummy_base, exp_cap, char*); Renew(dummy_level, exp_cap, char*);
4630             }
4631 25 100         if (strcmp(uniq_terms[j], "Intercept") == 0) {
4632 9           exp_terms[p_exp] = savepv("Intercept"); is_dummy[p_exp] = FALSE; p_exp++; continue;
4633             }
4634 16 100         if (is_column_categorical(aTHX_ data_hoa, row_hashes, n, uniq_terms[j])) {
4635 1           char **restrict levels = NULL; size_t num_levels = 0, levels_cap = 8;
4636 1 50         Newx(levels, levels_cap, char*);
4637 61 100         for (i = 0; i < n; i++) {
4638 60           char*restrict str_val = get_data_string_alloc(aTHX_ data_hoa, row_hashes, i, uniq_terms[j]);
4639 60 50         if (str_val) {
4640 60           bool found = FALSE;
4641 90 100         for (size_t l = 0; l < num_levels; l++) {
4642 88 100         if (strcmp(levels[l], str_val) == 0) { found = TRUE; break; }
4643             }
4644 60 100         if (!found) {
4645 2 50         if (num_levels >= levels_cap) { levels_cap *= 2; Renew(levels, levels_cap, char*); }
    0          
4646 2           levels[num_levels++] = savepv(str_val);
4647             }
4648 60           Safefree(str_val);
4649             }
4650             }
4651 1 50         if (num_levels > 0) {
4652 2 100         for (size_t l1 = 0; l1 < num_levels - 1; l1++) {
4653 2 100         for (size_t l2 = l1 + 1; l2 < num_levels; l2++) {
4654 1 50         if (strcmp(levels[l1], levels[l2]) > 0) {
4655 1           char *restrict tmp = levels[l1]; levels[l1] = levels[l2]; levels[l2] = tmp;
4656             }
4657             }
4658             }
4659 2 100         for (size_t l = 1; l < num_levels; l++) {
4660 1 50         if (p_exp >= exp_cap) {
4661 0           exp_cap *= 2;
4662 0           Renew(exp_terms, exp_cap, char*); Renew(is_dummy, exp_cap, bool);
4663 0           Renew(dummy_base, exp_cap, char*); Renew(dummy_level, exp_cap, char*);
4664             }
4665 1           size_t t_len = strlen(uniq_terms[j]) + strlen(levels[l]) + 1;
4666 1           exp_terms[p_exp] = (char*)safemalloc(t_len);
4667 1           snprintf(exp_terms[p_exp], t_len, "%s%s", uniq_terms[j], levels[l]);
4668 1           is_dummy[p_exp] = TRUE; dummy_base[p_exp] = savepv(uniq_terms[j]); dummy_level[p_exp] = savepv(levels[l]);
4669 1           p_exp++;
4670             }
4671 3 100         for (size_t l = 0; l < num_levels; l++) Safefree(levels[l]);
4672 1           Safefree(levels);
4673             } else {
4674 0           Safefree(levels); exp_terms[p_exp] = savepv(uniq_terms[j]); is_dummy[p_exp] = FALSE; p_exp++;
4675             }
4676             } else {
4677 15           exp_terms[p_exp] = savepv(uniq_terms[j]); is_dummy[p_exp] = FALSE; p_exp++;
4678             }
4679             }
4680 10           p = p_exp;
4681              
4682 10 50         Newx(X, n * p, NV); Newx(Y, n, NV);
    50          
4683 10 50         Newx(valid_row_names, n, char*);
4684              
4685             // --- Listwise Deletion ---
4686 301 100         for (size_t i = 0; i < n; i++) {
4687 291           NV y_val = evaluate_term(aTHX_ data_hoa, row_hashes, i, lhs);
4688 291 50         if (isnan(y_val)) { Safefree(row_names[i]); continue; }
4689              
4690 291           bool row_ok = TRUE;
4691 291           NV *restrict row_x = (NV*)safemalloc(p * sizeof(NV));
4692 1090 100         for (size_t j = 0; j < p; j++) {
4693 799 100         if (strcmp(exp_terms[j], "Intercept") == 0) {
4694 288           row_x[j] = 1.0;
4695 511 100         } else if (is_dummy[j]) {
4696 60           char* str_val = get_data_string_alloc(aTHX_ data_hoa, row_hashes, i, dummy_base[j]);
4697 60 50         if (str_val) {
4698 60 100         row_x[j] = (strcmp(str_val, dummy_level[j]) == 0) ? 1.0 : 0.0;
4699 60           Safefree(str_val);
4700 0           } else { row_ok = FALSE; break; }
4701             } else {
4702 451           row_x[j] = evaluate_term(aTHX_ data_hoa, row_hashes, i, exp_terms[j]);
4703 451 50         if (isnan(row_x[j])) { row_ok = FALSE; break; }
4704             }
4705             }
4706 291 50         if (!row_ok) { Safefree(row_names[i]); Safefree(row_x); continue; }
4707 291           Y[valid_n] = y_val;
4708 1090 100         for (size_t j = 0; j < p; j++) X[valid_n * p + j] = row_x[j];
4709 291           valid_row_names[valid_n] = row_names[i];
4710 291           valid_n++;
4711 291           Safefree(row_x);
4712             }
4713 10           Safefree(row_names);
4714 10 50         if (valid_n < p) {
4715 0 0         Safefree(X); Safefree(Y); Safefree(valid_row_names); if (row_hashes) Safefree(row_hashes);
4716 0           croak("glm: 0 degrees of freedom (too many NAs or parameters > observations)");
4717             }
4718             // --- R glm.fit IRLS Implementation ---
4719 10           mu = (NV*)safemalloc(valid_n * sizeof(NV)); eta = (NV*)safemalloc(valid_n * sizeof(NV));
4720 10           W = (NV*)safemalloc(valid_n * sizeof(NV)); Z = (NV*)safemalloc(valid_n * sizeof(NV));
4721 10           beta = (NV*)safemalloc(p * sizeof(NV)); beta_old = (NV*)safemalloc(p * sizeof(NV));
4722 10           aliased = (bool*)safemalloc(p * sizeof(bool));
4723 10           XtWX = (NV*)safemalloc(p * p * sizeof(NV)); XtWZ = (NV*)safemalloc(p * sizeof(NV));
4724 35 100         for (i = 0; i < p; i++) { beta[i] = 0.0; beta_old[i] = 0.0; }
4725             // Initialize (mustart / etastart equivalent)
4726 10           NV sum_y = 0.0;
4727 301 100         for (i = 0; i < valid_n; i++) sum_y += Y[i];
4728 10           NV mean_y = sum_y / valid_n;
4729 297 100         for (i = 0; i < valid_n; i++) {
4730 288 100         if (is_binomial) {
4731 37 100         if (Y[i] < 0.0 || Y[i] > 1.0) croak("glm: binomial family requires response between 0 and 1");
    50          
4732 36           mu[i] = (Y[i] + 0.5) / 2.0;
4733 36           eta[i] = log(mu[i] / (1.0 - mu[i]));
4734 36           NV dev = 0.0;
4735 36 100         if (Y[i] == 0.0) dev = -2.0 * log(1.0 - mu[i]);
4736 15 50         else if (Y[i] == 1.0) dev = -2.0 * log(mu[i]);
4737 0           else dev = 2.0 * (Y[i] * log(Y[i] / mu[i]) + (1.0 - Y[i]) * log((1.0 - Y[i]) / (1.0 - mu[i])));
4738 36           deviance_old += dev;
4739             } else {
4740 251           mu[i] = mean_y; // R gaussian init
4741 251           eta[i] = mu[i];
4742             }
4743             }
4744             // IRLS Loop
4745 45 50         for (iter = 1; iter <= max_iter; iter++) {
4746 924 100         for (i = 0; i < valid_n; i++) {
4747 879 100         if (is_binomial) {
4748 380           NV varmu = mu[i] * (1.0 - mu[i]);
4749 380           NV mu_eta = varmu; // Link derivative for logit
4750 380 100         if (varmu < 1e-10) varmu = 1e-10;
4751 380           Z[i] = eta[i] + (Y[i] - mu[i]) / mu_eta;
4752 380           W[i] = (mu_eta * mu_eta) / varmu;
4753             } else {
4754 499           W[i] = 1.0;
4755 499           Z[i] = Y[i];
4756             }
4757             }
4758             // Formulate XtWX and XtWZ
4759 425 100         for (i = 0; i < p; i++) { XtWZ[i] = 0.0; for (size_t j = 0; j < p; j++) XtWX[i * p + j] = 0.0; }
    100          
4760 924 100         for (size_t k = 0; k < valid_n; k++) {
4761 879           NV w = W[k], z = Z[k];
4762 3298 100         for (i = 0; i < p; i++) {
4763 2419           XtWZ[i] += X[k * p + i] * w * z;
4764 2419           NV xw = X[k * p + i] * w;
4765 9246 100         for (size_t j = 0; j < p; j++) XtWX[i * p + j] += xw * X[k * p + j];
4766             }
4767             }
4768 45           final_rank = sweep_matrix_ols(XtWX, p, aliased);
4769 153 100         for (i = 0; i < p; i++) {
4770 108 50         if (aliased[i]) { beta[i] = NAN; } else {
4771 108           NV sum = 0.0;
4772 380 50         for (size_t j = 0; j < p; j++) if (!aliased[j]) sum += XtWX[i * p + j] * XtWZ[j];
    100          
4773 108           beta[i] = sum;
4774             }
4775             }
4776             // Calculate updated ETA, MU, and Deviance (with Step-Halving)
4777 45           boundary = FALSE;
4778 495 100         for (unsigned short int half = 0; half < 10; half++) {
4779 450           deviance_new = 0.0;
4780 9240 100         for (i = 0; i < valid_n; i++) {
4781 8790           NV linear_pred = 0.0;
4782 32980 50         for (size_t j = 0; j < p; j++) if (!aliased[j]) linear_pred += X[i * p + j] * beta[j];
    100          
4783 8790           eta[i] = linear_pred;
4784 8790 100         if (is_binomial) {
4785 3800           mu[i] = 1.0 / (1.0 + exp(-eta[i]));
4786             // Boundary enforcement
4787 3800 50         if (mu[i] < 10 * DBL_EPSILON) mu[i] = 10 * DBL_EPSILON;
4788 3800 50         if (mu[i] > 1.0 - 10 * DBL_EPSILON) mu[i] = 1.0 - 10 * DBL_EPSILON;
4789 3800           NV dev = 0.0;
4790 3800 100         if (Y[i] == 0.0) dev = -2.0 * log(1.0 - mu[i]);
4791 1630 50         else if (Y[i] == 1.0) dev = -2.0 * log(mu[i]);
4792 0           else dev = 2.0 * (Y[i] * log(Y[i] / mu[i]) + (1.0 - Y[i]) * log((1.0 - Y[i]) / (1.0 - mu[i])));
4793 3800           deviance_new += dev;
4794             } else {
4795 4990           mu[i] = eta[i];
4796 4990           NV res = Y[i] - mu[i];
4797 4990           deviance_new += res * res;
4798             }
4799             }
4800             // Step halving divergence check
4801 450 100         if (!is_binomial || deviance_new <= deviance_old + 1e-7 || !isfinite(deviance_new)) {
    100          
    50          
4802 440           continue;
4803             }
4804 10           boundary = TRUE;
4805 40 100         for (size_t j = 0; j < p; j++) beta[j] = (beta[j] + beta_old[j]) / 2.0;
4806             }
4807             // Convergence Check
4808 45 100         if (fabs(deviance_new - deviance_old) / (0.1 + fabs(deviance_new)) < epsilon) {
4809 9           converged = TRUE; break;
4810             }
4811 36           deviance_old = deviance_new;
4812 121 100         for (size_t j = 0; j < p; j++) beta_old[j] = beta[j];
4813             }
4814             // Final accurate calculation of W for standard errors
4815 95 100         for (i = 0; i < p; i++) { for (size_t j = 0; j < p; j++) XtWX[i * p + j] = 0.0; }
    100          
4816 296 100         for (size_t k = 0; k < valid_n; k++) {
4817 287 100         NV w = is_binomial ? (mu[k] * (1.0 - mu[k])) : 1.0;
4818 287 100         if (w < 1e-10) w = 1e-10;
4819 1078 100         for (i = 0; i < p; i++) {
4820 791           NV xw = X[k * p + i] * w;
4821 3030 100         for (size_t j = 0; j < p; j++) XtWX[i * p + j] += xw * X[k * p + j];
4822             }
4823             }
4824 9           final_rank = sweep_matrix_ols(XtWX, p, aliased);
4825             // --- Null Deviance Calculation ---
4826             // If no intercept, the null model predicts the inverse-link of 0.
4827 9 100         NV wtdmu = has_intercept ? mean_y : (is_binomial ? 0.5 : 0.0);
    50          
4828              
4829 296 100         for (i = 0; i < valid_n; i++) {
4830 287 100         if (is_binomial) {
4831 36 100         if (Y[i] == 0.0) null_dev += -2.0 * log(1.0 - wtdmu);
4832 15 50         else if (Y[i] == 1.0) null_dev += -2.0 * log(wtdmu);
4833 0           else null_dev += 2.0 * (Y[i] * log(Y[i] / wtdmu) + (1.0 - Y[i]) * log((1.0 - Y[i]) / (1.0 - wtdmu)));
4834             } else {
4835 251           NV diff = Y[i] - wtdmu;
4836 251           null_dev += diff * diff;
4837             }
4838             }
4839             // --- AIC Calculation ---
4840 9 100         if (is_gaussian) {
4841 7           NV n_f = (NV)valid_n;
4842 7           NV dev_for_aic = deviance_new;
4843             // Guard against perfect fits (deviance == 0.0) causing log(0) = -inf.
4844             // R's QR decomposition leaves a noise floor of ~1.0355e-30 for perfect integer fits.
4845             // Clamping to this exact boundary replicates R's output of -197.91.
4846 7 100         if (dev_for_aic < 1.0355727742801604e-30) {
4847 1           dev_for_aic = 1.0355727742801604e-30;
4848             }
4849             // Mathematically matches R's gaussian()$aic + 2*rank
4850 7           aic = n_f * (log(2.0 * M_PI) + 1.0 + log(dev_for_aic / n_f)) + 2.0 * (final_rank + 1.0);
4851 2 50         } else if (is_binomial) {
4852 2           aic = deviance_new + 2.0 * final_rank;
4853             }
4854             // --- Return Structures ---
4855 9           res_hv = newHV(); coef_hv = newHV(); fitted_hv = newHV(); resid_hv = newHV();
4856 9           df_res = valid_n - final_rank;
4857 9 100         dispersion = is_binomial ? 1.0 : ((df_res > 0) ? (deviance_new / df_res) : NAN);
    50          
4858 296 100         for (size_t i = 0; i < valid_n; i++) {
4859 287           NV res = Y[i] - mu[i];
4860 287 100         if (is_binomial) {
4861             // Deviance residuals for binomial
4862 36           NV d_res = 0.0;
4863 36 100         if (Y[i] == 0.0) d_res = sqrt(-2.0 * log(1.0 - mu[i]));
4864 15 50         else if (Y[i] == 1.0) d_res = sqrt(-2.0 * log(mu[i]));
4865 0           else d_res = sqrt(2.0 * (Y[i] * log(Y[i] / mu[i]) + (1.0 - Y[i]) * log((1.0 - Y[i]) / (1.0 - mu[i]))));
4866 36 100         res = (Y[i] > mu[i]) ? d_res : -d_res;
4867             }
4868 287           hv_store(fitted_hv, valid_row_names[i], strlen(valid_row_names[i]), newSVnv(mu[i]), 0);
4869 287           hv_store(resid_hv, valid_row_names[i], strlen(valid_row_names[i]), newSVnv(res), 0);
4870 287           Safefree(valid_row_names[i]);
4871             }
4872 9           Safefree(valid_row_names);
4873 9           summary_hv = newHV(); terms_av = newAV();
4874 32 100         for (size_t j = 0; j < p; j++) {
4875 23           hv_store(coef_hv, exp_terms[j], strlen(exp_terms[j]), newSVnv(beta[j]), 0);
4876 23           av_push(terms_av, newSVpv(exp_terms[j], 0));
4877              
4878 23           HV *restrict row_hv = newHV();
4879 23 50         if (aliased[j]) {
4880 0           hv_store(row_hv, "Estimate", 8, newSVpv("NaN", 0), 0);
4881 0           hv_store(row_hv, "Std. Error", 10, newSVpv("NaN", 0), 0);
4882 0 0         hv_store(row_hv, is_binomial ? "z value" : "t value", 7, newSVpv("NaN", 0), 0);
4883 0 0         hv_store(row_hv, is_binomial ? "Pr(>|z|)" : "Pr(>|t|)", 8, newSVpv("NaN", 0), 0);
4884             } else {
4885 23           NV se = sqrt(dispersion * XtWX[j * p + j]);
4886 23           NV val_stat = beta[j] / se;
4887 23 100         NV p_val = is_binomial ? 2.0 * (1.0 - approx_pnorm(fabs(val_stat))) : get_t_pvalue(val_stat, df_res, "two.sided");
4888 23           hv_store(row_hv, "Estimate", 8, newSVnv(beta[j]), 0);
4889 23           hv_store(row_hv, "Std. Error", 10, newSVnv(se), 0);
4890 23 100         hv_store(row_hv, is_binomial ? "z value" : "t value", 7, newSVnv(val_stat), 0);
4891 23 100         hv_store(row_hv, is_binomial ? "Pr(>|z|)" : "Pr(>|t|)", 8, newSVnv(p_val), 0);
4892             }
4893 23           hv_store(summary_hv, exp_terms[j], strlen(exp_terms[j]), newRV_noinc((SV*)row_hv), 0);
4894             }
4895 9           hv_store(res_hv, "aic", 3, newSVnv(aic), 0);
4896 9           hv_store(res_hv, "coefficients", 12, newRV_noinc((SV*)coef_hv), 0);
4897 9           hv_store(res_hv, "converged", 9, newSVuv(converged ? 1 : 0), 0);
4898 9           hv_store(res_hv, "boundary", 8, newSVuv(boundary ? 1 : 0), 0);
4899 9           hv_store(res_hv, "deviance", 8, newSVnv(deviance_new), 0);
4900 9           hv_store(res_hv, "deviance.resid", 14, newRV_noinc((SV*)resid_hv), 0);
4901 9           hv_store(res_hv, "df.null", 7, newSVuv(valid_n - has_intercept), 0);
4902 9           hv_store(res_hv, "df.residual", 11, newSVuv(df_res), 0);
4903 9           hv_store(res_hv, "family", 6, newSVpv(family_str, 0), 0);
4904 9           hv_store(res_hv, "fitted.values", 13, newRV_noinc((SV*)fitted_hv), 0);
4905 9           hv_store(res_hv, "iter", 4, newSVuv(iter > max_iter ? max_iter : iter), 0);
4906 9           hv_store(res_hv, "null.deviance", 13, newSVnv(null_dev), 0);
4907 9           hv_store(res_hv, "rank", 4, newSVuv(final_rank), 0);
4908 9           hv_store(res_hv, "summary", 7, newRV_noinc((SV*)summary_hv), 0);
4909 9           hv_store(res_hv, "terms", 5, newRV_noinc((SV*)terms_av), 0);
4910             // --- Cleanup ---
4911 32 100         for (i = 0; i < num_terms; i++) Safefree(terms[i]);
4912 9           Safefree(terms);
4913 32 100         for (i = 0; i < num_uniq; i++) Safefree(uniq_terms[i]);
4914 9           Safefree(uniq_terms);
4915 32 100         for (size_t j = 0; j < p_exp; j++) {
4916 23           Safefree(exp_terms[j]);
4917 23 100         if (is_dummy[j]) { Safefree(dummy_base[j]); Safefree(dummy_level[j]); }
4918             }
4919 9           Safefree(exp_terms); Safefree(is_dummy); Safefree(dummy_base); Safefree(dummy_level);
4920 9           Safefree(mu); Safefree(eta); Safefree(Z); Safefree(W);
4921 9           Safefree(beta); Safefree(beta_old); Safefree(aliased);
4922 9           Safefree(XtWX); Safefree(XtWZ); Safefree(X); Safefree(Y);
4923 9 100         if (row_hashes) Safefree(row_hashes);
4924 9           RETVAL = newRV_noinc((SV*)res_hv);
4925             }
4926             OUTPUT:
4927             RETVAL
4928              
4929             SV* cor_test(...)
4930             CODE:
4931             {
4932 12 50         if (items < 2 || items % 2 != 0)
    50          
4933 0           croak("Usage: cor_test(\\@x, \\@y, method => 'pearson', ...)");
4934 12           SV *restrict x_ref = ST(0), *restrict y_ref = ST(1);
4935 12           const char *restrict alternative = "two.sided";
4936 12           const char *restrict method = "pearson";
4937 12           SV *restrict exact_sv = NULL;
4938 12           NV conf_level = 0.95;
4939 12           bool continuity = 0;
4940             /* Parse named arguments from the flat stack starting at index 2 */
4941 46 100         for (unsigned short int i = 2; i < items; i += 2) {
4942 34           const char *restrict key = SvPV_nolen(ST(i));
4943 34           SV *restrict val = ST(i + 1);
4944 34 100         if (strEQ(key, "alternative")) alternative = SvPV_nolen(val);
4945 27 100         else if (strEQ(key, "method")) method = SvPV_nolen(val);
4946 15 100         else if (strEQ(key, "exact")) exact_sv = val;
4947 14 100         else if (strEQ(key, "conf.level") || strEQ(key, "conf_level")) conf_level = SvNV(val);
    50          
4948 7 50         else if (strEQ(key, "continuity")) continuity = SvTRUE(val);
4949 0           else croak("cor_test: unknown argument '%s'", key);
4950             }
4951             AV *restrict x_av, *restrict y_av;
4952             NV *restrict x, *restrict y;
4953 12           NV estimate = 0, p_value = 0, statistic = 0, df = 0, ci_lower = 0, ci_upper = 0;
4954 12           bool is_pearson = (strcmp(method, "pearson") == 0);
4955 12           bool is_kendall = (strcmp(method, "kendall") == 0);
4956 12           bool is_spearman = (strcmp(method, "spearman") == 0);
4957             HV *restrict rhv;
4958 12 50         if (!SvOK(x_ref) || !SvROK(x_ref) || SvTYPE(SvRV(x_ref)) != SVt_PVAV ||
    50          
    50          
4959 12 50         !SvOK(y_ref) || !SvROK(y_ref) || SvTYPE(SvRV(y_ref)) != SVt_PVAV) {
    50          
    50          
4960 0           croak("cor_test: x and y must be array references");
4961             }
4962 12           x_av = (AV*)SvRV(x_ref);
4963 12           y_av = (AV*)SvRV(y_ref);
4964 12           size_t n_raw = av_len(x_av) + 1;
4965 12 50         if (n_raw != (size_t)(av_len(y_av) + 1)) croak("incompatible dimensions");
4966 12           x = safemalloc(n_raw * sizeof(NV));
4967 12           y = safemalloc(n_raw * sizeof(NV));
4968 12           size_t n = 0; /* Final count of pairwise complete observations */
4969 281 100         for (size_t i = 0; i < n_raw; i++) {
4970 269           SV **restrict x_val = av_fetch(x_av, i, 0);
4971 269           SV **restrict y_val = av_fetch(y_av, i, 0);
4972 269 50         NV xv = (x_val && SvOK(*x_val) && looks_like_number(*x_val)) ? SvNV(*x_val) : NAN;
    100          
    50          
4973 269 50         NV yv = (y_val && SvOK(*y_val) && looks_like_number(*y_val)) ? SvNV(*y_val) : NAN;
    100          
    50          
4974             /* Pairwise complete observations (skips NAs seamlessly like R) */
4975 269 100         if (!isnan(xv) && !isnan(yv)) {
    100          
4976 265           x[n] = xv;
4977 265           y[n] = yv;
4978 265           n++;
4979             }
4980             }
4981 12 50         if (n < 3) {
4982 0           Safefree(x);
4983 0           Safefree(y);
4984 0           croak("not enough finite observations");
4985             }
4986 12 100         if (is_pearson) {
4987             /* Welford's one-pass algorithm for Pearson correlation */
4988 6           NV mean_x = 0.0, mean_y = 0.0, M2_x = 0.0, M2_y = 0.0, cov = 0.0;
4989 36 100         for (size_t i = 0; i < n; i++) {
4990 30           NV dx = x[i] - mean_x;
4991 30           mean_x += dx / (i + 1);
4992 30           NV dy = y[i] - mean_y;
4993 30           mean_y += dy / (i + 1);
4994 30           M2_x += dx * (x[i] - mean_x);
4995 30           M2_y += dy * (y[i] - mean_y);
4996 30           cov += dx * (y[i] - mean_y);
4997             }
4998 6 50         estimate = (M2_x > 0.0 && M2_y > 0.0) ? cov / sqrt(M2_x * M2_y) : 0.0;
    50          
4999             /* Clamp to [-1, 1] to guard against floating-point overshoot */
5000 6 50         if (estimate > 1.0) estimate = 1.0;
5001 6 50         else if (estimate < -1.0) estimate = -1.0;
5002 6           df = (NV)(n - 2);
5003             /* BUG FIX: guard divide-by-zero when |estimate| == 1 exactly.
5004             * A perfect correlation gives t = ±Inf, matching R's behaviour. */
5005 6           NV denom_t = 1.0 - estimate * estimate;
5006 6 100         if (denom_t <= 0.0)
5007 2 100         statistic = (estimate > 0.0) ? INFINITY : -INFINITY;
5008             else
5009 4           statistic = estimate * sqrt(df / denom_t);
5010             /* Confidence interval via Fisher's Z transform.
5011             * BUG FIX: when |estimate| == 1 the log blows up; clamp first.
5012             * We use a half-ULP margin so tanh can recover ±1 cleanly. */
5013 6           NV est_clamped = estimate;
5014 6 100         if (est_clamped >= 1.0) est_clamped = 1.0 - DBL_EPSILON;
5015 5 100         else if (est_clamped <= -1.0) est_clamped = -1.0 + DBL_EPSILON;
5016 6           NV z = 0.5 * log((1.0 + est_clamped) / (1.0 - est_clamped));
5017 6           NV se = 1.0 / sqrt((NV)(n - 3));
5018 6           NV alpha = 1.0 - conf_level;
5019 6           NV q = inverse_normal_cdf(1.0 - alpha / 2.0);
5020 6           ci_lower = tanh(z - q * se);
5021 6           ci_upper = tanh(z + q * se);
5022             // High-precision p-value using incomplete beta
5023 6           p_value = get_t_pvalue(statistic, df, alternative);
5024 6 100         } else if (is_kendall) {
5025             // BUG FIX: use long to avoid int overflow for large n
5026 3           long c = 0, d = 0, tie_x = 0, tie_y = 0;
5027 210 100         for (size_t i = 0; i < n - 1; i++) {
5028 20127 100         for (size_t j = i + 1; j < n; j++) {
5029 19920           NV sign_x = (x[i] > x[j]) - (x[i] < x[j]);
5030 19920           NV sign_y = (y[i] > y[j]) - (y[i] < y[j]);
5031 19920 50         if (sign_x == 0 && sign_y == 0) { /* joint tie — ignore */ }
    0          
5032 19920 50         else if (sign_x == 0) tie_x++;
5033 19920 50         else if (sign_y == 0) tie_y++;
5034 19920 100         else if (sign_x * sign_y > 0) c++;
5035 19904           else d++;
5036             }
5037             }
5038 3           NV denom = sqrt((NV)(c + d + tie_x) * (NV)(c + d + tie_y));
5039             // BUG FIX: use NAN (from ) instead of 0.0/0.0 (UB in C)
5040 3 50         estimate = (denom == 0.0) ? NAN : (NV)(c - d) / denom;
5041 3 50         bool has_ties = (tie_x > 0 || tie_y > 0);
    50          
5042             bool do_exact;
5043             /* Mirror R: exact defaults to TRUE if n < 50 and no ties */
5044 3 100         if (!exact_sv || !SvOK(exact_sv))
    50          
5045 2 50         do_exact = (n < 50) && !has_ties;
    50          
5046             else
5047 1           do_exact = SvTRUE(exact_sv) ? 1 : 0;
5048             /* R overrides forced-exact back to approximation when ties exist */
5049 3 100         if (do_exact && has_ties) do_exact = 0;
    50          
5050 3 100         if (do_exact) {
5051 2           NV S_stat = (NV)(c - d);
5052 2           statistic = (NV)c;
5053 2           p_value = kendall_exact_pvalue(n, S_stat, alternative);
5054             } else {
5055             /* Normal approximation for large n or when ties are present */
5056 1           NV var_S = (NV)n * (NV)(n - 1) * (2.0 * (NV)n + 5.0) / 18.0;
5057 1           NV S = (NV)(c - d);
5058 1 50         if (continuity) S -= (S > 0.0 ? 1.0 : -1.0);
    0          
5059 1           statistic = S / sqrt(var_S);
5060              
5061 1 50         if (strcmp(alternative, "two.sided") == 0)
5062 1           p_value = 2.0 * (1.0 - approx_pnorm(fabs(statistic)));
5063 0 0         else if (strcmp(alternative, "less") == 0)
5064 0           p_value = approx_pnorm(statistic);
5065             else
5066 0           p_value = 1.0 - approx_pnorm(statistic);
5067             }
5068              
5069 3 50         } else if (is_spearman) {
5070 3           NV *restrict rank_x = safemalloc(n * sizeof(NV));
5071 3           NV *restrict rank_y = safemalloc(n * sizeof(NV));
5072 3           compute_ranks(x, rank_x, n);
5073 3           compute_ranks(y, rank_y, n);
5074              
5075             /* Spearman rho = Pearson r of the ranks (Welford's algorithm) */
5076 3           NV mean_x = 0.0, mean_y = 0.0, M2_x = 0.0, M2_y = 0.0, cov = 0.0;
5077 28 100         for (size_t i = 0; i < n; i++) {
5078 25           NV dx = rank_x[i] - mean_x;
5079 25           mean_x += dx / (i + 1);
5080 25           NV dy = rank_y[i] - mean_y;
5081 25           mean_y += dy / (i + 1);
5082 25           M2_x += dx * (rank_x[i] - mean_x);
5083 25           M2_y += dy * (rank_y[i] - mean_y);
5084 25           cov += dx * (rank_y[i] - mean_y);
5085             }
5086 3 50         estimate = (M2_x > 0.0 && M2_y > 0.0) ? cov / sqrt(M2_x * M2_y) : 0.0;
    50          
5087              
5088             /* Clamp to [-1, 1] to guard against floating-point overshoot */
5089 3 50         if (estimate > 1.0) estimate = 1.0;
5090 3 50         else if (estimate < -1.0) estimate = -1.0;
5091              
5092             /* S = sum of squared rank differences (R's reported statistic) */
5093 3           NV S_stat = 0.0;
5094 28 100         for (size_t i = 0; i < n; i++) {
5095 25           NV diff = rank_x[i] - rank_y[i];
5096 25           S_stat += diff * diff;
5097             }
5098              
5099             /* Ties produce fractional (averaged) ranks — detect them */
5100 3           bool has_ties = 0;
5101 28 100         for (size_t i = 0; i < n; i++) {
5102 25 50         if (rank_x[i] != floor(rank_x[i]) || rank_y[i] != floor(rank_y[i])) {
    50          
5103 0           has_ties = 1;
5104 0           break;
5105             }
5106             }
5107              
5108             bool do_exact;
5109 3 50         if (!exact_sv || !SvOK(exact_sv))
    0          
5110 3 100         do_exact = (n < 10) && !has_ties;
    50          
5111             else
5112 0           do_exact = SvTRUE(exact_sv) ? 1 : 0;
5113              
5114 3 100         if (do_exact) {
5115 1           statistic = S_stat;
5116 1           p_value = spearman_exact_pvalue(S_stat, n, alternative);
5117             } else {
5118 2           NV r = estimate;
5119             /* NOTE: R silently ignores continuity correction for Spearman.
5120             * The adjustment below is non-standard; a warning is emitted
5121             * so callers are not silently misled. */
5122 2 50         if (continuity) {
5123 0           warn("cor_test: continuity correction is not defined for Spearman in R and is ignored here");
5124             }
5125             /* BUG FIX: guard divide-by-zero when |r| == 1 exactly */
5126 2           NV denom_t = 1.0 - r * r;
5127 2 50         if (denom_t <= 0.0)
5128 2 100         statistic = (r > 0.0) ? INFINITY : -INFINITY;
5129             else
5130 0           statistic = r * sqrt((NV)(n - 2) / denom_t);
5131 2           p_value = get_t_pvalue(statistic, (NV)(n - 2), alternative);
5132             }
5133 3           Safefree(rank_x);
5134 3           Safefree(rank_y);
5135              
5136             } else {
5137 0           Safefree(x);
5138 0           Safefree(y);
5139 0           croak("Unknown method '%s': must be 'pearson', 'kendall', or 'spearman'", method);
5140             }
5141              
5142 12           Safefree(x);
5143 12           Safefree(y);
5144              
5145 12           rhv = newHV();
5146 12           hv_stores(rhv, "estimate", newSVnv(estimate));
5147 12           hv_stores(rhv, "p.value", newSVnv(p_value));
5148 12           hv_stores(rhv, "statistic", newSVnv(statistic));
5149 12           hv_stores(rhv, "method", newSVpv(method, 0));
5150 12           hv_stores(rhv, "alternative", newSVpv(alternative, 0));
5151 12 100         if (is_pearson) {
5152 6           hv_stores(rhv, "parameter", newSVnv(df));
5153 6           AV *restrict ci_av = newAV();
5154 6           av_push(ci_av, newSVnv(ci_lower));
5155 6           av_push(ci_av, newSVnv(ci_upper));
5156 6           hv_stores(rhv, "conf.int", newRV_noinc((SV*)ci_av));
5157             }
5158              
5159 12           RETVAL = newRV_noinc((SV*)rhv);
5160             }
5161             OUTPUT:
5162             RETVAL
5163              
5164             void shapiro_test(data)
5165             SV *data
5166             PREINIT:
5167             AV *restrict av;
5168             HV *restrict ret_hash;
5169 2           size_t n_raw, n = 0;
5170 2           NV *restrict x, w = 0.0, p_val = 0.0, mean = 0.0, ssq = 0.0;
5171             PPCODE:
5172 2 50         if (!SvROK(data) || SvTYPE(SvRV(data)) != SVt_PVAV) {
    50          
5173 0           croak("Expected an array reference");
5174             }
5175              
5176 2           av = (AV *)SvRV(data);
5177 2           n_raw = av_len(av) + 1;
5178              
5179 2 50         Newx(x, n_raw, NV);
5180              
5181             // Extract variables and calculate mean (skipping undefined/NaN values)
5182 26 100         for (size_t i = 0; i < n_raw; i++) {
5183 24           SV **restrict elem = av_fetch(av, i, 0);
5184 24 50         if (elem && SvOK(*elem)) {
    50          
5185 24           NV val = SvNV(*elem);
5186 24 50         if (!isnan(val)) {
5187 24           x[n] = val;
5188 24           mean += val;
5189 24           n++;
5190             }
5191             }
5192             }
5193              
5194 2 50         if (n < 3 || n > 5000) {
    50          
5195 0           Safefree(x);
5196 0           croak("Sample size must be between 3 and 5000 (R's limit)");
5197             }
5198              
5199 2           mean /= n;
5200             // Calculate Sum of Squares
5201 26 100         for (size_t i = 0; i < n; i++) {
5202 24           ssq += (x[i] - mean) * (x[i] - mean);
5203             }
5204 2 50         if (ssq == 0.0) {
5205 0           Safefree(x);
5206 0           croak("Data is perfectly constant; cannot compute Shapiro-Wilk test");
5207             }
5208 2           qsort(x, n, sizeof(NV), compare_doubles);
5209             // --- Core AS R94 Algorithm: Weights and Statistic W
5210 2 50         if (n == 3) {
5211 0           NV a_val = 0.7071067811865475; // sqrt(1/2)
5212 0           NV b_val = a_val * (x[2] - x[0]);
5213 0           w = (b_val * b_val) / ssq;
5214 0 0         if (w < 0.75) w = 0.75;
5215             // Exact P-value for n=3
5216 0           p_val = 1.90985931710274 * (asin(sqrt(w)) - 1.04719755119660);
5217             } else {
5218             NV *restrict m, *restrict a;
5219 2           NV sum_m2 = 0.0, b_val = 0.0;
5220 2 50         Newx(m, n, NV);
5221 2 50         Newx(a, n, NV);
5222 26 100         for (size_t i = 0; i < n; i++) {
5223 24           m[i] = inverse_normal_cdf((i + 1.0 - 0.375) / (n + 0.25));
5224 24           sum_m2 += m[i] * m[i];
5225             }
5226 2           NV u = 1.0 / sqrt((NV)n);
5227 2           NV a_n = -2.706056*pow(u,5) + 4.434685*pow(u,4) - 2.071190*pow(u,3) - 0.147981*pow(u,2) + 0.221157*u + m[n-1]/sqrt(sum_m2);
5228 2           a[n-1] = a_n;
5229 2           a[0] = -a_n;
5230 3 50         if (n == 4 || n == 5) {
    100          
5231 1           NV eps = (sum_m2 - 2.0 * m[n-1]*m[n-1]) / (1.0 - 2.0 * a_n*a_n);
5232 4 100         for (unsigned int i = 1; i < n-1; i++) {
5233 3           a[i] = m[i] / sqrt(eps);
5234             }
5235             } else {
5236 1           NV a_n1 = -3.582633*pow(u,5) + 5.682633*pow(u,4) - 1.752461*pow(u,3) - 0.293762*pow(u,2) + 0.042981*u + m[n-2]/sqrt(sum_m2);
5237 1           a[n-2] = a_n1;
5238 1           a[1] = -a_n1;
5239 1           NV eps = (sum_m2 - 2.0 * m[n-1]*m[n-1] - 2.0 * m[n-2]*m[n-2]) / (1.0 - 2.0 * a_n*a_n - 2.0 * a_n1*a_n1);
5240 16 100         for (unsigned int i = 2; i < n-2; i++) {
5241 15           a[i] = m[i] / sqrt(eps);
5242             }
5243             }
5244 26 100         for (size_t i = 0; i < n; i++) {
5245 24           b_val += a[i] * x[i];
5246             }
5247 2           w = (b_val * b_val) / ssq;
5248             // --- AS R94 P-Value Calculation: High Precision Refinement ---
5249             /* NOTE: p_val is declared in PREINIT above;
5250             * do NOT shadow it with a local 'double p_val' here or the result will never reach the caller.
5251             */
5252 2           NV y = log(1.0 - w);
5253             NV z;
5254 2 100         if (n <= 11) {
5255             // Royston's branch for 4 <= n <= 11 (AS R94, small-sample path).
5256             // gamma is the upper bound on y = log(1-W);
5257             // if y reaches gamma the p-value is essentially zero
5258 1           NV nn = (NV)n;
5259 1           NV gamma = 0.459 * nn - 2.273;
5260 1 50         if (y >= gamma) {
5261 0           p_val = 1e-19;
5262             } else {
5263             // Horner-form polynomials in n for mu and log(sigma)
5264 1           NV mu = 0.544 + nn * (-0.39978 + nn * ( 0.025054 - nn * 0.0006714));
5265 1           NV sig_val= 1.3822 + nn * (-0.77857 + nn * ( 0.062767 - nn * 0.0020322));
5266 1           NV sigma = exp(sig_val);
5267 1           z = (-log(gamma - y) - mu) / sigma;
5268             /* Upper-tail probability P(Z > z): small W → large z → small p-value.
5269             */
5270 1           p_val = 0.5 * erfc(z * M_SQRT1_2);
5271             }
5272             } else {
5273             // Royston's branch for n >= 12 (AS R94, large-sample path)
5274 1           NV ln_n = log((NV)n);
5275             // Horner-form polynomials in log(n) for mu and log(sigma). */
5276 1           NV mu = -1.5861 + ln_n * (-0.31082 + ln_n * (-0.083751 + ln_n * 0.0038915));
5277 1           NV sig_val= -0.4803 + ln_n * (-0.082676 + ln_n * 0.0030302);
5278 1           NV sigma = exp(sig_val);
5279 1           z = (y - mu) / sigma;
5280 1           p_val = 0.5 * erfc(z * M_SQRT1_2);
5281             }
5282             // Clamp the p-value
5283 2 50         if (p_val > 1.0) p_val = 1.0;
5284 2 50         if (p_val < 0.0) p_val = 0.0;
5285 2           Safefree(m); m = NULL; Safefree(a); a = NULL;
5286             }
5287 2           Safefree(x); x = NULL;
5288 2           ret_hash = newHV();
5289 2           hv_stores(ret_hash, "statistic", newSVnv(w));
5290 2           hv_stores(ret_hash, "W", newSVnv(w));
5291 2           hv_stores(ret_hash, "p_value", newSVnv(p_val));
5292 2           hv_stores(ret_hash, "p.value", newSVnv(p_val));
5293 2 50         EXTEND(SP, 1);
5294 2           PUSHs(sv_2mortal(newRV_noinc((SV *)ret_hash)));
5295              
5296             NV min(...)
5297             PROTOTYPE: @
5298             INIT:
5299 19           NV min_val = 0.0;
5300 19           size_t count = 0;
5301 19           bool first = TRUE;
5302             CODE:
5303 10052 100         for (unsigned short int i = 0; i < items; i++) {
5304 10035           SV* restrict arg = ST(i);
5305 10045 100         if (SvROK(arg) && SvTYPE(SvRV(arg)) == SVt_PVAV) {
    50          
5306 11           AV* restrict av = (AV*)SvRV(arg);
5307 11           size_t len = av_len(av) + 1;
5308 466 100         for (size_t j = 0; j < len; j++) {
5309 456           SV** restrict tv = av_fetch(av, j, 0);
5310 456 50         if (tv && SvOK(*tv)) {
    100          
5311 455           NV val = SvNV(*tv);
5312 455 100         if (first || val < min_val) {
    100          
5313 23           min_val = val;
5314 23           first = FALSE;
5315             }
5316 455           count++;
5317             } else {
5318 1           croak("min: undefined value at array ref index %zu (argument %d)", j, (int)i);
5319             }
5320             }
5321 10024 100         } else if (SvOK(arg)) {
5322 10023           NV val = SvNV(arg);
5323 10023 100         if (first || val < min_val) {
    100          
5324 17           min_val = val;
5325 17           first = FALSE;
5326             }
5327 10023           count++;
5328             } else {
5329 1           croak("min: undefined value at argument index %d", (int)i);
5330             }
5331             }
5332 17 100         if (count == 0) croak("min needs >= 1 numeric element");
5333 16 100         RETVAL = min_val;
5334             OUTPUT:
5335             RETVAL
5336              
5337             NV max(...)
5338             PROTOTYPE: @
5339             INIT:
5340 20           NV max_val = 0.0;
5341 20           size_t count = 0;
5342 20           bool first = TRUE;
5343             CODE:
5344 10053 100         for (size_t i = 0; i < items; i++) {
5345 10035           SV* restrict arg = ST(i);
5346 10046 100         if (SvROK(arg) && SvTYPE(SvRV(arg)) == SVt_PVAV) {
    50          
5347 12           AV* restrict av = (AV*)SvRV(arg);
5348 12           size_t len = av_len(av) + 1;
5349 567 100         for (size_t j = 0; j < len; j++) {
5350 556           SV** restrict tv = av_fetch(av, j, 0);
5351 556 50         if (tv && SvOK(*tv)) {
    100          
5352 555           NV val = SvNV(*tv);
5353 555 100         if (first || val > max_val) {
    100          
5354 52           max_val = val;
5355 52           first = FALSE;
5356             }
5357 555           count++;
5358             } else {
5359 1           croak("max: undefined value at array ref index %zu (argument %zu)", j, i);
5360             }
5361             }
5362 10023 100         } else if (SvOK(arg)) {
5363 10022           NV val = SvNV(arg);
5364 10022 100         if (first || val > max_val) {
    100          
5365 29           max_val = val;
5366 29           first = FALSE;
5367             }
5368 10022           count++;
5369             } else {
5370 1           croak("max: undefined value at argument index %zu", i);
5371             }
5372             }
5373 18 100         if (count == 0) croak("max needs >= 1 numeric element");
5374 17 100         RETVAL = max_val;
5375             OUTPUT:
5376             RETVAL
5377              
5378             SV* runif(...)
5379             CODE:
5380             {
5381 11           size_t n = 0;
5382 11           NV min = 0.0, max = 1.0;
5383              
5384             // Flags to track what has been assigned
5385 11           bool n_set = 0, min_set = 0, max_set = 0;
5386              
5387 11           unsigned int i = 0;
5388              
5389 11 50         if (items == 0) {
5390 0           croak("Usage: runif(n, [min=0], [max=1]) or runif(n => $n, ...)");
5391             }
5392              
5393 28 100         while (i < items) {
5394             // 1. Check if the current argument is a string key for a named parameter
5395 17 100         if (i + 1 < items && SvPOK(ST(i))) {
    100          
5396 6           char *restrict key = SvPV_nolen(ST(i));
5397 6 100         if (strEQ(key, "n")) {
5398 2           n = (size_t)SvUV(ST(i+1));
5399 2           n_set = 1;
5400 2           i += 2;
5401 2           continue;
5402 4 100         } else if (strEQ(key, "min")) {
5403 2           min = SvNV(ST(i+1));
5404 2           min_set = 1;
5405 2           i += 2;
5406 2           continue;
5407 2 50         } else if (strEQ(key, "max")) {
5408 2           max = SvNV(ST(i+1));
5409 2           max_set = 1;
5410 2           i += 2;
5411 2           continue;
5412             }
5413             }
5414              
5415             // 2. Fallback to positional parsing if it's not a recognized key
5416 11 100         if (!n_set) {
5417 9           n = (size_t)SvUV(ST(i));
5418 9           n_set = 1;
5419 2 100         } else if (!min_set) {
5420 1           min = SvNV(ST(i));
5421 1           min_set = 1;
5422 1 50         } else if (!max_set) {
5423 1           max = SvNV(ST(i));
5424 1           max_set = 1;
5425             } else {
5426 0           croak("Too many arguments or unrecognized parameter passed to runif()");
5427             }
5428 11           i++;
5429             }
5430 11 50         if (!n_set) {
5431 0           croak("runif() requires at least the 'n' parameter");
5432             }
5433             // Ensure PRNG is seeded
5434 11 50         AUTO_SEED_PRNG();
5435 11           AV *restrict results = newAV();
5436 11 50         if (n > 0) {
5437 11           av_extend(results, n - 1);
5438             }
5439 11           const NV range = max - min;
5440 20090 100         for (size_t j = 0; j < n; j++) {
5441             NV r;
5442 20079 50         if (max < min) {
5443 0           r = NAN; // R behavior for inverted ranges
5444             } else {
5445 20079           r = min + range * Drand01();
5446             }
5447 20079           av_push(results, newSVnv(r));
5448             }
5449 11           RETVAL = newRV_noinc((SV*)results);
5450             }
5451             OUTPUT:
5452             RETVAL
5453              
5454             SV* rbinom(...)
5455             CODE:
5456             {
5457             // Auto-seed the PRNG if the Perl script hasn't done so yet
5458 12 50         AUTO_SEED_PRNG();
5459 12 100         if (items % 2 != 0)
5460 1           croak("Usage: rbinom(n => 10, size => 100, prob => 0.5)");
5461             //Parse named arguments
5462 11           size_t n = 0, size = 0;
5463 11           NV prob = 0.5;
5464              
5465 11           bool size_set = FALSE, prob_set = FALSE;
5466              
5467 42 100         for (unsigned short i = 0; i < items; i += 2) {
5468 31           const char* restrict key = SvPV_nolen(ST(i));
5469 31           SV* restrict val = ST(i + 1);
5470              
5471 31 100         if (strEQ(key, "n")) n = (unsigned int)SvUV(val);
5472 20 100         else if (strEQ(key, "size")) { size = (unsigned int)SvUV(val); size_set = TRUE; }
5473 10 50         else if (strEQ(key, "prob")) { prob = SvNV(val); prob_set = TRUE; }
5474 0           else croak("rbinom: unknown argument '%s'", key);
5475             }
5476              
5477             // R requires size and prob to be explicitly passed in rbinom
5478 11 100         if (!size_set || !prob_set) croak("rbinom: 'size' and 'prob' are required arguments");
    100          
5479 9 100         if (prob < 0.0 || prob > 1.0) croak("rbinom: prob must be between 0 and 1");
    100          
5480              
5481 7           AV *restrict result_av = newAV();
5482 7 50         if (n > 0) {
5483 7           av_extend(result_av, n - 1);
5484 20506 100         for (unsigned int i = 0; i < n; i++) {
5485 20499           av_store(result_av, i, newSVuv(generate_binomial(aTHX_ size, prob)));
5486             }
5487             }
5488              
5489 7           RETVAL = newRV_noinc((SV*)result_av);
5490             }
5491             OUTPUT:
5492             RETVAL
5493              
5494             SV* hist(SV* x_sv, ...)
5495             CODE:
5496             {
5497             // 1. Validate Input
5498 9 100         if (!SvROK(x_sv) || SvTYPE(SvRV(x_sv)) != SVt_PVAV)
    100          
5499 2           croak("hist: first argument must be an array reference");
5500              
5501 7           AV*restrict x_av = (AV*)SvRV(x_sv);
5502 7           size_t n_raw = av_len(x_av) + 1;
5503 7 100         if (n_raw == 0) croak("hist: input array is empty");
5504              
5505             // 2. Extract Data & Find Range
5506             NV *restrict x;
5507 6 50         Newx(x, n_raw, NV);
5508 6           size_t n = 0;
5509 6           NV min_val = DBL_MAX, max_val = -DBL_MAX;
5510              
5511 2026 100         for (size_t i = 0; i < n_raw; i++) {
5512 2021           SV**restrict tv = av_fetch(x_av, i, 0);
5513 2021 50         if (tv && SvOK(*tv)) {
    50          
5514 2021           NV val = SvNV(*tv);
5515 2020           x[n++] = val;
5516 2020 100         if (val < min_val) min_val = val;
5517 2020 100         if (val > max_val) max_val = val;
5518             }
5519             }
5520 5 50         if (n == 0) {
5521 0           Safefree(x);
5522 0           croak("hist: input contains no valid numeric data");
5523             }
5524             // 3. Determine Bin Count (Sturges default or user-provided)
5525 5           size_t n_bins = 0;
5526 5 50         if (items == 2) {
5527             // Support pure positional argument: hist($data, 22)
5528 0           n_bins = (size_t)SvIV(ST(1));
5529 5 50         } else if (items > 2) {
5530             // Support named parameters even if mixed with positional arguments
5531 5 50         for (unsigned short i = 1; i < items - 1; i++) {
5532             // Make sure the SV holds a string before doing string comparison
5533 5 50         if (SvPOK(ST(i)) && strEQ(SvPV_nolen(ST(i)), "breaks")) {
    50          
5534 5           n_bins = (size_t)SvIV(ST(i+1));
5535 5           break;
5536             }
5537             }
5538             /* Fallback: if 'breaks' wasn't found but a positional number was given first */
5539 5 50         if (n_bins == 0 && looks_like_number(ST(1))) {
    0          
5540 0           n_bins = (size_t)SvIV(ST(1));
5541             }
5542             }
5543 5 50         if (n_bins == 0) n_bins = calculate_sturges_bins(n);
5544             // 4. Allocate Result Arrays
5545             NV *restrict breaks, *restrict mids, *restrict density;
5546             size_t *restrict counts;
5547 5 50         Newx(breaks, n_bins + 1, NV);
5548 5 50         Newx(mids, n_bins, NV);
5549 5 50         Newx(density, n_bins, NV);
5550 5 50         Newx(counts, n_bins, size_t);
5551             // Generate simple linear breaks
5552 5           NV step = (max_val - min_val) / (NV)n_bins;
5553 28 100         for (size_t i = 0; i <= n_bins; i++) {
5554 23           breaks[i] = min_val + (NV)i * step;
5555             }
5556             // 5. Compute Statistics
5557 5           compute_hist_logic(x, n, breaks, n_bins, counts, mids, density);
5558             // 6. Build Return HashRef
5559 5           HV*restrict res_hv = newHV();
5560 5           AV*restrict av_breaks = newAV();
5561 5           AV*restrict av_counts = newAV();
5562 5           AV*restrict av_mids = newAV();
5563 5           AV*restrict av_density = newAV();
5564 28 100         for (size_t i = 0; i <= n_bins; i++) {
5565 23           av_push(av_breaks, newSVnv(breaks[i]));
5566 23 100         if (i < n_bins) {
5567 18           av_push(av_counts, newSViv(counts[i]));
5568 18           av_push(av_mids, newSVnv(mids[i]));
5569 18           av_push(av_density, newSVnv(density[i]));
5570             }
5571             }
5572 5           hv_stores(res_hv, "breaks", newRV_noinc((SV*)av_breaks));
5573 5           hv_stores(res_hv, "counts", newRV_noinc((SV*)av_counts));
5574 5           hv_stores(res_hv, "mids", newRV_noinc((SV*)av_mids));
5575 5           hv_stores(res_hv, "density", newRV_noinc((SV*)av_density));
5576             // Clean
5577 5           Safefree(x); Safefree(breaks); Safefree(mids);
5578 5           Safefree(density); Safefree(counts);
5579 5           RETVAL = newRV_noinc((SV*)res_hv);
5580             }
5581             OUTPUT:
5582             RETVAL
5583              
5584             SV* quantile(...)
5585             CODE:
5586             {
5587 11           SV *restrict x_sv = NULL;
5588 11           SV *restrict probs_sv = NULL;
5589 11           unsigned int arg_idx = 0;
5590             // --- 1. Consume first positional arg as 'x' if it's an array ref
5591 11 50         if (arg_idx < items && SvROK(ST(arg_idx)) && SvTYPE(SvRV(ST(arg_idx))) == SVt_PVAV) {
    100          
    50          
5592 10           x_sv = ST(arg_idx);
5593 10           arg_idx++;
5594             }
5595             // --- 2. Remaining args must be key-value pairs
5596 11 50         if ((items - arg_idx) % 2 != 0)
5597 0           croak("Usage: quantile(\\@data, probs => \\@probs) OR quantile(x => \\@data, probs => \\@probs)");
5598              
5599 23 100         for (; arg_idx < items; arg_idx += 2) {
5600 12           const char *restrict key = SvPV_nolen(ST(arg_idx));
5601 12           SV *restrict val = ST(arg_idx + 1);
5602              
5603 12 100         if (strEQ(key, "x")) x_sv = val;
5604 11 50         else if (strEQ(key, "probs")) probs_sv = val;
5605 0           else croak("quantile: unknown argument '%s'", key);
5606             }
5607 11 50         if (!x_sv || !SvROK(x_sv) || SvTYPE(SvRV(x_sv)) != SVt_PVAV)
    50          
    50          
5608 0           croak("quantile: 'x' must be an array reference");
5609            
5610 11           AV *restrict x_av = (AV*)SvRV(x_sv);
5611 11           size_t n_raw = av_len(x_av) + 1;
5612 11 50         if (n_raw == 0) croak("quantile: 'x' is empty");
5613             // --- Extract valid numeric data & drop NAs (Upgraded to NV)
5614             NV *restrict x;
5615 11 50         Newx(x, n_raw, NV);
5616 11           size_t n = 0;
5617 458 100         for (size_t i = 0; i < n_raw; i++) {
5618 447           SV **restrict tv = av_fetch(x_av, i, 0);
5619 447 50         if (tv && SvOK(*tv)) {
    50          
5620 447           x[n++] = SvNV(*tv);
5621             }
5622             }
5623 11 50         if (n == 0) {
5624 0           Safefree(x);
5625 0           croak("quantile: 'x' contains no valid numbers");
5626             }
5627             // --- Sort Data for Quantile Math ---
5628             // Note: You must update `compare_doubles` to accept and compare `NV` types!
5629 11           qsort(x, n, sizeof(NV), compare_NVs);
5630             // --- Parse Probabilities (Upgraded to NV) ---
5631 11           NV default_probs[] = {0.0, 0.25, 0.50, 0.75, 1.0};
5632 11           unsigned int n_probs = 5;
5633             NV *restrict probs;
5634 22 50         if (probs_sv && SvROK(probs_sv) && SvTYPE(SvRV(probs_sv)) == SVt_PVAV) {
    50          
    50          
5635 11           AV *restrict p_av = (AV*)SvRV(probs_sv);
5636 11           n_probs = av_len(p_av) + 1;
5637 11           Newx(probs, n_probs, NV);
5638 34 100         for (unsigned int i = 0; i < n_probs; i++) {
5639 23           SV **tv = av_fetch(p_av, i, 0);
5640 23 50         probs[i] = (tv && SvOK(*tv)) ? SvNV(*tv) : 0.0;
    50          
5641 23 50         if (probs[i] < 0.0 || probs[i] > 1.0) {
    50          
5642 0           Safefree(x); Safefree(probs);
5643 0           croak("quantile: probabilities must be between 0 and 1");
5644             }
5645             }
5646             } else {
5647 0           Newx(probs, n_probs, NV);
5648 0 0         for (unsigned int i = 0; i < n_probs; i++) probs[i] = default_probs[i];
5649             }
5650             // --- Calculate Quantiles (R Type 7 Algorithm) ---
5651 11           HV *restrict res_hv = newHV();
5652 34 100         for (size_t i = 0; i < n_probs; i++) {
5653 23           NV p = probs[i];
5654 23           NV q = 0.0;
5655              
5656 23 100         if (n == 1) {
5657 1           q = x[0];
5658 22 100         } else if (p == 1.0) {
5659 1           q = x[n - 1];
5660 21 100         } else if (p == 0.0) {
5661 1           q = x[0];
5662             } else {
5663 20           NV h = (n - 1) * p;
5664 20           unsigned int j = (unsigned int)h;
5665 20           NV gamma = h - j;
5666 20           q = (1.0 - gamma) * x[j] + gamma * x[j + 1];
5667             }
5668             // --- Format hash key with Epsilon guarding ---
5669             char key[32];
5670 23           double pct = (double)(p * 100.0); // Safe to cast to double just for formatting
5671 23           double pct_rounded = floor(pct + 0.5); // C89 safe rounding
5672             // Use 1e-9 epsilon check instead of strict integer equality
5673 23 50         if (fabs(pct - pct_rounded) < 1e-9) {
5674 23           snprintf(key, sizeof(key), "%.0f%%", pct_rounded);
5675             } else {
5676 0           snprintf(key, sizeof(key), "%.1f%%", pct);
5677             }
5678            
5679 23           hv_store(res_hv, key, strlen(key), newSVnv(q), 0);
5680             }
5681 11           Safefree(x); Safefree(probs);
5682 11           RETVAL = newRV_noinc((SV*)res_hv);
5683             }
5684             OUTPUT:
5685             RETVAL
5686              
5687             NV mean(...)
5688             PROTOTYPE: @
5689             INIT:
5690 48           NV total = 0;
5691 48           size_t count = 0;
5692             CODE:
5693 107 100         for (size_t i = 0; i < items; i++) {
5694 61           SV* restrict arg = ST(i);
5695 105 100         if (SvROK(arg) && SvTYPE(SvRV(arg)) == SVt_PVAV) {
    50          
5696 45           AV* restrict av = (AV*)SvRV(arg);
5697 45           size_t len = av_len(av) + 1;
5698 20557 100         for (size_t j = 0; j < len; j++) {
5699 20513           SV** restrict tv = av_fetch(av, j, 0);
5700 20513 50         if (tv && SvOK(*tv)) {
    100          
5701 20512           total += SvNV(*tv);
5702 20512           count++;
5703             } else {
5704 1           croak("mean: undefined value at array ref index %zu (argument %zu)", j, i);
5705             }
5706             }
5707 16 100         } else if (SvOK(arg)) {
5708 15           total += SvNV(arg);
5709 15           count++;
5710             } else {
5711 1           croak("mean: undefined value at argument index %zu", i);
5712             }
5713             }
5714 46 100         if (count == 0) croak("mean needs >= 1 element");
5715 45 100         RETVAL = total / count;
5716             OUTPUT:
5717             RETVAL
5718              
5719             void mode(...)
5720             PROTOTYPE: @
5721             PREINIT:
5722             HV *restrict counts;
5723             HV *restrict originals;
5724 5           size_t max_count = 0, arg_count = 0;
5725             HE *restrict he;
5726             PPCODE:
5727             /* counts: string(value) -> occurrence count */
5728             /* originals: string(value) -> SV* first-seen original */
5729 5           counts = (HV *)sv_2mortal((SV *)newHV());
5730 5           originals = (HV *)sv_2mortal((SV *)newHV());
5731              
5732 16 100         for (size_t i = 0; i < items; i++) {
5733 12           SV *restrict arg = ST(i);
5734 13 100         if (SvROK(arg) && SvTYPE(SvRV(arg)) == SVt_PVAV) {
    50          
5735 1           AV *restrict av = (AV *)SvRV(arg);
5736 1           size_t len = av_len(av) + 1;
5737 5 100         for (size_t j = 0; j < len; j++) {
5738 4           SV **restrict tv = av_fetch(av, j, 0);
5739 4 50         if (tv && SvOK(*tv)) {
    50          
5740             STRLEN klen;
5741 4           const char *restrict key = SvPV(*tv, klen);
5742 4           SV **restrict slot = hv_fetch(counts, key, klen, 1);
5743 4 50         if (!slot) croak("mode: internal hash error");
5744 4 100         size_t cnt = SvOK(*slot) ? SvIV(*slot) + 1 : 1;
5745 4           sv_setiv(*slot, cnt);
5746 4 100         if (cnt > max_count) max_count = cnt;
5747 4 100         if (cnt == 1)
5748 2           hv_store(originals, key, klen, newSVsv(*tv), 0);
5749 4           arg_count++;
5750             } else {
5751 0           croak("mode: undefined value at array ref index %zu (argument %zu)", j, i);
5752             }
5753             }
5754 11 100         } else if (SvOK(arg)) {
5755             STRLEN klen;
5756 10           const char *restrict key = SvPV(arg, klen);
5757 10           SV **restrict slot = hv_fetch(counts, key, klen, 1);
5758 10 50         if (!slot) croak("mode: internal hash error");
5759 10 100         size_t cnt = SvOK(*slot) ? SvIV(*slot) + 1 : 1;
5760 10           sv_setiv(*slot, cnt);
5761 10 100         if (cnt > max_count) max_count = cnt;
5762 10 100         if (cnt == 1)
5763 6           hv_store(originals, key, klen, newSVsv(arg), 0);
5764 10           arg_count++;
5765             } else {
5766 1           croak("mode: undefined value at argument index %zu", i);
5767             }
5768             }
5769              
5770 4 100         if (arg_count == 0)
5771 1           croak("mode needs >= 1 element");
5772              
5773 3           hv_iterinit(counts);
5774 13 100         while ((he = hv_iternext(counts))) {
5775 7 100         if (SvIV(hv_iterval(counts, he)) == max_count) {
5776             STRLEN klen;
5777 4 50         const char *restrict key = HePV(he, klen);
5778 4           SV **restrict orig = hv_fetch(originals, key, klen, 0);
5779 4 50         mXPUSHs(orig ? newSVsv(*orig) : newSVpvn(key, klen));
    50          
5780             }
5781             }
5782              
5783             NV sum(...)
5784             PROTOTYPE: @
5785             INIT:
5786 5           NV total = 0;
5787 5           size_t count = 0;
5788             CODE:
5789 19 100         for (size_t i = 0; i < items; i++) {
5790 16           SV* restrict arg = ST(i);
5791 17 100         if (SvROK(arg) && SvTYPE(SvRV(arg)) == SVt_PVAV) {
    50          
5792 2           AV* restrict av = (AV*)SvRV(arg);
5793 2           size_t len = av_len(av) + 1;
5794 11 100         for (size_t j = 0; j < len; j++) {
5795 10           SV** restrict tv = av_fetch(av, j, 0);
5796 10 50         if (tv && SvOK(*tv)) {
    100          
5797 9           total += SvNV(*tv);
5798 9           count++;
5799             } else {
5800 1           croak("sum: undefined value at array ref index %zu (argument %zu)", j, i);
5801             }
5802             }
5803 14 100         } else if (SvOK(arg)) {
5804 13           total += SvNV(arg);
5805 13           count++;
5806             } else {
5807 1           croak("sum: undefined value at argument index %zu", i);
5808             }
5809             }
5810 3 50         if (count == 0) croak("sum needs >= 1 element");
5811 3 100         RETVAL = total;
5812             OUTPUT:
5813             RETVAL
5814              
5815             NV sd(...)
5816             PROTOTYPE: @
5817             INIT:
5818 23           NV mean = 0.0, M2 = 0.0;
5819 23           size_t count = 0;
5820             CODE:
5821             /* Single Pass Standard Deviation via Welford's Algorithm */
5822 58 100         for (size_t i = 0; i < items; i++) {
5823 37           SV* restrict arg = ST(i);
5824 54 100         if (SvROK(arg) && SvTYPE(SvRV(arg)) == SVt_PVAV) {
    50          
5825 18           AV* restrict av = (AV*)SvRV(arg);
5826 18           size_t len = av_len(av) + 1;
5827 10086 100         for (size_t j = 0; j < len; j++) {
5828 10069           SV** restrict tv = av_fetch(av, j, 0);
5829 10069 50         if (tv && SvOK(*tv)) {
    100          
5830 10068           count++;
5831 10068           NV val = SvNV(*tv);
5832 10068           NV delta = val - mean;
5833 10068           mean += delta / count;
5834 10068           M2 += delta * (val - mean);
5835             } else {
5836 1           croak("sd: undefined value at array ref index %zu (argument %zu)", j, i);
5837             }
5838             }
5839 19 100         } else if (SvOK(arg)) {
5840 18           count++;
5841 18           NV val = SvNV(arg);
5842 18           NV delta = val - mean;
5843 18           mean += delta / count;
5844 18           M2 += delta * (val - mean);
5845             } else {
5846 1           croak("sd: undefined value at argument index %zu", i);
5847             }
5848             }
5849 21 100         if (count < 2) croak("sd needs >= 2 elements");
5850 20 100         RETVAL = sqrt(M2 / (count - 1));
5851             OUTPUT:
5852             RETVAL
5853              
5854             NV var(...)
5855             PROTOTYPE: @
5856             INIT:
5857 8           NV mean = 0.0, M2 = 0.0;
5858 8           size_t count = 0;
5859             CODE:
5860             // Single Pass Variance via Welford's Algorithm
5861 21 100         for (size_t i = 0; i < items; i++) {
5862 15           SV* restrict arg = ST(i);
5863 18 100         if (SvROK(arg) && SvTYPE(SvRV(arg)) == SVt_PVAV) {
    50          
5864 4           AV* restrict av = (AV*)SvRV(arg);
5865 4           size_t len = av_len(av) + 1;
5866 10015 100         for (size_t j = 0; j < len; j++) {
5867 10012           SV** restrict tv = av_fetch(av, j, 0);
5868 10012 50         if (tv && SvOK(*tv)) {
    100          
5869 10011           count++;
5870 10011           NV val = SvNV(*tv);
5871 10011           NV delta = val - mean;
5872 10011           mean += delta / count;
5873 10011           M2 += delta * (val - mean);
5874             } else {
5875 1           croak("var: undefined value at array ref index %zu (argument %zu)", j, i);
5876             }
5877             }
5878 11 100         } else if (SvOK(arg)) {
5879 10           count++;
5880 10           NV val = SvNV(arg);
5881 10           NV delta = val - mean;
5882 10           mean += delta / count;
5883 10           M2 += delta * (val - mean);
5884             } else {
5885 1           croak("var: undefined value at argument index %zu", i);
5886             }
5887             }
5888 6 100         if (count < 2) croak("var needs >= 2 elements");
5889 5 100         RETVAL = M2 / (count - 1);
5890             OUTPUT:
5891             RETVAL
5892              
5893             SV* t_test(...)
5894             CODE:
5895             {
5896 53           SV*restrict x_sv = NULL;
5897 53           SV*restrict y_sv = NULL;
5898 53           NV mu = 0.0, conf_level = 0.95;
5899 53           bool paired = FALSE, var_equal = FALSE;
5900 53           const char*restrict alternative = "two.sided";
5901 53           unsigned short int arg_idx = 0;
5902             // 1. Shift first positional argument as 'x' if it's an array reference
5903 53 50         if (arg_idx < items && SvROK(ST(arg_idx)) && SvTYPE(SvRV(ST(arg_idx))) == SVt_PVAV) {
    100          
    50          
5904 27           x_sv = ST(arg_idx);
5905 27           arg_idx++;
5906             }
5907             // 2. Shift second positional argument as 'y' if it's an array reference
5908 53 50         if (arg_idx < items && SvROK(ST(arg_idx)) && SvTYPE(SvRV(ST(arg_idx))) == SVt_PVAV) {
    100          
    50          
5909 10           y_sv = ST(arg_idx);
5910 10           arg_idx++;
5911             }
5912             // Ensure the remaining arguments form complete key-value pairs
5913 53 50         if ((items - arg_idx) % 2 != 0) {
5914 0           croak("Usage: t_test(\\@x, [\\@y], key => value, ...)");
5915             }
5916             // --- Parse named arguments from the remaining flat stack ---
5917 129 100         for (; arg_idx < items; arg_idx += 2) {
5918 76           const char*restrict key = SvPV_nolen(ST(arg_idx));
5919 76           SV*restrict val = ST(arg_idx + 1);
5920              
5921 76 100         if (strEQ(key, "x")) x_sv = val;
5922 51 100         else if (strEQ(key, "y")) y_sv = val;
5923 46 100         else if (strEQ(key, "mu")) mu = SvNV(val);
5924 11 100         else if (strEQ(key, "paired")) paired = SvTRUE(val);
5925 7 100         else if (strEQ(key, "var_equal")) var_equal = SvTRUE(val);
5926 4 100         else if (strEQ(key, "conf_level")) conf_level = SvNV(val);
5927 2 50         else if (strEQ(key, "alternative")) alternative = SvPV_nolen(val);
5928 0           else croak("t_test: unknown argument '%s'", key);
5929             }
5930              
5931             // --- Validate required / types ---
5932 53 100         if (!x_sv || !SvROK(x_sv) || SvTYPE(SvRV(x_sv)) != SVt_PVAV)
    50          
    50          
5933 1           croak("t_test: 'x' is a required argument and must be an ARRAY reference");
5934 52           AV*restrict x_av = (AV*)SvRV(x_sv);
5935 52           size_t nx = av_len(x_av) + 1;
5936 52 50         if (nx < 2) croak("t_test: 'x' needs at least 2 elements");
5937 52           AV*restrict y_av = NULL;
5938 52 100         if (y_sv && SvROK(y_sv) && SvTYPE(SvRV(y_sv)) == SVt_PVAV)
    50          
    50          
5939 14           y_av = (AV*)SvRV(y_sv);
5940 52 50         if (conf_level <= 0.0 || conf_level >= 1.0)
    100          
5941 1           croak("t_test: 'conf_level' must be between 0 and 1");
5942             // --- Computation via Welford's Algorithm --- */
5943 51           NV mean_x = 0.0, M2_x = 0.0, var_x, t_stat, df, p_val, std_err, cint_est;
5944 51           HV*restrict results = newHV();
5945 447 100         for (size_t i = 0; i < nx; i++) {
5946 396           SV**restrict tv = av_fetch(x_av, i, 0);
5947 396 50         NV val = (tv && SvOK(*tv)) ? SvNV(*tv) : 0;
    50          
5948 396           NV delta = val - mean_x;
5949 396           mean_x += delta / (i + 1);
5950 396           M2_x += delta * (val - mean_x);
5951             }
5952 51           var_x = M2_x / (nx - 1);
5953 51 100         if (var_x == 0.0 && !y_av) croak("t_test: data are essentially constant");
    50          
5954              
5955 63 100         if (paired || y_av) {
    100          
5956 15 100         if (!y_av) croak("t_test: 'y' must be provided for paired or two-sample tests");
5957 14           size_t ny = av_len(y_av) + 1;
5958 14 100         if (paired && ny != nx) croak("t_test: Paired arrays must be same length");
    100          
5959 13           NV mean_y = 0.0, M2_y = 0.0, var_y;
5960 140 100         for (size_t i = 0; i < ny; i++) {
5961 127           SV**restrict tv = av_fetch(y_av, i, 0);
5962 127 50         NV val = (tv && SvOK(*tv)) ? SvNV(*tv) : 0;
    50          
5963 127           NV delta = val - mean_y;
5964 127           mean_y += delta / (i + 1);
5965 127           M2_y += delta * (val - mean_y);
5966             }
5967 13           var_y = M2_y / (ny - 1);
5968 13 100         if (paired) {
5969 2           NV mean_d = 0.0, M2_d = 0.0;
5970 14 100         for (size_t i = 0; i < nx; i++) {
5971 12           SV**restrict dx_ptr = av_fetch(x_av, i, 0);
5972 12           SV**restrict dy_ptr = av_fetch(y_av, i, 0);
5973 12 50         NV dx = (dx_ptr && SvOK(*dx_ptr)) ? SvNV(*dx_ptr) : 0.0;
    50          
5974 12 50         NV dy = (dy_ptr && SvOK(*dy_ptr)) ? SvNV(*dy_ptr) : 0.0;
    50          
5975 12           NV val = dx - dy;
5976 12           NV delta = val - mean_d;
5977 12           mean_d += delta / (i + 1);
5978 12           M2_d += delta * (val - mean_d);
5979             }
5980 2           NV var_d = M2_d / (nx - 1);
5981 2 50         if (var_d == 0.0) croak("t_test: data are essentially constant");
5982 2           cint_est = mean_d;
5983 2           std_err = sqrt(var_d / nx);
5984 2           t_stat = (cint_est - mu) / std_err;
5985 2           df = nx - 1;
5986 2           hv_store(results, "estimate", 8, newSVnv(mean_d), 0);
5987 11 100         } else if (var_equal) {
5988 2 50         if (var_x == 0.0 && var_y == 0.0) croak("t_test: data are essentially constant");
    0          
5989 2           NV pooled_var = ((nx - 1) * var_x + (ny - 1) * var_y) / (nx + ny - 2);
5990 2           cint_est = mean_x - mean_y;
5991 2           std_err = sqrt(pooled_var * (1.0 / nx + 1.0 / ny));
5992 2           t_stat = (cint_est - mu) / std_err;
5993 2           df = nx + ny - 2;
5994 2           hv_store(results, "estimate_x", 10, newSVnv(mean_x), 0);
5995 2           hv_store(results, "estimate_y", 10, newSVnv(mean_y), 0);
5996             } else {
5997 9 50         if (var_x == 0.0 && var_y == 0.0) croak("t_test: data are essentially constant");
    0          
5998 9           cint_est = mean_x - mean_y;
5999 9           NV stderr_x2 = var_x / nx;
6000 9           NV stderr_y2 = var_y / ny;
6001 9           std_err = sqrt(stderr_x2 + stderr_y2);
6002 9           t_stat = (cint_est - mu) / std_err;
6003 9           df = pow(stderr_x2 + stderr_y2, 2) /
6004 9           (pow(stderr_x2, 2) / (nx - 1) + pow(stderr_y2, 2) / (ny - 1));
6005 9           hv_store(results, "estimate_x", 10, newSVnv(mean_x), 0);
6006 9           hv_store(results, "estimate_y", 10, newSVnv(mean_y), 0);
6007             }
6008             } else {
6009 35           cint_est = mean_x;
6010 35           std_err = sqrt(var_x / nx);
6011 35           t_stat = (cint_est - mu) / std_err;
6012 35           df = nx - 1;
6013 35           hv_store(results, "estimate", 8, newSVnv(mean_x), 0);
6014             }
6015 48           p_val = get_t_pvalue(t_stat, df, alternative);
6016 48           NV alpha = 1.0 - conf_level, t_crit, ci_lower, ci_upper;
6017 48 100         if (strcmp(alternative, "less") == 0) {
6018 1           t_crit = qt_tail(df, alpha);
6019 1           ci_lower = -INFINITY;
6020 1           ci_upper = cint_est + t_crit * std_err;
6021 47 100         } else if (strcmp(alternative, "greater") == 0) {
6022 1           t_crit = qt_tail(df, alpha);
6023 1           ci_lower = cint_est - t_crit * std_err;
6024 1           ci_upper = INFINITY;
6025             } else {
6026 46           t_crit = qt_tail(df, alpha / 2.0);
6027 46           ci_lower = cint_est - t_crit * std_err;
6028 46           ci_upper = cint_est + t_crit * std_err;
6029             }
6030 48           AV*restrict conf_int = newAV();
6031 48           av_push(conf_int, newSVnv(ci_lower));
6032 48           av_push(conf_int, newSVnv(ci_upper));
6033 48           hv_store(results, "statistic", 9, newSVnv(t_stat), 0);
6034 48           hv_store(results, "df", 2, newSVnv(df), 0);
6035 48           hv_store(results, "p_value", 7, newSVnv(p_val), 0);
6036 48           hv_store(results, "conf_int", 8, newRV_noinc((SV*)conf_int), 0);
6037 48           RETVAL = newRV_noinc((SV*)results);
6038             }
6039             OUTPUT:
6040             RETVAL
6041              
6042             void p_adjust(SV* p_sv, const char* method = "holm")
6043             INIT:
6044 15 100         if (!SvROK(p_sv) || SvTYPE(SvRV(p_sv)) != SVt_PVAV) {
    50          
6045 1           croak("p_adjust: first argument must be an ARRAY reference of p-values");
6046             }
6047 14           AV *restrict p_av = (AV*)SvRV(p_sv);
6048 14           size_t n = av_len(p_av) + 1;
6049             // Handle empty input
6050 14 100         if (n == 0) {
6051 1           XSRETURN_EMPTY;
6052             }
6053             // Normalize method string
6054             char meth[64];
6055 13           strncpy(meth, method, 63); meth[63] = '\0';
6056 157 100         for(unsigned short int i = 0; meth[i]; i++) meth[i] = tolower(meth[i]);
6057             // Resolve aliases
6058 13 100         if (strstr(meth, "benjamini") && strstr(meth, "hochberg")) strcpy(meth, "bh");
    100          
6059 13 100         if (strstr(meth, "benjamini") && strstr(meth, "yekutieli")) strcpy(meth, "by");
    50          
6060 13 50         if (strcmp(meth, "fdr") == 0) strcpy(meth, "bh");
6061             // Allocate C memory
6062             PVal *restrict arr;
6063             NV *restrict adj;
6064 13 50         Newx(arr, n, PVal);
6065 13 50         Newx(adj, n, NV);
6066              
6067 369 100         for (size_t i = 0; i < n; i++) {
6068 356           SV**restrict tv = av_fetch(p_av, i, 0);
6069 356 50         arr[i].p = (tv && SvOK(*tv)) ? SvNV(*tv) : 1.0;
    50          
6070 356           arr[i].orig_idx = i;
6071             }
6072             // Sort ascending (Stable sort using original index)
6073 13           qsort(arr, n, sizeof(PVal), cmp_pval);
6074             PPCODE:
6075 13 100         if (strcmp(meth, "bonferroni") == 0) {
6076 53 100         for (size_t i = 0; i < n; i++) {
6077 51           NV v = arr[i].p * n;
6078 51 100         adj[arr[i].orig_idx] = (v < 1.0) ? v : 1.0;
6079             }
6080 11 100         } else if (strcmp(meth, "holm") == 0) {
6081 2           NV cummax = 0.0;
6082 53 100         for (size_t i = 0; i < n; i++) {
6083 51           NV v = arr[i].p * (n - i);
6084 51 100         if (v > cummax) cummax = v;
6085 51 100         adj[arr[i].orig_idx] = (cummax < 1.0) ? cummax : 1.0;
6086             }
6087 9 100         } else if (strcmp(meth, "hochberg") == 0) {
6088 2           NV cummin = 1.0;
6089 53 100         for (ssize_t i = n - 1; i >= 0; i--) {
6090 51           NV v = arr[i].p * (n - i);
6091 51 100         if (v < cummin) cummin = v;
6092 51 50         adj[arr[i].orig_idx] = (cummin < 1.0) ? cummin : 1.0;
6093             }
6094 7 100         } else if (strcmp(meth, "bh") == 0) {
6095 2           NV cummin = 1.0;
6096 53 100         for (ssize_t i = n - 1; i >= 0; i--) {
6097 51           NV v = arr[i].p * n / (i + 1.0);
6098 51 100         if (v < cummin) cummin = v;
6099 51 50         adj[arr[i].orig_idx] = (cummin < 1.0) ? cummin : 1.0;
6100             }
6101 5 100         } else if (strcmp(meth, "by") == 0) {
6102 2           NV q = 0.0;
6103 53 100         for (size_t i = 1; i <= n; i++) q += 1.0 / i;
6104 2           NV cummin = 1.0;
6105 53 100         for (ssize_t i = n - 1; i >= 0; i--) {
6106 51           NV v = arr[i].p * n / (i + 1.0) * q;
6107 51 100         if (v < cummin) cummin = v;
6108 51 100         adj[arr[i].orig_idx] = (cummin < 1.0) ? cummin : 1.0;
6109             }
6110 3 100         } else if (strcmp(meth, "hommel") == 0) {
6111             NV *restrict pa, *restrict q_arr;
6112 2 50         Newx(pa, n, NV);
6113 2 50         Newx(q_arr, n, NV);
6114             // Initial: min(n * p[i] / (i + 1))
6115 2           NV min_val = n * arr[0].p;
6116 51 100         for (size_t i = 1; i < n; i++) {
6117 49           NV temp = (n * arr[i].p) / (i + 1.0);
6118 49 50         if (temp < min_val) {
6119 0           min_val = temp;
6120             }
6121             }
6122             // pa <- q <- rep(min, n)
6123 53 100         for (size_t i = 0; i < n; i++) {
6124 51           pa[i] = min_val;
6125 51           q_arr[i] = min_val;
6126             }
6127 50 100         for (size_t j = n - 1; j >= 2; j--) {
6128 48           ssize_t n_mj = n - j; // Max index for 'ij'. Length is n_mj + 1
6129 48           ssize_t i2_len = j - 1; // Length of 'i2
6130             // Calculate q1 = min(j * p[i2] / (2:j))
6131 48           NV q1 = (j * arr[n_mj + 1].p) / 2.0;
6132 1176 100         for (size_t k = 1; k < i2_len; k++) {
6133 1128           NV temp_q1 = (j * arr[n_mj + 1 + k].p) / (2.0 + k);
6134 1128 100         if (temp_q1 < q1) {
6135 266           q1 = temp_q1;
6136             }
6137             }
6138             // q[ij] <- pmin(j * p[ij], q1)
6139 1272 100         for (size_t i = 0; i <= n_mj; i++) {
6140 1224           NV v = j * arr[i].p;
6141 1224 100         q_arr[i] = (v < q1) ? v : q1;
6142             }
6143             // q[i2] <- q[n - j]
6144 1224 100         for (size_t i = 0; i < i2_len; i++) {
6145 1176           q_arr[n_mj + 1 + i] = q_arr[n_mj];
6146             }
6147             // pa <- pmax(pa, q)
6148 2448 100         for (size_t i = 0; i < n; i++) {
6149 2400 100         if (pa[i] < q_arr[i]) {
6150 1401           pa[i] = q_arr[i];
6151             }
6152             }
6153             }
6154             // pmin(1, pmax(pa, p))[ro] — map sorted results back to original indices
6155 53 100         for (size_t i = 0; i < n; i++) {
6156 51 100         NV v = (pa[i] > arr[i].p) ? pa[i] : arr[i].p;
6157 51 50         if (v > 1.0) v = 1.0;
6158 51           adj[arr[i].orig_idx] = v;
6159             }
6160 2           Safefree(pa); Safefree(q_arr);
6161 1 50         } else if (strcmp(meth, "none") == 0) {
6162 0 0         for (size_t i = 0; i < n; i++) {
6163 0           adj[arr[i].orig_idx] = arr[i].p;
6164             }
6165             } else {
6166 1           Safefree(arr); Safefree(adj);
6167 1           croak("Unknown p-value adjustment method: %s", method);
6168             }
6169             // Push values onto the Perl stack as a flat list
6170 12 50         EXTEND(SP, n);
6171 318 100         for (size_t i = 0; i < n; i++) {
6172 306           PUSHs(sv_2mortal(newSVnv(adj[i])));
6173             }
6174 12           Safefree(arr); arr = NULL;
6175 12           Safefree(adj); adj = NULL;
6176              
6177             NV median(...)
6178             PROTOTYPE: @
6179             INIT:
6180 15           size_t total_count = 0, k = 0;
6181             NV* restrict nums;
6182 15           NV median_val = 0.0;
6183             CODE:
6184             // Pass 1: Count valid elements — die immediately on any undef
6185 32 100         for (size_t i = 0; i < items; i++) {
6186 19           SV* restrict arg = ST(i);
6187 30 100         if (SvROK(arg) && SvTYPE(SvRV(arg)) == SVt_PVAV) {
    50          
6188 12           AV* restrict av = (AV*)SvRV(arg);
6189 12           size_t len = av_len(av) + 1;
6190 295 100         for (size_t j = 0; j < len; j++) {
6191 284           SV** restrict tv = av_fetch(av, j, 0);
6192 284 50         if (tv && SvOK(*tv)) {
    100          
6193 283           total_count++;
6194             } else {
6195 1           croak("median: undefined value at array ref index %zu (argument %zu)", j, i);
6196             }
6197             }
6198 7 100         } else if (SvOK(arg)) {
6199 6           total_count++;
6200             } else {
6201 1           croak("median: undefined value at argument index %zu", i);
6202             }
6203             }
6204 13 100         if (total_count == 0) croak("median needs >= 1 element");
6205              
6206             /* Allocate C array now that we know the exact size */
6207 12 50         Newx(nums, total_count, NV);
6208              
6209             /* Pass 2: Populate the C array — Safefree before any croak */
6210 27 100         for (size_t i = 0; i < items; i++) {
6211 15           SV* restrict arg = ST(i);
6212 26 100         if (SvROK(arg) && SvTYPE(SvRV(arg)) == SVt_PVAV) {
    50          
6213 11           AV* restrict av = (AV*)SvRV(arg);
6214 11           size_t len = av_len(av) + 1;
6215 293 100         for (size_t j = 0; j < len; j++) {
6216 282           SV** restrict tv = av_fetch(av, j, 0);
6217 282 50         if (tv && SvOK(*tv)) {
    50          
6218 282           nums[k++] = SvNV(*tv);
6219             } else {
6220 0           Safefree(nums);
6221 0           croak("median: undefined value at array ref index %zu (argument %zu)", j, i);
6222             }
6223             }
6224 4 50         } else if (SvOK(arg)) {
6225 4           nums[k++] = SvNV(arg);
6226             } else {
6227 0           Safefree(nums);
6228 0           croak("median: undefined value at argument index %zu", i);
6229             }
6230             }
6231             /* Sort and calculate median */
6232 12           qsort(nums, total_count, sizeof(NV), compare_doubles);
6233 12 100         if (total_count % 2 == 0) {
6234 4           median_val = (nums[total_count / 2 - 1] + nums[total_count / 2]) / 2.0;
6235             } else {
6236 8           median_val = nums[total_count / 2];
6237             }
6238 12           Safefree(nums);
6239 12           nums = NULL;
6240 12 100         RETVAL = median_val;
6241             OUTPUT:
6242             RETVAL
6243              
6244             SV* cor(SV* x_sv, SV* y_sv = &PL_sv_undef, const char* method = "pearson")
6245             INIT:
6246             // --- validate method -------------------------------------------
6247 70 100         if (strcmp(method, "pearson") != 0 &&
6248 11 100         strcmp(method, "spearman") != 0 &&
6249 5 100         strcmp(method, "kendall") != 0)
6250 1           croak("cor: unknown method '%s' (use 'pearson', 'spearman', or 'kendall')",
6251             method);
6252              
6253             // --- validate x ------------------------------------------------
6254 69 50         if (!SvROK(x_sv) || SvTYPE(SvRV(x_sv)) != SVt_PVAV)
    50          
6255 0           croak("cor: x must be an ARRAY reference");
6256              
6257 69           AV*restrict x_av = (AV*)SvRV(x_sv);
6258 69           size_t nx = av_len(x_av) + 1;
6259 69 50         if (nx == 0) croak("cor: x is empty");
6260              
6261             // --- detect whether x is a flat vector or a matrix (AoA) -------
6262 69           bool x_is_matrix = 0;
6263             {
6264 69           SV**restrict fp = av_fetch(x_av, 0, 0);
6265 69 50         if (fp && SvROK(*fp) && SvTYPE(SvRV(*fp)) == SVt_PVAV)
    100          
    50          
6266 1           x_is_matrix = 1;
6267             }
6268              
6269             // --- detect y ----------------------------
6270 138 50         bool has_y = (SvOK(y_sv) && SvROK(y_sv) &&
    50          
6271 69 50         SvTYPE(SvRV(y_sv)) == SVt_PVAV);
6272              
6273 69 50         AV*restrict y_av = has_y ? (AV*)SvRV(y_sv) : NULL;
6274 69 50         size_t ny = has_y ? av_len(y_av) + 1 : 0;
6275              
6276 69           bool y_is_matrix = 0;
6277 69 50         if (has_y && ny > 0) {
    50          
6278 69           SV**restrict fp = av_fetch(y_av, 0, 0);
6279 69 50         if (fp && SvROK(*fp) && SvTYPE(SvRV(*fp)) == SVt_PVAV)
    100          
    50          
6280 1           y_is_matrix = 1;
6281             }
6282              
6283             CODE:
6284             // Branch 1: both inputs are flat vectors → scalar result
6285 69 100         if (!x_is_matrix && !y_is_matrix) {
    50          
6286 68 50         if (!has_y) {
6287             /* cor(vector) == 1 by definition */
6288 0           RETVAL = newSVnv(1.0);
6289             } else {
6290 68 100         if (nx != ny)
6291 1           croak("cor: x and y must have the same length (%lu vs %lu)",
6292             nx, ny);
6293 67 50         if (nx < 2)
6294 0           croak("cor: need at least 2 observations");
6295             NV *restrict xd, *restrict yd;
6296 67 50         Newx(xd, nx, NV);
6297 67 50         Newx(yd, ny, NV);
6298 67           bool x_sd0 = 1, y_sd0 = 1;
6299 67           NV x_first = NAN, y_first = NAN;
6300 385 100         for (size_t i = 0; i < nx; i++) {
6301 318           SV**restrict tv = av_fetch(x_av, i, 0);
6302 318 50         NV val = (tv && SvOK(*tv) && looks_like_number(*tv)) ? SvNV(*tv) : NAN;
    50          
    50          
6303 318           xd[i] = val;
6304 318 50         if (!isnan(val)) {
6305 318 100         if (isnan(x_first)) x_first = val;
6306 251 100         else if (val != x_first) x_sd0 = 0;
6307             }
6308             }
6309 385 100         for (size_t i = 0; i < ny; i++) {
6310 318           SV**restrict tv = av_fetch(y_av, i, 0);
6311 318 50         NV val = (tv && SvOK(*tv) && looks_like_number(*tv)) ? SvNV(*tv) : NAN;
    50          
    50          
6312 318           yd[i] = val;
6313 318 50         if (!isnan(val)) {
6314 318 100         if (isnan(y_first)) y_first = val;
6315 251 100         else if (val != y_first) y_sd0 = 0;
6316             }
6317             }
6318 67 100         if (x_sd0 || y_sd0) {
    50          
6319 9           Safefree(xd); Safefree(yd);
6320 9 50         if (x_sd0) croak("cor: standard deviation of x is 0");
6321 0           croak("cor: standard deviation of y is 0");
6322             }
6323 58           NV r = compute_cor(xd, yd, nx, method);
6324 58           Safefree(xd); Safefree(yd);
6325 58           RETVAL = newSVnv(r);
6326             }
6327             } else {//Branch 2: x is a matrix (or y is a matrix) → AoA result
6328             // -- resolve x matrix dimensions
6329 1 50         if (!x_is_matrix)
6330 0           croak("cor: x must be a matrix (array ref of array refs) "
6331             "when y is a matrix");
6332              
6333 1           SV**restrict xr0 = av_fetch(x_av, 0, 0);
6334 1 50         if (!xr0 || !SvROK(*xr0) || SvTYPE(SvRV(*xr0)) != SVt_PVAV)
    50          
    50          
6335 0           croak("cor: each row of x must be an ARRAY reference");
6336              
6337 1           size_t ncols_x = av_len((AV*)SvRV(*xr0)) + 1;
6338 1 50         if (ncols_x == 0) croak("cor: x matrix has zero columns");
6339              
6340 1           size_t nrows = nx; /* observations */
6341              
6342             // PRE-VALIDATION PASS: Ensure all rows are arrays to prevent memory leaks on croak
6343 4 100         for (size_t i = 0; i < nrows; i++) {
6344 3           SV**restrict rv = av_fetch(x_av, i, 0);
6345 3 50         if (!rv || !SvROK(*rv) || SvTYPE(SvRV(*rv)) != SVt_PVAV)
    50          
    50          
6346 0           croak("cor: x row %lu is not an array ref", i);
6347             }
6348              
6349 1 50         if (has_y && y_is_matrix) {
    50          
6350 1 50         if (ny != nrows) croak("cor: x and y must have the same number of rows (%lu vs %lu)", nrows, ny);
6351 4 100         for (size_t i = 0; i < nrows; i++) {
6352 3           SV**restrict rv = av_fetch(y_av, i, 0);
6353 3 50         if (!rv || !SvROK(*rv) || SvTYPE(SvRV(*rv)) != SVt_PVAV)
    50          
    50          
6354 0           croak("cor: y row %lu is not an array ref", i);
6355             }
6356             }
6357             // -- extract x columns
6358             NV **restrict col_x;
6359 1 50         Newx(col_x, ncols_x, NV*);
6360 3 100         for (size_t j = 0; j < ncols_x; j++) {
6361 2 50         Newx(col_x[j], nrows, NV);
6362 2           bool sd0 = 1;
6363 2           NV first = NAN;
6364 8 100         for (size_t i = 0; i < nrows; i++) {
6365 6           SV**restrict rv = av_fetch(x_av, i, 0);
6366 6           AV*restrict row = (AV*)SvRV(*rv);
6367 6           SV**restrict cv = av_fetch(row, j, 0);
6368 6 50         NV val = (cv && SvOK(*cv) && looks_like_number(*cv)) ? SvNV(*cv) : NAN;
    50          
    50          
6369 6           col_x[j][i] = val;
6370 6 50         if (!isnan(val)) {
6371 6 100         if (isnan(first)) first = val;
6372 4 50         else if (val != first) sd0 = 0;
6373             }
6374             }
6375 2 50         if (sd0) {
6376 0 0         for (size_t k = 0; k <= j; k++) Safefree(col_x[k]);
6377 0           Safefree(col_x);
6378 0           croak("cor: standard deviation is 0 in x column %lu", j);
6379             }
6380             }
6381             // -- resolve y: separate matrix or re-use x (symmetric)
6382             size_t ncols_y;
6383 1           NV **restrict col_y = NULL;
6384 1           bool symmetric = 0;
6385             // 1 = cor(X) — result is symmetric
6386 2 50         if (has_y && y_is_matrix) {
    50          
6387             // cross-correlation: X (nrows × p) vs Y (nrows × q)
6388 1           SV**restrict yr0 = av_fetch(y_av, 0, 0);
6389 1           ncols_y = av_len((AV*)SvRV(*yr0)) + 1;
6390 1 50         if (ncols_y == 0) croak("cor: y matrix has zero columns");
6391              
6392 1 50         Newx(col_y, ncols_y, NV*);
6393 3 100         for (size_t j = 0; j < ncols_y; j++) {
6394 2 50         Newx(col_y[j], nrows, NV);
6395 2           bool sd0 = 1;
6396 2           NV first = NAN;
6397 8 100         for (size_t i = 0; i < nrows; i++) {
6398 6           SV**restrict rv = av_fetch(y_av, i, 0);
6399 6           AV*restrict row = (AV*)SvRV(*rv);
6400 6           SV**restrict cv = av_fetch(row, j, 0);
6401 6 50         NV val = (cv && SvOK(*cv) && looks_like_number(*cv)) ? SvNV(*cv) : NAN;
    50          
    50          
6402 6           col_y[j][i] = val;
6403 6 50         if (!isnan(val)) {
6404 6 100         if (isnan(first)) first = val;
6405 4 50         else if (val != first) sd0 = 0;
6406             }
6407             }
6408 2 50         if (sd0) {
6409 0 0         for (size_t k = 0; k < ncols_x; k++) Safefree(col_x[k]);
6410 0           Safefree(col_x);
6411 0 0         for (size_t k = 0; k <= j; k++) Safefree(col_y[k]);
6412 0           Safefree(col_y);
6413 0           croak("cor: standard deviation is 0 in y column %lu", j);
6414             }
6415             }
6416             } else { // cor(X) — symmetric p×p result; share column arrays
6417 0           ncols_y = ncols_x;
6418 0           col_y = col_x;
6419 0           symmetric = 1;
6420             }
6421 1 50         if (nrows < 2)
6422 0           croak("cor: need at least 2 observations (got %lu)", nrows);
6423             // -- build cache for symmetric case: compute upper triangle, store results, mirror to lower triangle
6424 1           AV*restrict result_av = newAV();
6425 1           av_extend(result_av, ncols_x - 1);
6426             // Allocate per-row AVs up front so we can fill them in order
6427             AV **restrict rows_out;
6428 1 50         Newx(rows_out, ncols_x, AV*);
6429 3 100         for (size_t i = 0; i < ncols_x; i++) {
6430 2           rows_out[i] = newAV();
6431 2           av_extend(rows_out[i], ncols_y - 1);
6432             }
6433 1 50         if (symmetric) {
6434             /* Upper triangle + diagonal, then mirror. r_cache[i][j] (j >= i) holds the computed value. */
6435             NV **restrict r_cache;
6436 0 0         Newx(r_cache, ncols_x, NV*);
6437 0 0         for (size_t i = 0; i < ncols_x; i++)
6438 0 0         Newx(r_cache[i], ncols_x, NV);
6439              
6440 0 0         for (size_t i = 0; i < ncols_x; i++) {
6441 0           r_cache[i][i] = 1.0; // diagonal
6442 0 0         for (size_t j = i + 1; j < ncols_x; j++) {
6443 0           NV r = compute_cor(col_x[i], col_x[j], nrows, method);
6444 0           r_cache[i][j] = r;
6445 0           r_cache[j][i] = r; // symmetry
6446             }
6447             }
6448             // fill output AoA from cache
6449 0 0         for (size_t i = 0; i < ncols_x; i++)
6450 0 0         for (size_t j = 0; j < ncols_x; j++)
6451 0           av_store(rows_out[i], j, newSVnv(r_cache[i][j]));
6452              
6453 0 0         for (size_t i = 0; i < ncols_x; i++) Safefree(r_cache[i]);
6454 0           Safefree(r_cache); r_cache = NULL;
6455             } else {
6456             // cross-correlation: every (i,j) pair is independent
6457 3 100         for (size_t i = 0; i < ncols_x; i++)
6458 6 100         for (size_t j = 0; j < ncols_y; j++)
6459 4           av_store(rows_out[i], j, newSVnv(compute_cor(col_x[i], col_y[j], nrows, method)));
6460             }
6461             // push row AVs into result
6462 3 100         for (size_t i = 0; i < ncols_x; i++)
6463 2           av_store(result_av, i, newRV_noinc((SV*)rows_out[i]));
6464 1           Safefree(rows_out); rows_out = NULL;
6465             // -- free column arrays -------------------------------------
6466 3 100         for (size_t j = 0; j < ncols_x; j++) Safefree(col_x[j]);
6467 1           Safefree(col_x); col_x = NULL;
6468 1 50         if (!symmetric) {
6469 3 100         for (size_t j = 0; j < ncols_y; j++) Safefree(col_y[j]);
6470 1           Safefree(col_y);
6471             }
6472 1           RETVAL = newRV_noinc((SV*)result_av);
6473             }
6474             OUTPUT:
6475             RETVAL
6476              
6477             void scale(...)
6478             PROTOTYPE: @
6479             PPCODE:
6480             {
6481 5           bool do_center_mean = TRUE, do_scale_sd = TRUE;
6482 5           NV center_val = 0.0, scale_val = 1.0;
6483 5           size_t data_items = items;
6484             // 1. Parse Options Hash (if it exists as the last argument)
6485 5 50         if (items > 0) {
6486 5           SV*restrict last_arg = ST(items - 1);
6487 5 100         if (SvROK(last_arg) && SvTYPE(SvRV(last_arg)) == SVt_PVHV) {
    100          
6488 2           data_items = items - 1; // Exclude hash from data processing
6489 2           HV*restrict opt_hv = (HV*)SvRV(last_arg);
6490             // --- Parse 'center'
6491 2           SV**restrict center_sv = hv_fetch(opt_hv, "center", 6, 0);
6492 2 50         if (center_sv) {
6493 2           SV*restrict val_sv = *center_sv;
6494 2 50         if (!SvOK(val_sv)) {
6495 0           do_center_mean = FALSE; center_val = 0.0;
6496             } else {
6497 2           char *restrict str = SvPV_nolen(val_sv);
6498             /* Trap booleans and empty strings before numeric checks */
6499 2 50         if (strcasecmp(str, "mean") == 0 || strcasecmp(str, "true") == 0 || strcmp(str, "1") == 0) {
    50          
    100          
6500 1           do_center_mean = TRUE;
6501 1 50         } else if (strcasecmp(str, "none") == 0 || strcasecmp(str, "false") == 0 || strcmp(str, "0") == 0 || strcmp(str, "") == 0) {
    50          
    50          
    0          
6502 1           do_center_mean = FALSE; center_val = 0.0;
6503 0 0         } else if (looks_like_number(val_sv)) {
6504 0           do_center_mean = FALSE; center_val = SvNV(val_sv);
6505 0 0         } else if (SvTRUE(val_sv)) {
6506 0           do_center_mean = TRUE;
6507             } else {
6508 0           do_center_mean = FALSE; center_val = 0.0;
6509             }
6510             }
6511             }
6512             // --- Parse 'scale' ---
6513 2           SV**restrict scale_sv = hv_fetch(opt_hv, "scale", 5, 0);
6514 2 100         if (scale_sv) {
6515 1           SV*restrict val_sv = *scale_sv;
6516 1 50         if (!SvOK(val_sv)) {
6517 0           do_scale_sd = FALSE; scale_val = 1.0;
6518             } else {
6519 1           char *restrict str = SvPV_nolen(val_sv);
6520 1 50         if (strcasecmp(str, "sd") == 0 || strcasecmp(str, "true") == 0 || strcmp(str, "1") == 0) {
    50          
    50          
6521 0           do_scale_sd = TRUE;
6522 1 50         } else if (strcasecmp(str, "none") == 0 || strcasecmp(str, "false") == 0 || strcmp(str, "0") == 0 || strcmp(str, "") == 0) {
    50          
    50          
    0          
6523 1           do_scale_sd = FALSE; scale_val = 1.0;
6524 0 0         } else if (looks_like_number(val_sv)) {
6525 0           do_scale_sd = FALSE; scale_val = SvNV(val_sv);
6526 0 0         if (scale_val == 0.0) scale_val = 1.0; /* Prevent Division By Zero */
6527 0 0         } else if (SvTRUE(val_sv)) {
6528 0           do_scale_sd = TRUE;
6529             } else {
6530 0           do_scale_sd = FALSE; scale_val = 1.0;
6531             }
6532             }
6533             }
6534             }
6535             }
6536             // 2. Detect if the input is a Matrix (Array of Arrays)
6537 5           bool is_matrix = FALSE;
6538 5 100         if (data_items == 1) {
6539 2           SV*restrict first_arg = ST(0);
6540 2 100         if (SvROK(first_arg) && SvTYPE(SvRV(first_arg)) == SVt_PVAV) {
    50          
6541 1           AV*restrict av = (AV*)SvRV(first_arg);
6542 1 50         if (av_len(av) >= 0) {
6543 1           SV**restrict first_elem = av_fetch(av, 0, 0);
6544 1 50         if (first_elem && SvROK(*first_elem) && SvTYPE(SvRV(*first_elem)) == SVt_PVAV) {
    50          
    50          
6545 1           is_matrix = TRUE;
6546             }
6547             }
6548             }
6549             }
6550 5 100         if (is_matrix) {
6551             // MATRIX MODE: Scale columns independently (Just like R)
6552 1           AV*restrict mat_av = (AV*)SvRV(ST(0));
6553 1           size_t nrow = av_len(mat_av) + 1, ncol = 0;
6554 1           SV**restrict first_row = av_fetch(mat_av, 0, 0);
6555 1           ncol = av_len((AV*)SvRV(*first_row)) + 1;
6556 1 50         if (nrow == 0 || ncol == 0) croak("scale requires non-empty matrix");
    50          
6557             // Create a new matrix for the scaled output
6558 1           AV*restrict result_av = newAV();
6559 1           av_extend(result_av, nrow - 1);
6560 1           AV**restrict row_ptrs = (AV**)safemalloc(nrow * sizeof(AV*));
6561 4 100         for (size_t r = 0; r < nrow; r++) {
6562 3           row_ptrs[r] = newAV();
6563 3           av_extend(row_ptrs[r], ncol - 1);
6564 3           av_push(result_av, newRV_noinc((SV*)row_ptrs[r]));
6565             }
6566             // Calculate and apply scale per column
6567 3 100         for (size_t c = 0; c < ncol; c++) {
6568 2           NV col_sum = 0.0;
6569             NV *restrict col_data;
6570 2 50         Newx(col_data, nrow, NV);
6571             // Extract the column data
6572 8 100         for (size_t r = 0; r < nrow; r++) {
6573 6           SV**restrict row_sv = av_fetch(mat_av, r, 0);
6574 6 50         if (row_sv && SvROK(*row_sv)) {
    50          
6575 6           AV*restrict row_av = (AV*)SvRV(*row_sv);
6576 6           SV**restrict cell_sv = av_fetch(row_av, c, 0);
6577 6 50         col_data[r] = (cell_sv && SvOK(*cell_sv)) ? SvNV(*cell_sv) : 0.0;
    50          
6578             } else {
6579 0           col_data[r] = 0.0;
6580             }
6581 6           col_sum += col_data[r];
6582             }
6583              
6584 2 50         NV col_center = do_center_mean ? (col_sum / nrow) : center_val;
6585 2           NV col_scale = scale_val;
6586             // Calculate Standard Deviation for this specific column if needed
6587 2 50         if (do_scale_sd) {
6588 2 50         if (nrow <= 1) {
6589 0           Safefree(col_data);
6590 0           safefree(row_ptrs);
6591 0           croak("scale needs >= 2 rows to calculate standard deviation for a matrix column");
6592             }
6593 2           NV sum_sq = 0.0;
6594 8 100         for (size_t r = 0; r < nrow; r++) {
6595 6           NV diff = col_data[r] - col_center;
6596 6           sum_sq += diff * diff;
6597             }
6598 2           col_scale = sqrt(sum_sq / (nrow - 1));
6599             }
6600             // Store scaled values back into the new matrix rows
6601 8 100         for (size_t r = 0; r < nrow; r++) {
6602 6           NV centered = col_data[r] - col_center;
6603 6 50         NV final_val = (col_scale == 0.0) ? (0.0 / 0.0) : (centered / col_scale);
6604 6           av_store(row_ptrs[r], c, newSVnv(final_val));
6605             }
6606 2           Safefree(col_data);
6607             }
6608 1           safefree(row_ptrs);
6609             // Push the resulting matrix as a single Reference onto the Perl stack
6610 1 50         EXTEND(SP, 1);
6611 1           PUSHs(sv_2mortal(newRV_noinc((SV*)result_av)));
6612             } else {
6613             // FLAT LIST MODE: Original functionality
6614 4           size_t total_count = 0, k = 0;
6615             NV *restrict nums;
6616 4           NV sum = 0.0;
6617 20 100         for (size_t i = 0; i < data_items; i++) {
6618 16           SV*restrict arg = ST(i);
6619 16 50         if (SvROK(arg) && SvTYPE(SvRV(arg)) == SVt_PVAV) {
    0          
6620 0           AV*restrict av = (AV*)SvRV(arg);
6621 0           size_t len = av_len(av) + 1;
6622 0 0         for (unsigned int j = 0; j < len; j++) {
6623 0           SV**restrict tv = av_fetch(av, j, 0);
6624 0 0         if (tv && SvOK(*tv)) { total_count++; }
    0          
6625             }
6626 16 50         } else if (SvOK(arg)) {
6627 16           total_count++;
6628             }
6629             }
6630 4 50         if (total_count == 0) croak("scale requires at least 1 numeric element");
6631 4 50         Newx(nums, total_count, NV);
6632 20 100         for (size_t i = 0; i < data_items; i++) {
6633 16           SV*restrict arg = ST(i);
6634 16 50         if (SvROK(arg) && SvTYPE(SvRV(arg)) == SVt_PVAV) {
    0          
6635 0           AV*restrict av = (AV*)SvRV(arg);
6636 0           size_t len = av_len(av) + 1;
6637 0 0         for (size_t j = 0; j < len; j++) {
6638 0           SV**restrict tv = av_fetch(av, j, 0);
6639 0 0         if (tv && SvOK(*tv)) {
    0          
6640 0           NV val = SvNV(*tv);
6641 0           nums[k++] = val; sum += val;
6642             }
6643             }
6644 16 50         } else if (SvOK(arg)) {
6645 16           NV val = SvNV(arg);
6646 16           nums[k++] = val; sum += val;
6647             }
6648             }
6649 4 100         if (do_center_mean) center_val = sum / total_count;
6650 4 100         if (do_scale_sd) {
6651 3 100         if (total_count <= 1) {
6652 1           Safefree(nums);
6653 1           croak("scale needs >= 2 elements to calculate SD");
6654             }
6655 2           NV sum_sq = 0.0;
6656 12 100         for (size_t i = 0; i < total_count; i++) {
6657 10           NV diff = nums[i] - center_val;
6658 10           sum_sq += diff * diff;
6659             }
6660 2           scale_val = sqrt(sum_sq / (total_count - 1));
6661             }
6662 3 50         EXTEND(SP, total_count);
6663 18 100         for (size_t i = 0; i < total_count; i++) {
6664 15           NV centered = nums[i] - center_val;
6665 15 50         NV final_val = (scale_val == 0.0) ? (0.0 / 0.0) : (centered / scale_val);
6666 15           PUSHs(sv_2mortal(newSVnv(final_val)));
6667             }
6668 3           Safefree(nums); nums = NULL;
6669             }
6670             }
6671              
6672             SV* matrix(...)
6673             CODE:
6674 6           SV*restrict data_sv = NULL;
6675 6           size_t nrow = 0, ncol = 0;
6676 6           bool byrow = FALSE, nrow_set = FALSE, ncol_set = FALSE;
6677              
6678             /* Hybrid Argument Parser */
6679 6 50         if (items > 0 && SvROK(ST(0)) && SvTYPE(SvRV(ST(0))) == SVt_PVAV) {
    100          
    50          
6680             /* POSITIONAL: matrix($data_ref, $nrow, $ncol, $byrow) */
6681 1           data_sv = ST(0);
6682 1 50         if (items > 1 && SvOK(ST(1))) {
    50          
6683 1           nrow = (size_t)SvUV(ST(1));
6684 1           nrow_set = TRUE;
6685             }
6686 1 50         if (items > 2 && SvOK(ST(2))) {
    0          
6687 0           ncol = (size_t)SvUV(ST(2));
6688 0           ncol_set = TRUE;
6689             }
6690 1 50         if (items > 3 && SvOK(ST(3))) {
    0          
6691 0           byrow = SvTRUE(ST(3));
6692             }
6693 5 50         } else if (items % 2 == 0) {
6694             /* NAMED: matrix(data => [...], nrow => $n, ncol => $m) */
6695 16 100         for (size_t i = 0; i < items; i += 2) {
6696 11           char*restrict key = SvPV_nolen(ST(i));
6697 11           SV*restrict val = ST(i + 1);
6698 11 100         if (strEQ(key, "data")) {
6699 5           data_sv = val;
6700 6 100         } else if (strEQ(key, "nrow")) {
6701 4 50         if (SvOK(val)) { nrow = (size_t)SvUV(val); nrow_set = TRUE; }
6702 2 100         } else if (strEQ(key, "ncol")) {
6703 1 50         if (SvOK(val)) { ncol = (size_t)SvUV(val); ncol_set = TRUE; }
6704 1 50         } else if (strEQ(key, "byrow")) {
6705 1           byrow = SvTRUE(val);
6706             } else {
6707 0           croak("Unknown option: %s", key);
6708             }
6709             }
6710             } else {
6711 0           croak("Usage: matrix($data_ref, $nrow, $ncol, $byrow) OR matrix(data => $data_ref, ...)");
6712             }
6713             // Validate data input
6714 6 50         if (!data_sv || !SvROK(data_sv) || SvTYPE(SvRV(data_sv)) != SVt_PVAV) {
    100          
    50          
6715 1           croak("The 'data' option must be an array reference (e.g. [1..6] or rnorm(6))");
6716             }
6717 5           AV*restrict data_av = (AV*)SvRV(data_sv);
6718 5 50         size_t data_len = (UV)(av_top_index(data_av) + 1);
6719 5 100         if (data_len == 0) {
6720 1           croak("Data array cannot be empty");
6721             }
6722             // R-style dimension inference
6723 4 50         if (!nrow_set && !ncol_set) {
    0          
6724 0           nrow = data_len;
6725 0           ncol = 1;
6726 4 50         } else if (nrow_set && !ncol_set) {
    100          
6727 3           ncol = (data_len + nrow - 1) / nrow;
6728 1 50         } else if (!nrow_set && ncol_set) {
    0          
6729 0           nrow = (data_len + ncol - 1) / ncol;
6730             }
6731             // Final safety check for dimensions
6732 4 100         if (nrow == 0 || ncol == 0) {
    50          
6733 1           croak("Dimensions must be greater than 0");
6734             }
6735             // Create the matrix (Array of Arrays)
6736 3           AV*restrict result_av = newAV();
6737 3           av_extend(result_av, nrow - 1);
6738             size_t r, c; // Use unsigned types for counters to prevent negative indexing
6739 3           AV**restrict row_ptrs = (AV**restrict)safemalloc(nrow * sizeof(AV*)); /* Pre-allocate row pointers */
6740 9 100         for (r = 0; r < nrow; r++) {
6741 6           row_ptrs[r] = newAV();
6742 6           av_extend(row_ptrs[r], ncol - 1);
6743 6           av_push(result_av, newRV_noinc((SV*)row_ptrs[r]));
6744             }
6745             // Fill the matrix
6746 3           size_t total_cells = nrow * ncol;
6747 21 100         for (size_t i = 0; i < total_cells; i++) {
6748             // Vector recycling logic
6749 18           SV**restrict fetched = av_fetch(data_av, i % data_len, 0);
6750 18 50         SV*restrict val = fetched ? newSVsv(*fetched) : newSV(0);
6751 18 100         if (byrow) {
6752 6           r = i / ncol;
6753 6           c = i % ncol;
6754             } else {
6755 12           r = i % nrow;
6756 12           c = i / nrow;
6757             }
6758 18           av_store(row_ptrs[r], c, val);
6759             }
6760 3           safefree(row_ptrs);
6761 3           RETVAL = newRV_noinc((SV*)result_av);
6762             OUTPUT:
6763             RETVAL
6764              
6765             SV* lm(...)
6766             CODE:
6767             {
6768 22           const char *restrict formula = NULL;
6769 22           SV *restrict data_sv = NULL;
6770             char f_cpy[512];
6771             char *restrict src, *restrict dst, *restrict tilde, *restrict lhs, *restrict rhs, *restrict chunk;
6772 22           char **restrict terms = NULL, **restrict uniq_terms = NULL, **restrict exp_terms = NULL;
6773 22           bool *restrict is_dummy = NULL;
6774 22           char **restrict dummy_base = NULL, **restrict dummy_level = NULL;
6775 22           unsigned int term_cap = 64, exp_cap = 64, num_terms = 0, num_uniq = 0, p = 0, p_exp = 0;
6776 22           size_t n = 0, valid_n = 0, i, j, k, l, l1, l2;
6777 22           bool has_intercept = TRUE;
6778 22           char **restrict row_names = NULL, **restrict valid_row_names = NULL;
6779 22           HV **restrict row_hashes = NULL;
6780 22           HV *restrict data_hoa = NULL;
6781 22           SV *restrict ref = NULL;
6782 22           NV *restrict X = NULL, *restrict Y = NULL, *restrict XtX = NULL, *restrict XtY = NULL;
6783 22           bool *restrict aliased = NULL;
6784 22           NV *restrict beta = NULL;
6785 22           int final_rank = 0, df_res = 0;
6786             HV *restrict res_hv, *restrict coef_hv, *restrict fitted_hv, *restrict resid_hv, *restrict summary_hv;
6787             AV *restrict terms_av;
6788 22           NV rss = 0.0, rse_sq = 0.0;
6789             HE *restrict entry;
6790              
6791 22 50         if (items % 2 != 0) croak("Usage: lm(formula => 'mpg ~ wt * hp', data => \\%%mtcars)");
6792              
6793 64 100         for (unsigned short i_arg = 0; i_arg < items; i_arg += 2) {
6794 42           const char *restrict key = SvPV_nolen(ST(i_arg));
6795 42           SV *restrict val = ST(i_arg + 1);
6796 42 100         if (strEQ(key, "formula")) formula = SvPV_nolen(val);
6797 21 50         else if (strEQ(key, "data")) data_sv = val;
6798 0           else croak("lm: unknown argument '%s'", key);
6799             }
6800 22 100         if (!formula) croak("lm: formula is required");
6801 21 100         if (!data_sv || !SvROK(data_sv)) croak("lm: data is required and must be a reference");
    100          
6802              
6803             /* PHASE 1: Data Extraction */
6804 19           ref = SvRV(data_sv);
6805 19 50         if (SvTYPE(ref) == SVt_PVHV) {
6806 19           HV *restrict hv = (HV*)ref;
6807 19 50         if (hv_iterinit(hv) == 0) croak("lm: Data hash is empty");
6808 19           entry = hv_iternext(hv);
6809 19 50         if (entry) {
6810 19           SV *restrict val = hv_iterval(hv, entry);
6811 19 50         if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVAV) {
    100          
6812 12           data_hoa = hv;
6813 12           n = av_len((AV*)SvRV(val)) + 1;
6814 12 50         Newx(row_names, n, char*);
6815 82 100         for (size_t i = 0; i < n; i++) {
6816             char buf[32];
6817 70           snprintf(buf, sizeof(buf), "%lu", (unsigned long)(i + 1));
6818 70           row_names[i] = savepv(buf);
6819             }
6820 7 50         } else if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVHV) {
    50          
6821 7           n = hv_iterinit(hv);
6822 7 50         Newx(row_names, n, char*); Newx(row_hashes, n, HV*);
    50          
6823 7           i = 0;
6824 231 100         while ((entry = hv_iternext(hv))) {
6825             I32 len;
6826 224           row_names[i] = savepv(hv_iterkey(entry, &len));
6827 224           row_hashes[i] = (HV*)SvRV(hv_iterval(hv, entry));
6828 224           i++;
6829             }
6830 0           } else croak("lm: Hash values must be ArrayRefs (HoA) or HashRefs (HoH)");
6831             }
6832 0 0         } else if (SvTYPE(ref) == SVt_PVAV) {
6833 0           AV *restrict av = (AV*)ref; n = av_len(av) + 1;
6834 0 0         Newx(row_names, n, char*);
6835 0 0         Newx(row_hashes, n, HV*);
6836 0 0         for (size_t i = 0; i < n; i++) {
6837 0           SV **restrict val = av_fetch(av, i, 0);
6838 0 0         if (val && SvROK(*val) && SvTYPE(SvRV(*val)) == SVt_PVHV) {
    0          
    0          
6839 0           row_hashes[i] = (HV*)SvRV(*val);
6840 0           char buf[32]; snprintf(buf, sizeof(buf), "%lu", (unsigned long)(i + 1));
6841 0           row_names[i] = savepv(buf);
6842             } else {
6843 0 0         for (k = 0; k < i; k++) Safefree(row_names[k]);
6844 0           Safefree(row_names); Safefree(row_hashes);
6845 0           croak("lm: Array values must be HashRefs (AoH)");
6846             }
6847             }
6848 0           } else croak("lm: Data must be an Array or Hash reference");
6849             /* PHASE 2: Formula Parsing & `.` Expansion */
6850 19           src = (char*)formula; dst = f_cpy;
6851 215 100         while (*src && (dst - f_cpy < 511)) { if (!isspace(*src)) { *dst++ = *src; } src++; }
    100          
    50          
6852 19           *dst = '\0';
6853              
6854 19           tilde = strchr(f_cpy, '~');
6855 19 100         if (!tilde) {
6856 3 100         for (size_t i = 0; i < n; i++) Safefree(row_names[i]);
6857 1 50         Safefree(row_names); if (row_hashes) Safefree(row_hashes);
6858 1           croak("lm: invalid formula, missing '~'");
6859             }
6860 18           *tilde = '\0';
6861 18           lhs = f_cpy;
6862 18           rhs = tilde + 1;
6863              
6864             // Remove intercept-suppression markers from RHS.
6865             // IMPORTANT: skip tokens that appear inside I(...) wrappers so that
6866             // expressions like I(x^-1) are never mistakenly treated as "-1".
6867             {
6868 18           char *restrict p_idx = rhs;
6869 89 100         while (*p_idx) {
6870             // Skip over I(...) sub-expressions entirely
6871 71 50         if (p_idx[0] == 'I' && p_idx[1] == '(') {
    0          
6872 0           int depth = 0;
6873 0 0         while (*p_idx) { if (*p_idx == '(') depth++; else if (*p_idx == ')') { depth--; if (depth == 0) { p_idx++; break; } } p_idx++; }
    0          
    0          
    0          
6874 0           continue;
6875             }
6876             // Match bare -1
6877 71 100         if (p_idx[0] == '-' && p_idx[1] == '1' &&
    50          
6878 1 50         (p_idx[2] == '\0' || p_idx[2] == '+' || p_idx[2] == '-')) {
    0          
    0          
6879 1           has_intercept = FALSE;
6880 1           memmove(p_idx, p_idx + 2, strlen(p_idx + 2) + 1);
6881 1           continue; // re-examine same position
6882             }
6883             // Match +0
6884 70 100         if (p_idx[0] == '+' && p_idx[1] == '0' &&
    50          
6885 0 0         (p_idx[2] == '\0' || p_idx[2] == '+' || p_idx[2] == '-')) {
    0          
    0          
6886 0           has_intercept = FALSE;
6887 0           memmove(p_idx, p_idx + 2, strlen(p_idx + 2) + 1);
6888 0           continue;
6889             }
6890             // Match leading 0+
6891 70 100         if (p_idx == rhs && p_idx[0] == '0' && p_idx[1] == '+') {
    50          
    0          
6892 0           has_intercept = FALSE;
6893 0           memmove(p_idx, p_idx + 2, strlen(p_idx + 2) + 1);
6894 0           continue;
6895             }
6896             // Match bare 0 (entire rhs)
6897 70 100         if (p_idx == rhs && p_idx[0] == '0' && p_idx[1] == '\0') {
    50          
    0          
6898 0           has_intercept = FALSE; p_idx[0] = '\0'; break;
6899             }
6900             // Strip redundant +1 (keep intercept, just remove marker)
6901 70 100         if (p_idx[0] == '+' && p_idx[1] == '1' &&
    50          
6902 0 0         (p_idx[2] == '\0' || p_idx[2] == '+' || p_idx[2] == '-')) {
    0          
    0          
6903 0           memmove(p_idx, p_idx + 2, strlen(p_idx + 2) + 1);
6904 0           continue;
6905             }
6906             // Strip leading bare 1 or 1+
6907 70 100         if (p_idx == rhs) {
6908 18 50         if (p_idx[0] == '1' && p_idx[1] == '\0') { p_idx[0] = '\0'; break; }
    0          
6909 18 50         if (p_idx[0] == '1' && p_idx[1] == '+') { memmove(p_idx, p_idx + 2, strlen(p_idx + 2) + 1); continue; }
    0          
6910             }
6911 70           p_idx++;
6912             }
6913             }
6914             // Clean up stray `++`, leading `+`, trailing `+`
6915             {
6916             char *restrict p_idx;
6917 18 50         while ((p_idx = strstr(rhs, "++")) != NULL)
6918 0           memmove(p_idx, p_idx + 1, strlen(p_idx + 1) + 1);
6919 18 50         if (rhs[0] == '+') memmove(rhs, rhs + 1, strlen(rhs + 1) + 1);
6920 18           size_t len_rhs = strlen(rhs);
6921 18 50         if (len_rhs > 0 && rhs[len_rhs - 1] == '+') rhs[len_rhs - 1] = '\0';
    50          
6922             }
6923              
6924             // Expand `.` Operator
6925 18           char rhs_expanded[2048] = "";
6926 18           size_t rhs_len = 0;
6927 18           chunk = strtok(rhs, "+");
6928 44 100         while (chunk != NULL) {
6929 26 100         if (strcmp(chunk, ".") == 0) {
6930 1           AV *restrict cols = get_all_columns(aTHX_ data_hoa, row_hashes, n);
6931 4 100         for (size_t c = 0; c <= (size_t)av_len(cols); c++) {
6932 3           SV **restrict col_sv = av_fetch(cols, c, 0);
6933 3 50         if (col_sv && SvOK(*col_sv)) {
    50          
6934 3           const char *restrict col_name = SvPV_nolen(*col_sv);
6935 3 100         if (strcmp(col_name, lhs) != 0) {
6936 2           size_t slen = strlen(col_name);
6937 2 50         if (rhs_len + slen + 2 < sizeof(rhs_expanded)) {
6938 2 100         if (rhs_len > 0) { strcat(rhs_expanded, "+"); rhs_len++; }
6939 2           strcat(rhs_expanded, col_name);
6940 2           rhs_len += slen;
6941             }
6942             }
6943             }
6944             }
6945 1           SvREFCNT_dec(cols);
6946             } else {
6947 25           size_t slen = strlen(chunk);
6948 25 50         if (rhs_len + slen + 2 < sizeof(rhs_expanded)) {
6949 25 100         if (rhs_len > 0) { strcat(rhs_expanded, "+"); rhs_len++; }
6950 25           strcat(rhs_expanded, chunk);
6951 25           rhs_len += slen;
6952             }
6953             }
6954 26           chunk = strtok(NULL, "+");
6955             }
6956              
6957 18           Newx(terms, term_cap, char*); Newx(uniq_terms, term_cap, char*);
6958 18           Newx(exp_terms, exp_cap, char*); Newx(is_dummy, exp_cap, bool);
6959 18           Newx(dummy_base, exp_cap, char*); Newx(dummy_level, exp_cap, char*);
6960              
6961 18 100         if (has_intercept) { terms[num_terms++] = savepv("Intercept"); }
6962              
6963 18 50         if (strlen(rhs_expanded) > 0) {
6964 18           chunk = strtok(rhs_expanded, "+");
6965 45 100         while (chunk != NULL) {
6966 27 50         if (num_terms >= term_cap - 3) {
6967 0           term_cap *= 2;
6968 0           Renew(terms, term_cap, char*); Renew(uniq_terms, term_cap, char*);
6969             }
6970 27           char *restrict star = strchr(chunk, '*');
6971 27 100         if (star) {
6972 1           *star = '\0';
6973 1           char *restrict left = chunk;
6974 1           char *restrict right = star + 1;
6975 1           char *restrict c_l = strchr(left, '^');
6976 1 50         if (c_l && strncmp(left, "I(", 2) != 0) *c_l = '\0';
    0          
6977 1           char *restrict c_r = strchr(right, '^');
6978 1 50         if (c_r && strncmp(right, "I(", 2) != 0) *c_r = '\0';
    50          
6979 1           terms[num_terms++] = savepv(left);
6980 1           terms[num_terms++] = savepv(right);
6981 1           size_t inter_len = strlen(left) + strlen(right) + 2;
6982 1           terms[num_terms] = (char*)safemalloc(inter_len);
6983 1           snprintf(terms[num_terms++], inter_len, "%s:%s", left, right);
6984             } else {
6985 26           char *restrict c_chunk = strchr(chunk, '^');
6986 26 50         if (c_chunk && strncmp(chunk, "I(", 2) != 0) *c_chunk = '\0';
    0          
6987 26           terms[num_terms++] = savepv(chunk);
6988             }
6989 27           chunk = strtok(NULL, "+");
6990             }
6991             }
6992              
6993 64 100         for (i = 0; i < num_terms; i++) {
6994 46           bool found = FALSE;
6995 86 50         for (j = 0; j < num_uniq; j++) { if (strcmp(terms[i], uniq_terms[j]) == 0) { found = TRUE; break; } }
    100          
6996 46 50         if (!found) uniq_terms[num_uniq++] = savepv(terms[i]);
6997             }
6998 18           p = num_uniq;
6999             /* PHASE 3: Categorical Expansion*/
7000 64 100         for (j = 0; j < p; j++) {
7001 46 50         if (p_exp + 32 >= exp_cap) {
7002 0           exp_cap *= 2;
7003 0           Renew(exp_terms, exp_cap, char*); Renew(is_dummy, exp_cap, bool);
7004 0           Renew(dummy_base, exp_cap, char*); Renew(dummy_level, exp_cap, char*);
7005             }
7006 46 100         if (strcmp(uniq_terms[j], "Intercept") == 0) {
7007 17           exp_terms[p_exp] = savepv("Intercept"); is_dummy[p_exp] = FALSE; p_exp++; continue;
7008             }
7009 29 100         if (is_column_categorical(aTHX_ data_hoa, row_hashes, n, uniq_terms[j])) {
7010 5           char **restrict levels = NULL;
7011 5           unsigned int num_levels = 0, levels_cap = 8;
7012 5           Newx(levels, levels_cap, char*);
7013 47 100         for (i = 0; i < n; i++) {
7014 42           char *restrict str_val = get_data_string_alloc(aTHX_ data_hoa, row_hashes, i, uniq_terms[j]);
7015 42 50         if (str_val) {
7016 42           bool found = FALSE;
7017 81 100         for (l = 0; l < num_levels; l++) { if (strcmp(levels[l], str_val) == 0) { found = TRUE; break; } }
    100          
7018 42 100         if (!found) {
7019 14 50         if (num_levels >= levels_cap) { levels_cap *= 2; Renew(levels, levels_cap, char*); }
7020 14           levels[num_levels++] = savepv(str_val);
7021             }
7022 42           Safefree(str_val);
7023             }
7024             }
7025 5 50         if (num_levels > 0) {
7026 14 100         for (l1 = 0; l1 < num_levels - 1; l1++)
7027 22 100         for (l2 = l1 + 1; l2 < num_levels; l2++)
7028 13 100         if (strcmp(levels[l1], levels[l2]) > 0) { char *tmp = levels[l1]; levels[l1] = levels[l2]; levels[l2] = tmp; }
7029 14 100         for (l = 1; l < num_levels; l++) {
7030 9 50         if (p_exp >= exp_cap) {
7031 0           exp_cap *= 2;
7032 0           Renew(exp_terms, exp_cap, char*); Renew(is_dummy, exp_cap, bool);
7033 0           Renew(dummy_base, exp_cap, char*); Renew(dummy_level, exp_cap, char*);
7034             }
7035 9           size_t t_len = strlen(uniq_terms[j]) + strlen(levels[l]) + 1;
7036 9           exp_terms[p_exp] = (char*)safemalloc(t_len);
7037 9           snprintf(exp_terms[p_exp], t_len, "%s%s", uniq_terms[j], levels[l]);
7038 9           is_dummy[p_exp] = TRUE;
7039 9           dummy_base[p_exp] = savepv(uniq_terms[j]);
7040 9           dummy_level[p_exp] = savepv(levels[l]);
7041 9           p_exp++;
7042             }
7043 19 100         for (l = 0; l < num_levels; l++) Safefree(levels[l]);
7044 5           Safefree(levels);
7045             } else {
7046 0           Safefree(levels);
7047 0           exp_terms[p_exp] = savepv(uniq_terms[j]); is_dummy[p_exp] = FALSE; p_exp++;
7048             }
7049             } else {
7050 24           exp_terms[p_exp] = savepv(uniq_terms[j]); is_dummy[p_exp] = FALSE; p_exp++;
7051             }
7052             }
7053 18           p = p_exp;
7054 18 50         Newx(X, n * p, NV); Newx(Y, n, NV);
    50          
7055 18 50         Newx(valid_row_names, n, char*);
7056             //
7057             // PHASE 4: Matrix Construction & Listwise Deletion
7058             //
7059 310 100         for (i = 0; i < n; i++) {
7060 292           NV y_val = evaluate_term(aTHX_ data_hoa, row_hashes, i, lhs);
7061 292 100         if (isnan(y_val)) { Safefree(row_names[i]); continue; }
7062              
7063 289           bool row_ok = TRUE;
7064 289           NV *restrict row_x = (NV*)safemalloc(p * sizeof(NV));
7065 1112 100         for (j = 0; j < p; j++) {
7066 823 100         if (strcmp(exp_terms[j], "Intercept") == 0) {
7067 257           row_x[j] = 1.0;
7068 566 100         } else if (is_dummy[j]) {
7069 78           char *restrict str_val = get_data_string_alloc(aTHX_ data_hoa, row_hashes, i, dummy_base[j]);
7070 78 50         if (str_val) {
7071 78 100         row_x[j] = (strcmp(str_val, dummy_level[j]) == 0) ? 1.0 : 0.0;
7072 78           Safefree(str_val);
7073 0           } else { row_ok = FALSE; break; }
7074             } else {
7075 488           row_x[j] = evaluate_term(aTHX_ data_hoa, row_hashes, i, exp_terms[j]);
7076 488 50         if (isnan(row_x[j])) { row_ok = FALSE; break; }
7077             }
7078             }
7079 289 50         if (!row_ok) { Safefree(row_names[i]); Safefree(row_x); continue; }
7080 289           Y[valid_n] = y_val;
7081 1112 100         for (j = 0; j < p; j++) X[valid_n * p + j] = row_x[j];
7082 289           valid_row_names[valid_n] = row_names[i];
7083 289           valid_n++;
7084 289           Safefree(row_x);
7085             }
7086 18           Safefree(row_names);
7087 18 100         if (valid_n <= p) {
7088 7 100         for (i = 0; i < num_terms; i++) Safefree(terms[i]); Safefree(terms);
7089 7 100         for (i = 0; i < num_uniq; i++) Safefree(uniq_terms[i]); Safefree(uniq_terms);
7090 7 100         for (j = 0; j < p_exp; j++) {
7091 5           Safefree(exp_terms[j]);
7092 5 50         if (is_dummy[j]) { Safefree(dummy_base[j]); Safefree(dummy_level[j]); }
7093             }
7094 2           Safefree(exp_terms); Safefree(is_dummy); Safefree(dummy_base); Safefree(dummy_level);
7095 2           Safefree(X); Safefree(Y); Safefree(valid_row_names);
7096 2 50         if (row_hashes) Safefree(row_hashes);
7097 2           croak("lm: 0 degrees of freedom (too many NAs or parameters > observations)");
7098             }
7099             // PHASE 5: OLS Math
7100 16           Newxz(XtX, p * p, NV);
7101 61 100         for (i = 0; i < p; i++)
7102 178 100         for (j = 0; j < p; j++) {
7103 133           NV sum = 0.0;
7104 2620 100         for (k = 0; k < valid_n; k++) sum += X[k * p + i] * X[k * p + j];
7105 133           XtX[i * p + j] = sum;
7106             }
7107 16           Newxz(XtY, p, NV);
7108 61 100         for (i = 0; i < p; i++) {
7109 45           NV sum = 0.0;
7110 860 100         for (k = 0; k < valid_n; k++) sum += X[k * p + i] * Y[k];
7111 45           XtY[i] = sum;
7112             }
7113 16           Newx(aliased, p, bool);
7114 16           final_rank = sweep_matrix_ols(XtX, p, aliased);
7115 16           Newxz(beta, p, NV);
7116 61 100         for (i = 0; i < p; i++) {
7117 45 100         if (aliased[i]) { beta[i] = NAN; }
7118             else {
7119 44           NV sum = 0.0;
7120 174 100         for (j = 0; j < p; j++) if (!aliased[j]) sum += XtX[i * p + j] * XtY[j];
    100          
7121 44           beta[i] = sum;
7122             }
7123             }
7124             // PHASE 6: Metrics & Cleanup
7125 16           res_hv = newHV(); coef_hv = newHV(); fitted_hv = newHV(); resid_hv = newHV();
7126 16           summary_hv = newHV(); terms_av = newAV();
7127 16           df_res = (int)valid_n - final_rank;
7128             // rss / mss accumulated here — rse_sq computed AFTER this loop (not before)
7129 16           NV sum_y = 0.0, mss = 0.0;
7130 302 100         for (i = 0; i < valid_n; i++) sum_y += Y[i];
7131 16           NV mean_y = sum_y / (NV)valid_n;
7132 302 100         for (i = 0; i < valid_n; i++) {
7133 286           NV y_hat = 0.0;
7134 1101 100         for (j = 0; j < p; j++) if (!aliased[j]) y_hat += X[i * p + j] * beta[j];
    100          
7135 286           NV res = Y[i] - y_hat;
7136 286           rss += res * res;
7137 286 100         NV diff_m = has_intercept ? (y_hat - mean_y) : y_hat;
7138 286           mss += diff_m * diff_m;
7139 286           hv_store(fitted_hv, valid_row_names[i], strlen(valid_row_names[i]), newSVnv(y_hat), 0);
7140 286           hv_store(resid_hv, valid_row_names[i], strlen(valid_row_names[i]), newSVnv(res), 0);
7141 286           Safefree(valid_row_names[i]);
7142             }
7143 16           Safefree(valid_row_names);
7144             // Single, authoritative rse_sq calculation
7145 16 50         rse_sq = (df_res > 0) ? (rss / (NV)df_res) : NAN;
7146              
7147 16           int df_int = has_intercept ? 1 : 0;
7148 16           NV r_squared = 0.0, adj_r_squared = 0.0, f_stat = NAN, f_pvalue = NAN;
7149 16           int numdf = final_rank - df_int;
7150              
7151 16 50         if (final_rank != df_int && (mss + rss) > 0.0) {
    50          
7152 16           r_squared = mss / (mss + rss);
7153 16           adj_r_squared = 1.0 - (1.0 - r_squared) * ((valid_n - df_int) / (NV)df_res);
7154 16 50         if (rse_sq > 0.0 && numdf > 0) {
    50          
7155 16           f_stat = (mss / (NV)numdf) / rse_sq;
7156 16           f_pvalue = 1.0 - pf(f_stat, (NV)numdf, (NV)df_res);
7157 0 0         } else if (rse_sq == 0.0) {
7158 0           f_stat = INFINITY;
7159 0           f_pvalue = 0.0;
7160             }
7161 0 0         } else if (final_rank == df_int) {
7162 0           r_squared = 0.0; adj_r_squared = 0.0;
7163             }
7164 61 100         for (j = 0; j < p; j++) {
7165 45           hv_store(coef_hv, exp_terms[j], strlen(exp_terms[j]), newSVnv(beta[j]), 0);
7166 45           av_push(terms_av, newSVpv(exp_terms[j], 0));
7167 45           HV *restrict row_hv = newHV();
7168 45 100         if (aliased[j]) {
7169 1           hv_store(row_hv, "Estimate", 8, newSVpv("NaN", 0), 0);
7170 1           hv_store(row_hv, "Std. Error", 10, newSVpv("NaN", 0), 0);
7171 1           hv_store(row_hv, "t value", 7, newSVpv("NaN", 0), 0);
7172 1           hv_store(row_hv, "Pr(>|t|)", 8, newSVpv("NaN", 0), 0);
7173             } else {
7174 44           NV se = sqrt(rse_sq * XtX[j * p + j]);
7175 44 50         NV t_val = (se > 0.0) ? (beta[j] / se) : (INFINITY * (beta[j] >= 0.0 ? 1.0 : -1.0));
    0          
7176 44           NV p_val = get_t_pvalue(t_val, df_res, "two.sided");
7177 44           hv_store(row_hv, "Estimate", 8, newSVnv(beta[j]), 0);
7178 44           hv_store(row_hv, "Std. Error", 10, newSVnv(se), 0);
7179 44           hv_store(row_hv, "t value", 7, newSVnv(t_val), 0);
7180 44           hv_store(row_hv, "Pr(>|t|)", 8, newSVnv(p_val), 0);
7181             }
7182 45           hv_store(summary_hv, exp_terms[j], strlen(exp_terms[j]), newRV_noinc((SV*)row_hv), 0);
7183             }
7184 16           hv_store(res_hv, "coefficients", 12, newRV_noinc((SV*)coef_hv), 0);
7185 16           hv_store(res_hv, "fitted.values", 13, newRV_noinc((SV*)fitted_hv), 0);
7186 16           hv_store(res_hv, "residuals", 9, newRV_noinc((SV*)resid_hv), 0);
7187 16           hv_store(res_hv, "df.residual", 11, newSVuv(df_res), 0);
7188 16           hv_store(res_hv, "rank", 4, newSVuv(final_rank), 0);
7189 16           hv_store(res_hv, "rss", 3, newSVnv(rss), 0);
7190 16           hv_store(res_hv, "summary", 7, newRV_noinc((SV*)summary_hv),0);
7191 16           hv_store(res_hv, "terms", 5, newRV_noinc((SV*)terms_av), 0);
7192 16           hv_store(res_hv, "r.squared", 9, newSVnv(r_squared), 0);
7193 16           hv_store(res_hv, "adj.r.squared", 13, newSVnv(adj_r_squared), 0);
7194 16 50         if (!isnan(f_stat)) {
7195 16           AV *fstat_av = newAV();
7196 16           av_push(fstat_av, newSVnv(f_stat));
7197 16           av_push(fstat_av, newSViv(numdf));
7198 16           av_push(fstat_av, newSViv(df_res));
7199 16           hv_store(res_hv, "fstatistic", 10, newRV_noinc((SV*)fstat_av), 0);
7200 16           hv_store(res_hv, "f.pvalue", 8, newSVnv(f_pvalue), 0);
7201             }
7202             // Deep Cleanup
7203 57 100         for (i = 0; i < num_terms; i++) Safefree(terms[i]); Safefree(terms);
7204 57 100         for (i = 0; i < num_uniq; i++) Safefree(uniq_terms[i]); Safefree(uniq_terms);
7205 61 100         for (j = 0; j < p_exp; j++) {
7206 45           Safefree(exp_terms[j]);
7207 45 100         if (is_dummy[j]) { Safefree(dummy_base[j]); Safefree(dummy_level[j]); }
7208             }
7209 16           Safefree(exp_terms); Safefree(is_dummy); Safefree(dummy_base); Safefree(dummy_level);
7210 16           Safefree(X); Safefree(Y); Safefree(XtX); Safefree(XtY);
7211 16           Safefree(beta); Safefree(aliased);
7212 16 100         if (row_hashes) Safefree(row_hashes);
7213              
7214 16           RETVAL = newRV_noinc((SV*)res_hv);
7215             }
7216             OUTPUT:
7217             RETVAL
7218              
7219             void seq(from, to, by = 1.0)
7220             NV from
7221             NV to
7222             NV by
7223             PPCODE:
7224             {
7225             //Handle the zero 'by' case
7226 6 50         if (by == 0.0) {
7227 0 0         if (from == to) {
7228 0 0         EXTEND(SP, 1);
7229 0           mPUSHn(from);
7230 0           XSRETURN(1);
7231             } else {
7232 0           croak("invalid 'by' argument: cannot be zero when from != to");
7233             }
7234             }
7235             // Check for wrong direction / infinite loop
7236 6 100         if ((from < to && by < 0.0) || (from > to && by > 0.0)) {
    50          
    100          
    50          
7237 0           croak("wrong sign in 'by' argument");
7238             }
7239             /* * Calculate number of elements.
7240             * R uses a small epsilon (like 1e-10) to avoid dropping the last
7241             * element due to floating point inaccuracies.
7242             */
7243 6           NV n_elements_d = (to - from) / by;
7244 6 50         if (n_elements_d < 0.0) n_elements_d = 0.0;
7245 6           size_t n_elements = (n_elements_d + 1e-10) + 1;
7246             // Pre-extend the stack to avoid reallocating inside the loop
7247 6 50         EXTEND(SP, n_elements);
7248 3033 100         for (size_t i = 0; i < n_elements; i++) {
7249 3027           mPUSHn(from + i * by);
7250             }
7251 6           XSRETURN(n_elements);
7252             }
7253              
7254             SV* rnorm(...)
7255             CODE:
7256             {
7257             // Auto-seed the PRNG if the Perl script hasn't done so yet
7258 2 100         AUTO_SEED_PRNG();
7259 2           size_t n = 0;
7260 2           NV mean = 0.0, sd = 1.0;
7261 2           int arg_start = 0;
7262             // Check if the first argument is a simple integer (rnorm(33))
7263 2 50         if (items > 0 && SvIOK(ST(0)) && (items == 1 || items % 2 != 0)) {
    50          
    0          
    0          
7264 0           n = (unsigned int)SvUV(ST(0));
7265 0           arg_start = 1; // Start parsing named arguments from the second element
7266             }
7267              
7268             // --- Parse remaining named arguments from the flat stack ---
7269 2 50         if ((items - arg_start) % 2 != 0) {
7270 0           croak("Usage: rnorm(n), rnorm(n => 10, mean => 0, sd => 1), or rnorm(33, mean => 0)");
7271             }
7272              
7273 7 100         for (int i = arg_start; i < items; i += 2) {
7274 5           const char* restrict key = SvPV_nolen(ST(i));
7275 5           SV* restrict val = ST(i + 1);
7276              
7277 5 100         if (strEQ(key, "n")) n = (unsigned int)SvUV(val);
7278 3 100         else if (strEQ(key, "mean")) mean = SvNV(val);
7279 2 50         else if (strEQ(key, "sd")) sd = SvNV(val);
7280 0           else croak("rnorm: unknown argument '%s'", key);
7281             }
7282 2 100         if (sd < 0.0) croak("rnorm: standard deviation must be non-negative");
7283 1           AV *restrict result_av = newAV();
7284 1 50         if (n > 0) {
7285 1           av_extend(result_av, n - 1);
7286             // Generate random normals using the Box-Muller transform
7287 5002 100         for (size_t i = 0; i < n; ) {
7288             NV u, v, s;
7289             do {
7290             // Drand01() hooks into Perl's internal PRNG, respecting Perl's srand()
7291 6385           u = 2.0 * Drand01() - 1.0;
7292 6385           v = 2.0 * Drand01() - 1.0;
7293 6385           s = u * u + v * v;
7294 6385 100         } while (s >= 1.0 || s == 0.0);
    50          
7295 5000           NV mul = sqrt(-2.0 * log(s) / s);
7296             // Box-Muller generates two independent values per iteration
7297 5000           av_store(result_av, i++, newSVnv(mean + sd * u * mul));
7298 5000 100         if (i < n) {
7299 4999           av_store(result_av, i++, newSVnv(mean + sd * v * mul));
7300             }
7301             }
7302             }
7303 1           RETVAL = newRV_noinc((SV*)result_av);
7304             }
7305             OUTPUT:
7306             RETVAL
7307              
7308             SV* aov(data_sv, formula_sv = &PL_sv_undef)
7309             SV* data_sv
7310             SV* formula_sv
7311             CODE:
7312             {
7313             const char *restrict formula;
7314 10           SV *restrict orig_data_sv = data_sv;
7315 10           bool is_stacked = FALSE;
7316             //
7317             // PHASE 0: R-style stack() for missing formula
7318             //
7319 10 50         if (!formula_sv || !SvOK(formula_sv) || SvCUR(formula_sv) == 0) {
    100          
    50          
7320 1 50         if (!SvROK(data_sv) || SvTYPE(SvRV(data_sv)) != SVt_PVHV) {
    50          
7321 0           croak("aov: Without a formula, data must be a HashRef of ArrayRefs (mimicking R's named list)");
7322             }
7323              
7324 1           is_stacked = TRUE;
7325 1           HV *restrict input_hv = (HV*)SvRV(data_sv);
7326 1           HV *restrict stacked_hv = newHV();
7327 1           AV *restrict val_av = newAV();
7328 1           AV *restrict grp_av = newAV();
7329 1           hv_iterinit(input_hv);
7330             HE *restrict entry;
7331 3 100         while ((entry = hv_iternext(input_hv))) {
7332 2           SV *restrict grp_name_sv = hv_iterkeysv(entry);
7333 2           SV *restrict arr_ref = hv_iterval(input_hv, entry);
7334 4 50         if (SvROK(arr_ref) && SvTYPE(SvRV(arr_ref)) == SVt_PVAV) {
    50          
7335 2           AV *restrict arr = (AV*)SvRV(arr_ref);
7336 2           size_t len = av_len(arr);
7337 14 100         for (size_t k = 0; k <= len; k++) {
7338 12           SV **restrict v = av_fetch(arr, k, 0);
7339 12 50         if (v && *v && SvOK(*v)) {
    50          
    50          
7340 12           av_push(val_av, newSVsv(*v));
7341 12           av_push(grp_av, newSVsv(grp_name_sv));
7342             }
7343             }
7344             } else {
7345 0           SvREFCNT_dec(val_av); SvREFCNT_dec(grp_av); SvREFCNT_dec(stacked_hv);
7346 0           croak("aov: Hash values must be ArrayRefs when no formula is provided");
7347             }
7348             }
7349 1           hv_stores(stacked_hv, "Value", newRV_noinc((SV*)val_av));
7350 1           hv_stores(stacked_hv, "Group", newRV_noinc((SV*)grp_av));
7351             // sv_2mortal ensures memory is freed automatically on return or croak
7352 1           data_sv = sv_2mortal(newRV_noinc((SV*)stacked_hv));
7353 1           formula = "Value~Group";
7354             } else {
7355 9           formula = SvPV_nolen(formula_sv);
7356             }
7357             char f_cpy[512];
7358             char *restrict src, *restrict dst, *restrict tilde, *restrict lhs, *restrict rhs, *restrict chunk;
7359 10           char **restrict terms = NULL, **restrict uniq_terms = NULL, **restrict exp_terms = NULL, **restrict parent_term = NULL;
7360 10           bool *restrict is_dummy = NULL, *is_interact = NULL;
7361 10           char **restrict dummy_base = NULL, **restrict dummy_level = NULL;
7362 10           int *restrict term_map = NULL, *restrict left_idx = NULL, *restrict right_idx = NULL;
7363 10           unsigned int term_cap = 64, exp_cap = 64, num_terms = 0, num_uniq = 0, p = 0, p_exp = 0;
7364 10           size_t n = 0, valid_n = 0, i, j;
7365 10           bool has_intercept = TRUE;
7366 10           char **restrict row_names = NULL;
7367 10           HV **restrict row_hashes = NULL;
7368 10           HV *restrict data_hoa = NULL;
7369 10           SV *restrict ref = NULL;
7370             HE *restrict entry;
7371 10           NV **restrict X_mat = NULL;
7372 10           NV *restrict Y = NULL;
7373 10           char **restrict term_base_level = NULL; /* reference level for each uniq_term (NULL if not categorical) */
7374 10 50         if (!SvROK(data_sv)) croak("aov: data is required and must be a reference");
7375             //
7376             // PHASE 1: Data Extraction
7377             //
7378 10           ref = SvRV(data_sv);
7379 10 50         if (SvTYPE(ref) == SVt_PVHV) {
7380 10           HV*restrict hv = (HV*)ref;
7381 10 50         if (hv_iterinit(hv) == 0) croak("aov: Data hash is empty");
7382 10           entry = hv_iternext(hv);
7383 10 50         if (entry) {
7384 10           SV*restrict val = hv_iterval(hv, entry);
7385 10 50         if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVAV) {
    50          
7386 10           data_hoa = hv;
7387 10           n = av_len((AV*)SvRV(val)) + 1;
7388 10 50         Newx(row_names, n, char*);
7389 80 100         for(i = 0; i < n; i++) {
7390 70           char buf[32]; snprintf(buf, sizeof(buf), "%lu", (unsigned long)(i+1));
7391 70           row_names[i] = savepv(buf);
7392             }
7393 0 0         } else if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVHV) {
    0          
7394 0           n = hv_iterinit(hv);
7395 0 0         Newx(row_names, n, char*); Newx(row_hashes, n, HV*);
    0          
7396 0           i = 0;
7397 0 0         while ((entry = hv_iternext(hv))) {
7398             I32 len;
7399 0           row_names[i] = savepv(hv_iterkey(entry, &len));
7400 0           row_hashes[i] = (HV*)SvRV(hv_iterval(hv, entry));
7401 0           i++;
7402             }
7403 0           } else croak("aov: Hash values must be ArrayRefs (HoA) or HashRefs (HoH)");
7404             }
7405 0 0         } else if (SvTYPE(ref) == SVt_PVAV) {
7406 0           AV*restrict av = (AV*)ref;
7407 0           n = av_len(av) + 1;
7408 0 0         Newx(row_names, n, char*);
7409 0 0         Newx(row_hashes, n, HV*);
7410 0 0         for (i = 0; i < n; i++) {
7411 0           SV**restrict val = av_fetch(av, i, 0);
7412 0 0         if (val && SvROK(*val) && SvTYPE(SvRV(*val)) == SVt_PVHV) {
    0          
    0          
7413 0           row_hashes[i] = (HV*)SvRV(*val);
7414             char buf[32];
7415 0           snprintf(buf, sizeof(buf), "%lu", (unsigned long)(i + 1));
7416 0           row_names[i] = savepv(buf);
7417             } else {
7418 0 0         for (size_t k = 0; k < i; k++) Safefree(row_names[k]);
7419 0           Safefree(row_names); Safefree(row_hashes);
7420 0           croak("aov: Array values must be HashRefs (AoH)");
7421             }
7422             }
7423 0           } else croak("aov: Data must be an Array or Hash reference");
7424             //
7425             // PHASE 2: Formula Parsing & `.` Expansion
7426             //
7427 10           src = (char*)formula; dst = f_cpy;
7428 123 100         while (*src && (dst - f_cpy < 511)) { if (!isspace(*src)) { *dst++ = *src; } src++; }
    100          
    50          
7429 10           *dst = '\0';
7430 10           tilde = strchr(f_cpy, '~');
7431 10 100         if (!tilde) {
7432 3 100         for (i = 0; i < n; i++) Safefree(row_names[i]);
7433 1 50         Safefree(row_names); if (row_hashes) Safefree(row_hashes);
7434 1           croak("aov: invalid formula, missing '~'");
7435             }
7436 9           *tilde = '\0';
7437 9           lhs = f_cpy;
7438 9           rhs = tilde + 1;
7439             char *restrict p_idx;
7440 9 50         while ((p_idx = strstr(rhs, "-1")) != NULL) { has_intercept = FALSE; memmove(p_idx, p_idx + 2, strlen(p_idx + 2) + 1); }
7441 9 50         while ((p_idx = strstr(rhs, "+0")) != NULL) { has_intercept = FALSE; memmove(p_idx, p_idx + 2, strlen(p_idx + 2) + 1); }
7442 9 50         while ((p_idx = strstr(rhs, "0+")) != NULL) { has_intercept = FALSE; memmove(p_idx, p_idx + 2, strlen(p_idx + 2) + 1); }
7443 9 50         if (rhs[0] == '0' && rhs[1] == '\0') { has_intercept = FALSE; rhs[0] = '\0'; }
    0          
7444 9 50         while ((p_idx = strstr(rhs, "+1")) != NULL) { memmove(p_idx, p_idx + 2, strlen(p_idx + 2) + 1); }
7445 9 50         if (rhs[0] == '1' && rhs[1] == '\0') { rhs[0] = '\0'; }
    0          
7446 9 50         else if (rhs[0] == '1' && rhs[1] == '+') { memmove(rhs, rhs + 2, strlen(rhs + 2) + 1); }
    0          
7447              
7448 9 50         while ((p_idx = strstr(rhs, "++")) != NULL) memmove(p_idx, p_idx + 1, strlen(p_idx + 1) + 1);
7449 9 50         if (rhs[0] == '+') memmove(rhs, rhs + 1, strlen(rhs + 1) + 1);
7450 9           size_t len_rhs = strlen(rhs);
7451 9 50         if (len_rhs > 0 && rhs[len_rhs - 1] == '+') rhs[len_rhs - 1] = '\0';
    50          
7452 9           char rhs_expanded[2048] = "";
7453 9           size_t rhs_len = 0;
7454 9           chunk = strtok(rhs, "+");
7455 21 100         while (chunk != NULL) {
7456 12 100         if (strcmp(chunk, ".") == 0) {
7457 1           AV *restrict cols = get_all_columns(aTHX_ data_hoa, row_hashes, n);
7458 4 100         for (size_t c = 0; c <= av_len(cols); c++) {
7459 3           SV **restrict col_sv = av_fetch(cols, c, 0);
7460 3 50         if (col_sv && SvOK(*col_sv)) {
    50          
7461 3           const char *restrict col_name = SvPV_nolen(*col_sv);
7462 3 100         if (strcmp(col_name, lhs) != 0) {
7463 2           size_t slen = strlen(col_name);
7464 2 50         if (rhs_len + slen + 2 < sizeof(rhs_expanded)) {
7465 2 100         if (rhs_len > 0) { strcat(rhs_expanded, "+"); rhs_len++; }
7466 2           strcat(rhs_expanded, col_name);
7467 2           rhs_len += slen;
7468             }
7469             }
7470             }
7471             }
7472 1           SvREFCNT_dec(cols);
7473             } else {
7474 11           size_t slen = strlen(chunk);
7475 11 50         if (rhs_len + slen + 2 < sizeof(rhs_expanded)) {
7476 11 100         if (rhs_len > 0) { strcat(rhs_expanded, "+"); rhs_len++; }
7477 11           strcat(rhs_expanded, chunk);
7478 11           rhs_len += slen;
7479             }
7480             }
7481 12           chunk = strtok(NULL, "+");
7482             }
7483             // Setup arrays safely
7484 9           Newx(terms, term_cap, char*);
7485 9           Newx(uniq_terms, term_cap, char*);
7486 9           Newx(exp_terms, exp_cap, char*); Newx(parent_term, exp_cap, char*);
7487 9           Newx(is_dummy, exp_cap, bool); Newx(is_interact, exp_cap, bool);
7488 9           Newx(dummy_base, exp_cap, char*); Newx(dummy_level, exp_cap, char*);
7489 9           Newx(term_map, exp_cap, int); Newx(left_idx, exp_cap, int); Newx(right_idx, exp_cap, int);
7490 9 50         if (has_intercept) { terms[num_terms++] = savepv("Intercept"); }
7491 9 50         if (strlen(rhs_expanded) > 0) {
7492 9           chunk = strtok(rhs_expanded, "+");
7493 22 100         while (chunk != NULL) {
7494 13 50         if (num_terms >= term_cap - 3) {
7495 0           term_cap *= 2;
7496 0           Renew(terms, term_cap, char*); Renew(uniq_terms, term_cap, char*);
7497             }
7498 13           char *restrict star = strchr(chunk, '*');
7499 13 100         if (star) {
7500 1           *star = '\0';
7501 1           char *restrict left = chunk;
7502 1           char *restrict right = star + 1;
7503 1           char *restrict c_l = strchr(left, '^');
7504 1 50         if (c_l && strncmp(left, "I(", 2) != 0) *c_l = '\0';
    0          
7505 1 50         char *restrict c_r = strchr(right, '^'); if (c_r && strncmp(right, "I(", 2) != 0) *c_r = '\0';
    0          
7506 1           terms[num_terms++] = savepv(left);
7507 1           terms[num_terms++] = savepv(right);
7508 1           size_t inter_len = strlen(left) + strlen(right) + 2;
7509 1           terms[num_terms] = (char*)safemalloc(inter_len);
7510 1           snprintf(terms[num_terms++], inter_len, "%s:%s", left, right);
7511             } else {
7512 12           char *restrict c_chunk = strchr(chunk, '^');
7513 12 50         if (c_chunk && strncmp(chunk, "I(", 2) != 0) *c_chunk = '\0';
    0          
7514 12           terms[num_terms++] = savepv(chunk);
7515             }
7516 13           chunk = strtok(NULL, "+");
7517             }
7518             }
7519              
7520 33 100         for (i = 0; i < num_terms; i++) {
7521 24           bool found = FALSE;
7522 46 100         for (size_t k = 0; k < num_uniq; k++) {
7523 22 50         if (strcmp(terms[i], uniq_terms[k]) == 0) { found = TRUE; break; }
7524             }
7525 24 50         if (!found) uniq_terms[num_uniq++] = savepv(terms[i]);
7526             }
7527 9           p = num_uniq;
7528              
7529 9           Newxz(term_base_level, num_uniq, char*);
7530              
7531             /* PHASE 3: Categorical & Interaction Expansion */
7532 32 100         for (j = 0; j < p; j++) {
7533 24 100         if (p_exp + 64 >= exp_cap) {
7534 9           exp_cap *= 2;
7535 9           Renew(exp_terms, exp_cap, char*); Renew(parent_term, exp_cap, char*);
7536 9           Renew(is_dummy, exp_cap, bool); Renew(is_interact, exp_cap, bool);
7537 9           Renew(dummy_base, exp_cap, char*); Renew(dummy_level, exp_cap, char*);
7538 9           Renew(term_map, exp_cap, int); Renew(left_idx, exp_cap, int); Renew(right_idx, exp_cap, int);
7539             }
7540              
7541 24 100         if (strcmp(uniq_terms[j], "Intercept") == 0) {
7542 9           exp_terms[p_exp] = savepv("Intercept");
7543 9           parent_term[p_exp] = savepv("Intercept");
7544 9           is_dummy[p_exp] = FALSE; is_interact[p_exp] = FALSE;
7545 9           term_map[p_exp] = j;
7546 9           p_exp++;
7547 9           continue;
7548             }
7549              
7550 15           char *restrict colon = strchr(uniq_terms[j], ':');
7551 15 100         if (colon) {
7552             char left[256], right[256];
7553 2           strncpy(left, uniq_terms[j], colon - uniq_terms[j]);
7554 2           left[colon - uniq_terms[j]] = '\0';
7555 2           strcpy(right, colon + 1);
7556              
7557 2           int *restrict l_indices = (int*)safemalloc(p_exp * sizeof(int)); int l_count = 0;
7558 2           int *restrict r_indices = (int*)safemalloc(p_exp * sizeof(int)); int r_count = 0;
7559 6 100         for (size_t e = 0; e < p_exp; e++) {
7560 4 100         if (strcmp(parent_term[e], left) == 0) l_indices[l_count++] = e;
7561 4 100         if (strcmp(parent_term[e], right) == 0) r_indices[r_count++] = e;
7562             }
7563              
7564 2 100         if (l_count == 0 || r_count == 0) {
    50          
7565 1           Safefree(l_indices); Safefree(r_indices);
7566 1           croak("aov: Interaction term '%s' requires its main effects to be explicitly included in the formula", uniq_terms[j]);
7567             } else {
7568 2 100         for (unsigned int li = 0; li < l_count; li++) {
7569 2 100         for (unsigned int ri = 0; ri < r_count; ri++) {
7570 1 50         if (p_exp >= exp_cap) {
7571 0           exp_cap *= 2;
7572 0           Renew(exp_terms, exp_cap, char*); Renew(parent_term, exp_cap, char*);
7573 0           Renew(is_dummy, exp_cap, bool); Renew(is_interact, exp_cap, bool);
7574 0           Renew(dummy_base, exp_cap, char*); Renew(dummy_level, exp_cap, char*);
7575 0           Renew(term_map, exp_cap, int); Renew(left_idx, exp_cap, int); Renew(right_idx, exp_cap, int);
7576             }
7577 1           size_t t_len = strlen(exp_terms[l_indices[li]]) + strlen(exp_terms[r_indices[ri]]) + 2;
7578 1           exp_terms[p_exp] = (char*)safemalloc(t_len);
7579 1           snprintf(exp_terms[p_exp], t_len, "%s:%s", exp_terms[l_indices[li]], exp_terms[r_indices[ri]]);
7580 1           parent_term[p_exp] = savepv(uniq_terms[j]);
7581 1           is_dummy[p_exp] = FALSE; is_interact[p_exp] = TRUE;
7582 1           left_idx[p_exp] = l_indices[li];
7583 1           right_idx[p_exp] = r_indices[ri];
7584 1           term_map[p_exp] = j;
7585 1           p_exp++;
7586             }
7587             }
7588             }
7589 1           Safefree(l_indices); Safefree(r_indices);
7590             } else {
7591 13 100         if (is_column_categorical(aTHX_ data_hoa, row_hashes, n, uniq_terms[j])) {
7592 4           char **restrict levels = NULL;
7593 4           unsigned int num_levels = 0, levels_cap = 8;
7594 4           Newx(levels, levels_cap, char*);
7595 65 100         for (i = 0; i < n; i++) {
7596 61           char*restrict str_val = get_data_string_alloc(aTHX_ data_hoa, row_hashes, i, uniq_terms[j]);
7597 61 50         if (str_val) {
7598 61           bool found = FALSE;
7599 96 100         for (size_t l = 0; l < num_levels; l++) {
7600 87 100         if (strcmp(levels[l], str_val) == 0) { found = TRUE; break; }
7601             }
7602 61 100         if (!found) {
7603 9 50         if (num_levels >= levels_cap) { levels_cap *= 2; Renew(levels, levels_cap, char*); }
7604 9           levels[num_levels++] = savepv(str_val);
7605             }
7606 61           Safefree(str_val);
7607             }
7608             }
7609 4 50         if (num_levels > 0) {
7610 9 100         for (size_t l1 = 0; l1 < num_levels - 1; l1++) {
7611 11 100         for (size_t l2 = l1 + 1; l2 < num_levels; l2++) {
7612 6 100         if (strcmp(levels[l1], levels[l2]) > 0) {
7613 1           char *tmp = levels[l1]; levels[l1] = levels[l2]; levels[l2] = tmp;
7614             }
7615             }
7616             }
7617              
7618 4           term_base_level[j] = savepv(levels[0]);
7619              
7620 9 100         for (size_t l = 1; l < num_levels; l++) {
7621 5 50         if (p_exp >= exp_cap) {
7622 0           exp_cap *= 2;
7623 0           Renew(exp_terms, exp_cap, char*); Renew(parent_term, exp_cap, char*);
7624 0           Renew(is_dummy, exp_cap, bool); Renew(is_interact, exp_cap, bool);
7625 0           Renew(dummy_base, exp_cap, char*); Renew(dummy_level, exp_cap, char*);
7626 0           Renew(term_map, exp_cap, int); Renew(left_idx, exp_cap, int); Renew(right_idx, exp_cap, int);
7627             }
7628 5           size_t t_len = strlen(uniq_terms[j]) + strlen(levels[l]) + 1;
7629 5           exp_terms[p_exp] = (char*)safemalloc(t_len);
7630 5           snprintf(exp_terms[p_exp], t_len, "%s%s", uniq_terms[j], levels[l]);
7631 5           parent_term[p_exp] = savepv(uniq_terms[j]);
7632 5           is_dummy[p_exp] = TRUE; is_interact[p_exp] = FALSE;
7633 5           dummy_base[p_exp] = savepv(uniq_terms[j]);
7634 5           dummy_level[p_exp] = savepv(levels[l]);
7635 5           term_map[p_exp] = j;
7636 5           p_exp++;
7637             }
7638 13 100         for (size_t l = 0; l < num_levels; l++) Safefree(levels[l]);
7639 4           Safefree(levels);
7640             } else {
7641 0           Safefree(levels);
7642 0           exp_terms[p_exp] = savepv(uniq_terms[j]);
7643 0           parent_term[p_exp] = savepv(uniq_terms[j]);
7644 0           is_dummy[p_exp] = FALSE; is_interact[p_exp] = FALSE;
7645 0           term_map[p_exp] = j;
7646 0           p_exp++;
7647             }
7648             } else {
7649 9           exp_terms[p_exp] = savepv(uniq_terms[j]);
7650 9           parent_term[p_exp] = savepv(uniq_terms[j]);
7651 9           is_dummy[p_exp] = FALSE; is_interact[p_exp] = FALSE;
7652 9           term_map[p_exp] = j;
7653 9           p_exp++;
7654             }
7655             }
7656             }
7657 8           X_mat = (NV**)safemalloc(n * sizeof(NV*));
7658 72 100         for(i = 0; i < n; i++) X_mat[i] = (NV*)safemalloc(p_exp * sizeof(NV));
7659 8 50         Newx(Y, n, NV);
7660             // PHASE 4: Matrix Construction & Listwise Deletion
7661 72 100         for (i = 0; i < n; i++) {
7662 64           NV y_val = evaluate_term(aTHX_ data_hoa, row_hashes, i, lhs);
7663 64 50         if (isnan(y_val)) { Safefree(row_names[i]); continue; }
7664 64           bool row_ok = TRUE;
7665 64           NV *restrict row_x = (NV*)safemalloc(p_exp * sizeof(NV));
7666 258 100         for (j = 0; j < p_exp; j++) {
7667 194 100         if (strcmp(exp_terms[j], "Intercept") == 0) {
7668 64           row_x[j] = 1.0;
7669 130 100         } else if (is_interact[j]) {
7670 20           row_x[j] = row_x[left_idx[j]] * row_x[right_idx[j]];
7671 110 100         } else if (is_dummy[j]) {
7672 70           char*restrict str_val = get_data_string_alloc(aTHX_ data_hoa, row_hashes, i, dummy_base[j]);
7673 70 50         if (str_val) {
7674 70 100         row_x[j] = (strcmp(str_val, dummy_level[j]) == 0) ? 1.0 : 0.0;
7675 70           Safefree(str_val);
7676 0           } else { row_ok = FALSE; break; }
7677             } else {
7678 40           row_x[j] = evaluate_term(aTHX_ data_hoa, row_hashes, i, parent_term[j]);
7679 40 50         if (isnan(row_x[j])) { row_ok = FALSE; break; }
7680             }
7681             }
7682 64 50         if (!row_ok) { Safefree(row_names[i]); Safefree(row_x); continue; }
7683 64           Y[valid_n] = y_val;
7684 258 100         for (j = 0; j < p_exp; j++) X_mat[valid_n][j] = row_x[j];
7685 64           valid_n++;
7686 64           Safefree(row_x);
7687 64           Safefree(row_names[i]);
7688             }
7689 8           Safefree(row_names);
7690 8 100         if (valid_n <= p_exp) {
7691             // Full Clean Up
7692 4 100         for (i = 0; i < num_terms; i++) Safefree(terms[i]); Safefree(terms);
7693 4 100         for (i = 0; i < num_uniq; i++) Safefree(uniq_terms[i]); Safefree(uniq_terms);
7694 4 100         for (j = 0; j < p_exp; j++) {
7695 3           Safefree(exp_terms[j]); Safefree(parent_term[j]);
7696 3 50         if (is_dummy[j]) { Safefree(dummy_base[j]); Safefree(dummy_level[j]); }
7697             }
7698 1           Safefree(exp_terms); Safefree(parent_term);
7699 1           Safefree(is_dummy); Safefree(is_interact);
7700 1           Safefree(dummy_base); Safefree(dummy_level);
7701 1           Safefree(term_map); Safefree(left_idx); Safefree(right_idx);
7702 3 100         for(i = 0; i < n; i++) Safefree(X_mat[i]);
7703 1           Safefree(X_mat); Safefree(Y);
7704 1 50         if (row_hashes) Safefree(row_hashes);
7705 4 50         for (i = 0; i < num_uniq; i++) { if (term_base_level[i]) Safefree(term_base_level[i]); }
    100          
7706 1           Safefree(term_base_level);
7707 1           croak("aov: 0 degrees of freedom (too many NAs or parameters > observations)");
7708             }
7709             // PHASE 5: Math & Output Formatting
7710 7           bool *restrict aliased_qr = (bool*)safemalloc(p_exp * sizeof(bool));
7711 7           size_t *restrict rank_map = (size_t*)safemalloc(p_exp * sizeof(size_t));
7712 7           apply_householder_aov(X_mat, Y, valid_n, p_exp, aliased_qr, rank_map);
7713             NV *restrict term_ss;
7714             int *restrict term_df;
7715 7           Newxz(term_ss, num_uniq, NV);
7716 7           Newxz(term_df, num_uniq, int);
7717 27 100         for (i = 0; i < p_exp; i++) {
7718 20 100         if (strcmp(exp_terms[i], "Intercept") == 0) continue;
7719 13 100         if (aliased_qr[i]) continue;
7720 12           int t_idx = term_map[i];
7721 12           size_t r_k = rank_map[i];
7722 12           term_ss[t_idx] += Y[r_k] * Y[r_k];
7723 12           term_df[t_idx] += 1;
7724             }
7725 7           int rank = 0;
7726 27 100         for (i = 0; i < p_exp; i++) {
7727 20 100         if (!aliased_qr[i]) rank++;
7728             }
7729 7           NV rss_prev = 0.0;
7730 50 100         for (i = rank; i < valid_n; i++) {
7731 43           rss_prev += Y[i] * Y[i];
7732             }
7733 7           int res_df = valid_n - rank;
7734 7 50         NV ms_res = (res_df > 0) ? rss_prev / res_df : 0.0;
7735 7           HV*restrict ret_hash = newHV();
7736 26 100         for (j = 0; j < num_uniq; j++) {
7737 19 100         if (strcmp(uniq_terms[j], "Intercept") == 0) continue;
7738 12           HV*restrict term_stats = newHV();
7739 12           NV ss = term_ss[j];
7740 12           int df = term_df[j];
7741 12 100         NV ms = (df > 0) ? ss / df : 0.0;
7742              
7743 12           hv_stores(term_stats, "Df", newSViv(df));
7744 12           hv_stores(term_stats, "Sum Sq", newSVnv(ss));
7745 12           hv_stores(term_stats, "Mean Sq", newSVnv(ms));
7746 23 50         if (ms_res > 0.0 && df > 0) {
    100          
7747 11           NV f_val = ms / ms_res;
7748 11           hv_stores(term_stats, "F value", newSVnv(f_val));
7749 11           hv_stores(term_stats, "Pr(>F)", newSVnv(1.0 - pf(f_val, (NV)df, (NV)res_df)));
7750             } else {
7751 1           hv_stores(term_stats, "F value", newSVnv(NAN));
7752 1           hv_stores(term_stats, "Pr(>F)", newSVnv(NAN));
7753             }
7754 12           hv_store(ret_hash, uniq_terms[j], strlen(uniq_terms[j]), newRV_noinc((SV*)term_stats), 0);
7755             }
7756 7           HV*restrict res_stats = newHV();
7757 7           hv_stores(res_stats, "Df", newSViv(res_df));
7758 7           hv_stores(res_stats, "Sum Sq", newSVnv(rss_prev));
7759 7           hv_stores(res_stats, "Mean Sq", newSVnv(ms_res));
7760 7           hv_stores(ret_hash, "Residuals", newRV_noinc((SV*)res_stats));
7761             {
7762 7           HV *restrict tgt_hoa = data_hoa;
7763 7           HV **restrict tgt_row_hashes = row_hashes;
7764 7           size_t tgt_n = n;
7765             // Route evaluation to the original unstacked HoA when a formula was implied
7766 7 100         if (is_stacked) {
7767 1           tgt_hoa = (HV*)SvRV(orig_data_sv);
7768 1           tgt_row_hashes = NULL;
7769 1           hv_iterinit(tgt_hoa);
7770 1           HE *restrict e = hv_iternext(tgt_hoa);
7771 1 50         if (e) {
7772 1           SV *val = hv_iterval(tgt_hoa, e);
7773 1 50         if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVAV) {
    50          
7774 1           tgt_n = av_len((AV*)SvRV(val)) + 1;
7775             }
7776             }
7777             }
7778 7           AV *restrict all_cols = get_all_columns(aTHX_ tgt_hoa, tgt_row_hashes, tgt_n);
7779 7           HV *restrict mean_hv = newHV();
7780 7           HV *restrict size_hv = newHV();
7781 25 100         for (size_t c = 0; c <= (size_t)av_len(all_cols); c++) {
7782 18           SV **restrict col_sv = av_fetch(all_cols, c, 0);
7783 18 50         if (!col_sv || !SvOK(*col_sv)) continue;
    50          
7784 18           const char *restrict col_name = SvPV_nolen(*col_sv);
7785 18           NV col_sum = 0.0;
7786 18           IV col_count = 0;
7787 165 100         for (i = 0; i < tgt_n; i++) {
7788 147           NV val = evaluate_term(aTHX_ tgt_hoa, tgt_row_hashes, i, col_name);
7789 147 100         if (!isnan(val)) { col_sum += val; col_count++; }
7790             }
7791 18 100         NV col_mean = (col_count > 0) ? col_sum / col_count : NAN;
7792 18           hv_store(mean_hv, col_name, strlen(col_name), newSVnv(col_mean), 0);
7793 18           hv_store(size_hv, col_name, strlen(col_name), newSViv(col_count), 0);
7794             }
7795 7           SvREFCNT_dec(all_cols);
7796 7           HV *restrict gs_hv = newHV();
7797 7           hv_stores(gs_hv, "mean", newRV_noinc((SV*)mean_hv));
7798 7           hv_stores(gs_hv, "size", newRV_noinc((SV*)size_hv));
7799 7           hv_stores(ret_hash, "group_stats", newRV_noinc((SV*)gs_hv));
7800             }
7801             // Deep Cleanup
7802 26 100         for (i = 0; i < num_terms; i++) Safefree(terms[i]); Safefree(terms);
7803 26 100         for (i = 0; i < num_uniq; i++) Safefree(uniq_terms[i]); Safefree(uniq_terms);
7804 27 100         for (j = 0; j < p_exp; j++) {
7805 20           Safefree(exp_terms[j]); Safefree(parent_term[j]);
7806 20 100         if (is_dummy[j]) { Safefree(dummy_base[j]); Safefree(dummy_level[j]); }
7807             }
7808 7           Safefree(exp_terms); Safefree(parent_term);
7809 7           Safefree(is_dummy); Safefree(is_interact);
7810 7           Safefree(dummy_base); Safefree(dummy_level);
7811 7           Safefree(term_map); Safefree(left_idx); Safefree(right_idx);
7812 7           Safefree(term_ss); Safefree(term_df);
7813 69 100         for (i = 0; i < n; i++) Safefree(X_mat[i]);
7814 7           Safefree(X_mat); Safefree(Y);
7815 7           Safefree(aliased_qr); Safefree(rank_map);
7816 26 100         for (i = 0; i < num_uniq; i++) { if (term_base_level[i]) Safefree(term_base_level[i]); }
    100          
7817 7           Safefree(term_base_level);
7818 7 50         if (row_hashes) Safefree(row_hashes);
7819 7           RETVAL = newRV_noinc((SV*)ret_hash);
7820             }
7821             OUTPUT:
7822             RETVAL
7823              
7824             PROTOTYPES: DISABLE
7825              
7826              
7827             SV* fisher_test(...)
7828             CODE:
7829             {
7830 18 100         if (items < 1) croak("fisher_test requires at least a data reference");
7831              
7832 17           SV *restrict data_ref = ST(0);
7833 17           NV conf_level = 0.95;
7834 17           const char *restrict alternative = "two.sided";
7835              
7836 21 100         for (unsigned int i = 1; i < items; i += 2) {
7837 6 50         if (i + 1 >= items) croak("fisher_test: odd number of named arguments");
7838 6           const char *restrict key = SvPV_nolen(ST(i));
7839 6           SV *restrict val = ST(i + 1);
7840 6 100         if (strEQ(key, "conf_level") || strEQ(key, "conf.level")) {
    50          
7841 1           conf_level = SvNV(val);
7842 1 50         if (!(conf_level > 0 && conf_level < 1))
    50          
7843 1           croak("fisher_test: conf_level must be between 0 and 1");
7844 5 50         } else if (strEQ(key, "alternative")) {
7845 5           alternative = SvPV_nolen(val);
7846 5 50         if (strNE(alternative, "two.sided") && strNE(alternative, "less") &&
    100          
7847 3 100         strNE(alternative, "greater"))
7848 1           croak("fisher_test: alternative must be 'two.sided', 'less' or 'greater'");
7849             } else {
7850 0           croak("fisher_test: unknown argument '%s'", key);
7851             }
7852             }
7853 15 50         if (!SvROK(data_ref)) croak("fisher_test requires a reference to a 2x2 Array or Hash");
7854 15           SV *restrict deref = SvRV(data_ref);
7855 15           long a = 0, b = 0, c = 0, d = 0;
7856 15 100         if (SvTYPE(deref) == SVt_PVAV) {
7857 11           AV *restrict outer = (AV *)deref;
7858 11 50         if (av_len(outer) != 1) croak("Outer array must have exactly 2 rows");
7859 11           SV **restrict r1p = av_fetch(outer, 0, 0);
7860 11           SV **restrict r2p = av_fetch(outer, 1, 0);
7861 11 50         if (!(r1p && r2p && SvROK(*r1p) && SvROK(*r2p)
    50          
    50          
    50          
7862 11 50         && SvTYPE(SvRV(*r1p)) == SVt_PVAV && SvTYPE(SvRV(*r2p)) == SVt_PVAV))
    50          
7863 0           croak("Invalid 2D array structure: need two array-ref rows");
7864 11           AV *restrict r1 = (AV *)SvRV(*r1p), *r2 = (AV *)SvRV(*r2p);
7865 11 100         if (av_len(r1) != 1 || av_len(r2) != 1)
    50          
7866 1           croak("Each row must have exactly 2 columns");
7867 10           a = ft_cell(aTHX_ *av_fetch(r1, 0, 0), "cell [0][0]");
7868 10           b = ft_cell(aTHX_ *av_fetch(r1, 1, 0), "cell [0][1]");
7869 10           c = ft_cell(aTHX_ *av_fetch(r2, 0, 0), "cell [1][0]");
7870 10           d = ft_cell(aTHX_ *av_fetch(r2, 1, 0), "cell [1][1]");
7871 4 50         } else if (SvTYPE(deref) == SVt_PVHV) {
7872             /* 2x2 hash; rows and columns are ordered by lexical key sort so the
7873             * result is deterministic regardless of Perl's hash randomization. */
7874 4           HV *restrict outer = (HV *)deref;
7875 4 50         if (HvUSEDKEYS(outer) != 2) croak("Outer hash must have exactly 2 keys");
    50          
7876 4           hv_iterinit(outer);
7877 4           HE *restrict e1 = hv_iternext(outer), *e2 = hv_iternext(outer);
7878 4           const char *restrict ok1 = SvPV_nolen(hv_iterkeysv(e1));
7879 4           int swap_rows = strcmp(ok1, SvPV_nolen(hv_iterkeysv(e2))) > 0;
7880 4 100         SV *restrict row1_sv = hv_iterval(outer, swap_rows ? e2 : e1);
7881 4 100         SV *restrict row2_sv = hv_iterval(outer, swap_rows ? e1 : e2);
7882 4 50         if (!SvROK(row1_sv) || SvTYPE(SvRV(row1_sv)) != SVt_PVHV ||
    50          
7883 4 50         !SvROK(row2_sv) || SvTYPE(SvRV(row2_sv)) != SVt_PVHV)
    50          
7884 0           croak("Inner elements must be hash refs");
7885              
7886 4           HV *restrict rows[2]; rows[0] = (HV *)SvRV(row1_sv); rows[1] = (HV *)SvRV(row2_sv);
7887             long cells[2][2];
7888 12 100         for (unsigned int rr = 0; rr < 2; rr++) {
7889 8           HV *restrict in = rows[rr];
7890 8 50         if (HvUSEDKEYS(in) != 2) croak("Inner hashes must have exactly 2 keys");
    50          
7891 8           hv_iterinit(in);
7892 8           HE *c1 = hv_iternext(in), *c2 = hv_iternext(in);
7893 8           const char *k1 = SvPV_nolen(hv_iterkeysv(c1));
7894 8           int swap_cols = strcmp(k1, SvPV_nolen(hv_iterkeysv(c2))) > 0;
7895 8 100         HE *col0 = swap_cols ? c2 : c1;
7896 8 100         HE *col1 = swap_cols ? c1 : c2;
7897 8           cells[rr][0] = ft_cell(aTHX_ hv_iterval(in, col0), "hash cell");
7898 8           cells[rr][1] = ft_cell(aTHX_ hv_iterval(in, col1), "hash cell");
7899             }
7900 4           a = cells[0][0]; b = cells[0][1]; c = cells[1][0]; d = cells[1][1];
7901             } else {
7902 0           croak("Input must be a 2D Array or 2D Hash");
7903             }
7904 13 50         if (a + b + c + d == 0) croak("fisher_test: table is all zeros");
7905 13           NV p_val = exact_p_value(a, b, c, d, alternative);
7906             NV mle_or, ci_low, ci_high;
7907 13           calculate_exact_stats(a, b, c, d, conf_level, alternative, &mle_or, &ci_low, &ci_high);
7908              
7909 13           HV *restrict ret = newHV();
7910 13           hv_stores(ret, "method", newSVpv("Fisher's Exact Test for Count Data", 0));
7911 13           hv_stores(ret, "alternative", newSVpv(alternative, 0));
7912 13           AV *restrict ci = newAV();
7913 13           av_push(ci, newSVnv(ci_low));
7914 13           av_push(ci, newSVnv(ci_high));
7915 13           hv_stores(ret, "conf_int", newRV_noinc((SV *)ci));
7916 13           HV *restrict est = newHV();
7917 13           hv_stores(est, "odds ratio", newSVnv(mle_or));
7918 13           hv_stores(ret, "estimate", newRV_noinc((SV *)est));
7919 13           hv_stores(ret, "p_value", newSVnv(p_val));
7920 13           hv_stores(ret, "conf_level", newSVnv(conf_level));
7921 13           RETVAL = newRV_noinc((SV *)ret);
7922             }
7923             OUTPUT:
7924             RETVAL
7925              
7926             SV* power_t_test(...)
7927             CODE:
7928             {
7929 7           SV*restrict sv_n = NULL;
7930 7           SV*restrict sv_delta = NULL;
7931 7           SV*restrict sv_sd = NULL;
7932 7           SV*restrict sv_sig_level = NULL;
7933 7           SV*restrict sv_power = NULL;
7934              
7935 7           const char* restrict type = "two.sample";
7936 7           const char* restrict alternative = "two.sided";
7937 7           bool strict = FALSE;
7938 7           NV tol = pow(2.2204460492503131e-16, 0.25);
7939              
7940 7 50         if (items % 2 != 0) croak("Usage: power_t_test(n => 30, delta => 0.5, sd => 1.0, ...)");
7941 34 100         for (unsigned short int i = 0; i < items; i += 2) {
7942 27           const char* restrict key = SvPV_nolen(ST(i));
7943 27           SV* restrict val = ST(i+1);
7944              
7945 27 100         if (strEQ(key, "n")) sv_n = val;
7946 26 100         else if (strEQ(key, "delta")) sv_delta = val;
7947 19 100         else if (strEQ(key, "sd")) sv_sd = val;
7948 12 50         else if (strEQ(key, "sig.level") || strEQ(key, "sig_level")) sv_sig_level = val;
    100          
7949 11 100         else if (strEQ(key, "power")) sv_power = val;
7950 5 100         else if (strEQ(key, "type")) type = SvPV_nolen(val);
7951 2 50         else if (strEQ(key, "alternative")) alternative = SvPV_nolen(val);
7952 0 0         else if (strEQ(key, "strict")) strict = SvTRUE(val);
7953 0 0         else if (strEQ(key, "tol")) tol = SvNV(val);
7954 0           else croak("power_t_test: unknown argument '%s'", key);
7955             }
7956              
7957 7 100         bool is_null_n = (!sv_n || !SvOK(sv_n));
    50          
7958 7 50         bool is_null_delta = (!sv_delta || !SvOK(sv_delta));
    50          
7959 7 100         bool is_null_power = (!sv_power || !SvOK(sv_power));
    50          
7960 7 50         bool is_null_sd = (sv_sd && !SvOK(sv_sd));
    50          
7961 7 100         bool is_null_sig_level = (sv_sig_level && !SvOK(sv_sig_level));
    50          
7962              
7963 7           unsigned int missing_count = 0;
7964 7 100         if (is_null_n) missing_count++;
7965 7 50         if (is_null_delta) missing_count++;
7966 7 100         if (is_null_power) missing_count++;
7967 7 50         if (is_null_sd) missing_count++;
7968 7 50         if (is_null_sig_level) missing_count++;
7969              
7970 7 50         if (missing_count != 1) {
7971 0           croak("power_t_test: exactly one of 'n', 'delta', 'sd', 'power', and 'sig_level' must be undef/NULL");
7972             }
7973              
7974 7 100         NV n = is_null_n ? 0.0 : SvNV(sv_n);
7975 7 50         NV delta = is_null_delta ? 0.0 : SvNV(sv_delta);
7976 7 50         NV sd = (!sv_sd || is_null_sd) ? 1.0 : SvNV(sv_sd);
    50          
7977 7 100         NV sig_level = (!sv_sig_level || is_null_sig_level) ? 0.05 : SvNV(sv_sig_level);
    50          
7978 7 100         NV power = is_null_power ? 0.0 : SvNV(sv_power);
7979 7 100         short int tsample = (strEQ(type, "one.sample") || strEQ(type, "paired")) ? 1 : 2;
    100          
7980 7 100         short int tside = (strEQ(alternative, "one.sided") || strEQ(alternative, "greater") || strEQ(alternative, "less")) ? 1 : 2;
    50          
    50          
7981 7 100         if (tside == 2 && !is_null_delta) delta = fabs(delta);
    50          
7982 7 100         if (is_null_power) {
7983 1           power = p_body(n, delta, sd, sig_level, tsample, tside, strict);
7984 6 50         } else if (is_null_n) {
7985 6           NV low = 2.0, high = 1e7;
7986 6 50         while (p_body(high, delta, sd, sig_level, tsample, tside, strict) < power && high < 1e12) high *= 2.0;
    0          
7987 228 100         while (high - low > tol) {
7988 222           NV mid = low + (high - low) / 2.0;
7989 222 100         if (p_body(mid, delta, sd, sig_level, tsample, tside, strict) < power) low = mid;
7990 173           else high = mid;
7991             }
7992 6           n = low + (high - low) / 2.0;
7993 0 0         } else if (is_null_sd) {
7994 0           NV low = delta * 1e-7, high = delta * 1e7;
7995 0 0         while (high - low > tol) {
7996 0           NV mid = low + (high - low) / 2.0;
7997 0 0         if (p_body(n, delta, mid, sig_level, tsample, tside, strict) > power) low = mid;
7998 0           else high = mid;
7999             }
8000 0           sd = low + (high - low) / 2.0;
8001 0 0         } else if (is_null_delta) {
8002 0           NV low = sd * 1e-7, high = sd * 1e7;
8003 0 0         while (p_body(n, high, sd, sig_level, tsample, tside, strict) < power && high < 1e12) high *= 2.0;
    0          
8004 0 0         while (high - low > tol) {
8005 0           NV mid = low + (high - low) / 2.0;
8006 0 0         if (p_body(n, mid, sd, sig_level, tsample, tside, strict) < power) low = mid;
8007 0           else high = mid;
8008             }
8009 0           delta = low + (high - low) / 2.0;
8010 0 0         } else if (is_null_sig_level) {
8011 0           NV low = 1e-10, high = 1.0 - 1e-10;
8012 0 0         while (high - low > tol) {
8013 0           NV mid = low + (high - low) / 2.0;
8014 0 0         if (p_body(n, delta, sd, mid, tsample, tside, strict) < power) low = mid;
8015 0           else high = mid;
8016             }
8017 0           sig_level = low + (high - low) / 2.0;
8018             }
8019 7           HV*restrict ret = newHV();
8020 7           hv_stores(ret, "n", newSVnv(n));
8021 7           hv_stores(ret, "delta", newSVnv(delta));
8022 7           hv_stores(ret, "sd", newSVnv(sd));
8023 7           hv_stores(ret, "sig.level", newSVnv(sig_level));
8024 7           hv_stores(ret, "power", newSVnv(power));
8025 7           hv_stores(ret, "alternative", newSVpv(alternative, 0));
8026 7 100         const char*restrict m_str = (tsample == 1) ? (strEQ(type, "paired") ? "Paired t test power calculation" : "One-sample t test power calculation") : "Two-sample t test power calculation";
    100          
8027 7           hv_stores(ret, "method", newSVpv(m_str, 0));
8028 7 100         const char*restrict n_str = (tsample == 2) ? "n is number in *each* group" : (strEQ(type, "paired") ? "n is number of *pairs*, sd is std.dev. of *differences* within pairs" : "");
    100          
8029 7 100         if (n_str[0] != '\0') hv_stores(ret, "note", newSVpv(n_str, 0));
8030 7           RETVAL = newRV_noinc((SV*)ret);
8031             }
8032             OUTPUT:
8033             RETVAL
8034              
8035             SV* kruskal_test(...)
8036             CODE:
8037             {
8038 3           SV *restrict x_sv = NULL, *restrict g_sv = NULL, *restrict h_sv = NULL;
8039 3           unsigned int arg_idx = 0;
8040             // 1. Shift positional arguments
8041             // Accept either: (arrayref, arrayref) or (hashref)
8042 3 50         if (arg_idx < items && SvROK(ST(arg_idx))) {
    100          
8043 2           svtype t = SvTYPE(SvRV(ST(arg_idx)));
8044 2 100         if (t == SVt_PVAV) {
8045 1           x_sv = ST(arg_idx++);
8046 1 50         } else if (t == SVt_PVHV) {
8047 1           h_sv = ST(arg_idx++); /* hash-of-arrays shortcut */
8048             }
8049             }
8050 3 100         if (!h_sv && arg_idx < items
    50          
8051 2 100         && SvROK(ST(arg_idx))
8052 1 50         && SvTYPE(SvRV(ST(arg_idx))) == SVt_PVAV) {
8053 1           g_sv = ST(arg_idx++);
8054             }
8055             // 2. Parse named arguments (fallback)
8056 5 100         for (; arg_idx < items; arg_idx += 2) {
8057 2           const char *restrict key = SvPV_nolen(ST(arg_idx));
8058 2           SV *restrict val = ST(arg_idx + 1);
8059 2 100         if (strEQ(key, "x")) x_sv = val;
8060 1 50         else if (strEQ(key, "g")) g_sv = val;
8061 0 0         else if (strEQ(key, "h")) h_sv = val;
8062 0           else croak("kruskal_test: unknown argument '%s'", key);
8063             }
8064             // 3. Mutual-exclusion guard
8065 3 100         if (h_sv && (x_sv || g_sv))
    50          
    50          
8066 0           croak("kruskal_test: cannot mix 'h' (hash-of-arrays) with 'x'/'g' inputs");
8067              
8068             // Shared state filled by whichever input branch runs
8069 3           RankInfo *restrict ri = NULL;
8070 3           char **restrict group_names = NULL; /* Track names to build group_stats */
8071 3           size_t valid_n = 0, k = 0;
8072             /* 4a. Hash-of-arrays input path */
8073             /* my %x = ( group1 => [...], group2 => [...], ... ) */
8074             /* ------------------------------------------------------------------ */
8075 3 100         if (h_sv) {
8076 1 50         if (!SvROK(h_sv) || SvTYPE(SvRV(h_sv)) != SVt_PVHV)
    50          
8077 0           croak("kruskal_test: 'h' must be a HASH reference");
8078 1           HV *restrict h_hv = (HV*)SvRV(h_sv);
8079             // First pass – validate values and tally total elements
8080 1           size_t total = 0;
8081 1           hv_iterinit(h_hv);
8082             HE *restrict he;
8083 4 100         while ((he = hv_iternext(h_hv))) {
8084 3           SV *restrict val = HeVAL(he);
8085 3 50         if (!SvROK(val) || SvTYPE(SvRV(val)) != SVt_PVAV)
    50          
8086 0           croak("kruskal_test: every value in 'h' must be an ARRAY reference");
8087 3           total += (size_t)(av_len((AV*)SvRV(val)) + 1);
8088             }
8089 1 50         if (total < 2) croak("not enough observations");
8090 1           ri = (RankInfo *)safemalloc(total * sizeof(RankInfo));
8091 1 50         size_t num_keys = HvKEYS(h_hv);
8092 1           group_names = (char **)safecalloc(num_keys, sizeof(char*));
8093             /* 2nd pass – fill ri[], assigning one group_id per hash key */
8094 1           size_t group_id = 0;
8095 1           hv_iterinit(h_hv);
8096 4 100         while ((he = hv_iternext(h_hv))) {
8097             STRLEN klen;
8098 3 50         const char *restrict key_str = HePV(he, klen);
8099 3           group_names[group_id] = savepvn(key_str, klen); // Save string key
8100 3           AV *restrict av = (AV*)SvRV(HeVAL(he));
8101 3           size_t n_g = (size_t)(av_len(av) + 1);
8102 17 100         for (size_t i = 0; i < n_g; i++) {
8103 14           SV **restrict el = av_fetch(av, i, 0);
8104 14 50         if (el && SvOK(*el) && looks_like_number(*el)) {
    50          
    50          
8105 14           ri[valid_n].val = SvNV(*el);
8106 14           ri[valid_n].idx = group_id; /* group identity */
8107 14           valid_n++;
8108             }
8109             }
8110 3           group_id++;
8111             }
8112 1           k = group_id; /* number of unique groups = number of hash keys */
8113             /* 4b. Original x / g array-pair input path */
8114             } else {
8115 2 50         if (!x_sv || !SvROK(x_sv) || SvTYPE(SvRV(x_sv)) != SVt_PVAV)
    50          
    50          
8116 0           croak("kruskal_test: 'x' is a required argument and must be an ARRAY reference");
8117 2 50         if (!g_sv || !SvROK(g_sv) || SvTYPE(SvRV(g_sv)) != SVt_PVAV)
    50          
    50          
8118 0           croak("kruskal_test: 'g' is a required argument and must be an ARRAY reference");
8119              
8120 2           AV *restrict x_av = (AV*)SvRV(x_sv);
8121 2           AV *restrict g_av = (AV*)SvRV(g_sv);
8122 2           size_t nx = (size_t)(av_len(x_av) + 1);
8123 2           size_t ng = (size_t)(av_len(g_av) + 1);
8124 2 50         if (nx != ng) croak("kruskal_test: 'x' and 'g' must have the same length");
8125 2 50         if (nx < 2) croak("not enough observations");
8126              
8127 2           ri = (RankInfo *)safemalloc(nx * sizeof(RankInfo));
8128 2           group_names = (char **)safecalloc(nx, sizeof(char*)); // Upper bound
8129              
8130             // Map string group names → contiguous integer IDs
8131 2           HV *restrict group_map = newHV();
8132 2           size_t next_group_id = 0;
8133              
8134 30 100         for (size_t i = 0; i < nx; i++) {
8135 28           SV **restrict x_el = av_fetch(x_av, i, 0);
8136 28           SV **restrict g_el = av_fetch(g_av, i, 0);
8137 28 50         if (x_el && SvOK(*x_el) && looks_like_number(*x_el)
    50          
    50          
8138 28 50         && g_el && SvOK(*g_el)) {
    50          
8139 28           const char *restrict g_str = SvPV_nolen(*g_el);
8140 28           STRLEN glen = strlen(g_str);
8141 28           SV **restrict id_sv = hv_fetch(group_map, g_str, glen, 0);
8142             size_t group_id;
8143 28 100         if (id_sv) {
8144 22           group_id = SvUV(*id_sv);
8145             } else {
8146 6           group_id = next_group_id++;
8147 6           hv_store(group_map, g_str, glen, newSVuv(group_id), 0);
8148 6           group_names[group_id] = savepvn(g_str, glen); // Save string key
8149             }
8150 28           ri[valid_n].val = SvNV(*x_el);
8151 28           ri[valid_n].idx = group_id;
8152 28           valid_n++;
8153             }
8154             }
8155 2           k = next_group_id;
8156 2           SvREFCNT_dec(group_map);
8157             }
8158             /* 5. Shared post-extraction validation */
8159 3 50         if (valid_n < 2 || k < 2) {
    50          
8160 0           Safefree(ri);
8161 0 0         if (group_names) {
8162 0 0         for (size_t i = 0; i < k; i++) { if (group_names[i]) Safefree(group_names[i]); }
    0          
8163 0           Safefree(group_names);
8164             }
8165 0 0         if (valid_n < 2) croak("not enough observations");
8166 0           croak("all observations are in the same group");
8167             }
8168             // 6. Ranking and Tie Accumulation (Reusing LikeR Helper)
8169 3           bool has_ties = 0;
8170 3           NV tie_adj = rank_and_count_ties(ri, valid_n, &has_ties);
8171             // 7. Aggregate Sum of Ranks AND Actual Values by Group
8172 3           NV *restrict group_rank_sums = (NV *)safecalloc(k, sizeof(NV));
8173 3           NV *restrict group_val_sums = (NV *)safecalloc(k, sizeof(NV)); // For Mean
8174 3           size_t *restrict group_counts = (size_t *)safecalloc(k, sizeof(size_t));
8175 45 100         for (size_t i = 0; i < valid_n; i++) {
8176 42           size_t g_id = ri[i].idx;
8177 42           group_rank_sums[g_id] += ri[i].rank;
8178 42           group_val_sums[g_id] += ri[i].val;
8179 42           group_counts[g_id]++;
8180             }
8181             // 8. Calculate STATISTIC
8182 3           NV stat_base = 0.0;
8183 12 100         for (size_t i = 0; i < k; i++) {
8184 9 50         if (group_counts[i] > 0)
8185 9           stat_base += (group_rank_sums[i] * group_rank_sums[i])
8186 9           / (NV)group_counts[i];
8187             }
8188 3           NV n_d = (NV)valid_n;
8189 3           NV stat = (12.0 * stat_base / (n_d * (n_d + 1.0))) - 3.0 * (n_d + 1.0);
8190 3 50         if (tie_adj > 0.0) {
8191 0           NV tie_denom = 1.0 - (tie_adj / (n_d * n_d * n_d - n_d));
8192 0           stat /= tie_denom;
8193             }
8194 3           int df = (int)k - 1;
8195 3           NV p_val = get_p_value(stat, df);
8196             // 9. Return structured data exactly like R's htest
8197 3           HV *restrict res = newHV();
8198 3           hv_stores(res, "statistic", newSVnv(stat));
8199 3           hv_stores(res, "parameter", newSViv(df));
8200 3           hv_stores(res, "p_value", newSVnv(p_val));
8201 3           hv_stores(res, "p.value", newSVnv(p_val));
8202 3           hv_stores(res, "method", newSVpv("Kruskal-Wallis rank sum test", 0));
8203             // 10. Build the group_stats hash
8204 3           HV *restrict group_stats = newHV();
8205 3           HV *restrict stats_mean = newHV();
8206 3           HV *restrict stats_size = newHV();
8207 12 100         for (size_t i = 0; i < k; i++) {
8208 9 50         if (group_counts[i] > 0 && group_names[i]) {
    50          
8209 9           NV mean = group_val_sums[i] / (NV)group_counts[i];
8210 9           size_t nlen = strlen(group_names[i]);
8211 9           hv_store(stats_mean, group_names[i], nlen, newSVnv(mean), 0);
8212 9           hv_store(stats_size, group_names[i], nlen, newSVuv(group_counts[i]), 0);
8213             }
8214 9 50         if (group_names[i]) Safefree(group_names[i]); // Clean up name copy
8215             }
8216             // Embed the nested hashes
8217 3           hv_stores(group_stats, "mean", newRV_noinc((SV*)stats_mean));
8218 3           hv_stores(group_stats, "size", newRV_noinc((SV*)stats_size));
8219 3           hv_stores(res, "group_stats", newRV_noinc((SV*)group_stats));
8220             // Memory Cleanup
8221 3           Safefree(group_names); Safefree(group_rank_sums);
8222 3           Safefree(group_val_sums); Safefree(group_counts); Safefree(ri);
8223              
8224 3           RETVAL = newRV_noinc((SV*)res);
8225             }
8226             OUTPUT:
8227             RETVAL
8228              
8229             SV* var_test(...)
8230             CODE:
8231             {
8232 6           SV* restrict x_sv = NULL;
8233 6           SV* restrict y_sv = NULL;
8234 6           NV ratio = 1.0, conf_level = 0.95;
8235 6           const char* restrict alternative = "two.sided";
8236 6           unsigned int arg_idx = 0;
8237              
8238             // 1. Shift positional argument 'x' if it's an array reference
8239 6 50         if (arg_idx < items && SvROK(ST(arg_idx)) && SvTYPE(SvRV(ST(arg_idx))) == SVt_PVAV) {
    50          
    50          
8240 6           x_sv = ST(arg_idx);
8241 6           arg_idx++;
8242             }
8243              
8244             // 2. Shift positional argument 'y' if it's an array reference
8245 6 50         if (arg_idx < items && SvROK(ST(arg_idx)) && SvTYPE(SvRV(ST(arg_idx))) == SVt_PVAV) {
    50          
    50          
8246 6           y_sv = ST(arg_idx);
8247 6           arg_idx++;
8248             }
8249             // Ensure the remaining arguments form complete key-value pairs
8250 6 50         if ((items - arg_idx) % 2 != 0) {
8251 0           croak("Usage: var_test(\\@x, \\@y, key => value, ...)");
8252             }
8253             // --- Parse named arguments from the remaining flat stack ---
8254 8 100         for (; arg_idx < items; arg_idx += 2) {
8255 2           const char* restrict key = SvPV_nolen(ST(arg_idx));
8256 2           SV* restrict val = ST(arg_idx + 1);
8257              
8258 2 50         if (strEQ(key, "x")) x_sv = val;
8259 2 50         else if (strEQ(key, "y")) y_sv = val;
8260 2 100         else if (strEQ(key, "ratio")) ratio = SvNV(val);
8261 1 50         else if (strEQ(key, "conf_level") || strEQ(key, "conf.level")) conf_level = SvNV(val);
    0          
8262 0 0         else if (strEQ(key, "alternative")) alternative = SvPV_nolen(val);
8263 0           else croak("var_test: unknown argument '%s'", key);
8264             }
8265             // --- Validate required inputs / types ---
8266 6 50         if (!x_sv || !SvROK(x_sv) || SvTYPE(SvRV(x_sv)) != SVt_PVAV)
    50          
    50          
8267 0           croak("var_test: 'x' is a required argument and must be an ARRAY reference");
8268 6 50         if (!y_sv || !SvROK(y_sv) || SvTYPE(SvRV(y_sv)) != SVt_PVAV)
    50          
    50          
8269 0           croak("var_test: 'y' is a required argument and must be an ARRAY reference");
8270              
8271 6 50         if (ratio <= 0.0 || !isfinite(ratio))
    50          
8272 0           croak("var_test: 'ratio' must be a single positive number");
8273 6 50         if (conf_level <= 0.0 || conf_level >= 1.0 || !isfinite(conf_level))
    50          
    50          
8274 0           croak("var_test: 'conf.level' must be a single number between 0 and 1");
8275 6           AV* restrict x_av = (AV*)SvRV(x_sv);
8276 6           AV* restrict y_av = (AV*)SvRV(y_sv);
8277 6           size_t nx_raw = av_len(x_av) + 1;
8278 6           size_t ny_raw = av_len(y_av) + 1;
8279             // --- Computation via Welford's Algorithm (ignoring NaNs) ---
8280 6           NV mean_x = 0.0, M2_x = 0.0;
8281 6           size_t nx = 0;
8282 32 100         for (size_t i = 0; i < nx_raw; i++) {
8283 26           SV** restrict tv = av_fetch(x_av, i, 0);
8284 26 50         if (tv && SvOK(*tv) && looks_like_number(*tv)) {
    50          
    50          
8285 26           NV val = SvNV(*tv);
8286 26 50         if (!isnan(val) && isfinite(val)) {
    50          
8287 26           nx++;
8288 26           NV delta = val - mean_x;
8289 26           mean_x += delta / nx;
8290 26           M2_x += delta * (val - mean_x);
8291             }
8292             }
8293             }
8294              
8295 6           NV mean_y = 0.0, M2_y = 0.0;
8296 6           size_t ny = 0;
8297 27 100         for (size_t i = 0; i < ny_raw; i++) {
8298 21           SV** restrict tv = av_fetch(y_av, i, 0);
8299 21 50         if (tv && SvOK(*tv) && looks_like_number(*tv)) {
    50          
    50          
8300 21           NV val = SvNV(*tv);
8301 21 50         if (!isnan(val) && isfinite(val)) {
    50          
8302 21           ny++;
8303 21           NV delta = val - mean_y;
8304 21           mean_y += delta / ny;
8305 21           M2_y += delta * (val - mean_y);
8306             }
8307             }
8308             }
8309              
8310 6 100         if (nx < 2) croak("not enough 'x' observations");
8311 5 100         if (ny < 2) croak("not enough 'y' observations");
8312              
8313 4           NV df_x = (NV)(nx - 1);
8314 4           NV df_y = (NV)(ny - 1);
8315 4           NV var_x = M2_x / df_x;
8316 4           NV var_y = M2_y / df_y;
8317 4 100         if (var_y == 0.0) croak("var_test: variance of 'y' is zero (cannot divide by zero)");
8318             // --- Statistics Math ---
8319 3           NV estimate = var_x / var_y;
8320 3           NV statistic = estimate / ratio;
8321 3           NV p_val = pf(statistic, df_x, df_y);
8322 3           NV ci_lower = 0.0, ci_upper = INFINITY;
8323 3 50         if (strcmp(alternative, "less") == 0) {
8324 0           ci_upper = estimate / qf_bisection(1.0 - conf_level, df_x, df_y);
8325 3 50         } else if (strcmp(alternative, "greater") == 0) {
8326 0           p_val = 1.0 - p_val;
8327 0           ci_lower = estimate / qf_bisection(conf_level, df_x, df_y);
8328             } else {
8329             // two.sided
8330 3           NV p1 = p_val;
8331 3           NV p2 = 1.0 - p_val;
8332 3 50         p_val = 2.0 * (p1 < p2 ? p1 : p2);
8333 3           NV beta = (1.0 - conf_level) / 2.0;
8334 3           ci_lower = estimate / qf_bisection(1.0 - beta, df_x, df_y);
8335 3           ci_upper = estimate / qf_bisection(beta, df_x, df_y);
8336             }
8337             // --- Pack Results ---
8338 3           HV* restrict results = newHV();
8339 3           hv_store(results, "statistic", 9, newSVnv(statistic), 0);
8340 3           AV* restrict param_av = newAV();
8341 3           av_push(param_av, newSVnv(df_x));
8342 3           av_push(param_av, newSVnv(df_y));
8343 3           hv_store(results, "parameter", 9, newRV_noinc((SV*)param_av), 0);
8344 3           hv_store(results, "p_value", 7, newSVnv(p_val), 0);
8345 3           AV* restrict conf_int = newAV();
8346 3           av_push(conf_int, newSVnv(ci_lower));
8347 3           av_push(conf_int, newSVnv(ci_upper));
8348 3           hv_store(results, "conf_int", 8, newRV_noinc((SV*)conf_int), 0);
8349 3           hv_store(results, "estimate", 8, newSVnv(estimate), 0);
8350 3           hv_store(results, "null_value", 10, newSVnv(ratio), 0);
8351 3           hv_store(results, "alternative", 11, newSVpv(alternative, 0), 0);
8352 3           hv_store(results, "method", 6, newSVpv("F test to compare two variances", 0), 0);
8353 3           RETVAL = newRV_noinc((SV*)results);
8354             }
8355             OUTPUT:
8356             RETVAL
8357              
8358             SV *sample(ref, n = 1)
8359             SV *ref
8360             IV n
8361             PREINIT:
8362 6 50         SV *restrict ret = &PL_sv_undef;
8363             CODE:
8364 6 50         if (!PL_srand_called) {
8365 0           (void)seedDrand01((Rand_seed_t)Perl_seed(aTHX));
8366 0           PL_srand_called = TRUE;
8367             }
8368 6 50         if (n < 0) n = 0;
8369 6 50         if (SvROK(ref)) {
8370 6           SV *restrict rv = SvRV(ref);
8371             /* --- HASH REFERENCE --- */
8372 6 100         if (SvTYPE(rv) == SVt_PVHV) {
8373 3           HV *restrict hv = (HV *)rv;
8374 3           unsigned count = hv_iterinit(hv);
8375 3 50         unsigned limit = (n < (IV)count) ? (I32)n : count;
8376 3           HV *restrict ret_hv = newHV();
8377              
8378 3 50         if (count > 0 && limit > 0) {
    50          
8379             HE **restrict entries;
8380             HE *restrict entry;
8381             unsigned i;
8382 3           Newx(entries, count, HE *);
8383             /* Collect all HE pointers in one pass */
8384 3           i = 0;
8385 15 100         while ((entry = hv_iternext(hv)))
8386 12           entries[i++] = entry;
8387              
8388             /* Partial Fisher-Yates (only 'limit' passes) */
8389 9 100         for (i = 0; i < limit; i++) {
8390 6           I32 j = i + (I32)(Drand01() * (count - i));
8391 6           HE *restrict tmp = entries[i];
8392 6           entries[i] = entries[j];
8393 6           entries[j] = tmp;
8394             }
8395              
8396             /* Pre-size result hash to avoid rehashing during population */
8397 3           hv_ksplit(ret_hv, limit);
8398              
8399 9 100         for (i = 0; i < limit; i++) {
8400 6           HEK *restrict hek = HeKEY_hek(entries[i]);
8401             /*
8402             * hv_store() with a precomputed hash skips the hash
8403             * computation entirely. Negative klen signals UTF-8.
8404             */
8405 6 50         (void)hv_store(
8406             ret_hv,
8407             HEK_KEY(hek),
8408             HEK_UTF8(hek) ? -(I32)HEK_LEN(hek) : (I32)HEK_LEN(hek),
8409             SvREFCNT_inc(HeVAL(entries[i])), /* HeVAL: direct macro, no call */
8410             HeHASH(entries[i]) /* reuse precomputed hash */
8411             );
8412             }
8413 3           Safefree(entries);
8414             }
8415 3           ret = newRV_noinc((SV *)ret_hv);
8416 3 50         } else if (SvTYPE(rv) == SVt_PVAV) {/* --- ARRAY REFERENCE --- */
8417 3           AV *restrict av = (AV *)rv;
8418 3 50         size_t count = av_top_index(av) + 1; /* signed; 0 for empty AV */
8419 3           size_t limit = (n < count) ? (size_t)n : count;
8420 3           AV *restrict ret_av = newAV();
8421             /* Pre-allocate the result array to avoid incremental reallocs */
8422 3 50         if (n > 0)
8423 3           av_extend(ret_av, (size_t)n - 1);
8424 3 50         if (count > 0) {
8425 3           SV **restrict src = AvARRAY(av); /* direct pointer into AV's C array */
8426             size_t *restrict idx;
8427              
8428             /* Shuffle indices rather than SV** to keep the original AV intact */
8429 3 50         Newx(idx, count, size_t);
8430 18 100         for (size_t i = 0; i < count; i++)
8431 15           idx[i] = i;
8432             // Partial Fisher-Yates on the index array
8433 9 100         for (size_t i = 0; i < limit; i++) {
8434 6           size_t j = i + (size_t)(Drand01() * (count - i));
8435 6           size_t tmp = idx[i];
8436 6           idx[i] = idx[j];
8437 6           idx[j] = tmp;
8438             }
8439              
8440 9 100         for (size_t i = 0; i < (size_t)n; i++) {
8441 6 50         if (i < limit) {
8442 6           SV *restrict sv = src[idx[i]]; /* AvARRAY direct access — no av_fetch call */
8443             SV *restrict push_sv;
8444 6 50         if (sv && sv != &PL_sv_undef)
    50          
8445 6           push_sv = SvREFCNT_inc(sv);
8446             else
8447 0           push_sv = newSV(0);
8448 6           av_push(ret_av, push_sv);
8449             } else {
8450 0           av_push(ret_av, newSV(0));
8451             }
8452             }
8453 3           Safefree(idx);
8454             } else {
8455 0 0         for (size_t i = 0; i < (size_t)n; i++)
8456 0           av_push(ret_av, newSV(0));
8457             }
8458 3           ret = newRV_noinc((SV *)ret_av);
8459             }
8460             }
8461 6           RETVAL = ret;
8462             OUTPUT:
8463             RETVAL
8464              
8465             SV* dnorm(...)
8466             CODE:
8467             {
8468 23 50         if (items < 1) {
8469 0           croak("Usage: dnorm(x), dnorm(x, mean => 0, sd => 1, log => 0)");
8470             }
8471 23           SV*restrict x_sv = ST(0);
8472 23           NV mean = 0.0, sd = 1.0; /*defaults*/
8473 23           bool give_log = 0;
8474             // --- Parse remaining named arguments from the flat stack ---
8475 23 50         if ((items - 1) % 2 != 0) {
8476 0           croak("dnorm: Expected an even number of key-value named arguments after 'x'");
8477             }
8478 32 100         for (size_t i = 1; i < items; i += 2) {
8479 9           const char* restrict key = SvPV_nolen(ST(i));
8480 9           SV* restrict val = ST(i + 1);
8481 9 100         if (strEQ(key, "mean")) mean = SvNV(val);
8482 6 100         else if (strEQ(key, "sd")) sd = SvNV(val);
8483 2 50         else if (strEQ(key, "log")) give_log = SvTRUE(val) ? 1 : 0;
8484 0           else croak("dnorm: unknown argument '%s'", key);
8485             }
8486             // --- Branch based on scalar vs. arrayref for 'x' ---
8487 24 100         if (SvROK(x_sv) && SvTYPE(SvRV(x_sv)) == SVt_PVAV) {
    50          
8488             // x is an array reference
8489 1           AV *restrict x_av = (AV*)SvRV(x_sv);
8490 1           IV n = av_len(x_av) + 1;
8491 1           AV *restrict result_av = newAV();
8492 1 50         if (n > 0) {
8493 1           av_extend(result_av, n - 1);
8494 4 100         for (IV i = 0; i < n; i++) {
8495 3           SV **restrict elem = av_fetch(x_av, i, 0);
8496 3 50         NV x_val = (elem && *elem) ? SvNV(*elem) : NAN;
    50          
8497 3           NV res = c_dnorm(x_val, mean, sd, give_log);
8498 3           av_store(result_av, i, newSVnv(res));
8499             }
8500             }
8501 1           RETVAL = newRV_noinc((SV*)result_av);
8502             } else {
8503             // x is a single numeric scalar
8504 22           NV x_val = SvNV(x_sv);
8505 22           NV res = c_dnorm(x_val, mean, sd, give_log);
8506 22           RETVAL = newSVnv(res);
8507             }
8508             }
8509             OUTPUT:
8510             RETVAL
8511              
8512             void ljoin(h_ref, i_ref)
8513             SV *h_ref;
8514             SV *i_ref;
8515             PREINIT:
8516             HV *restrict h_hv, *restrict i_hv;
8517             HE *restrict h_entry;
8518             CODE:
8519             /* 1. Validate inputs are hash references */
8520 4 50         if (!SvROK(h_ref) || SvTYPE(SvRV(h_ref)) != SVt_PVHV) {
    50          
8521 0           croak("First argument to ljoin must be a hash reference");
8522             }
8523 4 50         if (!SvROK(i_ref) || SvTYPE(SvRV(i_ref)) != SVt_PVHV) {
    50          
8524 0           croak("Second argument to ljoin must be a hash reference");
8525             }
8526 4           h_hv = (HV *)SvRV(h_ref);
8527 4           i_hv = (HV *)SvRV(i_ref);
8528             /* 2. Iterate through the primary hash ($h) */
8529 4           hv_iterinit(h_hv);
8530 8 100         while ((h_entry = hv_iternext(h_hv))) {
8531 4           SV *restrict row_key_sv = hv_iterkeysv(h_entry);
8532 4           SV *restrict h_row_sv = hv_iterval(h_hv, h_entry);
8533             // 3. Check if this row key exists in the secondary hash ($i)
8534 4           HE *restrict i_fetch_he = hv_fetch_ent(i_hv, row_key_sv, 0, 0);
8535 4 50         if (i_fetch_he) {
8536 4           SV *restrict i_row_sv = HeVAL(i_fetch_he);
8537             // 4. Ensure $h->{row} is a Hash and $i->{row} is a valid reference
8538 4 100         if (SvROK(h_row_sv) && SvTYPE(SvRV(h_row_sv)) == SVt_PVHV && SvROK(i_row_sv)) {
    50          
    50          
8539 3           HV *restrict h_row_hv = (HV *)SvRV(h_row_sv);
8540             /* Case A: $i->{row} is a Hash Reference */
8541 3 100         if (SvTYPE(SvRV(i_row_sv)) == SVt_PVHV) {
8542 2           HV *restrict i_row_hv = (HV *)SvRV(i_row_sv);
8543             HE *restrict i_entry;
8544 2           hv_iterinit(i_row_hv);
8545 4 100         while ((i_entry = hv_iternext(i_row_hv))) {
8546 2           SV *restrict col_key_sv = hv_iterkeysv(i_entry);
8547 2           SV *restrict col_val = hv_iterval(i_row_hv, i_entry);
8548 2           hv_store_ent(h_row_hv, col_key_sv, SvREFCNT_inc(col_val), 0);
8549             }
8550 1 50         } else if (SvTYPE(SvRV(i_row_sv)) == SVt_PVAV) {
8551             // Case B: $i->{row} is an Array Reference
8552 1           AV *restrict i_row_av = (AV *)SvRV(i_row_sv);
8553             // av_len returns the top index (length - 1)
8554 1           SSize_t top_idx = av_len(i_row_av);
8555             // Iterate through the array in chunks of 2 (key-value pairs)
8556 3 100         for (SSize_t idx = 0; idx < top_idx; idx += 2) {
8557 2           SV **restrict key_svp = av_fetch(i_row_av, idx, 0);
8558 2           SV **restrict val_svp = av_fetch(i_row_av, idx + 1, 0);
8559             // Ensure both the key and value exist in the array
8560 2 50         if (key_svp && val_svp) {
    50          
8561 2           hv_store_ent(h_row_hv, *key_svp, SvREFCNT_inc(*val_svp), 0);
8562             }
8563             }
8564             }
8565             }
8566             }
8567             }
8568              
8569             void add_data(h_ref, i_ref)
8570             SV *h_ref;
8571             SV *i_ref;
8572             PREINIT:
8573 14           short int target_root_mode = 0; // 1 = Hash, 2 = Array
8574 14           short int i_root_mode = 0; // 1 = Hash, 2 = Array
8575 14           short int target_inner_mode = 0; // 0 = Unknown, 1 = Hash, 2 = Array
8576             CODE:
8577             // 1. Validate inputs (Allow both Hash and Array references at the root)
8578 14 100         if (!SvROK(h_ref) || (SvTYPE(SvRV(h_ref)) != SVt_PVHV && SvTYPE(SvRV(h_ref)) != SVt_PVAV)) {
    100          
    50          
8579 1           croak("1st argument to add_data must be a hash or array reference");
8580             }
8581 13 100         if (!SvROK(i_ref) || (SvTYPE(SvRV(i_ref)) != SVt_PVHV && SvTYPE(SvRV(i_ref)) != SVt_PVAV)) {
    100          
    50          
8582 1           croak("2nd argument to add_data must be a hash or array reference");
8583             }
8584 12 100         target_root_mode = (SvTYPE(SvRV(h_ref)) == SVt_PVHV) ? 1 : 2;
8585 12 100         i_root_mode = (SvTYPE(SvRV(i_ref)) == SVt_PVHV) ? 1 : 2;
8586             // Probe h_ref for inner structure
8587 12 100         if (target_root_mode == 1) {
8588 10           HV *restrict h_hv = (HV *)SvRV(h_ref);
8589 10 50         if (HvKEYS(h_hv) > 0) {
    100          
8590 8           HE **restrict probe_array = HvARRAY(h_hv);
8591 8           STRLEN probe_max = HvMAX(h_hv);
8592 54 50         for (STRLEN p_idx = 0; p_idx <= probe_max && target_inner_mode == 0; p_idx++) {
    100          
8593 54 100         for (HE *restrict p_entry = probe_array[p_idx]; p_entry && target_inner_mode == 0; p_entry = HeNEXT(p_entry)) {
    50          
8594 8           SV *restrict val = HeVAL(p_entry);
8595 8 50         if (SvROK(val)) {
8596 8 100         if (SvTYPE(SvRV(val)) == SVt_PVHV) target_inner_mode = 1;
8597 3 50         else if (SvTYPE(SvRV(val)) == SVt_PVAV) target_inner_mode = 2;
8598             }
8599             }
8600             }
8601             }
8602             } else {
8603 2           AV *restrict h_av = (AV *)SvRV(h_ref);
8604 2           SSize_t top = av_len(h_av);
8605 4 100         for (SSize_t p_idx = 0; p_idx <= top && target_inner_mode == 0; p_idx++) {
    50          
8606 2           SV **restrict svp = av_fetch(h_av, p_idx, 0);
8607 2 50         if (svp && *svp && SvROK(*svp)) {
    50          
    50          
8608 2 50         if (SvTYPE(SvRV(*svp)) == SVt_PVHV) target_inner_mode = 1;
8609 0 0         else if (SvTYPE(SvRV(*svp)) == SVt_PVAV) target_inner_mode = 2;
8610             }
8611             }
8612             }
8613             // Target is empty, infer intent from source hash/array
8614 12 100         if (target_inner_mode == 0) {
8615 2 50         if (i_root_mode == 1) {
8616 2           HV *restrict i_hv = (HV *)SvRV(i_ref);
8617 2 50         if (HvKEYS(i_hv) > 0) {
    50          
8618 2           HE **restrict probe_array = HvARRAY(i_hv);
8619 2           STRLEN probe_max = HvMAX(i_hv);
8620 14 50         for (STRLEN p_idx = 0; p_idx <= probe_max && target_inner_mode == 0; p_idx++) {
    100          
8621 14 100         for (HE *restrict p_entry = probe_array[p_idx]; p_entry && target_inner_mode == 0; p_entry = HeNEXT(p_entry)) {
    50          
8622 2           SV *restrict val = HeVAL(p_entry);
8623 2 50         if (SvROK(val)) {
8624 2 100         if (SvTYPE(SvRV(val)) == SVt_PVHV) target_inner_mode = 1;
8625 1 50         else if (SvTYPE(SvRV(val)) == SVt_PVAV) target_inner_mode = 2;
8626             }
8627             }
8628             }
8629             }
8630             } else {
8631 0           AV *restrict i_av = (AV *)SvRV(i_ref);
8632 0           SSize_t top = av_len(i_av);
8633 0 0         for (SSize_t p_idx = 0; p_idx <= top && target_inner_mode == 0; p_idx++) {
    0          
8634 0           SV **restrict svp = av_fetch(i_av, p_idx, 0);
8635 0 0         if (svp && *svp && SvROK(*svp)) {
    0          
    0          
8636 0 0         if (SvTYPE(SvRV(*svp)) == SVt_PVHV) target_inner_mode = 1;
8637 0 0         else if (SvTYPE(SvRV(*svp)) == SVt_PVAV) target_inner_mode = 2;
8638             }
8639             }
8640             }
8641             }
8642 12 50         if (target_inner_mode == 0) { target_inner_mode = 1; }
8643             // 2. Iterate through the SECONDARY structure ($i) using a unified loop
8644 12           SSize_t i_idx = 0, i_top = -1;
8645 12           HV *restrict i_hv = NULL;
8646 12           AV *restrict i_av = NULL;
8647 12 100         if (i_root_mode == 1) {
8648 10           i_hv = (HV *)SvRV(i_ref);
8649 10           hv_iterinit(i_hv);
8650             } else {
8651 2           i_av = (AV *)SvRV(i_ref);
8652 2           i_top = av_len(i_av);
8653             }
8654 24           while (1) {
8655 36           SV *restrict row_key_sv = NULL;
8656 36           SV *restrict i_row_sv = NULL;
8657 36           SSize_t current_idx = 0;
8658 36 100         if (i_root_mode == 1) {
8659 30           HE *restrict i_entry = hv_iternext(i_hv);
8660 30 100         if (!i_entry) break;
8661 20           row_key_sv = hv_iterkeysv(i_entry);
8662 20           i_row_sv = hv_iterval(i_hv, i_entry);
8663             // Prep integer index in case target is an Array (Suppress warnings for non-numeric string keys)
8664 20 100         current_idx = looks_like_number(row_key_sv) ? SvIV(row_key_sv) : -1;
8665             } else {
8666 6 100         if (i_idx > i_top) break;
8667 4           current_idx = i_idx++;
8668 4           SV **restrict svp = av_fetch(i_av, current_idx, 0);
8669 4 50         if (!svp || !*svp) continue;
    50          
8670 4           i_row_sv = *svp;
8671             // Prep string key in case target is a Hash
8672 4           row_key_sv = sv_2mortal(newSViv(current_idx));
8673             }
8674 24 100         if (SvROK(i_row_sv)) {
8675 23           SV *restrict h_row_sv = NULL;
8676 23           HV *restrict h_row_hv = NULL;
8677 23           AV *restrict h_row_av = NULL;
8678             // 3. Fetch from $h
8679 23 100         if (target_root_mode == 1) {
8680 18           HE *restrict h_fetch_he = hv_fetch_ent((HV *)SvRV(h_ref), row_key_sv, 0, 0);
8681 18 100         if (h_fetch_he) h_row_sv = HeVAL(h_fetch_he);
8682             } else {
8683 5 100         if (current_idx >= 0) {
8684 4           SV **restrict h_fetch_svp = av_fetch((AV *)SvRV(h_ref), current_idx, 0);
8685 4 100         if (h_fetch_svp && *h_fetch_svp) h_row_sv = *h_fetch_svp;
    50          
8686             }
8687             }
8688 23 100         if (h_row_sv && SvROK(h_row_sv)) {
    50          
8689 11 100         if (SvTYPE(SvRV(h_row_sv)) == SVt_PVHV) {
8690 7           h_row_hv = (HV *)SvRV(h_row_sv);
8691 4 50         } else if (SvTYPE(SvRV(h_row_sv)) == SVt_PVAV) {
8692 4           h_row_av = (AV *)SvRV(h_row_sv);
8693             }
8694             }
8695             // 4. Row DOES NOT exist (or is incompatible type): Create it matching target_inner_mode
8696 23 100         if (!h_row_hv && !h_row_av) {
    100          
8697 12 100         if (target_inner_mode == 2) {
8698 3           h_row_av = newAV();
8699 3           h_row_sv = newRV_noinc((SV *)h_row_av);
8700             } else {
8701 9           h_row_hv = newHV();
8702 9           h_row_sv = newRV_noinc((SV *)h_row_hv);
8703             }
8704 12 100         if (target_root_mode == 1) {
8705 9           hv_store_ent((HV *)SvRV(h_ref), row_key_sv, h_row_sv, 0);
8706             } else {
8707 3 100         if (current_idx >= 0) {
8708 2           av_store((AV *)SvRV(h_ref), current_idx, h_row_sv);
8709             }
8710             }
8711             }
8712             // 5. Merge data across potentially mismatched inner structures
8713 23 100         if (h_row_hv) {
8714 16 100         if (SvTYPE(SvRV(i_row_sv)) == SVt_PVHV) {
8715             // Hash into Hash (Direct copy)
8716 12           HV *restrict i_inner_hv = (HV *)SvRV(i_row_sv);
8717             HE *restrict i_inner_entry;
8718 12           hv_iterinit(i_inner_hv);
8719 25 100         while ((i_inner_entry = hv_iternext(i_inner_hv))) {
8720 13           SV *restrict col_key_sv = hv_iterkeysv(i_inner_entry);
8721 13           SV *restrict col_val = hv_iterval(i_inner_hv, i_inner_entry);
8722 13           hv_store_ent(h_row_hv, col_key_sv, SvREFCNT_inc(col_val), 0);
8723             }
8724 4 50         } else if (SvTYPE(SvRV(i_row_sv)) == SVt_PVAV) {
8725             // Array into Hash (Read pairs)
8726 4           AV *restrict i_inner_av = (AV *)SvRV(i_row_sv);
8727 4           SSize_t inner_top_idx = av_len(i_inner_av);
8728 10 100         for (SSize_t idx = 0; idx < inner_top_idx; idx += 2) {
8729 6           SV **restrict key_svp = av_fetch(i_inner_av, idx, 0);
8730 6           SV **restrict val_svp = av_fetch(i_inner_av, idx + 1, 0);
8731 6 50         if (key_svp && *key_svp && val_svp) {
    50          
    50          
8732 6 50         SV *restrict val_to_store = *val_svp ? *val_svp : &PL_sv_undef;
8733 6           hv_store_ent(h_row_hv, *key_svp, SvREFCNT_inc(val_to_store), 0);
8734             }
8735             }
8736             }
8737 7 50         } else if (h_row_av) {
8738 7 100         if (SvTYPE(SvRV(i_row_sv)) == SVt_PVAV) {
8739             // Array into Array (Direct push with non-null pointer assurance)
8740 5           AV *restrict i_inner_av = (AV *)SvRV(i_row_sv);
8741 5           SSize_t inner_top_idx = av_len(i_inner_av);
8742 16 100         for (SSize_t idx = 0; idx <= inner_top_idx; ++idx) {
8743 11           SV **restrict val_svp = av_fetch(i_inner_av, idx, 0);
8744 11 50         if (val_svp) {
8745 11 50         SV *restrict val_to_push = *val_svp ? *val_svp : &PL_sv_undef;
8746 11           SV *restrict sv_inc = SvREFCNT_inc(val_to_push);
8747 11 50         if (sv_inc) {
8748 11           av_push(h_row_av, sv_inc);
8749             }
8750             }
8751             }
8752 2 50         } else if (SvTYPE(SvRV(i_row_sv)) == SVt_PVHV) {
8753             // Hash into Array (Flatten and push pairs with non-null pointer assurance)
8754 2           HV *restrict i_inner_hv = (HV *)SvRV(i_row_sv);
8755             HE *restrict i_inner_entry;
8756 2           hv_iterinit(i_inner_hv);
8757 4 100         while ((i_inner_entry = hv_iternext(i_inner_hv))) {
8758 2           SV *restrict col_key_sv = hv_iterkeysv(i_inner_entry);
8759 2           SV *restrict col_val = hv_iterval(i_inner_hv, i_inner_entry);
8760 2 50         if (col_key_sv && col_val) {
    50          
8761 2           SV *restrict sv_key_inc = SvREFCNT_inc(col_key_sv);
8762 2           SV *restrict sv_val_inc = SvREFCNT_inc(col_val);
8763 2 50         if (sv_key_inc && sv_val_inc) {
    50          
8764 2           av_push(h_row_av, sv_key_inc);
8765 2           av_push(h_row_av, sv_val_inc);
8766             }
8767             }
8768             }
8769             }
8770             }
8771             }
8772             }
8773              
8774             SV* value_counts(...)
8775             PREINIT:
8776             HV*restrict counts_hv;
8777             SV*restrict arg1;
8778             CODE:
8779             // 1. CHECK FOR DATA FIRST to prevent memory leaks if we die
8780 11 100         if (items == 0) {
8781 1           croak("value_counts: no data provided. At least one argument is required.");
8782             }
8783 10           arg1 = ST(0);
8784 10 100         if (!SvOK(arg1)) {
8785 1           croak("First argument to value_counts is NOT defined");
8786             }
8787             // 2. Allocate memory only after we know we are proceeding
8788 9           counts_hv = newHV();
8789             // CASE 1: Flattened Array (or single scalar)
8790 9 100         if (!SvROK(arg1)) {
8791 6 100         for (unsigned i = 0; i < items; i++) {
8792 4           increment_count(aTHX_ counts_hv, ST(i));
8793             }
8794             } else {// CASE 2: Array Reference
8795 7           SV*restrict rv = SvRV(arg1);
8796 7 100         if (SvTYPE(rv) == SVt_PVAV) {
8797 1           AV*restrict av = (AV*)rv;
8798 1           SSize_t len = av_len(av) + 1;
8799 4 100         for (unsigned i = 0; i < len; i++) {
8800 3           SV**restrict valp = av_fetch(av, i, 0);
8801 3 50         if (valp) increment_count(aTHX_ counts_hv, *valp);
8802             }
8803 6 50         } else if (SvTYPE(rv) == SVt_PVHV) { // CASES 3, 4, 5: Hash Reference
8804 6           HV*restrict hv = (HV*)rv;
8805             // CASES 4 & 5: Nested Structure requiring a 2nd Argument
8806 6 100         if (items > 1) {
8807 3           SV*restrict arg2 = ST(1);
8808             STRLEN klen;
8809 3           const char*restrict key = SvPV(arg2, klen);
8810             // DataFrame-style Column-Oriented data check
8811 3           SV**restrict col_svp = hv_fetch(hv, key, klen, 0);
8812 4 100         if (col_svp && SvROK(*col_svp) && SvTYPE(SvRV(*col_svp)) == SVt_PVAV) {
    50          
    50          
8813 1           AV*restrict av = (AV*)SvRV(*col_svp);
8814 1           SSize_t len = av_len(av) + 1;
8815 4 100         for (unsigned i = 0; i < len; i++) {
8816 3           SV**restrict valp = av_fetch(av, i, 0);
8817 3 50         if (valp) increment_count(aTHX_ counts_hv, *valp);
8818             }
8819             } else {
8820             // Fallback: Row-Oriented nested structure
8821             HE*restrict he;
8822 2           hv_iterinit(hv);
8823 8 100         while ((he = hv_iternext(hv))) {
8824 6           SV*restrict inner_sv = HeVAL(he);
8825 6 50         if (SvROK(inner_sv)) {
8826 6           SV*restrict inner_rv = SvRV(inner_sv);
8827 6 50         if (SvTYPE(inner_rv) == SVt_PVHV) {// CASE 5: Hash of Hashes
8828 6           HV*restrict inner_hv = (HV*)inner_rv;
8829 6           SV**restrict valp = hv_fetch(inner_hv, key, klen, 0);
8830 6 100         if (valp) increment_count(aTHX_ counts_hv, *valp);
8831 0 0         } else if (SvTYPE(inner_rv) == SVt_PVAV) {// CASE 4: Hash of Arrays (Row-Oriented)
8832 0 0         if (looks_like_number(arg2)) {
8833 0           AV*restrict inner_av = (AV*)inner_rv;
8834 0           SSize_t idx = SvIV(arg2);
8835 0           SV**restrict valp = av_fetch(inner_av, idx, 0);
8836 0 0         if (valp) increment_count(aTHX_ counts_hv, *valp);
8837             }
8838             }
8839             }
8840             }
8841             }
8842             } else { // CASE 3: Hash Reference (No 2nd argument)
8843             HE*restrict he;
8844 3           hv_iterinit(hv);
8845 11 100         while ((he = hv_iternext(hv))) {
8846 8           SV*restrict val = HeVAL(he);
8847 8 100         if (SvROK(val)) {// --- SAFETY CHECK
8848 5           SV*restrict inner_rv = SvRV(val);
8849             // If it's a Hash of Arrays, count ALL elements in the inner arrays
8850 5 100         if (SvTYPE(inner_rv) == SVt_PVAV) {
8851 2           AV*restrict inner_av = (AV*)inner_rv;
8852 2           SSize_t len = av_len(inner_av) + 1;
8853 8 100         for (unsigned i = 0; i < len; i++) {
8854 6           SV**restrict valp = av_fetch(inner_av, i, 0);
8855 6 50         if (valp) increment_count(aTHX_ counts_hv, *valp);
8856             }
8857 3 50         } else if (SvTYPE(inner_rv) == SVt_PVHV) {
8858             // If it's a Hash of Hashes, count ALL elements across all inner keys
8859 3           HV*restrict inner_hv = (HV*)inner_rv;
8860             HE*restrict inner_he;
8861 3           hv_iterinit(inner_hv);
8862 7 100         while ((inner_he = hv_iternext(inner_hv))) {
8863 4           SV*restrict inner_val = HeVAL(inner_he);
8864 4           increment_count(aTHX_ counts_hv, inner_val);
8865             }
8866             } else { /* Unrecognized nested reference type */
8867 0           SvREFCNT_dec((SV*)counts_hv);
8868 0           croak("value_counts: Unsupported nested reference type.");
8869             }
8870             } else {
8871             /* Simple scalar value */
8872 3           increment_count(aTHX_ counts_hv, val);
8873             }
8874             }
8875             }
8876             } else {
8877             /* Safely decrement the reference count of our hash before dying to prevent a leak */
8878 0           SvREFCNT_dec((SV*)counts_hv);
8879 0           croak("value_counts: Unsupported reference type.");
8880             }
8881             }
8882 9           RETVAL = newRV_noinc((SV*)counts_hv);
8883             OUTPUT:
8884             RETVAL
8885              
8886             #define EVAL_FILTER(sub_sv, val_sv, keep) do { \
8887             dSP; \
8888             unsigned int count; \
8889             SV *restrict _ef_arg = (val_sv) ? (val_sv) : &PL_sv_undef; \
8890             ENTER; \
8891             SAVETMPS; \
8892             SAVE_DEFSV; \
8893             SvREFCNT_inc(_ef_arg); /* Prevent LEAVE from stealing the refcount */ \
8894             DEFSV_set(_ef_arg); \
8895             PUSHMARK(SP); \
8896             XPUSHs(_ef_arg); \
8897             PUTBACK; \
8898             count = call_sv(sub_sv, G_SCALAR | G_EVAL); \
8899             SPAGAIN; \
8900             if (SvTRUE(ERRSV)) { FREETMPS; LEAVE; croak(NULL); } \
8901             if (count > 0) { \
8902             SV *restrict ret_sv = POPs; \
8903             keep = SvTRUE(ret_sv); \
8904             } else { \
8905             keep = 0; \
8906             } \
8907             PUTBACK; \
8908             FREETMPS; \
8909             LEAVE; \
8910             } while (0)
8911              
8912             SV *group_by(data_ref, target_key_sv, group_key_sv, ...)
8913             SV *data_ref;
8914             SV *target_key_sv;
8915             SV *group_key_sv;
8916             PREINIT:
8917             HV *restrict result_hv;
8918 8           HV *restrict filter_hv = NULL;
8919             SV *restrict result_ref;
8920             CODE:
8921 8 100         if (!SvOK(data_ref)) {
8922 1           croak("First argument to group_by is NOT defined");
8923             }
8924 7 100         if (!SvOK(target_key_sv)) {
8925 1           croak("Second argument to group_by is NOT defined");
8926             }
8927 6 100         if (!SvOK(group_key_sv)) {
8928 1           croak("Third argument to group_by is NOT defined");
8929             }
8930             /* 1. Validate the primary input is a reference */
8931 5 50         if (!SvROK(data_ref)) {
8932 0           croak("First argument to group_by must be a reference (Array of Hashes, Hash of Arrays, or Hash of Hashes)");
8933             }
8934 5 100         if (items > 3) { /* Capture the optional filter argument */
8935 2           SV *restrict filter_ref = ST(3);
8936 2 50         if (SvROK(filter_ref) && SvTYPE(SvRV(filter_ref)) == SVt_PVHV) {
    50          
8937 2           filter_hv = (HV *)SvRV(filter_ref);
8938             }
8939             }
8940 5           result_hv = newHV(); /* 2. Allocate the hash that we will return */
8941             /* Mortalize immediately! If the callback croaks, the tmps stack
8942             * will safely clean this up. */
8943 5           result_ref = sv_2mortal(newRV_noinc((SV *)result_hv));
8944 5 100         if (SvTYPE(SvRV(data_ref)) == SVt_PVAV) { /* Input is an Array of Hashes (AoH) */
8945 2           AV *restrict data_av = (AV *)SvRV(data_ref);
8946 2           SSize_t len = av_len(data_av) + 1;
8947 10 100         for (SSize_t i = 0; i < len; i++) {
8948 8           SV **restrict row_svp = av_fetch(data_av, i, 0);
8949 8 50         if (row_svp && SvROK(*row_svp) && SvTYPE(SvRV(*row_svp)) == SVt_PVHV) {
    50          
    50          
8950 8           HV *restrict row_hv = (HV *)SvRV(*row_svp);
8951 8           HE *restrict group_he = hv_fetch_ent(row_hv, group_key_sv, 0, 0);
8952 8           HE *restrict target_he = hv_fetch_ent(row_hv, target_key_sv, 0, 0);
8953 8 50         if (group_he) {
8954 8           SV *restrict group_val = HeVAL(group_he);
8955 8 100         SV *restrict target_val = target_he ? HeVAL(target_he) : NULL;
8956 8 100         if (target_val && SvOK(target_val)) {
    50          
8957 7           bool pass_filter = 1;
8958 7 100         if (filter_hv) {
8959             HE *restrict f_he;
8960 4           hv_iterinit(filter_hv);
8961 6 100         while ((f_he = hv_iternext(filter_hv))) {
8962 4           SV *restrict f_col = hv_iterkeysv(f_he);
8963 4           SV *restrict f_sub = hv_iterval(filter_hv, f_he);
8964 4           HE *restrict val_he = hv_fetch_ent(row_hv, f_col, 0, 0);
8965 4 50         SV *restrict val_sv = val_he ? HeVAL(val_he) : NULL;
8966             bool keep;
8967 4 50         EVAL_FILTER(f_sub, val_sv, keep);
    50          
    50          
    50          
    50          
    0          
    50          
    50          
8968 4 100         if (!keep) {
8969 2           pass_filter = 0;
8970 2           break;
8971             }
8972             }
8973             }
8974 7 100         if (pass_filter) {
8975 5           HE *restrict res_he = hv_fetch_ent(result_hv, group_val, 0, 0);
8976             AV *restrict res_av;
8977 5 100         if (res_he) {
8978 1           res_av = (AV *)SvRV(HeVAL(res_he));
8979             } else {
8980 4           res_av = newAV();
8981 4           hv_store_ent(result_hv, group_val, newRV_noinc((SV *)res_av), 0);
8982             }
8983 5           av_push(res_av, newSVsv(target_val));
8984             }
8985             }
8986             }
8987             }
8988             }
8989 3 50         } else if (SvTYPE(SvRV(data_ref)) == SVt_PVHV) {
8990 3           HV *restrict data_hv = (HV *)SvRV(data_ref);
8991 3           HE *restrict group_he = hv_fetch_ent(data_hv, group_key_sv, 0, 0);
8992 3           HE *restrict target_he = hv_fetch_ent(data_hv, target_key_sv, 0, 0);
8993 3 100         if (group_he && target_he &&
    50          
8994 2 50         SvROK(HeVAL(group_he)) && SvTYPE(SvRV(HeVAL(group_he))) == SVt_PVAV &&
    50          
8995 4 50         SvROK(HeVAL(target_he)) && SvTYPE(SvRV(HeVAL(target_he))) == SVt_PVAV) {
    50          
8996 2           AV *restrict group_av = (AV *)SvRV(HeVAL(group_he));
8997 2           AV *restrict target_av = (AV *)SvRV(HeVAL(target_he));
8998 2           SSize_t g_len = av_len(group_av) + 1;
8999 2           SSize_t t_len = av_len(target_av) + 1;
9000 2           SSize_t len = g_len < t_len ? g_len : t_len;
9001 10 100         for (SSize_t i = 0; i < len; i++) {
9002 8           SV **restrict g_svp = av_fetch(group_av, i, 0);
9003 8           SV **restrict t_svp = av_fetch(target_av, i, 0);
9004 8 50         if (g_svp && *g_svp) {
    50          
9005 8           SV *restrict g_val = *g_svp;
9006 8 50         SV *restrict t_val = (t_svp && *t_svp) ? *t_svp : NULL;
    50          
9007 8 50         if (t_val && SvOK(t_val)) {
    100          
9008 7           bool pass_filter = 1;
9009 7 100         if (filter_hv) {
9010             HE *restrict f_he;
9011 4           hv_iterinit(filter_hv);
9012 6 100         while ((f_he = hv_iternext(filter_hv))) {
9013 4           SV *restrict f_col = hv_iterkeysv(f_he);
9014 4           SV *restrict f_sub = hv_iterval(filter_hv, f_he);
9015 4           SV *restrict val_sv = NULL;
9016 4           HE *restrict arr_he = hv_fetch_ent(data_hv, f_col, 0, 0);
9017 4 50         if (arr_he && SvROK(HeVAL(arr_he)) && SvTYPE(SvRV(HeVAL(arr_he))) == SVt_PVAV) {
    50          
    50          
9018 4           AV *restrict col_av = (AV *)SvRV(HeVAL(arr_he));
9019 4           SV **restrict val_svp = av_fetch(col_av, i, 0);
9020 4 50         if (val_svp) val_sv = *val_svp;
9021             }
9022             bool keep;
9023 4 50         EVAL_FILTER(f_sub, val_sv, keep);
    50          
    50          
    50          
    50          
    0          
    50          
    50          
9024 4 100         if (!keep) {
9025 2           pass_filter = 0;
9026 2           break;
9027             }
9028             }
9029             }
9030 7 100         if (pass_filter) {
9031 5           HE *restrict res_he = hv_fetch_ent(result_hv, g_val, 0, 0);
9032             AV *restrict res_av;
9033 5 100         if (res_he) {
9034 1           res_av = (AV *)SvRV(HeVAL(res_he));
9035             } else {
9036 4           res_av = newAV();
9037 4           hv_store_ent(result_hv, g_val, newRV_noinc((SV *)res_av), 0);
9038             }
9039 5           av_push(res_av, newSVsv(t_val));
9040             }
9041             }
9042             }
9043             }
9044             } else {
9045             HE *restrict row_he;
9046 1           hv_iterinit(data_hv);
9047 6 100         while ((row_he = hv_iternext(data_hv))) {
9048 5           SV *restrict row_val = hv_iterval(data_hv, row_he);
9049 5 50         if (SvROK(row_val) && SvTYPE(SvRV(row_val)) == SVt_PVHV) {
    50          
9050 5           HV *restrict inner_hv = (HV *)SvRV(row_val);
9051 5           HE *restrict inner_group_he = hv_fetch_ent(inner_hv, group_key_sv, 0, 0);
9052 5           HE *restrict inner_target_he = hv_fetch_ent(inner_hv, target_key_sv, 0, 0);
9053 5 50         if (inner_group_he) {
9054 5           SV *restrict g_val = HeVAL(inner_group_he);
9055 5 100         SV *restrict t_val = inner_target_he ? HeVAL(inner_target_he) : NULL;
9056 5 100         if (t_val && SvOK(t_val)) {
    100          
9057 3           bool pass_filter = 1;
9058 3 50         if (filter_hv) {
9059             HE *restrict f_he;
9060 0           hv_iterinit(filter_hv);
9061 0 0         while ((f_he = hv_iternext(filter_hv))) {
9062 0           SV *restrict f_col = hv_iterkeysv(f_he);
9063 0           SV *restrict f_sub = hv_iterval(filter_hv, f_he);
9064 0           HE *restrict val_he = hv_fetch_ent(inner_hv, f_col, 0, 0);
9065 0 0         SV *restrict val_sv = val_he ? HeVAL(val_he) : NULL;
9066             bool keep;
9067 0 0         EVAL_FILTER(f_sub, val_sv, keep);
    0          
    0          
    0          
    0          
    0          
    0          
    0          
9068 0 0         if (!keep) {
9069 0           pass_filter = 0;
9070 0           break;
9071             }
9072             }
9073             }
9074 3 50         if (pass_filter) {
9075 3           HE *restrict res_he = hv_fetch_ent(result_hv, g_val, 0, 0);
9076             AV *restrict res_av;
9077 3 100         if (res_he) {
9078 1           res_av = (AV *)SvRV(HeVAL(res_he));
9079             } else {
9080 2           res_av = newAV();
9081 2           hv_store_ent(result_hv, g_val, newRV_noinc((SV *)res_av), 0);
9082             }
9083 3           av_push(res_av, newSVsv(t_val));
9084             }
9085             }
9086             }
9087             }
9088             }
9089             }
9090             } else {
9091 0           croak("First argument to group_by must be an Array or Hash reference");
9092             }
9093             // Balance xsubpp's automatic sv_2mortal to prevent refcount dropping to -1
9094 5           RETVAL = SvREFCNT_inc(result_ref);
9095             OUTPUT:
9096             RETVAL
9097              
9098             SV* prcomp(...)
9099             CODE:
9100             {
9101 12           SV *restrict x_sv = NULL;
9102 12           bool retx = TRUE, center = TRUE, do_scale = FALSE;
9103 12           NV tol = -1.0;
9104 12           long rank_opt = -1;
9105 12           unsigned int arg_idx = 0;
9106             // 1. Shift positional 'x' argument if provided
9107 12 100         if (arg_idx < items && SvROK(ST(arg_idx))) {
    100          
9108 10           int t = SvTYPE(SvRV(ST(arg_idx)));
9109 10 100         if (t == SVt_PVAV || t == SVt_PVHV) {
    50          
9110 10           x_sv = ST(arg_idx);
9111 10           arg_idx++;
9112             }
9113             }
9114             // 2. Parse named arguments
9115 12 100         if ((items - arg_idx) % 2 != 0) croak("Usage: prcomp($data, key => value, ...)");
9116 14 100         for (; arg_idx < items; arg_idx += 2) {
9117 4           const char *restrict key = SvPV_nolen(ST(arg_idx));
9118 4           SV *restrict val = ST(arg_idx + 1);
9119 4 50         if (strEQ(key, "x")) x_sv = val;
9120 4 50         else if (strEQ(key, "retx")) retx = SvTRUE(val);
9121 4 50         else if (strEQ(key, "center")) center = SvTRUE(val);
9122 4 100         else if (strEQ(key, "scale")) do_scale = SvTRUE(val);
9123 2 100         else if (strEQ(key, "tol")) tol = SvOK(val) ? SvNV(val) : -1.0;
    50          
9124 1 50         else if (strEQ(key, "rank")) rank_opt = SvOK(val) ? (long)SvIV(val) : -1;
    50          
9125 0           else croak("prcomp: unknown argument '%s'", key);
9126             }
9127              
9128 10 100         if (!x_sv || !SvROK(x_sv))
    50          
9129 1           croak("prcomp: 'x' is a required argument and must be a reference");
9130              
9131             // 3. Detect Data Structure (AoA, HoA, HoH)
9132 9           bool is_aoa = FALSE, is_hoa = FALSE, is_hoh = FALSE;
9133 9           size_t n_raw = 0, p = 0;
9134 9           char **restrict colnames = NULL;
9135 9           SV *restrict ref = SvRV(x_sv);
9136              
9137 9 100         if (SvTYPE(ref) == SVt_PVAV) {
9138 7           AV *restrict av = (AV*)ref;
9139 7           n_raw = av_len(av) + 1;
9140 7 100         if (n_raw > 0) {
9141 6           SV **restrict first = av_fetch(av, 0, 0);
9142 6 50         if (first && SvROK(*first) && SvTYPE(SvRV(*first)) == SVt_PVAV) {
    50          
    50          
9143 6           is_aoa = TRUE;
9144 6           p = av_len((AV*)SvRV(*first)) + 1;
9145 0           } else croak("prcomp: Array reference must contain ArrayRefs (AoA)");
9146             }
9147 2 50         } else if (SvTYPE(ref) == SVt_PVHV) {
9148 2           HV *restrict hv = (HV*)ref;
9149 2 50         if (hv_iterinit(hv) > 0) {
9150 2           HE *restrict entry = hv_iternext(hv);
9151 2           SV *restrict val = hv_iterval(hv, entry);
9152 2 50         if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVAV) {
    100          
9153 1           is_hoa = TRUE;
9154 1           n_raw = av_len((AV*)SvRV(val)) + 1;
9155 1 50         } else if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVHV) {
    50          
9156 1           is_hoh = TRUE;
9157 1           n_raw = hv_iterinit(hv);
9158 0           } else croak("prcomp: Hash reference must contain ArrayRefs (HoA) or HashRefs (HoH)");
9159             }
9160             }
9161              
9162 9 100         if (n_raw == 0 || (p == 0 && !is_hoa && !is_hoh)) croak("prcomp: input matrix is empty or has zero columns");
    100          
    100          
    50          
9163              
9164             // 4. Extract and Sort Column Names (for Hash inputs)
9165 8 100         if (is_hoh) {
9166 1           HV *restrict hv = (HV*)ref;
9167 1           hv_iterinit(hv);
9168 1           HE *restrict entry = hv_iternext(hv);
9169 1           HV *restrict inner = (HV*)SvRV(hv_iterval(hv, entry));
9170 1           p = hv_iterinit(inner);
9171 1 50         if (p == 0) croak("prcomp: inner hashes cannot be empty");
9172              
9173 1           colnames = (char**)safemalloc(p * sizeof(char*));
9174 1           size_t c = 0;
9175 3 100         while ((entry = hv_iternext(inner))) {
9176 2           colnames[c++] = savepv(SvPV_nolen(hv_iterkeysv(entry)));
9177             }
9178 1           qsort(colnames, p, sizeof(char*), cmp_string_wt);
9179 7 100         } else if (is_hoa) {
9180 1           HV *restrict hv = (HV*)ref;
9181 1           p = hv_iterinit(hv);
9182 1 50         if (p == 0) croak("prcomp: input hash is empty");
9183 1           colnames = (char**)safemalloc(p * sizeof(char*));
9184 1           size_t c = 0;
9185             HE *restrict entry;
9186 3 100         while ((entry = hv_iternext(hv))) {
9187 2           colnames[c++] = savepv(SvPV_nolen(hv_iterkeysv(entry)));
9188             }
9189 1           qsort(colnames, p, sizeof(char*), cmp_string_wt);
9190             }
9191             // 5. Extract data & apply listwise deletion for NaNs
9192 8           NV *restrict X_mat = (NV*)safemalloc(n_raw * p * sizeof(NV));
9193 8           size_t n = 0;
9194 8 100         if (is_aoa) {
9195 6           AV *restrict av = (AV*)ref;
9196 24 100         for (size_t i = 0; i < n_raw; i++) {
9197 18           SV **restrict row_sv = av_fetch(av, i, 0);
9198 18 50         if (row_sv && SvROK(*row_sv) && SvTYPE(SvRV(*row_sv)) == SVt_PVAV) {
    50          
    50          
9199 18           AV *restrict row_av = (AV*)SvRV(*row_sv);
9200 18           bool row_ok = TRUE;
9201 54 100         for (size_t j = 0; j < p; j++) {
9202 36           SV **restrict cell_sv = av_fetch(row_av, j, 0);
9203 71 50         if (cell_sv && SvOK(*cell_sv) && looks_like_number(*cell_sv)) {
    50          
    100          
9204 35           NV v = SvNV(*cell_sv);
9205 35 50         if (!isfinite(v)) row_ok = FALSE;
9206 35           else X_mat[n * p + j] = v;
9207 1           } else row_ok = FALSE;
9208             }
9209 18 100         if (row_ok) n++;
9210             }
9211             }
9212 2 100         } else if (is_hoa) {
9213 1           HV *restrict hv = (HV*)ref;
9214 1           AV **restrict col_arrays = (AV**)safemalloc(p * sizeof(AV*));
9215 3 100         for (size_t j = 0; j < p; j++) {
9216 2           SV **restrict val = hv_fetch(hv, colnames[j], strlen(colnames[j]), 0);
9217 2           col_arrays[j] = (AV*)SvRV(*val);
9218             }
9219 4 100         for (size_t i = 0; i < n_raw; i++) {
9220 3           bool row_ok = TRUE;
9221 9 100         for (size_t j = 0; j < p; j++) {
9222 6           SV **restrict cell = av_fetch(col_arrays[j], i, 0);
9223 12 50         if (cell && SvOK(*cell) && looks_like_number(*cell)) {
    50          
    50          
9224 6           NV v = SvNV(*cell);
9225 6 50         if (!isfinite(v)) row_ok = FALSE;
9226 6           else X_mat[n * p + j] = v;
9227 0           } else row_ok = FALSE;
9228             }
9229 3 50         if (row_ok) n++;
9230             }
9231 1           Safefree(col_arrays);
9232 1 50         } else if (is_hoh) {
9233 1           HV *restrict hv = (HV*)ref;
9234 1           hv_iterinit(hv);
9235             HE *restrict entry;
9236 4 100         while ((entry = hv_iternext(hv))) {
9237 3           HV *restrict row_hv = (HV*)SvRV(hv_iterval(hv, entry));
9238 3           bool row_ok = TRUE;
9239 9 100         for (size_t j = 0; j < p; j++) {
9240 6           SV **restrict cell = hv_fetch(row_hv, colnames[j], strlen(colnames[j]), 0);
9241 12 50         if (cell && SvOK(*cell) && looks_like_number(*cell)) {
    50          
    50          
9242 6           NV v = SvNV(*cell);
9243 6 50         if (!isfinite(v)) row_ok = FALSE;
9244 6           else X_mat[n * p + j] = v;
9245 0           } else row_ok = FALSE;
9246             }
9247 3 50         if (row_ok) n++;
9248             }
9249             }
9250 8 50         if (n == 0) {
9251 0 0         if (colnames) {
9252 0 0         for (size_t i = 0; i < p; i++) Safefree(colnames[i]);
9253 0           Safefree(colnames);
9254             }
9255 0           Safefree(X_mat);
9256 0           croak("prcomp: 0 valid observations after listwise NA deletion");
9257             }
9258             // 6. Center and Scale
9259 8           NV *restrict cen_vec = (NV*)safecalloc(p, sizeof(NV));
9260 8           NV *restrict sc_vec = (NV*)safecalloc(p, sizeof(NV));
9261 22 100         for (size_t j = 0; j < p; j++) {
9262 15           NV col_sum = 0.0;
9263 58 100         for (size_t i = 0; i < n; i++) col_sum += X_mat[i * p + j];
9264 15 50         if (center) {
9265 15           cen_vec[j] = col_sum / n;
9266 58 100         for (size_t i = 0; i < n; i++) X_mat[i * p + j] -= cen_vec[j];
9267             }
9268 15 100         if (do_scale) {
9269 3           NV sum_sq = 0.0;
9270 12 100         for (size_t i = 0; i < n; i++) {
9271 9 50         NV val = X_mat[i * p + j] - (center ? 0 : (col_sum / n));
9272 9           sum_sq += val * val;
9273             }
9274 3 50         sc_vec[j] = (n > 1) ? sqrt(sum_sq / (n - 1)) : 0.0;
9275 3 100         if (sc_vec[j] <= 1e-15) {
9276 1           Safefree(X_mat); Safefree(cen_vec); Safefree(sc_vec);
9277 1 50         if (colnames) { for (size_t k = 0; k < p; k++) Safefree(colnames[k]); Safefree(colnames); }
    0          
9278 1           croak("prcomp: cannot rescale a constant/zero column to unit variance");
9279             }
9280 8 100         for (size_t i = 0; i < n; i++) X_mat[i * p + j] /= sc_vec[j];
9281             }
9282             }
9283             // 7. Construct Covariance Matrix X^T X
9284 7           NV *restrict XtX = (NV*)safecalloc(p * p, sizeof(NV));
9285 27 100         for (size_t i = 0; i < n; i++) {
9286 60 100         for (size_t j = 0; j < p; j++) {
9287 100 100         for (size_t k = j; k < p; k++) {
9288 60           XtX[j * p + k] += X_mat[i * p + j] * X_mat[i * p + k];
9289             }
9290             }
9291             }
9292             // Mirror the symmetric lower triangle
9293 21 100         for (size_t j = 0; j < p; j++) {
9294 21 100         for (size_t k = 0; k < j; k++) {
9295 7           XtX[j * p + k] = XtX[k * p + j];
9296             }
9297             }
9298             // 8. Jacobi Eigen Decomposition
9299 7           NV *restrict eigen_val = (NV*)safemalloc(p * sizeof(NV));
9300 7           NV *restrict eigen_vec = (NV*)safemalloc(p * p * sizeof(NV));
9301 7           jacobi_eigen(XtX, p, eigen_val, eigen_vec);
9302             // 9. Calculate singular values (sdev) & handle dimensions (rank/tol)
9303 7           size_t k_cols = (n < p) ? n : p;
9304 7 100         if (rank_opt > 0 && rank_opt < (long)k_cols) k_cols = (size_t)rank_opt;
    50          
9305 7           NV *restrict sdev = (NV*)safemalloc(k_cols * sizeof(NV));
9306 7 50         NV n_adj = (n > 1) ? (NV)(n - 1) : 1.0;
9307 20 100         for (size_t j = 0; j < k_cols; j++) {
9308 13           NV e_val = eigen_val[j];
9309 13 50         if (e_val < 0.0) e_val = 0.0; // clamp floating point inaccuracy
9310 13           sdev[j] = sqrt(e_val / n_adj);
9311             }
9312 7 100         if (tol >= 0.0) {
9313 1           size_t rank_est = 0;
9314 1           NV threshold = sdev[0] * tol;
9315 3 100         for (size_t j = 0; j < k_cols; j++) {
9316 2 100         if (sdev[j] > threshold) rank_est++;
9317             }
9318 1 50         if (rank_est < k_cols) k_cols = rank_est;
9319             }
9320             // 10. Build Return Hash
9321 7           HV *restrict res_hv = newHV();
9322 7           AV *restrict sdev_av = newAV();
9323 19 100         for (size_t j = 0; j < k_cols; j++) av_push(sdev_av, newSVnv(sdev[j]));
9324 7           hv_stores(res_hv, "sdev", newRV_noinc((SV*)sdev_av));
9325 7           AV *restrict rot_av = newAV();
9326 21 100         for (size_t j = 0; j < p; j++) {
9327 14           AV *restrict row_rot = newAV();
9328 38 100         for (size_t m = 0; m < k_cols; m++) {
9329 24           av_push(row_rot, newSVnv(eigen_vec[j * p + m]));
9330             }
9331 14           av_push(rot_av, newRV_noinc((SV*)row_rot));
9332             }
9333 7           hv_stores(res_hv, "rotation", newRV_noinc((SV*)rot_av));
9334 7 50         if (retx) {
9335 7           AV *restrict x_ret_av = newAV();
9336 27 100         for (size_t i = 0; i < n; i++) {
9337 20           AV *restrict row_x = newAV();
9338 54 100         for (size_t m = 0; m < k_cols; m++) {
9339 34           NV x_rot_val = 0.0;
9340 102 100         for (size_t c = 0; c < p; c++) {
9341 68           x_rot_val += X_mat[i * p + c] * eigen_vec[c * p + m];
9342             }
9343 34           av_push(row_x, newSVnv(x_rot_val));
9344             }
9345 20           av_push(x_ret_av, newRV_noinc((SV*)row_x));
9346             }
9347 7           hv_stores(res_hv, "x", newRV_noinc((SV*)x_ret_av));
9348             }
9349 7 100         if (colnames) {
9350 2           AV *restrict names_av = newAV();
9351 6 100         for (size_t j = 0; j < p; j++) {
9352 4           av_push(names_av, newSVpv(colnames[j], 0));
9353             }
9354 2           hv_stores(res_hv, "varnames", newRV_noinc((SV*)names_av));
9355             }
9356 7 50         if (center) {
9357 7           AV *restrict c_av = newAV();
9358 21 100         for (size_t j = 0; j < p; j++) av_push(c_av, newSVnv(cen_vec[j]));
9359 7           hv_stores(res_hv, "center", newRV_noinc((SV*)c_av));
9360             } else {
9361 0           hv_stores(res_hv, "center", newSVsv(&PL_sv_no));
9362             }
9363 7 100         if (do_scale) {
9364 1           AV *restrict sc_av = newAV();
9365 3 100         for (size_t j = 0; j < p; j++) av_push(sc_av, newSVnv(sc_vec[j]));
9366 1           hv_stores(res_hv, "scale", newRV_noinc((SV*)sc_av));
9367             } else {
9368 6           hv_stores(res_hv, "scale", newSVsv(&PL_sv_no));
9369             }
9370             // Cleanup
9371 7 100         if (colnames) {
9372 6 100         for (size_t i = 0; i < p; i++) Safefree(colnames[i]);
9373 2           Safefree(colnames);
9374             }
9375 7           Safefree(X_mat); Safefree(cen_vec); Safefree(sc_vec);
9376 7           Safefree(XtX); Safefree(eigen_val); Safefree(eigen_vec); Safefree(sdev);
9377              
9378 7           RETVAL = newRV_noinc((SV*)res_hv);
9379             }
9380             OUTPUT:
9381             RETVAL
9382              
9383             SV *transpose(input_ref)
9384             SV *input_ref
9385             PREINIT:
9386             svtype ref_type;
9387             SV *restrict retval_sv;
9388             CODE:
9389 38 50         SvGETMAGIC(input_ref);
    0          
9390 38 100         if (!SvROK(input_ref))
9391 1           croak("Stats::LikeR::transpose: Input must be a hash ref or array ref");
9392 37           ref_type = SvTYPE(SvRV(input_ref));
9393 37 100         if (ref_type == SVt_PVHV) {// ── Hash-of-Hashes
9394 14           HV *restrict in_hv = (HV *)SvRV(input_ref);
9395 14           HV *restrict out_hv = newHV();
9396             HE *restrict he_row, *restrict he_col, *restrict out_inner_he;
9397 14           retval_sv = sv_2mortal(newRV_noinc((SV *)out_hv));
9398 14           hv_iterinit(in_hv);
9399 35 100         while ((he_row = hv_iternext(in_hv))) {
9400 23           SV *restrict row_key_sv = hv_iterkeysv(he_row);
9401 23           SV *restrict row_val = hv_iterval(in_hv, he_row);
9402             HV *restrict in_inner_hv;
9403 23 50         SvGETMAGIC(row_val);
    0          
9404              
9405 23 100         if (!SvROK(row_val) || SvTYPE(SvRV(row_val)) != SVt_PVHV)
    100          
9406 2           croak("Stats::LikeR::transpose: Hash mode – inner element is not a hash ref");
9407 21           in_inner_hv = (HV *)SvRV(row_val);
9408 21           hv_iterinit(in_inner_hv);
9409 54 100         while ((he_col = hv_iternext(in_inner_hv))) {
9410 33           SV *restrict col_key_sv = hv_iterkeysv(he_col);
9411 33           SV *restrict val = hv_iterval(in_inner_hv, he_col);
9412             HV *restrict out_inner_hv;
9413             SV *restrict inner_ref;
9414 33 50         SvGETMAGIC(val);
    0          
9415 33           out_inner_he = hv_fetch_ent(out_hv, col_key_sv, 0, 0);
9416 33 100         if (out_inner_he) {
9417 14           inner_ref = HeVAL(out_inner_he);
9418 14 50         if (!SvROK(inner_ref) || SvTYPE(SvRV(inner_ref)) != SVt_PVHV)
    50          
9419 0           croak("Stats::LikeR::transpose: Internal error – output structure corrupted");
9420 14           out_inner_hv = (HV *)SvRV(inner_ref);
9421             } else {
9422 19           out_inner_hv = newHV();
9423 19           inner_ref = newRV_noinc((SV *)out_inner_hv);
9424 19 50         if (!hv_store_ent(out_hv, col_key_sv, inner_ref, 0)) {
9425 0           SvREFCNT_dec(inner_ref);
9426 0           croak("Stats::LikeR::transpose: Failed to allocate inner hash");
9427             }
9428             }
9429 33           SvREFCNT_inc(val);
9430 33 50         if (!hv_store_ent(out_inner_hv, row_key_sv, val, 0)) {
9431 0           SvREFCNT_dec(val);
9432 0           croak("Stats::LikeR::transpose: Failed to store transposed value");
9433             }
9434             }
9435             }
9436 23 100         } else if (ref_type == SVt_PVAV) { // Array-of-Arrays
9437 22           AV *restrict in_av = (AV *)SvRV(input_ref);
9438 22           AV *restrict out_av = newAV();
9439 22           SSize_t nrows = av_len(in_av) + 1;
9440 22           SSize_t ncols = 0;
9441 22           retval_sv = sv_2mortal(newRV_noinc((SV *)out_av));
9442 22 100         if (nrows > 0) {// Pass 1: validate all rows; fix ncols from row 0
9443             {
9444 21           SV **restrict elem = av_fetch(in_av, 0, 0);
9445 21 100         if (!elem || !*elem)
    50          
9446 1           croak("Stats::LikeR::transpose: Array mode – row 0 is missing");
9447 20 50         SvGETMAGIC(*elem);
    0          
9448 20 100         if (!SvROK(*elem) || SvTYPE(SvRV(*elem)) != SVt_PVAV)
    100          
9449 2           croak("Stats::LikeR::transpose: Array mode – row 0 is not an array ref");
9450 18           ncols = av_len((AV *)SvRV(*elem)) + 1;
9451             }
9452 35 100         for (SSize_t i = 1; i < nrows; i++) {
9453 19           SV **restrict elem = av_fetch(in_av, i, 0);
9454             SSize_t row_ncols;
9455 19 50         if (!elem || !*elem)
    50          
9456 0           croak("Stats::LikeR::transpose: Array mode – row %d is missing", (int)i);
9457 19 50         SvGETMAGIC(*elem);
    0          
9458 19 50         if (!SvROK(*elem) || SvTYPE(SvRV(*elem)) != SVt_PVAV)
    50          
9459 0           croak("Stats::LikeR::transpose: Array mode – row %d is not an array ref", (int)i);
9460 19           row_ncols = av_len((AV *)SvRV(*elem)) + 1;
9461 19 100         if (row_ncols != ncols)
9462 2           croak("Stats::LikeR::transpose: Array mode – ragged array: "
9463             "row 0 has %d cols, row %d has %d",
9464             (int)ncols, (int)i, (int)row_ncols);
9465             }
9466             // Pass 2: output[j][i] = input[i][j]
9467 16 100         if (ncols > 0) {
9468 15           av_extend(out_av, ncols - 1);
9469 47 100         for (SSize_t j = 0; j < ncols; j++) {
9470 32           AV *restrict out_col_av = newAV();
9471 32           SV *restrict col_ref = newRV_noinc((SV *)out_col_av);
9472 32 50         if (!av_store(out_av, j, col_ref)) {
9473 0           SvREFCNT_dec(col_ref);
9474 0           croak("Stats::LikeR::transpose: Array mode – "
9475             "failed to allocate output column %d", (int)j);
9476             }
9477 32           av_extend(out_col_av, nrows - 1);
9478 99 100         for (SSize_t i = 0; i < nrows; i++) {
9479 67           SV **restrict elem = av_fetch(in_av, i, 0);
9480 67 50         if (elem && *elem) {
    50          
9481 67 50         SvGETMAGIC(*elem);
    0          
9482             }
9483 67           AV *restrict in_row_av = (AV *)SvRV(*elem);
9484 67           SV **restrict val_ptr = av_fetch(in_row_av, j, 0);
9485 67 100         SV *restrict val = (val_ptr && *val_ptr) ? *val_ptr : &PL_sv_undef;
    50          
9486 67 50         SvGETMAGIC(val);
    0          
9487 67           SvREFCNT_inc(val);
9488 67 50         if (!av_store(out_col_av, i, val)) {
9489 0           SvREFCNT_dec(val);
9490 0           croak("Stats::LikeR::transpose: Array mode – "
9491             "failed to store [%d][%d]", (int)j, (int)i);
9492             }
9493             }
9494             }
9495             }
9496             }
9497             } else { // Unsupported
9498 1           croak("Stats::LikeR::transpose: Input must be a hash ref or array ref");
9499             }
9500 29           RETVAL = SvREFCNT_inc(retval_sv);
9501             OUTPUT:
9502             RETVAL