The Scaly Programming Language

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 valuenext thingmeaning
none, a prefix, or one waiting for its operandan operator namea prefix: it takes the next value (-x, a - -2, not not a)
a valuean operator its type declaresthat operator; it waits for its right operand
a functiona tuplethe call; the tuple is the argument list
a functionone other valuethe call with that one argument (print "hi", twice -5)
a typea tuplethe construction (Point(1, 2))
a typea vector literalthe generic instance (Vector[int])
a valuea vector literalindexing (v[i])
anyas T, is Tthe 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.

InputResult
truetrue
falsefalse

Integer Literals

Integer literals are sequences of decimal digits, optionally preceded by a minus sign for negative numbers.

InputResult
4242
00
-7-7

Hexadecimal Literals

Hexadecimal literals start with 0x or 0X followed by hexadecimal digits (0-9, a-f, A-F).

InputResult
0xFF255
0x1A26
0x00

Floating-Point Literals

Floating-point literals contain a decimal point separating the integer and fractional parts.

InputResult
3.143.14
2.02.0
0.50.5

String Literals

String literals are enclosed in double quotes. They may span multiple lines, with line breaks preserved in the resulting string.

InputResult
"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.

InputResult
"a" as chara
"Z" as charZ

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.

InputResult
-7-7
-(-3)3
+ 33
10 - -212
- -22
not not truetrue
function twice(n: int) returns int
    n * 2
twice -5
-10
= 6 * 742

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.

InputResult
3 + 47
10 - 37
6 * 742
84 / 242
17 % 52
3 + 4 * 211
20 - 5 - 312
1 << 2 + 18
1 + 2 * 3 = 7true
1 < 2 and 3 < 4true

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)).

InputResult
function twice(n: int) returns int
    n * 2
3 + twice(4) * 2
19
function twice(n: int) returns int
    n * 2
twice 21
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.

InputResult
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.

InputResult
let a: u8 200
let b: u8 100
a + b as int
44
let a: u8 200
let b: u8 100
a + (b as int)
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.

InputResult
10 div 33
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.

InputResult
let x 5
x
5
let x 5
x + 3
8
let x 5
let y 3
x + y
8
let x 2 + 3
x * 2
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.

InputResult
let t (x: 3, y: 4)
t.x * t.y
12
let (a, b) (6, 7)
a * b
42
function pair() returns (int, int)
    (3, 4)
let (p, q) pair()
p + q
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.

InputResult
function add(a: int, b: int) returns int
    a + b
let args (40, 2)
add args
42
define Point(x: int, y: int)
let args (1, 2)
let p Point(args)
p.y
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.

InputResult
function twice(n: int) returns int
    n * 2
let g twice
g(21)
42
define Counter(n: int)
{
    function plus(this, k: int) returns int
        n + k
}
let c Counter(40)
let m c.plus
m(2)
42
define IntFn delegate(int) returns int
function apply(f: IntFn, x: int) returns int
    f(x)
function twice(n: int) returns int
    n * 2
apply(twice, 21)
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.

InputResult
5 = 5true
5 = 3false
5 <> 3true
5 <> 5false

Relational Operators

Relational operators compare the ordering of values.

InputResult
3 < 5true
5 < 3false
5 > 3true
3 > 5false
5 <= 5true
5 <= 3false
5 >= 5true
3 >= 5false

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.

InputResult
not truefalse
not falsetrue
~truefalse
~falsetrue

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.

InputResult
true and falsefalse
false or truetrue
procedure noisy() returns bool
{
    print "evaluated"
    true
}
false and noisy()
false
procedure noisy() returns bool
{
    print "evaluated"
    false
}
true or noisy()
true
true & truetrue
true & falsefalse
false | falsefalse
true | falsetrue
true xor truefalse

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.

InputResult
1 << 416
5 << 340
16 >> 24
100 >> 312

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.

InputResult
let inc \x: x + 1
inc(5)
6
let add \x y: x + y
add(3, 4)
7
let dbl \x: x + x
let a dbl(10)
let b dbl(21)
a + b
62

Captures

A lambda captures enclosing variables by value. The captured value is fixed when the lambda is applied.

InputResult
let n 10
let f \x: x + n
f(5)
15
let a 3
let b 4
let g \x: x + a + b
g(10)
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.

InputResult
let n 10
let cap \x: { x + n }
cap(5)
15
let n 10
let shade \x: { let n 99
x + n }
shade(5)
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.

InputResult
let inc \x: x + 1
let add \x y: x + y
inc(5) + add(3, 4)
13
let inc \x: x + 1
let n 100
n + inc(9)
110
let n 100
let cap \x: x + n
cap(1) + cap(2)
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.

InputResult
define IntFn delegate(int) returns int
function apply(f: IntFn, x: int) returns int
    f(x)
let g: IntFn \x: x + 1
apply(g, 5)
6
define IntFn delegate(int) returns int
function twice(f: IntFn, x: int) returns int
    f(f(x))
let dbl: IntFn \x: x + x
twice(dbl, 20)
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.)

InputResult
define IntFn delegate(int) returns int
function apply(f: IntFn, x: int) returns int
    f(x)
let n 10
let add: IntFn \x: x + n
apply(add, 5)
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.

InputResult
define Option union(Some: int, None)
choose Option.Some(7): when v: Some: v else 0
7
define Option union(Some: int, None)
choose Option.None: when v: Some: v else 0
0

Choose with Binding

When a variant carries a value, the value can be bound to a name.

InputResult
define Result union(Ok: int, Err: int)
choose Result.Ok(42): when x: Ok: x + 1 else 0
43

Multiple When Clauses

Choose can have multiple when clauses to match different variants.

InputResult
define Result union(Ok: int, Err: int)
choose Result.Ok(10): when x: Ok: x when e: Err: e + 100
10
define Result union(Ok: int, Err: int)
choose Result.Err(5): when x: Ok: x when e: Err: e + 100
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.

InputResult
define Option[T] union: (Some: T, None)
function check(x: int?) returns int: if x is null: 0 else 1
check(Option[int].None)
0
define Option[T] union: (Some: T, None)
function check(x: int?) returns int: if x is null: 0 else 1
check(Option[int].Some(42))
1

Optional Type Sugar

T? is syntactic sugar for Option[T].

InputResult
define Option[T] union: (Some: T, None)
function unwrap(x: int?) returns int: choose x: when v: Some: v else 0
unwrap(Option[int].Some(42))
42

Generic Option Types

Generic unions like Option[T] work with choose for pattern matching.

InputResult
define Option[T] union: (Some: T, None)
choose Option[int].Some(42): when v: Some: v else 0
42
define Option[T] union: (Some: T, None)
choose Option[int].None: when v: Some: v else -1
-1

Variant Construction

Union variants can be constructed explicitly using Type.Variant syntax.

Construct Variants

Variants are constructed with Type.Variant(value) syntax.

InputResult
define Result union: (Ok: int, Error: int)
choose Result.Ok(42): when x: Ok: x else 0
42
define Result union: (Ok: int, Error: int)
choose Result.Error(100): when e: Error: e else 0
100

Variant Is Expression

The is expression tests if a union value matches a variant.

InputResult
define Result union: (Ok: int, Error: int)
if Result.Ok(50) is Ok : 1 else 0
1
define Result union: (Ok: int, Error: int)
if Result.Error(33) is Ok : 1 else 0
0