Expressions in Tenet
Tenet expressions are mostly familiar but with a few distinctive features, particularly around deconstruction and escaping.
Basic Operators
Tenet supports the usual arithmetic, comparison, and logical operators:
- Arithmetic:
+,-,*,/(floor division),%(modulo) - Comparison:
==,eq,!=,ne,lt,le,gt,ge - Logical:
not,and,or - Concatenation and set union:
++(for lists, strings, sets and maps)
What’s uncommon
Floor division instead of truncating. A common use case for modulo is to implement a hashtable, and users are surprised that the sign of the modulo is the sign of the numerator.
Keyword relational operators. We need the angle brackets; we swear it’s for a good cause.
Keyword logical operators. Keywords fit nicely with if and while.
Concatenation isn’t plus. It helps catch errors.
Compound assignment operators
All the arithmetic operators have compound assignment, but ++= is particularly useful, and and= and or= are short-circuiting.
Deconstruction Operators
Tenet provides two main parametric deconstruction operators that extract parts of composite values.
Record Field Access .
The dot operator . extracts a field from a record. The field name is a compile-time parameter.
do {
let p = (x: 3, y: 4);
print-line("x is " ++ int-to-str(p.x));
print-line("y is " ++ int-to-str(p.y));
}
Union Variant Unpacking !
The unpacking operator ! unpacks a tagged value from a union. The tag is a compile-time
parameter.
If the value has the expected tag, the variant is returned. If the tag doesn’t match, the entire value escapes.
do {
let result: ok ~ Str | #miss = ok ~ "apple";
let fruit = result ! ok;
print-line("Fruit is " ++ fruit);
}
This is directly analogous to record access:
.field-name→ parametric on field name (for Records)!tag→ parametric on tag (for Unions)
Expression-level Pattern Matching ? and ?!
The when operators ? and ?! perform pattern matching inside a larger expression without
introducing a full when block. They have very similar syntax to when, except that, being a
statement, when can do nothing and move on to the next statement.
The ? operator performs pattern matching and is always exhaustive — all cases must be covered,
and all branches must produce a value with a common supertype.
do {
let desc = error ~ "Problem" ? {
ok ~ let v -> "success: " ++ v;
#miss -> "not found";
error ~ let e -> "failed with " ++ e
};
print-line(desc);
}
The escaping when operator ?! is similar but does not have to be exhaustive: any unmatched
cases are allowed to escape.
do {
let value = #to-the-moon ?! {
ok ~ v -> v
#miss -> 0
// other tags are allowed to escape
}
}
Function calls
Functions are invoked with named arguments, but the early arguments can be passed positionally in the same order as in the function definition:
fun process(data: Str, mode: #strict | #loose) {
print-line(
"Got data " ++ data ++ "; mode: " ++
(mode ? { #strict -> "strict"; #loose -> "loose" })
);
}
do {
process(data: "input", mode: #strict);
process("input", mode: #strict);
process("input", #strict);
}