Statements in Tenet

Like more traditional languages, Tenet makes a distinction between statements and expressions. Statements start with a keyword and have some number of clauses within them.

Expression statements

Having said all that, we’ll start by showing where the rule is blurred, as expressions can be statements. An expression statement isn’t introduced with a keyword, it’s just an expression.

do {
  5  // Doesn't do anything.
  print-line("Invoking a builtin function is a bit more useful.")
}

That’s all fairly standard.

Let Bindings and Assignments

The let statement is used to declare new variable names. Let’s look at it working as a traditional statement:

// At the top level, `let` creates constant names:
let constant-name = expression

do {
    // A locally visible name can be defined,
    // its scope is restricted to the nearest braces.
    let local-name = expression
    // The mut modifier indicates the value may change
    let mut mutable-name = expression
    // We don't use let to update it:
    mutable-name = new-value-expression
}

We saw the mut modifier to let. By default, a name is immutable, and in Tenet this means it is fully immutable. If we declare it with a mut modifier, it’s fully mutable.

Tenet allows assignment expressions. While these can make code hard to read, sometimes they’re the right way to do it.

do {
    // A mutable name is declared with an initial value
    let mut mutable-name = 7    
    // assignments can happen in expressions
    accept-number(num: (mutable-name = 10) + 3)
}

Names can be defined with let within an expression, because these are technically expressions. The value of the expression is the value stored in the name, same as with an assignment.

do {
    accept-number(num: (let new-name = 3) + 6)
    accept-number(num: (let mut another-name = 3) + 6)
}

Here, new-name is visible within the nearest curly braces. Later, we’ll also see how let can be used in when statements.

Compound assignment

Related to let, but never used with let, are compound assignment operators. These operators follow the pattern that x = x ⊙ y can be abbreviated as x ⊙= y. So += does what you’d expect:

x += 5

A useful operator is ++= which appends strings and extends lists:

let mut str = ""
str ++= "foo"
let mut list = []
list ++= [new-item, new-item]

Mutable strings and lists are implemented with a buffered container, so ++= can be used within a loop without fear of quadratic complexity.

A further guarantee is that an expression like str = pre ++ str ++ suf should be rewritten to avoid copyies to temporaries, similar to:

var str = new StringBuilder("original");
// str = pre ++ str ++ suf
str.insert(0, "prefix ").append(" suffix");

And when the normal operators short-circuit, so do their compounds:

all-tested and= slow-test(new-item)

Function Definitions

A function in Tenet is typically defined as:

fun name(arg1: Type1, arg2: Type2) => ReturnType {
    ...
}

Early arguments may be passed positionally when calling the function. Later arguments must be named.

Block parameters are special: they are defined with a type of the form { args => Return } and are supplied at the call site with a block of code.

fun with-handler(input: Input, handle: { line: Str => Str }) => Output {
    let out = []
    for line in input.lines {
        out ++= [handle(line: line)]
    }
    return make-output(out)
}

// Blocks can be declared outside the parens
fun with-handler(input: Input) handle: { line: Str => Str } => Output {
    ...
}

Blocks cannot be stored in variables or returned: they are constructed only at the point of call.

Control Flow

If / Else

Tenet if/else chains are fairly typical. There’s no allowance, at this time, for dropping the braces.

if condition {
    ...
} else if other-condition {
    ...
} else {
    ...
}

When Statement

The when statement is the primary way to deconstruct unions and records via pattern matching.

when union-expression {
    red ~ let value -> { handle-red(value) }
    #yellow -> { handle-yellow() }
    green ~ (x: let a, y: let b) -> {
        handle-green(a: a, b: b)
    }
}

The let value acts as a capture. It defines a name value that can be used within the corresponding action.

Let’s look at another way to capture: assigning a wildcard. The wildcard pattern _ matches anything. We can use it if we’re not interested in the pattern, but also assign it to a mutable name.

Here we use let x-val because we only need the x values in the action, but we assign y-val to the wilcard to save the y values for later.

let mut y-val: Int = 0

when record-expression {
    (x: #red, y: #red) -> { all-red() }
    (x: green ~ let x-val, y: #red) -> { one-green(x: x-val) }
    (x: #red, y: green ~ y-val = _) -> { one-green(x: y-val) }
    (x: green ~ let x-val, y: green ~ y-val = _) -> { 
        two-green(x: x-val, y: y-val) 
    }
}

Matching is all or nothing and no assignments are made unless a pattern matches completely.

As a convenience, if only one action is required, when expr pattern -> action is allowed.

Return and reply

Any function that declares a return type must return an appropriate value.

fun regular-function() => Str {
    return "okay";
}

A reply statement is a kind of inner return. This is useful for the ? operator, since each action must reply with a value.

let answer = subject ? {
    #simple -> "simple"; // An expression doesn't require `reply`
    #ok -> { 
        notify-okay()
        reply "okay"     // A block does require reply.
    }
}

Do Statements

The do statement introduces a new block. It’s required at the top-level of a Tenet program.

do {
    let value = some-operation()
    something-else(value)
}

A do block can also be embedded in an expression, and passes a value with reply:

let complicated = do {
    let number = 5 // Not visible outside!
    reply complicated-calculation(number)
}

Catch blocks

A catch block can attach to any statement, including the function body, and is invoked if there’s an escaping value.

if a {
    ...
} else if b {
    ...
} else {
    ...
} catch {
    // catches anything escaping from
    // the evaluation of `a` onward
}

It pattern matches like a when statement.

do {
    let value = risky-operation() ! ok
    ...
} catch #miss -> {
    ...
} catch #invalid-input -> {
    ...
}

Esc

In some cases, it may be desirable to unconditionally escape at a given point in control. The esc statement does this.

esc #foo  // escapes with the #foo value specified

when subj {
  foo ~ let x -> handle-x(x);
  bar ~ _ -> esc; // implicitly escapes with subj 
}

Never

If you determine control flow should never reach a point, mark it with a never statement.

when red-or-green {
    (x: #red, y: #red) -> { 
        never 
    }
    (x: green ~ let x-val, y: #red) -> { ... }
    (x: #red, y: green ~ let y-val) -> { ... }
    (x: green ~ _, y: green ~ _) -> { 
        never 
    }
}

never cannot be caught by catch.

Loops

Loops largely work as in other languages, with additional control through break and continue.

While Loops

while condition {
    ...
    continue // go back to reevaluate the condition
    ...
    break    // abort early
    ...
}

For Loops

A for loop evaluates a collection of some kind. Generally, we can’t guarantee the ordering of sets and maps.

for item in list-expr {
    ...
}

for item in set-expr {
    ...
}

Iteration is over List, Set, or Map. The iterator variable is scoped to the loop body.

Future / Deferred