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

Only a name that begins with / is bound when the line ends. A bare word names a message and leaves it where it was written: foo : 10 stays on the stack as a value named foo, and a later line’s foo is not bound. Bare names are there for hosts whose own names have no slash. A Max object’s methods reach osen under Max’s message names, such as FullPacket, paired with a function that has one: ("FullPacket", "/FullPacket") /o/addmsg.

/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 ) ( 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 instead be a bracket, whose value is the function applied; then the left-hand side must be a bracket too. A bracket after a number, a string, a name or a list parses, and the program is refused. A number or a string on the right is a parse error.

(21, 2) /o/add
# → : 23
(3) /o/abs, (4) /o/abs
# → : 3  : 4
(5) (({ (/x, 1) /o/add }, ["/x"]) /o/defn)
# → : 6
1 (2)
# → That parsed, but it is not a program.
(1) 2
# → Error parsing message:

A name with a sigil, such as /<</reg or /!/println, isn’t looked up and applied: it’s an instruction, and it runs on what the arguments left. So (/t : 2) /<</reg appends /t : 2 to the register /reg, and leaves nothing.

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 string prints with the same escapes, so what the prompt prints reads back as the same string.

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

An escape is a pair, so a backslash and then a quote is two escapes, and a string can hold a string that holds a quote:

"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 those 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 ')':

Brackets nest at most 20 deep, on every host. Each level costs stack in the passes over the program, and a board that ran out rebooted, so a deeper program is refused when it’s parsed, on the desktop as well as on the board. Then whatever runs on one host runs on the others. To go deeper, name an inner part and call it by the name.

{{{{{{{{{{{{{{{{{{{{{ 1 }}}}}}}}}}}}}}}}}}}}}
# → Nested more than 20 deep at '{' on line 1, column 21.

Also accepted

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