Types

Fundamental Types

Name Description
s8 Signed 8-bit number.
s16 Signed 16-bit number.
s32 Signed 32-bit number.
s64 Signed 64-bit number.
u8 Unsigned 8-bit number.
u16 Unsigned 16-bit number.
u32 Unsigned 32-bit number.
u64 Unsigned 64-bit number.
usize Unsigned 64-bit size.
bool Boolean. (true/false)
f32 32-bit floating point number.
f64 64-bit floating point number.
type Type of any other type.

Pointer Type

Represents the address of some allocated data.

#import "std/test"

pointers :: fn () #test {
    i := 666;
    i_ptr : *s32 = &i; // taking the address of 'i' variable and set 'i_ptr'
    j := @i_ptr;       // pointer dereferencing

    test_true(j == i);
};

Array Type

The array is an aggregate type of multiple values of the same type. Size value must be known in compile-time (unless it's not explicitly marked for automatic element count detection based on content of compound expression). Arrays can be inline initialized with compound block; type is required. Zero initializers can be used for zero initializations of whole array storage, otherwise, we must specify a value for every element in an array.

array_type :: fn () #test {
    arr1 : [10] s32; // declare zero initialized array variable
    arr1[0] = 666;

    arr1.len; // yields array element count (s64)
    arr1.ptr; // yields pointer to first element '&arr[0]'

    // inline initialization of array type
    arr2 := [10].s32{};            // Initialize all elements to 0.
    arr3 := [4]s32.{ 1, 2, 3, 4 }; // Initialize array to the sequence 1, 2, 3, 4
    arr4 := [_]s32.{ 1, 2, 3, 4 ]; // The same as previous one but the legth of the array is set automatically.
};

Arrays can be implicitly converted to slices:

array_to_slice :: fn () #test {
    arr : [10] s32;
    slice : []s32 = arr;
};

String Type

String type in Biscuit aka string_view is a slice containing a pointer to string data and string length. String literals are always zero-terminated. The string_view represents a string of fixed length. In case you want a dynamically allocated string use string type and its associated methods. Values of string can be implicitly converted to string_view.

string_type :: fn () #test {
    msg : string_view = "Hello world\n";
    msg.len; // character count of the string
    msg.ptr; // pointer to the string content
};

Slice

The array slice consists of the array length and pointer to the first array element.

Slice layout:

Slice :: struct {
    len: s64;
    ptr: *T
};
array_slice :: fn () #test {
    arr :: [4]s32.{1, 2, 3, 4};
    slice : []s32 = arr;
    loop i := 0; i < slice.len; i += 1 {
        print("%\n", slice[i]);
    }
};

Note

alloc_slice can be used to allocate a slice on the heap.

Struct Type

The structure is a composite type representing a group of data as a single type. The structure is as an array another way to define a user data type, but types of structure members could be different. It can be used in situations when it's better to group data into one unit instead of interacting with separate units.

A structure can be declared with the use of struct keyword.

Person :: struct {
    id: s32;
    name: string_view;
    age: s32;
}

Structure Person in the example consists of id, name and age. Now we can create a variable of this type and fill it with data. To access a person's member fields use . operator.

main :: fn () s32 {
    my_person: Person; // Create instance of type Person
    my_person.id = 1;
    my_person.age = 20;
    my_person.name = "Martin";

    return 0;
}

Inline initialization is also possible. We can use a compound expression to set all members at once.

main :: fn () s32 {
    // Set all data in person to 0
    my_person1 := Person.{};

    // Initialize all members.
    my_person2 := Person.{ 1, "Martin", 20 };

    // We can explicitly name the members we want to initialize.
    my_person3 := Person.{ id = 1, name = "Martin", age = 20 };

    // We can change the order.
    my_person4 := Person.{ name = "Martin", age = 20, id = 1 };

    // Or initialize only something. In such a case the rest is initialized to 0 by default.
    my_person5 := Person.{ name = "Martin" };

    return 0;
}

Structure content can be printed by print function.

main :: fn () s32 {
    my_person := Person.{ 1, "Martin", 20 };
    print("%\n", my_person);

    return 0;
}
Person {id = 1, name = Martin, age = 20}

Due to lacking OOP support, we cannot declare member functions in structures and there is no class or object concept in the language. A common way to manipulate data is by passing them into the function as an argument.

person_add_age :: fn (person: *Person, add: s32) {
    // Age can be modified even if the 'person' argument is immutable.
    person.age += add;
}

A structure can extend any type with the use of #base <T>. This is a kind of inheritance (similar to the C style) where inheritance can be simulated by composition. The #base <T> inserts base: T; as the first member into the structure. The compiler can use this information later to provide more inheritance-related features like merging of scopes to enable direct access to base-type members via . operator or implicit cast from a child to a parent pointer type.

Example of structure extension:

Entity :: struct {
    id: s32
}

// Player has base type Entity
Player :: struct #base Entity {
    // base: Entity; is implicitly inserted as first member
    name: string_view;
};

Wall :: struct #base Entity {
    height: s32
};

Enemy :: struct #base Entity {
    health: s32
};

// Multi-level extension Boss -> Enemy -> Entity
Boss :: struct #base Enemy {
    // Extended struct can be empty.
};

struct_extending :: fn () #test {
    p: Player;
    p.id = 10; // direct access to base-type members
    p.name = "Travis";
    assert(p.base.id == 10); // access via .base

    w: Wall;
    w.id = 11;
    w.height = 666;

    e: Enemy;
    e.id = 12;
    e.health = 100;

    b: Boss;
    b.id = 13;

    // implicit down cast to entity
    update(&p);
    update(&w);
    update(&e);
    update(&b);
}

update :: fn (e: *Entity) {
    print("id = %\n", e.id);
}

Union Type

The union is a special composite type representing the value of multiple types. Union size is always equal to the size of the biggest member type and the memory offset of all members is the same. Union is usually associated with some enum providing information about the stored type.

Token :: union {
    as_string: string_view;
    as_int: s32;
}

Kind :: enum {
    String;
    Int;
}

test_union :: fn () #test {
    token1: Token;
    token2: Token;

    // Token has total size of the biggest member.
    assert(sizeof(token1 == sizeof(string_view));

    token1.as_string = "This is string";
    consumer(&token, Kind.String);

    token2.as_int = 666;
    consumer(&token, Kind.Int);
}

consumer :: fn (token: *Token, kind: TokenKind) {
    switch kind {
        Kind.String { print("%\n", token.as_string); }
        Kind.Int    { print("%\n", token.as_int); }
        default { panic(); }
    }
}

Any Type

Any type is a special builtin structure containing the pointer to TypeInfo and to the data. Any value can be implicitly casted to this type while passed into a function.

Any :: struct #compiler {
    type_info: *TypeInfo;
    data: *u8
};

The Any value should never own the original data!

Implicit conversion to Any type may cause stack allocation of a temporary variable on the call side in case the original value does not represent stack or heap-allocated memory. This may cause a hidden overhead in some cases.

...
foo(10); // temp for '10' is created here
...

foo :: fn (v: Any) {}

For types converted to Any compiler implicitly sets type_info field to a pointer to the TypeType type-info and the data field to the pointer to the actual type-info of the converted type.

...
foo(s32); // Type passed
...

foo :: fn (v: Any) {
    assert(v.type_info.kind == TypeKind.TYPE);

    data_info := cast(*TypeInfo) v.data;
    assert(data_info.kind == TypeKind.INT);
}

Any can be combined with vargs; a good example of this use case is a print function where args argument type is vargs of Any (... is the same as ...Any). The print function can take values of any type passed as args.

print :: fn (format: string_view, args: ...) {
    ...
};

Enum Type

The enum represents an integer type with a limited set of possible named values. The underlying integer type can be explicitly specified after enum keyword, otherwise s32 is used implicitly. Each possible variant lives in the enum namespace.

Color :: enum {
    RED;    // default value 0
    GREEN;  // default value 1
    BLUE    // default value 2
};

// Enum declaration (base type is u8)
Day :: enum u8 {
    SAT = 1; // first value explicitly set to 1
    SUN;     // implicitly set to previous value + 1 -> 2
    MON;     // 3
    TUE;     // ...
    WED;
    THU;
    FRI
};

EntityKind :: enum {
    // All values explicitly defined.
    PLAYER = 10;
    ENEMY  = 32;
    NPC    = 78;
}

main :: fn () s32 {
    // Print types:
    print("Color      = %\n", Color);
    print("Day        = %\n", Day);
    print("EntityKind = %\n", EntityKind);

    day := Day.MON;
    print("day = %\n", day);

    // We can use 'enum_count' helper function to get count
    // of all possible values.
    day_count :: enum_count(Day);
    print("We have % possible days.\n", day_count);

    // The 'using' can expose all possible variants of enum
    // into the current scope.
    using Day;
    switch day {
        MON { print("It's Monday!\n"); }
        FRI { print("It's Friday!\n"); }
        default { print("It's some other day!\n") ; }
    }

    return 0;
}

Nested Enum Type

An enumerator can be nested in any struct in case we want to explicitly associate the enumeration with some data type.

Fruit :: struct {
    kind: enum {
        APPLE;
        ORANGE;
        BANANA;
    };
}

main :: fn () s32 {
    // Access the nested anonymous enum type in the structure.
    kind: Fruit.kind;

    kind = Fruit.kind.APPLE;

    // We can use 'using'.
    using Fruit.kind;

    switch kind {
        APPLE  { print("Apple!\n");  }
        ORANGE { print("Orange!\n"); }
        BANANA { print("Banana!\n"); }
    }

    return 0;
}

Enum Flags Type

An enumerator can be used as a definition of bit flags by adding #flags directive to the type definition. This directive slightly changes the way how the enumerator values are generated. By default, the enumerator starts with zero variant (if it's not explicitly changed by the programmer) and every following enumerator variant has a value set to the previous one plus one. The flags enumerator starts with the first variant set to 1 and the following variants are set to the left-bit-shifted value of the previous one.

Enumerators marked as flags are also serialized as a combination of atomic flags instead of just one value.

OpenMode :: enum #flags {
    READ;   // 1
    WRITE;  // 2
    APPEND; // 4
    CREATE; // 8
    WRITE_APPEND = WRITE | APPEND; // Combination of multiple variants.
    WRITE_CREATE = WRITE | CREATE;
}

main :: fn () s32 {
    mode: OpenMode; // Set to OpenMode.Read by default
    print("mode = %\n", mode);

    //
    // Set the flag
    //

    // Set mode to combination of WRITE nad CREATE flags.
    mode = OpenMode.WRITE_CREATE;
    print("mode = %\n", mode);

    // Note that we can use the | (or) operator on the flags enumerators. To toggle one additional
    // flag like this:
    mode |= OpenMode.APPEND;
    print("mode = %\n", mode);

    // Reset the mode back to READ only.
    mode = OpenMode.READ;
    print("mode = %\n", mode);

    // To set more flags at once, we can use the combination of multiple flags like this:
    mode = OpenMode.WRITE | OpenMode.CREATE | OpenMode.APPEND;
    print("mode = %\n", mode);


    //
    // Check flags
    //

    if (mode & OpenMode.WRITE) == OpenMode.WRITE {
        print("Set!\n");
    } else {
        print("Not set!\n");
    }

    // We can use helper function for this.
    if is_flag(mode, OpenMode.WRITE) {
        print("Set!\n");
    } else {
        print("Not set!\n");
    }


    // Check the combination. Evaluates true in case WRITE or CREATE is set.
    if is_flag(mode, OpenMode.WRITE | OpenMode.CREATE) {
        print("Set!\n");
    } else {
        print("Not set!\n");
    }

    //
    // Clear flags
    //

    mode &= ~OpenMode.WRITE;
    print("mode = %\n", mode);

    // Now check the WRITE flag again...
    if is_flag(mode, OpenMode.WRITE) {
        print("Set!\n");
    } else {
        print("Not set!\n");
    }

    return 0;
}

So in comparison to conventional enumerators, bunch of binary operations are allowed for flags enumerators to easily manipulate (set, clear and check) its values. Namely:

Symbol Description
& Intersection.
| Union.
~ Bit flip.
&= Assign intersection.
|= Assign union.

Note

Since flags enumerators start implicitly with value 1, you can explicitly define NoFlag = 0; variant at the beginning of the variant list.

Note

Flags enumerators should use an unsigned number type as a base type (u32 by default).

Note

It's possible to do an implicit casting of flags enumerators to its base type.

Type Casting

Change the type of value to the other type. Conversions between integer types, from pointer to bool and from array to slice are generated implicitly by the compiler.

main :: fn () s32 {
    // Default type of integer literal is 's32'.
    i := 666;

    // Type of the integer literal is changed to u64.
    j : u16 = 666;

    // Implicit cast on function call.
    fn (num: u64) {
    } (j);

    // Explicit cast of 'f32' type to 's32'.
    l := 1.5f;
    m := cast(s32) l;
    return 0;
};

Type casting rules in BL are more strict compared to C or C++, there are no void pointers or implicit conversions between integers and enums etc. Despite this fact, an explicit cast can be in some cases replaced by auto cast. The auto cast operator does not need explicit destination type notation, it will automatically detect destination type based on expression if possible. When an auto operator cannot detect the type, it will keep an expression's type untouched. In such a case auto does not generate any instructions into the final binary.

main :: fn () s32 {
    s32_ptr : *s32;
    u32_ptr : *u32;

    // auto cast from *u32 to *s32
    s32_ptr = auto u32_ptr;

    // keep expession type s32
    i := auto 10;
    return 0;
};

Type Decomposition

Type decomposition can be used on the composite types to get a type of any of the nested members.

Person :: struct {
    name: string_view;
    age: s32;
}

main :: fn () s32 {
    name: Person.name; // string_view type
    age: Person.age; // s32 type
    return 0;
}

This can be extremely useful when generic structures are used in polymorphic functions and we don't know internal member types in advance.

MyContainer :: fn (TValue: type) type #comptime {
    return struct {
        value: TValue;
    };
}

// Return type is type of TContainer member value.
get_value :: fn (container: *?TContainer) *TContainer.value {
    return &container.value;
}

main :: fn () s32 {
    container: MyContainer(u64);
    value :: get_value(&container);
    return 0;
}

Pointer type dereference is also possible.

Person :: struct {
    name: string_view;
    age: s32;
    parent: *Person;
}

main :: fn () s32 {
    parent_by_value: @Person.parent; // Person type.
    return 0;
}