Statements

Statements are generally delimited by keywords at the beginning rather than semicolons at the end, but assignments may omit the let keyword.

Let statements

A let statement consists of the keyword let, a left-hand side expression, an assignment operator, and a right-hand side expression.

A let statement both defines a name and assigns a value. The scope of an assignment is every statement that logically follows within the current function body.

let a := 42
let n := [4, 5, 6]
let b := (a: 1, b: 3, c: n, d: foo ~ 7)

Lensing assignments

Values in Tenet are immutable, thus mutability is emulated through lensing transformations.

Specific assignments are described in operators, however all lensing assignments have the form:

let name op arg := rhs
;; becomes
let name := lens_op(name, arg, rhs)

Lensing may be applied repeatedly by observing that lensing is left associative:

let (((name op arg) op arg) op arg) ... opN argN := rhs

Repeated lensing is evaluated by peeling the last operation off one at a time:

let name op1 arg1 ... opM argM opN argN := rhs
;; Consider lhs = node op1 arg1 ... opM argM, then:
let lhs opN argN := rhs
;; Transforms to:
let lhs := lens_opN(lhs, opN, rhs)
;; Now rhs = lens_opN(lhs, opN, rhs)
;; And then peel off opM argM and repeat the process until name is determined.

A more direct example:

let x := (a: (b: (c: 1, d: 2), e: 3), f:4)
;; Operation to perform:
let x.a.b.c := 5
;; First round
let lhs1 = x.a.b
let lhs1.c := 5
let rhs1 := rec_set(lhs1, #c, 5)
;; Second round
let lhs2 = x.a
let lhs2.b := rhs1
let rhs2 := rec_set(lhs2, #b, rhs1)
;; Third round
let lhs3 = x
let lhs3.a := rhs2
let lhs3 := rec_set(lhs3, #a, rhs2)
;; Expand
let x := rec_set(x, #a, rec_set(x.a, #b, rec_set(x.a.b, #c, 5)))

Destructuring assignments

A let statement may also use destructuring to make multiple assignments from a record.

A record destructuring assignment has the following transformation:

let (slot1: name1, slot2: name2, ... slotN: nameN) := rhs
;; Transforms to:
let name1 := rhs.slot1
let name2 := rhs.slot2
...
let nameN := rhs.slotN

Within record destructuring, if names are omitted, the slot name is used.

let (slot1, slot2, ... slotN) := rhs
;; Transforms to:
let slot1 := rhs.slot1
let slot2 := rhs.slot2
...
let slotN := rhs.slotN

The destructuring expression may omit slots in the record type. For purposes of type-checking, the left-hand side has the special OpenRecord type.

Modified let statements

There are two possible modifications to a let statement.

The compiler may issue a warning if a bare let statement is potentially ambiguous. Ambiguity can be resolved either through a trailing semicolon or adding a let keyword.

A def statement signals the intent that a name is being defined for the first time in its scope. This is not necessary in reasonably short code, but in longer functions, it may catch unintentional shadowing. Because it must be the first time a value is used, augmented assignment operators are invalid in def statements.

Function definition

A function definition statement creates a value of type Func and assigns it to a variable in the current scope.

A defition consists of a signature and a body.

func name(Type_Param1, Return_Type, arg1: Type1, arg2: Type_Param1, arg3: Type2) -> Return_Type {
    pass ;; statements
}

A signature is a comma-delimited list of any combination of:

Generally, the type parameters are not required. Type annotations may be omitted from the signature as well, resulting in the following minimal definition:

func name(arg1, arg2, arg3) {
    pass ;; statements
}

Nested function definition

A function definition may be nested, in which case the defined function is available as a local variable.

A nested function definition may not specify type parameters. Type annotations in a nested function definition may refer to type parameters specified in the top-level function definition.

A nested function closes over any local variables in scope at the point at which it is defined that it references.

The closure is logically equivalent to the following lambda lifting transformation:

func outer(a, b, c) {
    let a += b * c
    func inner(x) {
        return a + x
    }
    return inner
}

;; transforms to

func outer_inner(a, x) {
    return a + x
}

func outer(a, b, c) {
    let a += b * c
    let inner := outer_inner(a:, ...)
    return inner
}

Return statement

The return statement breaks out of any loop and immediately exits the function, returning the right-hand side of the expression.

return expr

All control paths in a function must terminate with a return statement. It is an error to have a statement immediately following a return.

Pass statement

A pass statement is a dummy statement. All blocks in Tenet are required to have at least one statement, so a pass statement may be injected to meet this requirement.

If statement

An if statement introduces an if/ else if/else chain. The if branch is mandatory, it may be followed by any number of else if, and may be terminated with else. The else branch is logically equivalent to an else if true branch.

Per the syntax, all statements introducing blocks require braces. else if is special syntax to allow a chain of conditionals to be expressed without excessive indentation.

Each condition must be an expression that evaluates to a Bool value, and the condition is evaluated at runtime.

Switch statement

The switch statement is used to simultaneously match and deconstruct a pattern and then execute code.

switch my_rec {
case (a: 5):  ;; Treated the same as (a: 5, b: *, c: *)
    x := #a_5;
case (a:, b: 3):  ;; Shorthand to assign a := my_rec.a
    x := b_three ~ a;
case (c: 7):
    x := #c_7;
case (b:):
    x := b_val ~ b;
}

Pattern matching is static in a switch statement. Because of this, a potential confusion would be to match against an existing variable. To prevent this, reusing existing names defined outside the switch statement is prohibited.

Further, there is no fallthrough between cases. A break inside a switch is an entirely valid way to exit a loop, though. To mitigate this confusion, error checking should warn the user that break is not required to prevent fallthrough:

Case patterns

A case pattern attempts to be syntactically similar to a literal constructor.

A name pattern is a value identifier. A name pattern will match any value in its place, and assigns it temporarily to that name within the case body.

An any pattern, an asterisk *, will match any value in its place and does not capture the value.

A boolean pattern uses the same syntax as a boolean literal, the keywors true and false.

An integer pattern uses the same syntax as an integer literal, except that the sign is not an operator but part of the pattern’s grammar. Example: -55

A string pattern is identical to a string literal. Example: "daisy"

A record pattern uses a similar syntax to a record constructor, except that the values must be patterns. As with a record constructor, the values may be omitted and will then be captured in like named variables. Example: (flower: "daisy", flange:) would match a record whose flower slot is the string "daisy", match any value for flange and assign it to the name flange.

Note that a record pattern is open; any slots not specified are assumed to be wildcard matches. Slots may not be specified if they are not part of the type of the variable being matched.

A union pattern uses a similar syntax to a union constructor, again with the variant being a pattern. Example: tag ~ variant would match the tag tag and assign the variant to the name variant.

As a convenience, the syntax #tag is equivalent to tag ~ ().

There is no way to match function values, but they can be extracted with name patterns.

Try statement

As user-defined functions in Tenet must be total, any statement containing a partial operation must be protected by a try/catch block or a guard expression.

A try statement introduces a block of statements to execute, and may be followed by a series of catch statements that will be executed if a partial operation does not return a value.

try {
   b := get_denominator(x);
   a := 1 // b;
   return ok ~ a;
} catch Div_By_Zero {
   return #fail;
}

A catch block may not be redundant. There is no exception hierarchy in Tenet, so this means a catch block may not repeat an exception listed in a prior catch. A catch block may not catch an exception can’t be raised.

try {
   return ok ~ x[y];
} catch Out_Of_Bounds {
   return #fail_bounds;
} catch Out_Of_Bounds {
   return #fail_how;      ;; ERROR
} catch Div_By_Zero {
   return #fail_div_zero; ;; ERROR
}

For statement

A for loop iterates through the contents of an iterable container, assigning each value in turn to the iterator name and executing its body. The iterator name is only visible within the body of the foor loop.

for iter in iterable_expr {
   iter *= 2  ;; OK, but this does not affect iterable_expr
   sum += iter
   if sum > 50 {
       break
   }
}
return (iter:, sum:)  ;; ERROR: iter is not visible

As Tenet functions must be convergent, there is no while statement, nor is there a mechanism to restart iteration.

Because iteration may be arbitrarily nested and range over arbitrary functions, convergence doesn’t guarantee that functions will complete in a reasonable amount of time.

Within the body of a for loop, the iteration control statements may be used.

Break statement

The break statement terminates the closest enclosing for loop. It is an error to have any other statement immediately after a break statement.

Continue statement

The continue statement exits the body of the closest enclosing for loop and continues the next iteration.