Skip to main content

metricchrono_ffi/
lib.rs

1#![allow(clippy::missing_safety_doc)]
2
3use std::cell::{Cell, RefCell};
4use std::ffi::{c_char, c_int, c_void};
5use std::panic::{catch_unwind, AssertUnwindSafe};
6use std::ptr;
7
8use metricchrono_core::{
9    adaptive_ladder_distance, carry_rules, classify_regime, custom_ladder, geometric_ladder,
10    ladder_distance, ladder_pair, normalize_ticks, progress_efficiency, simple_weight_update,
11    smooth_tick_distance, tick_distance, tick_pair, validate_ladder, weighted_consensus, Absolute,
12    CoverageMeter, Euclidean, EventLog, Metric, MetricChronoError, MetricFn, Normalization,
13    OperatingRegime, PromotionCounter, SmoothParams, Tier,
14};
15
16const MC_METRIC_EUCLIDEAN: c_int = 0;
17const MC_METRIC_ABSOLUTE: c_int = 1;
18
19const MC_REGIME_QUIESCENT: c_int = 0;
20const MC_REGIME_PROGRESS: c_int = 1;
21const MC_REGIME_CHURN: c_int = 2;
22const MC_REGIME_CREEP: c_int = 3;
23
24const MC_NORMALIZATION_NONE: c_int = 0;
25const MC_NORMALIZATION_UNIT_MAX: c_int = 1;
26const MC_NORMALIZATION_TANH: c_int = 2;
27
28thread_local! {
29    static LAST_ERROR: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
30    static LAST_ERROR_SET_IN_CALL: Cell<bool> = const { Cell::new(false) };
31}
32
33#[repr(C)]
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub enum MCStatus {
36    Ok = 0,
37    Null = 1,
38    InvalidArgument = 2,
39    BufferTooSmall = 3,
40    Panic = 255,
41}
42
43#[repr(C)]
44#[derive(Clone, Copy, Debug)]
45pub struct MCTier {
46    pub epsilon: f64,
47    pub delta: f64,
48    pub p: f64,
49    pub epsilon_ref: f64,
50}
51
52#[repr(C)]
53#[derive(Clone, Copy, Debug)]
54pub struct MCZoomDecision {
55    pub evaluated_tiers: usize,
56    pub first_inactive_tier: usize,
57    pub has_first_inactive_tier: bool,
58    pub stopped_early: bool,
59}
60
61pub struct MCEventLog {
62    inner: EventLog<u64>,
63}
64
65pub struct MCLadder {
66    tiers: Vec<Tier>,
67}
68
69pub struct MCPromotionCounter {
70    inner: PromotionCounter,
71}
72
73/// Caller-supplied distance function over two `dim`-length state vectors.
74///
75/// The callback must not unwind, must remain callable for the lifetime of the
76/// meter it is registered with, and `user_data` (passed through verbatim) must
77/// outlive the meter. Returning NaN rejects admission, which is the safe
78/// failure mode for a callback that cannot compute a distance.
79pub type MCDistanceFn =
80    unsafe extern "C" fn(a: *const f64, b: *const f64, dim: usize, user_data: *mut c_void) -> f64;
81
82#[derive(Clone, Copy)]
83enum CoverageMetric {
84    Builtin(c_int),
85    Callback {
86        callback: MCDistanceFn,
87        user_data: *mut c_void,
88    },
89}
90
91pub struct MCCoverageMeter {
92    inner: CoverageMeter<Vec<f64>>,
93    dim: usize,
94    metric: CoverageMetric,
95    /// Reusable state buffer so rejected observations allocate nothing.
96    scratch: Vec<f64>,
97}
98
99impl From<MCTier> for Tier {
100    fn from(value: MCTier) -> Self {
101        Self {
102            epsilon: value.epsilon,
103            delta: value.delta,
104            p: value.p,
105            epsilon_ref: value.epsilon_ref,
106        }
107    }
108}
109
110impl From<Tier> for MCTier {
111    fn from(value: Tier) -> Self {
112        Self {
113            epsilon: value.epsilon,
114            delta: value.delta,
115            p: value.p,
116            epsilon_ref: value.epsilon_ref,
117        }
118    }
119}
120
121fn ffi_status<F>(func: F) -> MCStatus
122where
123    F: FnOnce() -> MCStatus,
124{
125    begin_ffi_call();
126    match catch_unwind(AssertUnwindSafe(func)) {
127        Ok(status) => finish_status(status),
128        Err(_) => {
129            set_last_error("panic");
130            MCStatus::Panic
131        }
132    }
133}
134
135fn begin_ffi_call() {
136    LAST_ERROR_SET_IN_CALL.with(|flag| flag.set(false));
137}
138
139fn set_last_error(message: impl AsRef<str>) {
140    LAST_ERROR.with(|slot| {
141        let mut slot = slot.borrow_mut();
142        slot.clear();
143        slot.extend_from_slice(message.as_ref().as_bytes());
144    });
145    LAST_ERROR_SET_IN_CALL.with(|flag| flag.set(true));
146}
147
148fn finish_status(status: MCStatus) -> MCStatus {
149    if status != MCStatus::Ok {
150        let already_set = LAST_ERROR_SET_IN_CALL.with(Cell::get);
151        if !already_set {
152            set_last_error(status_message(status));
153        }
154    }
155    status
156}
157
158fn status_message(status: MCStatus) -> &'static str {
159    match status {
160        MCStatus::Ok => "ok",
161        MCStatus::Null => "null pointer",
162        MCStatus::InvalidArgument => "invalid argument",
163        MCStatus::BufferTooSmall => "buffer too small",
164        MCStatus::Panic => "panic",
165    }
166}
167
168fn status_from_error(error: MetricChronoError) -> MCStatus {
169    let status = match error {
170        MetricChronoError::OutputTooSmall { .. } => MCStatus::BufferTooSmall,
171        _ => MCStatus::InvalidArgument,
172    };
173    set_last_error(error.to_string());
174    status
175}
176
177fn invalid_argument(message: &'static str) -> MCStatus {
178    status_from_error(MetricChronoError::InvalidArgument(message))
179}
180
181fn buffer_too_small(needed: usize, actual: usize) -> MCStatus {
182    status_from_error(MetricChronoError::OutputTooSmall { needed, actual })
183}
184
185fn normalization_from_id(id: c_int) -> Result<Normalization, MetricChronoError> {
186    match id {
187        MC_NORMALIZATION_NONE => Ok(Normalization::None),
188        MC_NORMALIZATION_UNIT_MAX => Ok(Normalization::UnitMax),
189        MC_NORMALIZATION_TANH => Ok(Normalization::Tanh),
190        _ => Err(MetricChronoError::InvalidArgument(
191            "unknown normalization id",
192        )),
193    }
194}
195
196#[no_mangle]
197pub extern "C" fn mc_error_message(status: c_int) -> *const c_char {
198    match status {
199        0 => b"ok\0".as_ptr().cast(),
200        1 => b"null pointer\0".as_ptr().cast(),
201        2 => b"invalid argument\0".as_ptr().cast(),
202        3 => b"buffer too small\0".as_ptr().cast(),
203        255 => b"panic\0".as_ptr().cast(),
204        _ => b"unknown status\0".as_ptr().cast(),
205    }
206}
207
208#[no_mangle]
209pub unsafe extern "C" fn mc_last_error_message(
210    buf: *mut c_char,
211    cap: usize,
212    out_len: *mut usize,
213) -> MCStatus {
214    match catch_unwind(AssertUnwindSafe(|| {
215        let Some(out_len) = (unsafe { out_len.as_mut() }) else {
216            set_last_error("null pointer");
217            return MCStatus::Null;
218        };
219
220        let needed = LAST_ERROR.with(|slot| slot.borrow().len() + 1);
221        *out_len = needed;
222        if cap < needed {
223            return MCStatus::BufferTooSmall;
224        }
225
226        if buf.is_null() {
227            set_last_error("null pointer");
228            return MCStatus::Null;
229        }
230
231        LAST_ERROR.with(|slot| {
232            let message = slot.borrow();
233            unsafe {
234                ptr::copy_nonoverlapping(message.as_ptr().cast::<c_char>(), buf, message.len());
235                *buf.add(message.len()) = 0;
236            }
237        });
238        MCStatus::Ok
239    })) {
240        Ok(status) => status,
241        Err(_) => {
242            set_last_error("panic");
243            MCStatus::Panic
244        }
245    }
246}
247
248unsafe fn slice_from_ptr<'a, T>(ptr: *const T, len: usize) -> Option<&'a [T]> {
249    if len == 0 {
250        Some(&[])
251    } else if ptr.is_null() {
252        None
253    } else {
254        Some(std::slice::from_raw_parts(ptr, len))
255    }
256}
257
258unsafe fn slice_from_mut_ptr<'a, T>(ptr: *mut T, len: usize) -> Option<&'a mut [T]> {
259    if len == 0 {
260        Some(&mut [])
261    } else if ptr.is_null() {
262        None
263    } else {
264        Some(std::slice::from_raw_parts_mut(ptr, len))
265    }
266}
267
268fn create_ladder(tiers: &[MCTier], out: &mut *mut MCLadder) -> MCStatus {
269    let tiers: Vec<Tier> = tiers.iter().copied().map(Tier::from).collect();
270    match custom_ladder(tiers) {
271        Ok(tiers) => {
272            *out = Box::into_raw(Box::new(MCLadder { tiers }));
273            MCStatus::Ok
274        }
275        Err(error) => {
276            *out = ptr::null_mut();
277            status_from_error(error)
278        }
279    }
280}
281
282#[no_mangle]
283pub unsafe extern "C" fn mc_tier_new(
284    epsilon: f64,
285    delta: f64,
286    p: f64,
287    epsilon_ref: f64,
288    out: *mut MCTier,
289) -> MCStatus {
290    ffi_status(|| {
291        let Some(out) = (unsafe { out.as_mut() }) else {
292            return MCStatus::Null;
293        };
294        match Tier::new(epsilon, delta, p, epsilon_ref) {
295            Ok(tier) => {
296                *out = tier.into();
297                MCStatus::Ok
298            }
299            Err(error) => status_from_error(error),
300        }
301    })
302}
303
304#[no_mangle]
305pub unsafe extern "C" fn mc_ladder_new(
306    tiers: *const MCTier,
307    len: usize,
308    out: *mut *mut MCLadder,
309) -> MCStatus {
310    ffi_status(|| {
311        let Some(out) = (unsafe { out.as_mut() }) else {
312            return MCStatus::Null;
313        };
314        let Some(tiers) = (unsafe { slice_from_ptr(tiers, len) }) else {
315            return MCStatus::Null;
316        };
317        create_ladder(tiers, out)
318    })
319}
320
321#[no_mangle]
322pub unsafe extern "C" fn mc_custom_ladder(
323    tiers: *const MCTier,
324    len: usize,
325    out: *mut *mut MCLadder,
326) -> MCStatus {
327    ffi_status(|| {
328        let Some(out) = (unsafe { out.as_mut() }) else {
329            return MCStatus::Null;
330        };
331        let Some(tiers) = (unsafe { slice_from_ptr(tiers, len) }) else {
332            return MCStatus::Null;
333        };
334        create_ladder(tiers, out)
335    })
336}
337
338#[no_mangle]
339pub unsafe extern "C" fn mc_ladder_free(ladder: *mut MCLadder) {
340    if ladder.is_null() {
341        return;
342    }
343    let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
344        drop(Box::from_raw(ladder));
345    }));
346}
347
348#[no_mangle]
349pub unsafe extern "C" fn mc_ladder_len(ladder: *const MCLadder, out_len: *mut usize) -> MCStatus {
350    ffi_status(|| {
351        let Some(ladder) = (unsafe { ladder.as_ref() }) else {
352            return MCStatus::Null;
353        };
354        let Some(out_len) = (unsafe { out_len.as_mut() }) else {
355            return MCStatus::Null;
356        };
357        *out_len = ladder.tiers.len();
358        MCStatus::Ok
359    })
360}
361
362#[no_mangle]
363pub unsafe extern "C" fn mc_validate_ladder(ladder: *const MCLadder) -> MCStatus {
364    ffi_status(|| {
365        let Some(ladder) = (unsafe { ladder.as_ref() }) else {
366            return MCStatus::Null;
367        };
368        validate_ladder(&ladder.tiers).map_or_else(status_from_error, |_| MCStatus::Ok)
369    })
370}
371
372#[no_mangle]
373pub unsafe extern "C" fn mc_ladder_distance_owned(
374    ladder: *const MCLadder,
375    distance: f64,
376    out: *mut f64,
377    out_len: usize,
378) -> MCStatus {
379    ffi_status(|| {
380        let Some(ladder) = (unsafe { ladder.as_ref() }) else {
381            return MCStatus::Null;
382        };
383        let Some(out) = (unsafe { slice_from_mut_ptr(out, out_len) }) else {
384            return MCStatus::Null;
385        };
386        ladder_distance(distance, &ladder.tiers, out)
387            .map_or_else(status_from_error, |_| MCStatus::Ok)
388    })
389}
390
391#[no_mangle]
392pub unsafe extern "C" fn mc_tick_distance(distance: f64, tier: MCTier, out: *mut f64) -> MCStatus {
393    ffi_status(|| {
394        let Some(out) = (unsafe { out.as_mut() }) else {
395            return MCStatus::Null;
396        };
397        let tier = Tier::from(tier);
398        match metricchrono_core::try_tick_distance(distance, tier) {
399            Ok(value) => {
400                *out = value;
401                MCStatus::Ok
402            }
403            Err(error) => status_from_error(error),
404        }
405    })
406}
407
408#[no_mangle]
409pub unsafe extern "C" fn mc_euclidean_distance(
410    a: *const f64,
411    b: *const f64,
412    len: usize,
413    out: *mut f64,
414) -> MCStatus {
415    ffi_status(|| {
416        let Some(out) = (unsafe { out.as_mut() }) else {
417            return MCStatus::Null;
418        };
419        let Some(a) = (unsafe { slice_from_ptr(a, len) }) else {
420            return MCStatus::Null;
421        };
422        let Some(b) = (unsafe { slice_from_ptr(b, len) }) else {
423            return MCStatus::Null;
424        };
425        *out = Euclidean.distance(a, b);
426        MCStatus::Ok
427    })
428}
429
430#[no_mangle]
431pub unsafe extern "C" fn mc_absolute_distance(
432    a: *const f64,
433    b: *const f64,
434    len: usize,
435    out: *mut f64,
436) -> MCStatus {
437    ffi_status(|| {
438        if len != 1 {
439            return invalid_argument("absolute metric requires len == 1");
440        }
441        let Some(out) = (unsafe { out.as_mut() }) else {
442            return MCStatus::Null;
443        };
444        let Some(a) = (unsafe { slice_from_ptr(a, len) }) else {
445            return MCStatus::Null;
446        };
447        let Some(b) = (unsafe { slice_from_ptr(b, len) }) else {
448            return MCStatus::Null;
449        };
450        *out = Absolute.distance(&a[0], &b[0]);
451        MCStatus::Ok
452    })
453}
454
455#[no_mangle]
456pub unsafe extern "C" fn mc_tick_pair(
457    metric_id: c_int,
458    a: *const f64,
459    b: *const f64,
460    len: usize,
461    tier: MCTier,
462    out: *mut f64,
463) -> MCStatus {
464    ffi_status(|| {
465        let Some(out) = (unsafe { out.as_mut() }) else {
466            return MCStatus::Null;
467        };
468        match metric_id {
469            MC_METRIC_EUCLIDEAN => {
470                let Some(a) = (unsafe { slice_from_ptr(a, len) }) else {
471                    return MCStatus::Null;
472                };
473                let Some(b) = (unsafe { slice_from_ptr(b, len) }) else {
474                    return MCStatus::Null;
475                };
476                match tick_pair(a, b, &Euclidean, Tier::from(tier)) {
477                    Ok(value) => {
478                        *out = value;
479                        MCStatus::Ok
480                    }
481                    Err(error) => status_from_error(error),
482                }
483            }
484            MC_METRIC_ABSOLUTE => {
485                if len != 1 {
486                    return invalid_argument("absolute metric requires len == 1");
487                }
488                let Some(a) = (unsafe { slice_from_ptr(a, len) }) else {
489                    return MCStatus::Null;
490                };
491                let Some(b) = (unsafe { slice_from_ptr(b, len) }) else {
492                    return MCStatus::Null;
493                };
494                match tick_pair(&a[0], &b[0], &Absolute, Tier::from(tier)) {
495                    Ok(value) => {
496                        *out = value;
497                        MCStatus::Ok
498                    }
499                    Err(error) => status_from_error(error),
500                }
501            }
502            _ => invalid_argument("unknown metric id"),
503        }
504    })
505}
506
507#[no_mangle]
508pub unsafe extern "C" fn mc_ladder_distance(
509    distance: f64,
510    tiers: *const MCTier,
511    len: usize,
512    out: *mut f64,
513    out_len: usize,
514) -> MCStatus {
515    ffi_status(|| {
516        let Some(tiers) = (unsafe { slice_from_ptr(tiers, len) }) else {
517            return MCStatus::Null;
518        };
519        let Some(out) = (unsafe { slice_from_mut_ptr(out, out_len) }) else {
520            return MCStatus::Null;
521        };
522        let tiers: Vec<Tier> = tiers.iter().copied().map(Tier::from).collect();
523        ladder_distance(distance, &tiers, out).map_or_else(status_from_error, |_| MCStatus::Ok)
524    })
525}
526
527#[no_mangle]
528pub unsafe extern "C" fn mc_ladder_pair(
529    metric_id: c_int,
530    a: *const f64,
531    b: *const f64,
532    len: usize,
533    ladder: *const MCLadder,
534    out: *mut f64,
535    cap: usize,
536    out_len: *mut usize,
537) -> MCStatus {
538    ffi_status(|| {
539        if metric_id != MC_METRIC_EUCLIDEAN && metric_id != MC_METRIC_ABSOLUTE {
540            return invalid_argument("unknown metric id");
541        }
542        if metric_id == MC_METRIC_ABSOLUTE && len != 1 {
543            return invalid_argument("absolute metric requires len == 1");
544        }
545        let Some(ladder) = (unsafe { ladder.as_ref() }) else {
546            return MCStatus::Null;
547        };
548        let Some(out_len) = (unsafe { out_len.as_mut() }) else {
549            return MCStatus::Null;
550        };
551        let needed = ladder.tiers.len();
552        *out_len = needed;
553        if cap < needed {
554            return buffer_too_small(needed, cap);
555        }
556        let Some(out) = (unsafe { slice_from_mut_ptr(out, cap) }) else {
557            return MCStatus::Null;
558        };
559        let Some(a) = (unsafe { slice_from_ptr(a, len) }) else {
560            return MCStatus::Null;
561        };
562        let Some(b) = (unsafe { slice_from_ptr(b, len) }) else {
563            return MCStatus::Null;
564        };
565        let values = match metric_id {
566            MC_METRIC_EUCLIDEAN => ladder_pair(a, b, &Euclidean, &ladder.tiers),
567            MC_METRIC_ABSOLUTE => ladder_pair(&a[0], &b[0], &Absolute, &ladder.tiers),
568            _ => unreachable!(),
569        };
570        match values {
571            Ok(values) => {
572                out[..needed].copy_from_slice(&values);
573                MCStatus::Ok
574            }
575            Err(error) => status_from_error(error),
576        }
577    })
578}
579
580#[no_mangle]
581pub unsafe extern "C" fn mc_adaptive_ladder_distance(
582    distance: f64,
583    tiers: *const MCTier,
584    len: usize,
585    out: *mut f64,
586    out_len: usize,
587    decision: *mut MCZoomDecision,
588) -> MCStatus {
589    ffi_status(|| {
590        let Some(tiers) = (unsafe { slice_from_ptr(tiers, len) }) else {
591            return MCStatus::Null;
592        };
593        let Some(out) = (unsafe { slice_from_mut_ptr(out, out_len) }) else {
594            return MCStatus::Null;
595        };
596        let Some(decision) = (unsafe { decision.as_mut() }) else {
597            return MCStatus::Null;
598        };
599        let tiers: Vec<Tier> = tiers.iter().copied().map(Tier::from).collect();
600        match adaptive_ladder_distance(distance, &tiers, out) {
601            Ok(value) => {
602                decision.evaluated_tiers = value.evaluated_tiers;
603                decision.first_inactive_tier = value.first_inactive_tier.unwrap_or(0);
604                decision.has_first_inactive_tier = value.first_inactive_tier.is_some();
605                decision.stopped_early = value.stopped_early;
606                MCStatus::Ok
607            }
608            Err(error) => status_from_error(error),
609        }
610    })
611}
612
613#[no_mangle]
614pub unsafe extern "C" fn mc_smooth_tick_distance(
615    distance: f64,
616    tier: MCTier,
617    sharpness: f64,
618    out: *mut f64,
619) -> MCStatus {
620    ffi_status(|| {
621        let Some(out) = (unsafe { out.as_mut() }) else {
622            return MCStatus::Null;
623        };
624        let params = match SmoothParams::sharpness(sharpness) {
625            Ok(params) => params,
626            Err(error) => return status_from_error(error),
627        };
628        match smooth_tick_distance(distance, Tier::from(tier), params) {
629            Ok(value) => {
630                *out = value;
631                MCStatus::Ok
632            }
633            Err(error) => status_from_error(error),
634        }
635    })
636}
637
638#[no_mangle]
639pub unsafe extern "C" fn mc_geometric_ladder(
640    epsilon0: f64,
641    delta0: f64,
642    ratio: f64,
643    tiers: usize,
644    p: f64,
645    epsilon_ref: f64,
646    out: *mut MCTier,
647    out_len: usize,
648) -> MCStatus {
649    ffi_status(|| {
650        if out_len < tiers {
651            return buffer_too_small(tiers, out_len);
652        }
653        let Some(out) = (unsafe { slice_from_mut_ptr(out, out_len) }) else {
654            return MCStatus::Null;
655        };
656        match geometric_ladder(epsilon0, delta0, ratio, tiers, p, epsilon_ref) {
657            Ok(values) => {
658                for (slot, tier) in out.iter_mut().zip(values) {
659                    *slot = tier.into();
660                }
661                MCStatus::Ok
662            }
663            Err(error) => status_from_error(error),
664        }
665    })
666}
667
668#[no_mangle]
669pub unsafe extern "C" fn mc_normalize_ticks(
670    ticks: *const f64,
671    len: usize,
672    normalization_id: c_int,
673    out: *mut f64,
674) -> MCStatus {
675    ffi_status(|| {
676        let mode = match normalization_from_id(normalization_id) {
677            Ok(mode) => mode,
678            Err(error) => return status_from_error(error),
679        };
680        let Some(ticks) = (unsafe { slice_from_ptr(ticks, len) }) else {
681            return MCStatus::Null;
682        };
683        let Some(out) = (unsafe { slice_from_mut_ptr(out, len) }) else {
684            return MCStatus::Null;
685        };
686        normalize_ticks(ticks, mode, out).map_or_else(status_from_error, |_| MCStatus::Ok)
687    })
688}
689
690#[no_mangle]
691pub unsafe extern "C" fn mc_carry_rules(
692    epsilons: *const f64,
693    len: usize,
694    out: *mut u64,
695    cap: usize,
696    out_len: *mut usize,
697) -> MCStatus {
698    ffi_status(|| {
699        let Some(epsilons) = (unsafe { slice_from_ptr(epsilons, len) }) else {
700            return MCStatus::Null;
701        };
702        let Some(out_len) = (unsafe { out_len.as_mut() }) else {
703            return MCStatus::Null;
704        };
705        let rules = match carry_rules(epsilons) {
706            Ok(rules) => rules,
707            Err(error) => return status_from_error(error),
708        };
709        let needed = rules.len();
710        *out_len = needed;
711        if cap < needed {
712            return buffer_too_small(needed, cap);
713        }
714        let Some(out) = (unsafe { slice_from_mut_ptr(out, cap) }) else {
715            return MCStatus::Null;
716        };
717        out[..needed].copy_from_slice(&rules);
718        MCStatus::Ok
719    })
720}
721
722#[no_mangle]
723pub unsafe extern "C" fn mc_weighted_consensus(
724    vectors: *const f64,
725    rows: usize,
726    cols: usize,
727    weights: *const f64,
728    out: *mut f64,
729    out_len: usize,
730) -> MCStatus {
731    ffi_status(|| {
732        if rows == 0 || cols == 0 {
733            return invalid_argument("rows and cols must be > 0");
734        }
735        let Some(vector_len) = rows.checked_mul(cols) else {
736            return invalid_argument("rows * cols overflow");
737        };
738        const MAX_F64_SLICE_LEN: usize = isize::MAX as usize / std::mem::size_of::<f64>();
739        if vector_len > MAX_F64_SLICE_LEN || out_len > MAX_F64_SLICE_LEN {
740            return invalid_argument("slice byte length exceeds isize::MAX");
741        }
742        let Some(vectors) = (unsafe { slice_from_ptr(vectors, vector_len) }) else {
743            return MCStatus::Null;
744        };
745        let Some(weights) = (unsafe { slice_from_ptr(weights, rows) }) else {
746            return MCStatus::Null;
747        };
748        let Some(out) = (unsafe { slice_from_mut_ptr(out, out_len) }) else {
749            return MCStatus::Null;
750        };
751        let rows: Vec<&[f64]> = vectors.chunks(cols).collect();
752        weighted_consensus(&rows, weights, out).map_or_else(status_from_error, |_| MCStatus::Ok)
753    })
754}
755
756#[no_mangle]
757pub unsafe extern "C" fn mc_simple_weight_update(
758    weights: *mut f64,
759    residuals: *const f64,
760    len: usize,
761    learning_rate: f64,
762    floor: f64,
763) -> MCStatus {
764    ffi_status(|| {
765        let Some(weights) = (unsafe { slice_from_mut_ptr(weights, len) }) else {
766            return MCStatus::Null;
767        };
768        let Some(residuals) = (unsafe { slice_from_ptr(residuals, len) }) else {
769            return MCStatus::Null;
770        };
771        simple_weight_update(weights, residuals, learning_rate, floor)
772            .map_or_else(status_from_error, |_| MCStatus::Ok)
773    })
774}
775
776#[no_mangle]
777pub unsafe extern "C" fn mc_promotion_counter_new(
778    quotas: *const u64,
779    len: usize,
780    out: *mut *mut MCPromotionCounter,
781) -> MCStatus {
782    ffi_status(|| {
783        let Some(out) = (unsafe { out.as_mut() }) else {
784            return MCStatus::Null;
785        };
786        let Some(quotas) = (unsafe { slice_from_ptr(quotas, len) }) else {
787            return MCStatus::Null;
788        };
789        match PromotionCounter::new(quotas.to_vec()) {
790            Ok(inner) => {
791                *out = Box::into_raw(Box::new(MCPromotionCounter { inner }));
792                MCStatus::Ok
793            }
794            Err(error) => {
795                *out = ptr::null_mut();
796                status_from_error(error)
797            }
798        }
799    })
800}
801
802#[no_mangle]
803pub unsafe extern "C" fn mc_promotion_counter_from_epsilons(
804    epsilons: *const f64,
805    len: usize,
806    out: *mut *mut MCPromotionCounter,
807) -> MCStatus {
808    ffi_status(|| {
809        let Some(out) = (unsafe { out.as_mut() }) else {
810            return MCStatus::Null;
811        };
812        let Some(epsilons) = (unsafe { slice_from_ptr(epsilons, len) }) else {
813            return MCStatus::Null;
814        };
815        match PromotionCounter::from_epsilons(epsilons) {
816            Ok(inner) => {
817                *out = Box::into_raw(Box::new(MCPromotionCounter { inner }));
818                MCStatus::Ok
819            }
820            Err(error) => {
821                *out = ptr::null_mut();
822                status_from_error(error)
823            }
824        }
825    })
826}
827
828#[no_mangle]
829pub unsafe extern "C" fn mc_promotion_counter_step(
830    counter: *mut MCPromotionCounter,
831    event_flags: *const bool,
832    flags_len: usize,
833    out: *mut bool,
834    cap: usize,
835    out_len: *mut usize,
836) -> MCStatus {
837    ffi_status(|| {
838        let Some(counter) = (unsafe { counter.as_mut() }) else {
839            return MCStatus::Null;
840        };
841        let Some(out_len) = (unsafe { out_len.as_mut() }) else {
842            return MCStatus::Null;
843        };
844        let needed = counter.inner.len();
845        *out_len = needed;
846        if cap < needed {
847            return buffer_too_small(needed, cap);
848        }
849        let flags = if event_flags.is_null() && flags_len == 0 {
850            None
851        } else {
852            let Some(flags) = (unsafe { slice_from_ptr(event_flags, flags_len) }) else {
853                return MCStatus::Null;
854            };
855            Some(flags)
856        };
857        let Some(out) = (unsafe { slice_from_mut_ptr(out, cap) }) else {
858            return MCStatus::Null;
859        };
860        counter
861            .inner
862            .step(flags, out)
863            .map_or_else(status_from_error, |_| MCStatus::Ok)
864    })
865}
866
867#[no_mangle]
868pub unsafe extern "C" fn mc_promotion_counter_counters(
869    counter: *const MCPromotionCounter,
870    out: *mut u64,
871    cap: usize,
872    out_len: *mut usize,
873) -> MCStatus {
874    ffi_status(|| {
875        let Some(counter) = (unsafe { counter.as_ref() }) else {
876            return MCStatus::Null;
877        };
878        let Some(out_len) = (unsafe { out_len.as_mut() }) else {
879            return MCStatus::Null;
880        };
881        let counters = counter.inner.counters();
882        let needed = counters.len();
883        *out_len = needed;
884        if cap < needed {
885            return buffer_too_small(needed, cap);
886        }
887        let Some(out) = (unsafe { slice_from_mut_ptr(out, cap) }) else {
888            return MCStatus::Null;
889        };
890        out[..needed].copy_from_slice(counters);
891        MCStatus::Ok
892    })
893}
894
895#[no_mangle]
896pub unsafe extern "C" fn mc_promotion_counter_quotas(
897    counter: *const MCPromotionCounter,
898    out: *mut u64,
899    cap: usize,
900    out_len: *mut usize,
901) -> MCStatus {
902    ffi_status(|| {
903        let Some(counter) = (unsafe { counter.as_ref() }) else {
904            return MCStatus::Null;
905        };
906        let Some(out_len) = (unsafe { out_len.as_mut() }) else {
907            return MCStatus::Null;
908        };
909        let quotas = counter.inner.quotas();
910        let needed = quotas.len();
911        *out_len = needed;
912        if cap < needed {
913            return buffer_too_small(needed, cap);
914        }
915        let Some(out) = (unsafe { slice_from_mut_ptr(out, cap) }) else {
916            return MCStatus::Null;
917        };
918        out[..needed].copy_from_slice(quotas);
919        MCStatus::Ok
920    })
921}
922
923#[no_mangle]
924pub unsafe extern "C" fn mc_promotion_counter_reset(counter: *mut MCPromotionCounter) -> MCStatus {
925    ffi_status(|| {
926        let Some(counter) = (unsafe { counter.as_mut() }) else {
927            return MCStatus::Null;
928        };
929        counter.inner.reset();
930        MCStatus::Ok
931    })
932}
933
934#[no_mangle]
935pub unsafe extern "C" fn mc_promotion_counter_free(counter: *mut MCPromotionCounter) {
936    if counter.is_null() {
937        return;
938    }
939    let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
940        drop(Box::from_raw(counter));
941    }));
942}
943
944fn coverage_distance(metric: CoverageMetric, a: &Vec<f64>, b: &Vec<f64>) -> f64 {
945    match metric {
946        CoverageMetric::Builtin(MC_METRIC_ABSOLUTE) => (a[0] - b[0]).abs(),
947        CoverageMetric::Builtin(_) => Euclidean.distance(a.as_slice(), b.as_slice()),
948        CoverageMetric::Callback {
949            callback,
950            user_data,
951        } => unsafe { callback(a.as_ptr(), b.as_ptr(), a.len(), user_data) },
952    }
953}
954
955#[no_mangle]
956pub unsafe extern "C" fn mc_coverage_meter_new(
957    epsilons: *const f64,
958    len: usize,
959    dim: usize,
960    metric: c_int,
961    out: *mut *mut MCCoverageMeter,
962) -> MCStatus {
963    ffi_status(|| {
964        let Some(out) = (unsafe { out.as_mut() }) else {
965            return MCStatus::Null;
966        };
967        *out = ptr::null_mut();
968        let Some(epsilons) = (unsafe { slice_from_ptr(epsilons, len) }) else {
969            return MCStatus::Null;
970        };
971        if dim == 0 {
972            set_last_error("coverage state dimension must be > 0");
973            return MCStatus::InvalidArgument;
974        }
975        if metric != MC_METRIC_EUCLIDEAN && metric != MC_METRIC_ABSOLUTE {
976            set_last_error("unknown metric id");
977            return MCStatus::InvalidArgument;
978        }
979        if metric == MC_METRIC_ABSOLUTE && dim != 1 {
980            set_last_error("absolute metric requires dimension 1");
981            return MCStatus::InvalidArgument;
982        }
983        match CoverageMeter::from_epsilons(epsilons) {
984            Ok(inner) => {
985                *out = Box::into_raw(Box::new(MCCoverageMeter {
986                    inner,
987                    dim,
988                    metric: CoverageMetric::Builtin(metric),
989                    scratch: Vec::with_capacity(dim),
990                }));
991                MCStatus::Ok
992            }
993            Err(error) => status_from_error(error),
994        }
995    })
996}
997
998#[no_mangle]
999pub unsafe extern "C" fn mc_coverage_meter_new_with_callback(
1000    epsilons: *const f64,
1001    len: usize,
1002    dim: usize,
1003    callback: Option<MCDistanceFn>,
1004    user_data: *mut c_void,
1005    out: *mut *mut MCCoverageMeter,
1006) -> MCStatus {
1007    ffi_status(|| {
1008        let Some(out) = (unsafe { out.as_mut() }) else {
1009            return MCStatus::Null;
1010        };
1011        *out = ptr::null_mut();
1012        let Some(epsilons) = (unsafe { slice_from_ptr(epsilons, len) }) else {
1013            return MCStatus::Null;
1014        };
1015        let Some(callback) = callback else {
1016            return MCStatus::Null;
1017        };
1018        if dim == 0 {
1019            set_last_error("coverage state dimension must be > 0");
1020            return MCStatus::InvalidArgument;
1021        }
1022        match CoverageMeter::from_epsilons(epsilons) {
1023            Ok(inner) => {
1024                *out = Box::into_raw(Box::new(MCCoverageMeter {
1025                    inner,
1026                    dim,
1027                    metric: CoverageMetric::Callback {
1028                        callback,
1029                        user_data,
1030                    },
1031                    scratch: Vec::with_capacity(dim),
1032                }));
1033                MCStatus::Ok
1034            }
1035            Err(error) => status_from_error(error),
1036        }
1037    })
1038}
1039
1040#[no_mangle]
1041pub unsafe extern "C" fn mc_coverage_meter_observe(
1042    meter: *mut MCCoverageMeter,
1043    state: *const f64,
1044    state_len: usize,
1045    out: *mut bool,
1046    cap: usize,
1047    out_len: *mut usize,
1048) -> MCStatus {
1049    ffi_status(|| {
1050        let Some(meter) = (unsafe { meter.as_mut() }) else {
1051            return MCStatus::Null;
1052        };
1053        let Some(out_len) = (unsafe { out_len.as_mut() }) else {
1054            return MCStatus::Null;
1055        };
1056        let needed = meter.inner.tier_count();
1057        *out_len = needed;
1058        if cap < needed {
1059            return buffer_too_small(needed, cap);
1060        }
1061        let Some(state) = (unsafe { slice_from_ptr(state, state_len) }) else {
1062            return MCStatus::Null;
1063        };
1064        if state_len != meter.dim {
1065            return status_from_error(MetricChronoError::ShapeMismatch {
1066                expected: meter.dim,
1067                actual: state_len,
1068                context: "coverage state dimension",
1069            });
1070        }
1071        let Some(out) = (unsafe { slice_from_mut_ptr(out, cap) }) else {
1072            return MCStatus::Null;
1073        };
1074        let metric_kind = meter.metric;
1075        let metric =
1076            MetricFn(move |a: &Vec<f64>, b: &Vec<f64>| coverage_distance(metric_kind, a, b));
1077        let MCCoverageMeter { inner, scratch, .. } = meter;
1078        scratch.clear();
1079        scratch.extend_from_slice(state);
1080        inner
1081            .observe_into(&metric, scratch, &mut out[..needed])
1082            .map_or_else(status_from_error, |_| MCStatus::Ok)
1083    })
1084}
1085
1086#[no_mangle]
1087pub unsafe extern "C" fn mc_coverage_meter_counts(
1088    meter: *const MCCoverageMeter,
1089    out: *mut u64,
1090    cap: usize,
1091    out_len: *mut usize,
1092) -> MCStatus {
1093    ffi_status(|| {
1094        let Some(meter) = (unsafe { meter.as_ref() }) else {
1095            return MCStatus::Null;
1096        };
1097        let Some(out_len) = (unsafe { out_len.as_mut() }) else {
1098            return MCStatus::Null;
1099        };
1100        let needed = meter.inner.tier_count();
1101        *out_len = needed;
1102        if cap < needed {
1103            return buffer_too_small(needed, cap);
1104        }
1105        let Some(out) = (unsafe { slice_from_mut_ptr(out, cap) }) else {
1106            return MCStatus::Null;
1107        };
1108        for (slot, count) in out.iter_mut().zip(meter.inner.counts()) {
1109            *slot = count as u64;
1110        }
1111        MCStatus::Ok
1112    })
1113}
1114
1115#[no_mangle]
1116pub unsafe extern "C" fn mc_coverage_meter_unique_representatives(
1117    meter: *const MCCoverageMeter,
1118    out: *mut u64,
1119) -> MCStatus {
1120    ffi_status(|| {
1121        let Some(meter) = (unsafe { meter.as_ref() }) else {
1122            return MCStatus::Null;
1123        };
1124        let Some(out) = (unsafe { out.as_mut() }) else {
1125            return MCStatus::Null;
1126        };
1127        *out = meter.inner.unique_representatives() as u64;
1128        MCStatus::Ok
1129    })
1130}
1131
1132#[no_mangle]
1133pub unsafe extern "C" fn mc_coverage_meter_tier_count(
1134    meter: *const MCCoverageMeter,
1135    out: *mut usize,
1136) -> MCStatus {
1137    ffi_status(|| {
1138        let Some(meter) = (unsafe { meter.as_ref() }) else {
1139            return MCStatus::Null;
1140        };
1141        let Some(out) = (unsafe { out.as_mut() }) else {
1142            return MCStatus::Null;
1143        };
1144        *out = meter.inner.tier_count();
1145        MCStatus::Ok
1146    })
1147}
1148
1149#[no_mangle]
1150pub unsafe extern "C" fn mc_coverage_meter_free(meter: *mut MCCoverageMeter) {
1151    if meter.is_null() {
1152        return;
1153    }
1154    let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
1155        drop(Box::from_raw(meter));
1156    }));
1157}
1158
1159#[no_mangle]
1160pub unsafe extern "C" fn mc_progress_efficiency(
1161    coverage: u64,
1162    epsilon: f64,
1163    path_length: f64,
1164    out: *mut f64,
1165) -> MCStatus {
1166    ffi_status(|| {
1167        let Some(out) = (unsafe { out.as_mut() }) else {
1168            return MCStatus::Null;
1169        };
1170        match progress_efficiency(coverage as usize, epsilon, path_length) {
1171            Some(value) => {
1172                *out = value;
1173                MCStatus::Ok
1174            }
1175            None => {
1176                set_last_error("path_length must be finite and positive");
1177                MCStatus::InvalidArgument
1178            }
1179        }
1180    })
1181}
1182
1183#[no_mangle]
1184pub extern "C" fn mc_classify_regime(throughput_delta: f64, coverage_delta: u64) -> c_int {
1185    match classify_regime(throughput_delta, coverage_delta as usize) {
1186        OperatingRegime::Quiescent => MC_REGIME_QUIESCENT,
1187        OperatingRegime::Progress => MC_REGIME_PROGRESS,
1188        OperatingRegime::Churn => MC_REGIME_CHURN,
1189        OperatingRegime::Creep => MC_REGIME_CREEP,
1190    }
1191}
1192
1193#[no_mangle]
1194pub extern "C" fn mc_event_log_new(tier_count: usize) -> *mut MCEventLog {
1195    begin_ffi_call();
1196    match catch_unwind(AssertUnwindSafe(|| match EventLog::new(tier_count) {
1197        Ok(inner) => Box::into_raw(Box::new(MCEventLog { inner })),
1198        Err(error) => {
1199            status_from_error(error);
1200            ptr::null_mut()
1201        }
1202    })) {
1203        Ok(ptr) => ptr,
1204        Err(_) => {
1205            set_last_error("panic");
1206            ptr::null_mut()
1207        }
1208    }
1209}
1210
1211#[no_mangle]
1212pub unsafe extern "C" fn mc_event_log_free(log: *mut MCEventLog) {
1213    if log.is_null() {
1214        return;
1215    }
1216    let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
1217        drop(Box::from_raw(log));
1218    }));
1219}
1220
1221#[no_mangle]
1222pub unsafe extern "C" fn mc_event_log_append(
1223    log: *mut MCEventLog,
1224    state_id: u64,
1225    ticks: *const f64,
1226    len: usize,
1227    out_index: *mut usize,
1228) -> MCStatus {
1229    ffi_status(|| {
1230        let Some(log) = (unsafe { log.as_mut() }) else {
1231            return MCStatus::Null;
1232        };
1233        let Some(ticks) = (unsafe { slice_from_ptr(ticks, len) }) else {
1234            return MCStatus::Null;
1235        };
1236        let Some(out_index) = (unsafe { out_index.as_mut() }) else {
1237            return MCStatus::Null;
1238        };
1239        match log.inner.append(state_id, ticks.to_vec()) {
1240            Ok(index) => {
1241                *out_index = index;
1242                MCStatus::Ok
1243            }
1244            Err(error) => status_from_error(error),
1245        }
1246    })
1247}
1248
1249#[no_mangle]
1250pub unsafe extern "C" fn mc_event_log_first_event(
1251    log: *const MCEventLog,
1252    tier: usize,
1253    out_index: *mut usize,
1254    out_has: *mut bool,
1255) -> MCStatus {
1256    ffi_status(|| {
1257        let Some(log) = (unsafe { log.as_ref() }) else {
1258            return MCStatus::Null;
1259        };
1260        let Some(out_index) = (unsafe { out_index.as_mut() }) else {
1261            return MCStatus::Null;
1262        };
1263        let Some(out_has) = (unsafe { out_has.as_mut() }) else {
1264            return MCStatus::Null;
1265        };
1266        if tier >= log.inner.tier_count() {
1267            return invalid_argument("event log tier is out of bounds");
1268        }
1269        if let Some(index) = log.inner.first_event(tier) {
1270            *out_index = index;
1271            *out_has = true;
1272        } else {
1273            *out_index = 0;
1274            *out_has = false;
1275        }
1276        MCStatus::Ok
1277    })
1278}
1279
1280#[no_mangle]
1281pub unsafe extern "C" fn mc_event_log_next_event(
1282    log: *const MCEventLog,
1283    index: usize,
1284    tier: usize,
1285    out_index: *mut usize,
1286    has_event: *mut bool,
1287) -> MCStatus {
1288    ffi_status(|| {
1289        let Some(log) = (unsafe { log.as_ref() }) else {
1290            return MCStatus::Null;
1291        };
1292        let Some(out_index) = (unsafe { out_index.as_mut() }) else {
1293            return MCStatus::Null;
1294        };
1295        let Some(has_event) = (unsafe { has_event.as_mut() }) else {
1296            return MCStatus::Null;
1297        };
1298        if tier >= log.inner.tier_count() || index >= log.inner.len() {
1299            return invalid_argument("event log index or tier is out of bounds");
1300        }
1301        if let Some(next) = log.inner.next_event(index, tier) {
1302            *out_index = next;
1303            *has_event = true;
1304        } else {
1305            *out_index = 0;
1306            *has_event = false;
1307        }
1308        MCStatus::Ok
1309    })
1310}
1311
1312#[no_mangle]
1313pub unsafe extern "C" fn mc_event_log_record(
1314    log: *const MCEventLog,
1315    index: usize,
1316    out_state_id: *mut u64,
1317    ticks_out: *mut f64,
1318    ticks_cap: usize,
1319    out_ticks_len: *mut usize,
1320) -> MCStatus {
1321    ffi_status(|| {
1322        let Some(log) = (unsafe { log.as_ref() }) else {
1323            return MCStatus::Null;
1324        };
1325        let Some(out_ticks_len) = (unsafe { out_ticks_len.as_mut() }) else {
1326            return MCStatus::Null;
1327        };
1328        let Some(record) = log.inner.record(index) else {
1329            return invalid_argument("event log index is out of bounds");
1330        };
1331        let needed = record.ticks.len();
1332        *out_ticks_len = needed;
1333        if ticks_cap < needed {
1334            return buffer_too_small(needed, ticks_cap);
1335        }
1336        let Some(out_state_id) = (unsafe { out_state_id.as_mut() }) else {
1337            return MCStatus::Null;
1338        };
1339        let Some(ticks_out) = (unsafe { slice_from_mut_ptr(ticks_out, ticks_cap) }) else {
1340            return MCStatus::Null;
1341        };
1342        *out_state_id = record.state_id;
1343        ticks_out[..needed].copy_from_slice(&record.ticks);
1344        MCStatus::Ok
1345    })
1346}
1347
1348#[no_mangle]
1349pub unsafe extern "C" fn mc_event_log_compact_summary(
1350    log: *const MCEventLog,
1351    tier: usize,
1352    idx_out: *mut usize,
1353    state_out: *mut u64,
1354    tick_out: *mut f64,
1355    cap: usize,
1356    out_len: *mut usize,
1357) -> MCStatus {
1358    ffi_status(|| {
1359        let Some(log) = (unsafe { log.as_ref() }) else {
1360            return MCStatus::Null;
1361        };
1362        let Some(out_len) = (unsafe { out_len.as_mut() }) else {
1363            return MCStatus::Null;
1364        };
1365        if tier >= log.inner.tier_count() {
1366            return invalid_argument("event log tier is out of bounds");
1367        }
1368        let summary = log.inner.compact_summary(tier);
1369        let needed = summary.len();
1370        *out_len = needed;
1371        if cap < needed {
1372            return buffer_too_small(needed, cap);
1373        }
1374        let Some(idx_out) = (unsafe { slice_from_mut_ptr(idx_out, cap) }) else {
1375            return MCStatus::Null;
1376        };
1377        let Some(state_out) = (unsafe { slice_from_mut_ptr(state_out, cap) }) else {
1378            return MCStatus::Null;
1379        };
1380        let Some(tick_out) = (unsafe { slice_from_mut_ptr(tick_out, cap) }) else {
1381            return MCStatus::Null;
1382        };
1383        for (offset, item) in summary.iter().enumerate() {
1384            idx_out[offset] = item.index;
1385            state_out[offset] = item.state_id;
1386            tick_out[offset] = item.tick;
1387        }
1388        MCStatus::Ok
1389    })
1390}
1391
1392#[no_mangle]
1393pub unsafe extern "C" fn mc_event_log_len(log: *const MCEventLog, out_len: *mut usize) -> MCStatus {
1394    ffi_status(|| {
1395        let Some(log) = (unsafe { log.as_ref() }) else {
1396            return MCStatus::Null;
1397        };
1398        let Some(out_len) = (unsafe { out_len.as_mut() }) else {
1399            return MCStatus::Null;
1400        };
1401        *out_len = log.inner.len();
1402        MCStatus::Ok
1403    })
1404}
1405
1406#[no_mangle]
1407pub unsafe extern "C" fn mc_event_log_tier_count(
1408    log: *const MCEventLog,
1409    out: *mut usize,
1410) -> MCStatus {
1411    ffi_status(|| {
1412        let Some(log) = (unsafe { log.as_ref() }) else {
1413            return MCStatus::Null;
1414        };
1415        let Some(out) = (unsafe { out.as_mut() }) else {
1416            return MCStatus::Null;
1417        };
1418        *out = log.inner.tier_count();
1419        MCStatus::Ok
1420    })
1421}
1422
1423#[no_mangle]
1424pub unsafe extern "C" fn mc_event_log_is_empty(log: *const MCEventLog, out: *mut bool) -> MCStatus {
1425    ffi_status(|| {
1426        let Some(log) = (unsafe { log.as_ref() }) else {
1427            return MCStatus::Null;
1428        };
1429        let Some(out) = (unsafe { out.as_mut() }) else {
1430            return MCStatus::Null;
1431        };
1432        *out = log.inner.is_empty();
1433        MCStatus::Ok
1434    })
1435}
1436
1437#[no_mangle]
1438pub extern "C" fn mc_tick_distance_raw(
1439    distance: f64,
1440    epsilon: f64,
1441    delta: f64,
1442    p: f64,
1443    epsilon_ref: f64,
1444) -> f64 {
1445    tick_distance(
1446        distance,
1447        Tier {
1448            epsilon,
1449            delta,
1450            p,
1451            epsilon_ref,
1452        },
1453    )
1454}
1455
1456#[cfg(test)]
1457mod tests {
1458    use super::*;
1459
1460    #[test]
1461    fn ffi_tick_and_ladder_return_stable_values() {
1462        let mut tier = MCTier {
1463            epsilon: 0.0,
1464            delta: 0.0,
1465            p: 0.0,
1466            epsilon_ref: 0.0,
1467        };
1468        assert_eq!(
1469            unsafe { mc_tier_new(0.5, 1.0, 0.5, 1.0, &mut tier) },
1470            MCStatus::Ok
1471        );
1472        assert_eq!(
1473            unsafe { mc_tier_new(1.0, 1.0, 0.0, 1.0, &mut tier) },
1474            MCStatus::InvalidArgument
1475        );
1476        let mut out = 0.0;
1477        assert_eq!(
1478            unsafe { mc_tick_distance(1.2, tier, &mut out) },
1479            MCStatus::Ok
1480        );
1481        assert_eq!(out, 2.0_f64.sqrt());
1482
1483        let tiers = [
1484            tier,
1485            MCTier {
1486                epsilon: 1.0,
1487                delta: 2.0,
1488                p: 0.5,
1489                epsilon_ref: 1.0,
1490            },
1491        ];
1492        let mut values = [0.0; 2];
1493        assert_eq!(
1494            unsafe {
1495                mc_ladder_distance(
1496                    1.2,
1497                    tiers.as_ptr(),
1498                    tiers.len(),
1499                    values.as_mut_ptr(),
1500                    values.len(),
1501                )
1502            },
1503            MCStatus::Ok
1504        );
1505        assert!(values[0] > 0.0 && values[1] > 0.0);
1506
1507        let mut ladder = std::ptr::null_mut();
1508        assert_eq!(
1509            unsafe { mc_ladder_new(tiers.as_ptr(), tiers.len(), &mut ladder) },
1510            MCStatus::Ok
1511        );
1512        assert!(!ladder.is_null());
1513        let mut len = 0;
1514        assert_eq!(unsafe { mc_ladder_len(ladder, &mut len) }, MCStatus::Ok);
1515        assert_eq!(len, 2);
1516        let mut owned_values = [0.0; 2];
1517        assert_eq!(
1518            unsafe {
1519                mc_ladder_distance_owned(ladder, 1.2, owned_values.as_mut_ptr(), owned_values.len())
1520            },
1521            MCStatus::Ok
1522        );
1523        assert_eq!(values, owned_values);
1524        unsafe { mc_ladder_free(ladder) };
1525    }
1526
1527    #[test]
1528    fn ffi_event_log_exposes_next_pointers() {
1529        let log = mc_event_log_new(2);
1530        assert!(!log.is_null());
1531        let mut index = usize::MAX;
1532        assert_eq!(
1533            unsafe { mc_event_log_append(log, 10, [1.0, 0.0].as_ptr(), 2, &mut index) },
1534            MCStatus::Ok
1535        );
1536        assert_eq!(index, 0);
1537        assert_eq!(
1538            unsafe { mc_event_log_append(log, 11, [1.0, 1.0].as_ptr(), 2, &mut index) },
1539            MCStatus::Ok
1540        );
1541        let mut next = 0;
1542        let mut has = false;
1543        assert_eq!(
1544            unsafe { mc_event_log_next_event(log, 0, 0, &mut next, &mut has) },
1545            MCStatus::Ok
1546        );
1547        assert!(has);
1548        assert_eq!(next, 1);
1549        unsafe { mc_event_log_free(log) };
1550    }
1551
1552    #[test]
1553    fn ffi_coverage_meter_round_trip() {
1554        let epsilons = [0.1, 0.2];
1555        let mut meter: *mut MCCoverageMeter = ptr::null_mut();
1556        assert_eq!(
1557            unsafe {
1558                mc_coverage_meter_new(epsilons.as_ptr(), 2, 2, MC_METRIC_EUCLIDEAN, &mut meter)
1559            },
1560            MCStatus::Ok
1561        );
1562        assert!(!meter.is_null());
1563
1564        let mut admitted = [false; 2];
1565        let mut out_len = 0usize;
1566        // first sample is always admitted at every tier
1567        assert_eq!(
1568            unsafe {
1569                mc_coverage_meter_observe(
1570                    meter,
1571                    [0.0, 0.0].as_ptr(),
1572                    2,
1573                    admitted.as_mut_ptr(),
1574                    2,
1575                    &mut out_len,
1576                )
1577            },
1578            MCStatus::Ok
1579        );
1580        assert_eq!(out_len, 2);
1581        assert_eq!(admitted, [true, true]);
1582        // 0.15 away: admitted at tier 0 (>= 0.1) but not tier 1 (< 0.2)
1583        assert_eq!(
1584            unsafe {
1585                mc_coverage_meter_observe(
1586                    meter,
1587                    [0.15, 0.0].as_ptr(),
1588                    2,
1589                    admitted.as_mut_ptr(),
1590                    2,
1591                    &mut out_len,
1592                )
1593            },
1594            MCStatus::Ok
1595        );
1596        assert_eq!(admitted, [true, false]);
1597
1598        let mut counts = [0u64; 2];
1599        assert_eq!(
1600            unsafe { mc_coverage_meter_counts(meter, counts.as_mut_ptr(), 2, &mut out_len) },
1601            MCStatus::Ok
1602        );
1603        assert_eq!(counts, [2, 1]);
1604
1605        let mut unique = 0u64;
1606        assert_eq!(
1607            unsafe { mc_coverage_meter_unique_representatives(meter, &mut unique) },
1608            MCStatus::Ok
1609        );
1610        assert_eq!(unique, 2);
1611
1612        // wrong state dimension is a shape error
1613        assert_eq!(
1614            unsafe {
1615                mc_coverage_meter_observe(
1616                    meter,
1617                    [0.0].as_ptr(),
1618                    1,
1619                    admitted.as_mut_ptr(),
1620                    2,
1621                    &mut out_len,
1622                )
1623            },
1624            MCStatus::InvalidArgument
1625        );
1626        unsafe { mc_coverage_meter_free(meter) };
1627
1628        // invalid constructions
1629        let mut bad: *mut MCCoverageMeter = ptr::null_mut();
1630        assert_eq!(
1631            unsafe {
1632                mc_coverage_meter_new(epsilons.as_ptr(), 2, 0, MC_METRIC_EUCLIDEAN, &mut bad)
1633            },
1634            MCStatus::InvalidArgument
1635        );
1636        assert_eq!(
1637            unsafe { mc_coverage_meter_new(epsilons.as_ptr(), 2, 3, MC_METRIC_ABSOLUTE, &mut bad) },
1638            MCStatus::InvalidArgument
1639        );
1640
1641        // callback-metric constructor: Chebyshev distinguishes itself from
1642        // euclidean on the pair ((0,0), (0.05, 0.09)): euclidean ~0.103 would
1643        // admit at eps=0.1, chebyshev 0.09 must reject
1644        unsafe extern "C" fn chebyshev(
1645            a: *const f64,
1646            b: *const f64,
1647            dim: usize,
1648            _user_data: *mut c_void,
1649        ) -> f64 {
1650            let a = unsafe { std::slice::from_raw_parts(a, dim) };
1651            let b = unsafe { std::slice::from_raw_parts(b, dim) };
1652            a.iter()
1653                .zip(b)
1654                .map(|(left, right)| (left - right).abs())
1655                .fold(0.0, f64::max)
1656        }
1657        let mut cb_meter: *mut MCCoverageMeter = ptr::null_mut();
1658        assert_eq!(
1659            unsafe {
1660                mc_coverage_meter_new_with_callback(
1661                    [0.1].as_ptr(),
1662                    1,
1663                    2,
1664                    Some(chebyshev),
1665                    ptr::null_mut(),
1666                    &mut cb_meter,
1667                )
1668            },
1669            MCStatus::Ok
1670        );
1671        let mut flag = [false; 1];
1672        assert_eq!(
1673            unsafe {
1674                mc_coverage_meter_observe(
1675                    cb_meter,
1676                    [0.0, 0.0].as_ptr(),
1677                    2,
1678                    flag.as_mut_ptr(),
1679                    1,
1680                    &mut out_len,
1681                )
1682            },
1683            MCStatus::Ok
1684        );
1685        assert_eq!(
1686            unsafe {
1687                mc_coverage_meter_observe(
1688                    cb_meter,
1689                    [0.05, 0.09].as_ptr(),
1690                    2,
1691                    flag.as_mut_ptr(),
1692                    1,
1693                    &mut out_len,
1694                )
1695            },
1696            MCStatus::Ok
1697        );
1698        assert_eq!(flag, [false], "chebyshev 0.09 < 0.1 must reject");
1699        unsafe { mc_coverage_meter_free(cb_meter) };
1700        // a null callback is rejected
1701        assert_eq!(
1702            unsafe {
1703                mc_coverage_meter_new_with_callback(
1704                    [0.1].as_ptr(),
1705                    1,
1706                    2,
1707                    None,
1708                    ptr::null_mut(),
1709                    &mut cb_meter,
1710                )
1711            },
1712            MCStatus::Null
1713        );
1714
1715        // pure helpers
1716        assert_eq!(mc_classify_regime(0.0, 0), MC_REGIME_QUIESCENT);
1717        assert_eq!(mc_classify_regime(1.0, 1), MC_REGIME_PROGRESS);
1718        assert_eq!(mc_classify_regime(1.0, 0), MC_REGIME_CHURN);
1719        assert_eq!(mc_classify_regime(0.0, 1), MC_REGIME_CREEP);
1720        let mut efficiency = -1.0;
1721        assert_eq!(
1722            unsafe { mc_progress_efficiency(11, 0.1, 2.0, &mut efficiency) },
1723            MCStatus::Ok
1724        );
1725        assert!((efficiency - 0.5).abs() < 1e-12);
1726        assert_eq!(
1727            unsafe { mc_progress_efficiency(11, 0.1, 0.0, &mut efficiency) },
1728            MCStatus::InvalidArgument
1729        );
1730    }
1731}