T Language Tutorial

The first layer of the Tyre language ecosystem

T is a C-level language where types are the core abstraction — that's what the name stands for. A type name simultaneously serves as field name, constructor, and enum variant. Newtypes, product types (&), and sum types (|) are enough to express everything C structs and unions can. T has two syntaxes: SLN (.sln.t) and C (.c.t), both compiling to the same AST.

Hello World

print "Hello, world!"
print("Hello, world!");

No fn main needed. Top-level statements are automatically wrapped into main. print outputs its arguments separated by spaces, followed by a newline. It works with all types.

Build and run:

cargo run -- run examples/tutorial/hello.sln.t
cargo run -- run examples/tutorial/hello.c.t
print 42
print 1 2 3
print true false
print 3.14
print "hello" "world"
print(42);
print(1, 2, 3);
print(true, false);
print(3.14);
print("hello", "world");

Output:

42
1 2 3
true false
3.14
hello world

Bindings

T has three kinds of bindings:

BindingMutable?Addressable?Internal Type
letnonoT (value)
refnoyes&T (auto-deref)
varyesyes|T (auto-deref)
let x 42
ref r 10
var y 10
print x r y
set y 99
print y
let x = 42;
ref r = 10;
var y = 10;
print(x, r, y);
y := 99;
print(y);

Types are inferred. You can annotate explicitly when needed: let z: i32 = 100. Mutation uses := (C-syntax) or set (SLN). Both ref and var are internally pointers — when used in a value context (operators, print, return), they're automatically dereferenced. ref is addressable but immutable, var is addressable and mutable.

The Type System

Types are the core abstraction in T — a type name is simultaneously a field name, a constructor, and an enum variant. There are no traditional struct or enum keywords, no type aliases — just newtypes. Newtypes create both product types (tuples, like structs) and sum types (enums). Together with primitives, pointers, and arrays, this is enough for C-level programming.

Primitive Types

Booleans (bool), signed integers (i8i128), unsigned integers (u8u128), and floats (f32, f64).

Newtypes

A newtype creates a distinct type that wraps another type:

type Meters f64
type Seconds f64

let d (Meters 42.0)
let s (Seconds 1.5)
let doubled (+ d d)
print d
print s
print doubled
type Meters, Seconds = f64;

let d = Meters: 42.0;
let s = Seconds: 1.5;
let doubled = d + d;
print(d);
print(s);
print(doubled);

Meters and Seconds are different types, even though both wrap f64. You can't accidentally add meters to seconds. The type name doubles as a constructor: Meters: 42.0 (C) or (Meters 42.0) / Meters:42.0 (SLN). Newtypes preserve their type through arithmetic: d + d produces Meters, not f64.

SLN's : shorthand is right-associative: speed:meters:42.0 equals (speed (meters 42.0)). It works with atoms (variables, literals) — for complex expressions, use parentheses.

Conversion rules:

Tuples (Product Types)

A tuple combines multiple newtypes into a compound type. This is how T does structs:

type x f64
type y f64
type Vec2 (tuple x y)

type lhs Vec2
type rhs Vec2
fn vec2_add (lhs rhs) (lhs rhs) Vec2
    (Vec2 (+ lhs.x rhs.x) (+ lhs.y rhs.y))

let a (Vec2 1.0 2.0)
let b (Vec2 3.0 4.0)
let sum (vec2_add (lhs a) (rhs b))
print "a =" a.x a.y
print "b =" b.x b.y
print "a + b =" sum.x sum.y
type x, y = f64;
type Vec2 = x & y;

type lhs = Vec2;
type rhs = Vec2;
fn vec2_add(lhs, rhs) -> Vec2 {
    Vec2 { x: (Vec2: lhs).x + (Vec2: rhs).x, y: (Vec2: lhs).y + (Vec2: rhs).y }
}

let a = Vec2 { x: 1.0, y: 2.0 };
let b = Vec2 { x: 3.0, y: 4.0 };
let sum = vec2_add(lhs: a, rhs: b);
print("a =", a.x, a.y);
print("b =", b.x, b.y);
print("a + b =", sum.x, sum.y);

Fields are identified by their type name: a.x returns a value of type x, not f64. The constructor matches values to fields by type — Point: (px, py) sees that px has type x and py has type y, so the order doesn't matter.

These rules follow from types being field identifiers:

Enums (Sum Types)

While tuples are product types (a value has all fields), enums are sum types (a value is one of several variants):

type ok i32
type err i32
type result (enum ok err)

let a (result ok:42)
let b (result err:-1)

match a
    (ok value) (print "ok:" value)
    (err code) (print "err:" code)
match b
    (ok value) (print "ok:" value)
    (err code) (print "err:" code)
type ok, err = i32;
type result = ok | err;

let a = result: ok: 42;
let b = result: err: -1;

match a {
    ok(value) => print("ok:", value),
    err(code) => print("err:", code),
}
match b {
    ok(value) => print("ok:", value),
    err(code) => print("err:", code),
}

Same rules as tuples: all variants must be newtypes, no duplicates, at least 2 variants. Enums compile to C tagged unions.

Newtype Stacking

Newtypes can wrap other newtypes. Downcast is implicit at each level:

type x f32
type y f32
type point (tuple x y)
type direction point

let d (direction (point x:1.0 y:2.0))
let p point d
print "point:" p.x p.y
type x, y = f32;
type point = x & y;
type direction = point;

let d = direction: point { x: 1.0, y: 2.0 };
let p: point = d;
print("point:", p.x, p.y);

Types are simultaneously field names (in tuples) and variant tags (in enums). This is the unifying principle of T's type system.

Constants and Sizeof

type Size u64

const MAX_SIZE Size 1024

print "MAX_SIZE =" (cast MAX_SIZE u64)
print "sizeof(i32) =" (sizeof i32)
print "sizeof(f64) =" (sizeof f64)
type Size = u64;

const MAX_SIZE: Size = 1024;

print("MAX_SIZE =", u64: MAX_SIZE);
print("sizeof(i32) =", sizeof(i32));
print("sizeof(f64) =", sizeof(f64));

Use cast (SLN) or Type: value (C-syntax) for explicit type conversions.

Functions

fn twice (n) (i32) i32
    (* n 2)

type a i32
type b i32
fn add (a b) i32
    (+ a.i32 b.i32)

print (twice 21)
print (add (a 10) (b 32))
fn twice(n: i32) -> i32 {
    n * 2
}

type a, b = i32;
fn add(a, b) -> i32 {
    (i32: a) + (i32: b)
}

print(twice(21));
print(add(a: 10, b: 32));

SLN lists parameter names, then types, then return type. C-style uses name: type and -> return. Void functions omit the return type. The last expression in the body is the return value. Types and functions share a namespace — a name cannot be both a type and a function.

Function Pointers

Function pointers are called with call(f, x) (C) or (call f x) (SLN), not f(x). This avoids ambiguity with type construction:

fn twice (n) (i32) i32
    (* n 2)

fn apply (f x) ((fn (i32) i32) i32) i32
    (call f x)

print (apply twice 5)
fn twice(n: i32) -> i32 {
    n * 2
}

fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
    call(f, x)
}

print(apply(twice, 5));

Direct function calls (twice(5)) use normal syntax. Only function pointers need call.

Pointers

T has two pointer types:

The syntax is mnemonic: & (AND) suggests many can share access, | (OR) suggests one-at-a-time access.

fn increment (pointer) (|i32)
    set+ pointer 1

var x i32 10
print x
increment x
print x
fn increment(pointer: |i32) {
    pointer += 1;
}

var x: i32 = 10;
print(x);
increment(x);
print(x);

x and y are var bindings — they're automatically passed as mutable pointers. No &mut at the call site needed.

Auto-Deref

var and ref bindings are internally pointers, but T automatically dereferences them in value contexts: operators, print, return, assignment values, let initializers, and label/jump arguments. Field access preserves the pointer: if p is a |Point, then p.x is a |x.

Assignment

Assignment (set / :=) returns the old value of the target. The left side must reach a mutable pointer (|T) — shared pointers (&) are automatically dereferenced to get there. For example, &|T := y auto-derefs through & to assign through the |T. This enables swap and rotation:

var a i32 1
var b i32 2
var c i32 3
print a b c
set a b b a
print a b c
set a b c c a b
print a b c
var a: i32 = 1;
var b: i32 = 2;
var c: i32 = 3;
print(a, b, c);
a, b := b, a;
print(a, b, c);
a, b, c := c, a, b;
print(a, b, c);

Output:

1 2 3
2 1 3
1 3 2

Control Flow

If

if is both a statement and an expression:

let x 15
let big (> x 10)
print big
let abs (if (> x 0) x (- 0 x))
print abs
let x = 15;
let big = x > 10;
print(big);
let abs = if x > 0 { x } else { 0 - x };
print(abs);

Match

match compiles to a C switch statement. It works on integers and enum variants.

fn fizzbuzz (n) (i32)
    let by3 (% n 3)
    let by5 (% n 5)
    let by15 (% n 15)
    match by15
        0
            print "FizzBuzz"
        _
            match by3
                0
                    print "Fizz"
                _
                    match by5
                        0
                            print "Buzz"
                        _
                            print n

label loop (i) (1)
if (> i 30)
    return
fizzbuzz i
jump loop (+ i 1)
fn fizzbuzz(n: i32) {
    let by3 = n % 3;
    let by5 = n % 5;
    let by15 = n % 15;
    match by15 {
        0 => print("FizzBuzz"),
        _ => match by3 {
            0 => print("Fizz"),
            _ => match by5 {
                0 => print("Buzz"),
                _ => print(n),
            },
        },
    }
}

label l(i = 1);
if i > 30 { return; }
fizzbuzz(i);
jump l(i + 1);

For enums, match uses variant patterns that bind the inner value: ok(value) => ... (C) or (ok value) ... (SLN). The _ wildcard matches anything.

Label / Jump

label and jump are goto with parameters. A jump can go forward (to skip code) or backward (to loop). Labels can have parameters with initial values — types are inferred.

let debug false

if debug
    jump skip
print "debug is off"
label skip

label loop (i) (0)
if (>= i 3)
    return
print "i =" i
jump loop (+ i 1)
let debug = false;

if debug {
    jump skip();
}
print("debug is off");
label skip();

label loop(i = 0);
if i >= 3 { return; }
print("i =", i);
jump loop(i + 1);

The first jump skip skips a line when debug is true. The second part uses label loop with a parameter i to count from 0 to 2. jump arguments are evaluated in parallel, so swapping works naturally.

A more complex example — Fibonacci:

fn fibonacci (n) (i32) i64
    label loop (i a b) (i32 i64 i64) (0 0 1)
    if (>= i n)
        return a
    jump loop (+ i 1) b (+ a b)

print "fib(10) =" (fibonacci 10)
print "fib(20) =" (fibonacci 20)
fn fibonacci(n: i32) -> i64 {
    label loop(i: i32 = 0, a: i64 = 0, b: i64 = 1);
    if i >= n { return a; }
    jump loop(i + 1, b, a + b);
}

print("fib(10) =", fibonacci(10));
print("fib(20) =", fibonacci(20));

Defer

defer schedules a statement to run at the end of the enclosing block. Multiple defers execute in reverse order (LIFO):

(print 1)
(defer print 3)
(print 2)
print(1);
defer print(3);
print(2);

Output: 1 2 3 — the deferred print(3) runs after print(2), at the end of the block.

Arrays

type buffer (& i32)
type length i32
fn sum (buffer length) (buffer length) i32
    label loop (i total) (0 0)
    if (>= i (cast length i32))
        return total
    let ptr &i32 (cast buffer &i32)
    jump loop (+ i 1) (+ total ptr@i)

var numbers (array i32 5) (array 10 20 30 40 50)
print "numbers:" numbers@0 numbers@1 numbers@2 numbers@3 numbers@4
set numbers@2 99
print "after mutation:" numbers@0 numbers@1 numbers@2 numbers@3 numbers@4
print "sum =" (sum (buffer (cast numbers &i32)) (length 5))
type buffer = &i32;
type length = i32;
fn sum(buffer, length) -> i32 {
    label l(i = 0, total = 0);
    if i >= (i32: length) { return total; }
    jump l(i + 1, total + (buffer as &i32)[i]);
}

var numbers: [i32]5 = [10, 20, 30, 40, 50];
print("numbers:", numbers[0], numbers[1], numbers[2], numbers[3], numbers[4]);
numbers[2] := 99;
print("after mutation:", numbers[0], numbers[1], numbers[2], numbers[3], numbers[4]);
print("sum =", sum(buffer: numbers as &i32, length: 5));

Arrays can be cast to pointers for passing to functions. @ (SLN) and [] (C) are used for indexing.

Bitwise Operations

let a 170
let b 92
print "AND:" (& a b)
print "OR:" (| a b)
print "XOR:" (^ a b)
print "NOT:" (~ a)
print "SHL:" (<< a 2)
print "SHR:" (>> a 2)
let a = 170;
let b = 92;
print("AND:", a & b);
print("OR:", a | b);
print("XOR:", a ^ b);
print("NOT:", ~a);
print("SHL:", a << 2);
print("SHR:", a >> 2);

Vectors (SIMD)

Vectors are fixed-size SIMD types. Arithmetic operates on all elements in parallel.

let a (vector 1.0 2.0 3.0 4.0)
let b (vector 5.0 6.0 7.0 8.0)
let sum (+ a b)
let product (* a b)
print "a =" a@0 a@1 a@2 a@3
print "b =" b@0 b@1 b@2 b@3
print "a + b =" sum@0 sum@1 sum@2 sum@3
print "a * b =" product@0 product@1 product@2 product@3
let a = {1.0, 2.0, 3.0, 4.0};
let b = {5.0, 6.0, 7.0, 8.0};
let sum = a + b;
let product = a * b;
print("a =", a[0], a[1], a[2], a[3]);
print("b =", b[0], b[1], b[2], b[3]);
print("a + b =", sum[0], sum[1], sum[2], sum[3]);
print("a * b =", product[0], product[1], product[2], product[3]);

Vectors support +, -, *, / and indexing. They compile to GCC/Clang vector extensions.

Visibility

By default, all declarations are internal (file-local). In C terms, they get static linkage. The pub keyword makes a declaration visible to other translation units:

(pub type meters f64)
(pub const SPEED_OF_LIGHT meters meters:299792458.0)

(pub fn twice (meters) (meters) meters
    (* meters meters:2.0))

(print (twice SPEED_OF_LIGHT))
pub type meters = f64;
pub const SPEED_OF_LIGHT: meters = meters: 299792458.0;

pub fn twice(meters) -> meters {
    meters * meters: 2.0
}

print(twice(SPEED_OF_LIGHT));

Import & Extern

extern declares functions defined elsewhere. import includes a header file so that the external definitions are available to the compiler:

(import "stdlib.h")
(extern abs (i32) i32)

(let negative i32 -42)
(print (abs negative))
(print (abs -7))
import "stdlib.h";
extern fn abs(n: i32) -> i32;

let negative: i32 = -42;
print(abs(negative));
print(abs(-7));

import is needed when the extern declaration must match an existing header definition. Without import, the compiler may not see the original prototype.

extern can also be used standalone for builtins or functions linked separately:

extern printf (&u8 ...) i32

printf "%s = 0x%02X\n" "value" 42
extern fn printf(format: &u8, ...) -> i32;

printf("%s = 0x%02X\n", "value", 42);

The ... denotes variadic parameters. extern and import are the dual of pub: extern imports, pub exports.

What T Doesn't Have

T is deliberately minimal. These features are reserved for higher layers:

These restrictions keep T close to C in capability and performance, while the newtype system and auto-deref semantics make it safer and more expressive.