Diagnostics

This section specifies the format and content of error messages. Clear, actionable diagnostics are essential for a human-authored format.

Diagnostic format

Styx implementations SHOULD emit diagnostics in the following format:

level: message
  --> file:line:column
   |
NN | source line
   | ^^^ annotation
   |
   = note: additional context
   = help: suggested fix

diag[diagnostic.format] A diagnostic SHOULD include:

  • Level: error, warning, or note
  • Message: A concise description of the problem
  • Location: File path, line number, and column
  • Source context: The relevant source line(s) with underline annotations
  • Help: When applicable, a concrete suggestion for fixing the problem

Secondary locations (e.g., "first defined here") use ------ underlines. Primary locations (the actual error site) use ^^^^^ underlines.

diag[diagnostic.actionable] Error messages SHOULD be actionable. When a fix is known, the diagnostic SHOULD show the corrected code, not just describe the problem.

Note (non-normative): For schema validation errors, diagnostics can be improved by including a note pointing to the schema declaration that applied the schema.

error: missing required field 'host'
  --> config.styx:1:1
  |
1 | server { ... }
  | ^^^^^^ missing 'host'
  |
  = note: schema validation failed
  --> config.styx:1:1
  |
1 | @ "./server.schema.styx"
  | ^^^^^^^^^^^^^^^^^^^^^^^^ schema applied here

Parser errors

Unexpected token

diag[diagnostic.parser.unexpected] When the parser encounters an unexpected token, the message SHOULD identify what was found and what was expected.

error: unexpected token
  --> config.styx:3:5
  |
3 |     = value
  |     ^ expected key or '}'

Unclosed delimiter

diag[diagnostic.parser.unclosed] When a delimiter is not closed, the message SHOULD show where the opening delimiter was and where the parser expected the closing delimiter.

error: unclosed '{'
  --> config.styx:1:8
  |
1 | server {
  |        ^ unclosed delimiter
  |
...
  |
5 | database {
  | -------- this '{' might be the problem (missing '}' before it?)

Invalid escape sequence

diag[diagnostic.parser.escape] When a quoted scalar contains an invalid escape sequence, the message SHOULD identify the specific invalid escape.

error: invalid escape sequence '\q'
  --> config.styx:2:12
  |
2 |   name "foo\qbar"
  |            ^^ invalid escape
  |
  = help: valid escapes are: \\, \", \n, \r, \t, \uXXXX, \u{X...}

Unterminated string

diag[diagnostic.parser.unterminated-string] When a quoted scalar is not terminated, the message SHOULD show where the string started.

error: unterminated string
  --> config.styx:2:8
  |
2 |   name "hello
  |        ^ string starts here
3 |   port 8080
  |
  = help: add closing '"' or use a heredoc for multiline strings

Unterminated heredoc

diag[diagnostic.parser.unterminated-heredoc] When a heredoc is not terminated, the message SHOULD show the expected delimiter and where the heredoc started.

error: unterminated heredoc, expected 'EOF'
  --> config.styx:2:10
  |
2 |   script <<EOF
  |          ^^^^^ heredoc starts here
  |
  = note: reached end of file while looking for 'EOF'
  = help: the closing delimiter must appear on its own line

Heredoc delimiter too long

diag[diagnostic.parser.heredoc-delimiter-length] When a heredoc delimiter exceeds 16 characters, the message SHOULD state the limit.

error: heredoc delimiter too long
  --> config.styx:2:10
  |
2 |   script <<THIS_DELIMITER_IS_WAY_TOO_LONG
  |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 35 characters
  |
  = help: delimiter must be at most 16 characters

Heredoc indentation error

diag[diagnostic.parser.heredoc-indent] When a heredoc content line is less indented than the closing delimiter, the message SHOULD show both locations.

error: heredoc line less indented than closing delimiter
  --> config.styx:4:1
  |
3 |     script <<BASH
4 | echo "hello"
  | ^^^^ this line has no indentation
5 |     BASH
  |     ---- closing delimiter is indented 4 spaces
  |
  = help: indent content to at least column 5, or dedent the closing delimiter

Comment without preceding whitespace

diag[diagnostic.parser.comment-whitespace] When // appears without preceding whitespace (making it part of a scalar), the parser cannot distinguish user intent. If subsequent parsing fails, the message SHOULD note the potential comment issue.

error: unexpected token 'comment'
  --> config.styx:2:13
  |
2 |   url foo// comment
  |             ^^^^^^^ unexpected token
  |
  = note: '//' without preceding space is part of the scalar 'foo//'
  = help: add a space before '//' to start a comment

Duplicate key

diag[diagnostic.parser.duplicate-key] When a key appears twice in the same object, the message SHOULD show both locations.

error: duplicate key 'port'
  --> config.styx:4:3
  |
2 |   port 8080
  |   ---- first defined here
  |
4 |   port 9090
  |   ^^^^ duplicate key

Mixed separators

diag[diagnostic.parser.mixed-separators] When an object mixes comma and newline separators, the message SHOULD identify both styles and suggest picking one.

error: mixed separators in object
  --> config.styx:2:7
  |
1 | {
2 |   a 1,
  |      ^ comma here
3 |   b 2
  |
  = help: use either commas or newlines, not both:
  |
  | {a 1, b 2}          // comma-separated
  |
  | {                   // newline-separated
  |   a 1
  |   b 2
  | }

Comma in sequence

diag[diagnostic.parser.sequence-comma] When a comma appears in a sequence, the message SHOULD explain that sequences use whitespace separation.

error: unexpected ',' in sequence
  --> config.styx:1:3
  |
1 | (a, b, c)
  |   ^ commas not allowed in sequences
  |
  = help: use whitespace to separate elements: (a b c)

Attribute object in sequence

diag[diagnostic.parser.attr-in-sequence] When an attribute object appears as a direct sequence element, the message SHOULD explain the ambiguity and suggest block form.

error: attribute object not allowed as sequence element
  --> config.styx:2:3
  |
2 |   a>1 b>2
  |   ^^^^^^^ attribute object
  |
  = note: ambiguous whether this is one object {a:1, b:2} or two {a:1} {b:2}
  = help: use block form: {a 1, b 2}

Trailing content after root

diag[diagnostic.parser.trailing-content] When content appears after a closed root object, the message SHOULD note that explicit root objects cannot have siblings.

error: unexpected token after root object
  --> config.styx:4:1
  |
1 | {
  | - root object starts here
2 |   key value
3 | }
  | - root object ends here
4 | extra
  | ^^^^^ unexpected token
  |
  = help: remove the '{ }' to allow multiple top-level entries

Too many atoms in entry

diag[diagnostic.parser.toomany] When an entry contains more than two atoms, the message SHOULD identify the unexpected atom and suggest common fixes like removing whitespace between a tag and its payload.

error: unexpected atom after value
  --> config.styx:1:9
  |
1 | key @tag { }
  |         ^ unexpected third atom
  |
  = help: did you mean `@tag{}`? whitespace is not allowed between a tag and its payload

Reopened path

diag[diagnostic.parser.reopened-path] When a path is reopened after being closed by a sibling, the message SHOULD show where the sibling closed the path.

error: cannot reopen path `foo.bar`
  --> config.styx:3:1
  |
2 | foo.baz 1
  | ------- path was closed when sibling appeared
3 | foo.bar.x 2
  | ^^^^^^^^^^^ cannot reopen path

Nesting into terminal value

diag[diagnostic.parser.nest-into-terminal] When attempting to add children to a path that already has a terminal value (scalar, sequence, tag, or unit), the message SHOULD show the terminal value.

error: cannot nest into `foo`
  --> config.styx:2:1
  |
1 | foo 1
  | --- path has a terminal value
2 | foo.bar 2
  | ^^^^^^^ cannot nest into `foo`

Missing whitespace before block

diag[diagnostic.parser.missing-whitespace] When a bare key is followed immediately by { or ( without whitespace, the message SHOULD suggest adding whitespace to distinguish from tag syntax.

error: missing whitespace before block
  --> config.styx:1:6
  |
1 | config{}
  |       ^ add whitespace before this
  |
  = help: bare keys must be separated from `{` or `(` by whitespace (to distinguish from tags like `@tag{}`)

Deserializer errors

Invalid value for type

diag[diagnostic.deser.invalid-value] When a scalar cannot be interpreted as the target type, the message SHOULD identify the expected type, explain what's wrong, and provide helpful guidance. This covers all scalar parsing failures: type mismatches, overflow, invalid formats, etc.

error: type mismatch
  --> config.styx:2:8
  |
2 |   port "eight thousand"
  |        ^^^^^^^^^^^^^^^^ expected integer, found string
  |
  = help: use a numeric value: port 8080
error: integer out of range
  --> config.styx:2:8
  |
2 |   port 99999999999999999999
  |        ^^^^^^^^^^^^^^^^^^^^ value exceeds u16 maximum (65535)
error: invalid duration
  --> config.styx:2:11
  |
2 |   timeout 30
  |           ^^ expected duration with unit
  |
  = help: valid formats: 30s, 10ms, 2h, 500us
  = help: valid units: ns, us, µs, ms, s, m, h, d
error: invalid timestamp
  --> config.styx:2:12
  |
2 |   created 2026-13-01T00:00:00Z
  |                ^^ month must be 01-12
  |
  = help: expected RFC 3339 format: YYYY-MM-DDTHH:MM:SSZ
error: invalid boolean
  --> config.styx:2:11
  |
2 |   enabled yes
  |           ^^^ expected 'true' or 'false'

Enum not a tagged value

diag[diagnostic.deser.enum-invalid] When deserializing an enum and the value is not a valid tagged value, the message SHOULD explain enum representation.

error: expected enum variant (tagged value)
  --> config.styx:2:10
  |
2 |   status "ok"
  |          ^^^^ expected tag like @ok, found scalar
  |
  = help: enum values use tag syntax:
  |
  | status @ok              // unit variant
  | status @err{msg "x"}    // variant with payload

Unknown enum variant

diag[diagnostic.deser.unknown-variant] When an enum variant name doesn't match any defined variant, the message SHOULD list the valid variants.

error: unknown variant 'unknown'
  --> config.styx:2:10
  |
2 |   status @unknown
  |          ^^^^^^^^ not a valid variant
  |
  = help: valid variants are: ok, pending, err

Missing required field

diag[diagnostic.deser.missing-field] When a required field is missing during deserialization, the message SHOULD identify the field and the containing object.

error: missing required field 'port'
  --> config.styx:1:1
  |
1 | server {
  | ^^^^^^ in this object
2 |   host localhost
3 | }
  |
  = help: add the required field: port 8080

Unknown field

diag[diagnostic.deser.unknown-field] When a field is present but not expected by the target type, the message SHOULD suggest similar field names if available.

error: unknown field 'prot'
  --> config.styx:3:3
  |
3 |   prot 8080
  |   ^^^^ unknown field
  |
  = help: did you mean 'port'?
  = note: expected fields: host, port, timeout

Expected object, found scalar

diag[diagnostic.deser.expected-object] When an object is expected but a scalar is found.

error: expected object, found scalar
  --> config.styx:2:10
  |
2 |   server localhost
  |          ^^^^^^^^^ expected object
  |
  = help: use braces for object: server {host localhost}

Expected sequence, found scalar

diag[diagnostic.deser.expected-sequence] When a sequence is expected but a scalar is found.

error: expected sequence, found scalar
  --> config.styx:2:9
  |
2 |   hosts localhost
  |         ^^^^^^^^^ expected sequence
  |
  = help: use parentheses for sequence: hosts (localhost)