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.