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.
Variables declared with
my are assigned the
my a = 123
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 getFirstRune(a) -- RuntimeError. Expected `string`.
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.
any type on the otherhand is a static type and must be explicitly declared using
var a any = 123 func getFirstRune(s string): return s 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.
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.
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
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"
add function defers type checking of
getValue() to runtime because it has the
Recent type inference. #
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.
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
my a = 123
The recent type can change at compile-time from another assignment.
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
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
a has the recent type of
any type after merging the
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:
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
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. #
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
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.
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]
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.
Union types. #
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".
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. #
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