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.
A tag MAY also chain directly into another tag using /@, producing a nested tagged value.
Chaining is right-associative and does NOT change the meaning of ().
@must_emit /@discover_start { executor default }
@must_not_emit /@exec_start
@outer /@inner "payload" These mean:
@must_emit/@discover_start{...}==@must_emit(<single tagged value @discover_start{...}>)@outer/@inner"payload"==@outer(<single tagged value @inner"payload">)
In particular, @a/@b(...) is a nested tagged value, while @a(@b(...)) is a sequence payload containing one element.
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, commas, or both. Duplicate keys are forbidden.
server {
host localhost
port 8080
}
{ a 1 , b 2 , c 3 }
{ a 1 , b 2
c 3 } // mixed separators allowed 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 parser[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 }