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 |
|---|---|---|
|
succeeds, off the process heap |
a fast path |
|
|
the bound |
What to notice¶
This is what makes the bump a capability-preserving substitution. A
std::pmr::monotonic_buffer_resourcealso spills past its buffer — but it spills to a throwing resource, which under-fno-exceptionsis theabort()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 findsused() == 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 ofstd::pmr::null_memory_resource(). Both mean “serve nothing”; only one of them says so by value.A spilled block is still returned properly.
releaseon 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_decodenamesnull_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.