LogoChemical Docs
Ctrl+K

Structs

Structs are the primary way to organize data in Chemical. They are contiguous blocks of memory.

Definition and Initialization

struct Point {

    var x : float

    var y : float

}



func demo() {

    var p = Point { x : 10.0, y : 20.0 }

}

When you use no initializer, Chemical doesn't zero out the variable, Chemical may generate an error in the future if you used it before initializing, for example

struct Point {

    var x : int

    var y : int

}

func demo() {

    var p : Point

    // ❌ runtime memory access exception, p hasn't been initialized

    // since p contains garbage data

    printf("%d, %d\n", p.x, p.y);

}

When you use brace initializer, you must initialize all the fields, otherwise compiler will produce an error

func demo() {

    // this will cause an error, you didn't initialize all the fields

    // the compiler will ask you to initialize the y field

    // ❌ Invalid, must initialize the field y

    var z = Point { x : 1.0 }

}

You can use default value for the field 'y' in the struct, compiler will no longer force you to initialize it

struct Point {

    var x : int

    var y : int = 99

}



func demo() {

    var p = Point { x : 10 }

    // p.y is 99 here

}

When the compiler can infer the type of the struct, you can use braces without the type to initialize it

struct Point { 

    var x : int = 10

    var y : int = 20 

}

func demo() {

    var p : Point = {}

    // p.x is 10 and p.y is 20

    

    var p2 : Point = { x : 20 }

    // p2.x is 20 and p2.y is also 20

}

Struct and Union members

structs can contain anonymous structs for complex layouts:

struct Storage {

    struct {

        var data : *char

        var length : size_t

    } heap;

    struct {

        var buffer : [16]char

        var length : uchar

    } sso;

}



var s : Storage

s.sso.buffer[0] = 'H'

s.sso.length = 1

Constructors

You can use constructors to initialize structs, which come in handy for structs that have a lot of fields. In chemical, constructors are just functions that return an object of the struct. You must however use the annotation @make for constructors. All constructors must have different names than their functions, otherwise conflicts will cause compilation errors. If there are two constructors that take the same number of arguments and types, the first constructor (that appears first at top) will be called.

struct Point {

    var x : int

    var y : int



    @make

    func make() {

        // you must return an object of Point

        return Point { x : 10, y : 20 }

    }

    

    @make

    func make2(value : int) {

        // constructors automatically have return type of the struct, therefore you don't need to mention it

        return { x : value, y : value }

    }



}



func demo() {

    // calls the first function, which has @make annotation, that can accept given arguments

    var p = Point()

    // p.x is 10, p.y is 20

    

    var p2 = Point(99)

    // p2.x is 99, p2.y is 99

}

Automatic Constructor Generation

The compiler will generate a constructor for you, as long as it can. There are these rules compiler follows 1 - Every member of the struct has a known default value 2 - Every inherited struct can be initialized via default values or has a default constructor If these are true, compiler generates a default constructor for you.

// will generate an automatic constructor

struct Auto {}



// will generate an automatic constructor

struct Auto2 { 

    var a : int = 10

    var b : int = 20

}



// ❌ will not generate an automatic constructor

// because default value for b is missing

struct Auto3 {

    var a : int = 10

    var b : int

}



// will generate an automatic constructor

struct Inherited {

    var a : int = 10

    var b : int = 20

}



// will generate an automatic constructor, that'll automatically initialize the Inherited struct

struct Auto4 : Inherited {



}



struct Inherited2 {

    var a : int

    var b : int

    @make

    func make() {

        return { a : 10, b : 20 }

    }

}



// will generate an automatic construcotr because Inherited2 has a default construcotr

struct Auto5 : Inherited2 {



}



// no default/automatic constructor, default value for b is missing

struct Inherited3 {

    var a : int = 10

    var b : int

}



// ❌ will not generate an automatic constructor

// because Inherited3 doesn't have a default constructor

struct Auto6 : Inherited3 {



}



func demo() {

    var a = Auto()

    var a2 = Auto2()

    var a4 = Auto4()

    var a5 = Auto5()

}

Nested Struct Initialization

When a struct has a default constructor (compiler generated or manual), It is called automatically when initializing.

// if you omit the default value for b, this would fail

struct DefaultValues {

    var a : int = 10

    var b : int = 20

}

struct Container {

    var d : DefaultValues

}

func demo() {

    var c = Container {}

    // c.d.a is 10 and c.d.b is 20

    

    // making it explicit

    var c2 = Container { d : DefaultValues() }

}

Since same syntax (brace initializer) is used in constructors, so no differences

struct Container {

    var inner : InnerStruct  // Has default constructor

    @make

    func make() {

        return {

            inner : InnerStruct()

        }

    }

    @make

    func make2() {

        return {} // compiler automatically calls InnerStruct's default constructor

    }

}

Methods

Methods can be inside structs, unions or variants, so we'll discuss them here once

struct Point {

    var a : int

    var b : int

 

    func sum(&self) : int {

        return a + b;

    }

}

func demo() {

    var a : Point { a : 10, b : 20 }

    var s = a.sum() // is 30

}

You need to have mutable implicit self argument, if you want to mutate the members of the structs

struct Point {

    var a : int

    var b : int

 

    // if you write &self, compiler will error out

    func set_both(&mut self, value : int) {

        a = value

        b = value

    }

}

func demo() {

    var a : Point { a : 10, b : 20 }

    a.set_both(0)

}

Struct Inheritance

Chemical supports single struct inheritance. A derived struct inherits all members and functions of its base.

struct Animal {

    var a : int

    var b : int

}



struct WalkingAnimal : Animal {

    var speed : int

}



struct Dog : WalkingAnimal {

    var c : int

    var d : int

}

Inheritance Initialization Syntax

When creating an instance of a derived struct, you must explicitly initialize all levels:

func demo() {

    var d = Dog {

        WalkingAnimal : WalkingAnimal {

            Animal : Animal { a : 30, b : 40 },

            speed : 90

        },

        c : 40,

        d : 40

    }

    

    // you can omit the types

    var d2 = Dog {

        // inherited struct can be initialized like a member can be

        WalkingAnimal : {

            Animal : { a : 30, b : 40 },

            speed : 90

        },

        c : 40,

        d : 40

    }

}

Accessing Inherited Members

You can access base struct members directly

func demo() {

    var d = Dog { WalkingAnimal { Animal : Animal { a : 30, b : 40 }, speed : 90 }, c : 40, d : 40 }

    d.a      // From Animal: 30

    d.b      // From Animal: 40

    d.speed  // From WalkingAnimal: 90

    d.c      // From Dog: 40

}

Methods on Inherited Structs

Methods defined on base structs work on derived structs:

struct Animal {

    var a : int

    var b : int

    

    func sum(&self) : int {

        return a + b

    }

}



struct Dog : Animal {

    var c : int

}



var dog = Dog { Animal : Animal { a : 5, b : 10 }, c : 20 }

dog.sum()  // Returns 15 (calls Animal.sum)

Passing Derived as Base

Derived structs can be passed to functions expecting base pointers:

func get_animal(animal : *Animal) : int {

    return animal.a + animal.b

}



var dog = Dog { ... }

get_animal(&dog)  // Works!

Destructors

You can define a destructor for a struct using the @delete annotation. This function will be called automatically when an instance goes out of scope or when destruct is called.

struct Resource {

    var raw_handle : *void

    

    @delete

    func destroy(&self) {

        unsafe {

             printf("Cleaning up resource...\n")

             close_handle(self.raw_handle)

        }

    }

}

Destructor Lifecycle

Understanding when @delete is called is critical for managing resources.

Scope Exit

Destructors are called in reverse order of declaration when a scope ends.

{

    var a = Resource { ... }

    var b = Resource { ... }

} // b.destroy() called, then a.destroy()

Control Flow: break, continue, return

Chemical ensures destructors are called even when exiting a block prematurely.

func process() {

    var res = Resource { ... }

    if (error) {

        return // res.destroy() is called before returning

    }

}



loop {

    var res = Resource { ... }

    if (done) {

        break // res.destroy() is called before breaking

    }

}

Moves and Destructors

If an object is moved to another variable or passed to a function by value, the original variable is considered "empty" and its destructor will not be called.

func take(res : Resource) { } // res.destroy() called here at end of take()



func example() {

    var a = Resource { ... }

    take(a) // a is MOVED to 'take', a.destroy() will NOT be called in example()

}

Assignment to Movable Types

When you assign a new value to a variable that already holds a movable object, the previous object's destructor is called before the assignment happens.

var a = Resource { name: "First" }

a = Resource { name: "Second" } // a.destroy() ("First") is called here