ijson 0.1.7

A more memory efficient replacement for serde_json::Value
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
//! Codegen tests: what the basic operations actually compile to.
//!
//! `codegen.rs` checks one coarse property across the whole library — that nothing
//! dispatches through a vtable. These check a handful of individual operations, closely:
//!
//!   - **Constructing** a small value is a compile-time constant. A short string, a small
//!     number, a bool and `null` live *in the pointer word*, so the constructor is pure
//!     arithmetic; for a known input it should fold to a single `ret` of the encoded word.
//!     That word is asserted exactly, which pins the bit layout from the outside — the
//!     encoding is otherwise only ever asserted by code sharing the very constants it is
//!     testing, so it could not catch a layout that quietly agreed with itself.
//!
//!   - **Reading** a value, when its representation is *not* known, generates the whole
//!     dispatch — so that is where a cost hiding in any one arm shows up. It cannot fold
//!     away, and there is no exact shape to demand of it; what it can do is stay free of
//!     the things that would make it slow: a vtable, an allocation, a panic path.
//!
//!   - **Reading** a value on the **fast path** — when the representation *is* known — is
//!     pinned exactly, instruction by instruction. A probe hands the compiler that one
//!     fact and nothing else (`assert_unchecked`, via `ijson::codegen_probes`; the value
//!     itself stays unknown), the dispatch folds away, and what is left is the fast path
//!     alone. Reading a small integer out of an `INumber` should be the pointer word, a
//!     shift, and nothing more — and that is asserted, not merely hoped for. The slow
//!     paths may be anything.
//!
//! # Reading the constants
//!
//! An inline word is `[ payload | flags | tag ]`, lowest bits first:
//!
//! ```text
//!   bits 0-2  tag        (`Inline` == 0)
//!   bit  3    IS_NUMBER
//!   bit  4    IS_STRING  (when IS_NUMBER is clear)
//!   bits 5-7  payload    (a constant's discriminant, or an inline string's length)
//!   bits 8..  the string's bytes, or a number's mantissa
//! ```
//!
//! # Fragility
//!
//! These are deliberately strict, so they *will* fail if the optimizer changes what it can
//! fold or inline — that is the point. A regression here means an operation that was free
//! stopped being free, and nothing else in the suite would notice: the behaviour stays
//! identical, only the generated code is worse. If a failure shows a different constant,
//! decode it against the table above before assuming the compiler is at fault; it more
//! likely means the layout moved.
//!
//! Fat LTO is required, because these operations are not `#[inline]` and so export no MIR:
//! without it the probe crate can only *call* them, and there is nothing to fold or look
//! inside.

mod common;

use std::path::{Path, PathBuf};
use std::sync::OnceLock;

/// The operations under test, in two crates.
///
/// Two, because a function LLVM sees called from *one* place is inlined unconditionally,
/// and from two it has to weigh the cost — so probes sharing a callee change each other's
/// codegen. Putting `n.to_i64()` in the same crate twice made it stop inlining into either,
/// which does not say anything about the library and quietly made the structural test below
/// vacuous: a probe that merely calls out has nothing in it to object to.
///
/// `#[no_mangle]`, so each survives LTO as an exported root and appears in the IR under a
/// name we can find. The Rust ABI, deliberately: an `extern "C"` function cannot unwind, so
/// rustc wraps its body in an abort-on-unwind shim and leaves the real work out of line —
/// which would hide the very code these tests exist to look at.
mod probes {
    /// Constructing a value, and reading one whose representation is *not* known: the whole
    /// dispatch is generated, so this is where a cost hiding in any arm shows up.
    pub const ANY: &str = r#"
use ijson::{INumber, IString, IValue, ValueType};

// Constructing, from an input known at compile time.
#[no_mangle] pub fn probe_null() -> IValue { IValue::NULL }
#[no_mangle] pub fn probe_true() -> IValue { IValue::from(true) }
#[no_mangle] pub fn probe_false() -> IValue { IValue::from(false) }
#[no_mangle] pub fn probe_empty_string() -> IString { IString::from("") }
#[no_mangle] pub fn probe_inline_string() -> IString { IString::from("abc") }
#[no_mangle] pub fn probe_zero() -> INumber { INumber::from(0i64) }
#[no_mangle] pub fn probe_small_int() -> INumber { INumber::from(42i64) }
#[no_mangle] pub fn probe_negative_int() -> INumber { INumber::from(-1i64) }
#[no_mangle] pub fn probe_half() -> IValue { IValue::from(0.5f64) }

// Reading, from a value whose representation is not known at compile time.
#[no_mangle] pub fn probe_to_i64(n: &INumber) -> Option<i64> { n.to_i64() }
#[no_mangle] pub fn probe_to_u64(n: &INumber) -> Option<u64> { n.to_u64() }
#[no_mangle] pub fn probe_to_f64(n: &INumber) -> Option<f64> { n.to_f64() }
#[no_mangle] pub fn probe_to_f64_lossy(n: &INumber) -> f64 { n.to_f64_lossy() }
#[no_mangle] pub fn probe_has_decimal_point(n: &INumber) -> bool { n.has_decimal_point() }
#[no_mangle] pub fn probe_is_number(v: &IValue) -> bool { v.is_number() }
#[no_mangle] pub fn probe_type(v: &IValue) -> ValueType { v.type_() }
"#;

    /// Reading one whose representation *is* known — the fast path, and nothing else.
    pub const FAST: &str = r#"
use ijson::codegen_probes::{assume_inline, assume_inline_integer};
use ijson::{INumber, IValue, ValueType};

#[no_mangle] pub fn probe_fast_is_number(n: &INumber) -> bool {
    unsafe { assume_inline(n) };
    AsRef::<IValue>::as_ref(n).is_number()
}
#[no_mangle] pub fn probe_fast_type(n: &INumber) -> ValueType {
    unsafe { assume_inline(n) };
    AsRef::<IValue>::as_ref(n).type_()
}
#[no_mangle] pub fn probe_fast_has_decimal_point(n: &INumber) -> bool {
    unsafe { assume_inline(n) };
    n.has_decimal_point()
}
#[no_mangle] pub fn probe_int_has_decimal_point(n: &INumber) -> bool {
    unsafe { assume_inline_integer(n) };
    n.has_decimal_point()
}
#[no_mangle] pub fn probe_int_to_i64(n: &INumber) -> Option<i64> {
    unsafe { assume_inline_integer(n) };
    n.to_i64()
}
#[no_mangle] pub fn probe_int_to_u64(n: &INumber) -> Option<u64> {
    unsafe { assume_inline_integer(n) };
    n.to_u64()
}
#[no_mangle] pub fn probe_int_to_f64_lossy(n: &INumber) -> f64 {
    unsafe { assume_inline_integer(n) };
    n.to_f64_lossy()
}
"#;
}

// --- Constructing -----------------------------------------------------------

/// The word each construction must fold to, and how it decomposes. The IR prints an `i64`
/// constant signed, which is why the negative mantissa comes out negative.
// Every number is written as `mantissa << 8 | exponent code << 4 | IS_NUMBER`, so the
// fields line up and can be compared by eye. Zero's mantissa is written out as such rather
// than dropped, which is the point clippy objects to.
#[allow(clippy::identity_op)]
fn constants() -> Vec<(&'static str, i64, &'static str)> {
    // Integers encode identically under both inline number representations — a plain
    // integer is a mantissa at the reserved exponent code either way — so only the *float*
    // differs, which is the one number below that is feature-dependent.
    let half = if cfg!(feature = "arbitrary_precision") {
        // Base 10: `5 * 10^-1`. mantissa 5 << 8 | code (-1 + bias 7) << 4 | IS_NUMBER.
        (5 << 8) | (6 << 4) | 8
    } else {
        // Base 2: `1 * 2^-1`. The same shape, a different mantissa and exponent for the
        // same value — which is the whole difference between the two representations.
        (1 << 8) | (6 << 4) | 8
    };

    vec![
        (
            "probe_null",
            1 << 5,
            "Null: discriminant 1 in the payload bits",
        ),
        ("probe_true", 3 << 5, "True: discriminant 3"),
        ("probe_false", 2 << 5, "False: discriminant 2"),
        (
            "probe_empty_string",
            1 << 4,
            "IS_STRING alone: length 0, no bytes — and still non-zero, so the empty string \
             is not the reserved niche",
        ),
        (
            "probe_inline_string",
            // 0x63'62'61'70: the bytes 'c','b','a' above the control byte
            // `IS_STRING | (3 << 5)`, little-endian.
            (i64::from(b'c') << 24)
                | (i64::from(b'b') << 16)
                | (i64::from(b'a') << 8)
                | (1 << 4)
                | (3 << 5),
            "\"abc\": control byte 0x70, then the three bytes",
        ),
        (
            "probe_zero",
            (0 << 8) | (15 << 4) | 8,
            "integer 0: a zero mantissa at the reserved exponent code — and *not* the \
             all-zero niche, because IS_NUMBER is set",
        ),
        (
            "probe_small_int",
            (42 << 8) | (15 << 4) | 8,
            "integer 42: mantissa 42 at the reserved exponent code",
        ),
        (
            "probe_negative_int",
            (-1 << 8) | (15 << 4) | 8,
            "integer -1: the mantissa is signed, so the top bits are all ones",
        ),
        ("probe_half", half, "0.5, in the active inline number base"),
    ]
}

// Ignored under Miri, which cannot shell out to a compiler — and anywhere the words would
// differ. The layout is the same shape elsewhere, but the mantissa and the inline-string
// capacity are narrower, so the constants below are the 64-bit little-endian encoding and
// there is nothing to gain from re-deriving them.
//
// One `cfg_attr`, not two: on a 32-bit Miri run both conditions hold, and the second
// `#[ignore]` would then be an attribute that does nothing — which is a warning, and CI
// denies those.
#[test]
#[cfg_attr(
    any(miri, not(all(target_pointer_width = "64", target_endian = "little"))),
    ignore = "needs a compiler to shell out to, and a 64-bit little-endian target to               expect these words of"
)]
fn constructing_a_small_value_folds_to_a_constant() {
    let ir = any_ir();

    let mut failures = Vec::new();
    for (name, word, meaning) in constants() {
        let Some(body) = common::body_of(ir, name) else {
            failures.push(format!("{}: not found in the emitted IR", name));
            continue;
        };

        // Exactly one instruction, returning exactly this word: no call, no branch, no
        // allocation — the construction happened at compile time.
        let want = format!("ret ptr inttoptr (i64 {} to ptr)", word);
        if body != [want.as_str()] {
            failures.push(format!(
                "{} ({})\n    expected: {}\n    got:      {}",
                name,
                meaning,
                want,
                body.join("\n              ")
            ));
        }
    }

    assert!(
        failures.is_empty(),
        "constructing a small value no longer folds to its constant:\n\n{}\n\n\
         Each of these should compile to a single `ret` of the encoded inline word. A \
         *different constant* most likely means the inline bit layout changed — decode it \
         against the table in this file's module docs. Anything other than a lone `ret` \
         means the constructor stopped folding, so building a small value now costs real \
         work at run time.",
        failures.join("\n\n")
    );
}

// --- Reading ----------------------------------------------------------------

/// An accessor, and the *only* calls its generated code is allowed to make.
///
/// An allow-list rather than a list of forbidden things: a fast path should call nothing
/// at all, and the exceptions are few enough to name. Anything else — a panic, an
/// allocation, or work left out of line that should have been inlined — is a regression,
/// and naming what is allowed means a *new* kind of cost cannot slip through unlisted.
struct FastPath {
    name: &'static str,
    /// Matched as substrings of the mangled symbol, so they survive both manglings.
    allowed: &'static [&'static str],
    /// Why each allowance is there — quoted back when the operation trips over it.
    because: &'static str,
}

fn fast_paths() -> Vec<FastPath> {
    // `INumber::to_f64_lossy` unwraps the `Option` that `IValue` returns, because an
    // `INumber` is always a number. The unwrap asserts the type's own invariant rather than
    // handling a case that can arise, and it is cold — better than the alternative, a
    // default value that would silently paper over the bug.
    const UNWRAP: &str = "unwrap_failed";

    // The numeric model. A conversion may call into it: the value work lives there, and
    // with `arbitrary_precision` an arm of it reduces a bignum — real work, rightly out of
    // line. Whether the *small* arms are inlined into the caller is LLVM's cost-model call
    // and differs by platform, so it is not pinned here; what is pinned is that nothing
    // else is called, and that none of it panics, allocates, or goes through a vtable.
    const NUMERIC: &str = "numeric";

    let dispatch_only = |name| FastPath {
        name,
        allowed: &[],
        because: "asking a value's type is a switch on the tag and nothing more",
    };
    let converts = |name| FastPath {
        name,
        allowed: &[NUMERIC],
        because: "a conversion may call into the numeric model, and nothing else",
    };

    vec![
        dispatch_only("probe_is_number"),
        dispatch_only("probe_type"),
        dispatch_only("probe_has_decimal_point"),
        converts("probe_to_i64"),
        converts("probe_to_u64"),
        converts("probe_to_f64"),
        FastPath {
            name: "probe_to_f64_lossy",
            allowed: &[NUMERIC, UNWRAP],
            because: "a conversion may call into the numeric model; and                       `INumber::to_f64_lossy` unwraps, asserting that an `INumber` really                       is a number",
        },
    ]
}

/// What a call it should not be making costs it.
fn cost_of(symbol: &str) -> &'static str {
    // An `unreachable` *instruction* is fine, and expected: an exhaustive switch over the
    // tag ends in `default.unreachable`, which generates nothing. A *call* into the panic
    // machinery is not — it drags in the formatting `Arguments` and a cold block, in an
    // operation that cannot actually fail.
    if symbol.contains("panic") || symbol.contains("unwrap_failed") {
        "panics"
    } else if symbol.contains("__rust_alloc") || symbol.contains("__rust_realloc") {
        "allocates"
    } else {
        "is not inlined — the work should be straight-line code here"
    }
}

#[test]
#[cfg_attr(miri, ignore = "shells out to a compiler, which Miri cannot run")]
fn reading_a_value_stays_on_the_fast_path() {
    let ir = any_ir();

    let mut failures = Vec::new();
    for probe in fast_paths() {
        let Some(body) = common::body_of(ir, probe.name) else {
            failures.push(format!("{}: not found in the emitted IR", probe.name));
            continue;
        };
        assert!(!body.is_empty(), "{}: empty body", probe.name);
        let called = common::called_symbols(&body);

        // Never, for any accessor: the dispatch resolved to a vtable. `codegen.rs` asserts
        // this across the library; here it is asserted of the operations that matter most.
        if body.iter().any(|line| common::is_indirect_call(line)) {
            failures.push(format!(
                "{}: calls through a function pointer — the representation dispatch is no \
                 longer devirtualized",
                probe.name
            ));
        }

        for symbol in called {
            if probe.allowed.iter().any(|allowed| symbol.contains(allowed)) {
                continue;
            }
            failures.push(format!(
                "{}: {} — calls `{}`\n      (all it may call: {})",
                probe.name,
                cost_of(symbol),
                symbol,
                probe.because
            ));
        }
    }

    assert!(
        failures.is_empty(),
        "reading a value no longer stays on the fast path:\n\n  {}\n\n\
         These run in the innermost loop of anything that walks a document, and every cost \
         above is invisible from behaviour alone — the results stay correct, they just stop \
         being cheap.\n\n\
         If an operation has started to panic, look for an `unreachable!()` on a state that \
         cannot occur: it is indeed unreachable, but the compiler does not know that, so it \
         emits the panic and its `Arguments` into every caller. State the invariant with a \
         `debug_assert!` over a total fallback instead — checked where checks are \
         affordable, and generating nothing where they are not.",
        failures.join("\n  "),
    );
}

// --- The fast path, exactly -------------------------------------------------

/// What each operation compiles to when the value turns out to be stored inline.
///
/// The probe has told the compiler *which representation* the value uses and nothing else
/// — never which value — so the dispatch folds away and what remains is the fast path
/// alone. The slow paths are free to be anything; this says exactly what the fast one is.
///
/// The opcodes, not the whole instructions: registers are numbered by the compiler, and
/// attributes and metadata come and go, none of which says anything about what the code
/// *does*. Exact about the part that matters, indifferent to the part that does not.
fn fast_paths_exactly() -> Vec<(&'static str, &'static [&'static str], &'static str)> {
    vec![
        (
            "probe_fast_is_number",
            &["ret"],
            "an inline number is a number: nothing to compute",
        ),
        (
            "probe_fast_type",
            &["ret"],
            "and its type is a constant, for the same reason",
        ),
        (
            "probe_fast_has_decimal_point",
            &["load", "ptrtoint", "and", "icmp", "ret"],
            "read the word, and compare the exponent code against the one that means \
             `integer`",
        ),
        (
            "probe_int_has_decimal_point",
            &["ret"],
            "an integer has no decimal point: once the exponent code is known, a constant",
        ),
        (
            "probe_int_to_i64",
            &["load", "ptrtoint", "ashr", "insertvalue", "ret"],
            "read the word, shift the mantissa down (an arithmetic shift, so it arrives \
             sign-extended), and wrap it in `Some`",
        ),
        (
            "probe_int_to_u64",
            &[
                "load",
                "ptrtoint",
                "ashr",
                "icmp",
                "zext",
                "insertvalue",
                "insertvalue",
                "ret",
            ],
            "the same, and a sign test — a negative integer is not a `u64`",
        ),
        (
            "probe_int_to_f64_lossy",
            &["load", "ptrtoint", "ashr", "sitofp", "ret"],
            "the same shift, and one conversion instruction",
        ),
    ]
}

#[test]
#[cfg_attr(miri, ignore = "shells out to a compiler, which Miri cannot run")]
fn the_fast_path_is_exactly_this() {
    let ir = fast_ir();

    let mut failures = Vec::new();
    for (name, expected, meaning) in fast_paths_exactly() {
        let Some(body) = common::fast_path_of(ir, name) else {
            failures.push(format!("{}: not found in the emitted IR", name));
            continue;
        };
        let actual = common::opcodes(&body);
        if actual != expected {
            failures.push(format!(
                "{}{}\n    expected: {}\n    got:      {}\n{}",
                name,
                meaning,
                expected.join(", "),
                actual.join(", "),
                body.iter()
                    .map(|line| format!("      {}\n", line))
                    .collect::<String>(),
            ));
        }
    }

    assert!(
        failures.is_empty(),
        "the fast path is no longer what it was:\n\n{}\n\
         These are the operations a document walk spends its time in, on the values it \
         spends its time on — a small integer, held inline. Each should be the pointer \
         word, a shift, and nothing else. An instruction that has appeared is work every \
         one of them now pays, and nothing about the behaviour will have changed to show \
         it: look for a function that stopped being inlined (the numeric model's decode \
         is the usual one — it is large, and only `#[inline]` keeps its hot branch \
         folding into the caller), or for a check that is now being made twice.",
        failures.join("\n\n")
    );
}

// --- Building the probes ----------------------------------------------------

/// The IR of the probe crate that reads values of *unknown* representation, built once
/// however many tests ask for it.
fn any_ir() -> &'static str {
    static IR: OnceLock<String> = OnceLock::new();
    IR.get_or_init(|| emit_probe_ir("any", probes::ANY))
}

/// The IR of the probe crate that reads values of *known* representation.
fn fast_ir() -> &'static str {
    static IR: OnceLock<String> = OnceLock::new();
    IR.get_or_init(|| emit_probe_ir("fast", probes::FAST))
}

/// Writes a crate of probes with a path dependency on this one, builds it with fat LTO,
/// and returns the emitted LLVM IR.
fn emit_probe_ir(name: &str, source: &str) -> String {
    let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(format!("codegen-probes-{}", name));
    std::fs::create_dir_all(dir.join("src")).expect("create the probe crate");

    // A single-quoted TOML string, so a Windows path's backslashes are not escapes. A
    // `cdylib` because its `#[no_mangle]` exports are LTO roots: a `lib` would have its
    // probes internalized and dropped, and a `bin` keeps only what `main` reaches.
    let manifest = format!(
        "[package]\nname = \"probes\"\nversion = \"0.0.0\"\nedition = \"2018\"\n\n\
         [lib]\ncrate-type = [\"cdylib\"]\n\n\
         [dependencies]\nijson = {{ path = '{}'{} }}\n\n\
         [profile.release]\nlto = \"fat\"\ncodegen-units = 1\n",
        env!("CARGO_MANIFEST_DIR"),
        if cfg!(feature = "arbitrary_precision") {
            ", features = [\"arbitrary_precision\"]"
        } else {
            ""
        },
    );
    write_if_changed(&dir.join("Cargo.toml"), &manifest);
    write_if_changed(&dir.join("src/lib.rs"), source);

    let status = common::nested_cargo()
        // Exposes `ijson::codegen_probes`, the hooks that let a probe state which
        // representation a value uses. Nothing but this build ever sets it.
        .env("RUSTFLAGS", "--cfg codegen_probes")
        .current_dir(&dir)
        .args([
            "rustc",
            "--release",
            "--",
            "--emit=llvm-ir",
            "-Cdebuginfo=0",
        ])
        .status()
        .expect("failed to run `cargo rustc` on the probe crate");
    assert!(status.success(), "building the probe crate failed");

    // `codegen-units = 1` means a single module, but the file is named for the crate, and
    // that naming is not something to hard-code across platforms.
    let deps = dir.join("target/release/deps");
    let ir_file = std::fs::read_dir(&deps)
        .expect("read the probe crate's deps directory")
        .filter_map(Result::ok)
        .map(|entry| entry.path())
        .filter(|path| {
            path.extension().is_some_and(|ext| ext == "ll")
                && path
                    .file_name()
                    .and_then(|name| name.to_str())
                    .is_some_and(|name| name.starts_with("probes"))
        })
        // A stale `.ll` from an earlier build can linger; take the freshest.
        .max_by_key(|path| path.metadata().and_then(|meta| meta.modified()).ok())
        .unwrap_or_else(|| panic!("no `probes*.ll` was emitted in {:?}", deps));

    let ir = std::fs::read_to_string(&ir_file).expect("read the probe crate's LLVM IR");

    // If the nested build was instrumented anyway, every probe begins by bumping a
    // profiling counter: nothing folds, nothing inlines, and the IR is not the library's.
    // Say that, rather than reporting the instrumentation as a codegen regression.
    assert!(
        !common::is_instrumented(&ir),
        "the probe crate was built with coverage instrumentation, so its IR is not the \
         library's. Something is still feeding flags into the nested build:\n\n{}",
        common::flag_environment()
    );
    ir
}

/// Leaves the file alone when the contents already match, so `cargo` does not rebuild the
/// probe crate (and re-run LTO) on every invocation.
fn write_if_changed(path: &Path, contents: &str) {
    if std::fs::read_to_string(path).ok().as_deref() != Some(contents) {
        std::fs::write(path, contents).unwrap_or_else(|e| panic!("write {:?}: {}", path, e));
    }
}