PXF: Proto eXpressive Format

Concrete syntax. v1.0

PXF is the human-readable text format in the ProtoWire family. The grammar below is written in ISO/IEC 14977 EBNF and matches the canonical reference parser. Whitespace and comments are insignificant between tokens; comments may appear wherever whitespace may appear. The v1.0 freeze of the wire format is documented here and in the IETF draft below.

Document

A PXF document is zero or more directives followed by zero or more field entries. Directives carry document-level metadata or alternative payload shapes — typing, side-channel annotations, tabular rows, embedded schemas. Field entries are the conventional message body. The directive prefix may be empty (a body-only document); the body may be empty (a directive-only document).

document      = { directive } , { field_entry } ;

Map entries (the ':' form) only appear inside a { … } block, where the parser cannot statically tell whether the surrounding field is a message or a map<K, V> — both forms are accepted there and disambiguated by the schema layer.

Directives

v1.0 freezes four directive shapes. @type, @dataset, and @proto each have their own production; everything else is a @<name> named directive that the application's runtime interprets. Directives may appear in any order. An implementation that doesn't recognize an application directive name MAY skip the directive (parsing its block for syntactic well-formedness only) or MAY error.

directive = type_directive
          | dataset_directive
          | proto_directive
          | named_directive ;

@type — body's message type

Pins the document's body to a fully-qualified message type. A decoder that expects a specific type refuses a document whose @type doesn't match.

type_directive = '@type' , identifier ;
@type infra.v1.ServerConfig
hostname = "web-01.prod.example.com"

@<name> & @entry — side-channel directives

Named directives carry document-level data that lives alongside the body. The grammar accepts zero or more identifier prefixes after the name and an optional inline block. The two registrations the spec defines are the conventional one-prefix form (@header pkg.Header { … }) and the spec-registered @entry bundle form (@entry [label] [type] { … } — both prefixes optional).

named_directive = '@' , directive_name , { identifier } , [ block_tail ] ;
// Side-channel header — runtime interprets the name and prefixes:
@header chameleon.v1.LayerHeader { id = "x" }

// Bundle form — @entry with optional label + type:
@entry mylabel pkg.MsgType { x = 1 }

@dataset — bulk rows (the CSV replacement)

Carries many instances of one message type in a single document — the protowire-native CSV. The header lists the row's message type and the columns; each subsequent parenthesised tuple is one row, with cells positionally bound to the columns. An empty cell denotes an absent field; the literal null denotes a present-but-null field.

dataset_directive = '@dataset' , [ identifier ] , '(' , column_list , ')' , { row } ;
column_list       = identifier , { ',' , identifier } ;
row               = '(' , row_cell , { ',' , row_cell } , ')' ;
row_cell          = [ row_value ] ;
@dataset trades.v1.Trade ( symbol, price, qty )
( "AAPL", 188.42, 100 )
( "MSFT", 415.10,  50 )
( "AAPL", 188.55,  75 )

A document containing any @dataset MUST NOT also carry @type or top-level field entries: the @dataset header IS the document's type declaration. Cells are scalar-shaped — lists ([ … ]) and blocks ({ … }) inside cells are rejected. The row message type MAY be omitted when an anonymous @proto precedes the directive (see below).

@proto — embedded schema

Carries the protobuf schema for the document's payload, making PXF self-describing. Four body shapes are lexically distinguished:

proto_directive       = '@proto' , proto_body ;
proto_body            = proto_anonymous_body
                      | proto_named_body
                      | proto_source_body
                      | proto_descriptor_body ;

proto_anonymous_body  = '{' , ?protobuf message body? , '}' ;
proto_named_body      = identifier , '{' , ?protobuf message body? , '}' ;
proto_source_body     = triple_string ;
proto_descriptor_body = bytes ;
ShapeBody interpretation
AnonymousMessage body source; binds to the next typeless directive in document order.
NamedMessage body source; registers the message under the supplied dotted name.
SourceTriple-quoted full .proto file source.
DescriptorBase64-encoded google.protobuf.FileDescriptorSet.
// Named — schema travels with the data:
@proto trades.v1.Trade {
  string symbol = 1;
  double price  = 2;
  int64  qty    = 3;
}
@dataset trades.v1.Trade ( symbol, price, qty )
( "AAPL", 188.42, 100 )

The contents of brace-bounded @proto bodies are protobuf source (or descriptor bytes) and are NOT decoded as PXF — the parser captures the raw bytes between the matching braces and hands them to a downstream protobuf consumer.

Reserved directive names

Thirteen names are reserved by the spec and forbidden as directive_name productions. Four are value keywords that the lexer already routes to their value form (null, true, false); three have their own production (type, dataset, proto); entry is the registered named-directive shape; and six are reserved for future allocation so applications cannot squat names before the spec defines them.

directive_name = identifier - ( 'type'        | 'dataset'    | 'proto'
                              | 'entry'       | 'table'      | 'datasource'
                              | 'view'        | 'procedure'  | 'function'
                              | 'permissions' | 'null'       | 'true'
                              | 'false' ) ;

Entries

An entry inside the body is either a field assignment / block (the message form) or a map entry (the map<K, V> form). The top level of a document permits only field entries; the map form is reserved for nested blocks where the surrounding field's schema disambiguates.

entry         = field_entry | map_entry ;

field_entry   = identifier , ( assignment_tail | block_tail ) ;
map_entry     = map_key , map_tail ;

assignment_tail = '=' , value ;
map_tail        = ':' , value ;
block_tail      = '{' , { entry } , '}' ;

map_key       = identifier | string | integer ;

Values

Values are scalars, lists, or block values. Lists accept comma- or newline-separated elements and may freely mix the two; the comma is consumed if present.

value       = string
            | integer
            | float
            | bool
            | null
            | bytes
            | timestamp
            | duration
            | identifier
            | list
            | block_value ;

list        = '[' , [ value , { [ ',' ] , value } ] , ']' ;
block_value = '{' , { entry } , '}' ;

Identifiers

Identifiers carry enum values, message-type names, and bare keys. They begin with a letter or underscore and may contain dots, which is useful for fully-qualified type names like infra.v1.ServerConfig.

identifier  = ident_start , { ident_part } ;
ident_start = letter | '_' ;
ident_part  = letter | digit | '_' | '.' ;

bool        = 'true' | 'false' ;
null        = 'null' ;

A protobuf schema bound to PXF MUST NOT declare a message field, oneof, or enum value whose name case-sensitively equals null, true, or false: the lexer always routes such names to their value branch, so the declared element would be unreachable from PXF surface syntax. pxf lint surfaces these violations.

Numbers

Decimal integers and IEEE-754 floats. Floats accept either a decimal point with optional exponent, or an exponent alone.

integer  = [ '-' ] , digit , { digit } ;

float    = [ '-' ] , digit , { digit } ,
           ( '.' , { digit } , [ exponent ]
           | exponent ) ;

exponent = ( 'e' | 'E' ) , [ '+' | '-' ] , digit , { digit } ;

Timestamps & durations

The lexer recognizes a four-digit year followed by - as an RFC 3339 timestamp, and a digit run followed by a time unit as a Go-style duration. Negative integers and identifiers that begin with a letter take precedence over those forms.

(* RFC 3339 date-time. e.g. 2024-01-15T10:30:00Z, 2024-01-15T10:30:00.123456789+02:00 *)
timestamp        = ?RFC 3339 date-time? ;

(* Go time.ParseDuration. e.g. 30s, 1h30m, 500ms, 1.5h *)
duration         = duration_segment , { duration_segment } ;
duration_segment = digit , { digit } ,
                   [ '.' , digit , { digit } ] , time_unit ;
time_unit        = 'ns' | 'us' | 'µs' | 'ms' | 's' | 'm' | 'h' ;

Strings

Single-quoted simple strings honor C-style escapes plus 2-digit hex, 3-digit octal, and 4/8-digit Unicode escapes. Triple-quoted strings preserve raw content with no escape interpretation; the leading newline is stripped, and the closing line's indent is removed from each preceding line.

string        = simple_string | triple_string ;

simple_string = '"' , { string_char | escape_seq } , '"' ;
triple_string = '"""' , ?any text not containing """? , '"""' ;

escape_seq    = '\' , ( simple_escape
                          | hex_escape
                          | octal_escape
                          | unicode_4_escape
                          | unicode_8_escape ) ;

Bytes

Byte literals carry standard or raw base64 with optional padding. Backslashes inside b"…" are not interpreted.

bytes       = 'b' , '"' , { base64_char } , '"' ;
base64_char = letter | digit | '+' | '/' | '=' ;

Comments

Three flavors, freely mixed. Block comments do not nest.

comment       = line_comment | block_comment ;
line_comment  = ( '#' | '//' ) , { ?any byte except LF? } ;
block_comment = '/*' , { ?any byte? } , '*/' ;

Full railroad diagram

The full diagram covers every production above and a few lexical helpers (character classes, hex/octal digits). It's tall; scroll inside the frame, or open it in a new tab.

ProtoWire PXF grammar railroad diagram