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
- More powerful pattern matching on the left-hand side of assignments
- indexed iteration over lists
- iteration over map entries