Type System

Zirric is dynamically typed but strongly typed. Values carry their type at runtime and can flow through any variable, field, or parameter. There are no implicit conversions — a String is never silently treated as an Int. Type information is communicated through type hints and checked at runtime through is expressions and switch matching.

This page covers the type categories, their construction and identity rules, type hints, runtime type checking, and protocol attributes.

Type Categories

Zirric has four categories of user-declared types and a set of built-in extern types.

Category Declared with Constructible from Zirric Runtime identity
Data types data Yes (constructor call) Nominal
Union types union No (members are) Membership-based
Attribute types attr Only via @Attr() syntax Nominal
Extern types extern type No (runtime-provided) Nominal

For declaration syntax, see Declarations.

Data Types

Data types are the most common types in Zirric. They store structured data as named fields.

data Person {
    name: String
    age: Int
}

Construction. Call the type name as a function with positional arguments matching the field order:

const alice = Person("Alice", 30)

Identity. Each data declaration creates a distinct type. Two data types with identical fields are not the same type. Type identity is nominal — it is determined by the declaration, not the structure.

Field access. Fields are accessed with .:

alice.name  // "Alice"

Equality. Two data instances are equal (==) if they have the same type and all fields are equal.

Union Types

Union types declare that a value may be one of a fixed set of member types. Members can be existing types or inline data declarations.

union Option {
    data Some { value }
    data None
}

Membership. A value belongs to a union if its concrete type is one of the declared members. Membership is checked at runtime. A value does not need to be “wrapped” in the union — if its type is a member, it is a member.

const x = Some(42)
x is Option    // true — Some is a member of Option
x is Some      // true — x is directly a Some

Nested declarations. data types declared inside a union body are hoisted to module scope. They exist as standalone types and as union members simultaneously.

Discrimination. Union values are typically discriminated using switch with is cases:

fn unwrap(opt: Option) {
    switch opt {
    case is Some:
        opt.value
    case is None:
        void
    case _:
        void
    }
}

See Expressions § Switch for the full matching semantics.

Unions of unions. A union can reference another union as a member. A value belongs to the outer union if it belongs to any of its members, transitively.

Extern Types

Extern types are built-in types provided by the runtime. They cannot be constructed from Zirric code — instances are created by literals, declarations, or runtime operations.

Type Created by Fields
Int Integer literals
Float Float literals
Bool true, false toggle()
String String literals length: Int
Char Runtime operations
Array Array literals length: Int
Dict Dict literals length: Int
Func fn declarations, closures arity: Int
Void void constant
Module mod declarations (members)
Any

Any is a special type that all values belong to. It acts as a universal supertype.

Field access on extern types. Some extern types expose fields (like String.length). These are accessed with . just like data fields.

Attribute Types

Attribute types are declared with attr and follow the same field syntax as data. However, they differ in a critical way: attribute instances can only be created through the @Attr(args) application syntax on declarations or fields. They cannot be called as constructor functions at runtime.

attr Doc {
    description: String
}

@Doc("A person record")
data Person { name }

Role in the type system. Attribute types participate in type checking through is @Attr expressions and switch case is @Attr patterns. When you write value is @Doc, the runtime checks whether the value’s type declaration carries the @Doc attribute — not whether the value itself is a Doc instance.

For how to declare and apply attributes, see Declarations § attr and Declarations § Attributes on Declarations.

Protocol attributes

A common pattern in Zirric is to use attributes to describe type capabilities, similar to interfaces or protocols in other languages. The prelude defines attributes like @Countable and @Iterable that the runtime uses to drive behavior.

attr Countable {
    length(value: @Countable) -> Int
}

attr Iterable {
    iterate(value: @Iterable, yield: Func) -> Void
}

Types opt into a protocol by applying the attribute with a function implementation:

@Countable(fn(v) { return v.length })
@Iterable(_arrayIterate)
extern type Array {
    length: Int
}

How for uses @Iterable. When a for loop iterates over a value, the runtime looks up the @Iterable attribute on the value’s type and calls the iterate function. The function receives the value and a yield callback. It calls yield with each element; if yield returns false, iteration stops (implementing break).

How len uses @Countable. The built-in len operation looks up the @Countable attribute on the value’s type and calls the length function.

Matching on attributes

is @Attr checks whether a value’s type carries the attribute, not whether the value is an attribute instance:

const items = [1, 2, 3]
items is @Iterable       // true — Array has @Iterable
items is @Countable      // true — Array has @Countable

This also works in switch:

switch value {
case is @Iterable:
    // value's type has @Iterable
case _:
    // fallback
}

Accessing attributes at runtime

Attribute values attached to a type can be accessed at runtime through the reflect module:

import future.reflect

const personType = reflect.typeOf(person)
const fields = reflect.fieldsOf(personType)
const attr = reflect.attribute(fields[0], json.HasKey)

Type Hints

Type hints annotate declarations and parameters with type information. They appear after : on fields, parameters, and variables, and after -> on function return types.

fn greet(name: String) -> String {
    "Hello, " + name
}

data Pair {
    first: Int
    second: Int
}

const x: Int = 42

What type hints express

Type hints communicate intended types to readers, editors, and the language server. The compiler records them for is checks and switch matching. Zirric does not perform static type checking — values flow dynamically regardless of hints.

Composite type hints

Beyond simple type references, type hints support composite forms:

Form Syntax Meaning
Named type Type A reference to a declared type
Qualified type mod.Type A type from another module
Array type [Element] An array of elements
Dict type [Key: Value] A dictionary with key and value types
Function type fn(P) -> R A function with parameter and return types
Attribute type @Attr A value whose type carries @Attr
Multi-attribute @Attr1 @Attr2 A value whose type carries all listed attributes
fn transform(items: [Int], f: fn(Int) -> String) -> [String] {
    return for item <- items {
        f(item)
    }
}

fn process(value: @Countable @Iterable) {
    // value's type must have both attributes
}

Type hints on declarations

See Declarations § Parameters and Type Hints for all positions where type hints can appear.

Type Checking with is

The is operator and switch case is patterns perform runtime type checks. The matching rules depend on the type hint form:

Pattern Matches when
is Type The value’s concrete type is Type
is UnionType The value’s concrete type is a member of the union
is @Attr The value’s type declaration carries the @Attr attribute

is always produces a Bool. In a switch, the matching case’s body is executed.

42 is Int           // true
42 is Number        // true (Number is union { Int, Float })
42 is String        // false
42 is @Numeric      // true (Int has @Numeric)

Common Prelude Types

The prelude defines a small set of types and unions available without explicit import.

Number — a union of Int and Float:

union Number {
    Int
    Float
}

Void — represents absence of a meaningful value. The constant void is the single instance. Functions without an explicit return value return void.

Any — the universal type. Every value is an Any. Useful as a type hint when no constraint is needed.

The prelude also defines common attributes (@Doc, @Deprecated, @Default, @Numeric) and protocol attributes (@Countable, @Iterable). See Standard Library § Prelude for the full list.