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.
| Associativity | Operators |
|---|---|
| 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 namefand the value resulting from evaluatinge.f: defines a field with the namefand the value of the variablefin 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 sizena[:n]has sizena[0:n:1]has sizena[:n:1]has sizen
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..<nhas sizen0..1..<nhas sizen1..2...nhas sizen
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..<nhas sizen0..<nhas sizen
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 (i8…i64,u8…u64); 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`: Usequalname, 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].