decode_into: a flat arena drawn from a stack slab (L2/L3 codec)

decode returns an owning tlv_t whose children vectors allocate on the global heap by construction — fine on a host, wrong at a terminus that must not touch the heap on the receive path. wire::decode_into (ADR-0041) answers the identical grammar with a flat, pre-order arena_tlv_t array whose storage comes from an injected block source (CONTEXT.md §Block source). Point that at a bump_source_t over a std::array and the whole decode allocates nothing.

What to notice

  • The arena holds structure; the bytes stay where they were. Every span in a node borrows the input buffer, which must outlive the arena. The example asserts a node’s body points into the frame, not beside it.

  • Navigation is index arithmetic. Children of node i begin at i + 1, and end is one past the last descendant — so next_sibling is a single load, and skipping a subtree costs the same as reading one field. No pointer chasing, no per-node allocation.

  • null_source() upstream makes the slab a hard bound. A bump_source_t spills to its upstream once full; passing the source that serves nothing means an overflowing frame is refused (TLV_NESTING_TOO_DEEP — see decode refusals) instead of quietly reaching the heap. That is the seam doing its job: exhaustion by value, never a throw and never an abort() on a -fno-exceptions target.

  • A bump source is scope-lifetime storage. Blocks are never individually reclaimed, so it is constructed per operation (or reset between them) — not wired as a long-lived receiver seam, where it would monotonically fill and then refuse everything.

  • This is a resolve-scoped object. Read it, take the ownership copies you need, drop it. Storing a borrowed span is the one thing the ADR-0041 contract forbids.

  • 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 — `decode_into`: the same frame as a flat arena, drawn from a stack slab.
 9 *
10 * `decode` returns an OWNING `tlv_t` whose `children` vectors allocate on the global heap by
11 * construction. `wire::decode_into` (ADR-0041) answers the same grammar with a flat,
12 * pre-order `arena_tlv_t` array whose storage comes from an injected
13 * `tr::mem::block_source_t` — point that at a `bump_source_t` over a stack buffer and the
14 * whole decode touches no heap. Every span in the arena borrows the input buffer; the arena
15 * holds structure only, never bytes.
16 *
17 * Navigation is index arithmetic rather than pointer chasing: children of node `i` start at
18 * `i + 1`, and `end` is one past the last descendant, so `next_sibling` is a single load.
19 *
20 * Runs under ctest as `example_wire_arena_decode`; returns non-zero on any failed check.
21 */
22
23#include <array>
24#include <cstddef>
25#include <cstdint>
26#include <cstdio>
27#include <span>
28#include <vector>
29
30#include "libtracer/mem_source.hpp"
31#include "libtracer/tracer.hpp"
32
33namespace {
34
35using tr::wire::tlv_t;
36using tr::wire::type_t;
37
38/** @brief Report expectation @p what and record a failure on @p ok. */
39void check(bool& ok, bool cond, const char* what) {
40    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
41    ok = ok && cond;
42}
43
44}  // namespace
45
46int main() {
47    bool ok = true;
48    const std::vector<std::byte> x{std::byte{0x11}, std::byte{0x22}};
49    const std::vector<std::byte> y{std::byte{0x33}, std::byte{0x44}};
50
51    tlv_t point;
52    point.type = type_t::POINT;
53    point.opt.pl = true;
54    point.children.push_back(tlv_t{.type = type_t::VALUE, .payload = std::span(x)});
55    point.children.push_back(tlv_t{.type = type_t::VALUE, .payload = std::span(y)});
56    const std::vector<std::byte> frame = tr::wire::encode(point);
57
58    // The whole decode's storage: one stack buffer. null_source() upstream makes the buffer
59    // the HARD bound — an overflowing frame is refused, never quietly served from the heap.
60    std::array<std::byte, 1024> slab{};
61    tr::mem::bump_source_t bump{slab, tr::mem::null_source()};
62
63    const auto arena = tr::wire::decode_into(frame, bump);
64    check(ok, arena.has_value(), "the frame decodes into the caller's slab");
65    if (!arena) return 1;
66
67    std::printf("%zu nodes, %zu slab bytes used, zero heap allocations\n", arena->size(),
68                bump.used());
69    check(ok, arena->size() == 3, "root plus two children, flat and pre-order");
70    check(ok, arena->root().type == type_t::POINT, "index 0 is the root");
71
72    const std::uint32_t first = tr::wire::tlv_arena_t::first_child(0);
73    check(ok, first == 1 && (*arena)[first].type == type_t::VALUE, "a child starts at i + 1");
74    const std::uint32_t second = arena->next_sibling(first);
75    check(ok, second == 2 && (*arena)[second].type == type_t::VALUE,
76          "and its sibling is one load away — next_sibling IS the subtree end");
77    check(ok, arena->next_sibling(second) == arena->root().end,
78          "walking off the last child lands exactly on the parent's end");
79    const std::span<const std::byte> body = (*arena)[second].body;
80    check(ok, body.data() == frame.data() + (frame.size() - body.size()),
81          "and the node's body is a span INTO the input buffer — no bytes were copied");
82    return ok ? 0 : 1;
83}

See also: frame codec · backends · memory substrate reference.