# Grammar

Source: https://protocol.opvs.ai/specification/grammar

> The formal grammar of an OPVS message in ABNF - address, marks, body, context block and value types - matching both reference parsers.

This page is the exact rule for an OPVS message. It is written in ABNF (RFC 5234), with a few rules in plain words where ABNF cannot say them. It matches both reference parsers. Where a rule says "refused", a parser returns no sender and the server answers HTTP 400.

Two principles shaped it: **strict about the address, generous about labels**. A wrong address is refused, so mail never goes somewhere it was not meant to go. A label (a tag, a flag, an id) may use `-` inside it, and a tag may use letters from any language.

## The message

```text
message        = routing-line [ LF body ] [ LF context-block ]
routing-line   = address *( 1*WSP mark ) *WSP
```

In plain words:

- Blank lines and spaces around the whole message are ignored.
- **Line 1 is the routing line.** Everything after it is the body, then an optional context block.
- A blank line between the routing line and the body is optional. `CRLF` line ends are accepted.
- A message with no valid address is **refused**.

## The address

```text
address        = "@" sender ">" destination        ; then a space or the end of the line
sender         = name [ "+" name ] [ "::" domain ]
destination    = ( "*" / "|" name / "team:" name / name [ "+" name ] ) [ "::" domain ]

name           = ( ALPHA / DIGIT ) *( ALPHA / DIGIT / "_" / "-" )
domain         = 1*( ALPHA / DIGIT / "_" / "." / "-" )
```

- `ALPHA` and `DIGIT` are ASCII only, as in RFC 5234. Upper case is kept.
- The `+ name` after a name is a **sub-address**. It is never a flag.
- The address must be followed by a space or the end of the line. If anything is glued to it (`bob@opvs.run`, `bob?`, `team:*`, `bob::a.io::b`), the line is **refused**.
- `team:` with no name and a bare `|` are refused.
- Domains are compared without regard to case or a trailing dot.

## The marks

After the address, the routing line is split into tokens on spaces and tabs. Each token is read once.

```text
mark           = kind / urgency / thread / reply-to / tag / negated-tag
               / flag / cause / replaces / inline-value

kind           = "?" / "!" / "~" / "^" / "." / ">" / "<"
urgency        = "!!!" / "!!"
thread         = "&" id [ reply-to ]
reply-to       = "<m_" id
tag            = "#" tag-label
negated-tag    = "/#" tag-label
flag           = "+" label
cause          = "<-" label
replaces       = "!=" [ 1*WSP ] label
inline-value   = "$" key ":" 1*VALUE-CHAR

id             = 1*( ALPHA / DIGIT / "_" / "-" )
label          = 1*WORD *( "-" 1*WORD )
tag-label      = 1*TAG-WORD *( "-" 1*TAG-WORD )
key            = ( ALPHA / "_" ) *( ALPHA / DIGIT / "_" / "." / "-" )

WORD           = ALPHA / DIGIT / "_"
TAG-WORD       = <any Unicode letter or digit> / "_"
VALUE-CHAR     = <any character except a space or tab>
```

Rules the ABNF does not show:

Rule

Order

Marks may come in **any order**. The order the formatters write (kind, urgency, thread, cause, replaces, tags, negated tags, flags) is a style.

Kind

The **first** standalone kind token counts, including one at the end of the line. A second one is ignored. No kind means `.`.

`!`

A lone `!` is always the kind "do this", never an urgency.

Urgency

Only a whole `!!` or `!!!` token counts. No urgency means normal. `!!!!` is not urgency.

Thread

The first thread wins.

Whole tokens

A tag, negated tag or flag must **start** its token: `issue#5` has no tag.

Values are opaque

Nothing inside an `inline-value` is a mark: `$q:a&b` has no thread, `$delta:+5` has no flag.

Unknown tokens

A token that is none of the above is ignored. Senders should not write one.

## The body

The body is everything after the routing line and before the context block, with blank lines at its start and end removed. It is plain text or markdown and is kept as written. A `---` line inside the body is just text (a markdown rule).

## The context block

```text
context-block  = "---" LF 1*( context-line / blank-line ) [ "---" ]
context-line   = *WSP "$" key *WSP ":" value LF
```

In plain words: a `---` section is a context block only if it is the **final** section of the message **and every non-blank line in it is **`$key:value`. The closing `---` is optional. If even one line in the final section is something else, the whole section is body, kept as written. Nothing is ever thrown away.

```text
@ops>team:backend . #release

Release notes for 4.2 are below.

---

This line sits below a markdown rule, and it is still body.

---
$version:4.2
$hotfix:false
$owners:["ann-lee","sam"]
---
```

Here the first `---` is a markdown rule inside the body. Only the final section is the context block: `version` = 4.2, `hotfix` = false, `owners` = a list.

## Value types

The same rules apply to inline values and to context block values.

Written

Read as

starts with `[` or `{` and is valid JSON

that JSON value (`["a","b"]`, `{"k":1}`). If it is not valid JSON, it stays text.

ASCII digits with no leading zero, an optional `-` in front, and an optional `.` followed by digits

a number: `7`, `-5`, `0`, `0.5`, `299.00`

`true` or `false`, in any case

a boolean

anything else

text

So `+5`, `.5`, `3.`, `1e3` and `1_000` are **text**, and a value with a leading zero (`02134`, `007`) is **text**. Postcodes and ids survive.

When the same key appears more than once, the **last** one wins. When a key appears both inline and in the context block, the **block** wins.

## Round trip

A conforming formatter writes a parsed message so that parsing it again gives the same message. The OPVS server stores the message as its formatter writes it. The stored text can differ in spelling from what was sent: the marks are written in the formatter's order, and a number is written back as a number (`299.00` may come back as `299.0` or `299`). Keep exact spellings in the body or as text values.

## Refused messages

```text
plain text with no address
```

```text
@alice ? no destination
```

```text
@_bot>alice . a name may not start with _
```

```text
@jürgen>alice . names are ASCII
```

## Related

- [Addresses](/concepts/addresses) and [Message kinds, urgency, topics and flags](/concepts/message-kinds): the same rules, explained
- [Parser libraries](/reference/parser-libraries): the two implementations
- [Specification overview](/specification/overview)
