Skip to content

mocompute/tess

Repository files navigation

The Tess Language

A practical statically-typed language that builds on C.

Tess adds type inference, generics, and tagged unions to a C foundation. It transpiles to plain C: you keep your toolchain, your debugger, and your performance.

#module Point

Point[T]: { x: T, y: T }

add(p1, p2) {
    Point(x = p1.x + p2.x, y = p1.y + p2.y)
}

eq(p1, p2) {
    p1.x == p2.x && p1.y == p2.y
}

#module main
#import <Print.tl>

println = Print.println

main() {
    a := Point(x = 1.0, y = 2.5)
    b := Point(x = 3.0, y = 4.5)
    c := a + b
    println(f"({c.x:.1f}, {c.y:.1f})")

    if a != b { println("different") }
    0
}

add and eq have no type annotations: the compiler infers them. Point is generic: the same definition works for integers, floats, or any type with + and ==.

Features

Type inference. Write the function, the compiler determines the types.

factorial(n) {
    if n <= 1 { 1 }
    else { n * factorial(n - 1) }
}

Tagged unions. Define variants with associated data. Match exhaustively, bind a single variant, or propagate errors: the compiler catches unhandled results.

Shape: | Circle { radius: Float }
       | Square { length: Float }

area(s) {
    when s {
        c: Circle { c.radius * c.radius * 3.14 }
        s: Square { s.length * s.length }
    }
}

Or, just bind the happy path, and ignore the rest:

    s: Some := get_the_thing() else { return "oops" }
    use_it(s.value)
    // ...

For functions that return nothing on success but may return an error, use:

    file.write_str("hello") else err { log_error(err.error); return false }

Or, condition on a single variant:

    if sq: Square := the_shape() {
        log("we saw a square")
    }
    // ...

Or, propagate errors with try:

    file := try open_file()    // unwraps Ok, or returns Err

Generics. One definition, multiple types, no macros. Type parameters can be constrained by traits: checked at compile time, specialized to concrete types.

Summable[T]: Add[T], Eq[T] { }

sum[T: Summable](a, b) { a + b }

Receiver blocks. Group methods by their receiver instead of repeating it on every function. No impl keyword — just parentheses and braces. Call sites use dot syntax.

#module Stack

Stack[T]: { data: Array[T] }

(self: Ptr[Stack[T]]): {
    push(x: T) -> Void { self->data.push(x) }
    pop()      -> T    { self->data.pop() }
}

C interop. Include a C header and call its functions directly. Export Tess functions back to C: the compiler generates the .h for you.

#include <math.h>
c_sqrt(x: CDouble) -> CDouble

result := c_sqrt(2.0)
[[c_export]] add(x: CInt, y: CInt) { x + y }
tess lib mylib.tl    # produces libmylib.so + libmylib.h

Conversely, generate Tess bindings from a C header automatically:

tess cbind sqlite3.h

Error handling. Result and Option replace error codes and null checks. try unwraps the success case or returns the error: no goto chains.

read_config(path) {
    content := try read_file(path)
    config  := try parse(content)
    Ok(config)
}

Tess also has closures, defer, operator overloading, function overloading, dot-call syntax, iterators, conditional compilation, and a package manager.

Build

make -j              # Build the compiler (~5 seconds)
make -j test         # Run tests (~30 seconds)
make install         # Install to /usr/local (see docs/BUILD.md)

The tess executable can be run from anywhere. It bundles its standard library, so it doesn't need a system-wide installation.

Quick Start

Run a single file: no project setup needed:

echo '#module main
#import <Print.tl>
main() { Print.println("Hello, world!"), 0 }' > hello.tl
tess run hello.tl

For multi-file projects:

tess init            # creates package.tl, src/main.tl
tess run             # compile and execute
tess exe -o myapp    # standalone binary

Documentation

Examples

The examples directory contains complete projects demonstrating C interop, command-line handling, and shared/static libraries.

Status

Currently at version 0.1.0, there are several known limitations:

  • The standard library is bare and its API is subject to breaking change.
  • The compiler operates as a whole-program compiler. Stress tests indicate that peak memory usage increases substantially with large numbers of specialized functions. For example, specializing a generic function to 8,000 unique parameter types requires more than 4GB of memory to compile. Other than stress tests, the compiler has not been tested on large scale programs.
  • The compiler is single-threaded. Compiling a program with 10,000 functions takes about five seconds, and compilation of larger stress tests grows super-linearly. For comparison, the Tess compiler itself is less than 1,500 functions of plain C.

License

Apache License 2.0

About

A practical statically-typed language that builds on C.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors