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, but also more complicated forms.
Grammar
atom ::= literal
| qualname ("." fieldid)*
| "(" ")"
| "(" exp ")" ("." fieldid)*
| "(" exp ("," exp)+ [","] ")"
| "{" "}"
| "{" field ("," field)* [","] "}"
| qualname slice
| "(" exp ")" slice
| quals "." "(" exp ")"
| "[" exp ("," exp)* [","] "]"
| "(" qualsymbol ")"
| "(" exp qualsymbol ")"
| "(" qualsymbol exp ")"
| "(" ( "." field )+ ")"
| "(" "." slice ")"
| "???"
exp ::= atom
| exp qualsymbol exp
| exp exp
| "!" exp
| "-" exp
| constructor exp*
| exp ":" type
| exp ":>" type
| exp [ ".." exp ] "..." exp
| exp [ ".." exp ] "..<" exp
| exp [ ".." exp ] "..>" exp
| "if" exp "then" exp "else" exp
| "let" size* pat "=" exp "in" exp
| "let" name slice "=" exp "in" exp
| "let" name type_param* pat+ [":" type] "=" exp "in" exp
| "|" pat ("," pat)* "|" exp
| "loop" pat ["=" exp] loopform "do" exp
| "#[" attr "]" exp
| "unsafe" exp
| "assert" atom atom
| exp "with" slice "=" exp
| exp "with" fieldid ("." fieldid)* "=" exp
| "match" exp ("case" pat "->" exp)+
slice ::= "[" index ("," index)* [","] "]"
field ::= fieldid "=" exp
| name
size ::= "[" name "]"
pat ::= name
| pat_literal
| "_"
| "(" ")"
| "(" pat ")"
| "(" pat ("," pat)+ [","] ")"
| "{" "}"
| "{" fieldid ["=" pat] ("," fieldid ["=" pat])* [","] "}"
| constructor pat*
| pat ":" type
| "#[" attr "]" pat
pat_literal ::= [ "-" ] intnumber
| [ "-" ] floatnumber
| charlit
| "true"
| "false"
loopform ::= "for" name "<" exp
| "for" pat "in" exp
| "while" exp
index ::= exp [":" [exp]] [":" [exp]]
| [exp] ":" exp [":" [exp]]
| [exp] [":" exp] ":" [exp]
Description
Some of the built-in expression forms have parallel semantics, but it is not guaranteed that the the parallel constructs in Wyn are evaluated in parallel, especially if they are nested in complicated ways. Their purpose is to give the compiler as much freedom and information is possible, in order to enable it to maximise the efficiency of the generated code.
Resolving Ambiguities
The above grammar contains some ambiguities, which in the concrete implementation is resolved via a combination of lexer and grammar transformations. For ease of understanding, they are presented here in natural text.
An expression x.y may either be a reference to the name y in the module x, or the field y in the record x. Modules and values occupy the same name space, so this is disambiguated by whether x is a value or module.
A type ascription (exp : type) cannot appear as an array index, as it conflicts with the syntax for slicing.
In f [x], there is an ambiguity between indexing the array f at position x, or calling the function f with the singleton array x. We resolve this the following way:
- If there is a space between
fand the opening bracket, it is treated as a function application. - Otherwise, it is an array index operation.
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. Note that the only prefix operators are the builtin ! and -, and more cannot be defined. In particular, a user-defined operator beginning with ! binds as !=, as on the table below, not as the prefix operator !.
Function and type application binds more tightly than infix operators.
#foo #bar is interpreted as a constructor with a #bar payload, not as applying #foo to #bar (the latter would be semantically invalid anyway).
Attributes bind less tightly than any other syntactic construct.
A type application pt [n]t is parsed as an application of the type constructor pt to the size argument [n] and the type t. To pass a single array-typed parameter, enclose it in parens.
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. The rows are listed in increasing order of precedence. Note that not all operators listed here are used in expressions; nevertheless, they are still used for resolving ambiguities.
| Associativity | Operators |
|---|---|
| left | , |
| left | :, :> |
| left | `symbol` |
| left | || |
| left | && |
| left | <= >= > < == != ! = |
| left | & ^ | |
| left | << >> >>> |
| left | + - |
| left | * / % // %% |
| left | |> |
| right | <| |
| right | -> |
| left | ** |
| left | juxtaposition |
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 can take 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 a comma-separated list of indexes instead of just a single index. If the number of indices given is less than the rank of the array, an array is returned. The index may be of any unsigned integer type.
The array a must be a variable name or a parenthesised expression. Furthermore, there may not be a space between a and the opening bracket. This disambiguates the array indexing a[i], from a [i], which is a function call with a literal 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 can be done only with expressions of type i64.
It is generally a bad idea for s to be non-constant. Slicing of multiple dimensions can be done by separating with commas, and may be intermixed freely with indexing.
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 become 0 and j become 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.
Large array optimisation: as a special case, large one-dimensional array literal consisting entirely of monomorphic constants (i.e., numbers must have a type suffix) are handled with specialised fast-path code by the compiler. To keep compile times manageable, make sure that all very large array literals (more than about ten thousand elements) are of this form. This is likely relevant only for generated code.
x..y…z
Construct a signed integer array whose first element is x and which proceeds with a stride of y-x until reaching z (inclusive). The ..y part can be elided in which case a stride of 1 is used. All components must be of the same unsigned 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 sizen.0..1..<nhas sizen.1..2...nhas sizen
x..y..<z
Construct a signed integer array whose first elements is x, and which proceeds upwards with a stride of y-x until reaching z (exclusive). The ..y part can 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 elements is x, and which proceeds downwards with a stride of y-x until reaching z (exclusive). The ..y part can 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 global 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 certain “magical” overloaded operators that work on several types. Overloaded operators cannot be defined by the user. Both operands must have the same type. The predefined operators and their semantics are:
**: Power operator, defined for all numeric types.//,%%: Division and remainder on integers, with rounding towards zero.*,/,%,+,-: The usual arithmetic operators, defined for all numeric types. Note that/and%rounds towards negative infinity when used on integers - this is different from in 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 length in bits of the left operand.
Note that, 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. Note that the latter is a type error in Wyn anyhow.
==,!=: Compare any two values of builtin or compound type for equality.<,<=.>,>=: Company any two values of numeric type for equality.`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
Apply the function f to the argument x.
#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, i.e. its entire payload provided. This means that constructors may not be partially applied.
e : t
Annotate that e is expected to be of type t, failing with a type error if it is not. If t is an array with shape declarations, the correctness of the shape declarations is checked at run-time.
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 explicit 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 compiler. See Attributes for more information.
unsafe e
Elide safety checks and assertions (such as bounds checking) that occur during execution of e. This is useful if the compiler is otherwise unable to avoid bounds checks (e.g. when using indirect indexes), but you really do not want them there. Make very sure that the code is correct; eliding such checks can lead to memory corruption.
This construct is deprecated. Use the #[unsafe] attribute instead.
assert cond e
Terminate execution with an error if cond evaluates to false, otherwise produce the result of evaluating e. Unless e produces a value that is used subsequently (it can just be a variable), dead code elimination may remove the assertion.
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 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, else 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 is optional if body is a let expression. 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 occurence of v replaced by e.
let [n] pat = e in body
As above, but bind sizes (here n) used in the pattern (here to the size of the array being bound). All sizes must be used in the pattern. Roughly equivalent to let f [n] pat = body in f e.
let a[i] = v in body
Write v to a[i] and evaluate body. The given index need not be complete and can also be a slice, but in these cases, the value of v must be an array of the proper size. This notation is syntactic sugar for let a = a with [i] = v in a.
let f params… = e in body
Bind f to a function with the given parameters and definition (e) and evaluate body. The function will be treated as aliasing any free variables in e. The function is not in scope of itself, and hence cannot be recursive.
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 can be left out, in which case initial values for the pattern are taken from equivalently named variables in the environment. I.e., 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 the step.
Return the final value of pat.
match x case p1 -> e1 case p2 -> e2
Match the value produced by x to each of the patterns 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 associated with a case must have the same type (but not necessarily match the type of x). It is a type error if there is not a case for every possible value of x - inexhaustive pattern matching is not allowed.
Function Expressions
|x, y, z| e
Produces an anonymous function taking parameters x, y, and z, whose body is e. Lambdas do not permit type parameters; use a named function if you want a polymorphic function.
(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,j])
An operator section that is equivalent to |x| x[i,j].