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 18: 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 environment
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, 3
The 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, 2
2.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 entries
The 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 other
2.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 or dictionary with a custom type.
<custom-constructor> ::= <identifier> "::"
(<array-constructor> | <dictionary-constructor>)
2.7 Function constructors
A function constructor creates a new function value.
<function-constructor> ::= "func"
<identifier>?
"(" <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 alternatively a macro invocation (which will be handled at compile time).
<call-expression> ::= <expression> "(" <argument>* %% "," ")"
<argument> ::= expression
If the call is a runtime invocation, the following steps happen:
Evaluate the function expression
Confirm 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 steps happen during compile time instead of at runtime; for details, see Chapter 12: Macros.
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
does the conversion, but also negates the boolean value.
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 Range constructor
A range constructor creates a new Range
.
<range-constructor> ::= <expression> ".." <expression>
2.15 Type check/cast operators
The type check operator is
checks a given value for inclusion in a given
type. The type cast operator as
does nothing to the value if it is already
of the given type, but 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.16 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> ::= "==" | "!="
2.17 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
: isa
strictly less thanb
?a <= b
: isa
less than or equal tob
?a > b
: isa
strictly greater thanb
?a >= b
: isa
greater than or equal tob
?
2.18 Logical operators
Logical connectives include conjunction ("and") and disjunction ("or").
Conjunction (&&
) is truthy if-and-only-if both operands are truthy, and
disjunction (||
) is falsy if-and-only-if both operands are falsy.
<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.19 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)
.
<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 needless assignment.
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.20 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 | ~ |
range | infix | .. |
type check/cast | infix | is , as |
equality | infix | == , != |
comparison | infix | < , <= , > , >= |
conjunction | infix | && |
disjunction | infix | || |
assignment (loosest) | infix | = , += , -= , etc. (right-associative) |
All operators are left-associative, except for the assignment operators which are right-associative.