The Grammar, Annotated

Osmol's grammar fits on one page, and that is not modesty, it is policy. A grammar small enough to hold in your head is also small enough to implement in a weekend and to audit in an afternoon. Two of the deepest guarantees of the language live not in any rule below but in the rules that are missing. This chapter reproduces the full EBNF from spec §5, walks through it production by production, and then looks at the absences.

The full EBNF

program     = { twin } ;
twin        = "twin" ident "{" { decl } "}" ;

decl        = hold | seek | owe | decide | express
            | membrane | attention | bond | roledecl | stake ;

hold        = "hold" fact "=" value [ "±" quantity ] [ "@" number ]
              [ [","] temporal ] ;
seek        = "seek" fact [ "by" instant ] [ "else" escalation ] ;
owe         = "owe" fact "to" party [ "by" instant ] ;
decide      = "decide" fact "among" "{" value { "," value } "}"
              [ "by" instant ] ;
express     = "express" "to" party ":" string ;

membrane    = "membrane" "{" { flowrule } "}" ;
flowrule    = selector "->" party [ "when" condition ] ":" transform ;

attention   = "attention" "{" { budget } "}" ;
budget      = "interrupts" nat "/" period
            | "quiet" timerange
            | "threshold" party number ;

bond        = "bond" party "{" "trust" level "}" ;
roledecl    = ( "assume" | "yield" ) "role" rolename ;
stake       = "stake" number "on" fact "toward" party ;

fact        = ident "(" [ arg { "," arg } ] ")" ;
selector    = fact | ident | "*" ;
party       = ident | "@role(" rolename ")"
            | "family" | "team" | "others" | "all" ;
transform   = "exact" | "coarse(" grain ")" | "category"
            | "existence" | "deny" ;
temporal    = "decays" ( instant | reltime )
            | "until" instant
            | "every" period ;
escalation  = "escalate" | "sync" | "drop" ;
condition   = fact comparator value | ident ;
value       = quantity | instant | place | ident ;
rolename    = ident "(" [ arg { "," arg } ] ")" ;
level       = "low" | "medium" | "high" | number ;

Production by production

program, twin. A program is a sequence of twins; a twin is a braced block of declarations, one per participant. There is nothing above the twin: no module system, no imports, no main. The twin is the compilation unit (Your First Twin), and since declaration order never matters (Axiom II), { decl } is in fact a set written with sequence syntax.

The ten decl kinds. Five speech acts: hold (assertion), seek (gap), owe (commitment), decide (decision), express (human-lane expression). Two sovereignty blocks: membrane (permissions as casts) and attention (the receiver's budget). Three postures: bond (trust), roledecl (assume/yield role, semantic addressing), and stake (priced unsolicited pressure). Note the optionality baked into the speech acts: by, else, ±, @, and the temporal clause are all bracketed, so the minimal forms, hold f(x) = v and seek f(x), stay one short line.

fact, selector, party, transform. A fact is an identifier applied to arguments, for example eta(dinner) or status(atlas.review), and dotted paths in idents give you hierarchies for free. A selector is a fact pattern for membrane rules, with * as the wildcard. A party is a named twin, a flow-time-resolved role, or one of the four audience classes. transform is the entire vocabulary of what a membrane may do (four downward casts and deny), and its shape is discussed under the absences below.

temporal, escalation, condition, value, rolename, level. The small vocabulary of knowledge physics: decays/until/every make validity part of the type; escalate/sync/drop are the three fates of a deadline-passed gap; conditions gate membrane rules on the twin's own state; values are quantities, instants, places, or idents; level grades a bond low/medium/high or numerically.

Grammar-versus-v0.1 gaps. The grammar is normative; the reference interpreter implements a documented subset. roledecl and stake are unimplemented; v0.1 rejects them as unrecognized declarations. when conditions on membrane rules parse but are never evaluated. Numeric levels in bond are not parsed (the interpreter's trust table knows only low/medium/high), and every in temporal is likewise unparsed. Every one of these gaps is listed, with its workaround, in the v0.1 pragmas; an implementation may honor them or consciously supersede them via RFC, but never silently diverge.

The load-bearing absences

Read the grammar again, this time for what is not there.

First, no production contains transmission. The word send appears on no right-hand side, and neither does any synonym, wrapper, or euphemism for it. There is no rule you can reach, no combination of productions you can compose, that yields "deliver this content to that party." The closest the grammar comes is express, which directs content to a party but still moves only by the mesh's settling, and stake, which prices the right to create pressure without a gap. Sending is not forbidden by a checker; it is unwritable.

Second, no production lets a seek, attention, or membrane name a foreign twin as its subject. Look at the rules: a seek has no party position at all, so you cannot seek at someone, only declare your own gap. attention and membrane are bare blocks with no owner argument; they attach to the twin whose braces they sit inside, and there is no syntax to point them elsewhere. The grammar can only speak about the twin it is inside.

As spec §5 puts it: the two deepest security properties of the system are enforced before type checking, by the shape of the syntax tree itself. Spam (O-003), attention theft (O-006), and wanting on someone else's behalf (O-001) are not policy violations that a checker must detect. They are sentences that cannot be parsed. The rest of the impossible sentences are cataloged in What Cannot Be Written.

For implementers

The engineering dissertation (Ch. 2) fixes the recommended front-end shape, and it is worth following even in a weekend port:

Lexing: handwritten and line-oriented. The grammar is line-oriented per spec §2, and a handwritten lexer gives the best diagnostic spans for a language this small; no generator earns its keep here. One deliberate theatrical choice is normative: the tombstone check runs at the lexer, before parsing exists. error[O-000]: there is no send should be the fastest error the toolchain can produce; the old world's verbs die at the first possible instant, before your parser has even seen a brace. (Strip string literals first: prose inside an express is payload, not a verb.)

Parsing: recursive descent, spans everywhere. Recursive descent over the EBNF above, producing a typed AST in which every node carries its source span. Parser combinators are acceptable; v0.1 recommends handwriting it, because the grammar is deliberately tiny and error recovery (parsing past a bad line to report many errors at once, rustc-style) is easiest to tune by hand. Spans on every node are not optional polish: in this language the error messages are the ethics, and ethics deserve line numbers.

Everything after the parser (the checkers, the lowering, the solver) is walked through in Implementing an Interpreter, with the 375-line reference as the map.