Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Expressions

Expressions are the basic construct of any Wyn program. An expression has a statically determined type and produces a value at runtime. Wyn is an eager/strict language (“call by value”). The basic elements of expressions are called atoms — for example literals and variables, plus the more complicated forms with their own productions in the grammar below.

Some expression forms — notably the second-order array operators (map, reduce, scan, filter) — have parallel semantics. The compiler may but need not execute them in parallel; programs must not depend on a specific evaluation order beyond what the operator itself promises.

Grammar

atom        ::= literal
                | qualname ("." fieldid)*
                | qualname "(" [exp ("," exp)*] ")"
                | qualname slice
                | "(" ")"
                | "(" exp ")" ("." fieldid)*
                | "(" exp ")" "(" [exp ("," exp)*] ")"
                | "(" exp ")" slice
                | "(" exp ("," exp)+ [","] ")"
                | "{" "}"
                | "{" field ("," field)* [","] "}"
                | quals "." "(" exp ")"
                | "[" exp ("," exp)* [","] "]"
                | "@[" exp ("," exp)* [","] "]"
                | "(" qualsymbol ")"
                | "(" exp qualsymbol ")"
                | "(" qualsymbol exp ")"
                | "(" ( "." field )+ ")"
                | "(" "." slice ")"
                | "???"

exp         ::= atom
                | exp qualsymbol exp
                | "!" exp
                | "-" exp
                | constructor [ "(" exp ("," exp)* [","] ")" ]
                | exp ":" type
                | exp ":>" type
                | exp [ ".." exp ] "..." exp
                | exp [ ".." exp ] "..<" exp
                | exp [ ".." exp ] "..>" exp
                | "if" exp "then" exp "else" exp
                | "let" pat [":" type] "=" exp ["in"] exp
                | "let" size+ pat "=" exp ["in"] exp
                | "let" name [generics] "(" [param ("," param)*] ")" "=" exp ["in"] exp
                | "|" pat ("," pat)* "|" exp
                | "loop" pat ["=" exp] loopform "do" exp
                | "#[" attr "]" exp
                | exp "with" slice "=" exp
                | exp "with" fieldid ("." fieldid)* "=" exp
                | exp "with" "." swizzle assign_op exp
                | "match" exp ("case" pat "->" exp)+

assign_op   ::= "=" | "*=" | "+=" | "-=" | "/="
swizzle     ::= [xyzw]+    -- or [rgba]+; one set, distinct chars, len 1..4

slice       ::= "[" index ("," index)* [","] "]"
field       ::= fieldid "=" exp
                | name
size        ::= "[" name "]"

pat         ::= name
                | pat_literal
                | "_"
                | "(" ")"
                | "(" pat ")"
                | "(" pat ("," pat)+ [","] ")"
                | "{" "}"
                | "{" fieldid ["=" pat] ("," fieldid ["=" pat])* [","] "}"
                | constructor [ "(" pat ("," pat)* [","] ")" ]
                | pat ":" type
                | "#[" attr "]" pat

pat_literal ::= [ "-" ] intnumber
                | [ "-" ] floatnumber
                | "true"
                | "false"

loopform    ::= "for" name "<" exp
                | "for" pat "in" exp
                | "while" exp

index       ::= exp [":" [exp]] [":" [exp]]
                | [exp] ":" exp [":" [exp]]
                | [exp] [":" exp] ":" [exp]

Resolving Ambiguities

The grammar above contains ambiguities; they are resolved by the rules below.

An expression x.y is either a reference to the name y in the module x, or the field y in the record x. Modules and values occupy the same namespace, so this is disambiguated by whether x is a value or a module.

A type ascription (exp : type) cannot appear as an array index, as it conflicts with the syntax for slicing.

An expression (-x) is parsed as the variable x negated and enclosed in parentheses, rather than an operator section partially applying the infix operator -.

Prefix operators bind more tightly than infix operators. The only prefix operators are the built-in ! and -; user-defined prefix operators are not supported. A user-defined operator beginning with ! binds as the infix operator (e.g. != row in the table below), not as the prefix !.

Attributes bind less tightly than any other syntactic construct.

The bodies of let, if, and loop extend as far to the right as possible.

The following table describes the precedence and associativity of infix operators in both expressions and type expressions. All operators in the same row have the same precedence. Rows are listed in increasing order of precedence. Not every operator listed is used in expressions; they remain in the table for ambiguity resolution.

AssociativityOperators
left,
left:, :>
left`symbol`
left||
left&&
left<= >= > < == != ! =
left& ^ |
left<< >> >>>
left+ -
left* / % // %%
left|>
right->
left**

Semantics of Simple Expressions

literal

Evaluates to itself.

qualname

A variable name; evaluates to its value in the current environment.

()

Evaluates to an empty tuple.

( e )

Evaluates to the result of e.

???

A typed hole, usable as a placeholder expression. The type checker will infer any necessary type for this expression. This can sometimes result in an ambiguous type, which can be resolved using a type ascription. Evaluating a typed hole results in a run-time error.

(e1, e2, …, eN)

Evaluates to a tuple containing N values. Equivalent to the record literal {0=e1, 1=e2, ..., N-1=eN}.

A record expression consists of a comma-separated sequence of field expressions. Each field expression defines the value of a field in the record. A field expression takes one of two forms:

  • f = e: defines a field with the name f and the value resulting from evaluating e.
  • f: defines a field with the name f and the value of the variable f in scope.

Each field may only be defined once.

a[i]

Return the element at the given position in the array. The index may be of any unsigned integer type. Multi-dimensional arrays are indexed by chaining: a[i][j] selects an element from a rank-2 array; a[i] alone returns the inner sub-array.

a[i:j:s]

Return a slice of the array a from index i to j, the former inclusive and the latter exclusive, taking every s-th element. The s parameter may not be zero. If s is negative, it means to start at i and descend by steps of size s to j (not inclusive). Slicing indices have type i64.

It is generally a bad idea for s to be non-constant. Slicing of multiple dimensions is done by chaining: a[i:j][k:l] slices the outer dimension first, then the inner one.

If s is elided it defaults to 1. If i or j is elided, their value depends on the sign of s. If s is positive, i becomes 0 and j becomes the length of the array. If s is negative, i becomes the length of the array minus one and j becomes minus one. This means that a[::-1] is the reverse of the array a.

In the general case, the size of the array produced by a slice is unknown (see Size Types). In a few cases the size is known statically:

  • a[0:n] has size n
  • a[:n] has size n
  • a[0:n:1] has size n
  • a[:n:1] has size n

This holds only if n is a variable or constant.

[x, y, z]

Create an array containing the indicated elements. Each element must have the same type and shape.

x..y…z

Construct a signed integer array whose first element is x, which proceeds with a stride of y-x until reaching z (inclusive). The ..y part may be elided, in which case a stride of 1 is used. All components must be of the same signed integer type.

A run-time error occurs if z is less than x or y, or if x and y are the same value.

In the general case, the size of the array produced by a range is unknown (see Size Types). In a few cases the size is known statically:

  • 0..<n has size n
  • 0..1..<n has size n
  • 1..2...n has size n

x..y..<z

Construct a signed integer array whose first element is x, which proceeds upwards with a stride of y-x until reaching z (exclusive). The ..y part may be elided, in which case a stride of 1 is used. A run-time error occurs if z is less than x or y, or if x and y are the same value.

  • 0..1..<n has size n
  • 0..<n has size n

This holds only if n is a variable or constant.

x..y..>z

Construct a signed integer array whose first element is x, which proceeds downwards with a stride of y-x until reaching z (exclusive). The ..y part may be elided, in which case a stride of -1 is used. A run-time error occurs if z is greater than x or y, or if x and y are the same value.

e.f

Access field f of the expression e, which must be a record or tuple.

m.(e)

Evaluate the expression e with the module m locally opened, as if by open. This can make some expressions easier to read and write without polluting the surrounding scope with a declaration-level open.

x binop y

Apply an operator to x and y. Operators are functions like any other and can be user-defined. Wyn pre-defines a set of overloaded operators that work across multiple numeric types; these overloaded operators cannot be redefined by the user (but they may be shadowed — see User-Defined Operators). Both operands must have the same type, except where noted below for **. The predefined operators are:

  • **: Power operator, defined for all numeric types. The base and exponent must have the same type, with one exception: if the base is a floating-point scalar (f16 / f32 / f64), the exponent may be any signed or unsigned integer type (i8i64, u8u64); the result type is the base’s float type, computed as if the integer exponent were first converted to the base’s float type.
  • //, %%: Integer division and remainder, rounding towards zero.
  • *, /, %, +, -: The usual arithmetic operators, defined for all numeric types. / and % round towards negative infinity when used on integers — different from C.
  • ^, &, |, >>, <<, >>>: Bitwise operators — respectively bitwise xor, and, or, arithmetic shift right, left shift, and logical (unsigned) shift right. Shifting is undefined if the right operand is negative, or greater than or equal to the bit width of the left operand.

Unlike in C, bitwise operators have higher priority than arithmetic operators. This means that x & y == z is understood as (x & y) == z, rather than x & (y == z) as it would in C. (The latter is a type error in Wyn anyway.)

  • ==, !=: Compare any two values of built-in or compound type for equality.
  • <, <=, >, >=: Compare any two values of numeric type for ordering.
  • `qualname`: Use qualname, which may be any non-operator function name, as an infix operator.

x && y

Short-circuiting logical conjunction; both operands must be of type bool.

x || y

Short-circuiting logical disjunction; both operands must be of type bool.

f(x, y, z)

Apply the function f to the arguments x, y, and z. Function application is always fully saturated: every parameter of f is given a value at the call site. Partial application is not supported.

#c(x, y, z)

Apply the sum type constructor #c to the payload x, y, and z. A constructor application is always assumed to be saturated, so constructors may not be partially applied. A nullary constructor is written bare, with no parentheses (#c).

e : t

Annotate that e is expected to be of type t, failing with a type error if it is not.

Due to ambiguities, this syntactic form cannot appear as an array index expression unless it is first enclosed in parentheses. However, as an array index must always be of type i64, there is never a reason to put an type ascription there.

e :> t

Coerce the size of e to t. The type of t must match the type of e, except that the sizes may be statically different. At run-time it will be verified that the sizes are the same.

! x

Logical negation if x is of type bool. Bitwise negation if x is of integral type.

- x

Numerical negation of x, which must be of numeric type.

#[attr] e

Apply the given attribute to the expression. Attributes are an ad-hoc and optional mechanism for providing extra information, directives, or hints to the implementation. See Attributes for more information.

a with [i] = e

Return a, but with the element at position i changed to contain the result of evaluating e. Consumes a.

r with f = e

Return the record r, but with field f changed to have value e. The type of the field must remain unchanged. Type inference here is limited: r must have a completely known type up to f. This sometimes requires extra type annotations to make the type of r known.

if c then a else b

If c evaluates to true, evaluate a; otherwise evaluate b.

Binding Expressions

let pat = e in body

Evaluate e and bind the result to the irrefutable pattern pat (see Patterns) while evaluating body. The in keyword may be omitted when body is itself a let expression, so chained bindings need only one closing in:

let x = 1
let y = 2 in
x + y

The binding is not let-generalised, meaning it has a monomorphic type. This can be significant if e is of functional type.

If e is of type i64 and pat binds only a single name v, then the type of the overall expression is the type of body, but with any occurrence of v replaced by e.

let [n] pat = e in body

As above, but additionally bind sizes (here n) used in the pattern (here to the size of the array being bound). All declared sizes must be used in the pattern.

let f(x, y) = e in body

Bind f to a local function with the given parameters and definition (e) and evaluate body. The function aliases any free variables in e.

loop pat = initial for x in a do loopbody

Bind pat to the initial values given in initial. For each element x in a, evaluate loopbody and rebind pat to the result of the evaluation. Return the final value of pat.

The = initial may be omitted, in which case initial values for the pattern are taken from equivalently named variables in the environment — loop (x) = ... is equivalent to loop (x = x) = ....

loop pat = initial for x < n do loopbody

Equivalent to loop (pat = initial) for x in (0..1..<n) do loopbody.

loop pat = initial while cond do loopbody

Bind pat to the initial values given in initial. If cond evaluates to true, bind pat to the result of evaluating loopbody, and repeat. Return the final value of pat when cond is false.

match x case p1 -> e1 case p2 -> e2

Match the value produced by x against each pattern in turn, picking the first one that succeeds. The result of the corresponding expression is the value of the entire match expression. All the expressions on the right of a case must have the same type (which need not be the type of x). It is a type error if the cases do not cover every possible value of x — non-exhaustive pattern matching is not allowed.

Function Expressions

|x, y, z| e

Produce an anonymous function taking parameters x, y, and z, whose body is e. See Lambdas for the semantics of environment capture and the restrictions that apply.

(binop)

An operator section that is equivalent to |x, y| x binop y.

(x binop)

An operator section that is equivalent to |y| x binop y.

(binop y)

An operator section that is equivalent to |x| x binop y.

(.a.b.c)

An operator section that is equivalent to |x| x.a.b.c.

(.[i])

An operator section that is equivalent to |x| x[i]. For multi-dimensional indexing, chain: (.[i][j]) is |x| x[i][j].