The upstream decides what the buffer means (L0 substrate)

tr::mem::bump_source_t takes a second argument nobody has to pass, and it is the one that changes what the source means. Once the caller’s buffer cannot fit a request, the bump draws from its upstream instead — so the same buffer is either a fast path or the node’s hard ceiling, depending on one word at the construction site.

upstream

an oversize request

what the buffer is

heap_source() (the default)

succeeds, off the process heap

a fast path

null_source()

nullptr

the bound

What to notice

  • This is what makes the bump a capability-preserving substitution. A std::pmr::monotonic_buffer_resource also spills past its buffer — but it spills to a throwing resource, which under -fno-exceptions is the abort() the failable seam exists to remove. Same shape, different ending.

  • used() is how you tell where a block came from. The example serves an oversize request through the default upstream and finds used() == 0: nothing was carved from the caller’s buffer, so a node that believed its buffer was the bound was quietly wrong. Watch this number, not the success of the call.

  • null_source() is the honest form of std::pmr::null_memory_resource(). Both mean “serve nothing”; only one of them says so by value.

  • A spilled block is still returned properly. release on a block that came from the upstream routes back to the upstream (only a block from the buffer is the no-op), so the composition leaks nothing — which is why the ASan leg is a real check on this example.

  • A refusal is not a broken source. After the oversize nullptr, the hard-bounded source still serves what fits and the earlier block is untouched. Exhaustion is backpressure, not a terminal state.

  • The shipped example of the bounded composition is the arena decode. wire_arena_decode names null_source() so a frame that outgrows the stack slab is refused rather than quietly served from the heap.

  • Nothing here is conditional — the target builds and runs under every CI leg.

Source

 1/*
 2 * SPDX-License-Identifier: Apache-2.0
 3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
 4 */
 5
 6/**
 7 * @file
 8 * @brief ONE CONCEPT — the UPSTREAM decides whether a buffer is a hint or the bound.
 9 *
10 * `tr::mem::bump_source_t` takes a second argument nobody has to pass, and it is the one
11 * that changes what the source MEANS. Once the caller's buffer cannot fit a request, the
12 * bump draws from its upstream instead:
13 *
14 * - the default upstream is `heap_source()`, so the buffer is a fast path and an oversize
15 *   request quietly succeeds off the process heap;
16 * - `null_source()` serves nothing, so the buffer IS the ceiling and an oversize request is
17 *   refused by value.
18 *
19 * That choice is why the bump is a capability-PRESERVING substitution for
20 * `std::pmr::monotonic_buffer_resource`, which also spills — but spills to a THROWING
21 * resource, which under `-fno-exceptions` is the `abort()` this seam exists to remove. A
22 * bounded node picks `null_source()`; `wire::decode_into` over a stack slab is the shipped
23 * example (see wire-arena-decode).
24 *
25 * Runs under ctest as `example_mem_bump_upstream`; returns non-zero on any failed check.
26 */
27
28#include <array>
29#include <cstddef>
30#include <cstdio>
31
32#include "libtracer/mem_source.hpp"
33
34namespace {
35
36/** @brief Report expectation @p what and record a failure on @p ok. */
37void check(bool& ok, bool cond, const char* what) {
38    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
39    ok = ok && cond;
40}
41
42}  // namespace
43
44int main() {
45    bool ok = true;
46    constexpr std::size_t kOversize = 512;  // larger than either buffer below
47
48    // Same buffer size, same request, two upstreams — and two different node behaviours.
49    alignas(std::max_align_t) std::array<std::byte, 128> soft_buf{};
50    alignas(std::max_align_t) std::array<std::byte, 128> hard_buf{};
51    tr::mem::bump_source_t soft{soft_buf};                          // upstream: heap_source()
52    tr::mem::bump_source_t hard{hard_buf, tr::mem::null_source()};  // upstream: nothing
53
54    void* const from_buffer = hard.try_alloc(64, 8);
55    check(ok, from_buffer == hard_buf.data(), "what FITS is served from the buffer either way");
56    check(ok, hard.used() == 64, "and only that is counted against the buffer");
57
58    void* const spilled = soft.try_alloc(kOversize, 8);
59    check(ok, spilled != nullptr, "with a heap upstream, an oversize request still succeeds");
60    check(ok, soft.used() == 0,
61          "but used() stays 0 — the block came from the UPSTREAM, not the caller's buffer");
62    std::printf("default upstream: %zu-byte request served, %zu buffer bytes used\n", kOversize,
63                soft.used());
64
65    // Releasing a spilled block routes back to the upstream (a bump block's release is the
66    // no-op; this one is not), so the composition leaks nothing.
67    soft.release(spilled, kOversize, 8);
68
69    check(ok, hard.try_alloc(kOversize, 8) == nullptr,
70          "with null_source() upstream the buffer is the HARD bound — the answer is nullptr");
71    check(ok, hard.used() == 64, "a refusal carves nothing; the earlier block is untouched");
72    check(ok, hard.try_alloc(32, 8) != nullptr,
73          "and the source keeps serving what still fits — a refusal is not a broken source");
74    return ok ? 0 : 1;
75}

See also: backends · memory substrate reference · a bump source · decode_into: a flat arena.