Type System

Type System. #

Cyber supports the use of both dynamically and statically typed code.

Dynamic typing. #

Dynamic typing can reduce the amount of friction when writing code, but it can also result in more runtime errors.

my declaration. #

Variables declared with my are assigned the dynamic type:

my a = 123

dynamic vs any #

dynamic values can be freely used and copied without any compile errors (if there is a chance it can succeed at runtime, see Recent type inference):

my a = 123

func getFirstRune(s string):
    return s[0]

getFirstRune(a)       -- RuntimeError. Expected `string`.

Since a is dynamic, passing it to a typed function parameter is allowed at compile-time, but will fail when the function is invoked at runtime.

The any type on the otherhand is a static type and must be explicitly declared using var:

var a any = 123

func getFirstRune(s string):
    return s[0]

getFirstRune(a)       -- CompileError. Expected `string`.

This same setup will now fail at compile-time because any does not satisfy the destination’s string type constraint.

The use of the dynamic type effectively defers type checking to runtime while any is a static type and must adhere to type constraints at compile-time.

A dynamic value can be used in any operation. It can be invoked as the callee, invoked as the receiver of a method call, or used with operators.

Invoking dynamic values. #

When a dynamic value is invoked, checks on whether the callee is a function is deferred to runtime.

my op = 123
print op(1, 2, 3)      -- RuntimeError. Expected a function.

Dynamic return value. #

When the return type of a function is not specified, it defaults to the dynamic type. This allows copying the return value to a typed destination without casting:

func getValue():
    return 123

func add(a int, b int):
    return a + b

print add(getValue(), 2)    -- Prints "125"

The add function defers type checking of getValue() to runtime because it has the dynamic type.

Recent type inference. #

Although a dynamic variable has the most flexibility, in some situations it is advantageous to know what type it could be.

The compiler keeps a running record of a dynamic variable’s most recent type to gain additional compile-time features without sacrificing flexibility. It can prevent inevitable runtime errors and avoid unnecessary type casts.

When a dynamic variable is first initialized, it has a recent type inferred from its initializer. In the following, a has the recent type of int at compile-time because numeric literals default to the int type:

my a = 123

The recent type can change at compile-time from another assignment. If a is then assigned to a string literal, a from that point on has the recent type of string at compile-time:

my a = 123
foo(a)           -- Valid call expression.
a = 'hello'
foo(a)           -- CompileError. Expected `int` argument, got `string`.

func foo(n int):
    pass

Even though a is dynamic and is usually allowed to defer type checking to runtime, the compiler knows that doing so in this context would always result in a runtime error, so it provides a compile error instead. This provides a quicker feedback to fix the problem.

The recent type of a can also change in branches. However, after the branch block, a will have a recent type after merging the types assigned to a from the two branched code paths. Currently, the any type is used if the types from the two branches differ. At the end of the following if block, a has the recent type of any type after merging the int and string types:

my a = 123
if a > 20:
    a = 'hello'
    foo(a)       -- Valid call expression. `foo` can be called without type casting.

foo(a)           -- CompileError. Expected `string` argument, got `any`.

func foo(s string):
    pass

Static typing. #

Static typing can be incrementally applied which provides compile-time guarantees and prevents runtime errors. Static typing also makes it easier to maintain and refactor your code.

Incomplete: There are some cases where calling static functions with dynamic values doesn’t do a runtime type check.

Builtin types. #

The following builtin types are available in every module: bool, float, int, string, List, Map, error, fiber, any.

var declaration. #

A var declaration automatically infers the type from the initializer:

-- Initialized as an `int` variable.
var a = 123

var declarations are strictly for static typing. If the assigned value’s type is dynamic, the variable’s type becomes any.

func getValue():
    return ['a', 'list']

-- Initialized as an `any` variable.
var a = getValue()

Typed variables. #

A typed local variable can be declared by attaching a type specifier after its name. The value assigned to the variable must satisfy the type constraint or a compile error is issued.

var a float = 123

var b int = 123.0    -- CompileError. Expected `int`, got `float`.

Any operation afterwards that violates the type constraint of the variable will result in a compile error.

a = 'hello'          -- CompileError. Expected `float`, got `string`.

Type specifiers must be resolved at compile-time.

var foo Foo = none   -- CompileError. Type `Foo` is not declared.

Static variables are declared in a similar way:

var Root.global Map = [:]

Object types. #

A type object declaration creates a new object type. Field types are optional and declared with a type specifier after their name.

type Student object:    -- Creates a new type named `Student`
    var name string
    var age  int
    var gpa  float

Instantiating a new object does not require typed fields to be initialized. Missing field values will default to their zero value:

var s = [Student:]
print s.name       -- Prints ""
print s.age        -- Prints "0"
print s.gpa        -- Prints "0.0"

Circular type dependencies are allowed if the object can be initialized:

Planned Feature: Optional types are not currently supported.

type Node object:
    var val  any
    var next Node?     -- Valid type specifier.

In this example, next has an optional Node? type so it can be initialized to none when creating a new Node object.

The following example will fail because this version of Node can not be initialized:

type Node object:
    var val  any
    var next Node

var n = [Node:]    -- CompileError. Can not zero initialize `next`
                   -- because of circular dependency.

Zero values. #

The following shows the zero values of builtin or created types.

TypeZero value
booleanfalse
int0
float0.0
string''
rawstring''
List[]
Map[:]
type S object[S:]
@host type S objectS.$zero()
dynamicnone
anynone
S?none

Type aliases. #

A type alias is declared from a single line type statement. This creates a new type symbol for an existing data type.

import util './util.cy'

type Vec3 util.Vec3

var v = [Vec3 x: 3, y: 4, z: 5]

Functions. #

Function parameter and return type specifiers follows a similiar syntax.

func mul(a float, b float) float:
    return a * b

print mul(3, 4)
print mul(3, '4')  -- CompileError. Function signature mismatch.

Traits. #

Planned Feature

Union types. #

Planned Feature

any type. #

A variable with the any type can hold any value, but copying it to narrowed type destination will result in a compile error:

func square(i int):
    return i * i

var a any = 123
a = ['a', 'list']         -- Valid assignment to a value with a different type.
a = 10

print square(a)           -- CompileError. Expected `int`, got `any`.

a must be explicitly casted to satisfy the type constraint:

print square(a as int)    -- Prints "100".

Invoking any values. #

Since any is a static type, invoking an any value must be explicitly casted to the appropriate function type.

Planned Feature: Casting to a function type is not currently supported.

func add(a int, b int) int:
    return a + b

var op any = add
print op(1, 2)         -- CompileError. Expected `func (int, int) any`

var opFunc = op as (func (int, int) int)
print opFunc(1, 2)     -- Prints "3".

Type casting. #

The as keyword can be used to cast a value to a specific type. Casting lets the compiler know what the expected type is and does not perform any conversions.

If the compiler knows the cast will always fail at runtime, a compile error is returned instead.

print('123' as int)       -- CompileError. Can not cast `string` to `int`.

If the cast fails at runtime, a panic is returned.

var erased any = 123
add(1, erased as int)     -- Success.
print(erased as string)   -- Panic. Can not cast `int` to `string`.

func add(a int, b int):
    return a + b