Chapter 7
Expressions
Overview
Expressions are the core computational units of Scaly. Every
expression evaluates to a value; one that answers nothing answers the
empty tuple (). An expression is an
operation: a sequence of things — values, names,
operator names, tuples, vector literals and blocks — that the compiler
reads from left to right. It holds one current value, and the
type of that value says what the next thing
means:
| current value | next thing | meaning |
|---|---|---|
| none, a prefix, or one waiting for its operand | an operator name | a prefix: it takes the next value (-x, a - -2, not not a) |
| a value | an operator its type declares | that operator; it waits for its right operand |
| a function | a tuple | the call; the tuple is the argument list |
| a function | one other value | the call with that one argument (print "hi", twice -5) |
| a type | a tuple | the construction (Point(1, 2)) |
| a type | a vector literal | the generic instance (Vector[int]) |
| a value | a vector literal | indexing (v[i]) |
| any | as T, is T | the cast or the test of the whole operation so far |
Parentheses make a tuple (and a tuple of one value is that value, so
they also group), brackets a vector literal, braces a block, and the dot
a member. Nothing else structures a sequence: there is no precedence
table in the compiler. How strongly operators bind is stated by the
standard library, as a protocol of pending types:
a + answers a value that waits for its right operand,
and the protocol decides what an operator after it means. The levels,
strongest first, are listed under Operations.
Two consequences are worth knowing before reading the rest of this chapter:
Application is stronger than waiting. Before an operator takes what follows, that thing takes what is applied to it:
a + f(1)adds the call's result,a + v[i]the element.A call without parentheses takes exactly one thing.
print f(x)is(print f)(x), which the compiler reports; an argument that is itself a call, an indexing or a construction takes its parentheses:print(f(x)).
Literals
Expressions are the core computational units of Scaly. Every expression evaluates to a value.
Boolean Literals
The boolean literals are true and false.
| Input | Result |
|---|---|
true | true
|
false | false
|
Integer Literals
Integer literals are sequences of decimal digits, optionally preceded by a minus sign for negative numbers.
| Input | Result |
|---|---|
42 | 42
|
0 | 0
|
-7 | -7
|
Hexadecimal Literals
Hexadecimal literals start with 0x or 0X followed by hexadecimal digits (0-9, a-f, A-F).
| Input | Result |
|---|---|
0xFF | 255
|
0x1A | 26
|
0x0 | 0
|
Floating-Point Literals
Floating-point literals contain a decimal point separating the integer and fractional parts.
| Input | Result |
|---|---|
3.14 | 3.14
|
2.0 | 2.0
|
0.5 | 0.5
|
String Literals
String literals are enclosed in double quotes. They may span multiple lines, with line breaks preserved in the resulting string.
| Input | Result |
|---|---|
"hello" | hello
|
"" |
|
Character Literals
A character is written as a string literal of one character wherever a char is expected: a char parameter, a typed binding, or the cast as char.
| Input | Result |
|---|---|
"a" as char | a
|
"Z" as char | Z
|
Operations
An operation is a sequence of things: values, names, operator names, tuples in parentheses, vector literals in brackets and blocks in braces, each perhaps followed by a member access with a dot. The compiler reads the sequence from left to right and holds one current value; the TYPE of that value says what the next thing means. A tuple after a function is the call, a tuple after a type the construction, a vector literal after a value the indexing, an operator name after a value one of the operators its type declares. Parentheses, brackets, braces and the dot are what structure a sequence, and nothing else does. An operation ends with a colon, a line break, or anything that is not part of it.
Prefix Operators
Where a value is expected an operator name is a prefix: it takes the next value as its operand. A value is expected at the start of an operation, after an operator, after a function, and after another prefix, so a - -2 subtracts minus two, twice -5 is twice(-5), and not not a reads as written. An = at the start of an operation does nothing, so let a = 42 reads as let a 42.
| Input | Result |
|---|---|
-7 | -7
|
-(-3) | 3
|
+ 3 | 3
|
10 - -2 | 12
|
- -2 | 2
|
not not true | true
|
| -10
|
= 6 * 7 | 42
|
Operator Chains
Several operators in a row combine by their binding strength, which the standard library states as a protocol of pending types: the compiler itself has no precedence table. Strongest first, the levels are * / % div mod; + -; << >>; < > <= >=; = <>; &; xor; |; and &&; or ||. An operator a type declares that no level names, such as == or a text name, binds most weakly of all. Operators of one level combine from left to right.
| Input | Result |
|---|---|
3 + 4 | 7
|
10 - 3 | 7
|
6 * 7 | 42
|
84 / 2 | 42
|
17 % 5 | 2
|
3 + 4 * 2 | 11
|
20 - 5 - 3 | 12
|
1 << 2 + 1 | 8
|
1 + 2 * 3 = 7 | true
|
1 < 2 and 3 < 4 | true
|
Application Before Operators
Before an operator takes the thing that follows it, that thing takes what is applied to it: a function its tuple, a value its vector literal, anything its members. So a + f(1) adds the call's result, and a + v[i] the element. A function called without parentheses takes exactly one thing; an argument that is itself a call, an indexing or a construction takes its parentheses, print(f(x)).
| Input | Result |
|---|---|
| 19
|
| 42
|
Parentheses and Grouping
Parentheses around a single value group it: (a) is a. The group is read as an operation of its own, so a prefix may begin it inside a longer chain, and it combines first.
| Input | Result |
|---|---|
5 * (- 2) | -10
|
2 * (3 + 4) | 14
|
(2 + 3) * (4 + 1) | 25
|
Casts Take the Whole Operation
A cast as T and a test is T apply to the whole operation before them: a + b as T is (a + b) as T. A cast of the right side alone takes parentheses.
| Input | Result |
|---|---|
| 44
|
| 300
|
Identifier Shapes
Identifiers can have name shape (alphanumeric) or operator shape (symbols). The shape is purely lexical and does not determine behavior: what a name means depends on its declaration and on what the operation holds when it is reached.
| Input | Result |
|---|---|
10 div 3 | 3
|
abs (- 5) | 5
|
Let Bindings
Let bindings introduce local variables. The syntax is let followed by a name and a value expression (no equals sign). Multiple statements are separated by colons or newlines.
| Input | Result |
|---|---|
| 5
|
| 8
|
| 8
|
| 10
|
Tuples and Function Values
A tuple is a value: an anonymous record written in parentheses. Its components may carry labels, and its type is written the same way, (int, String). The empty tuple () is the type of no value, which a routine without a result answers. Functions are values too: a name bound to a function, or a method bound to its receiver, is called later like the function itself.
Tuples
A labelled component is read by its label. Unlabelled components are reached by decomposition, let (a, b) t; a tuple is not an array and has no indexing. A routine returns several values as a tuple.
| Input | Result |
|---|---|
| 12
|
| 42
|
| 7
|
Applying a Bound Tuple
A tuple bound to a name stands for its components where it is applied: f args calls f with them, and Point(args) builds the record from them. Its components are evaluated once, when it is bound.
| Input | Result |
|---|---|
| 42
|
| 2
|
Functions as Values
let g twice binds the function at compile time and costs nothing; g(21) is a direct call. x.m without a tuple is the method bound to its receiver. Where a delegate type is declared, a named function becomes a closure value, chosen by the delegate's signature.
| Input | Result |
|---|---|
| 42
|
| 42
|
| 42
|
Comparison Operators
Comparison operators compare two values and return a boolean result.
Equality
The equality operator (=) tests if two values are equal. The inequality operator (not equal to) tests if two values are different.
| Input | Result |
|---|---|
5 = 5 | true
|
5 = 3 | false
|
5 <> 3 | true
|
5 <> 5 | false
|
Relational Operators
Relational operators compare the ordering of values.
| Input | Result |
|---|---|
3 < 5 | true
|
5 < 3 | false
|
5 > 3 | true
|
3 > 5 | false
|
5 <= 5 | true
|
5 <= 3 | false
|
5 >= 5 | true
|
3 >= 5 | false
|
Boolean Operators
Boolean operators combine boolean values.
Boolean Negation
The word operator not negates a boolean value. The tilde is the bitwise complement, which on a bool is the same. The exclamation mark is not negation in Scaly: it is the sigil of the thrown lifetime.
| Input | Result |
|---|---|
not true | false
|
not false | true
|
~true | false
|
~false | true
|
Boolean And/Or
The operators and and or (also spelled && and ||) are declared by the standard library with a lazy right operand: the right side is evaluated only where the left one does not decide. The bitwise operators &, | and xor combine two bools as well, and evaluate both sides.
| Input | Result |
|---|---|
true and false | false
|
false or true | true
|
| false
|
| true
|
true & true | true
|
true & false | false
|
false | false | false
|
true | false | true
|
true xor true | false
|
Bitwise Operators
Bitwise operators manipulate integer values at the bit level.
Bit Shifts
The left shift operator shifts bits to the left, effectively multiplying by powers of two. The right shift operator shifts bits to the right, effectively dividing by powers of two.
| Input | Result |
|---|---|
1 << 4 | 16
|
5 << 3 | 40
|
16 >> 2 | 4
|
100 >> 3 | 12
|
Lambdas
A lambda is an anonymous function written with a backslash:
\params: body. Parameters are space-separated. A lambda bound to a name
and applied to arguments is monomorphized at the call site for those
argument types, so the same lambda works at any argument type.
Lambda Application
A lambda bound to a name can be called like a function.
| Input | Result |
|---|---|
| 6
|
| 7
|
| 62
|
Captures
A lambda captures enclosing variables by value. The captured value is fixed when the lambda is applied.
| Input | Result |
|---|---|
| 15
|
| 17
|
Captures in Braced Bodies
Capture discovery reaches inside a braced body, and a body-local binding that shadows an enclosing name is the local, not a capture.
| Input | Result |
|---|---|
| 15
|
| 104
|
Embedded Calls
A lambda call may appear anywhere inside a larger expression, not only as the whole right-hand side of a binding. Like every application, each call takes its arguments before the operators around it combine.
| Input | Result |
|---|---|
| 13
|
| 110
|
| 203
|
Delegate Values
A delegate names a function type. A non-capturing lambda assigned
to a delegate-typed binding is a function-pointer value that can be passed
to a function and called indirectly. A lambda literal may also be passed
straight to a delegate parameter, and so may a named function.
| Input | Result |
|---|---|
| 6
|
| 80
|
Closures
A capturing lambda assigned to a delegate type is a true closure value carrying the captured variables. (Closures returned from a function escape onto the caller's region -- see the escaping-closure AOT regression test.)
| Input | Result |
|---|---|
| 15
|
Choose Expressions
Choose expressions pattern match on union types, allowing different code paths based on which variant is present.
Choose on Variant Construction
Choose can match directly on constructed variants.
| Input | Result |
|---|---|
| 7
|
| 0
|
Choose with Binding
When a variant carries a value, the value can be bound to a name.
| Input | Result |
|---|---|
| 43
|
Multiple When Clauses
Choose can have multiple when clauses to match different variants.
| Input | Result |
|---|---|
| 10
|
| 105
|
Optional Types
Optional types use null pointer optimization (NPO) for efficient representation.
Is Null Check
The is null expression checks if an optional value is None.
| Input | Result |
|---|---|
| 0
|
| 1
|
Optional Type Sugar
T? is syntactic sugar for Option[T].
| Input | Result |
|---|---|
| 42
|
Generic Option Types
Generic unions like Option[T] work with choose for pattern matching.
| Input | Result |
|---|---|
| 42
|
| -1
|
Variant Construction
Union variants can be constructed explicitly using Type.Variant syntax.
Construct Variants
Variants are constructed with Type.Variant(value) syntax.
| Input | Result |
|---|---|
| 42
|
| 100
|
Variant Is Expression
The is expression tests if a union value matches a variant.
| Input | Result |
|---|---|
| 1
|
| 0
|