Skip to main content

graphviz_pure_core/
diag.rs

1//! Diagnostics as `lib/cgraph/agerror.c` reports them: `agerrorf` prints `Error: ...` and
2//! raises the error level that `dot`'s `main` returns as its exit status
3//! (`graphviz_errors + agerrors()`), while the layout and the output go on. `agwarningf`
4//! prints `Warning: ...` and does not change the status.
5//!
6//! The level is per thread, so concurrent in-process renders do not see each other's errors.
7
8use std::cell::{Cell, RefCell};
9use std::collections::HashSet;
10
11thread_local! {
12    static ERRORS: Cell<usize> = const { Cell::new(0) };
13    static EMITTED: RefCell<HashSet<String>> = RefCell::new(HashSet::new());
14    static QUIET: Cell<u32> = const { Cell::new(0) };
15    static SILENT: Cell<u32> = const { Cell::new(0) };
16}
17
18/// Increments `depth` for the life of the returned guard.
19fn enter(depth: &'static std::thread::LocalKey<Cell<u32>>) -> impl Drop {
20    struct Guard(&'static std::thread::LocalKey<Cell<u32>>);
21    impl Drop for Guard {
22        fn drop(&mut self) {
23            self.0.with(|q| q.set(q.get() - 1));
24        }
25    }
26    depth.with(|q| q.set(q.get() + 1));
27    Guard(depth)
28}
29
30/// Runs `f` as a repeat of work C does once. The port sizes labels and nodes again after
31/// C's `common_init_node`/`common_init_edge`/`do_graph_label` (for ports, splines and
32/// rendering), and the diagnostics of those calls (HTML label warnings and errors,
33/// `bad label format`, `size too small for label`) must not print a second time.
34///
35/// Port warnings still print: C resolves ports again in some of these places and warns again.
36pub fn quietly<R>(f: impl FnOnce() -> R) -> R {
37    let _guard = enter(&QUIET);
38    f()
39}
40
41/// [`quietly`], and port warnings too, for a repeat that C does not make at all.
42pub fn silently<R>(f: impl FnOnce() -> R) -> R {
43    let _guard = enter(&SILENT);
44    f()
45}
46
47/// False inside [`quietly`] and [`silently`]: label and node sizing diagnostics are reported
48/// only outside them.
49pub fn reporting() -> bool {
50    QUIET.with(|q| q.get() == 0) && reporting_ports()
51}
52
53/// False inside [`silently`]: port warnings are reported only outside it.
54pub fn reporting_ports() -> bool {
55    SILENT.with(|q| q.get() == 0)
56}
57
58/// `emit_once(str)`: true the first time `key` is seen on this thread.
59pub fn emit_once(key: &str) -> bool {
60    EMITTED.with(|e| e.borrow_mut().insert(key.to_string()))
61}
62
63/// `agerrorf(msg)`: `msg` is the C format output, normally ending in a newline.
64pub fn agerrorf(msg: &str) {
65    crate::charset::eprint_bytes(&format!("Error: {msg}"));
66    ERRORS.with(|c| c.set(c.get() + 1));
67}
68
69/// `agwarningf(msg)`.
70pub fn agwarningf(msg: &str) {
71    crate::charset::eprint_bytes(&format!("Warning: {msg}"));
72}
73
74/// `agerr(AGPREV, msg)`: continues the previous message.
75pub fn agerr_prev(msg: &str) {
76    crate::charset::eprint_bytes(msg);
77}
78
79/// C `printf("%.{prec}g", v)` on the reference platform (macOS libc), as diagnostics and
80/// renderers print numbers.
81pub fn fmt_g(v: f64, prec: usize) -> String {
82    fn trim(mut s: String) -> String {
83        if s.contains('.') {
84            while s.ends_with('0') {
85                s.pop();
86            }
87            if s.ends_with('.') {
88                s.pop();
89            }
90        }
91        s
92    }
93    if v.is_nan() {
94        // macOS libc prints `nan` for either sign.
95        return "nan".to_string();
96    }
97    if v.is_infinite() {
98        return if v < 0.0 { "-inf" } else { "inf" }.to_string();
99    }
100    let p = prec.max(1);
101    if v == 0.0 {
102        return if v.is_sign_negative() { "-0" } else { "0" }.to_string();
103    }
104    // The exponent after rounding to `p` significant digits.
105    let e_form = format!("{:.*e}", p - 1, v);
106    let exp: i32 = e_form
107        .rsplit_once('e')
108        .and_then(|(_, e)| e.parse().ok())
109        .unwrap_or(0);
110    if exp < -4 || exp >= p as i32 {
111        let (mant, _) = e_form.rsplit_once('e').unwrap_or((&e_form, "0"));
112        let mant = trim(mant.to_string());
113        let sign = if exp < 0 { '-' } else { '+' };
114        format!("{mant}e{sign}{:02}", exp.abs())
115    } else {
116        let decimals = (p as i32 - 1 - exp).max(0) as usize;
117        trim(format!("{v:.decimals$}"))
118    }
119}
120
121/// Number of errors reported on this thread since the last [`reset_errors`].
122pub fn error_count() -> usize {
123    ERRORS.with(Cell::get)
124}
125
126/// Clears the error count and the once-only messages of this thread.
127pub fn reset_errors() {
128    ERRORS.with(|c| c.set(0));
129    EMITTED.with(|e| e.borrow_mut().clear());
130}