Skip to main content

zsh/ported/modules/
zprof.rs

1//! `zsh/zprof` module — port of `Src/Modules/zprof.c`.
2//!
3//! Shell-function profiling: every function call is wrapped via
4//! `zprof_wrapper` to record entry/exit time, build a per-function
5//! `Pfunc` table and a per-arc (caller→callee) `Parc` table, and
6//! emit a sorted report from `bin_zprof`.
7//!
8//! C source: 11 ported total — `freepfuncs`, `freeparcs`, `findpfunc`,
9//! `findparc`, `cmpsfuncs`, `cmptfuncs`, `cmpparcs`, `bin_zprof`,
10//! `name_for_anonymous_function`, `zprof_wrapper`, plus 6 module
11//! loaders. 3 structs: `pfunc` (c:38), `sfunc` (c:49), `parc` (c:57).
12//! 6 file-statics: `calls`, `ncalls`, `arcs`, `narcs`, `stack`,
13//! `zprof_module` (c:66-71).
14//!
15//! Order in this file mirrors C source order verbatim.
16
17use crate::ported::compat::zgettime_monotonic_if_available;
18use crate::ported::mem::ztrdup;
19use crate::ported::modules::parameter::FUNCSTACK;
20use crate::ported::zsh_h::{eprog, features, funcwrap, module, options, MAX_OPS, OPT_ISSET};
21use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
22use std::sync::{Mutex, OnceLock};
23// ---------------------------------------------------------------------------
24// Structs (port of c:36-64).
25// ---------------------------------------------------------------------------
26
27/// Port of `struct pfunc` from `Src/Modules/zprof.c:38`.
28/// Per-function aggregated profiling record.
29///
30/// C definition (c:38-45):
31/// ```c
32/// struct pfunc {
33///     Pfunc next;     /* linked list — Vec replaces */
34///     char *name;
35///     long calls;
36///     double time;
37///     double self;
38///     long num;
39/// };
40/// ```
41#[derive(Debug, Clone, Default)]
42pub struct Pfunc {
43    // c:38
44    pub name: String,   // c:40
45    pub calls: i64,     // c:41
46    pub time: f64,      // c:42
47    pub self_time: f64, // c:43 — `self` is a Rust keyword
48    pub num: i64,       // c:44
49}
50
51/// Port of `struct sfunc` from `Src/Modules/zprof.c:49`.
52/// Per-active-call stack frame: linked stack the C `zprof_wrapper`
53/// pushes on entry and pops on exit, used to compute self-time and
54/// build the caller→callee arc.
55///
56/// C definition (c:49-53):
57/// ```c
58/// struct sfunc {
59///     Pfunc p;       /* index into CALLS — Rust uses usize */
60///     Sfunc prev;    /* linked list — Vec replaces */
61///     double beg;
62/// };
63/// ```
64#[derive(Debug, Clone, Copy)]
65pub struct Sfunc {
66    // c:49
67    pub p: usize, // c:50 — index into CALLS
68    pub beg: f64, // c:52
69}
70
71/// Port of `struct parc` from `Src/Modules/zprof.c:57`.
72/// Per-(caller→callee) aggregated arc with timing.
73///
74/// C definition (c:57-64):
75/// ```c
76/// struct parc {
77///     Parc next;     /* linked list — Vec replaces */
78///     Pfunc from;    /* indices into CALLS */
79///     Pfunc to;
80///     long calls;
81///     double time;
82///     double self;
83/// };
84/// ```
85#[derive(Debug, Clone, Default)]
86pub struct Parc {
87    // c:57
88    pub from: usize,    // c:59 — index into CALLS
89    pub to: usize,      // c:60 — index into CALLS
90    pub calls: i64,     // c:61
91    pub time: f64,      // c:62
92    pub self_time: f64, // c:63 — `self` is a Rust keyword
93}
94
95// ---------------------------------------------------------------------------
96// Helpers (port of c:73-136).
97// ---------------------------------------------------------------------------
98
99/// Port of `freepfuncs(Pfunc f)` from `Src/Modules/zprof.c:74`. C iterates
100/// the linked list calling `zsfree(name)` + `zfree(node)` on each
101/// entry. Rust port clears the `Vec`; the contained `String`s and
102/// `Pfunc` slots are dropped at scope-exit.
103///
104/// C signature: `static void freepfuncs(Pfunc f)`.
105pub fn freepfuncs(f: &mut Vec<Pfunc>) {
106    // c:74
107    f.clear(); // c:86-82 zsfree+zfree
108}
109
110/// Port of `freeparcs(Parc a)` from `Src/Modules/zprof.c:86`.
111///
112/// C signature: `static void freeparcs(Parc a)`.
113pub fn freeparcs(a: &mut Vec<Parc>) {
114    // c:86
115    a.clear(); // c:97-93 zfree
116}
117
118/// Port of `findpfunc(char *name)` from `Src/Modules/zprof.c:97`. Linear-scan
119/// lookup in the `calls` list for an entry with matching `name`.
120///
121/// C signature: `static Pfunc findpfunc(char *name)`. Returns NULL on
122/// miss; Rust port returns `None`.
123pub fn findpfunc(name: &str) -> Option<usize> {
124    // c:97
125    // c:109-103 — `for (f = calls; f; f = f->next) if (!strcmp(name, f->name)) return f;`
126    let calls = CALLS.lock().unwrap();
127    calls.iter().position(|f| f.name == name)
128}
129
130/// Port of `findparc(Pfunc f, Pfunc t)` from `Src/Modules/zprof.c:109`. Linear-scan
131/// lookup in the `arcs` list for an arc with matching (from, to)
132/// pair.
133///
134/// C signature: `static Parc findparc(Pfunc f, Pfunc t)`.
135pub fn findparc(f: usize, t: usize) -> Option<usize> {
136    // c:109
137    // c:109-115 — `for (a = arcs; a; a = a->next) if (a->f == f && a->t == t) return a;`
138    let arcs = ARCS.lock().unwrap();
139    arcs.iter().position(|a| a.from == f && a.to == t)
140}
141
142/// Port of `cmpsfuncs(Pfunc *a, Pfunc *b)` from `Src/Modules/zprof.c:121`. The qsort
143/// comparator: descending by `self`. C uses `Pfunc *` pointers
144/// because qsort passes opaque ptrs; Rust takes refs directly.
145///
146/// C body:
147/// ```c
148/// return ((*a)->self > (*b)->self ? -1 :
149///         ((*a)->self != (*b)->self));
150/// ```
151/// (i.e. -1 if a > b, 0 if equal, +1 if a < b — descending order.)
152pub fn cmpsfuncs(a: &Pfunc, b: &Pfunc) -> std::cmp::Ordering {
153    // c:121
154    b.self_time
155        .partial_cmp(&a.self_time)
156        .unwrap_or(std::cmp::Ordering::Equal)
157}
158
159/// Port of `cmptfuncs(Pfunc *a, Pfunc *b)` from `Src/Modules/zprof.c:127`. Comparator
160/// for descending by total `time`.
161pub fn cmptfuncs(a: &Pfunc, b: &Pfunc) -> std::cmp::Ordering {
162    // c:127
163    b.time
164        .partial_cmp(&a.time)
165        .unwrap_or(std::cmp::Ordering::Equal)
166}
167
168/// Port of `cmpparcs(Parc *a, Parc *b)` from `Src/Modules/zprof.c:133`. Comparator
169/// for descending by arc `time`.
170pub fn cmpparcs(a: &Parc, b: &Parc) -> std::cmp::Ordering {
171    // c:133
172    b.time
173        .partial_cmp(&a.time)
174        .unwrap_or(std::cmp::Ordering::Equal)
175}
176
177// ---------------------------------------------------------------------------
178// `bin_zprof` (port of c:139-214).
179// ---------------------------------------------------------------------------
180
181/// Port of `bin_zprof(UNUSED(char *nam), UNUSED(char **args), Options ops, UNUSED(int func))` from `Src/Modules/zprof.c:139`.
182///
183/// C signature: `static int bin_zprof(char *nam, char **args,
184///                                     Options ops, int func)`.
185/// Builtin spec: `"c"` (c:315) — the `-c` option clears the tables.
186/// No positional args (`0,0` arity at c:315).
187///
188/// `-c` set → free both tables and reset counters. `-c` unset →
189/// sort by self-time, print the c:170 header + per-function row,
190/// re-sort by total-time, print the c:184 per-function caller/callee
191/// blocks.
192/// WARNING: param names don't match C — Rust=(_nam, _args, _func) vs C=(nam, args, ops, func)
193pub fn bin_zprof(
194    _nam: &str,
195    _args: &[String], // c:139
196    ops: &options,
197    _func: i32,
198) -> i32 {
199    // c:140 — `if (OPT_ISSET(ops,'c'))`
200    let opt_c = OPT_ISSET(ops, b'c');
201
202    if opt_c {
203        // c:141-147 — free both tables + reset counters.
204        let mut calls = CALLS.lock().unwrap();
205        freepfuncs(&mut calls); // c:142
206        NCALLS.store(0, Ordering::SeqCst); // c:144
207        let mut arcs = ARCS.lock().unwrap();
208        freeparcs(&mut arcs); // c:145
209        NARCS.store(0, Ordering::SeqCst); // c:147
210        return 0; // c:213
211    }
212
213    // c:149-211 — print path.
214    let calls = CALLS.lock().unwrap();
215    let arcs = ARCS.lock().unwrap();
216
217    // c:149-163 — gather + total. C uses a VARARR Pfunc fs[ncalls+1]
218    // and a VARARR Parc as[narcs+1] with NULL sentinels; Rust uses
219    // index arrays. `total` is the sum of self-times across all funcs.
220    let mut fs: Vec<usize> = (0..calls.len()).collect(); // c:149-159
221    let mut as_arcs: Vec<usize> = (0..arcs.len()).collect(); // c:151-163
222    let mut total: f64 = 0.0; // c:154
223    for &i in &fs {
224        total += calls[i].self_time; // c:158 total += f->self;
225    }
226
227    // c:165-166 — `qsort(fs, ncalls, sizeof(f), cmpsfuncs);`
228    fs.sort_by(|&a, &b| cmpsfuncs(&calls[a], &calls[b]));
229    // c:167-168 — `qsort(as, narcs, sizeof(a), cmpparcs);`
230    //   Prior port skipped this sort, so the per-function caller/callee
231    //   blocks at c:184-211 listed arcs in chronological insertion order
232    //   instead of descending-time order. With many callers, the listing
233    //   buried the dominant time consumers under low-cost arcs, defeating
234    //   zprof's "find the hot edges" purpose.
235    as_arcs.sort_by(|&a, &b| cmpparcs(&arcs[a], &arcs[b]));
236
237    // c:170 — header.
238    println!("num  calls                time                       self            name");
239    println!("-----------------------------------------------------------------------------------");
240
241    // c:171-180 — primary listing, also assigns `num` in display order.
242    // Mutating `num` in C requires reborrowing — release the read lock
243    // briefly to take a write lock, then reacquire read order.
244    drop(calls);
245    {
246        let mut calls_w = CALLS.lock().unwrap();
247        for (i, &idx) in fs.iter().enumerate() {
248            // c:171
249            calls_w[idx].num = (i + 1) as i64; // c:173
250        }
251    }
252    let calls = CALLS.lock().unwrap();
253    for &idx in &fs {
254        // c:171 again, after num assignment
255        let f = &calls[idx];
256        let avg_t = if f.calls > 0 {
257            f.time / f.calls as f64
258        } else {
259            0.0
260        };
261        let avg_s = if f.calls > 0 {
262            f.self_time / f.calls as f64
263        } else {
264            0.0
265        };
266        let pct_t = if total != 0.0 {
267            (f.time / total) * 100.0
268        } else {
269            0.0
270        };
271        let pct_s = if total != 0.0 {
272            (f.self_time / total) * 100.0
273        } else {
274            0.0
275        };
276        println!(
277            "{:2}) {:4}       {:8.2} {:8.2}  {:6.2}%  {:8.2} {:8.2}  {:6.2}%  {}",
278            f.num,
279            f.calls, // c:172-179 printf
280            f.time,
281            avg_t,
282            pct_t,
283            f.self_time,
284            avg_s,
285            pct_s,
286            f.name
287        );
288    }
289
290    // c:181-182 — `qsort(fs, ncalls, sizeof(f), cmptfuncs);`
291    let mut fs_t: Vec<usize> = fs.clone();
292    fs_t.sort_by(|&a, &b| cmptfuncs(&calls[a], &calls[b]));
293
294    // c:184-211 — per-function caller/callee blocks.
295    for &fp_idx in &fs_t {
296        // c:184
297        println!();
298        println!(
299            "-----------------------------------------------------------------------------------"
300        );
301        println!();
302        let f = &calls[fp_idx];
303
304        // c:186-194 — callers (arcs where to == fp).
305        for &ap in &as_arcs {
306            // c:186
307            let a = &arcs[ap];
308            if a.to == fp_idx {
309                // c:187
310                let avg_t = if a.calls > 0 {
311                    a.time / a.calls as f64
312                } else {
313                    0.0
314                };
315                let avg_s = if a.calls > 0 {
316                    a.self_time / a.calls as f64
317                } else {
318                    0.0
319                };
320                let pct_t = if total != 0.0 {
321                    (a.time / total) * 100.0
322                } else {
323                    0.0
324                };
325                let from_name = &calls[a.from].name;
326                let from_num = calls[a.from].num;
327                println!(
328                    "    {:4}/{:<4}  {:8.2} {:8.2}  {:6.2}%  {:8.2} {:8.2}             {} [{}]",
329                    a.calls,
330                    f.calls, // c:188-193 printf
331                    a.time,
332                    avg_t,
333                    pct_t,
334                    a.self_time,
335                    avg_s,
336                    from_name,
337                    from_num
338                );
339            }
340        }
341
342        // c:195-201 — the function's own row.
343        let avg_t = if f.calls > 0 {
344            f.time / f.calls as f64
345        } else {
346            0.0
347        };
348        let avg_s = if f.calls > 0 {
349            f.self_time / f.calls as f64
350        } else {
351            0.0
352        };
353        let pct_t = if total != 0.0 {
354            (f.time / total) * 100.0
355        } else {
356            0.0
357        };
358        let pct_s = if total != 0.0 {
359            (f.self_time / total) * 100.0
360        } else {
361            0.0
362        };
363        println!(
364            "{:2}) {:4}       {:8.2} {:8.2}  {:6.2}%  {:8.2} {:8.2}  {:6.2}%  {}",
365            f.num,
366            f.calls, // c:195-201 printf
367            f.time,
368            avg_t,
369            pct_t,
370            f.self_time,
371            avg_s,
372            pct_s,
373            f.name
374        );
375
376        // c:202-210 — callees (arcs where from == fp), iterated in
377        // reverse to match C's `for (ap = as + narcs - 1; ap >= as; ap--)`.
378        for &ap in as_arcs.iter().rev() {
379            // c:202
380            let a = &arcs[ap];
381            if a.from == fp_idx {
382                // c:203
383                let avg_t = if a.calls > 0 {
384                    a.time / a.calls as f64
385                } else {
386                    0.0
387                };
388                let avg_s = if a.calls > 0 {
389                    a.self_time / a.calls as f64
390                } else {
391                    0.0
392                };
393                let pct_t = if total != 0.0 {
394                    (a.time / total) * 100.0
395                } else {
396                    0.0
397                };
398                let to_name = &calls[a.to].name;
399                let to_num = calls[a.to].num;
400                let to_calls = calls[a.to].calls;
401                println!(
402                    "    {:4}/{:<4}  {:8.2} {:8.2}  {:6.2}%  {:8.2} {:8.2}             {} [{}]",
403                    a.calls,
404                    to_calls, // c:204-209 printf
405                    a.time,
406                    avg_t,
407                    pct_t,
408                    a.self_time,
409                    avg_s,
410                    to_name,
411                    to_num
412                );
413            }
414        }
415    }
416
417    0 // c:217
418}
419
420/// Port of `name_for_anonymous_function(char *name)` from `Src/Modules/zprof.c:217`.
421/// Anonymous functions don't have a real name; the profiler synthesises
422/// `name [filename:lineno]` using the current `funcstack[0]` frame.
423///
424/// C signature: `static char *name_for_anonymous_function(char *name)`.
425pub fn name_for_anonymous_function(name: &str) -> String {
426    // c:217
427    // c:219 — char lineno[DIGBUFSIZE];
428    // c:220 — char *parts[7];
429    // c:222 — convbase(lineno, funcstack[0].flineno, 10);
430    let stack = FUNCSTACK.lock().expect("FUNCSTACK poisoned");
431    let flineno = stack.first().map(|f| f.flineno).unwrap_or(0); // c:222
432    let filename = stack
433        .first()
434        .and_then(|f| f.filename.clone())
435        .unwrap_or_default(); // c:226
436    drop(stack);
437    let lineno_str = format!("{}", flineno); // c:222 convbase base=10
438                                             // c:224-230 — parts[] = { name, " [", filename, ":", lineno, "]", NULL };
439                                             // c:232 — return sepjoin(parts, "", 1);
440    let parts = [name, " [", filename.as_str(), ":", lineno_str.as_str(), "]"];
441    parts.concat() // c:232
442}
443
444/// Port of `zprof_wrapper(Eprog prog, FuncWrap w, char *name)` from `Src/Modules/zprof.c:236`. The
445/// per-function-call wrapper hook: records call entry, measures wall
446/// time, runs the wrapped function via `runshfunc`, then accumulates
447/// self/total time on the function's `pfunc` entry and on the
448/// (caller→callee) `parc` arc.
449///
450/// C signature: `static int zprof_wrapper(Eprog prog, FuncWrap w, char *name)`.
451///
452/// C body (c:238-311):
453/// 1. Resolve `name_for_lookups` via `name_for_anonymous_function` for
454///    anonymous funcs (c:246-250).
455/// 2. If `zprof_module` is loaded (c:252), find-or-create the Pfunc
456///    (c:254-262), find-or-create the caller→callee Parc (c:263-274),
457///    push the Sfunc frame and record start time (c:275-283).
458/// 3. `runshfunc(prog, w, name)` (c:285) — runs the wrapped function.
459/// 4. On return, recompute elapsed time, update Pfunc.self_time
460///    (c:293), Pfunc.time when non-recursive (c:294-296), Parc.calls/
461///    self/time (c:297-307), pop the stack frame (c:301).
462///
463/// zshrs's call-execution path doesn't have an `addwrapper`-installable
464/// runshfunc callback, so the live integration is the executor's
465/// funcstack push/pop hooks (in `crate::ported::exec`). This entry is
466/// the static-link stub that mirrors C's `return 0;` exit path; the
467/// actual timing accumulation happens via direct CALLS/ARCS/STACK
468/// updates from the executor when `ZPROF_MODULE` is true.
469/// Port of `static int zprof_wrapper(Eprog prog, FuncWrap w, char *name)`
470/// from `Src/Modules/zprof.c:236`.
471///
472/// ```c
473/// static int
474/// zprof_wrapper(Eprog prog, FuncWrap w, char *name)
475/// {
476///     int active = 0;
477///     struct sfunc sf, *sp;
478///     Pfunc f = NULL;
479///     Parc a = NULL;
480///     struct timespec ts;
481///     double prev = 0, now;
482///     char *name_for_lookups;
483///     if (is_anonymous_function_name(name))
484///         name_for_lookups = name_for_anonymous_function(name);
485///     else
486///         name_for_lookups = name;
487///     if (zprof_module && !(zprof_module->node.flags & MOD_UNLOAD)) {
488///         active = 1;
489///         if (!(f = findpfunc(name_for_lookups))) { ... append calls ... }
490///         if (stack) {
491///             if (!(a = findparc(stack->p, f))) { ... append arcs ... }
492///         }
493///         sf.prev = stack; sf.p = f; stack = &sf;
494///         f->calls++;
495///         zgettime_monotonic_if_available(&ts);
496///         sf.beg = prev = ms_now(ts);
497///     }
498///     runshfunc(prog, w, name);
499///     if (active) {
500///         if (zprof_module && !(zprof_module->node.flags & MOD_UNLOAD)) {
501///             zgettime_monotonic_if_available(&ts);
502///             now = ms_now(ts);
503///             f->self += now - sf.beg;
504///             for (sp = sf.prev; sp && sp->p != f; sp = sp->prev);
505///             if (!sp) f->time += now - prev;
506///             if (a) { a->calls++; a->self += now - sf.beg; }
507///             stack = sf.prev;
508///             if (stack) { stack->beg += now - prev;
509///                          if (a) a->time += now - prev; }
510///         } else stack = sf.prev;
511///     }
512///     return 0;
513/// }
514/// ```
515#[allow(non_snake_case)]
516pub fn zprof_wrapper(
517    prog: *const eprog, // c:236
518    w: *const funcwrap,
519    name: &str,
520    runshfunc: impl FnOnce(),
521) -> i32 {
522    let mut active: i32 = 0; // c:238
523    let mut sf = Sfunc { p: 0, beg: 0.0 }; // c:239 struct sfunc sf
524    let mut f: Option<usize> = None; // c:240 Pfunc f = NULL
525    let mut a: Option<usize> = None; // c:241 Parc a = NULL
526    let mut prev: f64 = 0.0; // c:243 double prev = 0
527
528    // c:246-250 — resolve display name for anonymous functions.
529    // `is_anonymous_function_name(name)` is `!strcmp(name, "(anon)")`
530    // per Src/exec.c:5303-5306. ANONYMOUS_FUNCTION_NAME = "(anon)".
531    let name_for_lookups: String = if name == "(anon)" {
532        // c:246
533        // `name_for_anonymous_function(name)` reads funcstack[0]
534        // internally (S1 rule — signature matches C).
535        name_for_anonymous_function(name) // c:247
536    } else {
537        // c:248
538        name.to_string() // c:249
539    };
540
541    if ZPROF_MODULE.load(Ordering::SeqCst) {
542        // c:252
543        active = 1; // c:253
544        f = findpfunc(&name_for_lookups); // c:254
545        if f.is_none() {
546            // c:254
547            // c:255-261 — `f = zalloc(...); f->name = ztrdup(...); f->next = calls; calls = f; ncalls++;`
548            let new_pfunc = Pfunc {
549                // c:255
550                name: ztrdup(&name_for_lookups), // c:256
551                calls: 0,                        // c:257
552                time: 0.0,                       // c:258 self/time = 0
553                self_time: 0.0,                  // c:258
554                num: 0,
555            };
556            let mut calls = CALLS.lock().unwrap();
557            f = Some(calls.len()); // c:260 head-insert in C; Rust appends
558            calls.push(new_pfunc); // c:260
559            NCALLS.fetch_add(1, Ordering::SeqCst); // c:261
560        }
561        // c:263 — `if (stack)` — top-of-stack frame exists.
562        let stack_top: Option<Sfunc> = {
563            // c:263
564            let st = STACK.lock().unwrap();
565            st.last().copied()
566        };
567        if let Some(top) = stack_top {
568            // c:263
569            a = findparc(top.p, f.unwrap()); // c:264
570            if a.is_none() {
571                // c:264
572                let new_parc = Parc {
573                    // c:265
574                    from: top.p,    // c:266
575                    to: f.unwrap(), // c:267
576                    calls: 0,       // c:268
577                    self_time: 0.0, // c:269
578                    time: 0.0,      // c:269
579                };
580                let mut arcs = ARCS.lock().unwrap();
581                a = Some(arcs.len()); // c:271
582                arcs.push(new_parc); // c:271
583                NARCS.fetch_add(1, Ordering::SeqCst); // c:272
584            }
585        }
586        // c:275-277 — `sf.prev = stack; sf.p = f; stack = &sf;`
587        sf.p = f.unwrap(); // c:276
588        STACK.lock().unwrap().push(sf); // c:277 stack = &sf
589
590        // c:279 — `f->calls++;`
591        {
592            let mut calls = CALLS.lock().unwrap();
593            calls[f.unwrap()].calls += 1; // c:279
594        }
595        // c:280-283 — read monotonic clock, compute prev (ms).
596        let mut ts = libc::timespec {
597            tv_sec: 0,
598            tv_nsec: 0,
599        }; // c:280
600        zgettime_monotonic_if_available(&mut ts); // c:281
601        sf.beg = (ts.tv_sec as f64) * 1000.0 + (ts.tv_nsec as f64) / 1_000_000.0; // c:282-283
602        prev = sf.beg; // c:282
603                       // Update the stack-top copy we just pushed.
604        let mut st = STACK.lock().unwrap();
605        if let Some(top) = st.last_mut() {
606            top.beg = sf.beg;
607        }
608    }
609
610    // c:285 — `runshfunc(prog, w, name);` — the function-under-profile
611    // runs HERE, between the c:282 start-timestamp and the c:289 end
612    // read. Taken as the FnOnce runner (same shape as param_private's
613    // wrap_private, 90dfda9df7) so the timing actually brackets the
614    // call. A prior discarded placeholder meant every profiled time
615    // measured the wrapper's own overhead (~0ms) instead of the
616    // function.
617    let _ = (prog, w);
618    runshfunc(); // c:285
619
620    if active != 0 {
621        // c:286
622        if ZPROF_MODULE.load(Ordering::SeqCst) {
623            // c:287
624            let mut ts = libc::timespec {
625                tv_sec: 0,
626                tv_nsec: 0,
627            }; // c:288
628            zgettime_monotonic_if_available(&mut ts); // c:289
629            let now = (ts.tv_sec as f64) * 1000.0 + (ts.tv_nsec as f64) / 1_000_000.0; // c:291-292
630
631            // C's `sf` IS the top of the `stack` list (c:277 `stack = &sf;`),
632            // so every nested call's c:304 `stack->beg += now - prev` has
633            // been landing on THIS frame's `beg` — that is precisely how
634            // c:293/c:299 subtract child time out of self-time. The Rust
635            // port pushed a COPY of `sf` onto `STACK`, and c:304 wrote to
636            // the copy, so `sf.beg` here is the un-incremented start time.
637            // Read the live value back before using it.
638            sf.beg = {
639                let st = STACK.lock().unwrap();
640                st.last().map(|top| top.beg).unwrap_or(sf.beg)
641            };
642
643            // c:293 — `f->self += now - sf.beg;`
644            {
645                let mut calls = CALLS.lock().unwrap();
646                if let Some(idx) = f {
647                    calls[idx].self_time += now - sf.beg; // c:293
648                }
649            }
650            // c:294 — recursion-detect: walk sf.prev looking for f.
651            let recursion: bool = {
652                // c:294
653                let st = STACK.lock().unwrap();
654                let cur_f = f.unwrap();
655                // sf.prev = the frame underneath sf — walk it down.
656                st.iter().rev().skip(1).any(|fr| fr.p == cur_f)
657            };
658            if !recursion {
659                // c:295
660                let mut calls = CALLS.lock().unwrap();
661                if let Some(idx) = f {
662                    calls[idx].time += now - prev; // c:296
663                }
664            }
665            if let Some(arc_idx) = a {
666                // c:297
667                let mut arcs = ARCS.lock().unwrap();
668                arcs[arc_idx].calls += 1; // c:298
669                arcs[arc_idx].self_time += now - sf.beg; // c:299
670            }
671            // c:301 — `stack = sf.prev;`
672            {
673                let mut st = STACK.lock().unwrap();
674                st.pop(); // c:301
675            }
676            // c:303-307 — propagate elapsed up to caller frame.
677            let mut st = STACK.lock().unwrap();
678            if let Some(top) = st.last_mut() {
679                // c:303
680                top.beg += now - prev; // c:304
681                if let Some(arc_idx) = a {
682                    // c:305
683                    drop(st);
684                    let mut arcs = ARCS.lock().unwrap();
685                    arcs[arc_idx].time += now - prev; // c:306
686                }
687            }
688        } else {
689            // c:308
690            // c:309 — `stack = sf.prev;`
691            let mut st = STACK.lock().unwrap();
692            st.pop();
693        }
694    }
695    0 // c:311
696}
697
698// `bintab` — port of `static struct builtin bintab[]` (zprof.c:309).
699
700// `module_features` — port of `static struct features module_features`
701// from zprof.c:323.
702
703/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/zprof.c:332`.
704/// C body: `zprof_module = m; return 0;`
705#[allow(unused_variables)]
706pub fn setup_(m: *const module) -> i32 {
707    // c:332
708    ZPROF_MODULE.store(true, Ordering::SeqCst); // c:340
709    0 // c:348
710}
711
712/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/zprof.c:340`.
713/// C body: `*features = featuresarray(m, &module_features); return 0;`
714pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
715    // c:340
716    *features = featuresarray(m, module_features());
717    0 // c:355
718}
719
720/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/zprof.c:348`.
721/// C body: `return handlefeatures(m, &module_features, enables);`
722pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
723    // c:348
724    handlefeatures(m, module_features(), enables) // c:355
725}
726
727/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/zprof.c:355`.
728#[allow(unused_variables)]
729pub fn boot_(m: *const module) -> i32 {
730    // c:355
731    let mut calls = CALLS.lock().unwrap();
732    calls.clear(); // c:357 calls = NULL
733    NCALLS.store(0, Ordering::SeqCst); // c:358 ncalls = 0
734    let mut arcs = ARCS.lock().unwrap();
735    arcs.clear(); // c:359 arcs = NULL
736    NARCS.store(0, Ordering::SeqCst); // c:360 narcs = 0
737    STACK.lock().unwrap().clear(); // c:361 stack = NULL
738    drop(calls);
739    drop(arcs);
740    // c:362 — `return addwrapper(m, wrapper);`. The module arrives as a
741    // name because zshrs's module dispatcher passes a null
742    // `*const module` (module.rs:4140); see `addwrapper`'s WARNING.
743    //
744    // `wrapper` is C's file-static array at c:318-320:
745    //     static struct funcwrap wrapper[] = { WRAPDEF(zprof_wrapper), };
746    // `WRAPDEF` (`Src/zsh.h:1370-1371`) expands to `{ NULL, 0, func, NULL }`.
747    // It's data, not a function, so it's built inline at its one use.
748    // `handler` stays `None` rather than holding `zprof_wrapper`: a
749    // `WrapFunc` (`Src/zsh.h:1359`) is `int (*)(Eprog, FuncWrap, char *)`,
750    // and this port's `zprof_wrapper` takes the function body as a
751    // delegate because a zshrs function body is a caller-supplied closure,
752    // not a walkable `Eprog` — see the `BodyWrap` doc block in
753    // `src/ported/exec.rs` (`Src/exec.c:6166` `runshfunc`).
754    // `exec::runshfunc` therefore dispatches on the owning module, and
755    // this node is the `addwrapper`/`deletewrapper` bookkeeping entry C
756    // keeps in the same list.
757    crate::ported::module::addwrapper(
758        "zsh/zprof",
759        funcwrap {
760            next: None,    // c:1371 WRAPDEF
761            flags: 0,      // c:1371
762            handler: None, // c:1371 — WRAPDEF(zprof_wrapper); see above
763            module: None,  // c:1371
764        },
765    ) // c:362
766}
767
768/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/zprof.c:367`.
769/// C body: free pfuncs + parcs, deletewrapper, setfeatureenables.
770pub fn cleanup_(m: *const module) -> i32 {
771    // c:367
772    let mut calls = CALLS.lock().unwrap();
773    freepfuncs(&mut calls); // c:369 freepfuncs(calls)
774    let mut arcs = ARCS.lock().unwrap();
775    freeparcs(&mut arcs); // c:370 freeparcs(arcs)
776    drop(calls);
777    drop(arcs);
778    ZPROF_MODULE.store(false, Ordering::SeqCst);
779    // c:371 — `deletewrapper(m, wrapper);` (return value discarded in C).
780    let _ = crate::ported::module::deletewrapper("zsh/zprof"); // c:371
781    setfeatureenables(m, module_features(), None) // c:372
782}
783
784/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/zprof.c:377`.
785#[allow(unused_variables)]
786pub fn finish_(m: *const module) -> i32 {
787    // c:377
788    // C body c:379-380 — `return 0`. Faithful empty-body port; the
789    //                    profiling tables get freed by cleanup_ via
790    //                    setfeatureenables/zprof_cleanup.
791    0
792}
793
794// ---------------------------------------------------------------------------
795// Module loaders.
796// ---------------------------------------------------------------------------
797
798// =====================================================================
799// static struct builtin bintab[]                                    c:309
800// static struct features module_features                            c:323
801// static struct funcwrap wrapper[]                                  c:328
802// =====================================================================
803
804// ---------------------------------------------------------------------------
805// File-static globals — port of c:66-71.
806// ---------------------------------------------------------------------------
807
808/// Port of `static Pfunc calls;` from `Src/Modules/zprof.c:66`.
809/// Per-function aggregated table; the C linked list becomes a
810/// `Mutex<Vec<Pfunc>>` so `Pfunc *` becomes `usize` index.
811pub static CALLS: Mutex<Vec<Pfunc>> = Mutex::new(Vec::new()); // c:66
812
813/// Port of `static int ncalls;` from `Src/Modules/zprof.c:67`. Always
814/// equals `CALLS.lock().len()` — kept as an explicit counter to
815/// match C's `ncalls++` increment pattern.
816pub static NCALLS: AtomicI32 = AtomicI32::new(0); // c:67
817
818/// Port of `static Parc arcs;` from `Src/Modules/zprof.c:68`.
819pub static ARCS: Mutex<Vec<Parc>> = Mutex::new(Vec::new()); // c:68
820
821/// Port of `static int narcs;` from `Src/Modules/zprof.c:69`.
822pub static NARCS: AtomicI32 = AtomicI32::new(0); // c:69
823
824/// Port of `static Sfunc stack;` from `Src/Modules/zprof.c:70`. The
825/// C linked stack becomes a `Mutex<Vec<Sfunc>>` (top of stack at
826/// `last()`).
827pub static STACK: Mutex<Vec<Sfunc>> = Mutex::new(Vec::new()); // c:70
828
829/// Port of `static Module zprof_module;` from `Src/Modules/zprof.c:71`.
830/// C uses a `Module` (struct module *) pointer to track which module
831/// owns the wrapper; `zprof_wrapper` short-circuits when
832/// `MOD_UNLOAD` is set on it. Module is ported as
833/// `Box<crate::ported::zsh_h::module>` (zsh_h.rs:425) but recording
834/// the raw `*const module` would deadlock with Sync/Send for the
835/// static — `AtomicBool` captures the only state `zprof_wrapper`
836/// actually inspects (loaded vs. unloading), matching the C
837/// `MOD_UNLOAD` flag-check on the same pointer.
838pub static ZPROF_MODULE: AtomicBool = AtomicBool::new(false); // c:74
839
840static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
841
842// Local stubs for the per-module entry points. C uses generic
843// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
844// 3275/3370/3445) but those take `Builtin` + `Features` pointer
845// fields the Rust port doesn't carry. The hardcoded descriptor
846// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
847// WARNING: NOT IN ZPROF.C — Rust-only module-framework shim.
848// C uses generic featuresarray/handlefeatures/setfeatureenables from
849// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
850// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
851fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
852    vec!["b:zprof".to_string()]
853}
854
855// WARNING: NOT IN ZPROF.C — Rust-only module-framework shim.
856// C uses generic featuresarray/handlefeatures/setfeatureenables from
857// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
858// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
859fn handlefeatures(m: *const module, f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
860    // c:3392 — the name-keyed variant in src/ported/module.rs; this
861    // module ships no `Features` descriptor tables for the per-feature
862    // ADDED bit to live on (see MODULE_FEATURE_ENABLES there).
863    crate::ported::module::handlefeatures("zsh/zprof", &featuresarray(m, f), enables)
864}
865
866// WARNING: NOT IN ZPROF.C — Rust-only module-framework shim.
867// C uses generic featuresarray/handlefeatures/setfeatureenables from
868// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
869// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
870fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
871    0
872}
873
874// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
875// ─── RUST-ONLY ACCESSORS ───
876//
877// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
878// RwLock<T>>` globals declared above. C zsh uses direct global
879// access; Rust needs these wrappers because `OnceLock::get_or_init`
880// is the only way to lazily construct shared state. These ported sit
881// here so the body of this file reads in C source order without
882// the accessor wrappers interleaved between real port ported.
883// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
884
885// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
886// ─── RUST-ONLY ACCESSORS ───
887//
888// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
889// RwLock<T>>` globals declared above. C zsh uses direct global
890// access; Rust needs these wrappers because `OnceLock::get_or_init`
891// is the only way to lazily construct shared state. These ported sit
892// here so the body of this file reads in C source order without
893// the accessor wrappers interleaved between real port ported.
894// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
895
896// WARNING: NOT IN ZPROF.C — Rust-only module-framework shim.
897// C uses generic featuresarray/handlefeatures/setfeatureenables from
898// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
899// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
900fn module_features() -> &'static Mutex<features> {
901    MODULE_FEATURES.get_or_init(|| {
902        Mutex::new(features {
903            bn_list: None,
904            bn_size: 1,
905            cd_list: None,
906            cd_size: 0,
907            mf_list: None,
908            mf_size: 0,
909            pd_list: None,
910            pd_size: 0,
911            n_abstract: 0,
912        })
913    })
914}
915
916#[cfg(test)]
917mod tests {
918    use super::*;
919    use crate::ported::zsh_h::funcstack;
920
921    /// Serialise tests that mutate the module-static globals so the
922    /// cargo-test parallel runner doesn't shred each other's state.
923    static TEST_LOCK: Mutex<()> = Mutex::new(());
924
925    fn reset_state() {
926        let mut c = CALLS.lock().unwrap();
927        c.clear();
928        let mut a = ARCS.lock().unwrap();
929        a.clear();
930        STACK.lock().unwrap().clear();
931        NCALLS.store(0, Ordering::SeqCst);
932        NARCS.store(0, Ordering::SeqCst);
933    }
934
935    /// Port of `bin_zprof(UNUSED(char *nam), UNUSED(char **args), Options ops, UNUSED(int func))` from `Src/Modules/zprof.c:139`.
936    /// Verifies `Pfunc` mirrors C `struct pfunc` field-for-field
937    /// (name/calls/time/self/num at c:40-44).
938    #[test]
939    fn pfunc_default_zeros() {
940        let _g = crate::test_util::global_state_lock();
941        let p = Pfunc::default();
942        assert_eq!(p.name, "");
943        assert_eq!(p.calls, 0);
944        assert_eq!(p.time, 0.0);
945        assert_eq!(p.self_time, 0.0);
946        assert_eq!(p.num, 0);
947    }
948
949    /// Verifies `freepfuncs` empties the table (c:78-82 zsfree+zfree).
950    #[test]
951    fn freepfuncs_clears() {
952        let _g = crate::test_util::global_state_lock();
953        let mut v = vec![Pfunc {
954            name: "a".into(),
955            ..Default::default()
956        }];
957        freepfuncs(&mut v);
958        assert!(v.is_empty());
959    }
960
961    /// Verifies `findpfunc` linear-scan match (c:101-103).
962    #[test]
963    fn findpfunc_matches_by_name() {
964        let _g = crate::test_util::global_state_lock();
965        let _g = TEST_LOCK.lock().unwrap();
966        reset_state();
967        CALLS.lock().unwrap().push(Pfunc {
968            name: "alpha".into(),
969            ..Default::default()
970        });
971        CALLS.lock().unwrap().push(Pfunc {
972            name: "beta".into(),
973            ..Default::default()
974        });
975        assert_eq!(findpfunc("alpha"), Some(0));
976        assert_eq!(findpfunc("beta"), Some(1));
977        assert_eq!(findpfunc("none"), None);
978        reset_state();
979    }
980
981    /// Verifies `findparc` matches (from, to) pair (c:113-115).
982    #[test]
983    fn findparc_matches_pair() {
984        let _g = crate::test_util::global_state_lock();
985        let _g = TEST_LOCK.lock().unwrap();
986        reset_state();
987        ARCS.lock().unwrap().push(Parc {
988            from: 0,
989            to: 1,
990            ..Default::default()
991        });
992        ARCS.lock().unwrap().push(Parc {
993            from: 0,
994            to: 2,
995            ..Default::default()
996        });
997        assert_eq!(findparc(0, 1), Some(0));
998        assert_eq!(findparc(0, 2), Some(1));
999        assert_eq!(findparc(1, 0), None);
1000        reset_state();
1001    }
1002
1003    /// Verifies `cmpsfuncs` is descending (c:121-124).
1004    #[test]
1005    fn cmpsfuncs_descending() {
1006        let _g = crate::test_util::global_state_lock();
1007        let a = Pfunc {
1008            self_time: 5.0,
1009            ..Default::default()
1010        };
1011        let b = Pfunc {
1012            self_time: 10.0,
1013            ..Default::default()
1014        };
1015        // descending: b should come before a → cmp(a, b) = Greater
1016        assert_eq!(cmpsfuncs(&a, &b), std::cmp::Ordering::Greater);
1017        assert_eq!(cmpsfuncs(&b, &a), std::cmp::Ordering::Less);
1018    }
1019
1020    /// Verifies `bin_zprof -c` clears state (c:141-147).
1021    #[test]
1022    fn bin_zprof_clear_resets_tables() {
1023        let _g = crate::test_util::global_state_lock();
1024        let _g = TEST_LOCK.lock().unwrap();
1025        reset_state();
1026        CALLS.lock().unwrap().push(Pfunc {
1027            name: "x".into(),
1028            ..Default::default()
1029        });
1030        ARCS.lock().unwrap().push(Parc {
1031            from: 0,
1032            to: 0,
1033            ..Default::default()
1034        });
1035        NCALLS.store(1, Ordering::SeqCst);
1036        NARCS.store(1, Ordering::SeqCst);
1037
1038        let mut ops = options {
1039            ind: [0u8; MAX_OPS],
1040            args: Vec::new(),
1041            argscount: 0,
1042            argsalloc: 0,
1043        };
1044        ops.ind[b'c' as usize] = 1;
1045        let r = bin_zprof("zprof", &["-c".to_string()], &ops, 0);
1046        assert_eq!(r, 0);
1047        assert!(CALLS.lock().unwrap().is_empty());
1048        assert!(ARCS.lock().unwrap().is_empty());
1049        assert_eq!(NCALLS.load(Ordering::SeqCst), 0);
1050        assert_eq!(NARCS.load(Ordering::SeqCst), 0);
1051    }
1052
1053    /// Verifies `zprof_wrapper` returns 0 (the static-link no-op
1054    /// path mirrors C's `return 0;` exit at c:311).
1055    #[test]
1056    fn zprof_wrapper_returns_zero() {
1057        let _g = crate::test_util::global_state_lock();
1058        assert_eq!(
1059            zprof_wrapper(std::ptr::null(), std::ptr::null(), "foo", || {}),
1060            0,
1061        );
1062    }
1063
1064    /// Verifies `name_for_anonymous_function` formats as
1065    /// `name [filename:lineno]` per c:224-232, reading filename
1066    /// and flineno from `funcstack[0]` per S1 rule.
1067    #[test]
1068    fn name_for_anonymous_function_format() {
1069        let _g = crate::test_util::global_state_lock();
1070        // Push a frame onto FUNCSTACK so the fn reads it.
1071        {
1072            let mut stack = FUNCSTACK.lock().unwrap();
1073            stack.clear();
1074            stack.push(funcstack {
1075                filename: Some("/tmp/foo.zsh".to_string()),
1076                flineno: 42,
1077                ..Default::default()
1078            });
1079        }
1080        let s = name_for_anonymous_function("anon");
1081        // Cleanup so subsequent tests aren't polluted.
1082        FUNCSTACK.lock().unwrap().clear();
1083        assert_eq!(s, "anon [/tmp/foo.zsh:42]");
1084    }
1085
1086    /// `name_for_anonymous_function` with an EMPTY funcstack must
1087    /// return `"name [:0]"` — empty filename + zero lineno — not
1088    /// panic on the unwrap. The C body's `funcstack[0].flineno`
1089    /// would segfault on an empty stack; the Rust port must defend
1090    /// because nothing in zsh prevents the profiler from being
1091    /// invoked before the first function frame is pushed (e.g.
1092    /// during init scripts that contain anonymous functions at top
1093    /// level).
1094    #[test]
1095    fn name_for_anonymous_function_empty_funcstack_defaults() {
1096        let _g = crate::test_util::global_state_lock();
1097        // Ensure stack is empty.
1098        FUNCSTACK.lock().unwrap().clear();
1099        // No panic on first().unwrap() — the fn uses Option chains.
1100        let s = std::panic::catch_unwind(|| name_for_anonymous_function("anon"))
1101            .expect("must not panic on empty funcstack");
1102        assert_eq!(
1103            s, "anon [:0]",
1104            "empty funcstack → empty filename + 0 lineno; got {:?}",
1105            s
1106        );
1107    }
1108
1109    /// c:97 — `findpfunc` on an empty table returns None. A
1110    /// regression that returns 0 (a valid index!) would silently
1111    /// corrupt every subsequent per-function profile accumulation.
1112    #[test]
1113    fn findpfunc_empty_table_returns_none() {
1114        let _g = crate::test_util::global_state_lock();
1115        let _g = TEST_LOCK.lock().unwrap();
1116        reset_state();
1117        assert!(findpfunc("never-called").is_none());
1118    }
1119
1120    /// c:97 — `findpfunc` after two inserts returns the right index.
1121    /// Pin the index-zero-based contract because the find result
1122    /// feeds back into CALLS[i].
1123    #[test]
1124    fn findpfunc_returns_correct_index_after_insert() {
1125        let _g = crate::test_util::global_state_lock();
1126        let _g = TEST_LOCK.lock().unwrap();
1127        reset_state();
1128        CALLS.lock().unwrap().push(Pfunc {
1129            name: "alpha".into(),
1130            ..Default::default()
1131        });
1132        CALLS.lock().unwrap().push(Pfunc {
1133            name: "beta".into(),
1134            ..Default::default()
1135        });
1136        assert_eq!(findpfunc("alpha"), Some(0));
1137        assert_eq!(findpfunc("beta"), Some(1));
1138        assert!(findpfunc("gamma").is_none());
1139    }
1140
1141    /// c:109 — `findparc(f, t)` on an empty arcs table → None.
1142    #[test]
1143    fn findparc_empty_table_returns_none() {
1144        let _g = crate::test_util::global_state_lock();
1145        let _g = TEST_LOCK.lock().unwrap();
1146        reset_state();
1147        assert!(findparc(0, 1).is_none());
1148    }
1149
1150    /// c:109 — `findparc` distinguishes (f1, t1) from (f1, t2):
1151    /// same `from`, different `to` is a different arc.
1152    #[test]
1153    fn findparc_distinguishes_to_field() {
1154        let _g = crate::test_util::global_state_lock();
1155        let _g = TEST_LOCK.lock().unwrap();
1156        reset_state();
1157        ARCS.lock().unwrap().push(Parc {
1158            from: 0,
1159            to: 1,
1160            ..Default::default()
1161        });
1162        ARCS.lock().unwrap().push(Parc {
1163            from: 0,
1164            to: 2,
1165            ..Default::default()
1166        });
1167        assert_eq!(findparc(0, 1), Some(0));
1168        assert_eq!(findparc(0, 2), Some(1));
1169        assert!(findparc(0, 99).is_none());
1170        assert!(findparc(99, 1).is_none());
1171    }
1172
1173    /// c:121 — `cmpsfuncs` compares by `self_time` DESCENDING (C
1174    /// source: `(int)((*b)->self < (*a)->self) - (int)((*a)->self < (*b)->self)`
1175    /// — i.e. higher self_time sorts first). Pin the direction
1176    /// because a regen that flips to ascending would silently
1177    /// invert the user-facing `zprof` output ordering.
1178    #[test]
1179    fn cmpsfuncs_compares_by_self_time_descending() {
1180        let _g = crate::test_util::global_state_lock();
1181        let high = Pfunc {
1182            name: "_".into(),
1183            self_time: 100.0,
1184            ..Default::default()
1185        };
1186        let low = Pfunc {
1187            name: "_".into(),
1188            self_time: 1.0,
1189            ..Default::default()
1190        };
1191        // higher self_time sorts FIRST → Ordering::Less for (high, low)
1192        assert_eq!(cmpsfuncs(&high, &low), std::cmp::Ordering::Less);
1193        assert_eq!(cmpsfuncs(&low, &high), std::cmp::Ordering::Greater);
1194        assert_eq!(cmpsfuncs(&high, &high), std::cmp::Ordering::Equal);
1195    }
1196
1197    /// c:74 — `freepfuncs` empties the input vec.
1198    #[test]
1199    fn freepfuncs_empties_input_vec() {
1200        let _g = crate::test_util::global_state_lock();
1201        let mut v = vec![
1202            Pfunc {
1203                name: "x".into(),
1204                ..Default::default()
1205            },
1206            Pfunc {
1207                name: "y".into(),
1208                ..Default::default()
1209            },
1210        ];
1211        freepfuncs(&mut v);
1212        assert!(v.is_empty(), "freepfuncs must clear the input vec");
1213    }
1214
1215    /// c:86 — `freeparcs` empties the input arc vec.
1216    #[test]
1217    fn freeparcs_empties_input_vec() {
1218        let _g = crate::test_util::global_state_lock();
1219        let mut v = vec![
1220            Parc {
1221                from: 0,
1222                to: 1,
1223                ..Default::default()
1224            },
1225            Parc {
1226                from: 2,
1227                to: 3,
1228                ..Default::default()
1229            },
1230        ];
1231        freeparcs(&mut v);
1232        assert!(v.is_empty(), "freeparcs must clear the input vec");
1233    }
1234
1235    /// c:121 vs c:127 — `cmpsfuncs` sorts by `self_time`, `cmptfuncs`
1236    /// sorts by `time` (cumulative). Pin they produce DIFFERENT
1237    /// orderings on an input where the two fields disagree, so a
1238    /// regen that aliases the field accessor in one of them gets
1239    /// caught.
1240    #[test]
1241    fn cmpsfuncs_and_cmptfuncs_differ_when_fields_disagree() {
1242        let _g = crate::test_util::global_state_lock();
1243        // `alpha` has high self_time but LOW cumulative time.
1244        // `beta`  has low self_time but HIGH cumulative time.
1245        let alpha = Pfunc {
1246            name: "_".into(),
1247            self_time: 100.0,
1248            time: 1.0,
1249            ..Default::default()
1250        };
1251        let beta = Pfunc {
1252            name: "_".into(),
1253            self_time: 1.0,
1254            time: 100.0,
1255            ..Default::default()
1256        };
1257        let by_self = cmpsfuncs(&alpha, &beta);
1258        let by_time = cmptfuncs(&alpha, &beta);
1259        // by_self: alpha has higher self_time → alpha sorts first → Less
1260        // by_time: beta has higher cumulative time → alpha sorts after → Greater
1261        assert_ne!(
1262            by_self, by_time,
1263            "cmpsfuncs (self_time) and cmptfuncs (time) must differ when fields disagree"
1264        );
1265    }
1266
1267    // ─── zsh-corpus pins for zprof helpers ────────────────────────
1268
1269    /// `cmpsfuncs` sorts higher self_time first.
1270    #[test]
1271    fn zprof_corpus_cmpsfuncs_higher_self_time_first() {
1272        let _g = crate::test_util::global_state_lock();
1273        let high = Pfunc {
1274            name: "high".into(),
1275            self_time: 100.0,
1276            ..Default::default()
1277        };
1278        let low = Pfunc {
1279            name: "low".into(),
1280            self_time: 1.0,
1281            ..Default::default()
1282        };
1283        assert_eq!(
1284            cmpsfuncs(&high, &low),
1285            std::cmp::Ordering::Less,
1286            "higher self_time sorts first"
1287        );
1288    }
1289
1290    /// `cmptfuncs` sorts higher total time first.
1291    #[test]
1292    fn zprof_corpus_cmptfuncs_higher_time_first() {
1293        let _g = crate::test_util::global_state_lock();
1294        let high = Pfunc {
1295            name: "high".into(),
1296            time: 100.0,
1297            ..Default::default()
1298        };
1299        let low = Pfunc {
1300            name: "low".into(),
1301            time: 1.0,
1302            ..Default::default()
1303        };
1304        assert_eq!(
1305            cmptfuncs(&high, &low),
1306            std::cmp::Ordering::Less,
1307            "higher total time sorts first"
1308        );
1309    }
1310
1311    /// `cmpsfuncs` equal self_time → Equal.
1312    #[test]
1313    fn zprof_corpus_cmpsfuncs_equal_self_time() {
1314        let _g = crate::test_util::global_state_lock();
1315        let a = Pfunc {
1316            name: "a".into(),
1317            self_time: 5.0,
1318            ..Default::default()
1319        };
1320        let b = Pfunc {
1321            name: "b".into(),
1322            self_time: 5.0,
1323            ..Default::default()
1324        };
1325        // Equal self_time may or may not tie-break by name; pin: not Greater
1326        let o = cmpsfuncs(&a, &b);
1327        assert_ne!(o, std::cmp::Ordering::Greater);
1328    }
1329
1330    /// `findpfunc` on empty list returns None.
1331    #[test]
1332    fn zprof_corpus_findpfunc_empty_returns_none() {
1333        let _g = crate::test_util::global_state_lock();
1334        assert!(findpfunc("__never_seen_function__").is_none());
1335    }
1336
1337    /// `findparc` with arbitrary indexes returns None on empty arcs.
1338    #[test]
1339    fn zprof_corpus_findparc_empty_returns_none() {
1340        let _g = crate::test_util::global_state_lock();
1341        assert!(findparc(usize::MAX, usize::MAX).is_none());
1342    }
1343
1344    /// `name_for_anonymous_function("(anon)")` includes the input name
1345    /// and bracket form `[filename:lineno]`.
1346    #[test]
1347    fn zprof_corpus_name_for_anonymous_function_format() {
1348        let _g = crate::test_util::global_state_lock();
1349        let s = name_for_anonymous_function("(anon)");
1350        assert!(s.starts_with("(anon)"), "starts with name, got {s:?}");
1351        assert!(s.contains('['), "has opening bracket");
1352        assert!(s.contains(']'), "has closing bracket");
1353        assert!(s.contains(':'), "has line-number separator");
1354    }
1355
1356    // ═══════════════════════════════════════════════════════════════════
1357    // C-parity tests for Src/Modules/zprof.c cmp comparators + free ops.
1358    // ═══════════════════════════════════════════════════════════════════
1359
1360    fn mk_pfunc(name: &str, self_time: f64, time: f64) -> Pfunc {
1361        Pfunc {
1362            name: name.to_string(),
1363            calls: 0,
1364            time,
1365            self_time,
1366            num: 0,
1367        }
1368    }
1369
1370    fn mk_parc(from: usize, to: usize, time: f64) -> Parc {
1371        Parc {
1372            from,
1373            to,
1374            calls: 0,
1375            time,
1376            ..Default::default()
1377        }
1378    }
1379
1380    /// c:121 — `cmpsfuncs` is descending by self_time: a > b → Less
1381    /// (sort places larger first).
1382    #[test]
1383    fn cmpsfuncs_descending_by_self_time() {
1384        let a = mk_pfunc("a", 10.0, 0.0);
1385        let b = mk_pfunc("b", 5.0, 0.0);
1386        assert_eq!(
1387            cmpsfuncs(&a, &b),
1388            std::cmp::Ordering::Less,
1389            "larger self_time sorts first"
1390        );
1391        assert_eq!(cmpsfuncs(&b, &a), std::cmp::Ordering::Greater);
1392    }
1393
1394    /// c:121 — equal self_time → Equal.
1395    #[test]
1396    fn cmpsfuncs_equal_returns_equal() {
1397        let a = mk_pfunc("a", 7.5, 0.0);
1398        let b = mk_pfunc("b", 7.5, 99.0); // different time, equal self_time
1399        assert_eq!(cmpsfuncs(&a, &b), std::cmp::Ordering::Equal);
1400    }
1401
1402    /// c:127 — `cmptfuncs` descending by total time (ignores self_time).
1403    #[test]
1404    fn cmptfuncs_descending_by_total_time() {
1405        let a = mk_pfunc("a", 0.0, 10.0);
1406        let b = mk_pfunc("b", 99.0, 5.0); // larger self but smaller time
1407        assert_eq!(
1408            cmptfuncs(&a, &b),
1409            std::cmp::Ordering::Less,
1410            "larger total time sorts first regardless of self_time"
1411        );
1412    }
1413
1414    /// c:133 — `cmpparcs` descending by time.
1415    #[test]
1416    fn cmpparcs_descending_by_time() {
1417        let a = mk_parc(0, 1, 10.0);
1418        let b = mk_parc(0, 1, 5.0);
1419        assert_eq!(cmpparcs(&a, &b), std::cmp::Ordering::Less);
1420        assert_eq!(cmpparcs(&b, &a), std::cmp::Ordering::Greater);
1421    }
1422
1423    /// c:121 — NaN comparison → Equal (partial_cmp unwrap_or branch).
1424    #[test]
1425    fn cmpsfuncs_nan_returns_equal() {
1426        let a = mk_pfunc("a", f64::NAN, 0.0);
1427        let b = mk_pfunc("b", 5.0, 0.0);
1428        assert_eq!(
1429            cmpsfuncs(&a, &b),
1430            std::cmp::Ordering::Equal,
1431            "NaN partial_cmp returns None → Equal fallback"
1432        );
1433    }
1434
1435    /// c:74 — `freepfuncs` empties the vec.
1436    #[test]
1437    fn freepfuncs_clears_vec() {
1438        let mut v = vec![mk_pfunc("a", 0.0, 0.0), mk_pfunc("b", 0.0, 0.0)];
1439        freepfuncs(&mut v);
1440        assert!(v.is_empty(), "freepfuncs must clear the Vec");
1441    }
1442
1443    /// c:86 — `freeparcs` empties the vec.
1444    #[test]
1445    fn freeparcs_clears_vec() {
1446        let mut v = vec![mk_parc(0, 1, 0.0), mk_parc(1, 2, 0.0)];
1447        freeparcs(&mut v);
1448        assert!(v.is_empty());
1449    }
1450
1451    /// c:74 — `freepfuncs` on empty vec is a no-op.
1452    #[test]
1453    fn freepfuncs_empty_is_noop() {
1454        let mut v: Vec<Pfunc> = Vec::new();
1455        freepfuncs(&mut v);
1456        assert!(v.is_empty());
1457    }
1458
1459    /// c:97 — `findpfunc` is deterministic for the same lookup.
1460    #[test]
1461    fn findpfunc_is_deterministic_for_missing() {
1462        let _g = crate::test_util::global_state_lock();
1463        let first = findpfunc("__nope__");
1464        for _ in 0..10 {
1465            assert_eq!(findpfunc("__nope__"), first);
1466        }
1467    }
1468
1469    // ═══════════════════════════════════════════════════════════════════
1470    // Additional C-parity tests for Src/Modules/zprof.c
1471    // c:123 findpfunc / c:135 findparc / c:152-170 cmp* / c:418 name_for_anon /
1472    // c:509 zprof_wrapper / c:682+ lifecycle
1473    // ═══════════════════════════════════════════════════════════════════
1474
1475    /// c:135 — `findparc(0,0)` on empty table returns None.
1476    #[test]
1477    fn findparc_zero_zero_empty_table_returns_none() {
1478        let _g = crate::test_util::global_state_lock();
1479        reset_state();
1480        assert_eq!(findparc(0, 0), None);
1481    }
1482
1483    /// c:123 — `findpfunc("")` empty name returns None on empty table.
1484    #[test]
1485    fn findpfunc_empty_name_empty_table_returns_none() {
1486        let _g = crate::test_util::global_state_lock();
1487        reset_state();
1488        assert_eq!(findpfunc(""), None);
1489    }
1490
1491    /// c:152 — `cmpsfuncs(a, a)` is Equal (reflexive).
1492    #[test]
1493    fn cmpsfuncs_reflexive() {
1494        let a = mk_pfunc("x", 1.0, 2.0);
1495        assert_eq!(cmpsfuncs(&a, &a), std::cmp::Ordering::Equal);
1496    }
1497
1498    /// c:161 — `cmptfuncs(a, a)` is Equal (reflexive).
1499    #[test]
1500    fn cmptfuncs_reflexive() {
1501        let a = mk_pfunc("x", 1.0, 2.0);
1502        assert_eq!(cmptfuncs(&a, &a), std::cmp::Ordering::Equal);
1503    }
1504
1505    /// c:170 — `cmpparcs(a, a)` is Equal (reflexive).
1506    #[test]
1507    fn cmpparcs_reflexive() {
1508        let a = mk_parc(0, 1, 5.0);
1509        assert_eq!(cmpparcs(&a, &a), std::cmp::Ordering::Equal);
1510    }
1511
1512    /// c:152 — `cmpsfuncs` is antisymmetric: if a vs b is X, b vs a
1513    /// is X.reverse().
1514    #[test]
1515    fn cmpsfuncs_antisymmetric() {
1516        let a = mk_pfunc("a", 1.0, 5.0);
1517        let b = mk_pfunc("b", 2.0, 5.0);
1518        let ab = cmpsfuncs(&a, &b);
1519        let ba = cmpsfuncs(&b, &a);
1520        assert_eq!(ab.reverse(), ba, "must be antisymmetric");
1521    }
1522
1523    /// c:509 — `zprof_wrapper(null, null, "")` no panic.
1524    #[test]
1525    fn zprof_wrapper_empty_name_no_panic() {
1526        let _g = crate::test_util::global_state_lock();
1527        let _ = zprof_wrapper(std::ptr::null(), std::ptr::null(), "", || {});
1528    }
1529
1530    /// c:418 — `name_for_anonymous_function` is deterministic.
1531    #[test]
1532    fn name_for_anonymous_function_is_deterministic() {
1533        let _g = crate::test_util::global_state_lock();
1534        let first = name_for_anonymous_function("anon");
1535        for _ in 0..5 {
1536            assert_eq!(name_for_anonymous_function("anon"), first);
1537        }
1538    }
1539
1540    /// c:682-... — full lifecycle setup→features→enables→boot→cleanup→finish.
1541    #[test]
1542    fn zprof_full_lifecycle_returns_zero_for_all() {
1543        let _g = crate::test_util::global_state_lock();
1544        let null = std::ptr::null();
1545        assert_eq!(setup_(null), 0);
1546        let mut feats = Vec::new();
1547        let _ = features_(null, &mut feats);
1548        let mut enables: Option<Vec<i32>> = None;
1549        let _ = enables_(null, &mut enables);
1550        assert_eq!(boot_(null), 0);
1551        assert_eq!(cleanup_(null), 0);
1552    }
1553
1554    // ═══════════════════════════════════════════════════════════════════
1555    // Additional C-parity tests for Src/Modules/zprof.c
1556    // c:105 freepfuncs / c:113 freeparcs / c:123 findpfunc / c:135 findparc
1557    // c:152 cmpsfuncs / c:161 cmptfuncs / c:170 cmpparcs /
1558    // c:418 name_for_anonymous_function / c:509 zprof_wrapper
1559    // ═══════════════════════════════════════════════════════════════════
1560
1561    /// c:105 — `freepfuncs` returns void (compile-time pin).
1562    #[test]
1563    fn freepfuncs_returns_void() {
1564        let mut v: Vec<Pfunc> = vec![];
1565        let _: () = freepfuncs(&mut v);
1566    }
1567
1568    /// c:113 — `freeparcs` returns void.
1569    #[test]
1570    fn freeparcs_returns_void() {
1571        let mut v: Vec<Parc> = vec![];
1572        let _: () = freeparcs(&mut v);
1573    }
1574
1575    /// c:123 — `findpfunc` returns Option<usize> (compile-time pin).
1576    #[test]
1577    fn findpfunc_returns_option_usize_type() {
1578        let _g = crate::test_util::global_state_lock();
1579        let _: Option<usize> = findpfunc("anything");
1580    }
1581
1582    /// c:135 — `findparc(N, M)` returns Option<usize>.
1583    #[test]
1584    fn findparc_returns_option_usize_type() {
1585        let _g = crate::test_util::global_state_lock();
1586        let _: Option<usize> = findparc(0, 0);
1587    }
1588
1589    /// c:152 — `cmpsfuncs` returns Ordering (compile-time pin).
1590    #[test]
1591    fn cmpsfuncs_returns_ordering_type() {
1592        let a = mk_pfunc("a", 0.0, 0.0);
1593        let _: std::cmp::Ordering = cmpsfuncs(&a, &a);
1594    }
1595
1596    /// c:161 — `cmptfuncs` returns Ordering.
1597    #[test]
1598    fn cmptfuncs_returns_ordering_type() {
1599        let a = mk_pfunc("a", 0.0, 0.0);
1600        let _: std::cmp::Ordering = cmptfuncs(&a, &a);
1601    }
1602
1603    /// c:170 — `cmpparcs` returns Ordering.
1604    #[test]
1605    fn cmpparcs_returns_ordering_type() {
1606        let a = mk_parc(0, 1, 0.0);
1607        let _: std::cmp::Ordering = cmpparcs(&a, &a);
1608    }
1609
1610    /// c:418 — `name_for_anonymous_function` returns String type pin.
1611    #[test]
1612    fn name_for_anonymous_function_returns_string_type() {
1613        let _g = crate::test_util::global_state_lock();
1614        let _: String = name_for_anonymous_function("anon");
1615    }
1616
1617    /// c:509 — `zprof_wrapper` returns i32 type pin.
1618    #[test]
1619    fn zprof_wrapper_returns_i32_type() {
1620        let _g = crate::test_util::global_state_lock();
1621        let _: i32 = zprof_wrapper(std::ptr::null(), std::ptr::null(), "x", || {});
1622    }
1623
1624    /// c:105 — `freepfuncs` empties the vec.
1625    #[test]
1626    fn freepfuncs_empties_vec_pin() {
1627        let mut v = vec![mk_pfunc("a", 0.0, 0.0)];
1628        freepfuncs(&mut v);
1629        assert!(v.is_empty(), "freepfuncs must empty");
1630    }
1631
1632    /// c:113 — `freeparcs` empties the vec.
1633    #[test]
1634    fn freeparcs_empties_vec_pin() {
1635        let mut v = vec![mk_parc(0, 1, 0.0)];
1636        freeparcs(&mut v);
1637        assert!(v.is_empty(), "freeparcs must empty");
1638    }
1639
1640    // ═══════════════════════════════════════════════════════════════════
1641    // Additional C-parity pins for Src/Modules/zprof.c
1642    // c:123 findpfunc / c:135 findparc / c:152-170 cmp* / c:193 bin_zprof /
1643    // c:418 name_for_anonymous_function / c:682-719 lifecycle hooks
1644    // ═══════════════════════════════════════════════════════════════════
1645
1646    /// c:123 — `findpfunc("")` empty name doesn't panic.
1647    #[test]
1648    fn findpfunc_empty_name_no_panic() {
1649        let _g = crate::test_util::global_state_lock();
1650        let _ = findpfunc("");
1651    }
1652
1653    /// c:123 — `findpfunc` is pure (no observable mutation across calls).
1654    #[test]
1655    fn findpfunc_repeated_calls_deterministic() {
1656        let _g = crate::test_util::global_state_lock();
1657        let a = findpfunc("__never_real_pfn__");
1658        let b = findpfunc("__never_real_pfn__");
1659        assert_eq!(a, b, "findpfunc must be pure across calls");
1660    }
1661
1662    /// c:135 — `findparc(0, 0)` doesn't panic.
1663    #[test]
1664    fn findparc_zero_indices_no_panic() {
1665        let _g = crate::test_util::global_state_lock();
1666        let _ = findparc(0, 0);
1667    }
1668
1669    /// c:135 — `findparc(usize::MAX, usize::MAX)` doesn't panic.
1670    #[test]
1671    fn findparc_extreme_indices_no_panic() {
1672        let _g = crate::test_util::global_state_lock();
1673        let _ = findparc(usize::MAX, usize::MAX);
1674    }
1675
1676    /// c:152 — `cmpsfuncs` reflexivity (alt).
1677    #[test]
1678    fn cmpsfuncs_reflexive_alt() {
1679        let p = mk_pfunc("a", 1.0, 2.0);
1680        let p2 = p.clone();
1681        assert_eq!(
1682            cmpsfuncs(&p, &p2),
1683            std::cmp::Ordering::Equal,
1684            "cmpsfuncs(x, x.clone()) must be Equal"
1685        );
1686    }
1687
1688    /// c:161 — `cmptfuncs` reflexivity (alt).
1689    #[test]
1690    fn cmptfuncs_reflexive_alt() {
1691        let p = mk_pfunc("a", 1.0, 2.0);
1692        let p2 = p.clone();
1693        assert_eq!(
1694            cmptfuncs(&p, &p2),
1695            std::cmp::Ordering::Equal,
1696            "cmptfuncs(x, x.clone()) must be Equal"
1697        );
1698    }
1699
1700    /// c:170 — `cmpparcs` reflexivity (alt).
1701    #[test]
1702    fn cmpparcs_reflexive_alt() {
1703        let a = mk_parc(0, 1, 5.0);
1704        let b = a.clone();
1705        assert_eq!(cmpparcs(&a, &b), std::cmp::Ordering::Equal);
1706    }
1707
1708    /// c:193 — `bin_zprof` empty args non-negative.
1709    #[test]
1710    fn bin_zprof_empty_args_non_negative() {
1711        let _g = crate::test_util::global_state_lock();
1712        let ops = crate::ported::zsh_h::options {
1713            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1714            args: Vec::new(),
1715            argscount: 0,
1716            argsalloc: 0,
1717        };
1718        let r = bin_zprof("zprof", &[], &ops, 0);
1719        assert!(r >= 0, "bin_zprof empty must be ≥ 0, got {}", r);
1720    }
1721
1722    /// c:418 — `name_for_anonymous_function("")` returns String type (alt).
1723    #[test]
1724    fn name_for_anonymous_function_returns_string_type_alt() {
1725        let _g = crate::test_util::global_state_lock();
1726        let _: String = name_for_anonymous_function("");
1727    }
1728
1729    /// c:418 — synthesized name is non-empty.
1730    #[test]
1731    fn name_for_anonymous_function_returns_non_empty() {
1732        let _g = crate::test_util::global_state_lock();
1733        let n = name_for_anonymous_function("");
1734        assert!(!n.is_empty(), "synthesized name must not be empty");
1735    }
1736
1737    /// c:682 — `setup_` is idempotent.
1738    #[test]
1739    fn zprof_setup_idempotent_alt_pin() {
1740        let _g = crate::test_util::global_state_lock();
1741        for _ in 0..10 {
1742            assert_eq!(setup_(std::ptr::null()), 0);
1743        }
1744    }
1745
1746    /// c:719 — `cleanup_` is idempotent.
1747    #[test]
1748    fn zprof_cleanup_idempotent_alt_pin() {
1749        let _g = crate::test_util::global_state_lock();
1750        for _ in 0..10 {
1751            assert_eq!(cleanup_(std::ptr::null()), 0);
1752        }
1753    }
1754}