Oters - Expressions
The full set of expressions in Oters with their corresponding syntax. Syntax should be quite familiar for Rust users although some expressions borrow from OCaml syntax. Although going through this entire page shouldn’t be necessary, you should definitely familiarize yourselves with the stream operator and the modal expressions.
letter ::= a..z | A..Z
lower_id ::= (a..z) (letter | 0..9 | ' )*
upper_id ::= (A..Z) (letter | 0..9 | ' )*
path ::= (lower_id ::)+
Type Expressions
generic_args = <(type_expr, )*>
type_expr ::= () // Unit type
| int | float | string | bool // Primitive types
| type_expr -> type_expr // Function types
| (type_expr, (type_expr, )+) // Tuple types
| [ type_expr ] // List type
| path? upper_id generic_args? // User defined types, structs, and enums
| @ type_expr // ◯ modal type
| # type_expr // □ modal type
| ( type_expr ) // Type in parentheses
Top-level Statements
These are the same top level statements as described in the Program Structure page plus the let ... and ... with ...
construction.
generic_params ::= <(upper_id, )*>
use
use_expr ::= path item
| path *
Imports a specific item found in another Oters file, or all items in the file with the *
import. Types, structs, enums and variables can all be imported.
use std::stream::map
use gui::widget::*
type
type_def ::= type upper_id generic_params? = type_expr
Aliases another type. GenArgs
are the generic arguments of the type and are written as a comma separated list of capitalized identifiers.
type Stream<A> = (A, @Stream)
struct
field_def ::= lower_id : type_expr,
struct_def ::= struct upper_id generic_params? { field_def+ }
Defines a struct.
struct Color {
r: int,
g: int,
b: int,
a: int,
}
enum
variant_def ::= upper_id type_def?,
enum_def ::= enum upper_id generic_params? { variant_def+ }
Defines an enum.
enum Option<T> {
None,
Some T,
}
let
let_pattern ::= let_pattern << let_pattern // Matches streams
| let_pattern : let_pattern // Matches lists
| lower_id
| _
| (let_pattern, (let_pattern ,)+) // Matches tuples
let_smnt ::= let let_pattern = expr
Binds the result of expression to the pattern’s variables if the corresponding types match.
let from = fn n -> n << @(from (n+1))
let (btn_id, btn_stream) = gui::widget::button frame
(100, 50)
(std::stream::const "Click me!")
let … and … with
let_and_with ::= let let_pattern = expr and lower_id = expr with expr
Defines mutually recursive streams.
Defining mutually recursive streams is tricky since there is essentially a chicken and egg problem. That is that we can’t know one stream’s current value without knowing the other’s, and vice versa. To get around this problem, we chose one stream to always execute first and use the other stream’s value from the previous time step. However, we then need to specify an initial value for this second stream which is provided following the with
.
There are a few further restrictions to defining mutually recursive variables using the expression \(\texttt{let}\ p_1 = e_1\ \texttt{and}\ p_2 = e_2\ \texttt{with}\ e_3\). Firstly, as implied by the syntax definition, is that \(p_2\) (the pattern which binds the second variable) must be a single identifier. This is because the second of the mutually recursive variables must have type Stream<T>
and the resulting stream must be bound to a single value. The expression \(e_3\) then specifies the first value of the second stream \(-\) the one bound to \(p_2\).
The following is a simple example with its corresponding execution through time:
let chicken = map #(fn b -> if b then 0 else 1) egg
and egg = map #(fn i -> if i == 1 then true else false) chicken
with false
time | -1 | 0 | 1 | 2 | 3 | ...
-----------------------------------
chicken | - | 1 | 0 | 1 | 0 | ...
egg | f | t | f | t | f | ...
Expressions
The right side of the =
in let
statements.
Values
value ::= true | false // Boolean values
| -? (0..9)+ // Integer values
| -? (0..9)+ . (0..9)* // Float values
| " * " // String values
| () // Unit values
| lower_id // Variable values
The *
for the string value is simply meant to represent any valid string.
Binary Operations
bin_op ::= || | && // Boolean operators
| == | < | > // Comparison operators
| : | << // List cons and stream construction
| + | - | // Arithmetic operators
| * | / | % // Arithmetic operators
bin_op_expr ::= expr bin_op expr
Operators above are defined in order of preference. The arithmetic operators are overloaded for int
s and float
s.
Of particular interest is the <<
stream operator. It constructs a stream from a head of type T
and a tail of type @T
. The latter is usually a recursive call to a function or value that returns a Stream<T>
wrapped in a delay expression. For instance in:
let from = fn n -> n << @(from (n+1))
n
is an int
and the expression @(from (n+1))
is of type @(Stream<T>)
.
Unary Operations
un_op ::= ~ | ! // Numeric negation and boolean negation
un_op_expr ::= un_op expr
Higher precedence than all binary operators.
List
list_expr ::= [ (expr ,)* ]
A list expression, could be the empty list []
.
Tuple
list_expr ::= ( expr, (expr ,)+ )
A tuple construction, minimum two elements.
Struct
field_expr ::= lower_id : expr,
struct_expr ::= path? upper_id { field_expr+ }
A struct construction. The struct’s definition must be in scope. This means it was either imported or defined earlier in the file.
Color { r: 255, g: 0, b: 255, a: 255 }
Variant
variant_expr ::= path? upper_id :: upper_id expr?
As in Rust and unlike OCaml, variants must reference their corresponding enum. However, unlike in rust, the expression use Option::None
is not valid.
Shape::Circle ((100, 100), 20, gui::color::Red)
Option::None
It is also important to note that Variant expressions have lower precedence than function application. This can be the source of error since a Variant expression needs to be wrapped in parentheses when applied to a function:
let (grp_id, grp_stream) = vgroup menu (200, 500) grp_elems (Alignment::Bottom)
Note how the Variant Alignment::Bottom
is wrapped in parentheses.
Function
fn_arg ::= fn_arg << fn_arg // Matches streams
| _
| lower_id
| # fn_arg // Function argument must be stable
| (fn_arg, (fn_arg ,)+) // Matches tuples
fn_expr ::= fn fn_arg -> expr
The definition of functions.
Arguments with #
prefixed must be of stable type. That is, they cannot be functions, or contain any temporal values (i.e. those inside @
) unless wrapped in a #
. For example, the const
function creates a stream of constant values and is defined as follows:
let const = fn #x -> x << @(const x)
This ensures that the value passed to the function is available at every time step.
If
if_expr ::= if expr then expr else expr
If expression that requires both branches such that it always returns a value.
Block
block_term ::= expr ;
| let let_pattern = expr ;
block_expr ::= { block_term* expr }
A sequence of expressions and value definitions that evaluate to ()
ending on some return value.
let nats = {
let from n -> n << @(from (n+1));
from 0
}
Application
app_expr ::= expr expr
Function application
Struct Projection
proj_expr ::= expr . lower_id
Accesses the field of a struct.
color.r
Match
match_item ::= pattern => expr ,
match_expr ::= match expr { match_item+ }
Standard match expressions using Rust syntax.
let double = fn stream ->
match stream {
x << xs => x * 2 << @(double !@xs)
}
Note that the match expression here is a bit superflous as we could immediately match the stream to the function arguments like so:
let double = fn (x << xs) -> x * 2 << @(double !@xs)
Delay
delay_expr ::= @ expr
The delay expression introduces the \(\bigcirc\) modality. It takes an expression that should be computed in the following time step. Most often used to construct the tail of a stream.
let from = fn n -> n << @(from (n+1))
Also important to note is that only values and variables available in the next time step can be accessed from within delay expressions. That is, only stable values and variables can be accessed from within delays.
Advance
adv_expr ::= !@ expr
Eliminates the \(\bigcirc\) modality. This expression can only be used from within delay expressions and is typically used to unwrap the tails of streams. For example in the double
example:
let double = fn (x << xs) -> x * 2 << @(double !@xs)
xs
has type @(Stream<int>)
while double has type Stream<int> -> Stream<int>
. So we unwrap the delay from xs
so that we can apply it to double
in the next time step once the tail of the stream becomes available.
Box
box_expr ::= # expr
The box expression introduces the \(\square\) modality to create stable versions of a value. In particular, this tends to be used to pass functions into recursive functions. Recall that functions are not stable because they can capture temporally variant values such as streams in their closure. However, we can wrap functions in a #
expression, and if they indeed do not capture any unstable variables, pass them recursively to functions.
The map
function is a good example of this:
let map = fun f (a << as) -> (!#f a) << @(map f !@as)
Here, f
is a boxed function of type #(A -> B)
. Any function passed to map
must not capture any unstable variables so that it can always be applied at any time step.
Unbox
unbox_expr ::= !# expr
The unbox expression eliminates the \(\square\) modality. An example of its use is just above in the map
function. In order to apply the function inside the #
, we first need to unbox f
. Hence, we get !#f a
which applies the function inside the #
to a
.
Patterns
These are the pattern expressions for match expressions:
field_pattern ::= lower_id : pattern // Binds a field to the pattern
| lower_id // Binds a field to a variable with the field's name
| .. // Stops binding the rest of the fields
pattern ::= pattern | pattern // Matches either the first or the second pattern
| pattern : pattern // Matches the head and tail of a list
| pattern << pattern // Matches streams
| [(pattern ,)*] // Matches lists element-wise
| (pattern, (pattern,)+) // Matches tuples
| path? upper_id { (field_pattern ,)+ } // Matches Structs
| path? upper_id :: upper_id pattern? // Matches Enum Variants
| value
| _
Note that the first |
in the pattern definition is an actual |
character in the syntax.