Parser
The parser converts Styx source text into a document tree.
Comments
Line comments start with // and extend to the end of the line.
Comments MUST either start at the beginning of the file or be preceded by whitespace.
// comment at start-of-file
host localhost // comment
url https://example.com // the :// is not a comment Doc comments start with /// and attach to the following entry.
Consecutive doc comment lines are concatenated.
A doc comment not followed by an entry (blank line or EOF) is an error.
/// The server configuration.
/// Supports TLS and HTTP/2.
server {
/// Hostname to bind to.
host @string
} Atoms
An atom is the fundamental parsing unit:
- Bare scalar — unquoted text:
localhost,8080,https://example.com - Quoted scalar — quoted text with escapes:
"hello\nworld" - Raw scalar — literal text:
r#"no escapes"# - Heredoc scalar — multi-line literal text:
<<EOF...EOF - Sequence — ordered list:
(a b c) - Object — ordered map:
{key value} - Unit — absence of value:
@ - Tag — labeled value:
@tag,@tag(...),@tag{...}
Scalars
Scalars are opaque text. The parser assigns no type information.
Bare scalars
A bare scalar starts with a character that is NOT:
whitespace, {, }, (, ), ,, ", =, @, or >.
After the first character, @ and = are allowed but > is still forbidden.
This allows URLs with @ (like user@host or crate:pkg@2) and query strings with =.
A bare scalar is terminated by any forbidden character or end of input.
url https://example.com/path Quoted scalars
Quoted scalars use "..." and support escape sequences:
\\, \", \n, \r, \t, \uXXXX, \u{X...}.
The \n escape sequence always produces a single LF character (U+000A), regardless of platform.
Use \r\n explicitly if CRLF is needed.
greeting "hello\nworld"
port "8080" // can deserialize as integer Raw scalars
Raw scalars use r#"..."# syntax. The number of # must match.
Content is literal — escape sequences are not processed.
pattern r#"no need to escape "quotes" or \n"# Heredoc scalars
Heredocs start with <<DELIMITER and end with the delimiter on its own line.
The delimiter MUST match [A-Z][A-Z0-9_]* and not exceed 16 characters.
The closing delimiter line MAY be indented; that indentation is stripped from content lines.
script <<BASH
echo "hello"
BASH A << sequence that is NOT immediately followed by an uppercase letter is a parse error.
This includes << followed by lowercase letters, digits, whitespace, or end of input.
value <<eof // ERROR: delimiter must start with uppercase
value <<123 // ERROR: delimiter must start with uppercase
value << // ERROR: missing delimiter Note: A single < not followed by another < is valid as part of a bare scalar.
A heredoc MAY include a language hint after the delimiter, separated by a comma.
The language hint MUST match [a-z][a-z0-9_.-]* (lowercase identifiers).
The language hint is metadata and does not affect the scalar content.
code <<EOF,rust
fn main () {
println! ( "Hello" );
}
EOF
query <<SQL,sql
SELECT * FROM users
SQL Unit
Tags
A tag labels a value with an identifier.
A tag MUST match the pattern @[A-Za-z_][A-Za-z0-9_-]*.
Note: dots are NOT allowed in tag names (they are path separators in keys).
A tag MAY be immediately followed (no whitespace) by a payload:
Follows @tag | Result |
|---|---|
{...} | tagged object |
(...) | tagged sequence |
"...", r#"..."#, <<HEREDOC | tagged scalar |
@ | tagged unit (explicit) |
| (nothing) | tagged unit (implicit) |
result @err { message "x" } // tagged object
color @rgb ( 255 128 0 ) // tagged sequence
name @nickname "Bob" // tagged scalar
status @ok // tagged unit Bare scalars cannot be tagged — there's no delimiter to separate tag from value.
Sequences
Sequences use ( ) delimiters. Empty sequences () are valid.
Elements are separated by whitespace (spaces, tabs, or newlines).
Commas are NOT allowed.
numbers ( 1 2 3 )
nested (( a b ) ( c d ))
matrix (
( 1 2 3 )
( 4 5 6 )
) Elements may be any atom type.
Objects
Objects are ordered collections of entries.
Objects use { } delimiters. Empty objects {} are valid.
Entries
An entry consists of a key and an optional value.
An entry has exactly one key and at most one value:
- 1 atom: the atom is the key, the value is implicit unit (
@) - 2 atoms: first is key, second is value
enabled // enabled = @
host localhost // host = localhost
type @string // type = @string
config @object {} // config = @object{} A bare scalar key MUST be separated from a following { or ( by whitespace.
This prevents visual confusion with tag syntax (e.g., @tag{...}).
config {} // valid: whitespace before {
items ( 1 2 3 ) // valid: whitespace before ( config {} // ERROR: missing whitespace before {
items ( 1 2 3 ) // ERROR: missing whitespace before ( Note: Quoted scalars, raw scalars, and tags do not have this restriction
since they have clear delimiters. @tag{} is a tagged object (one atom).
An entry with more than two atoms is a parse error.
key @tag {} // ERROR: 3 atoms
a b c // ERROR: 3 atoms A common mistake is putting whitespace between a tag and its payload. The error message SHOULD suggest removing the space:
key @tag {}
Error: unexpected `{` after value
Hint: did you mean `@tag{}`? Whitespace is not allowed between a tag and its payload.A key is a dotted path of one or more segments. Each segment may be:
- A bare key (like bare scalar but
.terminates it) - A quoted scalar
- Unit (
@) - A tag (
@nameor@name"payload")
Objects, sequences, and heredocs are not valid keys.
// Valid keys:
host localhost // bare key
"key with spaces" 42 // quoted key
@ mapped // unit key
@root schema // tagged unit key
@env "PATH" "/usr/bin" // tagged scalar key // Invalid keys:
{ a 1 } value // object as key
( a b ) value // sequence as key
<<EOF // heredoc as key
text
EOF
value A dotted key defines a nested path. Each segment separated by . becomes
a key in a nested object chain. The value is placed at the innermost level.
// Dotted path
selector.matchLabels app > web // Canonical
selector {
matchLabels {
app web
}
} a.b.c value // a { b { c value } }
server.host localhost // server { host localhost }
profile.release.lto true // profile { release { lto true } } Quoted segments do not split on dots:
"a.b" .c value // "a.b" { c value } Sibling dotted paths (paths sharing a common prefix) are allowed as long as they appear contiguously. Moving to a different key at any level closes the previous sibling path and all its descendants.
// Valid: sibling paths under common prefix
foo.bar.x value1
foo.bar.y value2 // foo.bar still open
foo.baz value3 // foo still open, foo.bar now closed Reopening a closed path is an error. A path is closed when a sibling path at the same level receives an entry.
foo.bar {}
foo.baz {} // closes foo.bar
foo.bar.x value // ERROR: foo.bar was closed a.b.c {}
a.b.d {} // closes a.b.c
a.x {} // closes a.b
a.b.e {} // ERROR: a.b was closed This rule enables streaming deserialization: once a different sibling appears, the previous subtree is complete and can be finalized without buffering.
To detect duplicate keys, the parser MUST compare keys by their parsed value:
- Scalar keys compare equal if their contents are exactly equal after parsing (quoted scalars are compared after escape processing).
- Unit keys compare equal to other unit keys.
- Tagged keys compare equal if both tag name and payload are equal.
Separators
Entries are separated by newlines or commas. Duplicate keys are forbidden. An object MUST use exactly one separator mode:
- newline-separated: entries separated by newlines; commas forbidden
- comma-separated: entries separated by commas; newlines forbidden
Comma-separated objects are single-line (except for heredoc content).
server {
host localhost
port 8080
}
{ a 1 , b 2 , c 3 } Attribute syntax
Attribute syntax is shorthand for inline object entries.
Attribute syntax key>value creates an object entry.
The > has no spaces around it.
Attribute keys MUST be bare scalars.
// Shorthand
server host > localhost port > 8080 // Canonical
server {
host localhost
port 8080
} Attribute values may be bare scalars, quoted scalars, sequences, or objects.
config name > app tags > ( web prod ) opts > { verbose true } Multiple attributes combine into a single object atom.
host > localhost port > 8080 { host localhost , port 8080 } Dotted paths compose naturally with attribute syntax.
// Path with attributes as value
spec.selector.matchLabels app > web tier > frontend // Canonical
spec {
selector {
matchLabels {
app web
tier frontend
}
}
} Document structure
A Styx document is an object. Top-level entries do not require braces.
The parser MUST interpret top-level entries as entries of an implicit root object.
Root entries follow the same separator rules as block objects: newlines or commas (see r[object.separators]).
If the document starts with {, it MUST be parsed as a single explicit block object.
// Implicit root
server {
host localhost
port 8080
} // Explicit root
{
server {
host localhost
port 8080
}
} Appendix: Minified Styx
Styx can be written on a single line using commas and explicit braces:
{ server { host localhost , port 8080 }, database { url "postgres://..." }} This is equivalent to:
server {
host localhost
port 8080
}
database {
url "postgres://..."
} This enables NDStyx (newline-delimited Styx) for streaming:
{ event login , user alice , time 2026-01-12T10:00:00Z }
{ event logout , user alice , time 2026-01-12T10:30:00Z }