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 transpiles to C11. Write .tnc source, emit portable C, and build it with any C11 toolchain.

Tinoc respects C and its usage. It is built on the philosophy that programming should be Meaningful, Accurate, Robust, Maximum Performance, and Simple — C11 unlocks features like _Generic dispatch in the tinoc.h runtime (saturating arithmetic) and gives the compiler room to grow (error unions, typed enums, generics) without fighting the target language.

v0.1.0 is out. Install the latest release binary into ~/.tinoc — no root needed — with the one-line installers on the install page.

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 transpiles to readable C11 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.

Language support

What the compiler supports end-to-end in the current release:

FeatureStatusNotes
var / const (incl. static)ShippedExplicit or inferred types; const-mutability enforced
fn functionsShippedForward calls, argument/return type checking, missing-return checks
struct + methodsShippedFields, struct literals, instance/static methods, by-value copy/params/returns
enum + methodsShippedFieldless variants and tagged unions with payloads; exhaustive switch with pattern binding
switchShippedEnum variants (exhaustive, no _ needed), integer/char literals, _ default arm; pattern-binding arms
PointersShipped^T, &x, x^, pointers to structs/enums/unions, self^.x → self->x
Control flowShippedif / else if / else, while, for 0..10 |i|, break, continue
Literals & operatorsShippedInteger (all bases, _ separators), float, string, char, bool; arithmetic/comparison/logical/bitwise; content-based str ==/!=
C interopShipped#importc "header.h" as alias; (clang/gcc parsing) and extern "C" fn declarations
Modules & #importShippedmodule keyword, pub visibility, all import forms, import caching + cycle detection, single merged C output with mangled names
Generic functions / structs / aliasesShippedfn foo:T, struct Pair:T, alias Opt:T = ?T; — monomorphized per concrete type-argument set, across module boundaries
union + methodsShippedC-style shared-memory fields, instance/static methods, generic unions
Arrays / slicesShipped[N]T, [_]T, []T, [N:x]T sentinel arrays, array literals, indexing, .len, for coll |x|, implicit array → slice
OptionalsShipped?T, null, orelse, x?, == null / != null; plain values auto-wrap where a ?T is expected
Error unionsPlanned!T, catch — planned
Standard library (std.io, …)PlannedUser modules are in; the std library ships when the language matures

See the changelog for what landed in each version.

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 special 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; result type is T
Optional Unwrapa?Read the payload — check a != null first
Defaulting Error Unwrapa catch b · a catch |err| bHandle errors with a default — planned
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
Array Multiplicationa ** bArray repetition
Pointer Dereferencea^Read through a pointer
Address Of&aTake a pointer
Error Set Mergea || bMerge error sets — planned

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. The compiler emits one named typedef per optional type used (?i32 → tnc_opt_i32), so every declaration of the same optional type shares one C struct type:

TinocC equivalentCategory
?Tstruct { T value; bool has_value; }Optional (nullable wrapper)

Error unions

Planned Errors will be first-class values. A result carries the value or the error. The representation is documented here ahead of implementation:

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

Optionals

An optional wraps a payload of type T with a has_value flag: a ?T is either empty (null) or holds a T.

Syntax

var maybe ?i32 = 42; // wrap a payload var none ?i32 = null; // the empty optional maybe orelse 0 // payload when some, else the fallback (result type is T) maybe? // unwrap: the payload (only after a != null check) maybe == null // presence checks maybe != null

Example

optionals.tnc
fn index_of(hay []i32, needle i32) ?i32 {
    var i i32 = 0;
    while i < hay.len {
        if hay[i] == needle {
            return i; // plain i32 auto-wraps into ?i32
        }
        i += 1;
    }
    return null;
}

fn main() void {
    var maybe ?i32 = 42;
    var none  ?i32 = null;

    if none == null {
        var v = maybe orelse 0; // 42 — v is an i32
        var u = maybe?;         // 42
    }
    var nums [4]i32 = [10, 20, 30, 40];
    var s []i32 = nums;
    var d = index_of(s, 99) orelse -1; // -1
}
  • A plain T value — or null — auto-wraps into a ?T wherever one is expected: initializers, call arguments, return values, and assignments. Untyped numeric literals adapt to the payload type (var x ?f64 = 5;).
  • orelse short-circuits: the fallback is only evaluated when the optional is null. Its result type is the payload type T.
  • maybe? reads the payload; unwrapping an empty optional reads the wrapped value field (garbage, not a crash) — always check maybe != null first.
  • Comparing an optional with anything but null is rejected — use == null / != null. orelse requires an optional on the left and ? unwrap requires an optional. Optionals of arrays or void are rejected (use ?[]T for a nullable collection view).

Error unions

Planned Error unions (!T, catch) are next on the roadmap. User modules are in today; the standard library ships when the language matures.

When they land, a result will carry either the value or the error, handled with a catch b or a catch |err| b. The planned C representation is a tagged struct:

result_T — planned representation
typedef struct {
    T value;
    E err;      // error code/enum
    bool is_err;
} result_T;

Track progress in the issue tracker.

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();
}
  • Fieldless variants compile to plain C enums; variants with payloads become tagged unions (tag + per-variant anonymous struct), so multi-field payloads never overlap.
  • switch over an enum is exhaustive — list every variant and no _ arm is needed; pattern binding (Shape.Rect(w, h) =>) binds payload fields directly.
  • Enums support instance methods (self ^Name), static methods, equality, and being passed/returned like any other type.

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
}
  • C-style shared memory: writing as_int and reading as_float reinterprets the same bytes — IEEE-754 punning works end-to-end.
  • Unions reuse struct field syntax and support instance methods (self ^Name) and static methods.
  • Comparisons and arithmetic on unions are rejected with clear diagnostics.

Arrays & slices

An array is a fixed-size, contiguous sequence of elements of type T. Its length is part of the type and known at compile time.

Syntax

[<length>]<T> // fixed-size array, e.g. [5]u8 [_]<T> // size inferred from the initializer, e.g. [_]f32 [<length>:<sentinel>]<T> // sentinel-terminated array, e.g. [_:0]u8 [<elem>, <elem>, …] // array literal; element type is inferred

Example

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

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

// explicit element type, size inferred from the literal
var floats [_]f32 = [1.5, 2.5, 3.5];

// element access and assignment
if message[0] != 'h' { ... }
message[4] = 'x';

// .len is the element count (compile-time constant for arrays)
if message.len != 5 { ... }

// iterate over the elements
var total i32 = 0;
for message |ch| {
    total += ch;
}

Slices

A slice []T is a fat pointer — a { ptr: ^T, len: usize } pair that views a contiguous region of memory, typically an array.

slices.tnc
var nums [5]i32 = [1, 2, 3, 4, 5];
var s []i32 = nums;   // array converts implicitly to a slice view

if s.len != 5 { ... } // runtime length
s[2] = 30;            // writes through to the backing array

fn total(s []i32) i32 { ... }  // functions take slices, not arrays
  • Arrays convert implicitly to slices at call sites and in assignments; the slice aliases the array's storage.
  • Functions cannot take or return array values — the compiler rejects them and points you to slices: array parameter a is not supported ([3]i32) — use a slice ([]i32) instead.
  • Assigning a whole array is rejected for the same reason; copy element-wise instead. Iterate with the collection form: for s |v| { ... }.
  • Slice types are emitted as named typedefs (tnc_slice_i32) so prototypes and definitions share one C type.

Multidimensional arrays

Nested [N]T types build row-major matrices: [4][5]f32 is 4 rows of 5 f32. A row (mat[i]) is itself an array, so iterate row-by-row:

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],
];

if mat4x5[2][3] != 0.0 { ... }

var total f32 = 0.0;
for 0..mat4x5.len |i| {
    for mat4x5[i] |cell| {
        total += cell;
    }
}
  • .len on a multidimensional array is the number of rows.
  • The compiler rejects iterating a multidimensional array directly (its element is itself an array) — iterate a row instead.

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. [N:x]T stores N + 1 elements (the sentinel sits at index N), matching C string conventions.

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.
  • Enum switches are exhaustive: list every variant and no _ arm is needed. Tagged-union payloads bind with patterns — Shape.Rect(w, h) => { ... }.

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 semantically, replacing C #include: modules are parsed and type-checked once, only pub exports are visible to importers, and every loaded module compiles into a single merged C translation unit — no text insertion, no header guards.

The module keyword

module name; names the file's module. A file without a declaration gets its module name from its file stem (vec.tnc → module vec), and convention files mod.tnc / index.tnc take their containing directory's name (shapes/mod.tnc → module shapes) — directories become modules automatically.

math.tnc
module math;   // this file is the module `math`; importers say: #import math;

pub const PI f64 = 3.14159;
const TAU f64 = 6.28318;   // private: only visible inside this module

pub fn square(x f64) f64 {
    return x * x;
}

module name { ... } groups declarations into an in-file namespace — members are reachable as name.member from the rest of the file. Blocks nest (module a { module b { ... } } → a.b.member) and accept dotted names (module a.b { ... }).

physics.tnc
module physics {
    pub const g f64 = 9.81;

    pub fn energy(m f64, v f64) f64 {
        return 0.5 * m * v * v;
    }
}

fn main() void {
    // physics.energy / physics.g usable here
}

Import forms

#import module; // namespace import -> module.symbol #import module.sub; // nested path / submodule #import module.*; // wildcard: every pub symbol, usable bare #import module.symbol; // single symbol, usable directly #import module.{a, b as c}; // selected symbols, per-symbol aliases #import module as alias; // rename the namespace #import "rel/path.tnc" as alias; // file import (module name from file)

Whether a bare dotted tail (math.PI) is a submodule or a single symbol is decided by the module loader: it tries a module file first (math/PI.tnc, math/PI/mod.tnc), then a pub symbol of the prefix module (math). The {...} and .* forms are unambiguous. module.symbol as name renames a single-symbol import; #import module as m; renames the namespace.

Example

shapes/vec.tnc — the submodule shapes.vec
pub struct Vec2 {
    x f32;
    y f32;
}

pub fn dot(a Vec2, b Vec2) f32 {
    return a.x * b.x + a.y * b.y;
}
main.tnc
#import math;
#import math.PI;                 // single symbol -> PI
#import math.E as EULER;         // single symbol, renamed
#import shapes.vec;              // namespace -> vec.Vec2 / vec.dot
#import shapes.vec.{Vec2, dot};  // selected symbols, usable directly
#import "shapes/vec.tnc" as vf;  // file import under an alias
#import shapes.*;                // wildcard -> circumference(...)

Visibility

pub marks a function, struct, enum, union, const, var, or alias as exported from its module; everything else is private and rejected when imported — symbol x is private to module math for a private member, module math has no public symbol x for a missing one. In-file module name { ... } blocks expose every member (they are the same file's namespace).

Modules & generics

Generic declarations (fn name:T, struct Name:T, alias Name:T = ...;, and the multi-param :(K, V) forms) cross module boundaries like any other pub symbol:

#import math; // qualified use: math.identity:i32(42) #import math.identity; // bare use: identity:i32(42) or inferred identity(7) #import math.identity as id; // renamed: id:str("hi") #import math.{Pair}; // bare struct template: Pair:f64 { .first = 1.0, ... } #import box.Opt; // bare alias template: Opt:i32 #import math.*; // wildcard binds every pub generic bare

A bare instantiation and a qualified call of the same generic produce one shared mangled C instance (identity:i32(42) and math.identity:i32(42) emit a single tnc_math_identity_i32), so an imported generic is never duplicated. Each concrete instantiation is monomorphized once per type-argument set, and generic bodies are checked in the defining module's scope — module-private helpers and consts resolve exactly as the template author wrote them.

Module resolution rules

  • Imports resolve relative to the importing file's directory: #import a.b.c; looks for a/b/c.tnc, then a/b/c/mod.tnc, then a/b/c/c.tnc.
  • Modules load recursively (imports of imports) with cycle detection (import cycle detected: a -> b -> a) and are cached by absolute path — diamond imports share one instance.
  • Re-importing the same module under the same local name is idempotent; binding a different module under a taken name is an error.
  • Two files claiming the same module name collide (module math is already defined by ...), since their items would mangle to the same C symbols.
  • The standard library is not available yet: #import std.io; is rejected with standard library modules are not yet available (std.io) — user modules only for now.
  • Every module's code is merged into one C translation unit; module items get mangled C names (math.abs → tnc_math_abs) so same-named items across modules never collide.

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 loads, checks, and merges every transitively imported module.
#importcC header import. #importc "stdio.h" "math.h" as c; parses real C headers (clang's JSON AST, gcc's -aux-info fallback) and exposes their functions, extern variables, enum constants, typedefs, and simple macros under the alias with full type checking. See C interop.
extern "C" fnHand-declared C function declarations (no header parsing). See C interop.
#runCompile-time execution as an expression or block. Useful for metaprogramming.
#partialTells the compiler about a partial implementation of a switch on an enum.
Other useful preprocessor directives may be added in future versions.

C interop

Tinoc talks to C two ways: #importc parses real headers, extern "C" fn declares functions by hand. Both give you type-checked calls into libc and any other C library.

#importc — import C headers

#importc "stdio.h"; // alias defaults to the file stem: stdio #importc "stdio.h" "math.h" as c; // multiple headers, explicit alias #importc "mylib.h" as mylib; // local header next to the source

The compiler parses each header and registers every function, extern variable, enum constant, typedef, and object-like macro under the alias:

cio.tnc
#importc "stdio.h" as cio;

fn main() void {
    cio.printf("%.1f\n", 3.14);   // full argument type/count checking
    var code i32 = cio.EOF;        // -1 (macro constant)
}

Codegen emits one #include per header into the merged output, so the declarations resolve at C compile time. Type safety is enforced at the Tinoc level first — unknown members (undefined: cio.doesNotExist) and wrong argument counts/types are caught before C ever runs.

extern "C" fn — hand-declared C functions

extern "C" fn printf(fmt *const char, ...) i32; extern "C" fn strlen(s *const char) usize; extern "C" fn my_puts.puts(s *const char) i32; // call my_puts, C symbol puts

Declarations must end with ; (no body); variadic declarations need at least one named parameter before .... str arguments are unwrapped to their underlying char* automatically, so printf("%s\n", lang) passes the string's data pointer.