Representation & Layout
Updated to SPEC-246 (July 2026).
Representation & Layout
Section titled “Representation & Layout”A struct defines what fields exist. @layout(...) defines how they
are stored. @repr(...) defines how each field is encoded. Those are
three different questions, and Janus keeps them apart.
Why this matters
Section titled “Why this matters”Most languages fuse “what fields exist” with “how they’re laid out in
memory.” C fuses them so completely that it can never improve layouts.
Rust picks the opposite default (compiler-chosen layout) and makes you
opt into ABI stability with #[repr(C)]. Zig inherits C’s vocabulary
and adds extern struct / packed struct as separate keywords.
Janus takes a third position, tuned to what the language is for: deterministic builds, sovereign systems, zero-copy, databases, serialization, protocols, capability-safe kernels. That domain spends most of its time at the systems boundary, where predictable layout is the more valuable default. But the load-bearing argument isn’t “systems code wants stable layout more often” — it’s the asymmetry of failure modes:
- If the default is optimized and you forget the ABI attribute, you get a correctness bug — sometimes a security bug. Silent and fatal, observable six months later in production.
- If the default is stable and you forget the optimization attribute, you get a performance bug — measurable, localizable, fixable, never silently wrong.
A language for capability-safe kernels and protocols should default to the failure mode that preserves correctness. A forgotten optimization shows up in a profiler. A forgotten binary contract shows up in production.
The three layers
Section titled “The three layers”Every struct in Janus carries three orthogonal concerns, each owned by exactly one mechanism:
| Concern | Owner | Answers |
|---|---|---|
| Logical fields | the struct declaration | what fields exist? |
| Physical organization | @layout(...) | how are they stored? |
| Field representation | @repr(...) | how is each field encoded? |
@layout(wire) // physical organization@repr(network) // field byte order (big-endian)struct Header { version: u16, length: u32, flags: u16,}No leakage. No conflation. No hidden contracts.
Layout profiles
Section titled “Layout profiles”@layout(...) selects a layout profile — a named bundle of
physical-organization policy.
| Profile | Optimizes for | Architecture-independent? |
|---|---|---|
stable (default) | deterministic compilation, correctness | no (per-target) |
optimal | execution speed (compiler may reorder, pad) | no |
wire | persistence and interchange (binary contract) | yes |
packed | exact density (no padding) | no |
c | foreign ABI (C-compatible layout + calling convention) | no |
simd | hardware vector alignment | no |
cacheline | cache-line alignment | no |
opaque | intentionally unknowable representation | n/a |
Layout profiles are an extensible family — future profiles
(gpu, mmio, transparent, …) will be added without grammar
changes, as new named entries in the @layout registry. This is the
property that retires extern struct: one attribute system absorbs
every future representation policy instead of one keyword per policy.
A struct without an explicit @layout(...) is @layout(stable).
stable — the default
Section titled “stable — the default”struct CacheEntry { // @layout(stable) is implicit key: u64, value: u64,}- Field declaration order preserved.
- The Janus-defined alignment algorithm computes offsets (not “the local C ABI” — Janus pins its own algorithm).
- No field reordering, ever.
- Offsets reproducible: same source + same target ⇒ identical offsets across all Janus compiler versions.
This carries a formal guarantee — the Stable Layout Law:
For a fixed source text and a fixed target triple, every Janus compiler version, every LTO configuration, every backend, and every LLVM upgrade MUST produce identical field offsets, size, and alignment for any
@layout(stable)type.
This is reproducibility parameterized by (source, target). It is NOT
a cross-target guarantee — usize and pointer widths vary by target,
and stable reflects that. (Cross-target identical layout is what
@layout(wire) is for.) The Law is stated in full force because
@layout(optimal) explicitly forfeits it; the two profiles are defined
as opposites on the reproducibility axis.
- Offsets MAY differ between RV32, RV64, ARM64, x86_64, CHERI —
pointer and
usizewidths differ. That’s intentional. In-memory structures are per-target.
This is the correctness-preserving default. Use it for almost everything internal: allocator state, AST nodes, hash tables, caches.
optimal — the optimization opt-in
Section titled “optimal — the optimization opt-in”@layout(optimal)struct HotLoopState { a: u8, b: u64, c: u8, d: u64,}The compiler MAY reorder fields, insert padding, and choose any layout
preserving field accessibility. Optimizes for speed. Equivalent to a
Rust struct. Use only where profiling demonstrates a measurable
benefit.
optimal is the “dangerous” profile in the sense that it forfeits the
determinism guarantees of stable. No reproducibility guarantee —
offsets may differ across compiler versions, LTO configurations, or
backend upgrades. The danger is made explicit; the default stays safe.
This profile is optimal, not native. The word native is reserved
in Janus’s attribute vocabulary for target-native properties
(@repr(native) means target byte order, which is deterministic). A
layout profile that permits compiler reordering is an aspirational
optimization, not a target-native property — hence optimal.
wire — the binary contract
Section titled “wire — the binary contract”@layout(wire)@repr(network)struct SbiFrame { magic: u32, version: u8, flags: u8, length: u16, payload_crc: u32,}- Architecture-independent in-memory representation.
- Field order preserved.
- Canonical-zero padding: all padding bytes are defined to be zero and preserved across mutations (a representation invariant, not just a construction rule — see below).
- Fixed endianness.
@layout(wire)requires a non-native@repr. - Forbids pointers,
usize, slices, owned collections, capabilities, handles, closures, trait objects — onlyWireSafefields (below).
wire is for SSTable headers, SKV records, WAL entries, network
protocol structs, SBI frames — anything that crosses a persistence or
network boundary.
packed — exact density
Section titled “packed — exact density”@layout(packed)struct UdpHeader { // 8 bytes, no padding src_port: u16, dst_port: u16, length: u16, checksum: u16,}No padding. Alignment is 1. Field order preserved. Misaligned field access is the programmer’s responsibility (the compiler emits unaligned loads/stores). Architecture-independent in size and offset.
c — foreign ABI
Section titled “c — foreign ABI”@layout(c)struct epoll_event { // matches the Linux kernel's layout exactly events: u32, data: u64,}Layout follows the target’s C ABI. Calling convention for values of
this type follows C. Pairs with @repr(native) by default (matching
the historical behavior of extern struct). You MAY choose
@repr(network) with @layout(c) when interfacing with a foreign ABI
that itself carries a declared byte order.
Required for FFI with C libraries and for type definitions shared across a C/Janus boundary.
simd, cacheline — hardware-aligned
Section titled “simd, cacheline — hardware-aligned”@layout(simd)struct Vec4 { x: f32, y: f32, z: f32, w: f32 } // 16-byte aligned
@layout(cacheline)struct PerCpuCounter { value: u64, pad: [56]u8 } // 64-byte alignedsimd aligns to the widest vector-alignment requirement among fields.
cacheline aligns to the target cacheline size (commonly 64;
override with @layout(cacheline=128)). Otherwise both follow stable
rules. Use simd for vector math; cacheline to prevent false
sharing in concurrent arrays.
opaque — intentionally unknowable
Section titled “opaque — intentionally unknowable”@layout(opaque)struct Mutex // size and layout hidden
fn make() -> *Mutex { ... } // legal: pointerfn bad() -> Mutex { ... } // ❌ E2304 — cannot instantiate by valueLayout is intentionally unspecified — an incomplete type, mirroring
C’s incomplete types. Illegal: sizeof, offset_of, T{}, stack
allocation, field access, by-value instantiation, by-value struct
fields. Legal: *T, &T, Capability[T], static storage at a
fixed address.
opaque is for handles, runtime internals, synchronization
primitives, compiler-managed state. Users cannot accidentally depend
on offsets. It is the mirror of wire: wire commits to a fully
specified representation; opaque commits to none at all.
Combining profiles
Section titled “Combining profiles”Most structs have exactly one profile. The exceptions are explicit specializations, written as combined attributes:
@layout(wire) @layout(packed) // wire contract + no padding@layout(wire) @repr(network) // wire contract + BE fieldsThe named profiles are internally coherent bundles of policy
choices — each one vetted by the language to be self-consistent. A
fully composable form (@layout { order: preserved, endian: big, ... })
is deferred to a future revision; it would shift the coherence-vetting
burden onto every programmer.
@repr — field representation
Section titled “@repr — field representation”Field byte representation is governed by the orthogonal @repr(...)
attribute, defined in the Endian-Aware Type System
(SPEC-250). The two attributes compose.
The interaction rules
Section titled “The interaction rules”-
@layout(wire)requires a non-native@repr. Awirestruct without one is a hard error:E2302: @layout(wire) requires a non-native @reprreason:wire layout is an architecture-independent binary contract;native endianness would produce different bytes on different CPUshelp:add @repr(network) for big-endian, or @repr(little) for little-endianRationale: a
u32field in CPU-native byte order has different bytes on little-endian (x86) vs big-endian (PowerPC) targets. Without a fixed endianness, the in-memory form is not architecture-independent — which iswire’s entire reason to exist. For wire, endianness IS layout. -
@layout(c)defaults to@repr(native), matching the historical behavior ofextern struct. -
A struct without an explicit
@repris@repr(native)for all profiles exceptwire. Zero-cost path, but no compile-time proof of wire-format compatibility.
WireSafe — the compiler-derived proof
Section titled “WireSafe — the compiler-derived proof”A struct participating in a declared binary contract may be proven
WireSafe — a compiler-derived proof that the type has a fixed
layout, no process-local ownership, no process-local identity, and
exactly one canonical byte representation per value.
WireSafeis a proof, not a promise. There is nounsafe impl WireSafe.
If the compiler can derive WireSafe, it does. If it can’t, you get a precise diagnostic and the answer is “express the design differently” — not “override the proof.” If you have a special encoding trick, implement a codec; don’t lie to the type system.
The headline invariant
Section titled “The headline invariant”Every
WireSafevalue has exactly one valid in-memory byte representation, and that representation IS the canonical wire representation.
This is the Canonical Representation Principle. It composes beautifully — and importantly, the compositions are theorems derived from the proof, not independent operations:
| Operation | For T: WireSafe | Why it follows |
|---|---|---|
a == b | ⟺ memcmp(&a, &b) == 0 — compiler MAY lower == to memcmp | canonical bytes ⟹ byte equality ⟺ value equality |
hash(a) | derives from canonical bytes (BLAKE3(canonical_bytes)) | hash is a pure function of canonical bytes; equal values ⟹ equal hashes |
| serialize | socket.write(&value) — no serializer needed | canonical bytes ARE the wire form; serializer is memcpy |
| disk load | mmap() + cast — done | bytes are already canonical (trusted source) |
| fingerprint | canonical bytes ⇒ schema fingerprint ⇒ Merkle ⇒ content-addressable | schema identity is a pure function of canonical structure |
ordering (a < b) | lexicographic byte compare, no per-field dispatch | extends naturally for T: WireSafe + Ord |
Most languages define equality, hashing, serialization, ordering, and
fingerprinting as independent operations, each implemented
separately, each capable of drifting out of agreement (the classic
bug: a == b but hash(a) != hash(b)). Canonical Representation
collapses all of them into consequences of one compiler-proven
property. The compiler proves WireSafe once; six operations follow
as theorems. They cannot drift, because they are not independently
implemented.
This is what “representation is a type property, not a serialization property” means concretely: serialization, hashing, equality, ordering, and fingerprinting are all consequences of the type’s representation, not separate policies imposed on it.
WireSafe-eligible fields
Section titled “WireSafe-eligible fields”| Type | WireSafe? |
|---|---|
u8/i8/u16/i16/u32/i32/u64/i64/u128/i128 | ✅ |
be* / le* endian-qualified integers | ✅ |
bool | ✅ |
| fixed-size enums with contiguous discriminants | ✅ |
[N]T where T: WireSafe | ✅ |
@layout(wire) struct with all WireSafe fields | ✅ |
| Type | WireSafe? | Why |
|---|---|---|
*T, ?*T, &T | ❌ | process-local addresses (E2301 — hard reject) |
usize, isize | ❌ | target-width-dependent |
slices []T | ❌ | runtime objects, dynamic length |
String, Vec[T], HashMap[K,V] | ❌ | process-local heap ownership |
| capabilities, mutexes, file handles | ❌ | process-local identity / OS resources |
| closures, trait objects | ❌ | process-local code/vtable addresses |
f32 / f64 | ❌ by default | NaN payloads violate canonical form — see below |
The pointer ban
Section titled “The pointer ban”@layout(wire) @repr(network)struct Node { next: *Node, // ❌ E2301}E2301: wire layout cannot contain pointer-typed fields
reason: pointers are process-local addresses and cannot form an architecture-independent binary contract
help: use an offset, index, UUID, or handle insteadPointers are inherently target-specific (address width, CHERI provenance). Use offsets, indices, UUIDs, or handles — all of which are WireSafe integers.
Generic WireSafe
Section titled “Generic WireSafe”Like Rust’s Send/Sync, WireSafe carries generic bounds:
@layout(wire) @repr(network)struct Pair[A, B] { a: A, b: B,}// WireSafe(Pair[A, B]) ⇔ WireSafe(A) ∧ WireSafe(B)The tagged-union rule
Section titled “The tagged-union rule”For Maybe[T], Result[T, E], etc. to be WireSafe, the inactive
variant’s payload MUST be canonicalized to zero when inactive. There
is no third category of byte — every byte in a WireSafe struct is
either a meaningful field byte or canonical-zero padding. This makes
memcmp agree with semantic equality: two Maybe[u32] with tag = None MUST have identical bytes in the inactive value field.
Floats — canonical IEEE for wire
Section titled “Floats — canonical IEEE for wire”IEEE floats carry many NaN payloads (NaN(5), NaN(8), NaN(17) are
all semantically NaN but representationally distinct). The Canonical
Representation Principle forces a choice.
Canonical NaN: the IEEE 754-2008 default NaN for the type
(f32: 7FC0_0000; f64: 7FF8_0000_0000_0000 — sign 0, exponent
all-1s, quiet bit set, payload zero).
Boundary rule (the subtle part): normalization is boundary-driven, not arithmetic-driven. Ordinary FP computation is untouched. Normalization happens only when a value enters a wire representation:
| Operation | Normalizes? | Why |
|---|---|---|
FP arithmetic (+, *, sqrt, …) | NO | hardware produces whatever NaN the ISA specifies; we don’t slow computation |
Store to a @layout(wire) field | YES | crossing into a wire representation |
Construct a @layout(wire) value | YES | construction is a wire-boundary crossing |
| Serialize wire struct → bytes | NO (already canonical) | serialization is memcpy |
| Deserialize from untrusted source | YES | attacker may supply arbitrary NaN payloads |
| Deserialize from trusted Janus source | NO | producer already canonicalized |
The rule in one sentence: normalization happens when a value enters a wire representation, not when it is computed. This preserves FP arithmetic speed (no per-op tax) and limits cost to wire-boundary crossings, which are already the expensive operations.
Why not normalize all FP arithmetic? Performance (no per-op tax on hot numeric loops), hardware diversity (ARM/x86/RISC-V produce different NaNs from the same op), and debuggability (a programmer chasing a NaN-propagation bug needs to see the actual NaN, not a normalized one). Canonicalization belongs at the contract boundary, not in the debugger’s view. The full normative treatment — including exact NaN bit patterns and SBI/zerocopy interactions — is in SPEC-246 Appendix A.
Canonical padding
Section titled “Canonical padding”For @layout(wire):
All padding bytes are defined to be zero. The compiler owns padding. Programs may neither read nor write it.
This is a representation invariant, not a construction rule. “Zero on init” is a one-time event the optimizer can later violate. Canonical padding is a permanent property: after every mutation, the invariant still holds. The optimizer is responsible for preserving it.
Consequences: deterministic bytes, free memcpy serialization, no
padding leaks, no UB, no hidden garbage.
The deserialize boundary (asymmetric): canonical padding makes
struct→bytes free, but bytes→struct from an untrusted source is
not free — an attacker may supply non-canonical padding. Deserializing
wire bytes from untrusted sources MUST canonicalize on read, or the
source must be attested as Janus-produced. For SSTables written and
read by Janus, free; for an untrusted network packet, pay the
canonicalization. The zerocopy Layer B
(:sovereign profile) is the natural enforcement point.
Wire layout changes are contract breaks
Section titled “Wire layout changes are contract breaks”If you edit a @layout(wire) struct — add a field, remove one, change
a type — that is not just a source change. It is a breaking
persistence/API change: existing on-disk files, in-flight packets,
and outstanding capability tokens for the old layout no longer match.
The language treats @layout(wire) as part of the compatibility
story. (Per-version fingerprinting and a wire-layout-changed
diagnostic are planned; see SPEC-246 §12.1.) When evolving a wire
type, use versioning (@wire(version = 2)) or a new type name.
Migrating from extern struct
Section titled “Migrating from extern struct”extern struct is deprecated syntax. The parser rewrites it
during parsing to:
@layout(c)@repr(native)struct Foo { ... }After parsing, the frontend never sees extern struct — there is
exactly one semantic representation of layout in the AST. Compiling
extern struct emits:
W1204: `extern struct` is deprecated.
Use:
@layout(c) struct Foo
instead.
`extern struct` is a parser alias for `@layout(c) @repr(native)` andwill be removed in Janus 2.0.Why not keep both?
Section titled “Why not keep both?”Keeping extern struct as a parallel grammar path would violate the
orthogonality at the heart of this design. Today @layout(...) is the
one place layout lives. If extern struct survived, eventually
someone would want extern packed struct, extern simd struct,
extern cacheline struct — inventing keywords whenever a new
representation policy appears. SPEC-246 eliminates exactly that.
Why not remove it immediately?
Section titled “Why not remove it immediately?”Source compatibility. extern struct already exists in checked-in
code. Breaking it for purely syntactic reasons buys almost nothing.
The parser-rewrite approach (old syntax parses → lowers to new
semantic form → warning educates → later edition removes) mirrors
Rust’s edition migrations: language evolution preserves semantics
before syntax.
Migration table
Section titled “Migration table”| Before | After |
|---|---|
extern struct Foo | @layout(c) struct Foo |
extern struct Foo (with BE intent) | @layout(c) @repr(network) struct Foo |
wire-format extern struct Foo | @layout(wire) @repr(network) struct Foo (and prove WireSafe) |
The third row is a semantic upgrade, not a mechanical edit: moving a
type to @layout(wire) commits it to a binary contract and triggers
the WireSafe proof. Use it for types that genuinely cross persistence
or network boundaries (SSTable headers, protocol frames).
The constitutional principles
Section titled “The constitutional principles”This design rests on four principles that sit alongside ownership, effects, capabilities, and profiles as core pillars of Janus.
The Implicit-Contract Principle
Section titled “The Implicit-Contract Principle”Implicit facts are acceptable if they are mechanically derivable from explicit declarations. Implicit contracts are not.
This justifies auto-derived WireSafe (a fact derivable from declared
structure) while rejecting hidden ABI / hidden persistence / hidden
network contracts. WireSafe is implicit behavior that produces
derivable facts; that’s permitted. An undeclared binary contract is
an implicit contract; that’s not.
The Representation-Orthogonality Principle
Section titled “The Representation-Orthogonality Principle”A struct answers two distinct questions: “what fields exist?” and “how are they stored?” These MUST NOT be conflated.
The Canonical Representation Principle
Section titled “The Canonical Representation Principle”A value participating in a declared binary contract shall possess exactly one valid in-memory representation.
Padding, inactive-union payloads, NaN payloads, and other semantically irrelevant bits are not distinct valid representations. The compiler enforces canonicalization.
The Representation-is-a-Type-Property Principle
Section titled “The Representation-is-a-Type-Property Principle”Representation is a property of the type, not a property of serialization.
Most languages treat a wire format as a serializer concern: the type declares fields, and a separate serializer decides how those fields become bytes — producing the chronic mismatch between in-memory layout and on-wire layout that every serialization framework spends its complexity budget papering over.
Janus inverts this. Representation is part of the semantic identity
of the type. A type declared @layout(wire) @repr(network) carries
its representation contract as part of what it is, not as something
a serializer imposes on it. Serialization, hashing, equality,
fingerprinting, disk mapping, and network transport then derive
from the type’s representation — they do not define it.
This is what distinguishes Janus from Rust, Zig, C, C++, Swift, Go,
and most serialization frameworks. The strategic shape mirrors how
Rust unified ownership into one concept that propagated everywhere:
Janus unifies representation into one concept that propagates
everywhere. Combined with the profile family being extensible
(gpu, mmio, transparent, … land later without grammar
changes), this makes representation a core semantic pillar rather than
a compiler-internal detail.
See also
Section titled “See also”- SBI: Sovereign Binary Interface — wire-format
serialization built on
@layout(wire) - Memory — raw memory verbs; zerocopy Layer B for untrusted-source canonicalization
- Equality Model — Axis 4 byte equality,
which agrees with
==for WireSafe types - Endian-Aware Type System —
@reprand thebe*/le*qualified integer types - SPEC-246 (operational spec,
.agents/specs/_PROPOSED/) — the full normative text
Developed with Voxis Forge assistance; doctrine sharpened over a four-turn design conversation and ratified as Position A+ (Canonical Representation) per Founder decision 2026-07-03.