The Tenet Language

Tenet is an early-binding but dynamic feeling language that aims to be easily embeddable to provide safe and consistent scripting. Tenet aims to address some complexities involved in embedding by being translatable to host languages.

Technical ideas

We’ll start with some concrete ideas in Tenet, and move on to some intangibles.

Type system

Values in Tenet are unified (no primitive / reference distinction) and have a well-defined equality. Notably, Tenet doesn’t have function values.

Types are algebraic and structural. Tenet’s union type is tagged unions, and the product type is records, along with atoms (strings, integers) and containers.

Mutability respects referential integrity. Any variable can be mutable, and references to mutable names can be shared safely. Operations like iteration over lists are guaranteed to be safe.

Control flow

Structured programming and concurrency guarantee that when a function ends, any references it borrowed are released.

Powerful control flow as control can jump out of block arguments or inner functions, and anything, including expressions, can have a label. Escaping (similar to throwing) is cheap and can be done within expressions.

Pattern matching is pervasive and recursive, and tagged unions are cheap.

Object orientation

Stators, coming soon, are the “guts” of an object, implemented as a hierarchical finite state machine, and can be stack allocated. Objects are stators that live on the heap and are managed by an object capability.

Capabilities, coming soon, are a kind of pseudo-value that help manage lifetimes and permissions. Capabilities are needed to allocate objects, perform IO, etc., and may themselves be managed by objects or passed as special arguments.

Design Rationale

The itch I was initially wanted to scratch was frustration at various anemic type systems. Working with languages like Haskell, they are a real joy and it’s fun bending your mind to work within the functional paradigm. That is until, one day,you realize that they solved the problems associated with mutation by forcing the user to implement it.

The problem of how to share values is fairly ubiquitous, but it points towards a deeper issue: how we establish a boundary between independent code units, and how that evolves over time. The solution that seems likely is we need to be able to translate Tenet to other languages, so that the other side of the boundary has consistent semantics.

I’d like Tenet to have a core feature set that’s fun and satisfyig to code in. For instance, the syntax tries to be very flexible, our guiding idea is that the programmer should be able to write what he means rather than trying to force everything into one true concept. Tenet rejects the idea that “everything is an object” or “everything is a function.”

By the same token, many of the semantics are very clearly defined, so a string is just a string, an integer is just an integer. The idea is that avoiding footguns and gotchas help the basics of Tenet feel solid and reliable.

Who Should Use Tenet?

At this point, people curious about a brand new language. Speculating as to how Tenet’s development will unfold, an early target will be Tenet’s own development tools. It should be pretty good at writing CLIs and simple web applications.

Before long, there will be some good applications for embedding. The language should be pretty quick in an interpreter, and it’s got a very straightforward and correct value system that can cross a language boundary.

This will make it ideal for embedding in an application, or even in a database. To dogfood this, I’d like to use Tenet to implement some of the support utilities.

Key Features

Clear Data Modeling

Tenet has a structural typing system. That is, any record type (x: Int, y: Int) is the same type regardless of its name, but (x: Int, y: Int, z: Int) has no relation to the first. That said, #b | #c is a subtype of #a | #b | #c.

type Point = (x: Int, y: Int)

type Result = ok ~ Str | #err

type User = (
    id: Int,
    name: Str,
    email: address ~ Str | #none
)

Pervasive Pattern Matching

The values work with well-defined pattern matching that can take a number of forms: a when statement, a ? operator, and even simple pattern matching on assignment.

do {
    when point ~ (x: 33, y: -22) {
        point ~ (x: let x, y: let y) -> 
            print-line(
                "x: " ++ int-to-str(x) ++ 
                " y: " ++ int-to-str(y)
            );
        #err -> print-line("error!");
    }
}

Escaping Instead of Exceptions

Pattern matching and values are the basis of Tenet’s escaping mechanism. The basic idea is that certain commands cause a value to “escape” normal computation until it’s caught.

fun show-email(user: (id: Int, name: Str, email: address ~ Str | #none)): () {
    if ok (let email = user.email ! address) {
        print-line("User " ++ user.name ++ " has email " ++ email)
    } else {
        print-line("User " ++ user.name ++ " has no email")    
    } 
}

do {
    show-email(user: (id: 5, name: "Bob", email: address~"bob@bob.com"));
    show-email(user: (id: 6, name: "Fred", email: #none));
}

Either the email address is an address, which is known to be a string, or the computation escapes to ok which reports an escaping expression. Future development : a ?? operator will replace an escaping expression with a default value.

This is very similar to the obj.?field.?field ?? default, except we don’t have to rely on the type author picking convenient null values, we can pick any path through unions.

Likewise, the author of the values doesn’t have to decide that some results are “Exception” or “Error,” rather, the caller places the expected computation in the happy path, and can then catch unexpected data in a catch block.

Here, we’ll simulate this with a when statement that operates

fun load-user() => ok ~ Str | #not-found | #permission-denied {
   return ok ~ "Bob";
}

do {
    when load-user() {
        ok ~ let name -> print-line("Processing user " ++ name);
        #not-found -> print-line("User not found");
        #permission-denied -> print-line("Permission denied"); 
    }
}

Collections

At the present, Tenet supports the reliable lists, sets and maps.

Tenet’s semantics give us some guarantees:

let flavors = \[#apple, #banana, #kiwi]
let colors = set\[#red, #green, #blue]
let scores = \["alice": 95, "bob": 87]

do {
    print-line("Alice's score is " ++ int-to-str(scores["alice"]));
    print-line("Bob's score is " ++ int-to-str(scores["bob"]));
}

We are working on a more general collections methodology. There should be some compiler support to allow describing a collection and its methods in terms of the relational algebra, making it reasonable to construct something like a map with list values or a bidirectional map, as well as putting constraints on a collection.