Language reference

Tinoc docs

The syntax, semantics, and standard library of Tinoc, written alongside the compiler in the repository. Click a section to jump in.

Getting started

Tinoc is a systems programming language that compiles to C99. Write .tnc source, emit portable C, and build it with any C99 toolchain.

The compiler is under active development, so there is nothing to install yet. These docs describe the language as it is being built and track the repository.

Hello, world

main.tnc
#import std.io;

// Main function
fn main() void {
    var name str = "Prathmesh";
    const lang = "Tinoc";

    io.println("{s} is creator of {s} Programming Language!", name, lang);
}

What the compiler emits

main.tnc compiles to readable C99 with a small runtime header. str becomes a plain struct, and io.println becomes printf.

main.c — emitted
#include <stdio.h>
#include <tinoc.h>

int main() {
    str name = {"Prathmesh", 9};
    const str lang = {"Tinoc", 5};

    printf("%s is creator of %s Programming Language!\n", name.data, lang.data);
}

The pipeline is tinoc build main.tnc, and main.c goes to your C compiler. There is no runtime, VM, or garbage collector.

Variables

A variable can be mutated.

Syntax

var <identifier> <type>; // Decl only, with explicit type. var <identifier> <type> = <expression>; // Decl + init, explicit type. var <identifier> = <expression>; // Decl + init, inferred.

Example

main.tnc
fn main() void {
    var name str = "Lucifer";
    name = "Julius";

    var number = 101;        // inferred as i32
    number += 10;

    var isAdmin = false;     // inferred as bool
    isAdmin = true;
}

Constants

A constant cannot be mutated.

Syntax

const <identifier> <type>; // Decl only, with explicit type. const <identifier> <type> = <expression>; // Decl + init, explicit type. const <identifier> = <expression>; // Decl + init, inferred.

Example

main.tnc
fn main() void {
    const lang str = "Tinoc";
    const codename = "C^";

    // lang = "Tinoc Is Not C"
    // Won't work — compile error.
}

Integers

Integer literals

literals.tnc
const decimal_int = 98222;
const hex_int = 0xff;
const another_hex_int = 0xFF;
const octal_int = 0o755;
const binary_int = 0b11110000;

// underscores may sit between two digits as a visual separator
const one_billion = 1_000_000_000;
const binary_mask = 0b1_1111_1111;
const permissions = 0o7_5_5;
const big_address = 0xFF80_0000_0000_0000;

The compiler infers the width of each integer literal. It defaults to i32 and widens to i64 or i128 when the value requires it.

For values not known at compile time, add the type explicitly:
divide.tnc
fn divide(a i32, b i32) i32 {
    return a / b;
}

Floats

Float literals

literals.tnc
const floating_point = 123.0E+77;
const another_float = 123.0;
const yet_another = 123.0e+77;
const hex_floating_point = 0x103.70p-5;
const another_hex_float = 0x103.70;
const yet_another_hex_float = 0x103.70P-5;

// underscores may sit between two digits as a visual separator
const lightspeed = 299_792_458.000_000;
const nanosecond = 0.000_000_001;
const more_hex = 0x1234_5678.9ABC_CDEFp-10;

Tinoc has no syntax for NaN, infinity, or negative infinity. Use std.math for these values.

math.tnc
#import std.math;

const inf = math.inf();
const negative_inf = - math.inf();
const nan = math.nan();

Operators

Operators, their syntax, and their behavior:

NameSyntaxNotes
Additiona + b · a += bIntegers & floats; can overflow
Wrapping Additiona +% b · a +%= bWraps on overflow
Saturating Additiona +| b · a +|= bClamps on overflow
Subtractiona - b · a -= bIntegers & floats
Wrapping Subtractiona -% b · a -%= bWraps on underflow
Saturating Subtractiona -| b · a -|= bClamps on underflow
Negation-aTwo's-complement negation
Wrapping Negation-%aWraps on overflow
Multiplicationa * b · a *= bIntegers & floats
Wrapping Multiplicationa *% b · a *%= bWraps on overflow
Saturating Multiplicationa *| b · a *|= bClamps on overflow
Divisiona / b · a /= bIntegers & floats
Remainder Divisiona % b · a %= bModulo
Bit Shift Lefta << b · a <<= bShift left
Saturating Bit Shift Lefta <<| b · a <<|= bClamps shifted-out bits
Bit Shift Righta >> b · a >>= bShift right
Bitwise Anda & b · a &= bBitwise AND
Bitwise Ora | b · a |= bBitwise OR
Bitwise Xora ^ b · a ^= bBitwise XOR
Bitwise Not~aBitwise complement
Defaulting Optional Unwrapa orelse bUse b when a is null
Optional Unwrapa?Unwrap, else compile error
Defaulting Error Unwrapa catch b · a catch |err| bHandle errors with a default
Logical Anda and bShort-circuit AND
Logical Ora or bShort-circuit OR
Logical Not!aNegate a boolean
Equalitya == bEqual
Null Checka == nullOptional is empty
Inequalitya != bNot equal
Non-Null Checka != nullOptional has a value
Greater Thana > bOrdering
Greater or Equala >= bOrdering
Less Thana < bOrdering
Lesser or Equala <= bOrdering
Pointer Dereferencea^Read through a pointer
Address Of&aTake a pointer
Error Set Mergea || bMerge error sets

Precedence

From highest to lowest binding:

x() x[] x.y x^ x? a!b x{} !x -x -%x ~x &x ?x * / % ** *% *| || + - ++ +% -% +| -| << >> <<| & ^ | orelse catch == != < > <= >= and or = *= *%= *|= /= %= += +%= +|= -= -%= -|= <<= <<|= >>= &= ^= |=

Functions

A function is a reusable block of code. Tinoc supports plain functions and functions with one or more generic parameters.

Syntax

fn <identifier>() <type> {…} fn <identifier>(<param> <type>, …) <type> {…} fn <identifier>:T() <type> {…} fn <identifier>:T(<param> <type>, …) <type> {…} fn <identifier>:(T, …)() <type> {…} fn <identifier>:(T, …)(<param> <type>, …) <type> {…}

Calling

<identifier>(); <identifier>(value, …); <identifier>:T(); // and the same for the other forms…

Example

main.tnc
#import std.io;

fn add(a i8, b i8) i8 {
    return a + b;
}

fn greet() void {
    io.println("Hi");
}

fn Identity:T(val T) T {
    return val;
}

fn main() void {
    const result = add(10, 25);
    greet();
    io.println("result = {d}", result);
    io.println("{any}", Identity:str("Tinoc"));
}

Types

Tinoc's type system covers primitives, compounds, pointer, optional, and error types, plus heap-allocated collections.

Integer types

TinocC equivalentCategory
u8uint8_tUnsigned integer
u16uint16_tUnsigned integer
u32uint32_tUnsigned integer
u64uint64_tUnsigned integer
u128__uint128_t (GCC/Clang)Unsigned integer
usizesize_tUnsigned, platform-width
i8int8_tSigned integer
i16int16_tSigned integer
i32int32_tSigned integer
i64int64_tSigned integer
i128__int128_t (GCC/Clang)Signed integer
isizeptrdiff_tSigned, platform-width

Floating-point types

TinocC equivalentCategory
f32floatFloat
f64doubleFloat
f128__float128 (GCC/Clang)Float

Core primitives

TinocC equivalentCategory
bool_Bool / stdbool.hBoolean
charuint32_tUnicode codepoint
voidvoidVoid

String

str has no direct C primitive. It is a plain struct:

TinocC equivalentCategory
strstruct { const char* data; size_t len; }String (struct)

Arrays & slices

TinocC equivalentCategory
[N]TT arr[N]Fixed-size array
[_]TT arr[N] (size inferred)Inferred-size array
[]Tstruct { T* ptr; size_t len; }Slice (fat pointer)

Pointer

TinocC equivalentCategory
^TT*Pointer

Optional

An optional is a nullable wrapper, not a raw pointer:

TinocC equivalentCategory
?Tstruct { T value; bool has_value; }Optional

Error unions

Errors are first-class values. A result carries the value or the error:

TinocC equivalentCategory
!Tstruct { T value; int err; bool is_err; }Inferred error union
E!Tstruct { T value; E err; bool is_err; }Explicit error union

Compounds

TinocC equivalentCategory
structstructCompound
enumenum + intCompound
unionunionCompound

Heap collections (std.collections)

Generic library types with no single C equivalent; they use :T and :(K, V) syntax:

TinocC equivalentCategory
hstrNone — heap-managed strHeap string
vec:TNone — dynamic arrayHeap collection
map:(K, V)None — hash mapHeap collection
set:TNone — hash setHeap collection

Structs

A struct is a compound type with fields, methods, and static methods.

Syntax

struct <identifier> { <field> <type>; … fn <identifier>(self ^<type-of-struct>, …) <type> {…} static fn <identifier>() <type> {…} static fn <identifier>(<param> <type>, …) <type> {…} }

Example

main.tnc
#import std.io;

// simple struct with method
struct Point {
    x f32;
    y f32;

    fn translate(self ^Point, dx f32, dy f32) void {
        self^.x += dx;
        self^.y += dy;
    }

    fn length(self ^Point) f32 {
        return sqrt(self^.x * self^.x + self^.y * self^.y);
    }
}

// generic struct with method
struct Pair:T {
    first T;
    second T;

    fn swap(self ^Pair:T) void {
        var tmp T = self^.first;
        self^.first = self^.second;
        self^.second = tmp;
    }
}

// multi-param generic struct
struct Map:(K, V) {
    key K;
    value V;
}

fn main() void {
    var p Point = Point { .x = 1.0, .y = 2.0 };
    p.translate(0.5, 1.5);

    var pair Pair:i32 = Pair:i32 { .first = 10, .second = 20 };
    pair.swap();
    io.println("{any}", p);
}

Enums

Enums are first-class and can carry data, methods, and static methods.

Example

main.tnc
#import std.{io, collection.*};

enum Direction {
    North, East, South, West,
}

// enum with data and method
enum Shape {
    Circle(f32),       // radius
    Rect(f32, f32),    // width, height
    Point,

    fn area(self ^Shape) f32 {
        // match on self variant
    }
}

enum Literal {
    String(hstr),
    Integer(usize),
    // …
    // enums can also have methods and static methods, like structs
}

enum Something:T {
    First,
    Second(T),
}

fn main() void {
    const direction1 = Direction.North;
    io.println("{any}", direction1);

    const literal1 = Literal.String(hstr.from("String Literal"));
    io.println("{any}", literal1); // prints: String("String Literal")

    var s Shape = Shape.Circle(5.0);
    var a f32 = s.area();
}

Unions

A union overlays multiple views over the same memory. Unions can have methods and generic parameters.

Syntax

union <Name> { … } // simple union with methods union <Name>:<T> { … } // generic union with methods union <Name>:(<T>, <U>, …) { … } // multi-param generic union

Example

main.tnc
// simple union with method
union Data {
    as_int i32;
    as_float f32;
    as_bytes [4]u8;

    fn zero(self ^Data) void {
        self^.as_int = 0;
    }
}

// generic union with method
union Either:T {
    value T;
    raw u64;

    fn clear(self ^Either:T) void {
        self^.raw = 0;
    }
}

// multi-param generic union
union OneOf:(A, B) {
    a A;
    b B;
}

fn main() void {
    var d Data;
    d.as_int = 42;
    d.zero();
    // d.as_float now reads the same memory as f32
}

Arrays

Array literals

arrays.tnc
// array literal
const message = ['h', 'e', 'l', 'l', 'o'];

// alternative initialization using a result location
const alt_message [5]u8 = ['h', 'e', 'l', 'l', 'o'];

Multidimensional arrays

matrix.tnc
const mat4x5 [4][5]f32 = [
    [1.0, 0.0, 0.0, 0.0, 0.0],
    [0.0, 1.0, 0.0, 1.0, 0.0],
    [0.0, 0.0, 1.0, 0.0, 0.0],
    [0.0, 0.0, 0.0, 1.0, 9.9],
];

Sentinel-terminated arrays

The syntax [N:x]T describes an array with a sentinel element of value x at the index corresponding to length N.

sentinel.tnc
#import std.testing.expectEqual;

test "0-terminated sentinel array" {
    const array [_:0]u8 = [1, 2, 3, 4];

    try expectEqual([4:0]u8, @TypeOf(array));
    try expectEqual(4, array.len);
    try expectEqual(0, array[4]);
}
Destructuring arrays is still being designed. Progress is tracked in the issue tracker.

Switch

switch matches on integer or enum values. There are no parentheses and no fallthrough.

Syntax

switch <expression> { <value> => { … } <value> => { … } _ => { … } // default case }

Example

main.tnc
enum TokenKind {
    Int,
    Ident,
    Plus,
    Eof,
}

fn main() void {
    var tok TokenKind = TokenKind.Plus;

    switch tok {
        TokenKind.Int   => { /* handle int */ }
        TokenKind.Ident => { /* handle ident */ }
        TokenKind.Plus  => { /* handle plus */ }
        _               => { /* default */ }
    }

    var x i32 = 2;

    switch x {
        1 => { /* one */ }
        2 => { /* two */ }
        _ => { /* anything else */ }
    }
}
  • No parentheses around the expression.
  • _ is the default case.
  • No fallthrough; each arm is independent.
  • Braces are required per arm.

If / Else

Conditional branching with no parentheses around the condition.

Syntax

if <condition> { … } // simple if if <condition> { … } else { … } // if / else if <condition> { … } else if <condition> { … } else { … } // chain

Example

main.tnc
fn main() void {
    var x i32 = 10;

    if x > 0 {
        // positive
    }

    if x > 0 {
        // positive
    } else {
        // zero or negative
    }

    if x > 0 {
        // positive
    } else if x == 0 {
        // zero
    } else {
        // negative
    }
}
  • No parentheses around the condition.
  • Braces are required; there is no single-line braceless form.

For

Iterate over ranges and collections with a capture binding.

Syntax

for <start>..<end> |<i>| { … } // range, capturing index for <collection> |<item>| { … } // collection, capturing item

Example

main.tnc
fn main() void {
    // range loop — 0 to 9
    for 0..10 |i| {
        // i goes 0, 1, 2, … 9
    }

    // range with a step is done with while, if needed
    var i i32 = 0;
    while i < 10 {
        i += 2;
    }

    // iterate over a slice
    var nums [5]i32 = [1, 2, 3, 4, 5];
    for nums |n| {
        // n is each element
    }
}
  • No parentheses around the range or collection.
  • |i| captures the loop variable. It is a binding, not a closure.
  • Range 0..10 is exclusive on the right: it iterates 0 through 9.
  • break exits; continue skips to the next iteration.

While

Condition-based loops. while true covers both the infinite loop and do-while patterns.

Syntax

while <condition> { … } // loop while condition is true while true { … } // infinite loop

Example

main.tnc
fn main() void {
    var i i32 = 0;

    // normal while
    while i < 10 {
        i += 1;
    }

    // infinite loop — break to exit
    while true {
        if i == 20 {
            break;
        }
        i += 1;
    }

    // do-while equivalent — run the body first, check at the end
    while true {
        i += 1;
        if i >= 5 {
            break;
        }
    }
}
  • No parentheses around the condition.
  • Braces are required.
  • while true replaces both infinite loops and do-while patterns from C.
  • break exits; continue skips to the next iteration.

Modules

#import resolves modules at compile time, not by text insertion like #include.

Syntax

#import <module.path>; // import a specific module #import <module.path>.*; // import all public exports

Example

util.tnc
pub fn add(a i32, b i32) i32 {
    return a + b;
}
main.tnc
#import std.io;
#import util;
#import std.collections.*;

fn main() void {
    // std.io symbols accessed via the module name
    io.println("Hello, Tinoc");

    // std.collections.* symbols available directly
    var v vec:i32;

    const result = util.add(125, 2022);
    io.println("{any}", result);
}
  • #import replaces C's #include. It resolves modules semantically instead of inserting file text.
  • .* imports all public exports from a module into the current scope.
  • pub marks a function, struct, enum, union, const, etc. as public for import.
  • Without .*, symbols are accessed via the last path segment (e.g. std.ioio.println).

Preprocessor

Directives are marked with a # prefix and run at compile time.

DirectivePurpose
#importCompile-time module resolution. Pass the main file (e.g. main.tnc) and the compiler handles the rest.
#runCompile-time execution as an expression or block. Useful for metaprogramming.
#partialTells the compiler about a partial implementation of a switch on an enum.
More directives may be added in future versions.