Types

What a teko program can name: the scalars it inherits from the mc core, the seven words teko adds on top of them, and the two declarations that build a type of your own — struct and class.

Every example on this page is a whole program, compiled and run by ../../scripts/check-docs.sh; the exit code is the assertion.


Scalars #

The core's own eight words are teko's (the core language § 2):

typewidthsignednotes
u81noa byte
u162no
u324no
u648no
i324yesC's int; the type an extern returning int declares
i648yesthe working integer
uptr8nothe only pointer: opaque, no pointee, byte arithmetic
void——a return type, and nothing else

teko adds seven names of its own. Each one is an alias: a new word for a representation the core already has, so mixing an alias with its base needs no cast and costs no instruction.

wordisnotes
boolu8the width of a declaration; true/false are the literals
byteu8
charu32a scalar code point; 'a', '\n' are its literals
isizei64index/size, architecture-dependent — 64-bit today
usizeu64the unsigned half of the same
ptruptrteko draws no signed/unsigned pointer distinction: ptr and uptr are one type
struptra NUL-terminated string, by pointer; there is no separate string object

f32 and f64 come from the <float> library the compiler loads, with their own literals (2.0, 0.5f) and the intrinsics ldf64/ldf32/stf64/stf32/sqrt_f64/ fabs/fmin/fmax. i8 and i16 (below, § "i8 and i16") are neither of the eight core words nor an alias of one: they are two primitives of teko's own, C#'s sbyte and short.

A type word is reserved program-wide: i64 str = 1; is refused, exactly as C# refuses a variable named int.

// expect-exit: 42
isize sum_isize(isize a, i64 b) {
    return a + b;
}

i64 main() {
    byte  b = 20;
    char  c = 12;
    usize u = (usize) b + (usize) c;
    isize n = sum_isize(10, 0);
    return (i64) u + (i64) n;
}

Casts, width and sign #

A cast is C-shaped and unambiguous, (u32) x, because a type word always follows the (. Narrowing masks; widening is by the type's own kind — zero for u8/u16/u32, sign for i32, i8 and i16. Arithmetic is 64-bit on the widened value and wraps at the next store. A comparison yields i64 0 or 1.

teko does not teach C#'s integer promotion (sbyte + sbyte is an int in C#): (i8) 100 + (i8) 100 is 200 while it is still in flight — an ordinary 64-bit value — and only narrows to -56 at the next store into an i8 slot, the same rule u8/u16/u32 already had. Teaching promotion for the two newest types while the three older ones kept the old rule would be the real inconsistency, and it would change the base grammar (the + in a + b is mc's own, D3).

i8 and i16 #

C#'s sbyte and short: signed, one and two bytes wide. Unlike the seven aliases above, each is its own primitive — type_new("i8", 1, 1, TK_SINT) and type_new("i16", 2, 2, TK_SINT) — because mc's own TK_SINT kind already does the sign-extending load, the sign-extending cast, a signed /, % and >>, a signed comparison and a narrowed call result for a type registered with it; nothing here re-teaches any of that.

// expect-exit: 42
i16 clamp16(i32 v) {
    if (v > 32767) return 32767;
    if (v < 0 - 32768) return 0 - 32768;
    return (i16) v;
}

i64 main() {
    i8  a = 0 - 5;
    i16 b = 0 - 300;
    if (a >> 1 != 0 - 3) return 1;               // an arithmetic shift, signed
    if (b / 2 != 0 - 150) return 2;               // a signed divide
    if (a >= 0) return 3;                         // a signed comparison
    i64 wide = a;                                 // widens with its sign
    if (wide != 0 - 5) return 4;
    f64 f = a;                                    // an integer converts to a float (above)
    if (f != 0.0 - 5.0) return 5;
    if (clamp16(70000) != 32767) return 6;        // saturates, narrows explicitly
    return 42;
}

i8/i16 convert to a wider integer or to f64/f32 the same way i64 already does (the section above; what does NOT convert is an f32 into an f64 slot or an f64 into an f32 one — the width gap not-yet.md lists, where the bytes are reinterpreted and the value is wrong — so keep an f32 in f32 slots); nothing converts back without a cast, and a float, null, or a class/struct/interface/delegate/T[] value does not convert INTO an i8/i16 slot either, with the same wording every other mismatched value already gets.

// no-run
i64 main() {
    i8 a = 1;
    i8 b = 2.5;                   // teko: a value of type f64 does not convert to i8
    i8 c = null;                  // teko: a value of type uptr does not convert to i8
    return 0;
}

sbyte and short are not taught: teko never inherited C#'s int/long either, and registering half of the C# alias family while int does not exist would be worse than neither half (not-yet.md).

bool, true and false #

true and false are integer literals 1 and 0, and both words are reserved. bool sizes a declaration (one byte); a truth value in flight is an i64, so if (x == 42) and bool ok = x == 42; are both ordinary.

// expect-exit: 42
bool is_the_answer(i64 x) {
    return x == 42;
}

i64 main() {
    bool ok = is_the_answer(42);
    if (ok == false) return 1;
    if (true == false) return 2;
    return 42;
}

f32 and f64 #

Float arithmetic is the <float> library's; the bit pattern of an f64 is reached through the runtime pair tk_f64_bits/tk_f64_from_bits (runtime.md), because a cast between a float and an integer converts the value, never the bits.

// expect-exit: 42
#include "rt.tk"

i64 main() {
    f64 a = 40.0;
    f64 b = 2.0;
    u64 bits = tk_f64_bits(a + b);
    f64 back = tk_f64_from_bits(bits);
    return (i64) back;
}

An integer converts to a float; a float does not convert back. That is C#'s own direction, and it holds in every slot: a variable initializer, an assignment, a return, an argument (of a free function, a method, a virtual call, an interface call), an element of a params f64[], a field store, and a binary mixing the two — where the integer operand is converted whichever side it stands on, so 1 + 2.5 and 2.5 + 1 are both three point five. The conversion is a cast the compiler writes for you, and it rounds the way C#'s long to double does: an f64 carries 53 bits of precision, so every integer up to 2^53 in magnitude arrives exact and a larger one lands on the nearest representable double.

// expect-exit: 42
#include "rt.tk"

f64 twice(f64 x) { return x + x; }

i64 main() {
    f64 y = 1;                                   // an initializer
    y = 5;                                       // an assignment
    if (twice(3) != 6.0) return 1;               // an argument
    if (1 + 2.5 != 3.5) return 2;                // the integer on the left
    if (2.5 + 1 != 3.5) return 3;                // ...and on the right
    return (i64) (y + 37);
}

The other direction is refused where it is written, because C# narrows nothing without a cast spelled out. It reads teko: a value of type f64 does not convert to i64 (diagnostics.md) — and (i64) x is the cast that says you meant it.

// no-run
i64 main() {
    i64 n = 2.5;                                 // teko: a value of type f64 does not convert to i64
    return n;
}

null is a reference and a number is not, so it does not land in a numeric slot either — teko: a value of type uptr does not convert to i64, uptr being the type null carries.

// no-run
i64 solo(i64 a) { return a; }

i64 main() {
    return solo(null);                           // teko: a value of type uptr does not convert to i64
}

Neither does a NON-null reference. A struct, a class, an interface, a delegate and a T[] of heap all carry a row of the type table, and none of them is a number — a class value reaching an i64/f64 slot used to pass its pointer through as a bit pattern, silently, in every one of the same slots listed above (D34). It is refused the same way, by the value's own type name:

// no-run
class Foo { public i64 v; }

i64 main() {
    Foo f = new Foo();
    i64 n = f;                                   // teko: a value of type Foo does not convert to i64
    return n;
}

A raw uptr/ptr value is left alone in a numeric slot — the core converts it to an integer of its own accord, and only a value the type table has a row for (or the null literal above) is judged here.

Two floats of different widths do not convert to each other yet: an f32 in an f64 slot is neither converted nor refused (not-yet.md).

ptr, uptr and str #

Memory is read and written by explicit width — ld8/ld16/ld32/ld64 and st8/st16/st32/st64 — and &x is the address of a local, a global or a function. p + 1 is one byte further: a pointer has no pointee to scale by. str is that same pointer with a NUL at the end, so tk_str_len and tk_str_slice (a view, zero copy) are ordinary functions over it.

// expect-exit: 42
#include "rt.tk"

u8 tbl[8];

ptr at(uptr base, i64 i) {
    return base + i;
}

i64 main() {
    st64(tbl, 40);
    ptr p = at(tbl, 0);
    str s = "hi";
    return ld64(p) + tk_str_len(s);
}

struct #

A struct is a block of fields with no header of its own: the first field sits at offset 0. A value of struct type is a pointer to the allocation, eight bytes wide, produced by new; new Name and new Name() both hand out zeroed bytes, so a field nobody assigned reads as 0 and a reference field reads as null.

struct Name { [modifier] type field; ... methods ... }

Fields are laid out in declaration order at their natural alignment. Every offset, and the total size, is published as a compile-time constant: NAME_FIELD (both parts uppercased) and NAME_SIZE.

// expect-exit: 42
#include "rt.tk"

struct Point {
    public u8  tag;
    public i64 x;
    public u32 y;
}

struct Line {
    public Point a;
    public i64   len;
}

i64 main() {
    if (POINT_TAG != 0) return 1;
    if (POINT_X != 8) return 2;                  // natural alignment, not offset 1
    if (POINT_Y != 16) return 3;
    if (POINT_SIZE != 24) return 4;

    Point p = new Point;
    if (p.x != 0) return 5;                      // the allocation is zeroed
    p.tag = 2;
    p.x = 30;
    p.y = 10;

    Line l = new Line();
    l.a = p;
    l.len = 0;
    return l.a.x + l.a.y + l.a.tag;              // a struct field chains
}

A struct declares methods, with the same implicit receiver, default arguments and overloads a class method has (classes.md). What it does not have is a vtable: virtual, override and use of a trait are refused on a struct by name, and a struct is never a base class.

A struct is not reference-counted — it has no vtable, hence no release function to reach. Its allocation lives for the run (memory.md).


class #

A class is a struct with two words in front of it: word 0 is the vtable pointer and word 1 the reference count, so its own fields start at offset 16, after the base class's.

[public|internal] [abstract] [partial] class Name [: Base] [, Iface...] { members }

NAME_FIELD/NAME_SIZE are published the same way, and they measure the object — a static field is a global of its own and takes none of it.

// expect-exit: 42
#include "rt.tk"

class Shape {
    public i64 side;

    public i64 twice() {
        return side * 2;                         // `side` is `this.side`
    }
}

class Square : Shape {
    public i64 tag;
}

i64 main() {
    if (SHAPE_SIDE != 16) return 1;              // the vtable and the count come first
    if (SHAPE_SIZE != 24) return 2;
    if (SQUARE_TAG != 24) return 3;              // the base's field keeps its place
    if (SQUARE_SIZE != 32) return 4;

    Shape p = new Shape;
    p.side = 21;
    return p.twice();
}

A class value is a reference. Assigning one to another name copies the reference and the object is released when the last reference to it dies (memory.md); everything a class can carry — inheritance, virtual, interfaces, traits, properties, operators, constructors and destructors — is classes.md.


Members #

The same member grammar serves a struct and a class.

memberwritten
fieldpublic i64 side;
methodpublic i64 area() { ... }
constructorpublic Name(i64 x) { ... } (a class only)
destructor~Name() { ... } (a class only)
propertypublic i64 Side { get; set; }
constantpublic const i64 MAX = 4;
operatorpublic static Vec operator+(Vec a, Vec b)

Modifiers #

Modifiers come before the type, in any order, each at most once.

modifieronmeans
publicmember, top-level typereachable from anywhere
privatememberthe declaring type only — the default for a member
protectedmemberthe declaring type and the types derived from it
internaltop-level typethis project only — the default for a type
staticmemberno receiver; reached through the type
constmembera compile-time constant; no slot in the object
virtual override abstractmethod, propertythe vtable slot (classes.md)

public and internal are reserved words, because they open a top-level declaration. private, protected, static, virtual, override, abstract and operator are contextual: they mean something inside a type body and are ordinary names outside it.

internal names the project: a declaration belongs to it when the file it was read from lives inside the directory of the build config. An absolute path, a path that climbs out (../elsewhere.tk) and a bundled #include <name> are all outside.

static #

A static field is one global named Type_field, so it takes no byte of the object; a static method takes no receiver. Both are reached through the type, never through an instance.

// expect-exit: 42
#include "rt.tk"

struct Point {
    public i64 x;

    public static i64 made;

    public static i64 tally() {
        return made;                             // the bare name, no receiver
    }
}

i64 main() {
    if (POINT_SIZE != 8) return 1;               // the static field is not in the object
    if (Point.made != 0) return 2;
    Point.made = 41;
    Point.made = Point.made + 1;
    return Point.tally();
}

const #

A member const has no slot either: it is a folded constant, read as Type.MAX from outside and by its bare name inside. A top-level const is the same thing without an owner, and it is what an array size or a const generic argument may name.

// expect-exit: 42
#include "rt.tk"

const i64 BASE = 30;
const i64 SIZE = 4;

i64 arr[SIZE];

class Counter {
    public const i64 MAX = 4;
    private const i64 STEP = 2;

    public i64 sum() {
        return MAX + STEP;                       // bare, from inside
    }
}

i64 main() {
    if (BASE != 30) return 1;
    arr[3] = 4;                                  // `SIZE` sized the global array
    if (arr[3] != 4) return 2;
    if (Counter.MAX != 4) return 3;              // through the type, from outside
    Counter c = new Counter;
    return BASE + c.sum() + arr[3] + 2;          // 30 + 6 + 4 + 2
}

There is no local const: declare it at the top or as a member.


Limits #

limitvalue
there is no type inside a typenested classes and structs are refused
a struct is never a base class: Base names a class
a struct has no reference countits allocation lives for the run
str has no length fieldtk_str_len walks to the NUL
ptr and uptr are the same typean overload cannot tell them apart
types declared in one source32
fields, summed across all types256

What a v0.4.0 program cannot write at all, and the message it gets, is not-yet.md.

Edit this page