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 ==.
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 ErrGenerics. 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.hConversely, generate Tess bindings from a C header automatically:
tess cbind sqlite3.hError 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.
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.
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.tlFor multi-file projects:
tess init # creates package.tl, src/main.tl
tess run # compile and execute
tess exe -o myapp # standalone binary- Language Reference: complete syntax guide
- Language Model: scoping, closures, and semantics
- Type System: how type inference and specialization work
- Standard Library: Array, String, HashMap, and more
- Packages: creating and consuming
.tpkglibraries - Building: build configurations, installation, and standalone binary
- Glossary: definitions of Tess-specific terms
- Tutorials: step-by-step guides for language features
- Changelog: release notes and version history
The examples directory contains complete projects demonstrating C interop, command-line handling, and shared/static libraries.
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.