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: is a strictly less than b?

  • a <= b: is a less than or equal to b?

  • a > b: is a strictly greater than b?

  • a >= b: is a greater than or equal to b?

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.

precedencefixityoperators
term (tightest)N/Aliterals, arrays, dicts, funcs, quasis
parenthesescircum(..)
calls, lookupspostfixf(..), x.y, x[y]
conversionprefix+, -, ~, ?, !
custom constructorinfix::
multiplicativeinfix*, //, %
additiveinfix+, -
concatenationinfix~
rangeinfix..
type check/castinfixis, as
equalityinfix==, !=
comparisoninfix<, <=, >, >=
conjunctioninfix&&
disjunctioninfix||
assignment (loosest)infix=, +=, -=, etc. (right-associative)

All operators are left-associative, except for the assignment operators which are right-associative.

The Camelia image is copyright 2009 by Larry Wall. "Raku" is trademark of the Yet Another Society. All rights reserved.