Link Search Menu Expand Document

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 ints and floats.

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.