02-expressions
Chapter 2: Expressions
An expression is an algebraic representation of a computation; evaluating an expression might yield a result value (unless the computation diverges), and might raise one or more side effects.
<expression> ::= <literal-expression>
| <variable-lookup>
| <array-constructor>
| <dictionary-constructor>
| <quasiquote>
| <custom-constructor>
| <function-constructor>
| <call-expression>
| <index-lookup>
| <property-lookup>
| <conversion-expression>
| <additive-expression>
| <multiplicative-expression>
| <string-expression>
| <range-constructor>
| <type-check-cast-expression>
| <equality-expression>
| <comparison-expression>
| <logical-expression>
| <assignment-expression>Expressions adhere to the expression protocol, which requires an expression to be combined with an environment for evaluation:
interface Expression {
method evaluate(env: Environment): Any;
}Functionally, the environment is a dictionary from names to values.
interface Environment {
method lookup(name: Str): Any;
}Terms are expressions without direct subexpressions, including literal expressions, variable lookups, data structure and function constructors, and quasiquotes.
Operator expressions are expressions with one or more subexpressions, and an operator token to determine how the subexpressions are to be combined. The majority of the built-in operator expressions are compositional, and their behavior begins by first evaluating all of the subexpressions and then combining their resulting values. A small few of the built-in operator expressions have thunk semantics, meaning that the subexpressions might not evaluate, or might evaluate multiple times (for side effects).
Some operators affect the parser, so that different grammatical rules for how
to parse the expression temporarily apply after parsing the operator. An
example is the . property lookup operator, which expects an identifier on its
right rather than any valid expression. For more on how to do this with
user-defined operators, see Chapter 19: Parsed macros.
2.1 Literal expressions
A literal expression consists of exactly one literal token, and evaluates directly to its denoted value without any additional computation.
<literal-expression> ::= <literal>Usage example:
false; # value `false`
42; # value `42`
"foo"; # value `"foo"`
none; # value `none`2.2 Variable lookups
A variable lookup finds up a bound value in the current environment by doing a keyed lookup using the variable's name.
<variable-lookup> ::= <identifier>Usage example:
x; # looks up `x` in current environmentThe exact rules around variable lookup become more subtle for code that was generated by macros. For more details, see Chapter 13: Macros.
2.3 Array constructors
An array constructor creates a new Array.
<array-constructor> ::= "[" (<expression>* %% ",") "]"Usage example:
[1, 2, 3]; # new Array with elements 1, 2, 3The unadorned square bracket syntax is syntactic sugar for the longer form
Array::[...], where the type is given explicitly. Other types may also
be specified, as long as they support the array constructor syntax:
Array::[1, 2, 1]; # array with elements 1, 2, 1
Set::[1, 2, 1]; # set with elements 1, 2
Bag::[1, 2, 1]; # bag with elements 1, 1, 22.4 Dictionary constructors
A dictionary constructor creates a new Dict. The keys of a dictionary are
parsed as "extended identifiers", which means that even tokens usually
considered to be keywords are recognized and allowed.
<dictionary-constructor> ::= "{" (<key-value-pair>* %% ",") "}"
<key-value-pair> ::= <key> "=>" <expression>
<key> ::= <identifier> | <keyword> | <alpha-literal>Usage examples:
{ foo => 42 }; # new Dict with one entry
{
x => 1,
y => 2,
}; # new Dict with two entriesThe unadorned curly brace syntax is syntactic sugar for the longer form
Dict::{ ... }, where the type is given explicitly. Other types may also
be specified, as long as they support the dictionary constructor syntax:
Dict::{ foo => 42 }; # dictionary with one key and one value
Bag::{ foo => 42 }; # bag with 42 copies of "foo"
Graph::{
n1 => ["n2"],
n2 => ["n1"],
}; # graph with two nodes, pointing to each other2.5 Quasiquotes
Quotation can mean many things, but for the purposes of this section, it means representing a bit of fixed Alma code as its abstract syntax tree.
Quasiquotation is like quotation, with some parts fixed and other parts computed dynamically.
<quasiquote> ::= "quasi"
"{" (<statement-unq> | <declaration-unq>)* "}"Uniquely for the code inside a quasiquote, the syntax unquote(...) is allowed
and permits dynamic evaluation of an expression. The rule names statement-unq
and declaration-unq are like statement and declaration, respectively,
except that they allow the unquote(...) syntax in term position.
my fragment = quasi { "OH HAI" };
my ast = quasi { say( unquote(fragment) ) }; # quoted `say("OH HAI")`Both the fixed parts and the dynamic/unquoted parts of the quasiquote follow the normal scope rules of blocks in Alma, and have full access to their outer environment. However, the dynamic code evaluates immediately on quasiquote construction, whereas the fixed code closes over its surrounding environment, much like a function does.
2.6 Custom constructor
A custom constructor modulates an array constructor, a dictionary constructor, or a quasiquote with a custom type.
<custom-constructor> ::= <qualified-name> "::"
(<array-constructor>
| <dictionary-constructor>
| <quasiquote>)2.7 Function constructors
A function constructor creates a new function value.
<function-constructor> ::= "func"
<identifier>?
("<" <type-parameter-list> ">")?
"(" <parameter-list> ")"
(":" <type>)?
<block>The func keyword is also used for function declarations (see Chapter 4:
Declarations); in situations where the func keyword
could either be the start of a function declaration or a function constructor,
it's always a function declaration.
The function name is optional in a function constructor. If supplied, that name is visible in the parameter list and body of the function, but not outside the function constructor (since it's not a declaration).
Names that were bound in the environment where the function was constructed are also visible inside the function. We say that the function closes over those names; in practice, the function value gets a copy of the environment where it was constructed. This environment is implicitly passed around together with the function, if the function value is passed around as a first-class value.
2.8 Call expressions
A call expression can represent a runtime invocation (to something that satisfies the invocation protocol), or a macro invocation (which is handled at compile time).
<call-expression> ::= <expression>
("<" <type-argument-list> ">")?
"(" <argument>* %% "," ")"
<argument> ::= expressionIf the call is a runtime invocation, the following steps happen:
Evaluate the function expression
Assert that the result is indeed callable, and has a matching signature (or signal a runtime error)
Evaluate each of the arguments, from left to right
Bind the evaluated argument values to the corresponding parameters
Run the function block, expecting a return value back representing the result of the call
If the call is a macro invocation, the corresponding invocation steps happen during compile time instead of at runtime; for details, see Chapter 13: Macros.
The use of the < character creates a well-known grammatical conflict between
type arguments for calls and the infix < operator. This conflict is resolved
by force (outside of normal LR(1) parsing) by first attempting to parse what
follows as a type argument list, and if that fails, backtracking and trying to
parse it as the < operator. Although it doesn't matter for parsing purposes,
it's recommended as a matter of style to use whitespace around the < operator
but not around the < used to indicate the beginning of a type argument list.
2.9 Indexed and keyed lookups
An indexed lookup represents looking up an element in an indexed container, and a keyed lookup represents lookup up a value in a keyed container. They both share the same syntax.
<index-lookup> ::= <expression> "[" <expression> "]"2.10 Property lookup
A property lookup represents looking up a property in an object.
<property-lookup> ::= <expression> "." <property>
<property> ::= <identifier> | <keyword> | <alpha-literal>2.11 Conversion operators
Built-in prefix conversion operators help convert values across types.
<conversion-expression> ::= <conversion-op> <expression>
<conversion-op> ::= "+" | "-" | "~" | "?" | "!"The + operator converts a value to a number, by default Int; the -
operator does the conversion, but also negates the number.
The ~ operator converts a value to a string (Str).
The ? operator converts a value to a boolean value (Bool); the ! operator
converts to a boolean value, but also negates the result.
2.12 Arithmetic operators
The arithmetic operators, addition, subtraction, multiplication, flooring division, and modulo, all take two integers as inputs and give an integer as a result. (Flooring division and modulo with a left-hand-side of 0 result in a runtime error.)
<additive-expression> ::= <expression> <additive-op> <expression>
<additive-op> ::= "+" | "-"
<multiplicative-expression> ::= <expression> <multiplicative-op> <expression>
<multiplicative-op> ::= "*" | "//" | "%"2.13 String operators
String concatenation takes two values, stringifying them, and gives a concatenated string as a result.
<string-expression> ::= <expression> <string-op> <expression>
<string-op> ::= "~"2.14 Type check/cast operators
The type check operator is checks a given value for inclusion in a given
type.
The type cast operator as asserts that a value is of a given type, and
fails with a runtime error if it isn't.
<type-check-cast-expression> ::= <expression> <type-check-cast-op> <expression>
<type-check-cast-op> ::= "is" | "as"2.15 Equality operators
Equality tests check whether two values are either equal or unequal,
returning a Bool to that effect.
<equality-expression> ::= <expression> <equality-op> <expression>
<equality-op> ::= "==" | "!="The equality and comparison operators are non-associative by default, which means that you are not allowed to chain them in code.
2.16 Comparison operators
Comparison tests compare two values, seen as ordered values or quantities,
returning a Bool.
<comparison-expression> ::= <expression> <comparison-op> <expression>
<comparison-op> ::= "<" | "<=" | ">" | ">="The semantics of the operators are as follows:
a < b: isaless than but not equal tob?a <= b: isaless than or equal tob?a > b: isagreater than but not equal tob?a >= b: isagreater than or equal tob?
The equality and comparison operators are non-associative by default, which means that you are not allowed to chain them in code.
2.17 Logical operators
Logical connectives include conjunction ("and") and disjunction ("or").
Conjunction (&&) returns the first/left operand if it is falsy, and otherwise
the second/right operand. Disjunction (||) returns the first/left operand if
it is truthy, and otherwise the second/right operand.
<logical-expression> ::= <expression> <logical-op> <expression>
<logical-op> ::= "&&" | "||"The built-in logical connectives are short-circuiting, meaning that if
evaluating the left operand is enough to conclude the result, the right
operand will not be evaluated. Specifically, false && b == false, and
true || b == true, both without evaluating b.
2.18 Assignment operators
Assignment operators store a computed value in a location. The = operator
does only this; any operator of the form a op= b means a = (a op b).
There are some expectations on the left-hand side of an assignment, inherent in the term "location". See Chapter 20: Lvalues.
<assignment-expression> ::= <expression> <assignment-op> <expression>
<assignment-op> ::= "=" | "+=" | "-=" | "*=" | "//=" | "%=" | "~=" | "&&="
| "||=" | ".="The assignment operators are right-associative; a chain of them causes the assignments to happen "from right to left".
Just like && and || are short-circuiting and will not evaluate the right
operand unless necessary, the &&= and ||= operators are short-circuiting
and will neither evaluate the right operator nor do the assignment unless
necessary.
Just as the right operator of . parses differently, so does .=.
Specifically, you should view the b of a .= b as parsing in the same way
as a = a.b, where b starts with something identifier-like but is otherwise
an expression.
2.19 Precedence table
Operator precedence determines the binding strength of operators; the tighter or more strongly binding ones always evaluate before the looser or less strongly binding ones.
| precedence | fixity | operators |
|---|---|---|
| term (tightest) | N/A | literals, arrays, dicts, funcs, quasis |
| parentheses | circum | (..) |
| calls, lookups | postfix | f(..), x.y, x[y] |
| conversion | prefix | +, -, ~, ?, ! |
| custom constructor | infix | :: |
| multiplicative | infix | *, //, % |
| additive | infix | +, - |
| concatenation | infix | ~ |
| type check/cast | infix | is, as |
| equality/comparison | infix | == != < <= > >= (non-assoc) |
| conjunction | infix | && |
| disjunction | infix | || |
| assignment (loosest) | infix | =, +=, -=, etc. (right-associative) |
All operators are left-associative, except for the assignment operators which are right-associative.