The grammar of osen

The rules, precisely stated, for a reader who has met the language in the tour and wants to know exactly what is accepted.

Notation

A rule is name = form ;. Quoted characters are literal; | separates alternatives; [ x ] is optional; { x } is zero or more; words name other rules. Whitespace — spaces, tabs and newlines — is allowed between any two tokens and means nothing by itself. Every example is a complete program whose answer follows →.

Values

value  = number | string | name ;
number = int32 | float ;
string = '"' { character | '\"' | '\\' | '\n' | '\t' } '"' ;

A value on its own is a message with no address, which is what the leading colon in the answer shows.

42
# → : 42
"hello"
# → : "hello"

Numbers

An int32 is what C’s strtol reads with base 0: decimal, 0x… hexadecimal, or octal with a leading 0, with an optional sign. If the digits are followed by . the token is a float instead, read by strtof, so .5, 1.5 and 1.5e2 are floats. Digits followed directly by letters are an integer and then a name: 1e3 is 1 applied to e3.

0x10
# → : 16
010
# → : 8
-5
# → : -5
1.5e2
# → : 150.0

Names and binding

name    = ( "/" | letter | "_" ) { address-character } ;
binding = name ":" item ;

A name that begins with / is an OSC address, which is what a name should be; the parser also accepts a bare word. After the first character, any character may follow except whitespace and , : ( ) { } [ ] # * ?. A name on its own reads the value bound to it. A colon binds one item — a value, a bundle, or a list — to a name.

/a : 10, /a
# → : 10
/x : 10, (/x, 2) /o/mul
# → : 20

Binding a name again replaces its value. A binding is an element like any other, so it needs a comma before the next element; a colon with no name before it is allowed and makes an anonymous message.

: 42
# → : 42

Separators

program = [ element { "," element } [ "," ] ] ;

A comma separates the elements of a program. A newline is whitespace: it does not separate anything, so a program written across lines ends each line with a comma, and an expression may be broken across lines. A trailing comma is allowed.

Two elements side by side with no comma between them are not two elements: an element followed by a name or a bracket is a call (below), and an element followed by anything else is an error.

1 2
# → Error parsing message:

Calls

call = element ( name | bundle | deferred ) ;

Arguments come first, then the function: an element followed, without a comma, by a name applies the name to the element. The right-hand side may also be a bracket that produces a function. A number or a string on the right is an error, and a bracket on the right of a bracket is not a call — the two are left side by side.

(21, 2) /o/add
# → : 23
(3) /o/abs, (4) /o/abs
# → : 3  : 4
(1) 2
# → Error parsing message:
(1) (2)
# → : [ ( : 1 ), 2 ]

Bundles

bundle   = "(" [ element { "," element } ] ")" ;

Parentheses gather elements into a bundle that is evaluated where it stands. As the left side of a call it is the bundle of arguments.

(1, 2)
# → ( : 1, : 2 )
/a : (1, 2), /a
# → : ( : 1, : 2 )

An empty () is an empty bundle.

Deferred bundles

deferred  = "{" [ element { "," element } ] "}" ;
qualified = "<" ( deferred | bundle ) ">" ;
closed    = "%{" [ element { "," element } ] "}" ;

Curly braces keep a bundle for later: nothing inside is evaluated where it is written. The result is one value, a blob holding the bundle, and the elements inside need commas between them like anywhere else.

{1, 2}
# → : { : 1, : 2 }

Angle brackets around a deferred bundle make it a bundle element rather than a blob, still unevaluated; around parentheses they change nothing. Angle brackets around a message, or in the item position of a message, are errors with their own diagnostics.

<{1, 2}>
# → ( : 1, : 2 )

%{ … } is a deferred bundle whose free names are filled in where it is written rather than where it runs. Inside a plain { … }, a name written /%/x is filled in where it is written and the others are not.

/x : 7, /q : %{ /a : /x }, /x : 99, /q
# → : { /a : 7 }

Lists

list = "[" [ item { "," item } ] "]" ;
item = value | bundle | deferred | closed ;

Square brackets write several items into one message. A list inside a list is flattened. A named message cannot be an item, so [/a : 1] is an error. A binding takes one item; to bind several values to one name, bind a list.

[1, 2, 3]
# → : [ 1, 2, 3 ]
[1, [2, 3]]
# → : [ 1, 2, 3 ]
/a : [1, 2], /a
# → : [ 1, 2 ]
/a : 1, 2
# → : 2

That last program is two elements — the binding, and the value 2 — not one message with two items.

Strings

Double quotes delimit a string; inside it \" is a quote, \\ a backslash, \n a newline and \t a tab. Two strings side by side are an error, since a string cannot be applied.

"a\"b"
# → : "a"b"
"a\\b"
# → : "a\b"

Comments

# starts a comment that runs to the end of the line, and so does /#/, which is the instruction language’s comment.

(1, 2) /o/add # a comment
# → : 3

Bundles inside messages

The parser has no literal for a bundle as a message item. Binding a bundle to a name produces one — the bundle is stored as an item of the message that carries the name — and the printer shows it as the bundle itself, which is what /a : (1, 2), /a answers above.

Errors

A parse error names the line and column and shows the character it stopped at. An unclosed bracket, a closing bracket with nothing open, a mismatched pair, a number or string where a function was expected, and < > around a message or in a message item each have their own message. The answers below, and the two under Separators and Calls, are the first line of the diagnostic; the machine prints the line and column and the source with a caret under it as well.

(1, 2
# → Unclosed '(' on line 1, column 1.
1) /o/add
# → Found an unexpected closing bracket ')':

Also accepted

A . on its own between elements is accepted and ignored.