Parser

The parser converts Styx source text into a document tree.

Comments

parser[comment.line] 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

parser[comment.doc] 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

parser[scalar.bare.chars] 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 =.

parser[scalar.bare.termination] A bare scalar is terminated by any forbidden character or end of input.

url https://example.com/path

Quoted scalars

parser[scalar.quoted.escapes] Quoted scalars use "..." and support escape sequences: \\, \", \n, \r, \t, \uXXXX, \u{X...}.

parser[scalar.quoted.newline] 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

parser[scalar.raw.syntax] 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

parser[scalar.heredoc.syntax] 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

parser[scalar.heredoc.invalid] 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.

parser[scalar.heredoc.lang] 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

parser[value.unit] The token @ not followed by an identifier is the unit value.

enabled @

Tags

A tag labels a value with an identifier.

parser[tag.syntax] 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).

parser[tag.payload] A tag MAY be immediately followed (no whitespace) by a payload:

Follows @tagResult
{...}tagged object
(...)tagged sequence
"...", r#"..."#, <<HEREDOCtagged 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

parser[sequence.syntax] 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)
)

parser[sequence.elements] Elements may be any atom type.

Objects

Objects are ordered collections of entries.

parser[object.syntax] Objects use { } delimiters. Empty objects {} are valid.

Entries

An entry consists of a key and an optional value.

parser[entry.structure] 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{}

parser[entry.whitespace] 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).

parser[entry.toomany] 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.

parser[entry.keys] 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 (@name or @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

parser[entry.path] 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.

/// styx
// Dotted path
selector.matchLabels app>web
/// styx
// 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 }

parser[entry.path.sibling] 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

parser[entry.path.reopen] 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.

parser[entry.key-equality] 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

parser[object.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.

parser[attr.syntax] Attribute syntax key>value creates an object entry. The > has no spaces around it. Attribute keys MUST be bare scalars.

/// styx
// Shorthand
server host>localhost port>8080
/// styx
// Canonical
server {
  host localhost
  port 8080
}

parser[attr.values] Attribute values may be bare scalars, quoted scalars, sequences, or objects.

config name>app tags>(web prod) opts>{verbose true}

parser[attr.atom] Multiple attributes combine into a single object atom.

/// styx
host>localhost port>8080
/// styx
{host localhost, port 8080}

parser[entry.path.attributes] Dotted paths compose naturally with attribute syntax.

/// styx
// Path with attributes as value
spec.selector.matchLabels app>web tier>frontend
/// styx
// Canonical
spec {
  selector {
    matchLabels {
      app web
      tier frontend
    }
  }
}

Document structure

A Styx document is an object. Top-level entries do not require braces.

parser[document.root] 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.

/// styx
// Implicit root
server {
  host localhost
  port 8080
}
/// styx
// Explicit root
{
  server {
    host localhost
    port 8080
  }
}

Appendix: Minified Styx

Styx can be written on a single line using commas and explicit braces:

styx
{server {host localhost,port 8080},database {url "postgres://..."}}

This is equivalent to:

styx
server {
  host localhost
  port 8080
}

database {
  url "postgres://..."
}

This enables NDStyx (newline-delimited Styx) for streaming:

styx
{event login,user alice,time 2026-01-12T10:00:00Z}
{event logout,user alice,time 2026-01-12T10:30:00Z}