Typesystem

Zirric is dynamically, but strongly typed. In the future it may add type
inference, but today values can flow through any variable, field, or parameter
as long as explicit conversions and runtime checks are satisfied.

The type system stays intentionally small. Zirric supports the following classes
of types:

  • data types are the most common types. They are used to store data and can be easily created by calling the type name as a function.
  • union types are used to express that their values can be one of a group of types. In other languages they are also called union types.
  • attr types are used to annotate declarations with metadata. They can only be constructed at compile time.
  • extern types are built-in types that are implemented in the runtime like Func, String or Int.

Data types

Data types are the most common types in Zirric and store data. In most other languages they are called classes or structs. They are defined by the data keyword followed by the type name and a list of fields.

data Person {
    name
    age
}

To create a new instance of a data type, simply call the type name as a function with the field values as arguments.

let person = Person("John", 42)

Fields

Fields are the building blocks of data types. They are defined by their name and optionally attributes.
To increase the expressiveness, function fields can be defined by adding a function signature after the field name.

data Greetable {
    greeting(ofValue) // function field
}

Field attributes

Fields can be annotated with metadata. These attributes will be processed at compile time and can be accessed at runtime.

data Person {
    @String
    @json.HasKey("name")
    name

    @Int
    @json.HasKey("age")
    age
}

Union types

Union types are used to express that their values can be one of a group of types. In other languages they are also called union types. They are defined by the union keyword followed by the type name and a list of types.
As convenience, you can even declare types within the union declaration. These will still be available outside of the union.

union JuristicPerson {
    Person
    data Company {
        name
        corporateForm
    }
}

In this example every Person and every Company is a JuristicPerson.

To discriminate union values, use switch with attribute cases:

fn nameOf(juristic) {
    switch juristic {
    case @Person:
        juristic.name
    case @Company:
        juristic.name + " " + juristic.corporateForm
    case _:
        "unknown"
    }
}

Attribute types

Attribute types are used to annotate declarations with metadata. They can only be constructed at compile time.
They are defined by the attr keyword followed by the type name and a list of fields.

mod json

attr HasKey {
    name
}

To annotate a declaration, start with the @ symbol followed by the attribute type name and a list of field values.

import json

data Person {
    @Type(String)
    @json.HasKey("name")
    name

    @Int // shorthand for @Type(Int)
    @json.HasKey("age")
    age
}

Accessing attributes

Attributes can be accessed at runtime by using the reflect module.

import json
import code.knabel.dev.zirric_lang.zirric.future.reflect

let person = Person("John", 42)
let personType = reflect.typeOf(person)
let fields = reflect.fieldsOf(personType)
let nameAnnotation = reflect.attribute(fields[0], json.HasKey)

Protocol-like attributes

The prelude defines attributes such as @Countable and @Iterable to describe
capabilities of types. The compiler and tooling use these attributes to drive
loop behavior and helper utilities.

@Countable({ v -> v.length })
@Iterable(_arrayIterate)
extern type Array {
    @Int length
}

Common prelude patterns

The prelude includes small data structures such as Result and Optional that
encode success/failure and nullable values without relying on implicit nulls.

Extern types

Extern types are built-in types that are implemented in the runtime like Func, String or Int. They are defined by the extern keyword followed by the type name. Optionally you can add a list of fields.

extern Int

extern String {
    length
}

Each extern type behaves slightly different in terms of how it is created and accessed.
Many types like String, Int, Float and Dict will be created by literals, types like Func and Module by declarations. Any on the other hand is more like an union containing all types.

Note: The extern keyword is also used to declare functions provided by the compiler like extern print(str).