path — addressing (L4)¶
In one paragraph
A path_t parses /sensor/temp (with an optional :field.sub[N] tail) into the
canonical PATH-TLV payload bytes — the concatenated packed segment records. Those
bytes, not the string, are the vertex-map key: dispatch is a byte compare, never a
string parse on the hot path.
What it does¶
path_t::parse validates and canonicalizes per the addressing rules (reference/03):
strip a trailing /, reject empty segments (//) and unrooted paths, enforce the
limits (≤64 B/segment, ≤1024 B total, ≤255 segments, ≤8 field steps). It emits the
canonical key — e.g. /sensor/temp → 06 'sensor' 04 'temp'
(12 bytes; each record is [u8 len][utf8], RFC-0018) — and parses the :-tail into a field_path_t (settings.app.setpoint,
subscribers[], subscribers[3]) for the field-write surface. path_key_t +
path_key_hash_t (FNV-1a over the bytes) key the unordered_map.
Interface¶
struct field_step_t { std::string name; bool indexed, append, wildcard; std::uint16_t index; };
struct field_path_t { std::vector<field_step_t> steps; };
class path_t {
explicit path_t(std::string_view); // known-good LITERAL: parses once, aborts if malformed
static result_t<path_t> parse(std::string_view); // RUNTIME string; result_t = expected<T, status_t>
std::span<const std::byte> key() const; // canonical PATH payload bytes
const field_path_t& field() const; // the :field.sub[N] tail
std::size_t segment_count() const;
};
class path_key_t { /* owned key bytes; ≤16 B inline, else one heap block */ };
struct path_key_hash_t { /* FNV-1a over the key bytes */ };
The two entry points differ in what failure means. path_t::parse is for a string whose
validity is itself a runtime condition, and returns status_t::INVALID_PATH
(core/src/path.cpp:98-106,119,124). The explicit constructor is for a compile-site
literal, where a malformed path is a source bug: it hard-aborts rather than yielding a
result_t the caller would only *-deref unchecked (path.hpp:194, defined
:335-339). Neither uses
exceptions, so both hold under -fno-exceptions.
String → bytes, once¶
flowchart LR
S["/sensor/temp:settings.app.setpoint"] --> P[path_t::parse]
P --> K["key bytes<br/>06 "sensor" · 04 "temp""]
P --> F["field<br/>settings → app → setpoint"]
K --> M{{"vertex map<br/>(byte-keyed)"}}
classDef e fill:#dbeafe,stroke:#1e40af
class M e
Consequences¶
No string work reaches dispatch. A path parses at one visible construction site and the graph API takes
const path_t&, so a held handle cannot re-parse; every subsequent read, write and subscribe on that path is a byte compare against the map key. The cost of the parse is paid once, at registration or at the literal, not per call.Local and remote addressing are the same bytes.
key()returns the PATH-TLV payload that travels on the wire, so a forwarded frame carries the key it was matched on and a remote address needs no translation into a local one.A malformed address fails at the boundary. Empty segments (
//), unrooted paths, reserved characters and every limit overrun reject at parse with a typedstatus_trather than surfacing as a miss deep in dispatch.The limits are a receiver’s buffer budget. ≤64 B per segment, ≤1024 B total, ≤255 segments (RFC-0023; the byte cap binds first under the current encoding, at 204) and ≤8 field steps (
core/include/libtracer/path.hpp:32,34,36,38) let a component size fixed scratch instead of allocating per frame — the mount-prefix walk’s stitch buffer is two segments’ worth,std::array<std::byte, kMaxSegmentBytes * 2>(core/src/fwd_router.cpp), and no longer scales with how wide a mount is (#523).Ordinary names cost no heap block.
path_key_tholds records up to 16 bytes inline (path_key_t::kInlineBytes,core/include/libtracer/path.hpp:356) — a packed segment record is a 1-byte length prefix plus the segment text, so a name of up to 15 characters never allocates; longer records spill to a single owned block.
API reference¶
Generated from core/include/libtracer/path.hpp by Doxygen.
-
class path_t¶
A parsed, canonical path: the PATH-TLV payload bytes (packed records) plus the optional field_path_t tail. The payload bytes are the vertex-map key.
Dispatch keys on the parsed bytes (key), never the string form — parse once, hold the value, and every read/write compares bytes (docs/reference/02 §dispatch).
A path also carries the optional bound form (binding, RFC-0024 §7.4). It stays a value type: the binding is an opaque slot the graph and transport tiers fill and validate, never a handle into either of them, so copying a path copies its binding and neither copy can outlive anything.
Public Functions
-
path_t() = default¶
An empty path (no segments, no field tail).
-
inline explicit path_t(std::string_view text)¶
Construct from a compile-site / known-good path LITERAL, parsing ONCE.
A
path_tis FIFTEEN pointer-widths: four owning containers plus a count.path_t p("/sensor/temp"); write(p, a); write(p, b);— parse the string a single time, then hold the value and reuse the handle; the graph API takesconst path_t&so a held path never re-parses on the hot path (docs/reference/02 §dispatch keys on the parsed PATH-TLV bytes, never the string). A malformed literal is a source bug, so this hard-aborts rather than yielding a fallibleresult_tthe caller would only*-deref unchecked. For a RUNTIME string whose validity is a genuine runtime condition, use parse (fallible).explicit— construction is always a visible, deliberate parse, never an implicit per-call one. No exceptions (usable under-fno-exceptions).Pinned, not observed. A path is a parse-once VALUE the API takes by
const&, and each of the two opaque slots (RFC-0024’s binding, RFC-0027’s path label) added a container to it — the exact growth an unwatched type absorbs one RFC at a time until somebody embeds it in a hot struct and pays for it per vertex. Expressed insizeof(void*)so it holds identically on rv32 and on a 64-bit host, which is the whole point of pinning a shape rather than a number: a change here is a deliberate edit, and it is the moment to ask whether the new member wants to be a slot at all. Same instrument, same reason, assizeof(path_label_t) == 4.
-
inline std::span<const std::byte> key() const noexcept¶
The vertex-map key: the canonical PATH-TLV payload bytes (packed records).
-
inline const field_path_t &field() const noexcept¶
The parsed
:fieldtail (empty when the path addresses the vertex value).
-
inline std::size_t segment_count() const noexcept¶
The number of segments in the path.
-
inline const path_binding_t &binding() const noexcept¶
This path’s bound form, if a mint has completed (RFC-0024 §7.4).
-
inline bool bind(std::span<const wire::path_ref_element_t> elements)¶
Record the bound form a mint answered with —
elementsin ROUTE order.Element 0 is the origin’s own reference to its first-hop connection vertex; the last is the terminus host’s reference to the target vertex. A mint answers only for the hosts that saw the operation, so the origin stacks its own element under what came back.
Refuses (leaving the path unbound) past the normative element bound: a route with more hosts than a
PATH_REFcan spell has no bound spelling, and the canonical path this object still holds is the answer — refusing beats truncating, which would produce a valid-looking binding for a different route.- Returns:
true iff the binding was recorded.
-
inline void clear_binding() noexcept¶
Drop the bound form and fall back to the canonical one (RFC-0024 §5.3).
What an origin does on a failed validation. There is nothing to tear down anywhere else — no hop holds state for a bound path — so forgetting the elements IS the teardown, and the next operation goes out canonically and may re-mint.
-
inline const path_label_cache_t &path_label() const noexcept¶
This path’s PATH-LABEL spelling, if a reply came back minted (RFC-0027 §6.1).
Named for the qualified term throughout, never bare “label”: §11.1 collision 1 was ruled **(a) qualify** at acceptance, so an unqualified “label” still means RFC-0004 §E.1’s per-link
u16and nothing else (’s is one), and this RFC’s concept is always the path label.route_handle.hpp
-
bool cache_path_label(std::span<const std::byte> body)¶
Cache the path-label spelling
bodya minted reply came back with (§6.1).The NET TIER’s call, never the application’s (§9). The bytes are a packed
PATHbody: a mixture of literal segments and label elements, in any order, exactly as the reply carried them — mixed paths are legal and expected, because a hop that does not mint simply leaves its own part a string (§5.2).Refused (leaving the path unlabelled, and nothing else touched) when the spelling has no reading or no point:
a body that does not walk cleanly, or that refuses the address on any record — a malformed element is
tr::path::invalid, and caching one would hand the next operation an address the far hop must refuse;a body carrying no path-label element — a pure-string spelling is what key already holds, and caching a second copy of it would buy a second thing to invalidate;
a body past
kMaxPathBytes, the same bound the canonical form is parsed under;a path that is already BOUND (
PATH_REF). §11.2 recommends against carrying two compressions of one address — they save the bytes one of them already saved and double the staleness surface for a single route — and this is the one arm where the refusal costs nothing to state, because the slot is new. It is deliberately NOT symmetric:bindis RFC-0024’s shipped surface and keeps its behaviour exactly, since §11.2 is a SHOULD pending §12.4’s measurement and the per-route choice is the mint call site’s (car 4). clear_path_label frees this arm whenever a caller wants the other form.
- Returns:
true iff the path-label spelling was cached.
-
inline void clear_path_label() noexcept¶
Drop the path-label spelling and fall back to the canonical one (RFC-0027 §7.2).
What an origin does on a
NOT_FOUND-class refusal. There is nothing to withdraw anywhere — no unbind frame, no lease, no TTL, no aging (§7.3) — so forgetting the bytes IS the recovery: the next operation goes out in strings, which always work, and the reply after it re-mints.
Public Static Functions
-
static result_t<path_t> parse(std::string_view text)¶
Parse and canonicalize a path string (fallible — for a RUNTIME string).
Accepts
"/sensor/temp"or"/sensor/temp:settings.deadline_ns". Canonicalizes: strip a trailing/, reject empty segments (//) and unrooted paths, enforce thekMaxSegmentBytes/kMaxPathBytes/kMaxSegments/kMaxFieldDepthlimits. A known-good literal uses the parse-once path_t(std::string_view) constructor instead.- Parameters:
text – The path string to parse.
- Returns:
The parsed path_t, or a
status_terror (e.g.INVALID_PATH).
-
path_t() = default¶
-
struct field_path_t¶
The parsed
:field.sub[N]tail of a path — a sequence of field_step_t.Empty when the path addresses the vertex value itself (no
:tail). Drives the field-write / field-read control surface (docs/reference/04).Public Functions
-
inline bool empty() const noexcept¶
True when there is no field tail (addresses the vertex value).
-
bool operator==(const field_path_t&) const = default¶
Value equality over the step sequence.
Public Members
-
std::vector<field_step_t> steps¶
The
.-separated steps; empty ⇒ the vertex value itself.
-
inline bool empty() const noexcept¶
-
struct field_step_t¶
One step of a field path: a NAME and an optional
[index]/[]append /[*]wildcard selector.The parsed form of one
.-separated component of a:field.sub[N]tail (docs/reference/03 §addressing). Exactly one of indexed (with index), append, or wildcard is meaningful when a[...]selector is present.Public Functions
-
bool operator==(const field_step_t&) const = default¶
Value equality over every field.
Public Members
-
std::string name¶
The step’s NAME (the text before any
[...]).
-
bool indexed = false¶
True if a
[...]selector was present.
-
bool append = false¶
True for
[]— append to a sequence.
-
bool wildcard = false¶
True for
[*]— FIELD index_mode=WILDCARD (RFC-0004 §C).
-
std::uint16_t index = 0¶
The
[N]index; valid whenindexed && !append && !wildcard.
-
bool operator==(const field_step_t&) const = default¶
The owned key¶
A path is looked up by an owned copy of its canonical bytes rather than by a
string. path_key_t is that copy, with a small-buffer optimization sized so that
a packed segment record — a 1-byte length prefix plus the segment text — fits inline
for names up to fifteen characters, which is the overwhelming norm; longer records spill to
one heap block. It is immutable after construction, matching its use: a vertex’s
name never changes. path_key_hash_t and path_key_eq_t are the hash-map
bindings over it, and target_key_t is the delivery-target key try_make_target_key
builds.
-
class path_key_t¶
Owned byte key (a copy of a path’s canonical payload / one segment record).
Small-buffer type (#380 §2): records up to kInlineBytes live inline — a packed record is ONE length byte plus the segment text (RFC-0018), so virtually every vertex name fits and costs NO heap block (a
std::vectorhere allocated ~32 B per named vertex). Longer records spill to one owned heap allocation. Immutable after construction/assignment (matches its use: a vertex’s name never changes, ADR-0057). Move leaves the source empty.Public Functions
-
inline explicit path_key_t(std::span<const std::byte> b)¶
Copy
binto the key (inline when it fits, else one heap block).
-
inline explicit path_key_t(const std::vector<std::byte> &b)¶
Copy the vector’s bytes (compat shape for
path_key_t{vector}callers).
-
inline path_key_t(const path_key_t &o)¶
Deep-copy
o'sbytes (inline or one spill block, as the length needs).
-
inline path_key_t &operator=(const path_key_t &o)¶
Replace this key with a deep copy of
o'sbytes.
-
inline path_key_t(path_key_t &&o) noexcept¶
Take over
o'sbytes (and spill block, if any);oreads empty after.
-
inline path_key_t &operator=(path_key_t &&o) noexcept¶
Release this key’s bytes and take over
o's;oreads empty after.
-
inline ~path_key_t()¶
Free the spill block, if this key owns one.
-
inline std::span<const std::byte> bytes() const noexcept¶
The key’s canonical bytes (inline or heap — one uniform window).
-
inline std::size_t size() const noexcept¶
The key’s byte length.
-
inline bool empty() const noexcept¶
True for the empty key (the root vertex’s name).
-
inline bool operator==(const path_key_t &o) const noexcept¶
Value equality over the key bytes.
Public Members
-
std::byte inline_[kInlineBytes]¶
In-place record storage (the norm).
-
std::byte *heap_¶
The spill block when
len_ > kInlineBytes.
Public Static Attributes
-
static constexpr std::size_t kInlineBytes = 16¶
Records at or under this many bytes are stored inline (no heap): a packed record is one length byte + the segment text, so names up to 15 characters — the overwhelming norm — never allocate.
-
inline explicit path_key_t(std::span<const std::byte> b)¶
-
struct path_key_hash_t¶
Hash functor for path_key_t (FNV-1a over the key bytes) — the map hasher.
Heterogeneous (
is_transparent): thestd::span<const std::byte>overload hashes the SAME bytes to the IDENTICAL value, so a by-span lookup keys the vertex map without materializing an owned path_key_t (the hot internal by-key path ingraph_t::find_ptr, which fans out fan_out / bubble_up / ACL-walk / FWD-resolve).Public Types
-
using is_transparent = void¶
Enables heterogeneous (by-span) map lookup.
Public Functions
-
std::size_t operator()(const path_key_t &k) const noexcept¶
Hash the key’s canonical PATH bytes.
-
std::size_t operator()(std::span<const std::byte> k) const noexcept¶
Hash canonical PATH bytes given as a span — same FNV-1a value as the owned key.
-
using is_transparent = void¶
-
struct path_key_eq_t¶
Heterogeneous equality for the vertex map (
is_transparent): compares path_key_t and rawstd::span<const std::byte>key bytes interchangeably, so a by-span lookup needs no owned key. Byte-equality, length included.Public Types
-
using is_transparent = void¶
Enables heterogeneous (by-span) map lookup.
Public Functions
-
inline bool operator()(const path_key_t &a, const path_key_t &b) const noexcept¶
True iff the two owned keys hold identical bytes.
-
inline bool operator()(const path_key_t &a, std::span<const std::byte> b) const noexcept¶
True iff the owned key’s bytes equal the span’s bytes.
-
inline bool operator()(std::span<const std::byte> a, const path_key_t &b) const noexcept¶
True iff the span’s bytes equal the owned key’s bytes.
-
inline bool operator()(std::span<const std::byte> a, std::span<const std::byte> b) const noexcept¶
True iff the two spans hold identical bytes.
-
using is_transparent = void¶
-
using tr::graph::target_key_t = std::shared_ptr<const std::vector<std::byte>>¶
A subscription edge’s canonical PATH key — immutable and refcount-shared.
Shared rather than owned because the dispatch snapshot must outlive a concurrent unsubscribe: vertex_t::snapshot_edges copies each active slot out under an edge pin so the graph can dispatch after releasing it, and the slot may be cleared in between. A deep copy satisfied that and cost a malloc + free per edge per delivery — a
std::vectorhas no small-buffer optimisation, so every non-null key allocated, which is the ordinary local-binding case (/sensor/temp:subscribers[] -> /dev/ctrl0/in/temp). Refcounting satisfies it for an atomic increment instead, exactly as edge_view_t::remote does one field over for the same hazard (#1448 — the whole cold half went the same way, so the snapshot now takes two refcounts and copies no bytes).Null ⇒ no local re-dispatch target (the callback-only or remote-only edge). The key is built once at admission and never mutated, so sharing it needs no synchronization beyond the control block’s own refcount.
-
inline target_key_t tr::graph::try_make_target_key(std::vector<std::byte> &&key) noexcept¶
Wrap
keyas a sharedtarget_key_t, NOTHROW — null on OOM or empty input.Mirrors
vertex_t::try_make_lkv’s probe-then-commit discipline (#477): under the MCU profile abad_allocis anabort(), and admission is reachable from a peer’s bytes (RFC-0014 made registration wire-driven), so this soft-fails by value instead.- Parameters:
key – The canonical PATH key bytes; an empty span yields null (no target).
-
inline bool tr::graph::valid_segment(std::string_view seg) noexcept¶
True iff
segis valid as ONE segment of the addressing grammar.THE segment predicate (ADR-0073 §1): every boundary where a name enters the graph — the local string parser, a wire
SPECcreation carrying a child name, a module registration — calls this ONE function, so the tiers cannot drift (#688; the drift, not any single missing check, is the recurring defect — cf. #681). A name that fails here answersINVALID_PATHwherever it is rejected.Checks: non-empty, at most
kMaxSegmentBytes, and none of the SEVEN reserved characters of reference/03 §Reserved characters —/and:are separators,.separates field levels,[/]delimit the grammar’s index suffix (which sits OUTSIDEname:segment = name [ index ]),*is the wildcard selector,?is reserved for the future. Rejecting the brackets is the normative MUST (reference/03 §Reserved characters, incorporated by spec v1 §3); the pre-#996 five-character subset admitted them on the theory thatframe[7]travels inside the NAME bytes — ruled the other way: address-index addressing, if it lands, lives outside the NAME bytes (#996, cf..out-of-scope/range-slice-addressing.md). The character set is pinned cross-tier by thepath/path-reserved-bracketsconformance vector.
-
constexpr std::size_t tr::graph::kMaxPathBytes = 1024¶
Max bytes in a whole canonical PATH payload (docs/reference/03 §limits).
-
constexpr std::size_t tr::graph::kMaxSegments = 255¶
Max segments in a path (reference/03 §limits; RFC-0023: min(255, byte cap)).
-
constexpr std::size_t tr::graph::kMaxSegmentBytes = 64¶
Max bytes in one path segment (docs/reference/03 §limits; the packed record’s
u8length field caps it at 255 forever, RFC-0018 §5).
-
constexpr std::size_t tr::graph::kMaxFieldDepth = 8¶
Max steps in a
:fieldtail (docs/reference/03 §limits).