Native program reference¶
This page documents the Rust core’s internal instruction set: the exact
Op variants rustruct.compile() lowers a schema into. Using the
Python API does not require reading it – How schemas work covers
derived fields, discriminants, and what a schema cannot express in terms of
the schema itself, with no Rust types involved. Read this page when
modifying rustruct-core, or when you want to know exactly what
instructions a particular schema compiles to and why.
A Program is a flat Vec<Op> the Rust core walks directly for both pack
and unpack, with no re-interpretation of the original declaration.
A Program is a fixed, fully unrolled tree: every nested struct, every
array element, every switch branch is inlined into the parent op at
compile time. There is no call/return or loop-back instruction – a schema
that would require one (a type referencing itself, directly or through a
cycle of other types) cannot be compiled, because the resulting Program
would have to be infinite.
Common¶
Every opcode except Fixed and BitRun carries a Common { key, span }:
key–Key::Named(name)if the field has a name and appears in the decoded mapping, orKey::Skipfor a field that’s part of the wire layout but never surfaces as its own key (an array element, a switch branch, awhenpayload – these get their name, if any, from the field that wraps them).span– set when a digest’sover=covers this field, so pack/unpack can record where its bytes start and end.
Fixed and BitRun don’t need a shared Common because they hold several
named sub-items directly (FixedItem/BitItem, each with its own key).
Fixed¶
Fixed { width: usize, items: Vec<FixedItem> }
A coalesced run of same-region fixed-width fields, checked and read/written
as one contiguous span with a single bounds check – not one op per scalar.
Each FixedItem has an off (byte offset within the run) and a FixKind:
Int { prim, be, expected, derived }–u8..u64/i8..i64.expectedis set forconst=;derivedis set once alen=/count=/size=reference claims this field as a backpatch target (see Derived fields and discriminants, in program terms).F32 { be }/F64 { be }.Bool { expected }– one byte, nonzero isTrueon unpack;expectedis set forconst=.Raw { len, expected }– a fixed-length byte string;expectedis set forconst=(checked on unpack, written on pack, e.g. a magic number).
Adjacent fixed fields (including across a struct’s whole run of them) always
coalesce into one Fixed op – a fully static schema lowers to exactly one.
BitRun¶
BitRun { nbytes: usize, items: Vec<BitItem> }
Consecutive bits() fields packed most-significant-bit-first into whole
bytes (nbytes = total_bits / 8; compilation rejects a run that isn’t a
whole number of bytes). Each BitItem has a width, signed, and its own
derived flag (the same backpatch mechanism as FixKind::Int, just scoped
to one item’s bits instead of a whole byte-aligned field).
Bytes / Str / CStr¶
Bytes { len: LenSrc, max: usize, c: Common }
Str { len: LenSrc, max: usize, enc: Enc, c: Common }
CStr { max: usize, enc: Enc, c: Common }
Dynamic-length byte/text fields. LenSrc is either Expr { expr, inv } (a
length expression, with inv set when it’s linearly invertible for pack-time
derivation) or Greedy (len="*"/until-region-end). Str additionally
carries an Enc (Utf8/Ascii/Latin1); CStr has no len at all –
its extent is a NUL terminator on unpack, and pack writes one after the
string’s bytes. max bounds the allocation regardless of what the length
expression evaluates to.
Flags¶
Flags { prim, be, items: Vec<FlagItem>, rest: RestPolicy, rest_mask: u64, c }
A fixed-width integer decoded into named boolean/masked sub-fields. Each
FlagItem has a mask, a shift, and is_bool (a single-bit mask exposed
as bool rather than an unshifted int). rest (Keep/Strict/Ignore)
governs the bits no FlagItem names: keep them and round-trip verbatim,
reject a nonzero value at that mask (Strict), or silently zero them
(Ignore).
Digest¶
Digest { algo: Algo, over: Over, verify: bool, be: bool, c: Common }
A checksum or hash. over is Star (the whole enclosing scope, treating the
digest field’s own bytes as zero while computing) or Spans(Vec<u8>) (a list
of span-register indices naming specific sibling fields). Pack always
computes and writes the digest, ignoring any caller-supplied value; unpack
verifies it unless verify=false, and always exposes the wire value either
way.
Nest¶
Nest { prog: Arc<Program>, size: Option<LenSrc>, c: Common }
A nested structure: prog is that structure’s own, independently-compiled
Program (a whole Vec<Op> inlined here, not a reference to a shared one –
even a nested struct’s own recursive-looking self-reference would need this
same prog field to hold an infinitely nested Program, which is why
self-referential schemas can’t compile). size, if present, makes this an
exact byte window (sized()): the nested program must consume exactly that
many bytes, no more, no less.
Array¶
Array { count: CountSrc, max_count: usize, elem: Box<Op>, elem_min: usize, c }
A repeated element. elem is one compiled Op reused for every element (the
same restriction as Nest applies transitively: an array of a
self-referential type can’t compile either). count is Expr{..} or
UntilEof (until_eof=True/greedy). max_count bounds the element count
regardless of what count’s expression evaluates to; elem_min (the
element’s own minimum size) lets streaming rustruct.Codec.parse() compute how many more
bytes are needed without decoding partial elements.
Switch¶
Switch { on: Expr, cases: Vec<(i64, Box<Op>)>, default: Option<Box<Op>>, c }
A tagged union: on selects a branch by exact integer match against cases
(no range matching – see below), falling back to default when present or
raising otherwise. Cases compile inline into the enclosing scope, not into
a scope of their own – a case’s own field references (a nested struct
does get its own scope, but the case wrapper itself does not) resolve
against the same symbol table as the switch’s own siblings. This is what
makes it possible for one field to be both a switch discriminant in some
branches and a length/count/size source in others (see next section).
Cond¶
Cond { pred: Expr, then: Box<Op>, c: Common }
A conditionally-present field (when(pred, then=...)): absent (zero wire
bytes, no key in the decoded mapping – not null) when pred evaluates to
0 against already-known registers, otherwise decoded/encoded as then.
Valid only as an ordinary struct field – never as an array element, a
switch branch, or nested inside another Cond – and, like an array element
or switch branch, gets no register of its own: nothing else can ref it,
and it can’t be named in a digest’s over=.
Derived fields and discriminants, in program terms¶
How schemas work explains why a field can be a derived length in some
switch branches and an explicit discriminant in others. Mechanically: a
FixKind::Int/BitItem’s derived flag and Switch’s use of a symbol as
on are tracked separately per symbol while a scope compiles, and only
resolved once the whole scope (all branches included) has been seen –
never at the first reference. A symbol used as on anywhere in its scope
never gets derived set, regardless of how many len=/count=/size=
expressions reference it elsewhere; those references compile to the same
“check against the actual data” path a const= field already uses.
Why self-reference and range dispatch don’t compile¶
Both limits in How schemas work’s “What a schema cannot express” follow
directly from Program having no recursion opcode and Switch matching by
exact value: a self-referential type would need Nest/Array’s prog/
elem to hold an infinitely large Program, and Switch’s cases is a
Vec<(i64, Box<Op>)> matched by equality, with no range representation at
all.