Table of Contents

Complete Portable grammar

This page is a precise syntax reference. You do not need to understand EBNF to follow the tutorial or use ordinary layouts.

EBNF (Extended Backus–Naur Form) is a compact way to describe syntax:

  • quoted text is written literally;
  • { x } means zero or more repetitions;
  • [ x ] means optional; and
  • | separates alternatives.

A layout must match through end-of-input; unsupported trailing text is not ignored. The source-text rules explain names, comments, and numbers. Other language pages explain combinations that are syntactically recognizable but invalid, such as an array bitfield or unsupported enum backing type.

Source and expression EBNF

definition       = trivia, { declaration | preprocessor-line }, end-of-input ;
declaration      = struct-declaration
                 | union-declaration
                 | anonymous-composite-declaration
                 | forward-declaration
                 | typedef-struct-declaration
                 | typedef-union-declaration
                 | typedef-tag-alias
                 | typedef-enum-declaration
                 | typedef-declaration
                 | enum-declaration ;

struct-declaration
                 = "struct", identifier, [ alignment-override ], "{", { struct-field }, "}", [ identifier ], [ ";" ] ;
union-declaration
                 = "union", identifier, [ alignment-override ], "{", { union-field }, "}", [ identifier ], [ ";" ] ;
anonymous-composite-declaration
                 = ( "struct" | "union" ), [ alignment-override ], "{", { struct-field }, "}", identifier, ";" ;
forward-declaration
                 = ( "struct" | "union" ), identifier, ";" ;
typedef-struct-declaration
                 = "typedef", "struct", [ identifier ], [ alignment-override ], "{", { struct-field }, "}", typedef-aliases ;
typedef-union-declaration
                 = "typedef", "union", [ identifier ], [ alignment-override ], "{", { union-field }, "}", typedef-aliases ;
typedef-aliases  = ";" | typedef-alias, { ",", typedef-alias }, ";" ;
typedef-alias    = pointer-stars, identifier ;
typedef-tag-alias
                 = "typedef", ( "struct" | "union" | "enum" ), identifier, identifier, ";" ;
typedef-enum-declaration
                 = "typedef", ( "enum" | "flag" ), [ identifier ], [ enum-storage ], "{", [ enum-values ], [ "," ], "}",
                   typedef-aliases ;
typedef-declaration
                 = "typedef", type-name, typedef-declarator, { ",", typedef-declarator }, ";" ;
typedef-declarator
                 = pointer-stars, identifier, { "[", expression, "]" } ;
enum-declaration = ( "enum" | "flag" ), [ identifier ], [ enum-storage ],
                   "{", [ enum-values ], [ "," ], "}", ";" ;
enum-storage     = ":", identifier ;
enum-values      = enum-value, { [ "," ], enum-value } ;
enum-value       = enum-member-name, [ "=", expression ] ;
enum-member-name = identifier | decimal-digit, { identifier-continue } ;
preprocessor-line
                 = define-declaration | constant-definition | undef-line | include-line | pragma-line
                 | conditional-line ;
define-declaration
                 = "#define", identifier, expression ;
constant-definition
                 = "#define", identifier, [ quoted-literal | "b", quoted-literal | macro-parameters, rest-of-line
                                          | rest-of-line ] ;
macro-parameters = "(", { non-line-end-character }, ")" ;
undef-line       = "#undef", identifier ;
include-line     = "#include", ( "<", { non-line-end-character }, ">" | '"', { non-line-end-character }, '"' ) ;
pragma-line      = "#pragma", ( "pack", "(", [ "push", [ ",", expression ] | "pop" | expression ], ")" | rest-of-line ) ;
conditional-line = ( "#ifdef" | "#ifndef" ), identifier | "#else" | "#endif" ;
quoted-literal   = '"', { character | escape }, '"' | "'", { character | escape }, "'" ;

struct-field     = field | inline-struct-field | conditional-field | switch-field ;
field-block      = "{", { struct-field }, "}" ;
conditional-field = "if", "(", expression, ")", field-block, [ "else", field-block ] ;
switch-field     = "switch", "(", expression, ")", "{", { switch-case }, [ "default", ":", field-block ], "}" ;
switch-case      = "case", expression, ":", field-block ;
inline-struct-field
                 = "struct", [ identifier ], [ alignment-override ], "{", { struct-field }, "}", [ declarator-list ], ";"
                 | "union", [ identifier ], [ alignment-override ], "{", { union-field }, "}", [ declarator-list ], ";" ;
declarator-list  = declarator, { ",", declarator } ;
union-field      = field | inline-struct-field ;
field            = { type-qualifier }, [ tag-keyword ], type-name, declarator, { ",", declarator }, ";" ;
declarator       = named-declarator | anonymous-bitfield ;
named-declarator = { type-qualifier }, pointer-stars, { type-qualifier }, identifier, [ array ], [ bit-width ],
                   [ placement-suffix ]
                 | "(", "*", identifier, ")", "(", { non-line-end-character }, ")" ;
anonymous-bitfield
                 = bit-width, [ placement-suffix ] ;
type-qualifier   = "const" | "volatile" | "restrict" ;
tag-keyword      = "struct" | "union" | "enum" ;
pointer-stars    = { "*" } ;
array            = { "[", [ expression | "EOF" ], "]" } ;
bit-width        = ":", expression ;
placement-suffix = alignment-override | offset-assertion ;
alignment-override
                 = "@align", "(", expression, ")" ;
offset-assertion = "@", expression ;

expression       = logical-or, [ "?", expression, ":", expression ] ;
logical-or       = logical-and, { "||", logical-and } ;
logical-and      = bitwise-or, { "&&", bitwise-or } ;
bitwise-or       = bitwise-xor, { "|", bitwise-xor } ;
bitwise-xor      = bitwise-and, { "^", bitwise-and } ;
bitwise-and      = equality, { "&", equality } ;
equality         = relational, { ( "==" | "!=" ), relational } ;
relational       = shift, { ( "<" | "<=" | ">" | ">=" ), shift } ;
shift            = additive, { ( "<<" | ">>" ), additive } ;
additive         = multiplicative, { ( "+" | "-" ), multiplicative } ;
multiplicative   = unary, { ( "*" | "/" | "%" ), unary } ;
unary            = { "-" | "~" | "!" }, primary ;
primary          = literal | qualified-name | size-call | "(", expression, ")" ;
qualified-name   = identifier, { ".", identifier } ;
size-call        = "sizeof", "(", type-spelling, ")" | "offsetof", "(", type-spelling, ",", identifier, ")" ;
type-spelling    = identifier, { identifier }, pointer-stars ;
literal          = sign, ( decimal | hexadecimal | binary | octal ), [ integer-suffix ] ;
integer-suffix   = { "u" | "U" | "l" | "L" } ;
sign             = [ "+" | "-" ] ;
decimal          = decimal-digits ;
hexadecimal      = ( "0x" | "0X" ), hex-digits ;
binary           = ( "0b" | "0B" ), binary-digits ;
octal            = ( "0o" | "0O" ), octal-digits ;
decimal-digits   = decimal-part, { decimal-part } ;
hex-digits       = hex-part, { hex-part } ;
binary-digits    = binary-part, { binary-part } ;
octal-digits     = octal-part, { octal-part } ;
decimal-part     = decimal-digit | "_" ;
hex-part         = hex-digit | "_" ;
binary-part      = binary-digit | "_" ;
octal-part       = octal-digit | "_" ;

type-name        = identifier | endian-primitive ;
endian-primitive = identifier, ( "<" | ">" ) ;
identifier       = identifier-start, { identifier-continue } ;
identifier-start = unicode-letter | "_" ;
identifier-continue
                 = unicode-letter | decimal-digit | "_" ;
trivia           = { whitespace | line-comment | block-comment | line-continuation } ;
line-continuation
                 = "\\", line-end ;
whitespace       = whitespace-character, { whitespace-character } ;
line-comment     = "//", { non-line-end-character }, [ line-end ] ;
block-comment    = "/*", { block-comment-character }, "*/" ;
line-end         = "\r\n" | "\r" | "\n" ;
end-of-input     = ? no remaining character ? ;

Each numeric digit sequence must contain at least one real digit; underscores alone are invalid. decimal-digit is 0–9, binary-digit is 0 or 1, octal-digit is 0–7, and hex-digit is 0–9, a–f, or A–F. unicode-letter and identifier continuation use .NET Unicode letter/letter-or-digit classification. Block comments do not nest. The parser accepts pointer stars adjacent to either token (uint8* p, uint8 *p, and uint8 * p) and normalizes the total star count.

An inline-struct-field's trailing identifier is optional: when omitted, this is an anonymous promoted member - its own fields splice directly into the containing struct's addressable path/mapped-class/JSON namespace (root.x, not root.<name>.x) instead of nesting under a name of their own. The empty declarator name reuses the same sentinel anonymous-bitfield already established for a nameless field. Promotion is transitive (an anonymous member's own anonymous members promote further) and a name collision anywhere in the flattened, transitively-promoted namespace is a construction-time error. Placement, size, and alignment are unaffected - this changes only which path/mapped-class/JSON name resolves to a field. Promotion is supported for inline structs, not inline unions. A field referencing a separately declared named union is supported. See Structs, unions, enums, and typedefs.

A declarator accepts zero or more bracketed dimensions, written outermost first (value[rows][columns]). Every dimension of a multidimensional declaration must currently be compile-time fixed, with no named expression dependencies. Literal arithmetic such as [2 + 1][4] is allowed; a named #define count is not. A one-dimensional array may use a runtime expression referencing an earlier field, caller variable, or definition. This is a Portable restriction, not a claim about C variable-length arrays. Empty [] has meaning only for a supported character type and is then a terminated string; it is accepted only as the sole dimension of a one-dimensional declarator (char name[10][] is rejected - an unsized dimension can never be an inner dimension of a multidimensional array). A fixed table of fixed-width strings (char names[10][32]) is ordinary within this rule: the innermost dimension behaves exactly like today's char[32] fixed buffer, and every outer dimension nests around it. See Arrays and strings. A field declaration may share one type across multiple comma-separated declarators (uint8 first, second;); each declarator has its own independent pointer stars, array, and bit width, matching C's declarator-list semantics - a leading star belongs only to the declarator it directly precedes, not to every name in the list, so uint8 *a, b; declares a as a pointer and b as a plain uint8. const, volatile, and restrict are recognized before the type or immediately after a pointer star (const uint8 value;, uint8 * const p;) and discarded with no effect on the compiled field; this is a fixed, closed set - other tokens in that position, or a qualifier in any other position, are still rejected. A field's type reference may optionally be written with a leading struct, union, or enum keyword (struct child value;), matching how C itself refers to a tagged type; the keyword is checked against the referenced declaration's actual kind at construction time and rejected on a mismatch, but otherwise has no effect - struct child value; and child value; compile to the identical field. A declarator with a bit width and no name at all (anonymous-bitfield) reserves storage as pure padding - its bits are consumed from the shared storage unit but it never becomes an addressable path, mapped-class member, or JSON field (uint8 flag:1, :3, other:4;). This applies to any declarator after the first without ambiguity, since its type is already fixed by the field's shared type-name. The first declarator is a special case: when exactly one word appears before it and a bit width follows, that one word is the whole type-name and the first declarator itself is the anonymous one (uint8 :3;); a run of two or more words before a bit width still splits normally into type-name plus a named first declarator, even though a bit width follows (uint8 flag:1;), since only a single-word run has no name token to spare. This means a multi-word anonymous type is not supported directly - it falls back to naming the field after its last word instead (unsigned int :3; declares a field named int of type unsigned, not an anonymous unsigned int). See bitfields. A declarator may carry at most one trailing placement suffix - either @align(N), overriding that one declarator's own natural alignment, or bare @N, asserting the declarator's expected byte offset without ever changing it. Both accept a full expression, evaluated the same way bit-width is, so a #defined constant works for either. @align(N)'s N must be a positive power of two; it only has an observable effect when the enclosing layout is constructed with aligned: true - in packed mode it is accepted but has no effect, the same as every field's own natural alignment already having none there. @N's value must be non-negative and is checked against the declarator's actual computed offset only when that offset is statically known at construction time; if not statically known, it is instead checked the first time any operation actually reaches the field. It is not supported on a bitfield declarator. See Layout, alignment, and padding. A struct/union declaration may itself carry [ alignment-override ] immediately before its opening brace, clamping every one of that composite's own fields' alignment to at most N (matching #pragma pack(N) semantics) unless a field carries its own explicit @align(N), which always wins outright instead of being further clamped; N=1 therefore has the effect of "packed" for that one composite. Like the field-level form, it only has an observable effect when the enclosing layout is constructed with aligned: true. See Layout, alignment, and padding. A #define is one object-like integer expression; there are no parameters or textual expansion. Function-call spelling is recognized only so construction can reject it explicitly. It is not a supported primary, and it never executes user code.

Whitespace between a directive and its name includes .NET whitespace characters such as a non-breaking space. Carriage return and line feed end the directive: a name cannot start on the next physical line.

Public path EBNF

path             = segment, { ".", segment } ;
segment          = identifier, { indexer } ;
indexer          = "[", canonical-decimal-index, "]" ;
canonical-decimal-index
                 = "0" | nonzero-decimal-digit, { decimal-digit } ;
pointer-accessor = ".address" | ".value" ;

pointer-accessor describes the special meaning of an ordinary path segment after a pointer: .address selects pointer storage and .value consumes one pointer level. It is not a separate lexical token. A non-pointer field may therefore still be named address or value. Indices have no sign, whitespace, leading zero, base prefix, or underscore, and must fit a non-negative 32-bit integer. A segment mirrors its field's own declaration syntax: an N-dimensional array accepts up to N repeated indexers in one segment (root.matrix[2][3], not comma-separated), one per dimension, outermost first. Supplying fewer than N selects the corresponding lower-dimensional sub-array rather than one scalar/struct element; supplying more than N is rejected. See paths and selection.

Production index

The table explains each production and links to the page that defines its additional meaning/range rules.

Production Meaning and detailed rules
definition Complete standalone input; Portable rules
declaration One exported declaration kind
struct-declaration Named sequential composite, optionally followed by an ignored object name; declarations
union-declaration Named overlapping composite, optionally followed by an ignored object name; declarations
anonymous-composite-declaration A body whose trailing name is the declared type (struct { ... } timeval;); declarations
forward-declaration struct node; - accepted and declares nothing; declarations
typedef-struct-declaration Named-tag or anonymous struct body with one or more aliases; the tag is declared too; typedefs
typedef-union-declaration Named-tag or anonymous union body with one or more aliases; typedefs
typedef-aliases ; alone (tag only) or a comma-separated alias list
typedef-alias One alias with its own pointer depth (*PX)
typedef-tag-alias typedef struct tag alias; (or union/enum) - an alias of a declared tag, kind-checked
typedef-enum-declaration Tagged or anonymous enum/flag body with aliases, mirroring the struct forms; typedefs
typedef-declaration One type spelling with one or more declarators
typedef-declarator Alias name with optional pointer depth and fixed array dimensions (typedef T name[N];)
enum-declaration Named integral enum or flag (bitmask enum); an unnamed one declares constants
enum-storage Optional explicit integral backing (any accepted integer spelling or a typedef of one)
enum-values Member sequence; the comma is optional because a value can never be followed by a name
enum-value Member name plus optional bounded expression
enum-member-name An identifier, or a name that starts with (or consists of) digits (32BIT_MACHINE, 0)
preprocessor-line One # line; see source text
define-declaration Object-like integer expression binding
constant-definition A text, byte, bare, function-like, or otherwise non-expression #define published as a constant
macro-parameters The parameter list glued to a function-like macro name
undef-line Removes a constant for the rest of the source
include-line Recorded path, never resolved
pragma-line pack maintains the alignment clamp stack; other pragmas are ignored
conditional-line #ifdef/#ifndef/#else/#endif over defined names
quoted-literal A "- or '-delimited literal with C escapes (\n, \r, \t, \0, \xHH, \")
struct-field Ordinary, named-inline-struct, or anonymous-promoted-struct member
inline-struct-field Inline struct or union member; anonymous (promoted) without a declarator; a tag makes the body a global type as well; declarations
declarator-list The member declarators of a tagged inline body (} gen, *pgen;)
union-field Ordinary field or an inline composite; conditionals are not accepted in a union
field One optionally qualified, optionally tagged type, one or more comma-separated declarators
declarator A named declarator, a _ padding field, or an anonymous nonzero-width bitfield; padding fields
named-declarator One name with its own optional qualifiers, pointer stars, optional array, optional bit width, and optional placement suffix; or a function-pointer declarator, stored as an opaque pointer
anonymous-bitfield A nameless bit-width-only declarator used as pure padding
type-qualifier A recognized layout-neutral qualifier, discarded with no effect on the compiled field
tag-keyword An optional struct/union/enum keyword, checked against the referenced declaration's actual kind
pointer-stars Zero or more data-pointer levels
array Zero or more fixed/runtime dimension counts or character-string markers, outermost first
bit-width One named nonzero portable bit slice, or unnamed reserved padding for anonymous-bitfield
placement-suffix At most one trailing alignment override or offset assertion per declarator
alignment-override An explicit per-declarator alignment override, effective only when aligned: true
offset-assertion An explicit per-declarator byte-offset assertion, checked when statically computable
expression Complete checked integer expression, optionally a conditional c ? a : b
bitwise-or Lowest-precedence bitwise OR
bitwise-xor Bitwise XOR, between & and \| as in C
qualified-name A variable, field, define, Enum.Member constant, or a nested field through its struct field (hdr.n, a.b.n)
size-call sizeof(type) / offsetof(type, field), folded to a literal at construction
type-spelling A primitive/typedef/enum/composite spelling with optional pointer stars
bitwise-and Bitwise AND
shift Checked left/right shift
additive Checked addition/subtraction
multiplicative Checked multiplication, division, and remainder
unary Negation and bitwise complement
primary Literal, variable/name, or parenthesized expression
literal Optional sign plus one radix-specific integer, plus an optional discarded C-style suffix
integer-suffix Zero or more u/U/l/L characters, recognized and discarded with no effect on the value
sign Literal-leading plus/minus
decimal Base-10 digit sequence
hexadecimal 0x/0X base-16 digit sequence
binary 0b/0B base-2 digit sequence
octal 0o/0O base-8 digit sequence
decimal-digits Decimal digits with optional visual underscores
hex-digits Hexadecimal digits with optional visual underscores
binary-digits Binary digits with optional visual underscores
octal-digits Octal digits with optional visual underscores
decimal-part One decimal digit or underscore
hex-part One hexadecimal digit or underscore
binary-part One binary digit or underscore
octal-part One octal digit or underscore
type-name Declared/aliased name or primitive with byte-order suffix
endian-primitive Primitive name followed by < or >
identifier Case-sensitive Unicode identifier
identifier-start Unicode letter or underscore
identifier-continue Unicode letter, digit, or underscore
line-continuation A backslash immediately before a line end joins the lines
trivia Ignorable whitespace and comments between tokens
whitespace One or more .NET whitespace characters
line-comment // through a line ending or input end
block-comment Non-nesting /* ... */ comment
line-end CRLF, CR, or LF
end-of-input Requires the parser to consume the complete input
path Dot-separated public selector
field-block Braced sequence of fields, including nested conditional groups
conditional-field Runtime if/else group with lazy inactive branches
switch-field Discriminator-selected cases and optional fallback
switch-case Compile-time constant label and a braced field block
logical-or Short-circuit logical OR
logical-and Short-circuit logical AND
equality Integer equality and inequality comparisons
relational Ordered integer comparisons
segment Named path component with zero or more indices, one per dimension actually indexed
indexer Normalized decimal array index
canonical-decimal-index Formal production name for 0 or an unpadded positive decimal integer
pointer-accessor .address/.value selection after a pointer

Invalid combinations—unknown types, duplicate names, recursive by-value storage, bad enum backing, oversized expressions, unsupported bitfield storage, and unsized non-character arrays—fail layout construction with CStructErrorCode.InvalidLayout. Syntax recognized only for a focused error does not expand the supported grammar.

Conditional field groups

For a step-by-step explanation with array items, calculations, scope exercises, and browser lessons, start with Choose fields with if and switch.

A struct body accepts if (expression) { fields } with an optional else { fields }, and switch (expression) { case expression: { fields } ... default: { fields } }. Each case requires braces and has no fall-through. The default is optional; a switch without a matching case/default contributes no fields. Cases and branches retain the enclosing field namespace: give alternatives distinct names, preferably named inline structs. Duplicate field names remain errors even in mutually exclusive branches.

Case labels must evaluate to distinct checked integer constants when the layout is compiled. Labels may use #define constants; later caller overrides do not change their values. Equivalent labels such as 1 and 1 + 0 are duplicates, including on empty arms. Runtime fields cannot supply case labels.

struct packet {
    uint8 kind;
    switch (kind) {
        case 1: { struct { uint16 value; } short_record; }
        case 2: { struct { uint32 value; } long_record; }
        default: { uint8 unknown_tag_marker; }
    }
    uint8 trailer;
};

Only active fields consume storage, appear in results/debug data, or resolve as paths. Predicates use earlier decoded fields, definitions and caller variables. In a composite containing conditional fields, local declarations shadow caller values from the start of that composite. A forward or inactive local is unavailable; it cannot reuse a value from an earlier array element. This includes fields exposed through anonymous struct promotion, including transitive promotion. Unavailable active expressions raise CStructLayoutException during reads, writes, and address resolution. Each group uses the variable environment captured when that group is first reached. Nested groups see fields read before their own entry. Named nested members cannot overwrite the enclosing conditional composite's local fields for later predicates; each struct-array element starts a fresh scope. Nested inactive predicates are not evaluated. Put conditional bitfields inside a named inline struct so each group owns its storage units. Ordinary unions retain their existing overlapping semantics.

Serialization chooses branches from supplied values and rejects supplied inactive members, including promoted members of inactive anonymous structs. For roots with reachable conditional types (including aliases and pointer targets), in-place updates validate the original and staged root, rejecting changes to active branch decisions or field byte ranges. This requires the complete root to be readable within the configured budgets. Serialize a new buffer when a change requires a different layout. Unrelated conditional type declarations do not force full-root validation for a selected update whose root has no conditional types.