Skip to content

Representation & Layout

Updated to SPEC-246 (July 2026).

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.


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.


Every struct in Janus carries three orthogonal concerns, each owned by exactly one mechanism:

ConcernOwnerAnswers
Logical fieldsthe struct declarationwhat 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(...) selects a layout profile — a named bundle of physical-organization policy.

ProfileOptimizes forArchitecture-independent?
stable (default)deterministic compilation, correctnessno (per-target)
optimalexecution speed (compiler may reorder, pad)no
wirepersistence and interchange (binary contract)yes
packedexact density (no padding)no
cforeign ABI (C-compatible layout + calling convention)no
simdhardware vector alignmentno
cachelinecache-line alignmentno
opaqueintentionally unknowable representationn/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).

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 usize widths 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.

@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.

@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 — only WireSafe fields (below).

wire is for SSTable headers, SKV records, WAL entries, network protocol structs, SBI frames — anything that crosses a persistence or network boundary.

@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.

@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.

@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 aligned

simd 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.

@layout(opaque)
struct Mutex // size and layout hidden
fn make() -> *Mutex { ... } // legal: pointer
fn bad() -> Mutex { ... } // ❌ E2304 — cannot instantiate by value

Layout 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.


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 fields

The 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.


Field byte representation is governed by the orthogonal @repr(...) attribute, defined in the Endian-Aware Type System (SPEC-250). The two attributes compose.

  • @layout(wire) requires a non-native @repr. A wire struct without one is a hard error:

    E2302: @layout(wire) requires a non-native @repr
    reason:
    wire layout is an architecture-independent binary contract;
    native endianness would produce different bytes on different CPUs
    help:
    add @repr(network) for big-endian, or @repr(little) for little-endian

    Rationale: a u32 field 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 is wire’s entire reason to exist. For wire, endianness IS layout.

  • @layout(c) defaults to @repr(native), matching the historical behavior of extern struct.

  • A struct without an explicit @repr is @repr(native) for all profiles except wire. Zero-cost path, but no compile-time proof of wire-format compatibility.


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.

WireSafe is a proof, not a promise. There is no unsafe 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.

Every WireSafe value 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:

OperationFor T: WireSafeWhy it follows
a == bmemcmp(&a, &b) == 0 — compiler MAY lower == to memcmpcanonical 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
serializesocket.write(&value) — no serializer neededcanonical bytes ARE the wire form; serializer is memcpy
disk loadmmap() + cast — donebytes are already canonical (trusted source)
fingerprintcanonical bytes ⇒ schema fingerprint ⇒ Merkle ⇒ content-addressableschema identity is a pure function of canonical structure
ordering (a < b)lexicographic byte compare, no per-field dispatchextends 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.

TypeWireSafe?
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
TypeWireSafe?Why
*T, ?*T, &Tprocess-local addresses (E2301 — hard reject)
usize, isizetarget-width-dependent
slices []Truntime objects, dynamic length
String, Vec[T], HashMap[K,V]process-local heap ownership
capabilities, mutexes, file handlesprocess-local identity / OS resources
closures, trait objectsprocess-local code/vtable addresses
f32 / f64❌ by defaultNaN payloads violate canonical form — see below
@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 instead

Pointers are inherently target-specific (address width, CHERI provenance). Use offsets, indices, UUIDs, or handles — all of which are WireSafe integers.

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)

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.

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:

OperationNormalizes?Why
FP arithmetic (+, *, sqrt, …)NOhardware produces whatever NaN the ISA specifies; we don’t slow computation
Store to a @layout(wire) fieldYEScrossing into a wire representation
Construct a @layout(wire) valueYESconstruction is a wire-boundary crossing
Serialize wire struct → bytesNO (already canonical)serialization is memcpy
Deserialize from untrusted sourceYESattacker may supply arbitrary NaN payloads
Deserialize from trusted Janus sourceNOproducer 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.


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.


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.


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)` and
will be removed in Janus 2.0.

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.

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.

BeforeAfter
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).


This design rests on four principles that sit alongside ownership, effects, capabilities, and profiles as core pillars of Janus.

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.

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.


  • 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@repr and the be*/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.